Merge "Fix Flaky testTrafficStatsForLocalhost" into android14-tests-dev am: 67df08a557

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

Change-Id: Ia60b6d89753a661758564bbd3743ebb04690d14f
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/.gitignore b/.gitignore
index ccff052..b517674 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,10 @@
 **/.idea
 **/*.iml
 **/*.ipr
+
+# VS Code project
+**/.vscode
+**/*.code-workspace
+
+# Vim temporary files
+**/*.swp
diff --git a/Cronet/OWNERS b/Cronet/OWNERS
index 62c5737..c24680e 100644
--- a/Cronet/OWNERS
+++ b/Cronet/OWNERS
@@ -1,2 +1,2 @@
 set noparent
-file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking
+file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
diff --git a/Cronet/tests/OWNERS b/Cronet/tests/OWNERS
deleted file mode 100644
index acb6ee6..0000000
--- a/Cronet/tests/OWNERS
+++ /dev/null
@@ -1,8 +0,0 @@
-# Bug component: 31808
-
-set noparent
-file:platform/packages/modules/Connectivity:master:/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 e17081a..0000000
--- a/Cronet/tests/common/Android.bp
+++ /dev/null
@@ -1,42 +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 {
-    // 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"
-    ],
-}
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 2ac418f..0000000
--- a/Cronet/tests/common/AndroidTest.xml
+++ /dev/null
@@ -1,36 +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" />
-    <!-- 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" />
-        <option name="hidden-api-checks" 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 7b52694..0000000
--- a/Cronet/tests/cts/Android.bp
+++ /dev/null
@@ -1,68 +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_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/assets/html/hello_world.html b/Cronet/tests/cts/assets/html/hello_world.html
deleted file mode 100644
index ea62ce2..0000000
--- a/Cronet/tests/cts/assets/html/hello_world.html
+++ /dev/null
@@ -1,24 +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.
-  -->
-
-<html>
-<head>
-    <title>hello world</title>
-</head>
-<body>
-<h3>hello world</h3><br>
-</body>
-</html>
\ No newline at end of file
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 0760e68..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt
+++ /dev/null
@@ -1,84 +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 kotlin.test.Test
-import kotlin.test.assertEquals
-import org.hamcrest.MatcherAssert
-import org.hamcrest.Matchers
-import org.junit.After
-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() {
-        // cancel active requests to enable engine shutdown.
-        stream?.let {
-            it.cancel()
-            callback.blockForDone()
-        }
-        httpEngine.shutdown()
-    }
-
-    private fun createBidirectionalStreamBuilder(url: String): BidirectionalStream.Builder {
-        return httpEngine.newBidirectionalStreamBuilder(url, callback.executor, callback)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun testBidirectionalStream_GetStream_CompletesSuccessfully() {
-        stream = createBidirectionalStreamBuilder(URL).setHttpMethod("GET").build()
-        stream!!.start()
-        callback.assumeCallback(ResponseStep.ON_SUCCEEDED)
-        val info = callback.mResponseInfo
-        assumeOKStatusCode(info)
-        MatcherAssert.assertThat(
-            "Received byte count must be > 0", info.receivedByteCount, Matchers.greaterThan(0L))
-        assertEquals("h2", info.negotiatedProtocol)
-    }
-}
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 9fc4389..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
+++ /dev/null
@@ -1,406 +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();
-        HttpCtsTestServer server =
-                new HttpCtsTestServer(ApplicationProvider.getApplicationContext());
-
-        String url = server.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";
-        HttpCtsTestServer server =
-                new HttpCtsTestServer(ApplicationProvider.getApplicationContext());
-        mEngine =
-                new HttpEngine.Builder(ApplicationProvider.getApplicationContext())
-                        .setUserAgent(userAgent)
-                        .build();
-
-        String url = server.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 07e7d45..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
+++ /dev/null
@@ -1,416 +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);
-    }
-
-    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 4e4251c..0000000
--- a/Cronet/tests/mts/Android.bp
+++ /dev/null
@@ -1,61 +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 {
-    // 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_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_tests__testing"
-     ],
-     test_suites: [
-         "general-tests",
-         "mts-tethering",
-     ],
-}
-
diff --git a/Cronet/tests/mts/AndroidManifest.xml b/Cronet/tests/mts/AndroidManifest.xml
deleted file mode 100644
index f597134..0000000
--- a/Cronet/tests/mts/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"
-          package="android.net.http.mts">
-
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-    <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 0d780a1..0000000
--- a/Cronet/tests/mts/AndroidTest.xml
+++ /dev/null
@@ -1,39 +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>
-
-    <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" />
-        <option name="hidden-api-checks" 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 a3e86de..0000000
--- a/Cronet/tests/mts/jarjar_excludes.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-# It's prohibited to jarjar androidx packages
-androidx\..+
-# 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(\$.+)?
-
-# 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(\$.+)?
\ 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 d44c36f..0000000
--- a/Cronet/tests/mts/res/xml/network_security_config.xml
+++ /dev/null
@@ -1,34 +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">
-        <!-- 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 5372a4d..0000000
--- a/Cronet/tools/import/copy.bara.sky
+++ /dev/null
@@ -1,119 +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/**",
-        "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/**",
-        # Note: Only used for tests.
-        "third_party/googletest/**",
-        "third_party/icu/**",
-        "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/Android.bp b/DnsResolver/Android.bp
new file mode 100644
index 0000000..716eb10
--- /dev/null
+++ b/DnsResolver/Android.bp
@@ -0,0 +1,87 @@
+//
+// 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",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+    name: "libcom.android.tethering.dns_helper",
+    version_script: "libcom.android.tethering.dns_helper.map.txt",
+    stubs: {
+        versions: [
+            "1",
+        ],
+        symbol_file: "libcom.android.tethering.dns_helper.map.txt",
+    },
+    defaults: ["netd_defaults"],
+    header_libs: [
+        "bpf_connectivity_headers",
+        "libcutils_headers",
+    ],
+    srcs: [
+        "DnsBpfHelper.cpp",
+        "DnsHelper.cpp",
+    ],
+    static_libs: [
+        "libmodules-utils-build",
+    ],
+    shared_libs: [
+        "libbase",
+    ],
+    export_include_dirs: ["include"],
+    header_abi_checker: {
+        enabled: true,
+        symbol_file: "libcom.android.tethering.dns_helper.map.txt",
+    },
+    sanitize: {
+        cfi: true,
+    },
+    apex_available: ["com.android.tethering"],
+    min_sdk_version: "30",
+}
+
+cc_test {
+    name: "dns_helper_unit_test",
+    defaults: ["netd_defaults"],
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
+    test_config_template: ":net_native_test_config_template",
+    header_libs: [
+        "bpf_connectivity_headers",
+    ],
+    srcs: [
+        "DnsBpfHelperTest.cpp",
+    ],
+    static_libs: [
+        "libcom.android.tethering.dns_helper",
+    ],
+    shared_libs: [
+        "libbase",
+        "libcutils",
+    ],
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
+}
diff --git a/DnsResolver/DnsBpfHelper.cpp b/DnsResolver/DnsBpfHelper.cpp
new file mode 100644
index 0000000..0719ade
--- /dev/null
+++ b/DnsResolver/DnsBpfHelper.cpp
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "DnsBpfHelper"
+
+#include "DnsBpfHelper.h"
+
+#include <android-base/logging.h>
+#include <android-modules-utils/sdk_level.h>
+
+namespace android {
+namespace net {
+
+#define RETURN_IF_RESULT_NOT_OK(result)                                                            \
+  do {                                                                                             \
+    if (!result.ok()) {                                                                            \
+      LOG(ERROR) << "L" << __LINE__ << " " << __func__ << ": " << strerror(result.error().code()); \
+      return result.error();                                                                       \
+    }                                                                                              \
+  } while (0)
+
+base::Result<void> DnsBpfHelper::init() {
+  if (!android::modules::sdklevel::IsAtLeastT()) {
+    LOG(ERROR) << __func__ << ": Unsupported before Android T.";
+    return base::Error(EOPNOTSUPP);
+  }
+
+  RETURN_IF_RESULT_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
+  RETURN_IF_RESULT_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
+  RETURN_IF_RESULT_NOT_OK(mDataSaverEnabledMap.init(DATA_SAVER_ENABLED_MAP_PATH));
+  return {};
+}
+
+base::Result<bool> DnsBpfHelper::isUidNetworkingBlocked(uid_t uid, bool metered) {
+  if (is_system_uid(uid)) return false;
+  if (!mConfigurationMap.isValid() || !mUidOwnerMap.isValid()) {
+    LOG(ERROR) << __func__
+               << ": BPF maps are not ready. Forgot to call ADnsHelper_init?";
+    return base::Error(EUNATCH);
+  }
+
+  auto enabledRules = mConfigurationMap.readValue(UID_RULES_CONFIGURATION_KEY);
+  RETURN_IF_RESULT_NOT_OK(enabledRules);
+
+  auto value = mUidOwnerMap.readValue(uid);
+  uint32_t uidRules = value.ok() ? value.value().rule : 0;
+
+  // For doze mode, battery saver, low power standby.
+  if (isBlockedByUidRules(enabledRules.value(), uidRules)) return true;
+
+  // For data saver.
+  // DataSaverEnabled map on V+ platforms is the only reliable source of information about the
+  // current data saver status. While ConnectivityService offers two ways to update this map for U
+  // and V+, the U- platform implementation can have delays, potentially leading to inaccurate
+  // results. Conversely, the V+ platform implementation is synchronized with the actual data saver
+  // 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_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);
+    RETURN_IF_RESULT_NOT_OK(dataSaverSetting);
+    return dataSaverSetting.value();
+  }
+
+  return false;
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/DnsResolver/DnsBpfHelper.h b/DnsResolver/DnsBpfHelper.h
new file mode 100644
index 0000000..f1c3992
--- /dev/null
+++ b/DnsResolver/DnsBpfHelper.h
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <android-base/result.h>
+
+#include "bpf/BpfMap.h"
+#include "netd.h"
+
+namespace android {
+namespace net {
+
+class DnsBpfHelper {
+ public:
+  DnsBpfHelper() = default;
+  DnsBpfHelper(const DnsBpfHelper&) = delete;
+  DnsBpfHelper& operator=(const DnsBpfHelper&) = delete;
+
+  base::Result<void> init();
+  base::Result<bool> isUidNetworkingBlocked(uid_t uid, bool metered);
+
+ private:
+  android::bpf::BpfMapRO<uint32_t, uint32_t> mConfigurationMap;
+  android::bpf::BpfMapRO<uint32_t, UidOwnerValue> mUidOwnerMap;
+  android::bpf::BpfMapRO<uint32_t, bool> mDataSaverEnabledMap;
+
+  // For testing
+  friend class DnsBpfHelperTest;
+};
+
+}  // namespace net
+}  // namespace android
diff --git a/DnsResolver/DnsBpfHelperTest.cpp b/DnsResolver/DnsBpfHelperTest.cpp
new file mode 100644
index 0000000..18a5df4
--- /dev/null
+++ b/DnsResolver/DnsBpfHelperTest.cpp
@@ -0,0 +1,210 @@
+/*
+ * 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.
+ */
+
+#include <gtest/gtest.h>
+#include <private/android_filesystem_config.h>
+
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+#include "DnsBpfHelper.h"
+
+using namespace android::bpf;  // NOLINT(google-build-using-namespace): exempted
+
+namespace android {
+namespace net {
+
+constexpr int TEST_MAP_SIZE = 2;
+
+#define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
+
+class DnsBpfHelperTest : public ::testing::Test {
+ protected:
+  DnsBpfHelper mDnsBpfHelper;
+  BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
+  BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
+  BpfMap<uint32_t, bool> mFakeDataSaverEnabledMap;
+
+  void SetUp() {
+    mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
+    ASSERT_VALID(mFakeConfigurationMap);
+
+    mFakeUidOwnerMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
+    ASSERT_VALID(mFakeUidOwnerMap);
+
+    mFakeDataSaverEnabledMap.resetMap(BPF_MAP_TYPE_ARRAY, DATA_SAVER_ENABLED_MAP_SIZE);
+    ASSERT_VALID(mFakeDataSaverEnabledMap);
+
+    mDnsBpfHelper.mConfigurationMap = mFakeConfigurationMap;
+    ASSERT_VALID(mDnsBpfHelper.mConfigurationMap);
+    mDnsBpfHelper.mUidOwnerMap = mFakeUidOwnerMap;
+    ASSERT_VALID(mDnsBpfHelper.mUidOwnerMap);
+    mDnsBpfHelper.mDataSaverEnabledMap = mFakeDataSaverEnabledMap;
+    ASSERT_VALID(mDnsBpfHelper.mDataSaverEnabledMap);
+  }
+
+  void ResetAllMaps() {
+    mDnsBpfHelper.mConfigurationMap.reset();
+    mDnsBpfHelper.mUidOwnerMap.reset();
+    mDnsBpfHelper.mDataSaverEnabledMap.reset();
+  }
+};
+
+TEST_F(DnsBpfHelperTest, IsUidNetworkingBlocked) {
+  struct TestConfig {
+    const uid_t uid;
+    const uint32_t enabledRules;
+    const uint32_t uidRules;
+    const int expectedResult;
+    std::string toString() const {
+      return fmt::format(
+          "uid: {}, enabledRules: {}, uidRules: {}, expectedResult: {}",
+          uid, enabledRules, uidRules, expectedResult);
+    }
+  } testConfigs[] = {
+    // clang-format off
+    //   No rule enabled:
+    // uid,         enabledRules,                  uidRules,                      expectedResult
+    {AID_APP_START, NO_MATCH,                      NO_MATCH,                      false},
+
+    //   An allowlist rule:
+    {AID_APP_START, NO_MATCH,                      DOZABLE_MATCH,                 false},
+    {AID_APP_START, DOZABLE_MATCH,                 NO_MATCH,                      true},
+    {AID_APP_START, DOZABLE_MATCH,                 DOZABLE_MATCH,                 false},
+    //   A denylist rule
+    {AID_APP_START, NO_MATCH,                      STANDBY_MATCH,                 false},
+    {AID_APP_START, STANDBY_MATCH,                 NO_MATCH,                      false},
+    {AID_APP_START, STANDBY_MATCH,                 STANDBY_MATCH,                 true},
+
+    //   Multiple rules enabled:
+    //     Match only part of the enabled allowlist rules.
+    {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH,                 true},
+    {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, POWERSAVE_MATCH,               true},
+    //     Match all of the enabled allowlist rules.
+    {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH|POWERSAVE_MATCH, false},
+    //     Match allowlist.
+    {AID_APP_START, DOZABLE_MATCH|STANDBY_MATCH,   DOZABLE_MATCH,                 false},
+    //     Match no rule.
+    {AID_APP_START, DOZABLE_MATCH|STANDBY_MATCH,   NO_MATCH,                      true},
+    {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, NO_MATCH,                      true},
+
+    // System UID: always unblocked.
+    {AID_SYSTEM,    NO_MATCH,                      NO_MATCH,                      false},
+    {AID_SYSTEM,    NO_MATCH,                      DOZABLE_MATCH,                 false},
+    {AID_SYSTEM,    DOZABLE_MATCH,                 NO_MATCH,                      false},
+    {AID_SYSTEM,    DOZABLE_MATCH,                 DOZABLE_MATCH,                 false},
+    {AID_SYSTEM,    NO_MATCH,                      STANDBY_MATCH,                 false},
+    {AID_SYSTEM,    STANDBY_MATCH,                 NO_MATCH,                      false},
+    {AID_SYSTEM,    STANDBY_MATCH,                 STANDBY_MATCH,                 false},
+    {AID_SYSTEM,    DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH,                 false},
+    {AID_SYSTEM,    DOZABLE_MATCH|POWERSAVE_MATCH, POWERSAVE_MATCH,               false},
+    {AID_SYSTEM,    DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH|POWERSAVE_MATCH, false},
+    {AID_SYSTEM,    DOZABLE_MATCH|STANDBY_MATCH,   DOZABLE_MATCH,                 false},
+    {AID_SYSTEM,    DOZABLE_MATCH|STANDBY_MATCH,   NO_MATCH,                      false},
+    {AID_SYSTEM,    DOZABLE_MATCH|POWERSAVE_MATCH, NO_MATCH,                      false},
+    // clang-format on
+  };
+
+  for (const auto& config : testConfigs) {
+    SCOPED_TRACE(config.toString());
+
+    // Setup maps.
+    EXPECT_RESULT_OK(mFakeConfigurationMap.writeValue(UID_RULES_CONFIGURATION_KEY,
+                                                      config.enabledRules, BPF_EXIST));
+    EXPECT_RESULT_OK(mFakeUidOwnerMap.writeValue(config.uid, {.iif = 0, .rule = config.uidRules},
+                                                 BPF_ANY));
+
+    // Verify the function.
+    auto result = mDnsBpfHelper.isUidNetworkingBlocked(config.uid, /*metered=*/false);
+    EXPECT_TRUE(result.ok());
+    EXPECT_EQ(config.expectedResult, result.value());
+  }
+}
+
+TEST_F(DnsBpfHelperTest, IsUidNetworkingBlocked_uninitialized) {
+  ResetAllMaps();
+
+  auto result = mDnsBpfHelper.isUidNetworkingBlocked(AID_APP_START, /*metered=*/false);
+  EXPECT_FALSE(result.ok());
+  EXPECT_EQ(EUNATCH, result.error().code());
+
+  result = mDnsBpfHelper.isUidNetworkingBlocked(AID_SYSTEM, /*metered=*/false);
+  EXPECT_TRUE(result.ok());
+  EXPECT_FALSE(result.value());
+}
+
+// Verify DataSaver on metered network.
+TEST_F(DnsBpfHelperTest, IsUidNetworkingBlocked_metered) {
+  struct TestConfig {
+    const uint32_t enabledRules;     // Settings in configuration map.
+    const bool dataSaverEnabled;     // Settings in data saver enabled map.
+    const uint32_t uidRules;         // Settings in uid owner map.
+    const int blocked;               // Whether the UID is expected to be networking blocked or not.
+    std::string toString() const {
+      return fmt::format(
+          ", enabledRules: {}, dataSaverEnabled: {},  uidRules: {}, expect blocked: {}",
+          enabledRules, dataSaverEnabled, uidRules, blocked);
+    }
+  } testConfigs[]{
+    // clang-format off
+    // 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
+  };
+
+  for (const auto& config : testConfigs) {
+    SCOPED_TRACE(config.toString());
+
+    // Setup maps.
+    EXPECT_RESULT_OK(mFakeConfigurationMap.writeValue(UID_RULES_CONFIGURATION_KEY,
+                                                      config.enabledRules, BPF_EXIST));
+    EXPECT_RESULT_OK(mFakeDataSaverEnabledMap.writeValue(DATA_SAVER_ENABLED_KEY,
+                                                      config.dataSaverEnabled, BPF_EXIST));
+    EXPECT_RESULT_OK(mFakeUidOwnerMap.writeValue(AID_APP_START, {.iif = 0, .rule = config.uidRules},
+                                                 BPF_ANY));
+
+    // Verify the function.
+    auto result = mDnsBpfHelper.isUidNetworkingBlocked(AID_APP_START, /*metered=*/true);
+    EXPECT_RESULT_OK(result);
+    EXPECT_EQ(config.blocked, result.value());
+  }
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/DnsResolver/DnsHelper.cpp b/DnsResolver/DnsHelper.cpp
new file mode 100644
index 0000000..3372908
--- /dev/null
+++ b/DnsResolver/DnsHelper.cpp
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+#include <errno.h>
+
+#include "DnsBpfHelper.h"
+#include "DnsHelperPublic.h"
+
+static android::net::DnsBpfHelper sDnsBpfHelper;
+
+int ADnsHelper_init() {
+  auto result = sDnsBpfHelper.init();
+  if (!result.ok()) return -result.error().code();
+
+  return 0;
+}
+
+int ADnsHelper_isUidNetworkingBlocked(uid_t uid, bool metered) {
+  auto result = sDnsBpfHelper.isUidNetworkingBlocked(uid, metered);
+  if (!result.ok()) return -result.error().code();
+
+  // bool -> int conversion.
+  return result.value();
+}
diff --git a/DnsResolver/include/DnsHelperPublic.h b/DnsResolver/include/DnsHelperPublic.h
new file mode 100644
index 0000000..44b0012
--- /dev/null
+++ b/DnsResolver/include/DnsHelperPublic.h
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <sys/cdefs.h>
+#include <sys/types.h>
+
+__BEGIN_DECLS
+
+/*
+ * Perform any required initialization - including opening any required BPF maps. This function
+ * needs to be called before using other functions of this library.
+ *
+ * Returns 0 on success, -EOPNOTSUPP when the function is called on the Android version before
+ * T. Returns a negative POSIX error code (see errno.h) on other failures.
+ */
+int ADnsHelper_init();
+
+/*
+ * The function reads bpf maps and returns whether the given uid has blocked networking or not. The
+ * function is supported starting from Android T.
+ *
+ * |uid| is a Linux/Android UID to be queried. It is a combination of UserID and AppID.
+ * |metered| indicates whether the uid is currently using a billing network.
+ *
+ * Returns 0(false)/1(true) on success, -EUNATCH when the ADnsHelper_init is not called before
+ * calling this function. Returns a negative POSIX error code (see errno.h) on other failures
+ * that return from bpf syscall.
+ */
+int ADnsHelper_isUidNetworkingBlocked(uid_t uid, bool metered);
+
+__END_DECLS
diff --git a/DnsResolver/libcom.android.tethering.dns_helper.map.txt b/DnsResolver/libcom.android.tethering.dns_helper.map.txt
new file mode 100644
index 0000000..3c965a2
--- /dev/null
+++ b/DnsResolver/libcom.android.tethering.dns_helper.map.txt
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+# This lists the entry points visible to applications that use the
+# libcom.android.tethering.dns_helper library. Other entry points present in
+# the library won't be usable.
+
+LIBCOM_ANDROID_TETHERING_DNS_HELPER {
+  global:
+    ADnsHelper_init; # apex
+    ADnsHelper_isUidNetworkingBlocked; # apex
+  local:
+    *;
+};
diff --git a/OWNERS b/OWNERS
index 07a775e..b2176cc 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,4 +1,5 @@
+# Bug component: 31808
 set noparent
-file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking
+file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
 
-per-file **IpSec* = file:platform/frameworks/base:master:/services/core/java/com/android/server/vcn/OWNERS
\ No newline at end of file
+per-file **IpSec* = file:platform/frameworks/base:main:/services/core/java/com/android/server/vcn/OWNERS
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 6d17476..6d8ed4a 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,21 +1,12 @@
-chiachangwang@google.com
-cken@google.com
-huangaaron@google.com
 jchalard@google.com
 junyulai@google.com
-lifr@google.com
 lorenzo@google.com
-lucaslin@google.com
-markchien@google.com
 martinwu@google.com
 maze@google.com
 motomuman@google.com
-nuccachen@google.com
 paulhu@google.com
 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 1844334..9e4e4a1 100644
--- a/OWNERS_core_networking_xts
+++ b/OWNERS_core_networking_xts
@@ -1,7 +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, 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 d2f6d6a..bcf5e8b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,16 +1,5 @@
 {
-  "presubmit": [
-    {
-      "name": "ConnectivityCoverageTests"
-    },
-    {
-      // In addition to ConnectivityCoverageTests, runs non-connectivity-module tests
-      "name": "FrameworksNetTests"
-    },
-    // 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.
+  "captiveportal-networkstack-resolve-tethering-mainline-presubmit": [
     {
       "name": "CtsNetTestCases",
       "options": [
@@ -18,13 +7,159 @@
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
         },
         {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
     },
-    // Also run CtsNetTestCasesLatestSdk to ensure tests using older shims pass.
     {
-      "name": "CtsNetTestCasesLatestSdk",
+      "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",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        }
+      ]
+    },
+    {
+      // In addition to ConnectivityCoverageTests, runs non-connectivity-module tests
+      "name": "FrameworksNetTests",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        }
+      ]
+    },
+    // Run in addition to mainline-presubmit as mainline-presubmit is not
+    // supported in every branch.
+    {
+      "name": "CtsNetTestCases",
       "options": [
         {
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -87,23 +222,10 @@
       "name": "TetheringIntegrationTests"
     },
     {
-      "name": "traffic_controller_unit_test"
-    },
-    {
       "name": "libnetworkstats_test"
     },
     {
       "name": "FrameworksNetIntegrationTests"
-    },
-    // Runs both NetHttpTests and CtsNetHttpTestCases
-    {
-      "name": "NetHttpCoverageTests",
-      "options": [
-        {
-          // These sometimes take longer than 1 min which is the presubmit timeout
-          "exclude-annotation": "androidx.test.filters.LargeTest"
-        }
-      ]
     }
   ],
   "postsubmit": [
@@ -115,21 +237,39 @@
       "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
     },
     {
-      "name": "traffic_controller_unit_test",
-      "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
+      "name": "dns_helper_unit_test"
     },
     {
       "name": "FrameworksNetDeflakeTest"
+    },
+    // Postsubmit on virtual devices to monitor flakiness of @SkipPresubmit methods
+    {
+      "name": "CtsNetTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
+    {
+      "name": "FrameworksNetTests"
+    },
+    // TODO: Move to presumit after meet SLO requirement.
+    {
+      "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"
         },
         {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
@@ -141,6 +281,9 @@
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
         },
         {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
@@ -152,6 +295,9 @@
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
         },
         {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
@@ -163,6 +309,9 @@
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
         },
         {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
@@ -170,16 +319,22 @@
     // 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"
         },
         {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         },
         {
           "exclude-annotation": "com.android.testutils.ConnectivityModuleTest"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.DnsResolverModuleTest"
         }
       ]
     },
@@ -188,13 +343,22 @@
     // 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"
         },
         {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.DnsResolverModuleTest"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
         }
       ]
     },
@@ -208,20 +372,21 @@
       "name": "netd_updatable_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     },
     {
-      "name": "ConnectivityCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
-    },
-    {
-      "name": "traffic_controller_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+      "name": "ConnectivityCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        }
+      ]
     },
     {
       "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]",
+      "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
       "options": [
         {
-          // These sometimes take longer than 1 min which is the presubmit timeout
-          "exclude-annotation": "androidx.test.filters.LargeTest"
+          "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
         }
       ]
     }
@@ -229,8 +394,26 @@
   "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"]
+    },
+    {
+      "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "keywords": ["sim"],
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
+        }
+      ]
+    },
+    // Postsubmit on virtual devices to monitor flakiness of @SkipMainlinePresubmit methods
+    {
+      "name": "CtsNetTestCases[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
     }
   ],
   "imports": [
@@ -247,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 b88ec7f..b4426a6 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -68,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-tethering",
         "netd-client",
         "tetheringstatsprotos",
     ],
@@ -81,7 +78,9 @@
         "framework-tethering.impl",
     ],
     manifest: "AndroidManifestBase.xml",
-    lint: { strict_updatability_linting: true },
+    lint: {
+        error_checks: ["NewApi"],
+    },
 }
 
 // build tethering static library, used to compile both variants of the tethering.
@@ -91,13 +90,15 @@
         "ConnectivityNextEnableDefaults",
         "TetheringAndroidLibraryDefaults",
         "TetheringApiLevel",
-        "TetheringReleaseTargetSdk"
+        "TetheringReleaseTargetSdk",
     ],
     static_libs: [
         "NetworkStackApiCurrentShims",
     ],
     apex_available: ["com.android.tethering"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 android_library {
@@ -105,19 +106,21 @@
     defaults: [
         "TetheringAndroidLibraryDefaults",
         "TetheringApiLevel",
-        "TetheringReleaseTargetSdk"
+        "TetheringReleaseTargetSdk",
     ],
     static_libs: [
         "NetworkStackApiStableShims",
     ],
     apex_available: ["com.android.tethering"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // 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",
     ],
@@ -184,24 +187,21 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
-    lint: { strict_updatability_linting: true },
 }
 
 // Updatable tethering packaged for finalized API
 android_app {
     name: "Tethering",
-    defaults: ["TetheringAppDefaults", "TetheringApiLevel"],
+    defaults: [
+        "TetheringAppDefaults",
+        "TetheringApiLevel",
+    ],
     static_libs: ["TetheringApiStableLib"],
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
-    // The network stack *must* be included to ensure security of the device
-    required: [
-        "NetworkStack",
-        "privapp_allowlist_com.android.tethering",
-    ],
+    privapp_allowlist: ":privapp_allowlist_com.android.tethering",
     apex_available: ["com.android.tethering"],
-    lint: { strict_updatability_linting: true },
 }
 
 android_app {
@@ -215,13 +215,11 @@
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
-    // The network stack *must* be included to ensure security of the device
-    required: [
-        "NetworkStackNext",
-        "privapp_allowlist_com.android.tethering",
-    ],
+    privapp_allowlist: ":privapp_allowlist_com.android.tethering",
     apex_available: ["com.android.tethering"],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        error_checks: ["NewApi"],
+    },
 }
 
 sdk {
@@ -232,13 +230,16 @@
         "com.android.tethering",
     ],
     native_shared_libs: [
+        "libcom.android.tethering.dns_helper",
         "libnetd_updatable",
     ],
 }
 
 java_library_static {
     name: "tetheringstatsprotos",
-    proto: {type: "lite"},
+    proto: {
+        type: "lite",
+    },
     srcs: [
         "src/com/android/networkstack/tethering/metrics/stats.proto",
     ],
@@ -251,6 +252,6 @@
     name: "statslog-tethering-java-gen",
     tools: ["stats-log-api-gen"],
     cmd: "$(location stats-log-api-gen) --java $(out) --module network_tethering" +
-         " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
+        " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
     out: ["com/android/networkstack/tethering/metrics/TetheringStatsLog.java"],
 }
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 253fb00..8ed5ac0 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -15,22 +15,17 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-prebuilt_etc {
-    name: "TetheringOutOfProcessFlag",
-    src: "out-of-process",
-    filename_from_src: true,
-    sub_dir: "flag",
-}
-
 // Defaults to enable/disable java targets which uses development APIs. "enabled" may have a
 // different value depending on the branch.
 java_defaults {
     name: "ConnectivityNextEnableDefaults",
     enabled: true,
 }
+
 java_defaults {
     name: "NetworkStackApiShimSettingsForCurrentBranch",
     // API shims to include in the networking modules built from the branch. Branches that disable
@@ -38,6 +33,7 @@
     // (X_current API level).
     static_libs: ["NetworkStackApiCurrentShims"],
 }
+
 apex_defaults {
     name: "ConnectivityApexDefaults",
     // Tethering app to include in the AOSP apex. Branches that disable the "next" targets may use
@@ -45,6 +41,7 @@
     // package names and keys, so that apex will be unused anyway.
     apps: ["TetheringNext"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false.
 }
+
 enable_tethering_next_apex = true
 // This is a placeholder comment to avoid merge conflicts
 // as the above target may have different "enabled" values
@@ -57,16 +54,6 @@
         "//external/cronet/third_party/boringssl:libcrypto",
         "//external/cronet/third_party/boringssl:libssl",
     ],
-    arch: {
-        riscv64: {
-            // TODO: remove this when there is a riscv64 libcronet
-            exclude_jni_libs: [
-                "cronet_aml_components_cronet_android_cronet",
-                "//external/cronet/third_party/boringssl:libcrypto",
-                "//external/cronet/third_party/boringssl:libssl",
-            ],
-        },
-    },
 }
 
 apex {
@@ -87,9 +74,11 @@
         first: {
             jni_libs: [
                 "libservice-connectivity",
+                "libservice-thread-jni",
                 "libandroid_net_connectivity_com_android_net_module_util_jni",
             ],
             native_shared_libs: [
+                "libcom.android.tethering.dns_helper",
                 "libcom.android.tethering.connectivity_native",
                 "libnetd_updatable",
             ],
@@ -97,12 +86,15 @@
         both: {
             jni_libs: [
                 "libframework-connectivity-jni",
-                "libframework-connectivity-tiramisu-jni"
+                "libframework-connectivity-tiramisu-jni",
             ],
         },
     },
     binaries: [
         "clatd",
+        "ethtool",
+        "netbpfload",
+        "ot-daemon",
     ],
     canned_fs_config: "canned_fs_config",
     bpfs: [
@@ -111,18 +103,18 @@
         "dscpPolicy.o",
         "netd.o",
         "offload.o",
-        "offload@btf.o",
+        "offload@mainline.o",
         "test.o",
-        "test@btf.o",
+        "test@mainline.o",
     ],
     apps: [
         "ServiceConnectivityResources",
-        "HalfSheetUX",
     ],
     prebuilts: [
         "current_sdkinfo",
-        "privapp_allowlist_com.android.tethering",
-        "TetheringOutOfProcessFlag",
+        "netbpfload.33rc",
+        "netbpfload.35rc",
+        "ot-daemon.init.34rc",
     ],
     manifest: "manifest.json",
     key: "com.android.tethering.key",
@@ -214,11 +206,14 @@
         // result in a build failure due to inconsistent flags.
         package_prefixes: [
             "android.nearby.aidl",
+            "android.remoteauth.aidl",
+            "android.remoteauth",
             "android.net.apf",
             "android.net.connectivity",
             "android.net.http.apihelpers",
             "android.net.netstats.provider",
             "android.net.nsd",
+            "android.net.thread",
             "android.net.wear",
         ],
     },
diff --git a/Tethering/apex/canned_fs_config b/Tethering/apex/canned_fs_config
index 5a03347..1f5fcfa 100644
--- a/Tethering/apex/canned_fs_config
+++ b/Tethering/apex/canned_fs_config
@@ -1,2 +1,3 @@
 /bin/for-system 0 1000 0750
 /bin/for-system/clatd 1029 1029 06755
+/bin/netbpfload 0 0 0750
diff --git a/Tethering/apex/out-of-process b/Tethering/apex/out-of-process
deleted file mode 100644
index e69de29..0000000
--- a/Tethering/apex/out-of-process
+++ /dev/null
diff --git a/Tethering/apex/permissions/Android.bp b/Tethering/apex/permissions/Android.bp
index ac9ec65..20772a8 100644
--- a/Tethering/apex/permissions/Android.bp
+++ b/Tethering/apex/permissions/Android.bp
@@ -15,14 +15,12 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
     default_visibility: ["//packages/modules/Connectivity/Tethering:__subpackages__"],
 }
 
-prebuilt_etc {
+filegroup {
     name: "privapp_allowlist_com.android.tethering",
-    sub_dir: "permissions",
-    filename: "permissions.xml",
-    src: "permissions.xml",
-    installable: false,
-}
\ No newline at end of file
+    srcs: ["permissions.xml"],
+}
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 898b124..0df9047 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -17,7 +17,6 @@
 package com.android.networkstack.tethering.apishim.api30;
 
 import android.net.INetd;
-import android.net.MacAddress;
 import android.net.TetherStatsParcel;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
@@ -32,7 +31,8 @@
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
 
 /**
  * Bpf coordinator class for API shims.
@@ -57,7 +57,17 @@
     };
 
     @Override
-    public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) {
+    public boolean addIpv6UpstreamRule(@NonNull final Ipv6UpstreamRule rule) {
+        return true;
+    };
+
+    @Override
+    public boolean removeIpv6UpstreamRule(@NonNull final Ipv6UpstreamRule rule) {
+        return true;
+    }
+
+    @Override
+    public boolean addIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
         try {
             mNetd.tetherOffloadRuleAdd(rule.toTetherOffloadRuleParcel());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -69,7 +79,7 @@
     };
 
     @Override
-    public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) {
+    public boolean removeIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
         try {
             mNetd.tetherOffloadRuleRemove(rule.toTetherOffloadRuleParcel());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -80,19 +90,6 @@
     }
 
     @Override
-    public boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
-            @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac,
-            @NonNull MacAddress outDstMac, int mtu) {
-        return true;
-    }
-
-    @Override
-    public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex,
-            int upstreamIfindex, @NonNull MacAddress inDstMac) {
-        return true;
-    }
-
-    @Override
     @Nullable
     public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
         final TetherStatsParcel[] tetherStatsList;
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 3cad1c6..4d1e7ef 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -18,7 +18,8 @@
 
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 
-import android.net.MacAddress;
+import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
@@ -36,7 +37,8 @@
 import com.android.net.module.util.bpf.TetherStatsKey;
 import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
 import com.android.networkstack.tethering.BpfUtils;
 import com.android.networkstack.tethering.Tether6Value;
 import com.android.networkstack.tethering.TetherDevKey;
@@ -164,9 +166,38 @@
     }
 
     @Override
-    public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) {
-        if (!isInitialized()) return false;
+    public boolean addIpv6UpstreamRule(@NonNull final Ipv6UpstreamRule rule) {
+        // RFC7421_PREFIX_LENGTH = 64 which is the most commonly used IPv6 subnet prefix length.
+        if (rule.sourcePrefix.getPrefixLength() != RFC7421_PREFIX_LENGTH) return false;
 
+        final TetherUpstream6Key key = rule.makeTetherUpstream6Key();
+        final Tether6Value value = rule.makeTether6Value();
+
+        try {
+            mBpfUpstream6Map.insertEntry(key, value);
+        } catch (ErrnoException | IllegalStateException e) {
+            mLog.e("Could not insert upstream IPv6 entry: " + e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean removeIpv6UpstreamRule(@NonNull final Ipv6UpstreamRule rule) {
+        // RFC7421_PREFIX_LENGTH = 64 which is the most commonly used IPv6 subnet prefix length.
+        if (rule.sourcePrefix.getPrefixLength() != RFC7421_PREFIX_LENGTH) return false;
+
+        try {
+            mBpfUpstream6Map.deleteEntry(rule.makeTetherUpstream6Key());
+        } catch (ErrnoException e) {
+            mLog.e("Could not delete upstream IPv6 entry: " + e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean addIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
         final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
         final Tether6Value value = rule.makeTether6Value();
 
@@ -181,9 +212,7 @@
     }
 
     @Override
-    public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) {
-        if (!isInitialized()) return false;
-
+    public boolean removeIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
         try {
             mBpfDownstream6Map.deleteEntry(rule.makeTetherDownstream6Key());
         } catch (ErrnoException e) {
@@ -197,43 +226,8 @@
     }
 
     @Override
-    public boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
-            @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac,
-            @NonNull MacAddress outDstMac, int mtu) {
-        if (!isInitialized()) return false;
-
-        final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfindex, inDstMac);
-        final Tether6Value value = new Tether6Value(upstreamIfindex, outSrcMac,
-                outDstMac, OsConstants.ETH_P_IPV6, mtu);
-        try {
-            mBpfUpstream6Map.insertEntry(key, value);
-        } catch (ErrnoException | IllegalStateException e) {
-            mLog.e("Could not insert upstream6 entry: " + e);
-            return false;
-        }
-        return true;
-    }
-
-    @Override
-    public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
-            @NonNull MacAddress inDstMac) {
-        if (!isInitialized()) return false;
-
-        final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfindex, inDstMac);
-        try {
-            mBpfUpstream6Map.deleteEntry(key);
-        } catch (ErrnoException e) {
-            mLog.e("Could not delete upstream IPv6 entry: " + e);
-            return false;
-        }
-        return true;
-    }
-
-    @Override
     @Nullable
     public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
-        if (!isInitialized()) return null;
-
         final SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
         try {
             // The reported tether stats are total data usage for all currently-active upstream
@@ -248,8 +242,6 @@
 
     @Override
     public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) {
-        if (!isInitialized()) return false;
-
         // The common case is an update, where the stats already exist,
         // hence we read first, even though writing with BPF_NOEXIST
         // first would make the code simpler.
@@ -305,8 +297,6 @@
     @Override
     @Nullable
     public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) {
-        if (!isInitialized()) return null;
-
         // getAndClearTetherOffloadStats is called after all offload rules have already been
         // deleted for the given upstream interface. Before starting to do cleanup stuff in this
         // function, use synchronizeKernelRCU to make sure that all the current running eBPF
@@ -352,8 +342,6 @@
     @Override
     public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
             @NonNull Tether4Value value) {
-        if (!isInitialized()) return false;
-
         try {
             if (downstream) {
                 mBpfDownstream4Map.insertEntry(key, value);
@@ -377,8 +365,6 @@
 
     @Override
     public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) {
-        if (!isInitialized()) return false;
-
         try {
             if (downstream) {
                 if (!mBpfDownstream4Map.deleteEntry(key)) return false;  // Rule did not exist
@@ -411,8 +397,6 @@
     @Override
     public void tetherOffloadRuleForEach(boolean downstream,
             @NonNull ThrowingBiConsumer<Tether4Key, Tether4Value> action) {
-        if (!isInitialized()) return;
-
         try {
             if (downstream) {
                 mBpfDownstream4Map.forEach(action);
@@ -426,8 +410,6 @@
 
     @Override
     public boolean attachProgram(String iface, boolean downstream, boolean ipv4) {
-        if (!isInitialized()) return false;
-
         try {
             BpfUtils.attachProgram(iface, downstream, ipv4);
         } catch (IOException e) {
@@ -439,8 +421,6 @@
 
     @Override
     public boolean detachProgram(String iface, boolean ipv4) {
-        if (!isInitialized()) return false;
-
         try {
             BpfUtils.detachProgram(iface, ipv4);
         } catch (IOException e) {
@@ -458,8 +438,6 @@
 
     @Override
     public boolean addDevMap(int ifIndex) {
-        if (!isInitialized()) return false;
-
         try {
             mBpfDevMap.updateEntry(new TetherDevKey(ifIndex), new TetherDevValue(ifIndex));
         } catch (ErrnoException e) {
@@ -471,8 +449,6 @@
 
     @Override
     public boolean removeDevMap(int ifIndex) {
-        if (!isInitialized()) return false;
-
         try {
             mBpfDevMap.deleteEntry(new TetherDevKey(ifIndex));
         } catch (ErrnoException e) {
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index 51cecfe..d28a397 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -16,7 +16,6 @@
 
 package com.android.networkstack.tethering.apishim.common;
 
-import android.net.MacAddress;
 import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
@@ -27,7 +26,8 @@
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
 
 /**
  * Bpf coordinator class for API shims.
@@ -53,51 +53,51 @@
     public abstract boolean isInitialized();
 
     /**
-     * Adds a tethering offload rule to BPF map, or updates it if it already exists.
+     * Adds a tethering offload upstream rule to BPF map, or updates it if it already exists.
+     *
+     * An existing rule will be updated if the input interface, destination MAC and source prefix
+     * match. Otherwise, a new rule will be created. Note that this can be only called on handler
+     * thread.
+     *
+     * @param rule The rule to add or update.
+     * @return true if operation succeeded or was a no-op, false otherwise.
+     */
+    public abstract boolean addIpv6UpstreamRule(@NonNull Ipv6UpstreamRule rule);
+
+    /**
+     * Deletes a tethering offload upstream rule from the BPF map.
+     *
+     * An existing rule will be deleted if the input interface, destination MAC and source prefix
+     * match. It is not an error if there is no matching rule to delete.
+     *
+     * @param rule The rule to delete.
+     * @return true if operation succeeded or was a no-op, false otherwise.
+     */
+    public abstract boolean removeIpv6UpstreamRule(@NonNull Ipv6UpstreamRule rule);
+
+    /**
+     * Adds a tethering offload downstream rule to BPF map, or updates it if it already exists.
      *
      * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be updated
      * if the input interface and destination prefix match. Otherwise, a new rule will be created.
      * Note that this can be only called on handler thread.
      *
      * @param rule The rule to add or update.
+     * @return true if operation succeeded or was a no-op, false otherwise.
      */
-    public abstract boolean tetherOffloadRuleAdd(@NonNull Ipv6ForwardingRule rule);
+    public abstract boolean addIpv6DownstreamRule(@NonNull Ipv6DownstreamRule rule);
 
     /**
-     * Deletes a tethering offload rule from the BPF map.
+     * Deletes a tethering offload downstream rule from the BPF map.
      *
      * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be deleted
      * if the destination IP address and the source interface match. It is not an error if there is
      * no matching rule to delete.
      *
      * @param rule The rule to delete.
+     * @return true if operation succeeded or was a no-op, false otherwise.
      */
-    public abstract boolean tetherOffloadRuleRemove(@NonNull Ipv6ForwardingRule rule);
-
-    /**
-     * Starts IPv6 forwarding between the specified interfaces.
-
-     * @param downstreamIfindex the downstream interface index
-     * @param upstreamIfindex the upstream interface index
-     * @param inDstMac the destination MAC address to use for XDP
-     * @param outSrcMac the source MAC address to use for packets
-     * @param outDstMac the destination MAC address to use for packets
-     * @return true if operation succeeded or was a no-op, false otherwise
-     */
-    public abstract boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
-            @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac,
-            @NonNull MacAddress outDstMac, int mtu);
-
-    /**
-     * Stops IPv6 forwarding between the specified interfaces.
-
-     * @param downstreamIfindex the downstream interface index
-     * @param upstreamIfindex the upstream interface index
-     * @param inDstMac the destination MAC address to use for XDP
-     * @return true if operation succeeded or was a no-op, false otherwise
-     */
-    public abstract boolean stopUpstreamIpv6Forwarding(int downstreamIfindex,
-            int upstreamIfindex, @NonNull MacAddress inDstMac);
+    public abstract boolean removeIpv6DownstreamRule(@NonNull Ipv6DownstreamRule rule);
 
     /**
      * Return BPF tethering offload statistics.
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index a957e23..39a7540 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -14,6 +14,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -35,14 +36,15 @@
         "//frameworks/base/core/tests/bandwidthtests",
         "//frameworks/base/core/tests/benchmarks",
         "//frameworks/base/core/tests/utillib",
-        "//frameworks/base/packages/Connectivity/tests:__subpackages__",
+        "//frameworks/base/services/tests/VpnTests",
         "//frameworks/base/tests/vcn",
-        "//frameworks/libs/net/common/testutils",
-        "//frameworks/libs/net/common/tests:__subpackages__",
         "//frameworks/opt/telephony/tests/telephonytests",
         "//packages/modules/CaptivePortalLogin/tests",
+        "//packages/modules/Connectivity/staticlibs/testutils",
+        "//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
         "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Connectivity/thread/tests:__subpackages__",
         "//packages/modules/IPsec/tests/iketests",
         "//packages/modules/NetworkStack/tests:__subpackages__",
         "//packages/modules/Wifi/service/tests/wifitests",
@@ -54,33 +56,20 @@
 
     hostdex: true, // for hiddenapi check
     permitted_packages: ["android.net"],
-    lint: { strict_updatability_linting: true },
-}
-
-java_defaults {
-    name: "CronetJavaDefaults",
-    srcs: [":cronet_aml_api_sources"],
-    libs: [
-        "androidx.annotation_annotation",
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+    },
+    aconfig_declarations: [
+        "com.android.net.flags-aconfig",
     ],
-    impl_only_static_libs: [
-        "cronet_aml_java",
-    ],
-}
-
-java_defaults {
-  name: "CronetJavaPrejarjarDefaults",
-  static_libs: [
-    "cronet_aml_api_java",
-    "cronet_aml_java"
-  ],
 }
 
 java_library {
-  name: "framework-tethering-pre-jarjar",
-  defaults: [
-    "framework-tethering-defaults",
-  ],
+    name: "framework-tethering-pre-jarjar",
+    defaults: [
+        "framework-tethering-defaults",
+    ],
 }
 
 java_genrule {
@@ -106,7 +95,7 @@
     name: "framework-tethering-defaults",
     defaults: ["framework-module-defaults"],
     srcs: [
-      ":framework-tethering-srcs"
+        ":framework-tethering-srcs",
     ],
     libs: ["framework-connectivity.stubs.module_lib"],
     aidl: {
@@ -125,5 +114,5 @@
         "src/**/*.aidl",
         "src/**/*.java",
     ],
-    path: "src"
+    path: "src",
 }
diff --git a/Tethering/apex/in-process b/Tethering/common/TetheringLib/api/lint-baseline.txt
similarity index 100%
rename from Tethering/apex/in-process
rename to Tethering/common/TetheringLib/api/lint-baseline.txt
diff --git a/Tethering/common/TetheringLib/api/module-lib-lint-baseline.txt b/Tethering/common/TetheringLib/api/module-lib-lint-baseline.txt
new file mode 100644
index 0000000..1d09598
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/module-lib-lint-baseline.txt
@@ -0,0 +1,23 @@
+// Baseline format: 1.0
+BroadcastBehavior: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+    Field 'ACTION_TETHER_STATE_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.TetheringManager#requestLatestTetheringEntitlementResult(int, boolean, java.util.concurrent.Executor, android.net.TetheringManager.OnTetheringEntitlementResultListener):
+    Method 'requestLatestTetheringEntitlementResult' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#startTethering(android.net.TetheringManager.TetheringRequest, java.util.concurrent.Executor, android.net.TetheringManager.StartTetheringCallback):
+    Method 'startTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#startTethering(int, java.util.concurrent.Executor, android.net.TetheringManager.StartTetheringCallback):
+    Method 'startTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopAllTethering():
+    Method 'stopAllTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopTethering(int):
+    Method 'stopTethering' documentation mentions permissions already declared by @RequiresPermission
+
+
+SdkConstant: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+    Field 'ACTION_TETHER_STATE_CHANGED' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+
+
+Todo: android.net.TetheringConstants:
+    Documentation mentions 'TODO'
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 844ff64..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
@@ -95,13 +96,16 @@
     method public default void onUpstreamChanged(@Nullable android.net.Network);
   }
 
-  public static class TetheringManager.TetheringRequest {
+  public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
+    method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public int describeContents();
     method @Nullable public android.net.LinkAddress getClientStaticIpv4Address();
     method public int getConnectivityScope();
     method @Nullable public android.net.LinkAddress getLocalIpv4Address();
     method public boolean getShouldShowEntitlementUi();
     method public int getTetheringType();
     method public boolean isExemptFromEntitlementCheck();
+    method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringManager.TetheringRequest> CREATOR;
   }
 
   public static class TetheringManager.TetheringRequest.Builder {
diff --git a/Tethering/common/TetheringLib/api/system-lint-baseline.txt b/Tethering/common/TetheringLib/api/system-lint-baseline.txt
new file mode 100644
index 0000000..e678ce1
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/system-lint-baseline.txt
@@ -0,0 +1,17 @@
+// Baseline format: 1.0
+BroadcastBehavior: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+    Field 'ACTION_TETHER_STATE_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.TetheringManager#requestLatestTetheringEntitlementResult(int, boolean, java.util.concurrent.Executor, android.net.TetheringManager.OnTetheringEntitlementResultListener):
+    Method 'requestLatestTetheringEntitlementResult' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#startTethering(android.net.TetheringManager.TetheringRequest, java.util.concurrent.Executor, android.net.TetheringManager.StartTetheringCallback):
+    Method 'startTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopAllTethering():
+    Method 'stopAllTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopTethering(int):
+    Method 'stopTethering' documentation mentions permissions already declared by @RequiresPermission
+
+
+SdkConstant: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+    Field 'ACTION_TETHER_STATE_CHANGED' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
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 cd914d3..bc4ac971 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -18,6 +18,7 @@
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
 
 import android.Manifest;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -28,6 +29,8 @@
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.util.ArrayMap;
@@ -59,6 +62,16 @@
  */
 @SystemApi
 public class TetheringManager {
+    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+    // available here
+    /** @hide */
+    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();
     private static final int DEFAULT_TIMEOUT_MS = 60_000;
     private static final long CONNECTOR_POLL_INTERVAL_MILLIS = 200L;
@@ -184,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)
@@ -673,14 +698,44 @@
     /**
      *  Use with {@link #startTethering} to specify additional parameters when starting tethering.
      */
-    public static class TetheringRequest {
+    public static final class TetheringRequest implements Parcelable {
         /** A configuration set for TetheringRequest. */
         private final TetheringRequestParcel mRequestParcel;
 
-        private TetheringRequest(final TetheringRequestParcel request) {
+        private TetheringRequest(@NonNull final TetheringRequestParcel request) {
             mRequestParcel = request;
         }
 
+        private TetheringRequest(@NonNull Parcel in) {
+            mRequestParcel = in.readParcelable(TetheringRequestParcel.class.getClassLoader());
+        }
+
+        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @NonNull
+        public static final Creator<TetheringRequest> CREATOR = new Creator<>() {
+            @Override
+            public TetheringRequest createFromParcel(@NonNull Parcel in) {
+                return new TetheringRequest(in);
+            }
+
+            @Override
+            public TetheringRequest[] newArray(int size) {
+                return new TetheringRequest[size];
+            }
+        };
+
+        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            dest.writeParcelable(mRequestParcel, flags);
+        }
+
         /** Builder used to create TetheringRequest. */
         public static class Builder {
             private final TetheringRequestParcel mBuilderParcel;
@@ -1302,6 +1357,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);
 
@@ -1456,6 +1514,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/jni/onload.cpp b/Tethering/jni/onload.cpp
index ed80128..fd40d41 100644
--- a/Tethering/jni/onload.cpp
+++ b/Tethering/jni/onload.cpp
@@ -25,7 +25,6 @@
 int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
 int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
 int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env);
-int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env);
 int register_com_android_networkstack_tethering_util_TetheringUtils(JNIEnv* env);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
diff --git a/Tethering/lint-baseline.xml b/Tethering/lint-baseline.xml
index 37511c6..4f92c9c 100644
--- a/Tethering/lint-baseline.xml
+++ b/Tethering/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
@@ -8,7 +8,7 @@
         errorLine2="                                           ~~~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/OffloadController.java"
-            line="293"
+            line="283"
             column="44"/>
     </issue>
 
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 109bbda..47e2848 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -15,6 +15,10 @@
     native <methods>;
 }
 
+-keep class com.android.networkstack.tethering.util.TetheringUtils {
+    native <methods>;
+}
+
 # Ensure runtime-visible field annotations are kept when using R8 full mode.
 -keepattributes RuntimeVisibleAnnotations,AnnotationDefault
 -keep interface com.android.networkstack.tethering.util.Struct$Field {
diff --git a/Tethering/res/values-ky/strings.xml b/Tethering/res/values-ky/strings.xml
index 35e6453..4696a72 100644
--- a/Tethering/res/values-ky/strings.xml
+++ b/Tethering/res/values-ky/strings.xml
@@ -20,7 +20,7 @@
     <string name="tethered_notification_message" msgid="2338023450330652098">"Тууралоо үчүн басыңыз."</string>
     <string name="disable_tether_notification_title" msgid="3183576627492925522">"Модем режими өчүк"</string>
     <string name="disable_tether_notification_message" msgid="6655882039707534929">"Кеңири маалымат үчүн администраторуңузга кайрылыңыз"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Хотспот жана байланыш түйүнүүн статусу"</string>
+    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Байланыш түйүнү жана байланыш түйүнүүн статусу"</string>
     <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
     <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
     <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
diff --git a/Tethering/res/values-mcc310-mnc004-eu/strings.xml b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
index c970dd7..ff2a505 100644
--- a/Tethering/res/values-mcc310-mnc004-eu/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
@@ -18,7 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="no_upstream_notification_title" msgid="3584617491053416666">"Konexioa partekatzeko aukerak ez du Interneteko konexiorik"</string>
     <string name="no_upstream_notification_message" msgid="5626323795587558017">"Ezin dira konektatu gailuak"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desaktibatu konexioa partekatzeko aukera"</string>
+    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desaktibatu konexioa partekatzea"</string>
     <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago"</string>
     <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Baliteke tarifa gehigarriak ordaindu behar izatea ibiltaritza erabili bitartean"</string>
 </resources>
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 6affb62..2ddbfc0 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -28,12 +28,12 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 import static android.net.TetheringManager.TetheringRequest.checkStaticAddressConfiguration;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
-import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
 import static android.net.util.NetworkConstants.asByte;
 import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
-import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
+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.util.PrefixUtils.asIpPrefix;
 import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
 
@@ -55,40 +55,40 @@
 import android.net.dhcp.IDhcpServer;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
 import android.os.Handler;
-import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.MessageUtils;
 import com.android.internal.util.State;
-import com.android.internal.util.StateMachine;
 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.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.Ipv6ForwardingRule;
 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 java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -102,7 +102,7 @@
  *
  * @hide
  */
-public class IpServer extends StateMachine {
+public class IpServer extends StateMachineShim {
     public static final int STATE_UNAVAILABLE = 0;
     public static final int STATE_AVAILABLE   = 1;
     public static final int STATE_TETHERED    = 2;
@@ -127,11 +127,12 @@
     // TODO: have this configurable
     private static final int DHCP_LEASE_TIME_SECS = 3600;
 
+    private static final int NO_UPSTREAM = 0;
     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
     };
@@ -182,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);
@@ -227,12 +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_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;
@@ -244,6 +238,8 @@
     private final INetd mNetd;
     @NonNull
     private final BpfCoordinator mBpfCoordinator;
+    @NonNull
+    private final RoutingCoordinatorManager mRoutingCoordinator;
     private final Callback mCallback;
     private final InterfaceController mInterfaceCtrl;
     private final PrivateAddressCoordinator mPrivateAddressCoordinator;
@@ -252,7 +248,6 @@
     private final int mInterfaceType;
     private final LinkProperties mLinkProperties;
     private final boolean mUsingLegacyDhcp;
-    private final boolean mUsingBpfOffload;
     private final int mP2pLeasesSubnetPrefixLength;
 
     private final Dependencies mDeps;
@@ -260,6 +255,12 @@
     private int mLastError;
     private int mServingMode;
     private InterfaceSet mUpstreamIfaceSet;  // may change over time
+    // mInterfaceParams can't be final now because IpServer will be created when receives
+    // WIFI_AP_STATE_CHANGED broadcasts or when it detects that the wifi interface has come up.
+    // In the latter case, the interface is not fully initialized and the MAC address might not
+    // be correct (it will be set with a randomized MAC address later).
+    // TODO: Consider create the IpServer only when tethering want to enable it, then we can
+    //       make mInterfaceParams final.
     private InterfaceParams mInterfaceParams;
     // TODO: De-duplicate this with mLinkProperties above. Currently, these link
     // properties are those selected by the IPv6TetheringCoordinator and relayed
@@ -283,37 +284,34 @@
     private List<TetheredClient> mDhcpLeases = Collections.emptyList();
 
     private int mLastIPv6UpstreamIfindex = 0;
-
-    private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer {
-        public void accept(NeighborEvent e) {
-            sendMessage(CMD_NEIGHBOR_EVENT, e);
-        }
-    }
-
-    private final IpNeighborMonitor mIpNeighborMonitor;
+    @NonNull
+    private Set<IpPrefix> mLastIPv6UpstreamPrefixes = Collections.emptySet();
 
     private LinkAddress mIpv4Address;
 
     private final TetheringMetrics mTetheringMetrics;
+    private final Handler mHandler;
 
     // TODO: Add a dependency object to pass the data members or variables from the tethering
     // object. It helps to reduce the arguments of the constructor.
     public IpServer(
-            String ifaceName, Looper looper, int interfaceType, SharedLog log,
-            INetd netd, @NonNull BpfCoordinator coordinator, Callback callback,
+            String ifaceName, Handler handler, int interfaceType, SharedLog log,
+            INetd netd, @NonNull BpfCoordinator bpfCoordinator,
+            RoutingCoordinatorManager routingCoordinatorManager, Callback callback,
             TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
             TetheringMetrics tetheringMetrics, Dependencies deps) {
-        super(ifaceName, looper);
+        super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
+        mHandler = handler;
         mLog = log.forSubComponent(ifaceName);
         mNetd = netd;
-        mBpfCoordinator = coordinator;
+        mBpfCoordinator = bpfCoordinator;
+        mRoutingCoordinator = routingCoordinatorManager;
         mCallback = callback;
         mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
         mIfaceName = ifaceName;
         mInterfaceType = interfaceType;
         mLinkProperties = new LinkProperties();
         mUsingLegacyDhcp = config.useLegacyDhcpServer();
-        mUsingBpfOffload = config.isBpfOffloadEnabled();
         mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
         mPrivateAddressCoordinator = addressCoordinator;
         mDeps = deps;
@@ -322,29 +320,27 @@
         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 offload
-        // forwarding rules per client. If BPF offload is not supported, don't start listening
-        // for neighbor events. See updateIpv6ForwardingRules, addIpv6ForwardingRule,
-        // removeIpv6ForwardingRule.
-        if (mUsingBpfOffload && !mIpNeighborMonitor.start()) {
-            mLog.e("Failed to create IpNeighborMonitor on " + mIfaceName);
-        }
-
         mInitialState = new InitialState();
         mLocalHotspotState = new LocalHotspotState();
         mTetheredState = new TetheredState();
         mUnavailableState = new UnavailableState();
         mWaitingForRestartState = new WaitingForRestartState();
-        addState(mInitialState);
-        addState(mLocalHotspotState);
-        addState(mTetheredState);
-        addState(mWaitingForRestartState, mTetheredState);
-        addState(mUnavailableState);
+        final ArrayList allStates = new ArrayList<StateInfo>();
+        allStates.add(new StateInfo(mInitialState, null));
+        allStates.add(new StateInfo(mLocalHotspotState, null));
+        allStates.add(new StateInfo(mTetheredState, null));
+        allStates.add(new StateInfo(mWaitingForRestartState, mTetheredState));
+        allStates.add(new StateInfo(mUnavailableState, null));
+        addAllStates(allStates);
+    }
 
-        setInitialState(mInitialState);
+    private Handler getHandler() {
+        return mHandler;
+    }
+
+    /** Start IpServer state machine. */
+    public void start() {
+        start(mInitialState);
     }
 
     /** Interface name which IpServer served.*/
@@ -379,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.
@@ -485,7 +497,12 @@
 
         private void handleError() {
             mLastError = TETHER_ERROR_DHCPSERVER_ERROR;
-            transitionTo(mInitialState);
+            if (USE_SYNC_SM) {
+                sendMessage(CMD_SERVICE_FAILED_TO_START, TETHER_ERROR_DHCPSERVER_ERROR);
+            } else {
+                sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START,
+                        TETHER_ERROR_DHCPSERVER_ERROR);
+            }
         }
     }
 
@@ -743,7 +760,7 @@
         RaParams params = null;
         String upstreamIface = null;
         InterfaceParams upstreamIfaceParams = null;
-        int upstreamIfIndex = 0;
+        int upstreamIfIndex = NO_UPSTREAM;
 
         if (v6only != null) {
             upstreamIface = v6only.getInterfaceName();
@@ -757,13 +774,8 @@
 
             if (params.hasDefaultRoute) params.hopLimit = getHopLimit(upstreamIface, ttlAdjustment);
 
-            for (LinkAddress linkAddr : v6only.getLinkAddresses()) {
-                if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
-
-                final IpPrefix prefix = new IpPrefix(
-                        linkAddr.getAddress(), linkAddr.getPrefixLength());
-                params.prefixes.add(prefix);
-
+            params.prefixes = getTetherableIpv6Prefixes(v6only);
+            for (IpPrefix prefix : params.prefixes) {
                 final Inet6Address dnsServer = getLocalDnsIpFor(prefix);
                 if (dnsServer != null) {
                     params.dnses.add(dnsServer);
@@ -775,19 +787,22 @@
         // CMD_TETHER_CONNECTION_CHANGED. Adding the mapping update here to the avoid potential
         // timing issue. It prevents that the IPv6 capability is updated later than
         // CMD_TETHER_CONNECTION_CHANGED.
-        mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfIndex, upstreamIface);
+        mBpfCoordinator.maybeAddUpstreamToLookupTable(upstreamIfIndex, upstreamIface);
 
         // If v6only is null, we pass in null to setRaParams(), which handles
         // deprecation of any existing RA data.
-
         setRaParams(params);
-        // Be aware that updateIpv6ForwardingRules use mLastIPv6LinkProperties, so this line should
-        // be eariler than updateIpv6ForwardingRules.
-        // TODO: avoid this dependencies and move this logic into BpfCoordinator.
-        mLastIPv6LinkProperties = v6only;
 
-        updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfIndex, null);
+        // Not support BPF on virtual upstream interface
+        final Set<IpPrefix> upstreamPrefixes = params != null ? params.prefixes : Set.of();
+        // 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;
         if (mDadProxy != null) {
             mDadProxy.setUpstreamIface(upstreamIfaceParams);
         }
@@ -804,21 +819,40 @@
         for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
     }
 
-    private void addRoutesToLocalNetwork(@NonNull final List<RouteInfo> toBeAdded) {
+    private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
         try {
-            // It's safe to call networkAddInterface() even if
-            // the interface is already in the local_network.
-            mNetd.networkAddInterface(INetd.LOCAL_NET_ID, mIfaceName);
-            try {
-                // Add routes from local network. Note that adding routes that
-                // already exist does not cause an error (EEXIST is silently ignored).
-                NetdUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
-            } catch (IllegalStateException e) {
-                mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
-                return;
-            }
-        } 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 {
+        mRoutingCoordinator.addInterfaceForward(fromIface, toIface);
+    }
+
+    private void removeInterfaceForward(@NonNull final String fromIface,
+            @NonNull final String toIface) {
+        try {
+            mRoutingCoordinator.removeInterfaceForward(fromIface, toIface);
+        } catch (RuntimeException e) {
+            mLog.e("Exception in removeInterfaceForward", e);
+        }
+    }
+
+    private void addRoutesToLocalNetwork(@NonNull final List<RouteInfo> toBeAdded) {
+        // It's safe to call addInterfaceToNetwork() even if
+        // the interface is already in the local_network.
+        addInterfaceToNetwork(INetd.LOCAL_NET_ID, mIfaceName);
+        try {
+            // Add routes from local network. Note that adding routes that
+            // already exist does not cause an error (EEXIST is silently ignored).
+            NetdUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
+        } catch (IllegalStateException e) {
+            mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
             return;
         }
 
@@ -826,7 +860,7 @@
     }
 
     private void configureLocalIPv6Routes(
-            HashSet<IpPrefix> deprecatedPrefixes, HashSet<IpPrefix> newPrefixes) {
+            ArraySet<IpPrefix> deprecatedPrefixes, ArraySet<IpPrefix> newPrefixes) {
         // [1] Remove the routes that are deprecated.
         if (!deprecatedPrefixes.isEmpty()) {
             removeRoutesFromLocalNetwork(getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
@@ -834,7 +868,7 @@
 
         // [2] Add only the routes that have not previously been added.
         if (newPrefixes != null && !newPrefixes.isEmpty()) {
-            HashSet<IpPrefix> addedPrefixes = (HashSet) newPrefixes.clone();
+            ArraySet<IpPrefix> addedPrefixes = new ArraySet<IpPrefix>(newPrefixes);
             if (mLastRaParams != null) {
                 addedPrefixes.removeAll(mLastRaParams.prefixes);
             }
@@ -846,7 +880,7 @@
     }
 
     private void configureLocalIPv6Dns(
-            HashSet<Inet6Address> deprecatedDnses, HashSet<Inet6Address> newDnses) {
+            ArraySet<Inet6Address> deprecatedDnses, ArraySet<Inet6Address> newDnses) {
         // TODO: Is this really necessary? Can we not fail earlier if INetd cannot be located?
         if (mNetd == null) {
             if (newDnses != null) newDnses.clear();
@@ -867,7 +901,7 @@
 
         // [2] Add only the local DNS IP addresses that have not previously been added.
         if (newDnses != null && !newDnses.isEmpty()) {
-            final HashSet<Inet6Address> addedDnses = (HashSet) newDnses.clone();
+            final ArraySet<Inet6Address> addedDnses = new ArraySet<Inet6Address>(newDnses);
             if (mLastRaParams != null) {
                 addedDnses.removeAll(mLastRaParams.dnses);
             }
@@ -890,116 +924,6 @@
         }
     }
 
-    private void addIpv6ForwardingRule(Ipv6ForwardingRule rule) {
-        // Theoretically, we don't need this check because IP neighbor monitor doesn't start if BPF
-        // offload is disabled. Add this check just in case.
-        // TODO: Perhaps remove this protection check.
-        if (!mUsingBpfOffload) return;
-
-        mBpfCoordinator.tetherOffloadRuleAdd(this, rule);
-    }
-
-    private void removeIpv6ForwardingRule(Ipv6ForwardingRule rule) {
-        // TODO: Perhaps remove this protection check.
-        // See the related comment in #addIpv6ForwardingRule.
-        if (!mUsingBpfOffload) return;
-
-        mBpfCoordinator.tetherOffloadRuleRemove(this, rule);
-    }
-
-    private void clearIpv6ForwardingRules() {
-        if (!mUsingBpfOffload) return;
-
-        mBpfCoordinator.tetherOffloadRuleClear(this);
-    }
-
-    private void updateIpv6ForwardingRule(int newIfindex) {
-        // TODO: Perhaps remove this protection check.
-        // See the related comment in #addIpv6ForwardingRule.
-        if (!mUsingBpfOffload) return;
-
-        mBpfCoordinator.tetherOffloadRuleUpdate(this, newIfindex);
-    }
-
-    private boolean isIpv6VcnNetworkInterface() {
-        if (mLastIPv6LinkProperties == null) return false;
-
-        return isVcnInterface(mLastIPv6LinkProperties.getInterfaceName());
-    }
-
-    // Handles all updates to IPv6 forwarding rules. These can currently change only if the upstream
-    // changes or if a neighbor event is received.
-    private void updateIpv6ForwardingRules(int prevUpstreamIfindex, int upstreamIfindex,
-            NeighborEvent e) {
-        // If no longer have an upstream or it is virtual network, clear forwarding rules and do
-        // nothing else.
-        // TODO: Rather than always clear rules, ensure whether ipv6 ever enable first.
-        if (upstreamIfindex == 0 || isIpv6VcnNetworkInterface()) {
-            clearIpv6ForwardingRules();
-            return;
-        }
-
-        // If the upstream interface has changed, remove all rules and re-add them with the new
-        // upstream interface.
-        if (prevUpstreamIfindex != upstreamIfindex) {
-            updateIpv6ForwardingRule(upstreamIfindex);
-        }
-
-        // If we're here to process a NeighborEvent, do so now.
-        // 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 Ipv6ForwardingRule constructor to ensure that we never
-        // add rules with a null MAC, only delete them.
-        MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
-        Ipv6ForwardingRule rule = new Ipv6ForwardingRule(upstreamIfindex,
-                mInterfaceParams.index, (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
-        if (e.isValid()) {
-            addIpv6ForwardingRule(rule);
-        } else {
-            removeIpv6ForwardingRule(rule);
-        }
-    }
-
-    // TODO: consider moving into BpfCoordinator.
-    private void updateClientInfoIpv4(NeighborEvent e) {
-        // TODO: Perhaps remove this protection check.
-        // See the related comment in #addIpv6ForwardingRule.
-        if (!mUsingBpfOffload) return;
-
-        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) {
-            updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamIfindex, e);
-            updateClientInfoIpv4(e);
-        }
-    }
-
     private byte getHopLimit(String upstreamIface, int adjustTTL) {
         try {
             int upstreamHopLimit = Integer.parseUnsignedInt(
@@ -1034,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 "
@@ -1106,14 +1029,6 @@
         }
     }
 
-    private void startConntrackMonitoring() {
-        mBpfCoordinator.startMonitoring(this);
-    }
-
-    private void stopConntrackMonitoring() {
-        mBpfCoordinator.stopMonitoring(this);
-    }
-
     abstract class BaseServingState extends State {
         private final int mDesiredInterfaceState;
 
@@ -1123,12 +1038,24 @@
 
         @Override
         public void enter() {
-            startConntrackMonitoring();
+            mBpfCoordinator.addIpServer(IpServer.this);
 
             startServingInterface();
 
             if (mLastError != TETHER_ERROR_NO_ERROR) {
-                transitionTo(mInitialState);
+                // This will transition to InitialState right away, regardless of whether any
+                // message is already waiting in the StateMachine queue (including maybe some
+                // message to go to InitialState). InitialState will then process any pending
+                // message (and generally ignores them). It is difficult to know for sure whether
+                // this is correct in all cases, but this is equivalent to what IpServer was doing
+                // in previous versions of the mainline module.
+                // TODO : remove sendMessageAtFrontOfQueueToAsyncSM after migrating to the Sync
+                // StateMachine.
+                if (USE_SYNC_SM) {
+                    sendSelfMessageToSyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
+                } else {
+                    sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
+                }
             }
 
             if (DBG) Log.d(TAG, getStateString(mDesiredInterfaceState) + " serve " + mIfaceName);
@@ -1179,7 +1106,7 @@
             }
 
             stopIPv4();
-            stopConntrackMonitoring();
+            mBpfCoordinator.removeIpServer(IpServer.this);
 
             resetLinkProperties();
 
@@ -1218,6 +1145,9 @@
                     mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
                     transitionTo(mWaitingForRestartState);
                     break;
+                case CMD_SERVICE_FAILED_TO_START:
+                    mLog.e("start serving fail, error: " + message.arg1);
+                    transitionTo(mInitialState);
                 default:
                     return false;
             }
@@ -1328,6 +1258,7 @@
         @Override
         public void exit() {
             cleanupUpstream();
+            mBpfCoordinator.clearAllIpv6Rules(IpServer.this);
             super.exit();
         }
 
@@ -1346,7 +1277,8 @@
 
             for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
             mUpstreamIfaceSet = null;
-            clearIpv6ForwardingRules();
+            mBpfCoordinator.updateIpv6UpstreamInterface(IpServer.this, NO_UPSTREAM,
+                    Collections.emptySet());
         }
 
         private void cleanupUpstreamInterface(String upstreamIface) {
@@ -1355,16 +1287,7 @@
             // to remove their rules, which generates errors.
             // Just do the best we can.
             mBpfCoordinator.maybeDetachProgram(mIfaceName, upstreamIface);
-            try {
-                mNetd.ipfwdRemoveInterfaceForward(mIfaceName, upstreamIface);
-            } catch (RemoteException | ServiceSpecificException e) {
-                mLog.e("Exception in ipfwdRemoveInterfaceForward: " + e.toString());
-            }
-            try {
-                mNetd.tetherRemoveForward(mIfaceName, upstreamIface);
-            } catch (RemoteException | ServiceSpecificException e) {
-                mLog.e("Exception in disableNat: " + e.toString());
-            }
+            removeInterfaceForward(mIfaceName, upstreamIface);
         }
 
         @Override
@@ -1413,17 +1336,16 @@
                             final InterfaceParams upstreamIfaceParams =
                                     mDeps.getInterfaceParams(ifname);
                             if (upstreamIfaceParams != null) {
-                                mBpfCoordinator.addUpstreamNameToLookupTable(
+                                mBpfCoordinator.maybeAddUpstreamToLookupTable(
                                         upstreamIfaceParams.index, ifname);
                             }
                         }
 
                         mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname);
                         try {
-                            mNetd.tetherAddForward(mIfaceName, ifname);
-                            mNetd.ipfwdAddInterfaceForward(mIfaceName, ifname);
-                        } catch (RemoteException | ServiceSpecificException e) {
-                            mLog.e("Exception enabling NAT: " + e.toString());
+                            addInterfaceForward(mIfaceName, ifname);
+                        } catch (RuntimeException e) {
+                            mLog.e("Exception enabling iface forward", e);
                             cleanupUpstream();
                             mLastError = TETHER_ERROR_ENABLE_FORWARDING_ERROR;
                             transitionTo(mInitialState);
@@ -1431,9 +1353,6 @@
                         }
                     }
                     break;
-                case CMD_NEIGHBOR_EVENT:
-                    handleNeighborEvent((NeighborEvent) message.obj);
-                    break;
                 default:
                     return false;
             }
@@ -1473,7 +1392,6 @@
     class UnavailableState extends State {
         @Override
         public void enter() {
-            mIpNeighborMonitor.stop();
             mLastError = TETHER_ERROR_NO_ERROR;
             sendInterfaceState(STATE_UNAVAILABLE);
         }
@@ -1504,7 +1422,7 @@
     // Accumulate routes representing "prefixes to be assigned to the local
     // interface", for subsequent modification of local_network routing.
     private static ArrayList<RouteInfo> getLocalRoutesFor(
-            String ifname, HashSet<IpPrefix> prefixes) {
+            String ifname, ArraySet<IpPrefix> prefixes) {
         final ArrayList<RouteInfo> localRoutes = new ArrayList<RouteInfo>();
         for (IpPrefix ipp : prefixes) {
             localRoutes.add(new RouteInfo(ipp, null, ifname, RTN_UNICAST));
@@ -1531,4 +1449,21 @@
         }
         return random;
     }
+
+    /** Get IPv6 prefixes from LinkProperties */
+    @NonNull
+    @VisibleForTesting
+    static ArraySet<IpPrefix> getTetherableIpv6Prefixes(@NonNull Collection<LinkAddress> addrs) {
+        final ArraySet<IpPrefix> prefixes = new ArraySet<>();
+        for (LinkAddress linkAddr : addrs) {
+            if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
+            prefixes.add(new IpPrefix(linkAddr.getAddress(), RFC7421_PREFIX_LENGTH));
+        }
+        return prefixes;
+    }
+
+    @NonNull
+    private ArraySet<IpPrefix> getTetherableIpv6Prefixes(@NonNull LinkProperties lp) {
+        return getTetherableIpv6Prefixes(lp.getLinkAddresses());
+    }
 }
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 18c2171..d848ea8 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -16,7 +16,6 @@
 
 package android.net.ip;
 
-import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.SOCK_RAW;
@@ -30,6 +29,7 @@
 import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
 import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
 import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
+import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
 import static com.android.net.module.util.NetworkStackConstants.TAG_SYSTEM_NEIGHBOR;
 import static com.android.networkstack.tethering.util.TetheringUtils.getAllNodesForScopeId;
 
@@ -41,6 +41,7 @@
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.StructTimeval;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
@@ -134,23 +135,23 @@
         public boolean hasDefaultRoute;
         public byte hopLimit;
         public int mtu;
-        public HashSet<IpPrefix> prefixes;
-        public HashSet<Inet6Address> dnses;
+        public ArraySet<IpPrefix> prefixes;
+        public ArraySet<Inet6Address> dnses;
 
         public RaParams() {
             hasDefaultRoute = false;
             hopLimit = DEFAULT_HOPLIMIT;
             mtu = IPV6_MIN_MTU;
-            prefixes = new HashSet<IpPrefix>();
-            dnses = new HashSet<Inet6Address>();
+            prefixes = new ArraySet<IpPrefix>();
+            dnses = new ArraySet<Inet6Address>();
         }
 
         public RaParams(RaParams other) {
             hasDefaultRoute = other.hasDefaultRoute;
             hopLimit = other.hopLimit;
             mtu = other.mtu;
-            prefixes = (HashSet) other.prefixes.clone();
-            dnses = (HashSet) other.dnses.clone();
+            prefixes = new ArraySet<IpPrefix>(other.prefixes);
+            dnses = new ArraySet<Inet6Address>(other.dnses);
         }
 
         /**
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 976f5df..5c853f4 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -28,6 +28,7 @@
 import static android.system.OsConstants.ETH_P_IPV6;
 
 import static com.android.net.module.util.NetworkStackConstants.IPV4_MIN_MTU;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
 import static com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
 import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
 import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
@@ -37,6 +38,7 @@
 
 import android.app.usage.NetworkStatsManager;
 import android.net.INetd;
+import android.net.IpPrefix;
 import android.net.LinkProperties;
 import android.net.MacAddress;
 import android.net.NetworkStats;
@@ -49,6 +51,7 @@
 import android.system.ErrnoException;
 import android.system.OsConstants;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
@@ -66,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;
@@ -74,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;
@@ -149,6 +154,7 @@
     static final int NF_CONNTRACK_UDP_TIMEOUT_STREAM = 180;
     @VisibleForTesting
     static final int INVALID_MTU = 0;
+    static final int NO_UPSTREAM = 0;
 
     // List of TCP port numbers which aren't offloaded because the packets require the netfilter
     // conntrack helper. See also TetherController::setForwardRules in netd.
@@ -178,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
@@ -186,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;
@@ -217,6 +219,23 @@
     // TODO: Remove the unused interface name.
     private final SparseArray<String> mInterfaceNames = new SparseArray<>();
 
+    // How IPv6 upstream rules and downstream rules are managed in BpfCoordinator:
+    // 1. Each upstream rule represents a downstream interface to an upstream interface forwarding.
+    //    No upstream rule will be exist if there is no upstream interface.
+    //    Note that there is at most one upstream interface for a given downstream interface.
+    // 2. Each downstream rule represents an IPv6 neighbor, regardless of the existence of the
+    //    upstream interface. If the upstream is not present, the downstream rules have an upstream
+    //    interface index of NO_UPSTREAM, only exist in BpfCoordinator and won't be written to the
+    //    BPF map. When the upstream comes back, those downstream rules will be updated by calling
+    //    Ipv6DownstreamRule#onNewUpstream and written to the BPF map again. We don't remove the
+    //    downstream rules when upstream is lost is because the upstream may come back with the
+    //    same prefix and we won't receive any neighbor update event in this case.
+    //    TODO: Remove downstream rules when upstream is lost and dump neighbors table when upstream
+    //    interface comes back in order to reconstruct the downstream rules.
+    // 3. It is the same thing for BpfCoordinator if there is no upstream interface or the upstream
+    //    interface is a virtual interface (which currently not supports BPF). In this case,
+    //    IpServer will update its upstream ifindex to NO_UPSTREAM to the BpfCoordinator.
+
     // Map of downstream rule maps. Each of these maps represents the IPv6 forwarding rules for a
     // given downstream. Each map:
     // - Is owned by the IpServer that is responsible for that downstream.
@@ -233,8 +252,18 @@
     // rules function without a valid IPv6 downstream interface index even if it may have one
     // before. IpServer would need to call getInterfaceParams() in the constructor instead of when
     // startIpv6() is called, and make mInterfaceParams final.
-    private final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>>
-            mIpv6ForwardingRules = new LinkedHashMap<>();
+    private final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6DownstreamRule>>
+            mIpv6DownstreamRules = new LinkedHashMap<>();
+
+    // Map of IPv6 upstream rules maps. Each of these maps represents the IPv6 upstream rules for a
+    // given downstream. Each map:
+    // - Is owned by the IpServer that is responsible for that downstream.
+    // - Must only be modified by that IpServer.
+    // - Is created when the IpServer adds its first upstream rule, and deleted when the IpServer
+    //   deletes its last upstream rule (or clears its upstream rules)
+    // - Each upstream rule in the ArraySet is corresponding to an upstream interface.
+    private final ArrayMap<IpServer, ArraySet<Ipv6UpstreamRule>>
+            mIpv6UpstreamRules = new ArrayMap<>();
 
     // Map of downstream client maps. Each of these maps represents the IPv4 clients for a given
     // downstream. Needed to build IPv4 forwarding rules when conntrack events are received.
@@ -249,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.
@@ -273,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.
@@ -308,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);
@@ -348,7 +382,7 @@
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH,
-                    BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
+                    Tether4Key.class, Tether4Value.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create downstream4 map: " + e);
                 return null;
@@ -360,7 +394,7 @@
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_UPSTREAM4_MAP_PATH,
-                    BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
+                    Tether4Key.class, Tether4Value.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create upstream4 map: " + e);
                 return null;
@@ -372,7 +406,7 @@
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH,
-                    BpfMap.BPF_F_RDWR, TetherDownstream6Key.class, Tether6Value.class);
+                    TetherDownstream6Key.class, Tether6Value.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create downstream6 map: " + e);
                 return null;
@@ -383,7 +417,7 @@
         @Nullable public IBpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
             if (!isAtLeastS()) return null;
             try {
-                return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+                return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH,
                         TetherUpstream6Key.class, Tether6Value.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create upstream6 map: " + e);
@@ -396,7 +430,7 @@
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_STATS_MAP_PATH,
-                    BpfMap.BPF_F_RDWR, TetherStatsKey.class, TetherStatsValue.class);
+                    TetherStatsKey.class, TetherStatsValue.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create stats map: " + e);
                 return null;
@@ -408,7 +442,7 @@
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_LIMIT_MAP_PATH,
-                    BpfMap.BPF_F_RDWR, TetherLimitKey.class, TetherLimitValue.class);
+                    TetherLimitKey.class, TetherLimitValue.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create limit map: " + e);
                 return null;
@@ -420,7 +454,7 @@
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_DEV_MAP_PATH,
-                    BpfMap.BPF_F_RDWR, TetherDevKey.class, TetherDevValue.class);
+                    TetherDevKey.class, TetherDevValue.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create dev map: " + e);
                 return null;
@@ -455,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(
@@ -474,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 #tetherOffloadRuleRemove. After the
-     * last rule is removed from the upstream, #tetherOffloadRuleRemove does the cleanup
+     * 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);
@@ -514,18 +539,28 @@
             mHandler.removeCallbacks(mScheduledPollingStats);
         }
         updateForwardedStats();
-        mPollingStarted = false;
 
-        mLog.i("Polling stopped");
+        mLog.i("Polling stopped.");
     }
 
+    /**
+     * Return whether BPF offload is supported
+     */
+    public boolean isUsingBpfOffload() {
+        return isUsingBpf();
+    }
+
+    // This is identical to isUsingBpfOffload above but is only used internally.
+    // The reason for having two separate methods is that the code calls isUsingBpf
+    // very often. But the tests call verifyNoMoreInteractions, which will check all
+    // calls to public methods. If isUsingBpf were public, the test would need to
+    // verify all calls to it, which would clutter the test.
     private boolean isUsingBpf() {
         return mIsBpfEnabled && mBpfCoordinatorShim.isInitialized();
     }
 
     /**
      * 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.
@@ -545,175 +580,194 @@
      * +------------------+--------------------------------------------------------+
      * 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.");
     }
 
     /**
-     * Add forwarding rule. After adding the first rule on a given upstream, must add the data
-     * limit on the given upstream.
-     * Note that this can be only called on handler thread.
+     * Add IPv6 upstream rule. After adding the first rule on a given upstream, must add the
+     * data limit on the given upstream.
      */
-    public void tetherOffloadRuleAdd(
-            @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
+    private void addIpv6UpstreamRule(
+            @NonNull final IpServer ipServer, @NonNull final Ipv6UpstreamRule rule) {
         if (!isUsingBpf()) return;
 
-        // TODO: Perhaps avoid to add a duplicate rule.
-        if (!mBpfCoordinatorShim.tetherOffloadRuleAdd(rule)) return;
-
-        if (!mIpv6ForwardingRules.containsKey(ipServer)) {
-            mIpv6ForwardingRules.put(ipServer, new LinkedHashMap<Inet6Address,
-                    Ipv6ForwardingRule>());
-        }
-        LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(ipServer);
-
         // Add upstream and downstream interface index to dev map.
         maybeAddDevMap(rule.upstreamIfindex, rule.downstreamIfindex);
 
         // When the first rule is added to an upstream, setup upstream forwarding and data limit.
         maybeSetLimit(rule.upstreamIfindex);
 
-        if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
-            final int downstream = rule.downstreamIfindex;
-            final int upstream = rule.upstreamIfindex;
-            // TODO: support upstream forwarding on non-point-to-point interfaces.
-            // TODO: get the MTU from LinkProperties and update the rules when it changes.
-            if (!mBpfCoordinatorShim.startUpstreamIpv6Forwarding(downstream, upstream, rule.srcMac,
-                    NULL_MAC_ADDRESS, NULL_MAC_ADDRESS, NetworkStackConstants.ETHER_MTU)) {
-                mLog.e("Failed to enable upstream IPv6 forwarding from "
-                        + getIfName(downstream) + " to " + getIfName(upstream));
-            }
+        // TODO: support upstream forwarding on non-point-to-point interfaces.
+        // TODO: get the MTU from LinkProperties and update the rules when it changes.
+        if (!mBpfCoordinatorShim.addIpv6UpstreamRule(rule)) {
+            return;
         }
 
-        // Must update the adding rule after calling #isAnyRuleOnUpstream because it needs to
-        // check if it is about adding a first rule for a given upstream.
+        ArraySet<Ipv6UpstreamRule> rules = mIpv6UpstreamRules.computeIfAbsent(
+                ipServer, k -> new ArraySet<Ipv6UpstreamRule>());
+        rules.add(rule);
+    }
+
+    /**
+     * Clear all IPv6 upstream rules for a given downstream. After removing the last rule on a given
+     * upstream, must clear data limit, update the last tether stats and remove the tether stats in
+     * the BPF maps.
+     */
+    private void clearIpv6UpstreamRules(@NonNull final IpServer ipServer) {
+        if (!isUsingBpf()) return;
+
+        final ArraySet<Ipv6UpstreamRule> upstreamRules = mIpv6UpstreamRules.remove(ipServer);
+        if (upstreamRules == null) return;
+
+        int upstreamIfindex = 0;
+        for (Ipv6UpstreamRule rule: upstreamRules) {
+            if (upstreamIfindex != 0 && rule.upstreamIfindex != upstreamIfindex) {
+                Log.wtf(TAG, "BUG: upstream rules point to more than one interface");
+            }
+            upstreamIfindex = rule.upstreamIfindex;
+            mBpfCoordinatorShim.removeIpv6UpstreamRule(rule);
+        }
+        // Clear the limit if there are no more rules on the given upstream.
+        // Using upstreamIfindex outside the loop is fine because all the rules for a given IpServer
+        // will always have the same upstream index (since they are always added all together by
+        // updateAllIpv6Rules).
+        // The upstreamIfindex can't be 0 because we won't add an Ipv6UpstreamRule with
+        // upstreamIfindex == 0 and if there is no Ipv6UpstreamRule for an IpServer, it will be
+        // removed from mIpv6UpstreamRules.
+        if (upstreamIfindex == 0) {
+            Log.wtf(TAG, "BUG: upstream rules have empty Set or rule.upstreamIfindex == 0");
+            return;
+        }
+        maybeClearLimit(upstreamIfindex);
+    }
+
+    /**
+     * Add IPv6 downstream rule.
+     */
+    private void addIpv6DownstreamRule(
+            @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
+        if (!isUsingBpf()) return;
+
+        // TODO: Perhaps avoid to add a duplicate rule.
+        if (rule.upstreamIfindex != NO_UPSTREAM
+                && !mBpfCoordinatorShim.addIpv6DownstreamRule(rule)) return;
+
+        LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
+                mIpv6DownstreamRules.computeIfAbsent(ipServer,
+                        k -> new LinkedHashMap<Inet6Address, Ipv6DownstreamRule>());
         rules.put(rule.address, rule);
     }
 
     /**
-     * Remove forwarding rule. After removing the last rule on a given upstream, must clear
-     * data limit, update the last tether stats and remove the tether stats in the BPF maps.
-     * Note that this can be only called on handler thread.
+     * Remove IPv6 downstream rule.
      */
-    public void tetherOffloadRuleRemove(
-            @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
+    private void removeIpv6DownstreamRule(
+            @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
         if (!isUsingBpf()) return;
 
-        if (!mBpfCoordinatorShim.tetherOffloadRuleRemove(rule)) return;
+        if (rule.upstreamIfindex != NO_UPSTREAM
+                && !mBpfCoordinatorShim.removeIpv6DownstreamRule(rule)) return;
 
-        LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(ipServer);
+        LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules = mIpv6DownstreamRules.get(ipServer);
         if (rules == null) return;
 
-        // Must remove rules before calling #isAnyRuleOnUpstream because it needs to check if
-        // the last rule is removed for a given upstream. If no rule is removed, return early.
-        // Avoid unnecessary work on a non-existent rule which may have never been added or
-        // removed already.
+        // If no rule is removed, return early. Avoid unnecessary work on a non-existent rule which
+        // may have never been added or removed already.
         if (rules.remove(rule.address) == null) return;
 
         // Remove the downstream entry if it has no more rule.
         if (rules.isEmpty()) {
-            mIpv6ForwardingRules.remove(ipServer);
+            mIpv6DownstreamRules.remove(ipServer);
         }
+    }
 
-        // If no more rules between this upstream and downstream, stop upstream forwarding.
-        if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
-            final int downstream = rule.downstreamIfindex;
-            final int upstream = rule.upstreamIfindex;
-            if (!mBpfCoordinatorShim.stopUpstreamIpv6Forwarding(downstream, upstream,
-                    rule.srcMac)) {
-                mLog.e("Failed to disable upstream IPv6 forwarding from "
-                        + getIfName(downstream) + " to " + getIfName(upstream));
-            }
+    /**
+      * Clear all downstream rules for a given IpServer and return a copy of all removed rules.
+      */
+    @Nullable
+    private Collection<Ipv6DownstreamRule> clearIpv6DownstreamRules(
+            @NonNull final IpServer ipServer) {
+        final LinkedHashMap<Inet6Address, Ipv6DownstreamRule> downstreamRules =
+                mIpv6DownstreamRules.remove(ipServer);
+        if (downstreamRules == null) return null;
+
+        final Collection<Ipv6DownstreamRule> removedRules = downstreamRules.values();
+        for (final Ipv6DownstreamRule rule : removedRules) {
+            if (rule.upstreamIfindex == NO_UPSTREAM) continue;
+            mBpfCoordinatorShim.removeIpv6DownstreamRule(rule);
         }
-
-        // Do cleanup functionality if there is no more rule on the given upstream.
-        maybeClearLimit(rule.upstreamIfindex);
+        return removedRules;
     }
 
     /**
      * Clear all forwarding rules for a given downstream.
      * Note that this can be only called on handler thread.
-     * TODO: rename to tetherOffloadRuleClear6 because of IPv6 only.
      */
-    public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) {
+    public void clearAllIpv6Rules(@NonNull final IpServer ipServer) {
         if (!isUsingBpf()) return;
 
-        final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(
-                ipServer);
-        if (rules == null) return;
-
-        // Need to build a rule list because the rule map may be changed in the iteration.
-        for (final Ipv6ForwardingRule rule : new ArrayList<Ipv6ForwardingRule>(rules.values())) {
-            tetherOffloadRuleRemove(ipServer, rule);
-        }
+        // Clear downstream rules first, because clearing upstream rules fetches the stats, and
+        // fetching the stats requires that no rules be forwarding traffic to or from the upstream.
+        clearIpv6DownstreamRules(ipServer);
+        clearIpv6UpstreamRules(ipServer);
     }
 
     /**
-     * Update existing forwarding rules to new upstream for a given downstream.
-     * Note that this can be only called on handler thread.
+     * Delete all upstream and downstream rules for the passed-in IpServer, and if the new upstream
+     * is nonzero, reapply them to the new upstream.
      */
-    public void tetherOffloadRuleUpdate(@NonNull final IpServer ipServer, int newUpstreamIfindex) {
+    private void updateAllIpv6Rules(@NonNull final IpServer ipServer,
+            final InterfaceParams interfaceParams, int newUpstreamIfindex,
+            @NonNull final Set<IpPrefix> newUpstreamPrefixes) {
         if (!isUsingBpf()) return;
 
-        final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(
-                ipServer);
-        if (rules == null) return;
+        // Remove IPv6 downstream rules. Remove the old ones before adding the new rules, otherwise
+        // we need to keep a copy of the old rules.
+        // We still need to keep the downstream rules even when the upstream goes away because it
+        // may come back with the same prefixes (unlikely, but possible). Neighbor entries won't be
+        // deleted and we're not expected to receive new Neighbor events in this case.
+        // TODO: Add new rule first to reduce the latency which has no rule. But this is okay
+        //       because if this is a new upstream, it will probably have different prefixes than
+        //       the one these downstream rules are in. If so, they will never see any downstream
+        //       traffic before new neighbor entries are created.
+        final Collection<Ipv6DownstreamRule> deletedDownstreamRules =
+                clearIpv6DownstreamRules(ipServer);
 
-        // Need to build a rule list because the rule map may be changed in the iteration.
-        // First remove all the old rules, then add all the new rules. This is because the upstream
-        // forwarding code in tetherOffloadRuleAdd cannot support rules on two upstreams at the
-        // same time. Deleting the rules first ensures that upstream forwarding is disabled on the
-        // old upstream when the last rule is removed from it, and re-enabled on the new upstream
-        // when the first rule is added to it.
-        // TODO: Once the IPv6 client processing code has moved from IpServer to BpfCoordinator, do
-        // something smarter.
-        final ArrayList<Ipv6ForwardingRule> rulesCopy = new ArrayList<>(rules.values());
-        for (final Ipv6ForwardingRule rule : rulesCopy) {
-            // Remove the old rule before adding the new one because the map uses the same key for
-            // both rules. Reversing the processing order causes that the new rule is removed as
-            // unexpected.
-            // TODO: Add new rule first to reduce the latency which has no rule.
-            tetherOffloadRuleRemove(ipServer, rule);
+        // Remove IPv6 upstream rules. Downstream rules must be removed first because
+        // BpfCoordinatorShimImpl#tetherOffloadGetAndClearStats will be called after the removal of
+        // the last upstream rule and it requires that no rules be forwarding traffic to or from
+        // that upstream.
+        clearIpv6UpstreamRules(ipServer);
+
+        // Add new upstream rules.
+        if (newUpstreamIfindex != 0 && interfaceParams != null && interfaceParams.macAddr != null) {
+            for (final IpPrefix ipPrefix : newUpstreamPrefixes) {
+                addIpv6UpstreamRule(ipServer, new Ipv6UpstreamRule(
+                        newUpstreamIfindex, interfaceParams.index, ipPrefix,
+                        interfaceParams.macAddr, NULL_MAC_ADDRESS, NULL_MAC_ADDRESS));
+            }
         }
-        for (final Ipv6ForwardingRule rule : rulesCopy) {
-            tetherOffloadRuleAdd(ipServer, rule.onNewUpstream(newUpstreamIfindex));
+
+        // Add updated downstream rules.
+        if (deletedDownstreamRules == null) return;
+        for (final Ipv6DownstreamRule rule : deletedDownstreamRules) {
+            addIpv6DownstreamRule(ipServer, rule.onNewUpstream(newUpstreamIfindex));
         }
     }
 
@@ -723,7 +777,7 @@
      * expects the interface name in NetworkStats object.
      * Note that this can be only called on handler thread.
      */
-    public void addUpstreamNameToLookupTable(int upstreamIfindex, @NonNull String upstreamIface) {
+    public void maybeAddUpstreamToLookupTable(int upstreamIfindex, @Nullable String upstreamIface) {
         if (!isUsingBpf()) return;
 
         if (upstreamIfindex == 0 || TextUtils.isEmpty(upstreamIface)) return;
@@ -800,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.
      */
@@ -993,7 +1182,7 @@
      * TODO: consider error handling if the attach program failed.
      */
     public void maybeAttachProgram(@NonNull String intIface, @NonNull String extIface) {
-        if (isVcnInterface(extIface)) return;
+        if (!isUsingBpf() || isVcnInterface(extIface)) return;
 
         if (forwardingPairExists(intIface, extIface)) return;
 
@@ -1017,6 +1206,8 @@
      * Detach BPF program
      */
     public void maybeDetachProgram(@NonNull String intIface, @NonNull String extIface) {
+        if (!isUsingBpf()) return;
+
         forwardingPairRemove(intIface, extIface);
 
         // Detaching program may fail because the interface has been removed already.
@@ -1048,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());
@@ -1139,14 +1330,14 @@
     private void dumpIpv6ForwardingRulesByDownstream(@NonNull IndentingPrintWriter pw) {
         pw.println("IPv6 Forwarding rules by downstream interface:");
         pw.increaseIndent();
-        if (mIpv6ForwardingRules.size() == 0) {
-            pw.println("No IPv6 rules");
+        if (mIpv6DownstreamRules.size() == 0) {
+            pw.println("No downstream IPv6 rules");
             pw.decreaseIndent();
             return;
         }
 
-        for (Map.Entry<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>> entry :
-                mIpv6ForwardingRules.entrySet()) {
+        for (Map.Entry<IpServer, LinkedHashMap<Inet6Address, Ipv6DownstreamRule>> entry :
+                mIpv6DownstreamRules.entrySet()) {
             IpServer ipServer = entry.getKey();
             // The rule downstream interface index is paired with the interface name from
             // IpServer#interfaceName. See #startIPv6, #updateIpv6ForwardingRules in IpServer.
@@ -1155,8 +1346,8 @@
                     + "[srcmac] [dstmac]");
 
             pw.increaseIndent();
-            LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = entry.getValue();
-            for (Ipv6ForwardingRule rule : rules.values()) {
+            LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules = entry.getValue();
+            for (Ipv6DownstreamRule rule : rules.values()) {
                 final int upstreamIfindex = rule.upstreamIfindex;
                 pw.println(String.format("%d(%s) %d(%s) %s [%s] [%s]", upstreamIfindex,
                         getIfName(upstreamIfindex), rule.downstreamIfindex,
@@ -1168,10 +1359,30 @@
         pw.decreaseIndent();
     }
 
+    /**
+     * Returns a /64 IpPrefix corresponding to the passed in byte array
+     *
+     * @param ip64 byte array to convert format
+     * @return the converted IpPrefix
+     */
+    @VisibleForTesting
+    public static IpPrefix bytesToPrefix(final byte[] ip64) {
+        IpPrefix sourcePrefix;
+        byte[] prefixBytes = Arrays.copyOf(ip64, IPV6_ADDR_LEN);
+        try {
+            sourcePrefix = new IpPrefix(InetAddress.getByAddress(prefixBytes), 64);
+        } catch (UnknownHostException e) {
+            // Cannot happen. InetAddress.getByAddress can only throw an exception if the byte array
+            // is the wrong length, but we allocate it with fixed length IPV6_ADDR_LEN.
+            throw new IllegalArgumentException("Invalid IPv6 address");
+        }
+        return sourcePrefix;
+    }
+
     private String ipv6UpstreamRuleToString(TetherUpstream6Key key, Tether6Value value) {
-        return String.format("%d(%s) [%s] -> %d(%s) %04x [%s] [%s]",
-                key.iif, getIfName(key.iif), key.dstMac, value.oif, getIfName(value.oif),
-                value.ethProto, value.ethSrcMac, value.ethDstMac);
+        return String.format("%d(%s) [%s] [%s] -> %d(%s) %04x [%s] [%s]",
+                key.iif, getIfName(key.iif), key.dstMac, bytesToPrefix(key.src64), value.oif,
+                getIfName(value.oif), value.ethProto, value.ethSrcMac, value.ethDstMac);
     }
 
     private void dumpIpv6UpstreamRules(IndentingPrintWriter pw) {
@@ -1221,8 +1432,8 @@
     // TODO: use dump utils with headerline and lambda which prints key and value to reduce
     // duplicate bpf map dump code.
     private void dumpBpfForwardingRulesIpv6(IndentingPrintWriter pw) {
-        pw.println("IPv6 Upstream: iif(iface) [inDstMac] -> oif(iface) etherType [outSrcMac] "
-                + "[outDstMac]");
+        pw.println("IPv6 Upstream: iif(iface) [inDstMac] [sourcePrefix] -> oif(iface) etherType "
+                + "[outSrcMac] [outDstMac]");
         pw.increaseIndent();
         dumpIpv6UpstreamRules(pw);
         pw.decreaseIndent();
@@ -1234,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.
@@ -1266,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;
@@ -1403,13 +1601,13 @@
         pw.decreaseIndent();
     }
 
-    /** IPv6 forwarding rule class. */
-    public static class Ipv6ForwardingRule {
-        // The upstream6 and downstream6 rules are built as the following tables. Only raw ip
-        // upstream interface is supported.
+    /** IPv6 upstream forwarding rule class. */
+    public static class Ipv6UpstreamRule {
+        // The upstream6 rules are built as the following tables. Only raw ip upstream interface is
+        // supported.
         // TODO: support ether ip upstream interface.
         //
-        // NAT network topology:
+        // Tethering network topology:
         //
         //         public network (rawip)                 private network
         //                   |                 UE                |
@@ -1419,15 +1617,15 @@
         //
         // upstream6 key and value:
         //
-        // +------+-------------+
-        // | TetherUpstream6Key |
-        // +------+------+------+
-        // |field |iif   |dstMac|
-        // |      |      |      |
-        // +------+------+------+
-        // |value |downst|downst|
-        // |      |ream  |ream  |
-        // +------+------+------+
+        // +------+-------------------+
+        // | TetherUpstream6Key       |
+        // +------+------+------+-----+
+        // |field |iif   |dstMac|src64|
+        // |      |      |      |     |
+        // +------+------+------+-----+
+        // |value |downst|downst|upstr|
+        // |      |ream  |ream  |eam  |
+        // +------+------+------+-----+
         //
         // +------+----------------------------------+
         // |      |Tether6Value                      |
@@ -1439,6 +1637,89 @@
         // |      |am    |      |      |IP    |      |
         // +------+------+------+------+------+------+
         //
+        public final int upstreamIfindex;
+        public final int downstreamIfindex;
+        @NonNull
+        public final IpPrefix sourcePrefix;
+        @NonNull
+        public final MacAddress inDstMac;
+        @NonNull
+        public final MacAddress outSrcMac;
+        @NonNull
+        public final MacAddress outDstMac;
+
+        public Ipv6UpstreamRule(int upstreamIfindex, int downstreamIfindex,
+                @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac,
+                @NonNull MacAddress outSrcMac, @NonNull MacAddress outDstMac) {
+            this.upstreamIfindex = upstreamIfindex;
+            this.downstreamIfindex = downstreamIfindex;
+            this.sourcePrefix = sourcePrefix;
+            this.inDstMac = inDstMac;
+            this.outSrcMac = outSrcMac;
+            this.outDstMac = outDstMac;
+        }
+
+        /**
+         * Return a TetherUpstream6Key object built from the rule.
+         */
+        @NonNull
+        public TetherUpstream6Key makeTetherUpstream6Key() {
+            final byte[] prefix64 = Arrays.copyOf(sourcePrefix.getRawAddress(), 8);
+            return new TetherUpstream6Key(downstreamIfindex, inDstMac, prefix64);
+        }
+
+        /**
+         * Return a Tether6Value object built from the rule.
+         */
+        @NonNull
+        public Tether6Value makeTether6Value() {
+            return new Tether6Value(upstreamIfindex, outDstMac, outSrcMac, ETH_P_IPV6,
+                    NetworkStackConstants.ETHER_MTU);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof Ipv6UpstreamRule)) return false;
+            Ipv6UpstreamRule that = (Ipv6UpstreamRule) o;
+            return this.upstreamIfindex == that.upstreamIfindex
+                    && this.downstreamIfindex == that.downstreamIfindex
+                    && Objects.equals(this.sourcePrefix, that.sourcePrefix)
+                    && Objects.equals(this.inDstMac, that.inDstMac)
+                    && Objects.equals(this.outSrcMac, that.outSrcMac)
+                    && Objects.equals(this.outDstMac, that.outDstMac);
+        }
+
+        @Override
+        public int hashCode() {
+            return 13 * upstreamIfindex + 41 * downstreamIfindex
+                    + Objects.hash(sourcePrefix, inDstMac, outSrcMac, outDstMac);
+        }
+
+        @Override
+        public String toString() {
+            return "upstreamIfindex: " + upstreamIfindex
+                    + ", downstreamIfindex: " + downstreamIfindex
+                    + ", sourcePrefix: " + sourcePrefix
+                    + ", inDstMac: " + inDstMac
+                    + ", outSrcMac: " + outSrcMac
+                    + ", outDstMac: " + outDstMac;
+        }
+    }
+
+    /** IPv6 downstream forwarding rule class. */
+    public static class Ipv6DownstreamRule {
+        // The downstream6 rules are built as the following tables. Only raw ip upstream interface
+        // is supported.
+        // TODO: support ether ip upstream interface.
+        //
+        // Tethering network topology:
+        //
+        //         public network (rawip)                 private network
+        //                   |                 UE                |
+        // +------------+    V    +------------+------------+    V    +------------+
+        // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
+        // +------------+         +------------+------------+         +------------+
+        //
         // downstream6 key and value:
         //
         // +------+--------------------+
@@ -1472,11 +1753,11 @@
         @NonNull
         public final MacAddress dstMac;
 
-        public Ipv6ForwardingRule(int upstreamIfindex, int downstreamIfIndex,
+        public Ipv6DownstreamRule(int upstreamIfindex, int downstreamIfindex,
                 @NonNull Inet6Address address, @NonNull MacAddress srcMac,
                 @NonNull MacAddress dstMac) {
             this.upstreamIfindex = upstreamIfindex;
-            this.downstreamIfindex = downstreamIfIndex;
+            this.downstreamIfindex = downstreamIfindex;
             this.address = address;
             this.srcMac = srcMac;
             this.dstMac = dstMac;
@@ -1484,8 +1765,8 @@
 
         /** Return a new rule object which updates with new upstream index. */
         @NonNull
-        public Ipv6ForwardingRule onNewUpstream(int newUpstreamIfindex) {
-            return new Ipv6ForwardingRule(newUpstreamIfindex, downstreamIfindex, address, srcMac,
+        public Ipv6DownstreamRule onNewUpstream(int newUpstreamIfindex) {
+            return new Ipv6DownstreamRule(newUpstreamIfindex, downstreamIfindex, address, srcMac,
                     dstMac);
         }
 
@@ -1525,8 +1806,8 @@
 
         @Override
         public boolean equals(Object o) {
-            if (!(o instanceof Ipv6ForwardingRule)) return false;
-            Ipv6ForwardingRule that = (Ipv6ForwardingRule) o;
+            if (!(o instanceof Ipv6DownstreamRule)) return false;
+            Ipv6DownstreamRule that = (Ipv6DownstreamRule) o;
             return this.upstreamIfindex == that.upstreamIfindex
                     && this.downstreamIfindex == that.downstreamIfindex
                     && Objects.equals(this.address, that.address)
@@ -1536,9 +1817,8 @@
 
         @Override
         public int hashCode() {
-            // TODO: if this is ever used in production code, don't pass ifindices
-            // to Objects.hash() to avoid autoboxing overhead.
-            return Objects.hash(upstreamIfindex, downstreamIfindex, address, srcMac, dstMac);
+            return 13 * upstreamIfindex + 41 * downstreamIfindex
+                    + Objects.hash(address, srcMac, dstMac);
         }
 
         @Override
@@ -1861,15 +2141,23 @@
         }
     }
 
+    @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 */;
     }
 
     private int getInterfaceIndexFromRules(@NonNull String ifName) {
-        for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
-                .values()) {
-            for (Ipv6ForwardingRule rule : rules.values()) {
+        for (ArraySet<Ipv6UpstreamRule> rules : mIpv6UpstreamRules.values()) {
+            for (Ipv6UpstreamRule rule : rules) {
                 final int upstreamIfindex = rule.upstreamIfindex;
                 if (TextUtils.equals(ifName, mInterfaceNames.get(upstreamIfindex))) {
                     return upstreamIfindex;
@@ -1887,6 +2175,7 @@
     }
 
     private boolean sendDataLimitToBpfMap(int ifIndex, long quotaBytes) {
+        if (!isUsingBpf()) return false;
         if (ifIndex == 0) {
             Log.wtf(TAG, "Invalid interface index.");
             return false;
@@ -1960,28 +2249,14 @@
     // TODO: Rename to isAnyIpv6RuleOnUpstream and define an isAnyRuleOnUpstream method that called
     // both isAnyIpv6RuleOnUpstream and mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream.
     private boolean isAnyRuleOnUpstream(int upstreamIfindex) {
-        for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
-                .values()) {
-            for (Ipv6ForwardingRule rule : rules.values()) {
+        for (ArraySet<Ipv6UpstreamRule> rules : mIpv6UpstreamRules.values()) {
+            for (Ipv6UpstreamRule rule : rules) {
                 if (upstreamIfindex == rule.upstreamIfindex) return true;
             }
         }
         return false;
     }
 
-    private boolean isAnyRuleFromDownstreamToUpstream(int downstreamIfindex, int upstreamIfindex) {
-        for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
-                .values()) {
-            for (Ipv6ForwardingRule rule : rules.values()) {
-                if (downstreamIfindex == rule.downstreamIfindex
-                        && upstreamIfindex == rule.upstreamIfindex) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
     // TODO: remove the index from map while the interface has been removed because the map size
     // is 64 entries. See packages\modules\Connectivity\Tethering\bpf_progs\offload.c.
     private void maybeAddDevMap(int upstreamIfindex, int downstreamIfindex) {
@@ -2202,9 +2477,7 @@
         });
     }
 
-    private void maybeSchedulePollingStats() {
-        if (!mPollingStarted) return;
-
+    private void schedulePollingStats() {
         if (mHandler.hasCallbacks(mScheduledPollingStats)) {
             mHandler.removeCallbacks(mScheduledPollingStats);
         }
@@ -2212,9 +2485,7 @@
         mHandler.postDelayed(mScheduledPollingStats, getPollingInterval());
     }
 
-    private void maybeScheduleConntrackTimeoutUpdate() {
-        if (!mPollingStarted) return;
-
+    private void scheduleConntrackTimeoutUpdate() {
         if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) {
             mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate);
         }
@@ -2223,13 +2494,13 @@
                 CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
     }
 
-    // Return forwarding rule map. This is used for testing only.
+    // Return IPv6 downstream forwarding rule map. This is used for testing only.
     // Note that this can be only called on handler thread.
     @NonNull
     @VisibleForTesting
-    final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>>
-            getForwardingRulesForTesting() {
-        return mIpv6ForwardingRules;
+    final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6DownstreamRule>>
+            getIpv6DownstreamRulesForTesting() {
+        return mIpv6DownstreamRules;
     }
 
     // Return upstream interface name map. This is used for testing only.
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
index e7dc757..9ef0f45 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
@@ -19,18 +19,21 @@
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_AIDL;
 
 import android.annotation.NonNull;
+import android.annotation.TargetApi;
 import android.hardware.tetheroffload.ForwardedStats;
 import android.hardware.tetheroffload.IOffload;
 import android.hardware.tetheroffload.ITetheringOffloadCallback;
 import android.hardware.tetheroffload.NatTimeoutUpdate;
 import android.hardware.tetheroffload.NetworkProtocol;
 import android.hardware.tetheroffload.OffloadCallbackEvent;
+import android.os.Build;
 import android.os.Handler;
 import android.os.NativeHandle;
 import android.os.ParcelFileDescriptor;
 import android.os.ServiceManager;
 import android.system.OsConstants;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.tethering.OffloadHardwareInterface.OffloadHalCallback;
@@ -40,6 +43,7 @@
 /**
  * The implementation of IOffloadHal which based on Stable AIDL interface
  */
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
 public class OffloadHalAidlImpl implements IOffloadHal {
     private static final String TAG = OffloadHalAidlImpl.class.getSimpleName();
     private static final String HAL_INSTANCE_NAME = IOffload.DESCRIPTOR + "/default";
@@ -52,6 +56,7 @@
 
     private TetheringOffloadCallback mTetheringOffloadCallback;
 
+    @VisibleForTesting
     public OffloadHalAidlImpl(int version, @NonNull IOffload offload, @NonNull Handler handler,
             @NonNull SharedLog log) {
         mOffloadVersion = version;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
index e0a9878..71922f9 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
@@ -74,10 +74,7 @@
      */
     public boolean initOffload(@NonNull NativeHandle handle1, @NonNull NativeHandle handle2,
             @NonNull OffloadHalCallback callback) {
-        final String logmsg = String.format("initOffload(%d, %d, %s)",
-                handle1.getFileDescriptor().getInt$(), handle2.getFileDescriptor().getInt$(),
-                (callback == null) ? "null"
-                : "0x" + Integer.toHexString(System.identityHashCode(callback)));
+        final String logmsg = "initOffload()";
 
         mOffloadHalCallback = callback;
         mTetheringOffloadCallback = new TetheringOffloadCallback(
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index de15c5b..13a7a22 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -16,6 +16,7 @@
 
 package com.android.networkstack.tethering;
 
+import static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_RECV_BUFSIZE;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
 
@@ -198,7 +199,8 @@
         public NativeHandle createConntrackSocket(final int groups) {
             final FileDescriptor fd;
             try {
-                fd = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_NETFILTER);
+                fd = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_NETFILTER,
+                        SOCKET_RECV_BUFSIZE);
             } catch (ErrnoException e) {
                 mLog.e("Unable to create conntrack socket " + e);
                 return null;
@@ -283,13 +285,16 @@
 
     private int initWithHandles(NativeHandle h1, NativeHandle h2) {
         if (h1 == null || h2 == null) {
+            // Set mIOffload to null has two purposes:
+            // 1. NativeHandles can be closed after initWithHandles() fails
+            // 2. Prevent mIOffload.stopOffload() to be called in stopOffload()
+            mIOffload = null;
             mLog.e("Failed to create socket.");
             return OFFLOAD_HAL_VERSION_NONE;
         }
 
         requestSocketDump(h1);
         if (!mIOffload.initOffload(h1, h2, mOffloadHalCallback)) {
-            mIOffload.stopOffload();
             mLog.e("Failed to initialize offload.");
             return OFFLOAD_HAL_VERSION_NONE;
         }
@@ -329,9 +334,9 @@
         mOffloadHalCallback = offloadCb;
         final int version = initWithHandles(h1, h2);
 
-        // Explicitly close FDs for HIDL. AIDL will pass the original FDs to the service,
-        // they shouldn't be closed here.
-        if (version < OFFLOAD_HAL_VERSION_AIDL) {
+        // Explicitly close FDs for HIDL or when mIOffload is null (cleared in initWithHandles).
+        // AIDL will pass the original FDs to the service, they shouldn't be closed here.
+        if (mIOffload == null || mIOffload.getVersion() < OFFLOAD_HAL_VERSION_AIDL) {
             maybeCloseFdInNativeHandles(h1, h2);
         }
         return version;
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index 6c0ca82..528991f 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -79,6 +79,7 @@
     private final TetheringConfiguration mConfig;
     // keyed by downstream type(TetheringManager.TETHERING_*).
     private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
+    private final Random mRandom;
 
     public PrivateAddressCoordinator(Context context, TetheringConfiguration config) {
         mDownstreams = new ArraySet<>();
@@ -95,6 +96,7 @@
 
         mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16"),
             new IpPrefix("172.16.0.0/12"), new IpPrefix("10.0.0.0/8")));
+        mRandom = new Random();
     }
 
     /**
@@ -187,7 +189,10 @@
             return cachedAddress;
         }
 
-        for (IpPrefix prefixRange : mTetheringPrefixes) {
+        final int prefixIndex = getStartedPrefixIndex();
+        for (int i = 0; i < mTetheringPrefixes.size(); i++) {
+            final IpPrefix prefixRange = mTetheringPrefixes.get(
+                    (prefixIndex + i) % mTetheringPrefixes.size());
             final LinkAddress newAddress = chooseDownstreamAddress(prefixRange);
             if (newAddress != null) {
                 mDownstreams.add(ipServer);
@@ -200,6 +205,28 @@
         return null;
     }
 
+    private int getStartedPrefixIndex() {
+        if (!mConfig.isRandomPrefixBaseEnabled()) return 0;
+
+        final int random = getRandomInt() & 0xffffff;
+        // This is to select the starting prefix range (/8, /12, or /16) instead of the actual
+        // LinkAddress. To avoid complex operations in the selection logic and make the selected
+        // rate approximate consistency with that /8 is around 2^4 times of /12 and /12 is around
+        // 2^4 times of /16, we simply define a map between the value and the prefix value like
+        // this:
+        //
+        // Value 0 ~ 0xffff (65536/16777216 = 0.39%) -> 192.168.0.0/16
+        // Value 0x10000 ~ 0xfffff (983040/16777216 = 5.86%) -> 172.16.0.0/12
+        // Value 0x100000 ~ 0xffffff (15728640/16777216 = 93.7%) -> 10.0.0.0/8
+        if (random > 0xfffff) {
+            return 2;
+        } else if (random > 0xffff) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
     private int getPrefixBaseAddress(final IpPrefix prefix) {
         return inet4AddressToIntHTH((Inet4Address) prefix.getAddress());
     }
@@ -263,12 +290,13 @@
         // is less than 127.0.0.0 = 0x7f000000 = 2130706432.
         //
         // Additionally, it makes debug output easier to read by making the numbers smaller.
-        final int randomPrefixStart = getRandomInt() & ~prefixRangeMask & prefixMask;
+        final int randomInt = getRandomInt();
+        final int randomPrefixStart = randomInt & ~prefixRangeMask & prefixMask;
 
         // A random offset within the prefix. Used to determine the local address once the prefix
         // is selected. It does not result in an IPv4 address ending in .0, .1, or .255
-        // For a PREFIX_LENGTH of 255, this is a number between 2 and 254.
-        final int subAddress = getSanitizedSubAddr(~prefixMask);
+        // For a PREFIX_LENGTH of 24, this is a number between 2 and 254.
+        final int subAddress = getSanitizedSubAddr(randomInt, ~prefixMask);
 
         // Find a prefix length PREFIX_LENGTH between randomPrefixStart and the end of the block,
         // such that the prefix does not conflict with any upstream.
@@ -310,12 +338,12 @@
     /** Get random int which could be used to generate random address. */
     @VisibleForTesting
     public int getRandomInt() {
-        return (new Random()).nextInt();
+        return mRandom.nextInt();
     }
 
     /** Get random subAddress and avoid selecting x.x.x.0, x.x.x.1 and x.x.x.255 address. */
-    private int getSanitizedSubAddr(final int subAddrMask) {
-        final int randomSubAddr = getRandomInt() & subAddrMask;
+    private int getSanitizedSubAddr(final int randomInt, final int subAddrMask) {
+        final int randomSubAddr = randomInt & subAddrMask;
         // If prefix length > 30, the selecting speace would be less than 4 which may be hard to
         // avoid 3 consecutive address.
         if (PREFIX_LENGTH > 30) return randomSubAddr;
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
index 5893885..0cc3dd9 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
@@ -29,13 +29,17 @@
     @Field(order = 0, type = Type.S32)
     public final int iif; // The input interface index.
 
-    @Field(order = 1, type = Type.EUI48, padding = 2)
+    @Field(order = 1, type = Type.EUI48, padding = 6)
     public final MacAddress dstMac; // Destination ethernet mac address (zeroed iff rawip ingress).
 
-    public TetherUpstream6Key(int iif, @NonNull final MacAddress dstMac) {
+    @Field(order = 2, type = Type.ByteArray, arraysize = 8)
+    public final byte[] src64; // The top 64-bits of the source ip.
+
+    public TetherUpstream6Key(int iif, @NonNull final MacAddress dstMac, final byte[] src64) {
         Objects.requireNonNull(dstMac);
 
         this.iif = iif;
         this.dstMac = dstMac;
+        this.src64 = src64;
     }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index e5f644e..6c6191f 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;
@@ -135,7 +136,9 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
 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.RoutingCoordinatorManager;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
@@ -157,12 +160,10 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
 
 /**
  *
@@ -172,8 +173,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
@@ -240,15 +241,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;
+    private final RoutingCoordinatorManager mRoutingCoordinator;
     private final UserRestrictionActionListener mTetheringRestriction;
     private final ActiveDataSubIdListener mActiveDataSubIdListener;
     private final ConnectedClientsTracker mConnectedClientsTracker;
@@ -266,8 +265,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;
@@ -278,6 +275,7 @@
     private TetheredInterfaceRequestShim mBluetoothIfaceRequest;
     private String mConfiguredEthernetIface;
     private String mConfiguredBluetoothIface;
+    private String mConfiguredVirtualIface;
     private EthernetCallback mEthernetCallback;
     private TetheredInterfaceCallbackShim mBluetoothCallback;
     private SettingsObserver mSettingsObserver;
@@ -294,10 +292,11 @@
         mLog.mark("Tethering.constructed");
         mDeps = deps;
         mContext = mDeps.getContext();
-        mNetd = mDeps.getINetd(mContext);
-        mLooper = mDeps.getTetheringLooper();
-        mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
-        mTetheringMetrics = mDeps.getTetheringMetrics();
+        mNetd = mDeps.getINetd(mContext, mLog);
+        mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext, mLog);
+        mLooper = mDeps.makeTetheringLooper();
+        mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper);
+        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
@@ -311,7 +310,7 @@
         mTetherMainSM.start();
 
         mHandler = mTetherMainSM.getHandler();
-        mOffloadController = mDeps.getOffloadController(mHandler, mLog,
+        mOffloadController = mDeps.makeOffloadController(mHandler, mLog,
                 new OffloadController.Dependencies() {
 
                     @Override
@@ -319,15 +318,16 @@
                         return mConfig;
                     }
                 });
-        mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMainSM, mLog,
-                TetherMainSM.EVENT_UPSTREAM_CALLBACK);
-        mForwardedDownstreams = new HashSet<>();
+        mUpstreamNetworkMonitor = mDeps.makeUpstreamNetworkMonitor(mContext, mHandler, mLog,
+                (what, obj) -> {
+                    mTetherMainSM.sendMessage(TetherMainSM.EVENT_UPSTREAM_CALLBACK, what, 0, obj);
+                });
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
         // EntitlementManager will send EVENT_UPSTREAM_PERMISSION_CHANGED when cellular upstream
         // permission is changed according to entitlement check result.
-        mEntitlementMgr = mDeps.getEntitlementManager(mContext, mHandler, mLog,
+        mEntitlementMgr = mDeps.makeEntitlementManager(mContext, mHandler, mLog,
                 () -> mTetherMainSM.sendMessage(
                 TetherMainSM.EVENT_UPSTREAM_PERMISSION_CHANGED));
         mEntitlementMgr.setOnTetherProvisioningFailedListener((downstream, reason) -> {
@@ -360,14 +360,15 @@
 
         // Load tethering configuration.
         updateConfiguration();
+        mConfig.readEnableSyncSM(mContext);
         // It is OK for the configuration to be passed to the PrivateAddressCoordinator at
         // construction time because the only part of the configuration it uses is
         // shouldEnableWifiP2pDedicatedIp(), and currently do not support changing that.
-        mPrivateAddressCoordinator = mDeps.getPrivateAddressCoordinator(mContext, mConfig);
+        mPrivateAddressCoordinator = mDeps.makePrivateAddressCoordinator(mContext, mConfig);
 
         // Must be initialized after tethering configuration is loaded because BpfCoordinator
         // constructor needs to use the configuration.
-        mBpfCoordinator = mDeps.getBpfCoordinator(
+        mBpfCoordinator = mDeps.makeBpfCoordinator(
                 new BpfCoordinator.Dependencies() {
                     @NonNull
                     public Handler getHandler() {
@@ -396,7 +397,7 @@
                 });
 
         if (SdkLevel.isAtLeastT() && mConfig.isWearTetheringEnabled()) {
-            mWearableConnectionManager = mDeps.getWearableConnectionManager(mContext);
+            mWearableConnectionManager = mDeps.makeWearableConnectionManager(mContext);
         } else {
             mWearableConnectionManager = null;
         }
@@ -716,6 +717,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;
@@ -754,7 +758,6 @@
             }
             if ((enable && mgr.startTetheredHotspot(null /* use existing softap config */))
                     || (!enable && mgr.stopSoftAp())) {
-                mWifiTetherRequested = enable;
                 return TETHER_ERROR_NO_ERROR;
             }
         } finally {
@@ -865,7 +868,7 @@
 
     private void changeBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
             final boolean enable) {
-        final BluetoothPanShim panShim = mDeps.getBluetoothPanShim(bluetoothPan);
+        final BluetoothPanShim panShim = mDeps.makeBluetoothPanShim(bluetoothPan);
         if (enable) {
             if (mBluetoothIfaceRequest != null) {
                 Log.d(TAG, "Bluetooth tethering settings already enabled");
@@ -970,6 +973,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 {
@@ -1461,10 +1479,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);
@@ -1496,8 +1510,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:
@@ -1644,11 +1657,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) {
@@ -1679,6 +1687,8 @@
         static final int EVENT_IFACE_UPDATE_LINKPROPERTIES      = BASE_MAIN_SM + 7;
         // Events from EntitlementManager to choose upstream again.
         static final int EVENT_UPSTREAM_PERMISSION_CHANGED      = BASE_MAIN_SM + 8;
+        // Internal request from IpServer to enable or disable downstream.
+        static final int EVENT_REQUEST_CHANGE_DOWNSTREAM        = BASE_MAIN_SM + 9;
         private final State mInitialState;
         private final State mTetherModeAliveState;
 
@@ -1704,12 +1714,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();
@@ -1727,7 +1741,7 @@
             addState(mSetDnsForwardersErrorState);
 
             mNotifyList = new ArrayList<>();
-            mIPv6TetheringCoordinator = deps.getIPv6TetheringCoordinator(mNotifyList, mLog);
+            mIPv6TetheringCoordinator = deps.makeIPv6TetheringCoordinator(mNotifyList, mLog);
             mOffload = new OffloadWrapper();
 
             setInitialState(mInitialState);
@@ -1845,11 +1859,12 @@
 
             setUpstreamNetwork(ns);
             final Network newUpstream = (ns != null) ? ns.network : null;
-            if (mTetherUpstream != newUpstream) {
+            if (!Objects.equals(mTetherUpstream, newUpstream)) {
                 mTetherUpstream = newUpstream;
                 reportUpstreamChanged(mTetherUpstream);
-                // Need to notify capabilities change after upstream network changed because new
-                // network's capabilities should be checked every time.
+                // Need to notify capabilities change after upstream network changed because
+                // upstream may switch to existing network which don't have
+                // UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES callback.
                 mNotificationUpdater.onUpstreamCapabilitiesChanged(
                         (ns != null) ? ns.networkCapabilities : null);
             }
@@ -2044,6 +2059,10 @@
             }
         }
 
+        private boolean upstreamWanted() {
+            return !mForwardedDownstreams.isEmpty();
+        }
+
         class TetherModeAliveState extends State {
             boolean mUpstreamWanted = false;
             boolean mTryCell = true;
@@ -2066,9 +2085,6 @@
                     chooseUpstreamType(true);
                     mTryCell = false;
                 }
-
-                // TODO: Check the upstream interface if it is managed by BPF offload.
-                mBpfCoordinator.startPolling();
             }
 
             @Override
@@ -2082,7 +2098,6 @@
                     reportUpstreamChanged(null);
                     mNotificationUpdater.onUpstreamCapabilitiesChanged(null);
                 }
-                mBpfCoordinator.stopPolling();
                 mTetheringMetrics.cleanup();
             }
 
@@ -2177,6 +2192,12 @@
                         }
                         break;
                     }
+                    case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
+                        final int tetheringType = message.arg1;
+                        final Boolean enabled = (Boolean) message.obj;
+                        enableTetheringInternal(tetheringType, enabled, null);
+                        break;
+                    }
                     default:
                         retValue = false;
                         break;
@@ -2412,9 +2433,6 @@
 
     /** Unregister tethering event callback */
     void unregisterTetheringEventCallback(ITetheringEventCallback callback) {
-        if (callback == null) {
-            throw new NullPointerException();
-        }
         mHandler.post(() -> {
             mTetheringEventCallbacks.unregister(callback);
         });
@@ -2633,7 +2651,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();
 
@@ -2675,31 +2693,10 @@
             return;
         }
 
-        final CountDownLatch latch = new CountDownLatch(1);
-
-        // Don't crash the system if something in doDump throws an exception, but try to propagate
-        // the exception to the caller.
-        AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
-        mHandler.post(() -> {
-            try {
-                doDump(fd, writer, args);
-            } catch (RuntimeException e) {
-                exceptionRef.set(e);
-            }
-            latch.countDown();
-        });
-
-        try {
-            if (!latch.await(DUMP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
-                return;
-            }
-        } catch (InterruptedException e) {
-            exceptionRef.compareAndSet(null, new IllegalStateException("Dump interrupted", e));
+        if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, writer, args),
+                DUMP_TIMEOUT_MS)) {
+            writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
         }
-
-        final RuntimeException e = exceptionRef.get();
-        if (e != null) throw e;
     }
 
     private void maybeDhcpLeasesChanged() {
@@ -2715,83 +2712,73 @@
         }
     }
 
-    private IpServer.Callback makeControlCallback() {
-        return new IpServer.Callback() {
-            @Override
-            public void updateInterfaceState(IpServer who, int state, int lastError) {
-                notifyInterfaceStateChange(who, state, lastError);
+    private class ControlCallback extends IpServer.Callback {
+        @Override
+        public void updateInterfaceState(IpServer who, int state, int lastError) {
+            final String iface = who.interfaceName();
+            final TetherState tetherState = mTetherStates.get(iface);
+            if (tetherState != null && tetherState.ipServer.equals(who)) {
+                tetherState.lastState = state;
+                tetherState.lastError = lastError;
+            } else {
+                if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
             }
 
-            @Override
-            public void updateLinkProperties(IpServer who, LinkProperties newLp) {
-                notifyLinkPropertiesChanged(who, newLp);
-            }
+            mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, lastError));
 
-            @Override
-            public void dhcpLeasesChanged() {
-                maybeDhcpLeasesChanged();
+            // If TetherMainSM is in ErrorState, TetherMainSM stays there.
+            // Thus we give a chance for TetherMainSM to recover to InitialState
+            // by sending CMD_CLEAR_ERROR
+            if (lastError == TETHER_ERROR_INTERNAL_ERROR) {
+                mTetherMainSM.sendMessage(TetherMainSM.CMD_CLEAR_ERROR, who);
             }
-
-            @Override
-            public void requestEnableTethering(int tetheringType, boolean enabled) {
-                enableTetheringInternal(tetheringType, enabled, null);
+            int which;
+            switch (state) {
+                case IpServer.STATE_UNAVAILABLE:
+                case IpServer.STATE_AVAILABLE:
+                    which = TetherMainSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
+                    break;
+                case IpServer.STATE_TETHERED:
+                case IpServer.STATE_LOCAL_ONLY:
+                    which = TetherMainSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
+                    break;
+                default:
+                    Log.wtf(TAG, "Unknown interface state: " + state);
+                    return;
             }
-        };
-    }
-
-    // TODO: Move into TetherMainSM.
-    private void notifyInterfaceStateChange(IpServer who, int state, int error) {
-        final String iface = who.interfaceName();
-        final TetherState tetherState = mTetherStates.get(iface);
-        if (tetherState != null && tetherState.ipServer.equals(who)) {
-            tetherState.lastState = state;
-            tetherState.lastError = error;
-        } else {
-            if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
+            mTetherMainSM.sendMessage(which, state, 0, who);
+            sendTetherStateChangedBroadcast();
         }
 
-        mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, error));
-
-        // If TetherMainSM is in ErrorState, TetherMainSM stays there.
-        // Thus we give a chance for TetherMainSM to recover to InitialState
-        // by sending CMD_CLEAR_ERROR
-        if (error == TETHER_ERROR_INTERNAL_ERROR) {
-            mTetherMainSM.sendMessage(TetherMainSM.CMD_CLEAR_ERROR, who);
-        }
-        int which;
-        switch (state) {
-            case IpServer.STATE_UNAVAILABLE:
-            case IpServer.STATE_AVAILABLE:
-                which = TetherMainSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
-                break;
-            case IpServer.STATE_TETHERED:
-            case IpServer.STATE_LOCAL_ONLY:
-                which = TetherMainSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
-                break;
-            default:
-                Log.wtf(TAG, "Unknown interface state: " + state);
+        @Override
+        public void updateLinkProperties(IpServer who, LinkProperties newLp) {
+            final String iface = who.interfaceName();
+            final int state;
+            final TetherState tetherState = mTetherStates.get(iface);
+            if (tetherState != null && tetherState.ipServer.equals(who)) {
+                state = tetherState.lastState;
+            } else {
+                mLog.log("got notification from stale iface " + iface);
                 return;
-        }
-        mTetherMainSM.sendMessage(which, state, 0, who);
-        sendTetherStateChangedBroadcast();
-    }
+            }
 
-    private void notifyLinkPropertiesChanged(IpServer who, LinkProperties newLp) {
-        final String iface = who.interfaceName();
-        final int state;
-        final TetherState tetherState = mTetherStates.get(iface);
-        if (tetherState != null && tetherState.ipServer.equals(who)) {
-            state = tetherState.lastState;
-        } else {
-            mLog.log("got notification from stale iface " + iface);
-            return;
+            mLog.log(String.format(
+                    "OBSERVED LinkProperties update iface=%s state=%s lp=%s",
+                    iface, IpServer.getStateString(state), newLp));
+            final int which = TetherMainSM.EVENT_IFACE_UPDATE_LINKPROPERTIES;
+            mTetherMainSM.sendMessage(which, state, 0, newLp);
         }
 
-        mLog.log(String.format(
-                "OBSERVED LinkProperties update iface=%s state=%s lp=%s",
-                iface, IpServer.getStateString(state), newLp));
-        final int which = TetherMainSM.EVENT_IFACE_UPDATE_LINKPROPERTIES;
-        mTetherMainSM.sendMessage(which, state, 0, newLp);
+        @Override
+        public void dhcpLeasesChanged() {
+            maybeDhcpLeasesChanged();
+        }
+
+        @Override
+        public void requestEnableTethering(int tetheringType, boolean enabled) {
+            mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
+                    tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
+        }
     }
 
     private boolean hasSystemFeature(final String feature) {
@@ -2832,9 +2819,10 @@
 
         mLog.i("adding IpServer for: " + iface);
         final TetherState tetherState = new TetherState(
-                new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
-                             makeControlCallback(), mConfig, mPrivateAddressCoordinator,
-                             mTetheringMetrics, mDeps.getIpServerDependencies()), isNcm);
+                new IpServer(iface, mHandler, interfaceType, mLog, mNetd, mBpfCoordinator,
+                        mRoutingCoordinator, new ControlCallback(), mConfig,
+                        mPrivateAddressCoordinator, mTetheringMetrics,
+                        mDeps.makeIpServerDependencies()), isNcm);
         mTetherStates.put(iface, tetherState);
         tetherState.ipServer.start();
     }
@@ -2860,4 +2848,9 @@
             } catch (RemoteException e) { }
         });
     }
+
+    @VisibleForTesting
+    public TetherMainSM getTetherMainSMForTesting() {
+        return mTetherMainSM;
+    }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index b0aa668..298940e 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -22,9 +22,7 @@
 import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
 import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 
-import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
 import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
 
 import android.content.ContentResolver;
@@ -112,8 +110,8 @@
      * config_tether_upstream_automatic when set to true.
      *
      * This flag is enabled if !=0 and less than the module APEX version: see
-     * {@link DeviceConfigUtils#isFeatureEnabled}. It is also ignored after R, as later devices
-     * should just set config_tether_upstream_automatic to true instead.
+     * {@link DeviceConfigUtils#isTetheringFeatureEnabled}. It is also ignored after R, as later
+     * devices should just set config_tether_upstream_automatic to true instead.
      */
     public static final String TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION =
             "tether_force_upstream_automatic_version";
@@ -132,12 +130,20 @@
     public static final String TETHER_ENABLE_WEAR_TETHERING =
             "tether_enable_wear_tethering";
 
+    public static final String TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION =
+            "tether_force_random_prefix_base_selection";
+
+    public static final String TETHER_ENABLE_SYNC_SM = "tether_enable_sync_sm";
+
     /**
      * Default value that used to periodic polls tether offload stats from tethering offload HAL
      * to make the data warnings work.
      */
     public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
 
+    /** A flag for using synchronous or asynchronous state machine. */
+    public static boolean USE_SYNC_SM = false;
+
     public final String[] tetherableUsbRegexs;
     public final String[] tetherableWifiRegexs;
     public final String[] tetherableWigigRegexs;
@@ -170,6 +176,7 @@
     private final int mP2pLeasesSubnetPrefixLength;
 
     private final boolean mEnableWearTethering;
+    private final boolean mRandomPrefixBase;
 
     private final int mUsbTetheringFunction;
     protected final ContentResolver mContentResolver;
@@ -179,16 +186,30 @@
      */
     @VisibleForTesting
     public static class Dependencies {
-        boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
-                @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) {
-            return DeviceConfigUtils.isFeatureEnabled(context, namespace, name,
-                    moduleName, defaultEnabled);
+        boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) {
+            return DeviceConfigUtils.isTetheringFeatureEnabled(context, name);
         }
 
         boolean getDeviceConfigBoolean(@NonNull String namespace, @NonNull String name,
                 boolean defaultValue) {
             return DeviceConfig.getBoolean(namespace, name, defaultValue);
         }
+
+        /**
+         * TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION is used to force enable the feature on specific
+         * R devices. Just checking the flag value is enough since the flag has been pushed to
+         * enable the feature on the old version and any new binary will always have a version
+         * number newer than the flag.
+         * This flag is wrongly configured in the connectivity namespace so this method reads the
+         * flag value from the connectivity namespace. But the tethering module should use the
+         * tethering namespace. This method can be removed after R EOL.
+         */
+        boolean isTetherForceUpstreamAutomaticFeatureEnabled() {
+            final int flagValue = DeviceConfigUtils.getDeviceConfigPropertyInt(
+                    NAMESPACE_CONNECTIVITY, TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION,
+                    0 /* defaultValue */);
+            return flagValue > 0;
+        }
     }
 
     public TetheringConfiguration(@NonNull Context ctx, @NonNull SharedLog log, int id) {
@@ -237,7 +258,7 @@
         // - S, T: can be enabled/disabled by resource config_tether_upstream_automatic.
         // - U+  : automatic mode only.
         final boolean forceAutomaticUpstream = SdkLevel.isAtLeastU() || (!SdkLevel.isAtLeastS()
-                && isConnectivityFeatureEnabled(ctx, TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION));
+                && mDeps.isTetherForceUpstreamAutomaticFeatureEnabled());
         chooseUpstreamAutomatically = forceAutomaticUpstream || getResourceBoolean(
                 res, R.bool.config_tether_upstream_automatic, false /** defaultValue */);
         preferredUpstreamIfaceTypes = getUpstreamIfaceTypes(res, isDunRequired);
@@ -273,6 +294,8 @@
 
         mEnableWearTethering = shouldEnableWearTethering(ctx);
 
+        mRandomPrefixBase = mDeps.isFeatureEnabled(ctx, TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION);
+
         configLog.log(toString());
     }
 
@@ -361,6 +384,20 @@
         return mEnableWearTethering;
     }
 
+    public boolean isRandomPrefixBaseEnabled() {
+        return mRandomPrefixBase;
+    }
+
+    /**
+     * Check whether sync SM is enabled then set it to USE_SYNC_SM. This should be called once
+     * when tethering is created. Otherwise if the flag is pushed while tethering is enabled,
+     * then it's possible for some IpServer(s) running the new sync state machine while others
+     * use the async state machine.
+     */
+    public void readEnableSyncSM(final Context ctx) {
+        USE_SYNC_SM = mDeps.isFeatureEnabled(ctx, TETHER_ENABLE_SYNC_SM);
+    }
+
     /** Does the dumping.*/
     public void dump(PrintWriter pw) {
         pw.print("activeDataSubId: ");
@@ -411,6 +448,12 @@
 
         pw.print("mUsbTetheringFunction: ");
         pw.println(isUsingNcm() ? "NCM" : "RNDIS");
+
+        pw.print("mRandomPrefixBase: ");
+        pw.println(mRandomPrefixBase);
+
+        pw.print("USE_SYNC_SM: ");
+        pw.println(USE_SYNC_SM);
     }
 
     /** Returns the string representation of this object.*/
@@ -607,32 +650,13 @@
 
     private boolean shouldEnableWearTethering(Context context) {
         return SdkLevel.isAtLeastT()
-            && isTetheringFeatureEnabled(context, TETHER_ENABLE_WEAR_TETHERING);
+            && mDeps.isFeatureEnabled(context, TETHER_ENABLE_WEAR_TETHERING);
     }
 
     private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
         return mDeps.getDeviceConfigBoolean(NAMESPACE_CONNECTIVITY, name, defaultValue);
     }
 
-    /**
-     * This is deprecated because connectivity namespace already be used for NetworkStack mainline
-     * module. Tethering should use its own namespace to roll out the feature flag.
-     * @deprecated new caller should use isTetheringFeatureEnabled instead.
-     */
-    @Deprecated
-    private boolean isConnectivityFeatureEnabled(Context ctx, String featureVersionFlag) {
-        return isFeatureEnabled(ctx, NAMESPACE_CONNECTIVITY, featureVersionFlag);
-    }
-
-    private boolean isTetheringFeatureEnabled(Context ctx, String featureVersionFlag) {
-        return isFeatureEnabled(ctx, NAMESPACE_TETHERING, featureVersionFlag);
-    }
-
-    private boolean isFeatureEnabled(Context ctx, String namespace, String featureVersionFlag) {
-        return mDeps.isFeatureEnabled(ctx, namespace, featureVersionFlag, TETHERING_MODULE_NAME,
-                false /* defaultEnabled */);
-    }
-
     private Resources getResources(Context ctx, int subId) {
         if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
             return getResourcesForSubIdWrapper(ctx, subId);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 741a5c5..5d9d349 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -16,11 +16,13 @@
 
 package com.android.networkstack.tethering;
 
+import android.annotation.Nullable;
 import android.app.usage.NetworkStatsManager;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothPan;
 import android.content.Context;
 import android.net.INetd;
+import android.net.connectivity.ConnectivityInternalApiUtil;
 import android.net.ip.IpServer;
 import android.os.Build;
 import android.os.Handler;
@@ -32,7 +34,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
-import com.android.internal.util.StateMachine;
+import com.android.modules.utils.build.SdkLevel;
+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;
@@ -49,58 +53,58 @@
  */
 public abstract class TetheringDependencies {
     /**
-     * Get a reference to the BpfCoordinator to be used by tethering.
+     * Make the BpfCoordinator to be used by tethering.
      */
-    public @NonNull BpfCoordinator getBpfCoordinator(
+    public @NonNull BpfCoordinator makeBpfCoordinator(
             @NonNull BpfCoordinator.Dependencies deps) {
         return new BpfCoordinator(deps);
     }
 
     /**
-     * Get a reference to the offload hardware interface to be used by tethering.
+     * Make the offload hardware interface to be used by tethering.
      */
-    public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
+    public OffloadHardwareInterface makeOffloadHardwareInterface(Handler h, SharedLog log) {
         return new OffloadHardwareInterface(h, log);
     }
 
     /**
-     * Get a reference to the offload controller to be used by tethering.
+     * Make the offload controller to be used by tethering.
      */
     @NonNull
-    public OffloadController getOffloadController(@NonNull Handler h,
+    public OffloadController makeOffloadController(@NonNull Handler h,
             @NonNull SharedLog log, @NonNull OffloadController.Dependencies deps) {
         final NetworkStatsManager statsManager =
                 (NetworkStatsManager) getContext().getSystemService(Context.NETWORK_STATS_SERVICE);
-        return new OffloadController(h, getOffloadHardwareInterface(h, log),
+        return new OffloadController(h, makeOffloadHardwareInterface(h, log),
                 getContext().getContentResolver(), statsManager, log, deps);
     }
 
 
     /**
-     * Get a reference to the UpstreamNetworkMonitor to be used by tethering.
+     * Make the UpstreamNetworkMonitor to be used by tethering.
      */
-    public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target,
-            SharedLog log, int what) {
-        return new UpstreamNetworkMonitor(ctx, target, log, what);
+    public UpstreamNetworkMonitor makeUpstreamNetworkMonitor(Context ctx, Handler h,
+            SharedLog log, UpstreamNetworkMonitor.EventListener listener) {
+        return new UpstreamNetworkMonitor(ctx, h, log, listener);
     }
 
     /**
-     * Get a reference to the IPv6TetheringCoordinator to be used by tethering.
+     * Make the IPv6TetheringCoordinator to be used by tethering.
      */
-    public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
+    public IPv6TetheringCoordinator makeIPv6TetheringCoordinator(
             ArrayList<IpServer> notifyList, SharedLog log) {
         return new IPv6TetheringCoordinator(notifyList, log);
     }
 
     /**
-     * Get dependencies to be used by IpServer.
+     * Make dependencies to be used by IpServer.
      */
-    public abstract IpServer.Dependencies getIpServerDependencies();
+    public abstract IpServer.Dependencies makeIpServerDependencies();
 
     /**
-     * Get a reference to the EntitlementManager to be used by tethering.
+     * Make the EntitlementManager to be used by tethering.
      */
-    public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log,
+    public EntitlementManager makeEntitlementManager(Context ctx, Handler h, SharedLog log,
             Runnable callback) {
         return new EntitlementManager(ctx, h, log, callback);
     }
@@ -116,32 +120,50 @@
     /**
      * 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 a reference to the TetheringNotificationUpdater to be used by tethering.
+     * Get the routing coordinator.
      */
-    public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx,
+    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);
+    }
+
+    /**
+     * Make the TetheringNotificationUpdater to be used by tethering.
+     */
+    public TetheringNotificationUpdater makeNotificationUpdater(@NonNull final Context ctx,
             @NonNull final Looper looper) {
         return new TetheringNotificationUpdater(ctx, looper);
     }
 
     /**
-     * Get tethering thread looper.
+     * Make tethering thread looper.
      */
-    public abstract Looper getTetheringLooper();
+    public abstract Looper makeTetheringLooper();
 
     /**
-     *  Get Context of TetheringSerice.
+     *  Get Context of TetheringService.
      */
     public abstract Context getContext();
 
     /**
      * Get a reference to BluetoothAdapter to be used by tethering.
      */
+    @Nullable
     public abstract BluetoothAdapter getBluetoothAdapter();
 
     /**
@@ -152,34 +174,34 @@
     }
 
     /**
-     * Get a reference to PrivateAddressCoordinator to be used by Tethering.
+     * Make PrivateAddressCoordinator to be used by Tethering.
      */
-    public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
+    public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx,
             TetheringConfiguration cfg) {
         return new PrivateAddressCoordinator(ctx, cfg);
     }
 
     /**
-     * Get BluetoothPanShim object to enable/disable bluetooth tethering.
+     * Make BluetoothPanShim object to enable/disable bluetooth tethering.
      *
      * TODO: use BluetoothPan directly when mainline module is built with API 32.
      */
-    public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
+    public BluetoothPanShim makeBluetoothPanShim(BluetoothPan pan) {
         return BluetoothPanShimImpl.newInstance(pan);
     }
 
     /**
-     * Get a reference to the TetheringMetrics to be used by tethering.
+     * Make the TetheringMetrics to be used by tethering.
      */
-    public TetheringMetrics getTetheringMetrics() {
-        return new TetheringMetrics();
+    public TetheringMetrics makeTetheringMetrics(Context ctx) {
+        return new TetheringMetrics(ctx);
     }
 
     /**
      * Returns the implementation of WearableConnectionManager.
      */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public WearableConnectionManager getWearableConnectionManager(Context ctx) {
+    public WearableConnectionManager makeWearableConnectionManager(Context ctx) {
         return new WearableConnectionManager(ctx);
     }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 96ddfa0..93b3508 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;
@@ -162,6 +163,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 +177,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);
@@ -322,7 +327,7 @@
     public TetheringDependencies makeTetheringDependencies() {
         return new TetheringDependencies() {
             @Override
-            public Looper getTetheringLooper() {
+            public Looper makeTetheringLooper() {
                 final HandlerThread tetherThread = new HandlerThread("android.tethering");
                 tetherThread.start();
                 return tetherThread.getLooper();
@@ -334,7 +339,7 @@
             }
 
             @Override
-            public IpServer.Dependencies getIpServerDependencies() {
+            public IpServer.Dependencies makeIpServerDependencies() {
                 return new IpServer.Dependencies() {
                     @Override
                     public void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
@@ -377,7 +382,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/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index ac2aa7b..7a05d74 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -44,7 +44,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.StateMachine;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
@@ -111,9 +110,8 @@
 
     private final Context mContext;
     private final SharedLog mLog;
-    private final StateMachine mTarget;
     private final Handler mHandler;
-    private final int mWhat;
+    private final EventListener mEventListener;
     private final HashMap<Network, UpstreamNetworkState> mNetworkMap = new HashMap<>();
     private HashSet<IpPrefix> mLocalPrefixes;
     private ConnectivityManager mCM;
@@ -135,12 +133,11 @@
     private Network mDefaultInternetNetwork;
     private boolean mPreferTestNetworks;
 
-    public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, SharedLog log, int what) {
+    public UpstreamNetworkMonitor(Context ctx, Handler h, SharedLog log, EventListener listener) {
         mContext = ctx;
-        mTarget = tgt;
-        mHandler = mTarget.getHandler();
+        mHandler = h;
         mLog = log.forSubComponent(TAG);
-        mWhat = what;
+        mEventListener = listener;
         mLocalPrefixes = new HashSet<>();
         mIsDefaultCellularUpstream = false;
         mCM = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -374,11 +371,12 @@
                     network, newNc));
         }
 
-        mNetworkMap.put(network, new UpstreamNetworkState(
-                prev.linkProperties, newNc, network));
+        final UpstreamNetworkState uns =
+                new UpstreamNetworkState(prev.linkProperties, newNc, network);
+        mNetworkMap.put(network, uns);
         // TODO: If sufficient information is available to select a more
         // preferable upstream, do so now and notify the target.
-        notifyTarget(EVENT_ON_CAPABILITIES, network);
+        mEventListener.onUpstreamEvent(EVENT_ON_CAPABILITIES, uns);
     }
 
     private @Nullable UpstreamNetworkState updateLinkProperties(@NonNull Network network,
@@ -411,7 +409,7 @@
     private void handleLinkProp(Network network, LinkProperties newLp) {
         final UpstreamNetworkState ns = updateLinkProperties(network, newLp);
         if (ns != null) {
-            notifyTarget(EVENT_ON_LINKPROPERTIES, ns);
+            mEventListener.onUpstreamEvent(EVENT_ON_LINKPROPERTIES, ns);
         }
     }
 
@@ -438,7 +436,7 @@
         // preferable upstream, do so now and notify the target.  Likewise,
         // if the current upstream network is gone, notify the target of the
         // fact that we now have no upstream at all.
-        notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network));
+        mEventListener.onUpstreamEvent(EVENT_ON_LOST, mNetworkMap.remove(network));
     }
 
     private void maybeHandleNetworkSwitch(@NonNull Network network) {
@@ -456,14 +454,14 @@
         // Default network changed. Update local data and notify tethering.
         Log.d(TAG, "New default Internet network: " + network);
         mDefaultInternetNetwork = network;
-        notifyTarget(EVENT_DEFAULT_SWITCHED, ns);
+        mEventListener.onUpstreamEvent(EVENT_DEFAULT_SWITCHED, ns);
     }
 
     private void recomputeLocalPrefixes() {
         final HashSet<IpPrefix> localPrefixes = allLocalPrefixes(mNetworkMap.values());
         if (!mLocalPrefixes.equals(localPrefixes)) {
             mLocalPrefixes = localPrefixes;
-            notifyTarget(NOTIFY_LOCAL_PREFIXES, localPrefixes.clone());
+            mEventListener.onUpstreamEvent(NOTIFY_LOCAL_PREFIXES, localPrefixes.clone());
         }
     }
 
@@ -502,12 +500,13 @@
                 // onLinkPropertiesChanged right after this method and mDefaultInternetNetwork will
                 // be updated then.
                 //
-                // Technically, not updating here isn't necessary, because the notifications to
-                // Tethering sent by notifyTarget are messages sent to a state machine running on
-                // the same thread as this method, and so cannot arrive until after this method has
-                // returned. However, it is not a good idea to rely on that because fact that
-                // Tethering uses multiple state machines running on the same thread is a major
-                // source of race conditions and something that should be fixed.
+                // Technically, mDefaultInternetNetwork could be updated here, because the
+                // Callback#onChange implementation sends messages to the state machine running
+                // on the same thread as this method. If there is new default network change,
+                // the message cannot arrive until onLinkPropertiesChanged returns.
+                // However, it is not a good idea to rely on that because fact that Tethering uses
+                // multiple state machines running on the same thread is a major source of race
+                // conditions and something that should be fixed.
                 //
                 // TODO: is it correct that this code always updates EntitlementManager?
                 // This code runs when the default network connects or changes capabilities, but the
@@ -551,7 +550,7 @@
                 mIsDefaultCellularUpstream = false;
                 mEntitlementMgr.notifyUpstream(false);
                 Log.d(TAG, "Lost default Internet network: " + network);
-                notifyTarget(EVENT_DEFAULT_SWITCHED, null);
+                mEventListener.onUpstreamEvent(EVENT_DEFAULT_SWITCHED, null);
                 return;
             }
 
@@ -569,14 +568,6 @@
         if (cb != null) cm().unregisterNetworkCallback(cb);
     }
 
-    private void notifyTarget(int which, Network network) {
-        notifyTarget(which, mNetworkMap.get(network));
-    }
-
-    private void notifyTarget(int which, Object obj) {
-        mTarget.sendMessage(mWhat, which, 0, obj);
-    }
-
     private static class TypeStatePair {
         public int type = TYPE_NONE;
         public UpstreamNetworkState ns = null;
@@ -698,4 +689,10 @@
     public void setPreferTestNetworks(boolean prefer) {
         mPreferTestNetworks = prefer;
     }
+
+    /** An interface to notify upstream network changes. */
+    public interface EventListener {
+        /** Notify the client of some event */
+        void onUpstreamEvent(int what, Object obj);
+    }
 }
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
new file mode 100644
index 0000000..c236188
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
@@ -0,0 +1,197 @@
+/*
+ * 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.networkstack.tethering.util;
+
+import android.annotation.Nullable;
+import android.os.Looper;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.net.module.util.SyncStateMachine;
+import com.android.net.module.util.SyncStateMachine.StateInfo;
+
+import java.util.List;
+
+/** A wrapper to decide whether use synchronous state machine for tethering. */
+public class StateMachineShim {
+    // Exactly one of mAsyncSM or mSyncSM is non-null.
+    private final AsyncStateMachine mAsyncSM;
+    private final SyncStateMachine mSyncSM;
+
+    /**
+     * The Looper parameter is only needed for AsyncSM, so if looper is null, the shim will be
+     * created for SyncSM.
+     */
+    public StateMachineShim(final String name, @Nullable final Looper looper) {
+        this(name, looper, new Dependencies());
+    }
+
+    @VisibleForTesting
+    public StateMachineShim(final String name, @Nullable final Looper looper,
+            final Dependencies deps) {
+        if (looper == null) {
+            mAsyncSM = null;
+            mSyncSM = deps.makeSyncStateMachine(name, Thread.currentThread());
+        } else {
+            mAsyncSM = deps.makeAsyncStateMachine(name, looper);
+            mSyncSM = null;
+        }
+    }
+
+    /** A dependencies class which used for testing injection. */
+    @VisibleForTesting
+    public static class Dependencies {
+        /** Create SyncSM instance, for injection. */
+        public SyncStateMachine makeSyncStateMachine(final String name, final Thread thread) {
+            return new SyncStateMachine(name, thread);
+        }
+
+        /** Create AsyncSM instance, for injection. */
+        public AsyncStateMachine makeAsyncStateMachine(final String name, final Looper looper) {
+            return new AsyncStateMachine(name, looper);
+        }
+    }
+
+    /** Start the state machine */
+    public void start(final State initialState) {
+        if (mSyncSM != null) {
+            mSyncSM.start(initialState);
+        } else {
+            mAsyncSM.setInitialState(initialState);
+            mAsyncSM.start();
+        }
+    }
+
+    /** Add states to state machine. */
+    public void addAllStates(final List<StateInfo> stateInfos) {
+        if (mSyncSM != null) {
+            mSyncSM.addAllStates(stateInfos);
+        } else {
+            for (final StateInfo info : stateInfos) {
+                mAsyncSM.addState(info.state, info.parent);
+            }
+        }
+    }
+
+    /**
+     * Transition to given state.
+     *
+     * SyncSM doesn't allow this be called during state transition (#enter() or #exit() methods),
+     * or multiple times while processing a single message.
+     */
+    public void transitionTo(final State state) {
+        if (mSyncSM != null) {
+            mSyncSM.transitionTo(state);
+        } else {
+            mAsyncSM.transitionTo(state);
+        }
+    }
+
+    /** Send message to state machine. */
+    public void sendMessage(int what) {
+        sendMessage(what, 0, 0, null);
+    }
+
+    /** Send message to state machine. */
+    public void sendMessage(int what, Object obj) {
+        sendMessage(what, 0, 0, obj);
+    }
+
+    /** Send message to state machine. */
+    public void sendMessage(int what, int arg1) {
+        sendMessage(what, arg1, 0, null);
+    }
+
+    /**
+     * Send message to state machine.
+     *
+     * If using asynchronous state machine, putting the message into looper's message queue.
+     * Tethering runs on single looper thread that ipServers and mainSM all share with same message
+     * queue. The enqueued message will be processed by asynchronous state machine when all the
+     * messages before such enqueued message are processed.
+     * If using synchronous state machine, the message is processed right away without putting into
+     * looper's message queue.
+     */
+    public void sendMessage(int what, int arg1, int arg2, Object obj) {
+        if (mSyncSM != null) {
+            mSyncSM.processMessage(what, arg1, arg2, obj);
+        } else {
+            mAsyncSM.sendMessage(what, arg1, arg2, obj);
+        }
+    }
+
+    /**
+     * Send message after delayMillis millisecond.
+     *
+     * This can only be used with async state machine, so this will throw if using sync state
+     * machine.
+     */
+    public void sendMessageDelayedToAsyncSM(final int what, final long delayMillis) {
+        if (mSyncSM != null) {
+            throw new IllegalStateException("sendMessageDelayed can only be used with async SM");
+        }
+
+        mAsyncSM.sendMessageDelayed(what, delayMillis);
+    }
+
+    /**
+     * Enqueue a message to the front of the queue.
+     * Protected, may only be called by instances of async state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected void sendMessageAtFrontOfQueueToAsyncSM(int what, int arg1) {
+        if (mSyncSM != null) {
+            throw new IllegalStateException("sendMessageAtFrontOfQueue can only be used with"
+                    + " async SM");
+        }
+
+        mAsyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1);
+    }
+
+    /**
+     * Send self message.
+     * This can only be used with sync state machine, so this will throw if using async state
+     * machine.
+     */
+    public void sendSelfMessageToSyncSM(final int what, final Object obj) {
+        if (mSyncSM == null) {
+            throw new IllegalStateException("sendSelfMessage can only be used with sync SM");
+        }
+
+        mSyncSM.sendSelfMessage(what, 0, 0, obj);
+    }
+
+    /**
+     * An alias StateMahchine class with public construtor.
+     *
+     * Since StateMachine.java only provides protected construtor, adding a child class so that this
+     * shim could create StateMachine instance.
+     */
+    @VisibleForTesting
+    public static class AsyncStateMachine extends StateMachine {
+        public AsyncStateMachine(final String name, final Looper looper) {
+            super(name, looper);
+        }
+
+        /** Enqueue a message to the front of the queue for this state machine. */
+        public void sendMessageAtFrontOfQueueToAsyncSM(int what, int arg1) {
+            sendMessageAtFrontOfQueue(what, arg1);
+        }
+    }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java b/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java
index e94febb..e845f3f 100644
--- a/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java
+++ b/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java
@@ -19,7 +19,7 @@
 import android.companion.AssociationInfo;
 import android.companion.CompanionDeviceManager;
 import android.content.Context;
-import android.net.connectivity.TiramisuConnectivityInternalApiUtil;
+import android.net.connectivity.ConnectivityInternalApiUtil;
 import android.net.wear.ICompanionDeviceManagerProxy;
 import android.os.Build;
 import android.os.RemoteException;
@@ -39,7 +39,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public CompanionDeviceManagerProxy(Context context) {
         mService = ICompanionDeviceManagerProxy.Stub.asInterface(
-                TiramisuConnectivityInternalApiUtil.getCompanionDeviceManagerProxyService(context));
+                ConnectivityInternalApiUtil.getCompanionDeviceManagerProxyService(context));
     }
 
     /**
diff --git a/Tethering/tests/Android.bp b/Tethering/tests/Android.bp
index 72ca666..22cf3c5 100644
--- a/Tethering/tests/Android.bp
+++ b/Tethering/tests/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -24,5 +25,5 @@
     visibility: [
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
-    ]
+    ],
 }
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 5e08aba..337d408 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -14,6 +14,7 @@
 // limitations under the License.
 //
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -25,12 +26,14 @@
     ],
     min_sdk_version: "30",
     static_libs: [
-        "NetworkStackApiStableLib",
+        "DhcpPacketLib",
         "androidx.test.rules",
         "cts-net-utils",
-        "mockito-target-extended-minus-junit4",
+        "mockito-target-minus-junit4",
         "net-tests-utils",
+        "net-utils-device-common",
         "net-utils-device-common-bpf",
+        "net-utils-device-common-struct-base",
         "testables",
         "connectivity-net-module-utils-bpf",
     ],
@@ -39,21 +42,16 @@
         "android.test.base",
         "android.test.mock",
     ],
-    jni_libs: [
-        // For mockito extended
-        "libdexmakerjvmtiagent",
-        "libstaticjvmtiagent",
-    ],
 }
 
 android_library {
     name: "TetheringIntegrationTestsBaseLib",
     target_sdk_version: "current",
-    platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
     visibility: [
         "//packages/modules/Connectivity/Tethering/tests/mts",
-    ]
+        "//packages/modules/Connectivity/tests/cts/net",
+    ],
 }
 
 // Library including tethering integration tests targeting the latest stable SDK.
@@ -61,7 +59,6 @@
 android_library {
     name: "TetheringIntegrationTestsLatestSdkLib",
     target_sdk_version: "33",
-    platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
     srcs: [
         "src/**/*.java",
@@ -70,7 +67,7 @@
         "//packages/modules/Connectivity/tests/cts/tethering",
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
-    ]
+    ],
 }
 
 // Library including tethering integration tests targeting current development SDK.
@@ -78,7 +75,6 @@
 android_library {
     name: "TetheringIntegrationTestsLib",
     target_sdk_version: "current",
-    platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
     srcs: [
         "src/**/*.java",
@@ -86,7 +82,7 @@
     visibility: [
         "//packages/modules/Connectivity/tests/cts/tethering",
         "//packages/modules/Connectivity/Tethering/tests/mts",
-    ]
+    ],
 }
 
 // TODO: remove because TetheringIntegrationTests has been covered by ConnectivityCoverageTests.
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index dfb4aa5..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;
@@ -25,17 +24,14 @@
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringTester.buildTcpPacket;
+import static android.net.TetheringTester.buildUdpPacket;
+import static android.net.TetheringTester.isAddressIpv4;
 import static android.net.TetheringTester.isExpectedIcmpPacket;
 import static android.net.TetheringTester.isExpectedTcpPacket;
 import static android.net.TetheringTester.isExpectedUdpPacket;
-import static android.system.OsConstants.IPPROTO_IP;
-import static android.system.OsConstants.IPPROTO_IPV6;
-import static android.system.OsConstants.IPPROTO_TCP;
-import static android.system.OsConstants.IPPROTO_UDP;
 
 import static com.android.net.module.util.HexDump.dumpHexString;
-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.ICMPV6_ROUTER_ADVERTISEMENT;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
@@ -45,13 +41,11 @@
 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;
 import static org.junit.Assume.assumeTrue;
 
-import android.app.UiAutomation;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.net.EthernetManager.TetheredInterfaceCallback;
@@ -67,11 +61,8 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.modules.utils.build.SdkLevel;
-import com.android.net.module.util.PacketBuilder;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.testutils.HandlerUtils;
@@ -88,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;
@@ -109,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;
 
@@ -124,29 +117,17 @@
             (Inet4Address) parseNumericAddress("8.8.8.8");
     protected static final Inet6Address REMOTE_IP6_ADDR =
             (Inet6Address) parseNumericAddress("2002:db8:1::515:ca");
+    // The IPv6 network address translation of REMOTE_IP4_ADDR if pref64::/n is 64:ff9b::/96.
+    // For more information, see TetheringTester#PREF64_IPV4ONLY_ADDR, which assumes a prefix
+    // of 64:ff9b::/96.
     protected static final Inet6Address REMOTE_NAT64_ADDR =
             (Inet6Address) parseNumericAddress("64:ff9b::808:808");
-    protected static final IpPrefix TEST_NAT64PREFIX = new IpPrefix("64:ff9b::/96");
 
-    // IPv4 header definition.
-    protected static final short ID = 27149;
-    protected static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0
-    protected static final byte TIME_TO_LIVE = (byte) 0x40;
-    protected static final byte TYPE_OF_SERVICE = 0;
-
-    // IPv6 header definition.
-    private static final short HOP_LIMIT = 0x40;
-    // version=6, traffic class=0x0, flowlabel=0x0;
-    private static final int VERSION_TRAFFICCLASS_FLOWLABEL = 0x60000000;
-
-    // UDP and TCP header definition.
     // LOCAL_PORT is used by public port and private port. Assume port 9876 has not been used yet
     // before the testing that public port and private port are the same in the testing. Note that
     // NAT port forwarding could be different between private port and public port.
     protected static final short LOCAL_PORT = 9876;
     protected static final short REMOTE_PORT = 433;
-    private static final short WINDOW = (short) 0x2000;
-    private static final short URGENT_POINTER = 0;
 
     // Payload definition.
     protected static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]);
@@ -157,19 +138,20 @@
     protected static final ByteBuffer TX_PAYLOAD =
             ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 });
 
-    private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
-    private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
-    private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
-    private final PackageManager mPackageManager = mContext.getPackageManager();
-    private final CtsNetUtils mCtsNetUtils = new CtsNetUtils(mContext);
-    private final UiAutomation mUiAutomation =
-            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+    private static final Context sContext =
+            InstrumentationRegistry.getInstrumentation().getContext();
+    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;
@@ -178,6 +160,10 @@
     private TapPacketReader mDownstreamReader;
     private MyTetheringEventCallback mTetheringEventCallback;
 
+    public Context getContext() {
+        return sContext;
+    }
+
     @Before
     public void setUp() throws Exception {
         mHandlerThread = new HandlerThread(getClass().getSimpleName());
@@ -187,13 +173,14 @@
         mRunTests = isEthernetTetheringSupported();
         assumeTrue(mRunTests);
 
-        mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
+        mTetheredInterfaceRequester = new TetheredInterfaceRequester();
+        sCallbackErrors.clear();
     }
 
     private boolean isEthernetTetheringSupported() throws Exception {
-        if (mEm == null) return false;
+        if (sEm == null) return false;
 
-        return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> mTm.isTetheringSupported());
+        return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> sTm.isTetheringSupported());
     }
 
     protected void maybeStopTapPacketReader(final TapPacketReader tapPacketReader)
@@ -204,7 +191,7 @@
         }
     }
 
-    protected void maybeCloseTestInterface(final TestNetworkInterface testInterface)
+    protected static void maybeCloseTestInterface(final TestNetworkInterface testInterface)
             throws Exception {
         if (testInterface != null) {
             testInterface.getFileDescriptor().close();
@@ -212,8 +199,8 @@
         }
     }
 
-    protected void maybeUnregisterTetheringEventCallback(final MyTetheringEventCallback callback)
-            throws Exception {
+    protected static void maybeUnregisterTetheringEventCallback(
+            final MyTetheringEventCallback callback) throws Exception {
         if (callback != null) {
             callback.awaitInterfaceUntethered();
             callback.unregister();
@@ -222,7 +209,7 @@
 
     protected void stopEthernetTethering(final MyTetheringEventCallback callback) {
         runAsShell(TETHER_PRIVILEGED, () -> {
-            mTm.stopTethering(TETHERING_ETHERNET);
+            sTm.stopTethering(TETHERING_ETHERNET);
             maybeUnregisterTetheringEventCallback(callback);
         });
     }
@@ -255,70 +242,51 @@
         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();
-            mUiAutomation.dropShellPermissionIdentity();
+        }
+
+        if (sCallbackErrors.size() > 0) {
+            fail("Some callbacks had errors: " + sCallbackErrors);
         }
     }
 
     protected 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, () -> mEm.isAvailable());
-        }
-
         // 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(mHandler, mEm);
-        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 void setIncludeTestInterfaces(boolean include) {
+    protected static void setIncludeTestInterfaces(boolean include) {
         runAsShell(NETWORK_SETTINGS, () -> {
-            mEm.setIncludeTestInterfaces(include);
+            sEm.setIncludeTestInterfaces(include);
         });
     }
 
-    protected void setPreferTestNetworks(boolean prefer) {
+    protected static void setPreferTestNetworks(boolean prefer) {
         runAsShell(NETWORK_SETTINGS, () -> {
-            mTm.setPreferTestNetworks(prefer);
+            sTm.setPreferTestNetworks(prefer);
         });
     }
 
-    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;
@@ -337,7 +305,6 @@
 
 
     protected static final class MyTetheringEventCallback implements TetheringEventCallback {
-        private final TetheringManager mTm;
         private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
         private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1);
         private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1);
@@ -348,7 +315,7 @@
         private final TetheringInterface mIface;
         private final Network mExpectedUpstream;
 
-        private boolean mAcceptAnyUpstream = false;
+        private final boolean mAcceptAnyUpstream;
 
         private volatile boolean mInterfaceWasTethered = false;
         private volatile boolean mInterfaceWasLocalOnly = false;
@@ -356,24 +323,31 @@
         private volatile Collection<TetheredClient> mClients = null;
         private volatile Network mUpstream = null;
 
-        MyTetheringEventCallback(TetheringManager tm, String iface) {
-            this(tm, iface, null);
+        // The dnsmasq in R might block netd for 20 seconds, which can also block tethering
+        // enable/disable for 20 seconds. To fix this, changing the timeouts from 5 seconds to 30
+        // seconds. See b/289881008.
+        private static final int EXPANDED_TIMEOUT_MS = 30000;
+
+        MyTetheringEventCallback(String iface) {
+            mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+            mExpectedUpstream = null;
             mAcceptAnyUpstream = true;
         }
 
-        MyTetheringEventCallback(TetheringManager tm, String iface, Network expectedUpstream) {
-            mTm = tm;
+        MyTetheringEventCallback(String iface, @NonNull Network expectedUpstream) {
+            Objects.requireNonNull(expectedUpstream);
             mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
             mExpectedUpstream = expectedUpstream;
+            mAcceptAnyUpstream = false;
         }
 
         public void unregister() {
-            mTm.unregisterTetheringEventCallback(this);
+            sTm.unregisterTetheringEventCallback(this);
             mUnregistered = true;
         }
         @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
@@ -394,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
@@ -414,13 +388,13 @@
         }
 
         public void awaitInterfaceTethered() throws Exception {
-            assertTrue("Ethernet not tethered after " + TIMEOUT_MS + "ms",
-                    mTetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertTrue("Ethernet not tethered after " + EXPANDED_TIMEOUT_MS + "ms",
+                    mTetheringStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
 
         public void awaitInterfaceLocalOnly() throws Exception {
-            assertTrue("Ethernet not local-only after " + TIMEOUT_MS + "ms",
-                    mLocalOnlyStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertTrue("Ethernet not local-only after " + EXPANDED_TIMEOUT_MS + "ms",
+                    mLocalOnlyStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
 
         // Used to check if the callback has registered. When the callback is registered,
@@ -434,8 +408,9 @@
         }
 
         public void awaitCallbackRegistered() throws Exception {
-            if (!mCallbackRegisteredLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                fail("Did not receive callback registered signal after " + TIMEOUT_MS + "ms");
+            if (!mCallbackRegisteredLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                fail("Did not receive callback registered signal after " + EXPANDED_TIMEOUT_MS
+                        + "ms");
             }
         }
 
@@ -447,11 +422,11 @@
             if (!mInterfaceWasTethered && !mInterfaceWasLocalOnly) return;
 
             if (mInterfaceWasTethered) {
-                assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
-                        mTetheringStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+                assertTrue(mIface + " not untethered after " + EXPANDED_TIMEOUT_MS + "ms",
+                        mTetheringStoppedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
             } else if (mInterfaceWasLocalOnly) {
-                assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
-                        mLocalOnlyStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+                assertTrue(mIface + " not untethered after " + EXPANDED_TIMEOUT_MS + "ms",
+                        mLocalOnlyStoppedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
             } else {
                 fail(mIface + " cannot be both tethered and local-only. Update this test class.");
             }
@@ -462,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
@@ -478,8 +453,9 @@
         }
 
         public Collection<TetheredClient> awaitClientConnected() throws Exception {
-            assertTrue("Did not receive client connected callback after " + TIMEOUT_MS + "ms",
-                    mClientConnectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertTrue("Did not receive client connected callback after "
+                    + EXPANDED_TIMEOUT_MS + "ms",
+                    mClientConnectedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
             return mClients;
         }
 
@@ -490,16 +466,21 @@
 
             Log.d(TAG, "Got upstream changed: " + network);
             mUpstream = network;
+            // The callback always updates the current tethering status when it's first registered.
+            // If the caller registers the callback before tethering starts, the null upstream
+            // would be updated. Filtering out the null case because it's not a valid upstream that
+            // we care about.
+            if (mUpstream == null) return;
             if (mAcceptAnyUpstream || Objects.equals(mUpstream, mExpectedUpstream)) {
                 mUpstreamLatch.countDown();
             }
         }
 
         public Network awaitUpstreamChanged(boolean throwTimeoutException) throws Exception {
-            if (!mUpstreamLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            if (!mUpstreamLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                 final String errorMessage = "Did not receive upstream "
                             + (mAcceptAnyUpstream ? "any" : mExpectedUpstream)
-                            + " callback after " + TIMEOUT_MS + "ms";
+                            + " callback after " + EXPANDED_TIMEOUT_MS + "ms";
 
                 if (throwTimeoutException) {
                     throw new TimeoutException(errorMessage);
@@ -511,18 +492,23 @@
         }
     }
 
-    protected MyTetheringEventCallback enableEthernetTethering(String iface,
+    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
         // after etherent tethering started.
         final MyTetheringEventCallback callback;
         if (expectedUpstream != null) {
-            callback = new MyTetheringEventCallback(mTm, iface, expectedUpstream);
+            callback = new MyTetheringEventCallback(iface, expectedUpstream);
         } else {
-            callback = new MyTetheringEventCallback(mTm, iface);
+            callback = new MyTetheringEventCallback(iface);
         }
         runAsShell(NETWORK_SETTINGS, () -> {
-            mTm.registerTetheringEventCallback(mHandler::post, callback);
+            sTm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
             // Need to hold the shell permission until callback is registered. This helps to avoid
             // the test become flaky.
             callback.awaitCallbackRegistered();
@@ -537,12 +523,12 @@
 
             @Override
             public void onTetheringFailed(int resultCode) {
-                fail("Unexpectedly got onTetheringFailed");
+                addCallbackError("Unexpectedly got onTetheringFailed");
             }
         };
         Log.d(TAG, "Starting Ethernet tethering");
         runAsShell(TETHER_PRIVILEGED, () -> {
-            mTm.startTethering(request, mHandler::post /* executor */, startTetheringCallback);
+            sTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
             // Binder call is an async call. Need to hold the shell permission until tethering
             // started. This helps to avoid the test become flaky.
             if (!tetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
@@ -565,7 +551,7 @@
         return callback;
     }
 
-    protected MyTetheringEventCallback enableEthernetTethering(String iface,
+    protected static MyTetheringEventCallback enableEthernetTethering(String iface,
             Network expectedUpstream) throws Exception {
         return enableEthernetTethering(iface,
                 new TetheringRequest.Builder(TETHERING_ETHERNET)
@@ -591,15 +577,12 @@
     }
 
     protected static final class TetheredInterfaceRequester implements TetheredInterfaceCallback {
-        private final Handler mHandler;
-        private final EthernetManager mEm;
-
         private TetheredInterfaceRequest mRequest;
         private final CompletableFuture<String> mFuture = new CompletableFuture<>();
 
-        TetheredInterfaceRequester(Handler handler, EthernetManager em) {
-            mHandler = handler;
-            mEm = em;
+        TetheredInterfaceRequester() {
+            mRequest = runAsShell(NETWORK_SETTINGS, () ->
+                    sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
         }
 
         @Override
@@ -613,34 +596,27 @@
             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, () ->
-                    mEm.requestTetheredInterface(mHandler::post, 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());
         }
     }
 
-    protected TestNetworkInterface createTestInterface() throws Exception {
+    protected static TestNetworkInterface createTestInterface() throws Exception {
         TestNetworkManager tnm = runAsShell(MANAGE_TEST_NETWORKS, () ->
-                mContext.getSystemService(TestNetworkManager.class));
+                sContext.getSystemService(TestNetworkManager.class));
         TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () ->
                 tnm.createTapInterface());
         Log.d(TAG, "Created test interface " + iface.getInterfaceName());
@@ -654,75 +630,11 @@
         final LinkProperties lp = new LinkProperties();
         lp.setLinkAddresses(addresses);
         lp.setDnsServers(dnses);
-        lp.setNat64Prefix(TEST_NAT64PREFIX);
 
-        return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(mContext, lp, TIMEOUT_MS));
-    }
-
-    private short getEthType(@NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp) {
-        return isAddressIpv4(srcIp, dstIp) ? (short) ETHER_TYPE_IPV4 : (short) ETHER_TYPE_IPV6;
-    }
-
-    private int getIpProto(@NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp) {
-        return isAddressIpv4(srcIp, dstIp) ? IPPROTO_IP : IPPROTO_IPV6;
-    }
-
-    @NonNull
-    protected ByteBuffer buildUdpPacket(
-            @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
-            @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
-            short srcPort, short dstPort, @Nullable final ByteBuffer payload)
-            throws Exception {
-        final int ipProto = getIpProto(srcIp, dstIp);
-        final boolean hasEther = (srcMac != null && dstMac != null);
-        final int payloadLen = (payload == null) ? 0 : payload.limit();
-        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, ipProto, IPPROTO_UDP,
-                payloadLen);
-        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
-
-        // [1] Ethernet header
-        if (hasEther) {
-            packetBuilder.writeL2Header(srcMac, dstMac, getEthType(srcIp, dstIp));
-        }
-
-        // [2] IP header
-        if (ipProto == IPPROTO_IP) {
-            packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
-                    TIME_TO_LIVE, (byte) IPPROTO_UDP, (Inet4Address) srcIp, (Inet4Address) dstIp);
-        } else {
-            packetBuilder.writeIpv6Header(VERSION_TRAFFICCLASS_FLOWLABEL, (byte) IPPROTO_UDP,
-                    HOP_LIMIT, (Inet6Address) srcIp, (Inet6Address) dstIp);
-        }
-
-        // [3] UDP header
-        packetBuilder.writeUdpHeader(srcPort, dstPort);
-
-        // [4] Payload
-        if (payload != null) {
-            buffer.put(payload);
-            // in case data might be reused by caller, restore the position and
-            // limit of bytebuffer.
-            payload.clear();
-        }
-
-        return packetBuilder.finalizePacket();
-    }
-
-    @NonNull
-    protected ByteBuffer buildUdpPacket(@NonNull final InetAddress srcIp,
-            @NonNull final InetAddress dstIp, short srcPort, short dstPort,
-            @Nullable final ByteBuffer payload) throws Exception {
-        return buildUdpPacket(null /* srcMac */, null /* dstMac */, srcIp, dstIp, srcPort,
-                dstPort, payload);
-    }
-
-    private boolean isAddressIpv4(@NonNull final  InetAddress srcIp,
-            @NonNull final InetAddress dstIp) {
-        if (srcIp instanceof Inet4Address && dstIp instanceof Inet4Address) return true;
-        if (srcIp instanceof Inet6Address && dstIp instanceof Inet6Address) return false;
-
-        fail("Unsupported conditions: srcIp " + srcIp + ", dstIp " + dstIp);
-        return false;  // unreachable
+        // 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,
@@ -766,45 +678,6 @@
         });
     }
 
-
-    @NonNull
-    private ByteBuffer buildTcpPacket(
-            @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
-            @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
-            short srcPort, short dstPort, final short seq, final short ack,
-            final byte tcpFlags, @NonNull final ByteBuffer payload) throws Exception {
-        final int ipProto = getIpProto(srcIp, dstIp);
-        final boolean hasEther = (srcMac != null && dstMac != null);
-        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, ipProto, IPPROTO_TCP,
-                payload.limit());
-        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
-
-        // [1] Ethernet header
-        if (hasEther) {
-            packetBuilder.writeL2Header(srcMac, dstMac, getEthType(srcIp, dstIp));
-        }
-
-        // [2] IP header
-        if (ipProto == IPPROTO_IP) {
-            packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
-                    TIME_TO_LIVE, (byte) IPPROTO_TCP, (Inet4Address) srcIp, (Inet4Address) dstIp);
-        } else {
-            packetBuilder.writeIpv6Header(VERSION_TRAFFICCLASS_FLOWLABEL, (byte) IPPROTO_TCP,
-                    HOP_LIMIT, (Inet6Address) srcIp, (Inet6Address) dstIp);
-        }
-
-        // [3] TCP header
-        packetBuilder.writeTcpHeader(srcPort, dstPort, seq, ack, tcpFlags, WINDOW, URGENT_POINTER);
-
-        // [4] Payload
-        buffer.put(payload);
-        // in case data might be reused by caller, restore the position and
-        // limit of bytebuffer.
-        payload.clear();
-
-        return packetBuilder.finalizePacket();
-    }
-
     protected void sendDownloadPacketTcp(@NonNull final InetAddress srcIp,
             @NonNull final InetAddress dstIp, short seq, short ack, byte tcpFlags,
             @NonNull final ByteBuffer payload, @NonNull final TetheringTester tester,
@@ -943,7 +816,7 @@
     private void maybeRetryTestedUpstreamChanged(final Network expectedUpstream,
             final TimeoutException fallbackException) throws Exception {
         // Fall back original exception because no way to reselect if there is no WIFI feature.
-        assertTrue(fallbackException.toString(), mPackageManager.hasSystemFeature(FEATURE_WIFI));
+        assertTrue(fallbackException.toString(), sPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         // Try to toggle wifi network, if any, to reselect upstream network via default network
         // switching. Because test network has higher priority than internet network, this can
@@ -954,12 +827,12 @@
         // trigger the reselection, the total test time may over test suite 1 minmute timeout.
         // Probably need to disable/restore all internet networks in a common place of test
         // process. Currently, EthernetTetheringTest is part of CTS test which needs wifi network
-        // connection if device has wifi feature. CtsNetUtils#toggleWifi() checks wifi connection
-        // during the toggling process.
-        // See Tethering#chooseUpstreamType, CtsNetUtils#toggleWifi.
+        // connection if device has wifi feature.
+        // See Tethering#chooseUpstreamType
         // TODO: toggle cellular network if the device has no WIFI feature.
         Log.d(TAG, "Toggle WIFI to retry upstream selection");
-        mCtsNetUtils.toggleWifi();
+        sCtsNetUtils.disableWifi();
+        sCtsNetUtils.ensureWifiConnected();
 
         // Wait for expected upstream.
         final CompletableFuture<Network> future = new CompletableFuture<>();
@@ -973,14 +846,14 @@
             }
         };
         try {
-            mTm.registerTetheringEventCallback(mHandler::post, callback);
+            sTm.registerTetheringEventCallback(mHandler::post, callback);
             assertEquals("onUpstreamChanged for unexpected network", expectedUpstream,
                     future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         } catch (TimeoutException e) {
             throw new AssertionError("Did not receive upstream " + expectedUpstream
                     + " callback after " + TIMEOUT_MS + "ms");
         } finally {
-            mTm.unregisterTetheringEventCallback(callback);
+            sTm.unregisterTetheringEventCallback(callback);
         }
     }
 
@@ -1017,7 +890,7 @@
         mDownstreamReader = makePacketReader(mDownstreamIface);
         mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
 
-        final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        final ConnectivityManager cm = sContext.getSystemService(ConnectivityManager.class);
         // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make
         // sure tethering already have ipv6 connectivity before testing.
         if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) {
diff --git a/Tethering/tests/integration/base/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
index ae39b24..ae4ae55 100644
--- a/Tethering/tests/integration/base/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/base/android/net/TetheringTester.java
@@ -16,17 +16,24 @@
 
 package android.net;
 
+import static android.net.DnsResolver.CLASS_IN;
+import static android.net.DnsResolver.TYPE_AAAA;
 import static android.net.InetAddresses.parseNumericAddress;
+import static android.system.OsConstants.ICMP_ECHO;
+import static android.system.OsConstants.ICMP_ECHOREPLY;
 import static android.system.OsConstants.IPPROTO_ICMP;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
-
 import static com.android.net.module.util.DnsPacket.ANSECTION;
-import static com.android.net.module.util.DnsPacket.ARSECTION;
-import static com.android.net.module.util.DnsPacket.NSSECTION;
+import static com.android.net.module.util.DnsPacket.DnsHeader;
+import static com.android.net.module.util.DnsPacket.DnsRecord;
 import static com.android.net.module.util.DnsPacket.QDSECTION;
 import static com.android.net.module.util.HexDump.dumpHexString;
+import static com.android.net.module.util.IpUtils.icmpChecksum;
+import static com.android.net.module.util.IpUtils.ipChecksum;
 import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
 import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
@@ -38,11 +45,14 @@
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMP_CHECKSUM_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
 import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
 import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
-
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
@@ -58,7 +68,9 @@
 
 import com.android.net.module.util.DnsPacket;
 import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.PacketBuilder;
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.arp.ArpPacket;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv4Header;
 import com.android.net.module.util.structs.Icmpv6Header;
@@ -70,7 +82,6 @@
 import com.android.net.module.util.structs.RaHeader;
 import com.android.net.module.util.structs.TcpHeader;
 import com.android.net.module.util.structs.UdpHeader;
-import com.android.networkstack.arp.ArpPacket;
 import com.android.testutils.TapPacketReader;
 
 import java.net.Inet4Address;
@@ -101,6 +112,44 @@
             DhcpPacket.DHCP_LEASE_TIME,
     };
     private static final InetAddress LINK_LOCAL = parseNumericAddress("fe80::1");
+    // IPv4 header definition.
+    protected static final short ID = 27149;
+    protected static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0
+    protected static final byte TIME_TO_LIVE = (byte) 0x40;
+    protected static final byte TYPE_OF_SERVICE = 0;
+
+    // IPv6 header definition.
+    private static final short HOP_LIMIT = 0x40;
+    // version=6, traffic class=0x0, flowlabel=0x0;
+    private static final int VERSION_TRAFFICCLASS_FLOWLABEL = 0x60000000;
+
+    // UDP and TCP header definition.
+    private static final short WINDOW = (short) 0x2000;
+    private static final short URGENT_POINTER = 0;
+
+    // ICMP definition.
+    private static final short ICMPECHO_CODE = 0x0;
+
+    // Prefix64 discovery definition. See RFC 7050 section 8.
+    // Note that the AAAA response Pref64::WKAs consisting of Pref64::/n and WKA.
+    // Use 64:ff9b::/96 as Pref64::/n and WKA 192.0.0.17{0|1} here.
+    //
+    // Host                                          DNS64 server
+    //   |                                                |
+    //   |  "AAAA" query for "ipv4only.arpa."             |
+    //   |----------------------------------------------->|
+    //   |                                                |
+    //   |  "AAAA" response with:                         |
+    //   |  "64:ff9b::192.0.0.170"                        |
+    //   |<-----------------------------------------------|
+    //
+    private static final String PREF64_IPV4ONLY_HOSTNAME = "ipv4only.arpa";
+    private static final InetAddress PREF64_IPV4ONLY_ADDR = parseNumericAddress(
+            "64:ff9b::192.0.0.170");
+
+    // DNS header definition.
+    private static final short FLAG = (short) 0x8100;  // qr, ra
+    private static final short TTL = (short) 0;
 
     public static final String DHCP_HOSTNAME = "testhostname";
 
@@ -462,6 +511,11 @@
             super(data);
         }
 
+        TestDnsPacket(@NonNull DnsHeader header, @Nullable ArrayList<DnsRecord> qd,
+                @Nullable ArrayList<DnsRecord> an) {
+            super(header, qd, an);
+        }
+
         @Nullable
         public static TestDnsPacket getTestDnsPacket(final ByteBuffer buf) {
             try {
@@ -628,7 +682,191 @@
         return false;
     }
 
-    private void sendUploadPacket(ByteBuffer packet) throws Exception {
+    @NonNull
+    public static ByteBuffer buildUdpPacket(
+            @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+            @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
+            short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+            throws Exception {
+        final int ipProto = getIpProto(srcIp, dstIp);
+        final boolean hasEther = (srcMac != null && dstMac != null);
+        final int payloadLen = (payload == null) ? 0 : payload.limit();
+        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, ipProto, IPPROTO_UDP,
+                payloadLen);
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+
+        // [1] Ethernet header
+        if (hasEther) {
+            packetBuilder.writeL2Header(srcMac, dstMac, getEthType(srcIp, dstIp));
+        }
+
+        // [2] IP header
+        if (ipProto == IPPROTO_IP) {
+            packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                    TIME_TO_LIVE, (byte) IPPROTO_UDP, (Inet4Address) srcIp, (Inet4Address) dstIp);
+        } else {
+            packetBuilder.writeIpv6Header(VERSION_TRAFFICCLASS_FLOWLABEL, (byte) IPPROTO_UDP,
+                    HOP_LIMIT, (Inet6Address) srcIp, (Inet6Address) dstIp);
+        }
+
+        // [3] UDP header
+        packetBuilder.writeUdpHeader(srcPort, dstPort);
+
+        // [4] Payload
+        if (payload != null) {
+            buffer.put(payload);
+            // in case data might be reused by caller, restore the position and
+            // limit of bytebuffer.
+            payload.clear();
+        }
+
+        return packetBuilder.finalizePacket();
+    }
+
+    @NonNull
+    public static ByteBuffer buildUdpPacket(@NonNull final InetAddress srcIp,
+            @NonNull final InetAddress dstIp, short srcPort, short dstPort,
+            @Nullable final ByteBuffer payload) throws Exception {
+        return buildUdpPacket(null /* srcMac */, null /* dstMac */, srcIp, dstIp, srcPort,
+                dstPort, payload);
+    }
+
+    @NonNull
+    public static ByteBuffer buildTcpPacket(
+            @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+            @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
+            short srcPort, short dstPort, final short seq, final short ack,
+            final byte tcpFlags, @NonNull final ByteBuffer payload) throws Exception {
+        final int ipProto = getIpProto(srcIp, dstIp);
+        final boolean hasEther = (srcMac != null && dstMac != null);
+        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, ipProto, IPPROTO_TCP,
+                payload.limit());
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+
+        // [1] Ethernet header
+        if (hasEther) {
+            packetBuilder.writeL2Header(srcMac, dstMac, getEthType(srcIp, dstIp));
+        }
+
+        // [2] IP header
+        if (ipProto == IPPROTO_IP) {
+            packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                    TIME_TO_LIVE, (byte) IPPROTO_TCP, (Inet4Address) srcIp, (Inet4Address) dstIp);
+        } else {
+            packetBuilder.writeIpv6Header(VERSION_TRAFFICCLASS_FLOWLABEL, (byte) IPPROTO_TCP,
+                    HOP_LIMIT, (Inet6Address) srcIp, (Inet6Address) dstIp);
+        }
+
+        // [3] TCP header
+        packetBuilder.writeTcpHeader(srcPort, dstPort, seq, ack, tcpFlags, WINDOW, URGENT_POINTER);
+
+        // [4] Payload
+        buffer.put(payload);
+        // in case data might be reused by caller, restore the position and
+        // limit of bytebuffer.
+        payload.clear();
+
+        return packetBuilder.finalizePacket();
+    }
+
+    // PacketBuilder doesn't support IPv4 ICMP packet. It may need to refactor PacketBuilder first
+    // because ICMP is a specific layer 3 protocol for PacketBuilder which expects packets always
+    // have layer 3 (IP) and layer 4 (TCP, UDP) for now. Since we don't use IPv4 ICMP packet too
+    // much in this test, we just write a ICMP packet builder here.
+    @NonNull
+    public static ByteBuffer buildIcmpEchoPacketV4(
+            @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+            @NonNull final Inet4Address srcIp, @NonNull final Inet4Address dstIp,
+            int type, short id, short seq) throws Exception {
+        if (type != ICMP_ECHO && type != ICMP_ECHOREPLY) {
+            fail("Unsupported ICMP type: " + type);
+        }
+
+        // Build ICMP echo id and seq fields as payload. Ignore the data field.
+        final ByteBuffer payload = ByteBuffer.allocate(4);
+        payload.putShort(id);
+        payload.putShort(seq);
+        payload.rewind();
+
+        final boolean hasEther = (srcMac != null && dstMac != null);
+        final int etherHeaderLen = hasEther ? Struct.getSize(EthernetHeader.class) : 0;
+        final int ipv4HeaderLen = Struct.getSize(Ipv4Header.class);
+        final int Icmpv4HeaderLen = Struct.getSize(Icmpv4Header.class);
+        final int payloadLen = payload.limit();
+        final ByteBuffer packet = ByteBuffer.allocate(etherHeaderLen + ipv4HeaderLen
+                + Icmpv4HeaderLen + payloadLen);
+
+        // [1] Ethernet header
+        if (hasEther) {
+            final EthernetHeader ethHeader = new EthernetHeader(dstMac, srcMac, ETHER_TYPE_IPV4);
+            ethHeader.writeToByteBuffer(packet);
+        }
+
+        // [2] IP header
+        final Ipv4Header ipv4Header = new Ipv4Header(TYPE_OF_SERVICE,
+                (short) 0 /* totalLength, calculate later */, ID,
+                FLAGS_AND_FRAGMENT_OFFSET, TIME_TO_LIVE, (byte) IPPROTO_ICMP,
+                (short) 0 /* checksum, calculate later */, srcIp, dstIp);
+        ipv4Header.writeToByteBuffer(packet);
+
+        // [3] ICMP header
+        final Icmpv4Header icmpv4Header = new Icmpv4Header((byte) type, ICMPECHO_CODE,
+                (short) 0 /* checksum, calculate later */);
+        icmpv4Header.writeToByteBuffer(packet);
+
+        // [4] Payload
+        packet.put(payload);
+        packet.flip();
+
+        // [5] Finalize packet
+        // Used for updating IP header fields. If there is Ehternet header, IPv4 header offset
+        // in buffer equals ethernet header length because IPv4 header is located next to ethernet
+        // header. Otherwise, IPv4 header offset is 0.
+        final int ipv4HeaderOffset = hasEther ? etherHeaderLen : 0;
+
+        // Populate the IPv4 totalLength field.
+        packet.putShort(ipv4HeaderOffset + IPV4_LENGTH_OFFSET,
+                (short) (ipv4HeaderLen + Icmpv4HeaderLen + payloadLen));
+
+        // Populate the IPv4 header checksum field.
+        packet.putShort(ipv4HeaderOffset + IPV4_CHECKSUM_OFFSET,
+                ipChecksum(packet, ipv4HeaderOffset /* headerOffset */));
+
+        // Populate the ICMP checksum field.
+        packet.putShort(ipv4HeaderOffset + IPV4_HEADER_MIN_LEN + ICMP_CHECKSUM_OFFSET,
+                icmpChecksum(packet, ipv4HeaderOffset + IPV4_HEADER_MIN_LEN,
+                        Icmpv4HeaderLen + payloadLen));
+        return packet;
+    }
+
+    @NonNull
+    public static ByteBuffer buildIcmpEchoPacketV4(@NonNull final Inet4Address srcIp,
+            @NonNull final Inet4Address dstIp, int type, short id, short seq)
+            throws Exception {
+        return buildIcmpEchoPacketV4(null /* srcMac */, null /* dstMac */, srcIp, dstIp,
+                type, id, seq);
+    }
+
+    private static short getEthType(@NonNull final InetAddress srcIp,
+            @NonNull final InetAddress dstIp) {
+        return isAddressIpv4(srcIp, dstIp) ? (short) ETHER_TYPE_IPV4 : (short) ETHER_TYPE_IPV6;
+    }
+
+    private static int getIpProto(@NonNull final InetAddress srcIp,
+            @NonNull final InetAddress dstIp) {
+        return isAddressIpv4(srcIp, dstIp) ? IPPROTO_IP : IPPROTO_IPV6;
+    }
+
+    public static boolean isAddressIpv4(@NonNull final  InetAddress srcIp,
+            @NonNull final InetAddress dstIp) {
+        if (srcIp instanceof Inet4Address && dstIp instanceof Inet4Address) return true;
+        if (srcIp instanceof Inet6Address && dstIp instanceof Inet6Address) return false;
+
+        fail("Unsupported conditions: srcIp " + srcIp + ", dstIp " + dstIp);
+        return false;  // unreachable
+    }
+
+    public void sendUploadPacket(ByteBuffer packet) throws Exception {
         mDownstreamReader.sendResponse(packet);
     }
 
@@ -650,10 +888,85 @@
         return null;
     }
 
+    @NonNull
+    private ByteBuffer buildUdpDnsPrefix64ReplyPacket(int dnsId, @NonNull final Inet6Address srcIp,
+            @NonNull final Inet6Address dstIp, short srcPort, short dstPort) throws Exception {
+        // [1] Build prefix64 DNS message.
+        final ArrayList<DnsRecord> qlist = new ArrayList<>();
+        // Fill QD section.
+        qlist.add(DnsRecord.makeQuestion(PREF64_IPV4ONLY_HOSTNAME, TYPE_AAAA, CLASS_IN));
+        final ArrayList<DnsRecord> alist = new ArrayList<>();
+        // Fill AN sections.
+        alist.add(DnsRecord.makeAOrAAAARecord(ANSECTION, PREF64_IPV4ONLY_HOSTNAME, CLASS_IN, TTL,
+                PREF64_IPV4ONLY_ADDR));
+        final TestDnsPacket dns = new TestDnsPacket(
+                new DnsHeader(dnsId, FLAG, qlist.size(), alist.size()), qlist, alist);
+
+        // [2] Build IPv6 UDP DNS packet.
+        return buildUdpPacket(srcIp, dstIp, srcPort, dstPort, ByteBuffer.wrap(dns.getBytes()));
+    }
+
+    private void maybeReplyUdpDnsPrefix64Discovery(@NonNull byte[] packet) {
+        final ByteBuffer buf = ByteBuffer.wrap(packet);
+
+        // [1] Parse the prefix64 discovery DNS query for hostname ipv4only.arpa.
+        // Parse IPv6 and UDP header.
+        Ipv6Header ipv6Header = null;
+        try {
+            ipv6Header = Struct.parse(Ipv6Header.class, buf);
+            if (ipv6Header == null || ipv6Header.nextHeader != IPPROTO_UDP) return;
+        } catch (Exception e) {
+            // Parsing packet fail means it is not IPv6 UDP packet.
+            return;
+        }
+        final UdpHeader udpHeader = Struct.parse(UdpHeader.class, buf);
+
+        // Parse DNS message.
+        final TestDnsPacket pref64Query = TestDnsPacket.getTestDnsPacket(buf);
+        if (pref64Query == null) return;
+        if (pref64Query.getHeader().isResponse()) return;
+        if (pref64Query.getQDCount() != 1) return;
+        if (pref64Query.getANCount() != 0) return;
+        if (pref64Query.getNSCount() != 0) return;
+        if (pref64Query.getARCount() != 0) return;
+
+        final List<DnsRecord> qdRecordList = pref64Query.getRecordList(QDSECTION);
+        if (qdRecordList.size() != 1) return;
+        if (!qdRecordList.get(0).dName.equals(PREF64_IPV4ONLY_HOSTNAME)) return;
+
+        // [2] Build prefix64 DNS discovery reply from received query.
+        // DNS response transaction id must be copied from DNS query. Used by the requester
+        // to match up replies to outstanding queries. See RFC 1035 section 4.1.1. Also reverse
+        // the source/destination address/port of query packet for building reply packet.
+        final ByteBuffer replyPacket;
+        try {
+            replyPacket = buildUdpDnsPrefix64ReplyPacket(pref64Query.getHeader().getId(),
+                    ipv6Header.dstIp /* srcIp */, ipv6Header.srcIp /* dstIp */,
+                    (short) udpHeader.dstPort /* srcPort */,
+                    (short) udpHeader.srcPort /* dstPort */);
+        } catch (Exception e) {
+            fail("Failed to build prefix64 discovery reply for " + ipv6Header.srcIp + ": " + e);
+            return;
+        }
+
+        Log.d(TAG, "Sending prefix64 discovery reply");
+        try {
+            sendDownloadPacket(replyPacket);
+        } catch (Exception e) {
+            fail("Failed to reply prefix64 discovery for " + ipv6Header.srcIp + ": " + e);
+        }
+    }
+
     private byte[] getUploadPacket(Predicate<byte[]> filter) {
         assertNotNull("Can't deal with upstream interface in local only mode", mUpstreamReader);
 
-        return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
+        byte[] packet;
+        while ((packet = mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS)) != null) {
+            if (filter.test(packet)) return packet;
+
+            maybeReplyUdpDnsPrefix64Discovery(packet);
+        }
+        return null;
     }
 
     private @NonNull byte[] verifyPacketNotNull(String message, @Nullable byte[] packet) {
@@ -680,4 +993,12 @@
 
         return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
     }
+
+    // Send DHCPDISCOVER to DHCP server to see if DHCP server is still alive to handle
+    // the upcoming DHCP packets. This method should be only used when we know the DHCP
+    // server has been created successfully before.
+    public boolean testDhcpServerAlive(final MacAddress mac) throws Exception {
+        sendDhcpDiscover(mac.toByteArray());
+        return getNextDhcpPacket() != null;
+    }
 }
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 55854e2..9cdba2f 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -16,57 +16,66 @@
 
 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;
 import static android.net.TetheringTester.TestDnsPacket;
+import static android.net.TetheringTester.buildIcmpEchoPacketV4;
+import static android.net.TetheringTester.buildUdpPacket;
 import static android.net.TetheringTester.isExpectedIcmpPacket;
 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_ICMP;
+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.IpUtils.icmpChecksum;
-import static com.android.net.module.util.IpUtils.ipChecksum;
-import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
 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.net.module.util.NetworkStackConstants.ICMP_CHECKSUM_OFFSET;
-import static com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET;
-import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
-import static com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET;
+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.MediumTest;
+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.structs.EthernetHeader;
-import com.android.net.module.util.structs.Icmpv4Header;
+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;
@@ -80,14 +89,14 @@
 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)
-@MediumTest
+@LargeTest
 public class EthernetTetheringTest extends EthernetTetheringTestBase {
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
@@ -95,10 +104,29 @@
     private static final String TAG = EthernetTetheringTest.class.getSimpleName();
 
     private static final short DNS_PORT = 53;
-    private static final short ICMPECHO_CODE = 0x0;
     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[] {
@@ -159,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.
@@ -180,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);
 
@@ -202,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;
@@ -213,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);
 
@@ -243,7 +279,7 @@
         try {
             downstreamIface = createTestInterface();
 
-            final String iface = getTetheredInterface();
+            final String iface = mTetheredInterfaceRequester.getInterface();
             assertEquals("TetheredInterfaceCallback for unexpected interface",
                     downstreamIface.getInterfaceName(), iface);
 
@@ -317,7 +353,7 @@
         try {
             downstreamIface = createTestInterface();
 
-            final String iface = getTetheredInterface();
+            final String iface = mTetheredInterfaceRequester.getInterface();
             assertEquals("TetheredInterfaceCallback for unexpected interface",
                     downstreamIface.getInterfaceName(), iface);
 
@@ -335,6 +371,14 @@
 
             waitForRouterAdvertisement(downstreamReader, iface, WAIT_RA_TIMEOUT_MS);
             expectLocalOnlyAddresses(iface);
+
+            // After testing the IPv6 local address, the DHCP server may still be in the process
+            // of being created. If the downstream interface is killed by the test while the
+            // DHCP server is starting, a DHCP server error may occur. To ensure that the DHCP
+            // server has started completely before finishing the test, also test the dhcp server
+            // by calling runDhcp.
+            final TetheringTester tester = new TetheringTester(downstreamReader);
+            tester.runDhcp(MacAddress.fromString("1:2:3:4:5:6").toByteArray());
         } finally {
             maybeStopTapPacketReader(downstreamReader);
             maybeCloseTestInterface(downstreamIface);
@@ -359,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 */);
@@ -480,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 */);
@@ -512,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
@@ -563,85 +604,6 @@
         runClatUdpTest();
     }
 
-    // PacketBuilder doesn't support IPv4 ICMP packet. It may need to refactor PacketBuilder first
-    // because ICMP is a specific layer 3 protocol for PacketBuilder which expects packets always
-    // have layer 3 (IP) and layer 4 (TCP, UDP) for now. Since we don't use IPv4 ICMP packet too
-    // much in this test, we just write a ICMP packet builder here.
-    // TODO: move ICMPv4 packet build function to common utilis.
-    @NonNull
-    private ByteBuffer buildIcmpEchoPacketV4(
-            @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
-            @NonNull final Inet4Address srcIp, @NonNull final Inet4Address dstIp,
-            int type, short id, short seq) throws Exception {
-        if (type != ICMP_ECHO && type != ICMP_ECHOREPLY) {
-            fail("Unsupported ICMP type: " + type);
-        }
-
-        // Build ICMP echo id and seq fields as payload. Ignore the data field.
-        final ByteBuffer payload = ByteBuffer.allocate(4);
-        payload.putShort(id);
-        payload.putShort(seq);
-        payload.rewind();
-
-        final boolean hasEther = (srcMac != null && dstMac != null);
-        final int etherHeaderLen = hasEther ? Struct.getSize(EthernetHeader.class) : 0;
-        final int ipv4HeaderLen = Struct.getSize(Ipv4Header.class);
-        final int Icmpv4HeaderLen = Struct.getSize(Icmpv4Header.class);
-        final int payloadLen = payload.limit();
-        final ByteBuffer packet = ByteBuffer.allocate(etherHeaderLen + ipv4HeaderLen
-                + Icmpv4HeaderLen + payloadLen);
-
-        // [1] Ethernet header
-        if (hasEther) {
-            final EthernetHeader ethHeader = new EthernetHeader(dstMac, srcMac, ETHER_TYPE_IPV4);
-            ethHeader.writeToByteBuffer(packet);
-        }
-
-        // [2] IP header
-        final Ipv4Header ipv4Header = new Ipv4Header(TYPE_OF_SERVICE,
-                (short) 0 /* totalLength, calculate later */, ID,
-                FLAGS_AND_FRAGMENT_OFFSET, TIME_TO_LIVE, (byte) IPPROTO_ICMP,
-                (short) 0 /* checksum, calculate later */, srcIp, dstIp);
-        ipv4Header.writeToByteBuffer(packet);
-
-        // [3] ICMP header
-        final Icmpv4Header icmpv4Header = new Icmpv4Header((byte) type, ICMPECHO_CODE,
-                (short) 0 /* checksum, calculate later */);
-        icmpv4Header.writeToByteBuffer(packet);
-
-        // [4] Payload
-        packet.put(payload);
-        packet.flip();
-
-        // [5] Finalize packet
-        // Used for updating IP header fields. If there is Ehternet header, IPv4 header offset
-        // in buffer equals ethernet header length because IPv4 header is located next to ethernet
-        // header. Otherwise, IPv4 header offset is 0.
-        final int ipv4HeaderOffset = hasEther ? etherHeaderLen : 0;
-
-        // Populate the IPv4 totalLength field.
-        packet.putShort(ipv4HeaderOffset + IPV4_LENGTH_OFFSET,
-                (short) (ipv4HeaderLen + Icmpv4HeaderLen + payloadLen));
-
-        // Populate the IPv4 header checksum field.
-        packet.putShort(ipv4HeaderOffset + IPV4_CHECKSUM_OFFSET,
-                ipChecksum(packet, ipv4HeaderOffset /* headerOffset */));
-
-        // Populate the ICMP checksum field.
-        packet.putShort(ipv4HeaderOffset + IPV4_HEADER_MIN_LEN + ICMP_CHECKSUM_OFFSET,
-                icmpChecksum(packet, ipv4HeaderOffset + IPV4_HEADER_MIN_LEN,
-                        Icmpv4HeaderLen + payloadLen));
-        return packet;
-    }
-
-    @NonNull
-    private ByteBuffer buildIcmpEchoPacketV4(@NonNull final Inet4Address srcIp,
-            @NonNull final Inet4Address dstIp, int type, short id, short seq)
-            throws Exception {
-        return buildIcmpEchoPacketV4(null /* srcMac */, null /* dstMac */, srcIp, dstIp,
-                type, id, seq);
-    }
-
     @Test
     public void testIcmpv4Echo() throws Exception {
         final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
@@ -649,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 */,
@@ -757,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.
@@ -801,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 */,
@@ -839,4 +801,256 @@
                 REMOTE_NAT64_ADDR /* downloadSrcIp */, clatIp6 /* downloadDstIp */,
                 tester, true /* isClat */);
     }
+
+    private static final byte[] ZeroLengthDhcpPacket = new byte[] {
+            // scapy.Ether(
+            //   dst="ff:ff:ff:ff:ff:ff")
+            // scapy.IP(
+            //   dst="255.255.255.255")
+            // scapy.UDP(sport=68, dport=67)
+            /* Ethernet Header */
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+            (byte) 0xe0, (byte) 0x4f, (byte) 0x43, (byte) 0xe6, (byte) 0xfb, (byte) 0xd2,
+            (byte) 0x08, (byte) 0x00,
+            /* Ip header */
+            (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x1c, (byte) 0x00, (byte) 0x01,
+            (byte) 0x00, (byte) 0x00, (byte) 0x40, (byte) 0x11, (byte) 0xb6, (byte) 0x58,
+            (byte) 0x64, (byte) 0x4f, (byte) 0x60, (byte) 0x29, (byte) 0xff, (byte) 0xff,
+            (byte) 0xff, (byte) 0xff,
+            /* UDP header */
+            (byte) 0x00, (byte) 0x44, (byte) 0x00, (byte) 0x43,
+            (byte) 0x00, (byte) 0x08, (byte) 0x3a, (byte) 0xdf
+    };
+
+    // This test requires the update in NetworkStackModule(See b/269692093).
+    @NetworkStackModuleTest
+    @Test
+    public void testTetherZeroLengthDhcpPacket() throws Exception {
+        final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
+                toList(TEST_IP4_DNS));
+        tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
+
+        // Send a zero-length DHCP packet to upstream DHCP server.
+        final ByteBuffer packet = ByteBuffer.wrap(ZeroLengthDhcpPacket);
+        tester.sendUploadPacket(packet);
+
+        // Send DHCPDISCOVER packet from another downstream tethered device to verify that
+        // upstream DHCP server doesn't close the listening socket and stop reading, then we
+        // can still receive the next DHCP packet from server.
+        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 4f4b03c..c4d5636 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -44,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 c890197..3597a91 100644
--- a/Tethering/tests/privileged/Android.bp
+++ b/Tethering/tests/privileged/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -52,4 +53,5 @@
         "TetheringApiCurrentLib",
     ],
     compile_multilib: "both",
+    min_sdk_version: "30",
 }
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
index 328e3fb..90ceaa1 100644
--- a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -16,8 +16,6 @@
 
 package android.net.ip;
 
-import static android.net.RouteInfo.RTN_UNICAST;
-
 import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
@@ -42,12 +40,14 @@
 import android.net.INetd;
 import android.net.IpPrefix;
 import android.net.MacAddress;
-import android.net.RouteInfo;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.ArraySet;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -55,7 +55,6 @@
 
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.Ipv6Utils;
-import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
@@ -79,8 +78,6 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
-import java.util.HashSet;
-import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -239,7 +236,7 @@
                         final RdnssOption rdnss = Struct.parse(RdnssOption.class, RdnssBuf);
                         final String msg =
                                 rdnss.lifetime > 0 ? "Unknown dns" : "Unknown deprecated dns";
-                        final HashSet<Inet6Address> dnses =
+                        final ArraySet<Inet6Address> dnses =
                                 rdnss.lifetime > 0 ? mNewParams.dnses : mOldParams.dnses;
                         assertNotNull(msg, dnses);
 
@@ -332,10 +329,12 @@
         // Add a default route "fe80::/64 -> ::" to local network, otherwise, device will fail to
         // send the unicast RA out due to the ENETUNREACH error(No route to the peer's link-local
         // address is present).
-        final String iface = mTetheredParams.name;
-        final RouteInfo linkLocalRoute =
-                new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST);
-        NetdUtils.addRoutesToLocalNetwork(sNetd, iface, List.of(linkLocalRoute));
+        try {
+            sNetd.networkAddRoute(INetd.LOCAL_NET_ID, mTetheredParams.name,
+                    "fe80::/64", INetd.NEXTHOP_NONE);
+        } catch (RemoteException | ServiceSpecificException e) {
+            throw new IllegalStateException(e);
+        }
 
         final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788");
         mTetheredPacketReader.sendResponse(rs);
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 0e8b044..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, BpfMap.BPF_F_RDWR,
-                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 {
@@ -135,7 +168,7 @@
                 assertEquals(OsConstants.EPERM, expected.errno);
             }
         }
-        try (BpfMap readWriteMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+        try (BpfMap readWriteMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH,
                 TetherDownstream6Key.class, Tether6Value.class)) {
             assertNotNull(readWriteMap);
         }
@@ -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<>();
@@ -389,15 +441,25 @@
     public void testOpenNonexistentMap() throws Exception {
         try {
             final BpfMap<TetherDownstream6Key, Tether6Value> nonexistentMap = new BpfMap<>(
-                    "/sys/fs/bpf/tethering/nonexistent", BpfMap.BPF_F_RDWR,
+                    "/sys/fs/bpf/tethering/nonexistent",
                     TetherDownstream6Key.class, Tether6Value.class);
         } catch (ErrnoException expected) {
             assertEquals(OsConstants.ENOENT, expected.errno);
         }
     }
 
-    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,23 +468,82 @@
         // 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, BpfMap.BPF_F_RDWR,
-                TetherDownstream6Key.class, Tether6Value.class)) {
+                    TETHER_DOWNSTREAM6_FS_PATH,
+                    TetherDownstream6Key.class, Tether6Value.class)) {
                 // 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/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
index b3fb3e4..60f2d17 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -44,6 +44,7 @@
 import com.android.net.module.util.netlink.NetlinkUtils;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -84,6 +85,14 @@
         mOffloadHw = new OffloadHardwareInterface(mHandler, mLog, mDeps);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+    }
+
     void findConnectionOrThrow(FileDescriptor fd, InetSocketAddress local, InetSocketAddress remote)
             throws Exception {
         Log.d(TAG, "Looking for socket " + local + " -> " + remote);
@@ -106,6 +115,7 @@
                 ConntrackMessage.Tuple tuple = ctmsg.tupleOrig;
 
                 if (nlmsghdr.nlmsg_type == (NFNL_SUBSYS_CTNETLINK << 8 | IPCTNL_MSG_CT_NEW)
+                        && tuple != null
                         && tuple.protoNum == IPPROTO_TCP
                         && tuple.srcIp.equals(local.getAddress())
                         && tuple.dstIp.equals(remote.getAddress())
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 36d9a63..24407ca 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -16,6 +16,7 @@
 
 // Tests in this folder are included both in unit tests and CTS.
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -23,7 +24,7 @@
     name: "TetheringCommonTests",
     srcs: [
         "common/**/*.java",
-        "common/**/*.kt"
+        "common/**/*.kt",
     ],
     static_libs: [
         "androidx.test.rules",
@@ -95,7 +96,7 @@
     visibility: [
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
-    ]
+    ],
 }
 
 android_test {
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 46e50ef..177296a 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -34,15 +34,10 @@
 import static android.net.ip.IpServer.STATE_LOCAL_ONLY;
 import static android.net.ip.IpServer.STATE_TETHERED;
 import static android.net.ip.IpServer.STATE_UNAVAILABLE;
-import static android.system.OsConstants.ETH_P_IPV6;
+import static android.net.ip.IpServer.getTetherableIpv6Prefixes;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
-import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
-import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH;
-import static com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED;
-import static com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE;
-import static com.android.net.module.util.netlink.StructNdMsg.NUD_STALE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -59,12 +54,11 @@
 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;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -80,8 +74,6 @@
 import android.net.LinkProperties;
 import android.net.MacAddress;
 import android.net.RouteInfo;
-import android.net.TetherOffloadRuleParcel;
-import android.net.TetherStatsParcel;
 import android.net.dhcp.DhcpServerCallbacks;
 import android.net.dhcp.DhcpServingParamsParcel;
 import android.net.dhcp.IDhcpEventCallbacks;
@@ -94,35 +86,16 @@
 import android.os.test.TestLooper;
 import android.text.TextUtils;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.net.module.util.BpfMap;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.NetworkStackConstants;
+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.Struct.S32;
-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.ip.ConntrackMonitor;
-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.networkstack.tethering.BpfCoordinator;
-import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
-import com.android.networkstack.tethering.Tether6Value;
-import com.android.networkstack.tethering.TetherDevKey;
-import com.android.networkstack.tethering.TetherDevValue;
-import com.android.networkstack.tethering.TetherDownstream6Key;
-import com.android.networkstack.tethering.TetherLimitKey;
-import com.android.networkstack.tethering.TetherLimitValue;
-import com.android.networkstack.tethering.TetherUpstream6Key;
 import com.android.networkstack.tethering.TetheringConfiguration;
 import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
@@ -136,17 +109,15 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatcher;
 import org.mockito.Captor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.net.Inet4Address;
-import java.net.Inet6Address;
 import java.net.InetAddress;
-import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -158,6 +129,7 @@
     private static final String UPSTREAM_IFACE = "upstream0";
     private static final String UPSTREAM_IFACE2 = "upstream1";
     private static final String IPSEC_IFACE = "ipsec0";
+    private static final int NO_UPSTREAM = 0;
     private static final int UPSTREAM_IFINDEX = 101;
     private static final int UPSTREAM_IFINDEX2 = 102;
     private static final int IPSEC_IFINDEX = 103;
@@ -183,38 +155,40 @@
     private final LinkAddress mTestAddress = new LinkAddress("192.168.42.5/24");
     private final IpPrefix mBluetoothPrefix = new IpPrefix("192.168.44.0/24");
 
+    private static final Set<LinkAddress> NO_ADDRESSES = Set.of();
+    private static final Set<IpPrefix> NO_PREFIXES = Set.of();
+    private static final Set<LinkAddress> UPSTREAM_ADDRESSES =
+            Set.of(new LinkAddress("2001:db8:0:1234::168/64"));
+    private static final Set<IpPrefix> UPSTREAM_PREFIXES =
+            Set.of(new IpPrefix("2001:db8:0:1234::/64"));
+    private static final Set<LinkAddress> UPSTREAM_ADDRESSES2 = Set.of(
+            new LinkAddress("2001:db8:0:1234::168/64"),
+            new LinkAddress("2001:db8:0:abcd::168/64"));
+    private static final Set<IpPrefix> UPSTREAM_PREFIXES2 = Set.of(
+            new IpPrefix("2001:db8:0:1234::/64"), new IpPrefix("2001:db8:0:abcd::/64"));
+
     @Mock private INetd mNetd;
     @Mock private IpServer.Callback mCallback;
     @Mock private SharedLog mSharedLog;
     @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;
+    @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
     @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
-    @Mock private ConntrackMonitor mConntrackMonitor;
     @Mock private TetheringMetrics mTetheringMetrics;
-    @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
-    @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
-    @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
-    @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
-    @Mock private BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
-    @Mock private BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
-    @Mock private BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
-    @Mock private BpfMap<S32, S32> mBpfErrorMap;
+    @Mock private BpfCoordinator mBpfCoordinator;
 
     @Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
 
-    private final TestLooper mLooper = new TestLooper();
+    private TestLooper mLooper;
+    private Handler mHandler;
     private final ArgumentCaptor<LinkProperties> mLinkPropertiesCaptor =
             ArgumentCaptor.forClass(LinkProperties.class);
     private IpServer mIpServer;
     private InterfaceConfigurationParcel mInterfaceConfiguration;
-    private NeighborEventConsumer mNeighborEventConsumer;
-    private BpfCoordinator mBpfCoordinator;
-    private BpfCoordinator.Dependencies mBpfDeps;
 
     private void initStateMachine(int interfaceType) throws Exception {
         initStateMachine(interfaceType, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
@@ -236,19 +210,12 @@
             mInterfaceConfiguration.prefixLength = BLUETOOTH_DHCP_PREFIX_LENGTH;
         }
 
-        ArgumentCaptor<NeighborEventConsumer> neighborCaptor =
-                ArgumentCaptor.forClass(NeighborEventConsumer.class);
-        doReturn(mIpNeighborMonitor).when(mDependencies).getIpNeighborMonitor(any(), any(),
-                neighborCaptor.capture());
-
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
         when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
         when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
-        mIpServer = new IpServer(
-                IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
-                mCallback, mTetherConfig, mAddressCoordinator, mTetheringMetrics, mDependencies);
+        when(mBpfCoordinator.isUsingBpfOffload()).thenReturn(usingBpfOffload);
+        mIpServer = createIpServer(interfaceType);
         mIpServer.start();
-        mNeighborEventConsumer = neighborCaptor.getValue();
 
         // 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.
@@ -260,20 +227,32 @@
 
     private void initTetheredStateMachine(int interfaceType, String upstreamIface)
             throws Exception {
-        initTetheredStateMachine(interfaceType, upstreamIface, false,
+        initTetheredStateMachine(interfaceType, upstreamIface, NO_ADDRESSES, false,
                 DEFAULT_USING_BPF_OFFLOAD);
     }
 
     private void initTetheredStateMachine(int interfaceType, String upstreamIface,
-            boolean usingLegacyDhcp, boolean usingBpfOffload) throws Exception {
+            Set<LinkAddress> upstreamAddresses, boolean usingLegacyDhcp, boolean usingBpfOffload)
+            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);
             LinkProperties lp = new LinkProperties();
             lp.setInterfaceName(upstreamIface);
+            lp.setLinkAddresses(upstreamAddresses);
             dispatchTetherConnectionChanged(upstreamIface, lp, 0);
+            Set<IpPrefix> upstreamPrefixes = getTetherableIpv6Prefixes(lp.getLinkAddresses());
+            // 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, mCallback, mAddressCoordinator, mBpfCoordinator);
+        reset(mNetd, mBpfCoordinator, mCallback, mAddressCoordinator);
         when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
                 anyBoolean())).thenReturn(mTestAddress);
     }
@@ -301,90 +280,22 @@
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
         when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
 
-        mBpfDeps = new BpfCoordinator.Dependencies() {
-                    @NonNull
-                    public Handler getHandler() {
-                        return new Handler(mLooper.getLooper());
-                    }
-
-                    @NonNull
-                    public INetd getNetd() {
-                        return mNetd;
-                    }
-
-                    @NonNull
-                    public NetworkStatsManager getNetworkStatsManager() {
-                        return mStatsManager;
-                    }
-
-                    @NonNull
-                    public SharedLog getSharedLog() {
-                        return mSharedLog;
-                    }
-
-                    @Nullable
-                    public TetheringConfiguration getTetherConfig() {
-                        return mTetherConfig;
-                    }
-
-                    @NonNull
-                    public ConntrackMonitor getConntrackMonitor(
-                            ConntrackMonitor.ConntrackEventConsumer consumer) {
-                        return mConntrackMonitor;
-                    }
-
-                    @Nullable
-                    public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
-                        return mBpfDownstream4Map;
-                    }
-
-                    @Nullable
-                    public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
-                        return mBpfUpstream4Map;
-                    }
-
-                    @Nullable
-                    public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
-                        return mBpfDownstream6Map;
-                    }
-
-                    @Nullable
-                    public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
-                        return mBpfUpstream6Map;
-                    }
-
-                    @Nullable
-                    public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
-                        return mBpfStatsMap;
-                    }
-
-                    @Nullable
-                    public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
-                        return mBpfLimitMap;
-                    }
-
-                    @Nullable
-                    public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
-                        return mBpfDevMap;
-                    }
-
-                    @Nullable
-                    public BpfMap<S32, S32> getBpfErrorMap() {
-                        return mBpfErrorMap;
-                    }
-                };
-        mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
-
         setUpDhcpServer();
     }
 
-    @Test
-    public void startsOutAvailable() {
-        when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
-                .thenReturn(mIpNeighborMonitor);
-        mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
-                mNetd, mBpfCoordinator, mCallback, mTetherConfig, mAddressCoordinator,
+    // In order to interact with syncSM from the test, IpServer must be created in test thread.
+    private IpServer createIpServer(final int interfaceType) {
+        mLooper = new TestLooper();
+        mHandler = new Handler(mLooper.getLooper());
+        return new IpServer(IFACE_NAME, mHandler, interfaceType, mSharedLog, mNetd, mBpfCoordinator,
+                mRoutingCoordinatorManager, mCallback, mTetherConfig, mAddressCoordinator,
                 mTetheringMetrics, mDependencies);
+
+    }
+
+    @Test
+    public void startsOutAvailable() throws Exception {
+        mIpServer = createIpServer(TETHERING_BLUETOOTH);
         mIpServer.start();
         mLooper.dispatchAll();
         verify(mCallback).updateInterfaceState(
@@ -526,106 +437,120 @@
         // 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).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX,
+        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).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+        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.
-        inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+        // 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).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+        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).tetherOffloadRuleClear(mIpServer);
+        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.
+        inOrder.verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
         inOrder.verify(mNetd).tetherApplyDnsInterfaces();
         inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
         inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
@@ -633,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
@@ -690,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)));
@@ -811,458 +741,90 @@
 
     @Test
     public void doesNotStartDhcpServerIfDisabled() throws Exception {
-        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, true /* usingLegacyDhcp */,
-                DEFAULT_USING_BPF_OFFLOAD);
+        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, NO_ADDRESSES,
+                true /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
 
         verify(mDependencies, never()).makeDhcpServer(any(), any(), any());
     }
 
-    private InetAddress addr(String addr) throws Exception {
-        return InetAddresses.parseNumericAddress(addr);
-    }
-
-    private void recvNewNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
-        mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_NEWNEIGH, ifindex, addr,
-                nudState, mac));
-        mLooper.dispatchAll();
-    }
-
-    private void recvDelNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
-        mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_DELNEIGH, ifindex, addr,
-                nudState, mac));
-        mLooper.dispatchAll();
-    }
-
-    /**
-     * Custom ArgumentMatcher for TetherOffloadRuleParcel. This is needed because generated stable
-     * AIDL classes don't have equals(), so we cannot just use eq(). A custom assert, such as:
-     *
-     * private void checkFooCalled(StableParcelable p, ...) {
-     *     ArgumentCaptor<FooParam> captor = ArgumentCaptor.forClass(FooParam.class);
-     *     verify(mMock).foo(captor.capture());
-     *     Foo foo = captor.getValue();
-     *     assertFooMatchesExpectations(foo);
-     * }
-     *
-     * almost works, but not quite. This is because if the code under test calls foo() twice, the
-     * first call to checkFooCalled() matches both the calls, putting both calls into the captor,
-     * and then fails with TooManyActualInvocations. It also makes it harder to use other mockito
-     * features such as never(), inOrder(), etc.
-     *
-     * This approach isn't great because if the match fails, the error message is unhelpful
-     * (actual: "android.net.TetherOffloadRuleParcel@8c827b0" or some such), but at least it does
-     * work.
-     *
-     * TODO: consider making the error message more readable by adding a method that catching the
-     * AssertionFailedError and throwing a new assertion with more details. See
-     * NetworkMonitorTest#verifyNetworkTested.
-     *
-     * See ConnectivityServiceTest#assertRoutesAdded for an alternative approach which solves the
-     * TooManyActualInvocations problem described above by forcing the caller of the custom assert
-     * method to specify all expected invocations in one call. This is useful when the stable
-     * parcelable class being asserted on has a corresponding Java object (eg., RouteInfo and
-     * RouteInfoParcelable), and the caller can just pass in a list of them. It not useful here
-     * because there is no such object.
-     */
-    private static class TetherOffloadRuleParcelMatcher implements
-            ArgumentMatcher<TetherOffloadRuleParcel> {
-        public final int upstreamIfindex;
-        public final InetAddress dst;
-        public final MacAddress dstMac;
-
-        TetherOffloadRuleParcelMatcher(int upstreamIfindex, InetAddress dst, MacAddress dstMac) {
-            this.upstreamIfindex = upstreamIfindex;
-            this.dst = dst;
-            this.dstMac = dstMac;
-        }
-
-        public boolean matches(TetherOffloadRuleParcel parcel) {
-            return upstreamIfindex == parcel.inputInterfaceIndex
-                    && (TEST_IFACE_PARAMS.index == parcel.outputInterfaceIndex)
-                    && Arrays.equals(dst.getAddress(), parcel.destination)
-                    && (128 == parcel.prefixLength)
-                    && Arrays.equals(TEST_IFACE_PARAMS.macAddr.toByteArray(), parcel.srcL2Address)
-                    && Arrays.equals(dstMac.toByteArray(), parcel.dstL2Address);
-        }
-
-        public String toString() {
-            return String.format("TetherOffloadRuleParcelMatcher(%d, %s, %s",
-                    upstreamIfindex, dst.getHostAddress(), dstMac);
-        }
-    }
-
-    @NonNull
-    private static TetherOffloadRuleParcel matches(
-            int upstreamIfindex, InetAddress dst, MacAddress dstMac) {
-        return argThat(new TetherOffloadRuleParcelMatcher(upstreamIfindex, dst, dstMac));
-    }
-
-    @NonNull
-    private static Ipv6ForwardingRule makeForwardingRule(
-            int upstreamIfindex, @NonNull InetAddress dst, @NonNull MacAddress dstMac) {
-        return new Ipv6ForwardingRule(upstreamIfindex, TEST_IFACE_PARAMS.index,
-                (Inet6Address) dst, TEST_IFACE_PARAMS.macAddr, dstMac);
-    }
-
-    @NonNull
-    private static TetherDownstream6Key makeDownstream6Key(int upstreamIfindex,
-            @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst) {
-        return new TetherDownstream6Key(upstreamIfindex, upstreamMac, dst.getAddress());
-    }
-
-    @NonNull
-    private static Tether6Value makeDownstream6Value(@NonNull final MacAddress dstMac) {
-        return new Tether6Value(TEST_IFACE_PARAMS.index, dstMac,
-                TEST_IFACE_PARAMS.macAddr, ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
-    }
-
-    private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
-        if (inOrder != null) {
-            return inOrder.verify(t);
-        } else {
-            return verify(t);
-        }
-    }
-
-    private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder, int upstreamIfindex,
-            @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
-            @NonNull final MacAddress dstMac) throws Exception {
-        if (mBpfDeps.isAtLeastS()) {
-            verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry(
-                    makeDownstream6Key(upstreamIfindex, upstreamMac, dst),
-                    makeDownstream6Value(dstMac));
-        } else {
-            verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(upstreamIfindex, dst,
-                    dstMac));
-        }
-    }
-
-    private void verifyNeverTetherOffloadRuleAdd(int upstreamIfindex,
-            @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
-            @NonNull final MacAddress dstMac) throws Exception {
-        if (mBpfDeps.isAtLeastS()) {
-            verify(mBpfDownstream6Map, never()).updateEntry(
-                    makeDownstream6Key(upstreamIfindex, upstreamMac, dst),
-                    makeDownstream6Value(dstMac));
-        } else {
-            verify(mNetd, never()).tetherOffloadRuleAdd(matches(upstreamIfindex, dst, dstMac));
-        }
-    }
-
-    private void verifyNeverTetherOffloadRuleAdd() throws Exception {
-        if (mBpfDeps.isAtLeastS()) {
-            verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
-        } else {
-            verify(mNetd, never()).tetherOffloadRuleAdd(any());
-        }
-    }
-
-    private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder, int upstreamIfindex,
-            @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
-            @NonNull final MacAddress dstMac) throws Exception {
-        if (mBpfDeps.isAtLeastS()) {
-            verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry(makeDownstream6Key(
-                    upstreamIfindex, upstreamMac, dst));
-        } else {
-            // |dstMac| is not required for deleting rules. Used bacause tetherOffloadRuleRemove
-            // uses a whole rule to be a argument.
-            // See system/netd/server/TetherController.cpp/TetherController#removeOffloadRule.
-            verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(upstreamIfindex, dst,
-                    dstMac));
-        }
-    }
-
-    private void verifyNeverTetherOffloadRuleRemove() throws Exception {
-        if (mBpfDeps.isAtLeastS()) {
-            verify(mBpfDownstream6Map, never()).deleteEntry(any());
-        } else {
-            verify(mNetd, never()).tetherOffloadRuleRemove(any());
-        }
-    }
-
-    private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex)
-            throws Exception {
-        if (!mBpfDeps.isAtLeastS()) return;
-        final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
-                TEST_IFACE_PARAMS.macAddr);
-        final Tether6Value value = new Tether6Value(upstreamIfindex,
-                MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
-                ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
-        verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
-    }
-
-    private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder)
-            throws Exception {
-        if (!mBpfDeps.isAtLeastS()) return;
-        final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
-                TEST_IFACE_PARAMS.macAddr);
-        verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
-    }
-
-    private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
-        if (!mBpfDeps.isAtLeastS()) return;
-        if (inOrder != null) {
-            inOrder.verify(mBpfUpstream6Map, never()).deleteEntry(any());
-            inOrder.verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
-            inOrder.verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
-        } else {
-            verify(mBpfUpstream6Map, never()).deleteEntry(any());
-            verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
-            verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
-        }
-    }
-
-    @NonNull
-    private static TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) {
-        TetherStatsParcel parcel = new TetherStatsParcel();
-        parcel.ifIndex = ifIndex;
-        return parcel;
-    }
-
-    private void resetNetdBpfMapAndCoordinator() throws Exception {
-        reset(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfCoordinator);
-        // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
-        // potentially crash the test) if the stats map is empty.
-        when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
-        when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX))
-                .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX));
-        when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX2))
-                .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX2));
-        // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
-        // potentially crash the test) if the stats map is empty.
-        final TetherStatsValue allZeros = new TetherStatsValue(0, 0, 0, 0, 0, 0);
-        when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX))).thenReturn(allZeros);
-        when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX2))).thenReturn(allZeros);
-    }
-
     @Test
-    public void addRemoveipv6ForwardingRules() throws Exception {
-        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
-                DEFAULT_USING_BPF_OFFLOAD);
+    public void ipv6UpstreamInterfaceChanges() throws Exception {
+        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+                false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
 
-        final int myIfindex = TEST_IFACE_PARAMS.index;
-        final int notMyIfindex = myIfindex - 1;
-
-        final InetAddress neighA = InetAddresses.parseNumericAddress("2001:db8::1");
-        final InetAddress neighB = InetAddresses.parseNumericAddress("2001:db8::2");
-        final InetAddress neighLL = InetAddresses.parseNumericAddress("fe80::1");
-        final InetAddress neighMC = InetAddresses.parseNumericAddress("ff02::1234");
-        final MacAddress macNull = MacAddress.fromString("00:00:00:00:00:00");
-        final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a");
-        final MacAddress macB = MacAddress.fromString("11:22:33:00:00:0b");
-
-        resetNetdBpfMapAndCoordinator();
-        verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
-
-        // TODO: Perhaps verify the interaction of tetherOffloadSetInterfaceQuota and
-        // tetherOffloadGetAndClearStats in netd while the rules are changed.
-
-        // Events on other interfaces are ignored.
-        recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, macA);
-        verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
-
-        // Events on this interface are received and sent to netd.
-        recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
-        verifyTetherOffloadRuleAdd(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
-        verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
-        resetNetdBpfMapAndCoordinator();
-
-        recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
-        verifyTetherOffloadRuleAdd(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
-        verifyNoUpstreamIpv6ForwardingChange(null);
-        resetNetdBpfMapAndCoordinator();
-
-        // Link-local and multicast neighbors are ignored.
-        recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, macA);
-        verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
-        recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, macA);
-        verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
-
-        // A neighbor that is no longer valid causes the rule to be removed.
-        // NUD_FAILED events do not have a MAC address.
-        recvNewNeigh(myIfindex, neighA, NUD_FAILED, null);
-        verify(mBpfCoordinator).tetherOffloadRuleRemove(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macNull));
-        verifyTetherOffloadRuleRemove(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macNull);
-        verifyNoUpstreamIpv6ForwardingChange(null);
-        resetNetdBpfMapAndCoordinator();
-
-        // A neighbor that is deleted causes the rule to be removed.
-        recvDelNeigh(myIfindex, neighB, NUD_STALE, macB);
-        verify(mBpfCoordinator).tetherOffloadRuleRemove(
-                mIpServer,  makeForwardingRule(UPSTREAM_IFINDEX, neighB, macNull));
-        verifyTetherOffloadRuleRemove(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macNull);
-        verifyStopUpstreamIpv6Forwarding(null);
-        resetNetdBpfMapAndCoordinator();
-
-        // Upstream changes result in updating the rules.
-        recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
-        verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
-        recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
-        resetNetdBpfMapAndCoordinator();
-
-        InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+        // Upstream interface changes result in updating the rules.
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName(UPSTREAM_IFACE2);
+        lp.setLinkAddresses(UPSTREAM_ADDRESSES);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
-        verify(mBpfCoordinator).tetherOffloadRuleUpdate(mIpServer, UPSTREAM_IFINDEX2);
-        verifyTetherOffloadRuleRemove(inOrder,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
-        verifyTetherOffloadRuleRemove(inOrder,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
-        verifyStopUpstreamIpv6Forwarding(inOrder);
-        verifyTetherOffloadRuleAdd(inOrder,
-                UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
-        verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2);
-        verifyTetherOffloadRuleAdd(inOrder,
-                UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
-        verifyNoUpstreamIpv6ForwardingChange(inOrder);
-        resetNetdBpfMapAndCoordinator();
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
+        reset(mBpfCoordinator);
+
+        // Upstream link addresses change result in updating the rules.
+        LinkProperties lp2 = new LinkProperties();
+        lp2.setInterfaceName(UPSTREAM_IFACE2);
+        lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, -1);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+        reset(mBpfCoordinator);
 
         // When the upstream is lost, rules are removed.
         dispatchTetherConnectionChanged(null, null, 0);
-        // Clear function is called two times by:
+        // Upstream clear function is called two times by:
         // - 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)).tetherOffloadRuleClear(mIpServer);
-        verifyTetherOffloadRuleRemove(null,
-                UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
-        verifyTetherOffloadRuleRemove(null,
-                UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
-        verifyStopUpstreamIpv6Forwarding(inOrder);
-        resetNetdBpfMapAndCoordinator();
+        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);
-        resetNetdBpfMapAndCoordinator();
-        recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
-        // Clear function is called by #updateIpv6ForwardingRules for the IPv6 upstream is lost.
-        verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
-        verifyNoUpstreamIpv6ForwardingChange(null);
-        verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+        verify(mBpfCoordinator, never()).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
+        reset(mBpfCoordinator);
 
-        // Rules can be added again once upstream IPv6 connectivity is available.
+        // Rules are added again once upstream IPv6 connectivity is available.
         lp.setInterfaceName(UPSTREAM_IFACE);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
-        recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
-        verifyTetherOffloadRuleAdd(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
-        verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
-        verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
-        verifyNeverTetherOffloadRuleAdd(
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+        reset(mBpfCoordinator);
 
         // If upstream IPv6 connectivity is lost, rules are removed.
-        resetNetdBpfMapAndCoordinator();
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
-        verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
-        verifyTetherOffloadRuleRemove(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
-        verifyStopUpstreamIpv6Forwarding(null);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
+        reset(mBpfCoordinator);
 
-        // When the interface goes down, rules are removed.
+        // When upstream IPv6 connectivity comes back, rules are added.
         lp.setInterfaceName(UPSTREAM_IFACE);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
-        recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
-        recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
-        verifyTetherOffloadRuleAdd(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
-        verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
-        verifyTetherOffloadRuleAdd(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
-        resetNetdBpfMapAndCoordinator();
+        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).removeIpServer(mIpServer);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
+        reset(mBpfCoordinator);
+    }
+
+    @Test
+    public void removeIpServerWhenInterfaceDown() throws Exception {
+        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+                false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
 
         mIpServer.stop();
         mLooper.dispatchAll();
-        verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
-        verifyTetherOffloadRuleRemove(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
-        verifyTetherOffloadRuleRemove(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
-        verifyStopUpstreamIpv6Forwarding(null);
-        verify(mIpNeighborMonitor).stop();
-        resetNetdBpfMapAndCoordinator();
-    }
-
-    @Test
-    public void enableDisableUsingBpfOffload() throws Exception {
-        final int myIfindex = TEST_IFACE_PARAMS.index;
-        final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
-        final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a");
-        final MacAddress macNull = MacAddress.fromString("00:00:00:00:00:00");
-
-        // Expect that rules can be only added/removed when the BPF offload config is enabled.
-        // Note that the BPF offload disabled case is not a realistic test case. Because IP
-        // neighbor monitor doesn't start if BPF offload is disabled, there should have no
-        // neighbor event listening. This is used for testing the protection check just in case.
-        // TODO: Perhaps remove the BPF offload disabled case test once this check isn't needed
-        // anymore.
-
-        // [1] Enable BPF offload.
-        // A neighbor that is added or deleted causes the rule to be added or removed.
-        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
-                true /* usingBpfOffload */);
-        resetNetdBpfMapAndCoordinator();
-
-        recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macA));
-        verifyTetherOffloadRuleAdd(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macA);
-        verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
-        resetNetdBpfMapAndCoordinator();
-
-        recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
-        verify(mBpfCoordinator).tetherOffloadRuleRemove(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macNull));
-        verifyTetherOffloadRuleRemove(null,
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macNull);
-        verifyStopUpstreamIpv6Forwarding(null);
-        resetNetdBpfMapAndCoordinator();
-
-        // [2] Disable BPF offload.
-        // A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
-        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
-                false /* usingBpfOffload */);
-        resetNetdBpfMapAndCoordinator();
-
-        recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
-        verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(any(), any());
-        verifyNeverTetherOffloadRuleAdd();
-        verifyNoUpstreamIpv6ForwardingChange(null);
-        resetNetdBpfMapAndCoordinator();
-
-        recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
-        verify(mBpfCoordinator, never()).tetherOffloadRuleRemove(any(), any());
-        verifyNeverTetherOffloadRuleRemove();
-        verifyNoUpstreamIpv6ForwardingChange(null);
-        resetNetdBpfMapAndCoordinator();
-    }
-
-    @Test
-    public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception {
-        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
-                false /* usingBpfOffload */);
-
-        // IP neighbor monitor doesn't start if BPF offload is disabled.
-        verify(mIpNeighborMonitor, never()).start();
+        verify(mBpfCoordinator).removeIpServer(mIpServer);
     }
 
     private LinkProperties buildIpv6OnlyLinkProperties(final String iface) {
@@ -1518,75 +1080,4 @@
     public void testDadProxyUpdates_EnabledAfterR() throws Exception {
         checkDadProxyEnabled(true);
     }
-
-    @Test
-    public void testSkipVirtualNetworkInBpf() throws Exception {
-        initTetheredStateMachine(TETHERING_BLUETOOTH, null);
-        final LinkProperties v6Only = new LinkProperties();
-        v6Only.setInterfaceName(IPSEC_IFACE);
-        dispatchTetherConnectionChanged(IPSEC_IFACE, v6Only, 0);
-
-        verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, IPSEC_IFACE);
-        verify(mNetd).tetherAddForward(IFACE_NAME, IPSEC_IFACE);
-        verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, IPSEC_IFACE);
-
-        final int myIfindex = TEST_IFACE_PARAMS.index;
-        final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
-        final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
-        recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, mac);
-        verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(IPSEC_IFINDEX, neigh, mac));
-    }
-
-    // TODO: move to BpfCoordinatorTest once IpNeighborMonitor is migrated to BpfCoordinator.
-    @Test
-    public void addRemoveTetherClient() throws Exception {
-        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
-                DEFAULT_USING_BPF_OFFLOAD);
-
-        final int myIfindex = TEST_IFACE_PARAMS.index;
-        final int notMyIfindex = myIfindex - 1;
-
-        final InetAddress neighA = InetAddresses.parseNumericAddress("192.168.80.1");
-        final InetAddress neighB = InetAddresses.parseNumericAddress("192.168.80.2");
-        final InetAddress neighLL = InetAddresses.parseNumericAddress("169.254.0.1");
-        final InetAddress neighMC = InetAddresses.parseNumericAddress("224.0.0.1");
-        final MacAddress macNull = MacAddress.fromString("00:00:00:00:00:00");
-        final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a");
-        final MacAddress macB = MacAddress.fromString("11:22:33:00:00:0b");
-
-        // Events on other interfaces are ignored.
-        recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, macA);
-        verifyNoMoreInteractions(mBpfCoordinator);
-
-        // Events on this interface are received and sent to BpfCoordinator.
-        recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
-        verify(mBpfCoordinator).tetherOffloadClientAdd(mIpServer, new ClientInfo(myIfindex,
-                TEST_IFACE_PARAMS.macAddr, (Inet4Address) neighA, macA));
-        clearInvocations(mBpfCoordinator);
-
-        recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
-        verify(mBpfCoordinator).tetherOffloadClientAdd(mIpServer, new ClientInfo(myIfindex,
-                TEST_IFACE_PARAMS.macAddr, (Inet4Address) neighB, macB));
-        clearInvocations(mBpfCoordinator);
-
-        // Link-local and multicast neighbors are ignored.
-        recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, macA);
-        verifyNoMoreInteractions(mBpfCoordinator);
-        recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, macA);
-        verifyNoMoreInteractions(mBpfCoordinator);
-        clearInvocations(mBpfCoordinator);
-
-        // 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);
-        verify(mBpfCoordinator).tetherOffloadClientRemove(mIpServer,  new ClientInfo(myIfindex,
-                TEST_IFACE_PARAMS.macAddr, (Inet4Address) neighA, macNull));
-        clearInvocations(mBpfCoordinator);
-
-        // A neighbor that is deleted causes the client to be removed.
-        recvDelNeigh(myIfindex, neighB, NUD_STALE, macB);
-        verify(mBpfCoordinator).tetherOffloadClientRemove(mIpServer, new ClientInfo(myIfindex,
-                TEST_IFACE_PARAMS.macAddr, (Inet4Address) neighB, macNull));
-    }
 }
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 4f32f3c..e54a7e0 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -43,6 +43,11 @@
 import static com.android.net.module.util.netlink.ConntrackMessage.TupleProto;
 import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
 import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH;
+import static com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED;
+import static com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE;
+import static com.android.net.module.util.netlink.StructNdMsg.NUD_STALE;
 import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS;
 import static com.android.networkstack.tethering.BpfCoordinator.INVALID_MTU;
 import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED;
@@ -55,7 +60,9 @@
 import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
 import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
 import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.testutils.MiscAsserts.assertSameElements;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -72,8 +79,11 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.app.usage.NetworkStatsManager;
@@ -92,6 +102,8 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
@@ -113,12 +125,16 @@
 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;
 import com.android.networkstack.tethering.BpfCoordinator.BpfConntrackEventConsumer;
 import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -135,6 +151,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
+import org.mockito.verification.VerificationMode;
 
 import java.io.StringWriter;
 import java.net.Inet4Address;
@@ -144,7 +161,9 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -158,25 +177,41 @@
     private static final int TEST_NET_ID = 24;
     private static final int TEST_NET_ID2 = 25;
 
-    private static final int INVALID_IFINDEX = 0;
+    private static final int NO_UPSTREAM = 0;
     private static final int UPSTREAM_IFINDEX = 1001;
     private static final int UPSTREAM_XLAT_IFINDEX = 1002;
     private static final int UPSTREAM_IFINDEX2 = 1003;
     private static final int DOWNSTREAM_IFINDEX = 2001;
     private static final int DOWNSTREAM_IFINDEX2 = 2002;
+    private static final int IPSEC_IFINDEX = 103;
 
     private static final String UPSTREAM_IFACE = "rmnet0";
     private static final String UPSTREAM_XLAT_IFACE = "v4-rmnet0";
     private static final String UPSTREAM_IFACE2 = "wlan0";
+    private static final String DOWNSTREAM_IFACE = "downstream1";
+    private static final String DOWNSTREAM_IFACE2 = "downstream2";
+    private static final String IPSEC_IFACE = "ipsec0";
 
     private static final MacAddress DOWNSTREAM_MAC = MacAddress.fromString("12:34:56:78:90:ab");
     private static final MacAddress DOWNSTREAM_MAC2 = MacAddress.fromString("ab:90:78:56:34:12");
 
     private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a");
     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 InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
-    private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2");
+    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);
+    private static final Set<IpPrefix> UPSTREAM_PREFIXES2 =
+            Set.of(UPSTREAM_PREFIX, UPSTREAM_PREFIX2);
+    private static final Set<IpPrefix> NO_PREFIXES = Set.of();
+
+    private static final InetAddress NEIGH_A =
+            InetAddresses.parseNumericAddress("2001:db8:0:1234::1");
+    private static final InetAddress NEIGH_B =
+            InetAddresses.parseNumericAddress("2001:db8:0:1234::2");
+    private static final InetAddress NEIGH_LL = InetAddresses.parseNumericAddress("fe80::1");
+    private static final InetAddress NEIGH_MC = InetAddresses.parseNumericAddress("ff02::1234");
 
     private static final Inet4Address REMOTE_ADDR =
             (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
@@ -211,6 +246,14 @@
     private static final InterfaceParams UPSTREAM_IFACE_PARAMS2 = new InterfaceParams(
             UPSTREAM_IFACE2, UPSTREAM_IFINDEX2, MacAddress.fromString("44:55:66:00:00:0c"),
             NetworkStackConstants.ETHER_MTU);
+    private static final InterfaceParams DOWNSTREAM_IFACE_PARAMS = new InterfaceParams(
+            DOWNSTREAM_IFACE, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, NetworkStackConstants.ETHER_MTU);
+    private static final InterfaceParams DOWNSTREAM_IFACE_PARAMS2 = new InterfaceParams(
+            DOWNSTREAM_IFACE2, DOWNSTREAM_IFINDEX2, DOWNSTREAM_MAC2,
+            NetworkStackConstants.ETHER_MTU);
+    private static final InterfaceParams IPSEC_IFACE_PARAMS = new InterfaceParams(
+            IPSEC_IFACE, IPSEC_IFINDEX, MacAddress.ALL_ZEROS_ADDRESS,
+            NetworkStackConstants.ETHER_MTU);
 
     private static final Map<Integer, UpstreamInformation> UPSTREAM_INFORMATIONS = Map.of(
             UPSTREAM_IFINDEX, new UpstreamInformation(UPSTREAM_IFACE_PARAMS,
@@ -390,6 +433,7 @@
     @Mock private IpServer mIpServer2;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private ConntrackMonitor mConntrackMonitor;
+    @Mock private IpNeighborMonitor mIpNeighborMonitor;
 
     // Late init since methods must be called by the thread that created this object.
     private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
@@ -398,6 +442,7 @@
     // Late init since the object must be initialized by the BPF coordinator instance because
     // it has to access the non-static function of BPF coordinator.
     private BpfConntrackEventConsumer mConsumer;
+    private NeighborEventConsumer mNeighborEventConsumer;
     private HashMap<IpServer, HashMap<Inet4Address, ClientInfo>> mTetherClients;
 
     private long mElapsedRealtimeNanos = 0;
@@ -405,6 +450,7 @@
     private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
             ArgumentCaptor.forClass(ArrayList.class);
     private final TestLooper mTestLooper = new TestLooper();
+    private final Handler mHandler = new Handler(mTestLooper.getLooper());
     private final IBpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map =
             spy(new TestBpfMap<>(Tether4Key.class, Tether4Value.class));
     private final IBpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map =
@@ -425,7 +471,7 @@
             spy(new BpfCoordinator.Dependencies() {
                     @NonNull
                     public Handler getHandler() {
-                        return new Handler(mTestLooper.getLooper());
+                        return mHandler;
                     }
 
                     @NonNull
@@ -505,6 +551,8 @@
     @Before public void setUp() {
         MockitoAnnotations.initMocks(this);
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
+        when(mIpServer.getInterfaceParams()).thenReturn(DOWNSTREAM_IFACE_PARAMS);
+        when(mIpServer2.getInterfaceParams()).thenReturn(DOWNSTREAM_IFACE_PARAMS2);
     }
 
     private void waitForIdle() {
@@ -517,9 +565,39 @@
         when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
     }
 
+    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));
+    }
+
+    private void recvDelNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
+        mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_DELNEIGH, ifindex, addr,
+                nudState, mac));
+    }
+
     @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();
@@ -534,6 +612,10 @@
         mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
         mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
 
+        if (addDefaultIpServer) {
+            coordinator.addIpServer(mIpServer);
+        }
+
         return coordinator;
     }
 
@@ -615,10 +697,14 @@
     }
 
     private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+        return verifyWithOrder(inOrder, t, times(1));
+    }
+
+    private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t, VerificationMode mode) {
         if (inOrder != null) {
-            return inOrder.verify(t);
+            return inOrder.verify(t, mode);
         } else {
-            return verify(t);
+            return verify(t, mode);
         }
     }
 
@@ -638,22 +724,49 @@
         }
     }
 
-    private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex,
-            MacAddress downstreamMac, int upstreamIfindex) throws Exception {
+    private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex,
+            @NonNull Set<IpPrefix> upstreamPrefixes) throws Exception {
         if (!mDeps.isAtLeastS()) return;
-        final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac);
-        final Tether6Value value = new Tether6Value(upstreamIfindex,
-                MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
-                ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
-        verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
+        ArrayMap<TetherUpstream6Key, Tether6Value> expected = new ArrayMap<>();
+        for (IpPrefix upstreamPrefix : upstreamPrefixes) {
+            final byte[] prefix64 = prefixToIp64(upstreamPrefix);
+            final TetherUpstream6Key key = new TetherUpstream6Key(DOWNSTREAM_IFACE_PARAMS.index,
+                    DOWNSTREAM_IFACE_PARAMS.macAddr, prefix64);
+            final Tether6Value value = new Tether6Value(upstreamIfindex,
+                    MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS, ETH_P_IPV6,
+                    NetworkStackConstants.ETHER_MTU);
+            expected.put(key, value);
+        }
+        ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+                ArgumentCaptor.forClass(TetherUpstream6Key.class);
+        ArgumentCaptor<Tether6Value> valueCaptor =
+                ArgumentCaptor.forClass(Tether6Value.class);
+        verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).insertEntry(
+                keyCaptor.capture(), valueCaptor.capture());
+        List<TetherUpstream6Key> keys = keyCaptor.getAllValues();
+        List<Tether6Value> values = valueCaptor.getAllValues();
+        ArrayMap<TetherUpstream6Key, Tether6Value> captured = new ArrayMap<>();
+        for (int i = 0; i < keys.size(); i++) {
+            captured.put(keys.get(i), values.get(i));
+        }
+        assertEquals(expected, captured);
     }
 
-    private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex,
-            MacAddress downstreamMac)
-            throws Exception {
+    private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder,
+            @NonNull Set<IpPrefix> upstreamPrefixes) throws Exception {
         if (!mDeps.isAtLeastS()) return;
-        final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac);
-        verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
+        Set<TetherUpstream6Key> expected = new ArraySet<>();
+        for (IpPrefix upstreamPrefix : upstreamPrefixes) {
+            final byte[] prefix64 = prefixToIp64(upstreamPrefix);
+            final TetherUpstream6Key key = new TetherUpstream6Key(DOWNSTREAM_IFACE_PARAMS.index,
+                    DOWNSTREAM_IFACE_PARAMS.macAddr, prefix64);
+            expected.add(key);
+        }
+        ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+                ArgumentCaptor.forClass(TetherUpstream6Key.class);
+        verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).deleteEntry(
+                keyCaptor.capture());
+        assertEquals(expected, new ArraySet(keyCaptor.getAllValues()));
     }
 
     private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
@@ -669,8 +782,41 @@
         }
     }
 
-    private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder,
-            @NonNull Ipv6ForwardingRule rule) throws Exception {
+    private void verifyAddUpstreamRule(@Nullable InOrder inOrder,
+            @NonNull Ipv6UpstreamRule rule) throws Exception {
+        if (!mDeps.isAtLeastS()) return;
+        verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(
+                rule.makeTetherUpstream6Key(), rule.makeTether6Value());
+    }
+
+    private void verifyAddUpstreamRules(@Nullable InOrder inOrder,
+            @NonNull Set<Ipv6UpstreamRule> rules) throws Exception {
+        if (!mDeps.isAtLeastS()) return;
+        ArrayMap<TetherUpstream6Key, Tether6Value> expected = new ArrayMap<>();
+        for (Ipv6UpstreamRule rule : rules) {
+            expected.put(rule.makeTetherUpstream6Key(), rule.makeTether6Value());
+        }
+        ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+                ArgumentCaptor.forClass(TetherUpstream6Key.class);
+        ArgumentCaptor<Tether6Value> valueCaptor =
+                ArgumentCaptor.forClass(Tether6Value.class);
+        verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).insertEntry(
+                keyCaptor.capture(), valueCaptor.capture());
+        List<TetherUpstream6Key> keys = keyCaptor.getAllValues();
+        List<Tether6Value> values = valueCaptor.getAllValues();
+        ArrayMap<TetherUpstream6Key, Tether6Value> captured = new ArrayMap<>();
+        for (int i = 0; i < keys.size(); i++) {
+            captured.put(keys.get(i), values.get(i));
+        }
+        assertEquals(expected, captured);
+    }
+
+    private void verifyAddDownstreamRule(@NonNull Ipv6DownstreamRule rule) throws Exception {
+        verifyAddDownstreamRule(null, rule);
+    }
+
+    private void verifyAddDownstreamRule(@Nullable InOrder inOrder,
+            @NonNull Ipv6DownstreamRule rule) throws Exception {
         if (mDeps.isAtLeastS()) {
             verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry(
                     rule.makeTetherDownstream6Key(), rule.makeTether6Value());
@@ -679,7 +825,12 @@
         }
     }
 
-    private void verifyNeverTetherOffloadRuleAdd() throws Exception {
+    private void verifyNeverAddUpstreamRule() throws Exception {
+        if (!mDeps.isAtLeastS()) return;
+        verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+    }
+
+    private void verifyNeverAddDownstreamRule() throws Exception {
         if (mDeps.isAtLeastS()) {
             verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
         } else {
@@ -687,8 +838,34 @@
         }
     }
 
-    private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder,
-            @NonNull final Ipv6ForwardingRule rule) throws Exception {
+    private void verifyRemoveUpstreamRule(@Nullable InOrder inOrder,
+            @NonNull final Ipv6UpstreamRule rule) throws Exception {
+        if (!mDeps.isAtLeastS()) return;
+        verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(
+                rule.makeTetherUpstream6Key());
+    }
+
+    private void verifyRemoveUpstreamRules(@Nullable InOrder inOrder,
+            @NonNull Set<Ipv6UpstreamRule> rules) throws Exception {
+        if (!mDeps.isAtLeastS()) return;
+        List<TetherUpstream6Key> expected = new ArrayList<>();
+        for (Ipv6UpstreamRule rule : rules) {
+            expected.add(rule.makeTetherUpstream6Key());
+        }
+        ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+                ArgumentCaptor.forClass(TetherUpstream6Key.class);
+        verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).deleteEntry(
+                keyCaptor.capture());
+        assertSameElements(expected, keyCaptor.getAllValues());
+    }
+
+    private void verifyRemoveDownstreamRule(@NonNull final Ipv6DownstreamRule rule)
+            throws Exception {
+        verifyRemoveDownstreamRule(null, rule);
+    }
+
+    private void verifyRemoveDownstreamRule(@Nullable InOrder inOrder,
+            @NonNull final Ipv6DownstreamRule rule) throws Exception {
         if (mDeps.isAtLeastS()) {
             verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry(
                     rule.makeTetherDownstream6Key());
@@ -697,7 +874,12 @@
         }
     }
 
-    private void verifyNeverTetherOffloadRuleRemove() throws Exception {
+    private void verifyNeverRemoveUpstreamRule() throws Exception {
+        if (!mDeps.isAtLeastS()) return;
+        verify(mBpfUpstream6Map, never()).deleteEntry(any());
+    }
+
+    private void verifyNeverRemoveDownstreamRule() throws Exception {
         if (mDeps.isAtLeastS()) {
             verify(mBpfDownstream6Map, never()).deleteEntry(any());
         } else {
@@ -761,24 +943,31 @@
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
 
         // InOrder is required because mBpfStatsMap may be accessed by both
         // BpfCoordinator#tetherOffloadRuleAdd and BpfCoordinator#tetherOffloadGetAndClearStats.
         // The #verifyTetherOffloadGetAndClearStats can't distinguish who has ever called
         // mBpfStatsMap#getValue and get a wrong calling count which counts all.
-        final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
-        coordinator.tetherOffloadRuleAdd(mIpServer, rule);
-        verifyTetherOffloadRuleAdd(inOrder, rule);
+        final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfDownstream6Map, mBpfLimitMap,
+                mBpfStatsMap);
+        final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(
+                mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+        final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(
+                mobileIfIndex, NEIGH_A, MAC_A);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
+        verifyAddUpstreamRule(inOrder, upstreamRule);
+        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 netd.
+        // Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
-        coordinator.tetherOffloadRuleRemove(mIpServer, rule);
-        verifyTetherOffloadRuleRemove(inOrder, rule);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
+        verifyRemoveDownstreamRule(inOrder, downstreamRule);
+        verifyRemoveUpstreamRule(inOrder, upstreamRule);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
     }
 
@@ -800,11 +989,10 @@
 
         doReturn(usingApiS).when(mDeps).isAtLeastS();
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
                 buildTestTetherStatsParcel(mobileIfIndex, 1000, 100, 2000, 200)});
@@ -836,7 +1024,6 @@
         setupFunctioningNetdInterface();
 
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         final String wlanIface = "wlan0";
         final Integer wlanIfIndex = 100;
@@ -845,8 +1032,8 @@
 
         // Add interface name to lookup table. In realistic case, the upstream interface name will
         // be added by IpServer when IpServer has received with a new IPv6 upstream update event.
-        coordinator.addUpstreamNameToLookupTable(wlanIfIndex, wlanIface);
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+        coordinator.maybeAddUpstreamToLookupTable(wlanIfIndex, wlanIface);
+        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // [1] Both interface stats are changed.
         // Setup the tether stats of wlan and mobile interface. Note that move forward the time of
@@ -892,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.
@@ -906,11 +1093,10 @@
         setupFunctioningNetdInterface();
 
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // Verify that set quota to 0 will immediately triggers a callback.
         mTetherStatsProvider.onSetAlert(0);
@@ -937,8 +1123,37 @@
         mTetherStatsProviderCb.assertNoCallback();
     }
 
-    // The custom ArgumentMatcher simply comes from IpServerTest.
-    // TODO: move both of them into a common utility class for reusing the code.
+    /**
+     * Custom ArgumentMatcher for TetherOffloadRuleParcel. This is needed because generated stable
+     * AIDL classes don't have equals(), so we cannot just use eq(). A custom assert, such as:
+     *
+     * private void checkFooCalled(StableParcelable p, ...) {
+     *     ArgumentCaptor<@FooParam> captor = ArgumentCaptor.forClass(FooParam.class);
+     *     verify(mMock).foo(captor.capture());
+     *     Foo foo = captor.getValue();
+     *     assertFooMatchesExpectations(foo);
+     * }
+     *
+     * almost works, but not quite. This is because if the code under test calls foo() twice, the
+     * first call to checkFooCalled() matches both the calls, putting both calls into the captor,
+     * and then fails with TooManyActualInvocations. It also makes it harder to use other mockito
+     * features such as never(), inOrder(), etc.
+     *
+     * This approach isn't great because if the match fails, the error message is unhelpful
+     * (actual: "android.net.TetherOffloadRuleParcel@8c827b0" or some such), but at least it does
+     * work.
+     *
+     * TODO: consider making the error message more readable by adding a method that catching the
+     * AssertionFailedError and throwing a new assertion with more details. See
+     * NetworkMonitorTest#verifyNetworkTested.
+     *
+     * See ConnectivityServiceTest#assertRoutesAdded for an alternative approach which solves the
+     * TooManyActualInvocations problem described above by forcing the caller of the custom assert
+     * method to specify all expected invocations in one call. This is useful when the stable
+     * parcelable class being asserted on has a corresponding Java object (eg., RouteInfo and
+     * RouteInfoParcelable), and the caller can just pass in a list of them. It not useful here
+     * because there is no such object.
+     */
     private static class TetherOffloadRuleParcelMatcher implements
             ArgumentMatcher<TetherOffloadRuleParcel> {
         public final int upstreamIfindex;
@@ -947,7 +1162,7 @@
         public final MacAddress srcMac;
         public final MacAddress dstMac;
 
-        TetherOffloadRuleParcelMatcher(@NonNull Ipv6ForwardingRule rule) {
+        TetherOffloadRuleParcelMatcher(@NonNull Ipv6DownstreamRule rule) {
             upstreamIfindex = rule.upstreamIfindex;
             downstreamIfindex = rule.downstreamIfindex;
             address = rule.address;
@@ -971,46 +1186,93 @@
     }
 
     @NonNull
-    private TetherOffloadRuleParcel matches(@NonNull Ipv6ForwardingRule rule) {
+    private TetherOffloadRuleParcel matches(@NonNull Ipv6DownstreamRule rule) {
         return argThat(new TetherOffloadRuleParcelMatcher(rule));
     }
 
     @NonNull
-    private static Ipv6ForwardingRule buildTestForwardingRule(
+    private static Ipv6UpstreamRule buildTestUpstreamRule(int upstreamIfindex,
+            int downstreamIfindex, @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac) {
+        return new Ipv6UpstreamRule(upstreamIfindex, downstreamIfindex, sourcePrefix, inDstMac,
+                MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS);
+    }
+
+    @NonNull
+    private static Ipv6DownstreamRule buildTestDownstreamRule(
             int upstreamIfindex, @NonNull InetAddress address, @NonNull MacAddress dstMac) {
-        return new Ipv6ForwardingRule(upstreamIfindex, DOWNSTREAM_IFINDEX, (Inet6Address) address,
-                DOWNSTREAM_MAC, dstMac);
+        return new Ipv6DownstreamRule(upstreamIfindex, DOWNSTREAM_IFINDEX,
+                (Inet6Address) address, DOWNSTREAM_MAC, dstMac);
     }
 
     @Test
-    public void testRuleMakeTetherDownstream6Key() throws Exception {
+    public void testIpv6DownstreamRuleMakeTetherDownstream6Key() throws Exception {
         final int mobileIfIndex = 100;
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
 
         final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
-        assertEquals(key.iif, mobileIfIndex);
-        assertEquals(key.dstMac, MacAddress.ALL_ZEROS_ADDRESS);  // rawip upstream
-        assertTrue(Arrays.equals(key.neigh6, NEIGH_A.getAddress()));
+        assertEquals(mobileIfIndex, key.iif);
+        assertEquals(MacAddress.ALL_ZEROS_ADDRESS, key.dstMac);  // rawip upstream
+        assertArrayEquals(NEIGH_A.getAddress(), key.neigh6);
         // iif (4) + dstMac(6) + padding(2) + neigh6 (16) = 28.
         assertEquals(28, key.writeToBytes().length);
     }
 
     @Test
-    public void testRuleMakeTether6Value() throws Exception {
+    public void testIpv6DownstreamRuleMakeTether6Value() throws Exception {
         final int mobileIfIndex = 100;
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
 
         final Tether6Value value = rule.makeTether6Value();
-        assertEquals(value.oif, DOWNSTREAM_IFINDEX);
-        assertEquals(value.ethDstMac, MAC_A);
-        assertEquals(value.ethSrcMac, DOWNSTREAM_MAC);
-        assertEquals(value.ethProto, ETH_P_IPV6);
-        assertEquals(value.pmtu, NetworkStackConstants.ETHER_MTU);
-        // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20.
+        assertEquals(DOWNSTREAM_IFINDEX, value.oif);
+        assertEquals(MAC_A, value.ethDstMac);
+        assertEquals(DOWNSTREAM_MAC, value.ethSrcMac);
+        assertEquals(ETH_P_IPV6, value.ethProto);
+        assertEquals(NetworkStackConstants.ETHER_MTU, value.pmtu);
+        // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20
         assertEquals(20, value.writeToBytes().length);
     }
 
     @Test
+    public void testIpv6UpstreamRuleMakeTetherUpstream6Key() {
+        final byte[] bytes = new byte[]{(byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0xab, (byte) 0xcd, (byte) 0xfe, (byte) 0x00};
+        final IpPrefix prefix = new IpPrefix("2001:db8:abcd:fe00::/64");
+        final Ipv6UpstreamRule rule = buildTestUpstreamRule(UPSTREAM_IFINDEX,
+                DOWNSTREAM_IFINDEX, prefix, DOWNSTREAM_MAC);
+
+        final TetherUpstream6Key key = rule.makeTetherUpstream6Key();
+        assertEquals(DOWNSTREAM_IFINDEX, key.iif);
+        assertEquals(DOWNSTREAM_MAC, key.dstMac);
+        assertArrayEquals(bytes, key.src64);
+        // iif (4) + dstMac (6) + padding (6) + src64 (8) = 24
+        assertEquals(24, key.writeToBytes().length);
+    }
+
+    @Test
+    public void testIpv6UpstreamRuleMakeTether6Value() {
+        final IpPrefix prefix = new IpPrefix("2001:db8:abcd:fe00::/64");
+        final Ipv6UpstreamRule rule = buildTestUpstreamRule(UPSTREAM_IFINDEX,
+                DOWNSTREAM_IFINDEX, prefix, DOWNSTREAM_MAC);
+
+        final Tether6Value value = rule.makeTether6Value();
+        assertEquals(UPSTREAM_IFINDEX, value.oif);
+        assertEquals(MAC_NULL, value.ethDstMac);
+        assertEquals(MAC_NULL, value.ethSrcMac);
+        assertEquals(ETH_P_IPV6, value.ethProto);
+        assertEquals(NetworkStackConstants.ETHER_MTU, value.pmtu);
+        // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20
+        assertEquals(20, value.writeToBytes().length);
+    }
+
+    @Test
+    public void testBytesToPrefix() {
+        final byte[] bytes = new byte[]{(byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x12, (byte) 0x34};
+        final IpPrefix prefix = new IpPrefix("2001:db8:0:1234::/64");
+        assertEquals(prefix, BpfCoordinator.bytesToPrefix(bytes));
+    }
+
+    @Test
     public void testSetDataLimit() throws Exception {
         setupFunctioningNetdInterface();
 
@@ -1018,17 +1280,18 @@
 
         final String mobileIface = "rmnet_data0";
         final int mobileIfIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
 
         // [1] Default limit.
         // Set the unlimited quota as default if the service has never applied a data limit for a
         // given upstream. Note that the data limit only be applied on an upstream which has rules.
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
-        final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
-        coordinator.tetherOffloadRuleAdd(mIpServer, rule);
-        verifyTetherOffloadRuleAdd(inOrder, rule);
+        final Ipv6UpstreamRule rule = buildTestUpstreamRule(
+                mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+        final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
+        verifyAddUpstreamRule(inOrder, rule);
         inOrder.verifyNoMoreInteractions();
 
         // [2] Specific limit.
@@ -1053,7 +1316,6 @@
         }
     }
 
-    // TODO: Test the case in which the rules are changed from different IpServer objects.
     @Test
     public void testSetDataLimitOnRule6Change() throws Exception {
         setupFunctioningNetdInterface();
@@ -1062,39 +1324,43 @@
 
         final String mobileIface = "rmnet_data0";
         final int mobileIfIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(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.
         final long limit = 12345;
-        final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
+        final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
         mTetherStatsProvider.onSetLimit(mobileIface, limit);
         waitForIdle();
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
-        // Adding the first rule on current upstream immediately sends the quota to netd.
-        final Ipv6ForwardingRule ruleA = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
-        coordinator.tetherOffloadRuleAdd(mIpServer, ruleA);
-        verifyTetherOffloadRuleAdd(inOrder, ruleA);
+        // Adding the first rule on current upstream immediately sends the quota to BPF.
+        final Ipv6UpstreamRule ruleA = buildTestUpstreamRule(
+                mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+        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 netd.
-        final Ipv6ForwardingRule ruleB = buildTestForwardingRule(mobileIfIndex, NEIGH_B, MAC_B);
-        coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
-        verifyTetherOffloadRuleAdd(inOrder, ruleB);
+        // 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);
+        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 netd.
-        coordinator.tetherOffloadRuleRemove(mIpServer, ruleB);
-        verifyTetherOffloadRuleRemove(inOrder, ruleB);
+        // Removing the second rule on current upstream does not send the quota to BPF.
+        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 netd.
+        // Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
-        coordinator.tetherOffloadRuleRemove(mIpServer, ruleA);
-        verifyTetherOffloadRuleRemove(inOrder, ruleA);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
+        verifyRemoveUpstreamRule(inOrder, ruleA);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
         inOrder.verifyNoMoreInteractions();
     }
@@ -1109,8 +1375,6 @@
         final String mobileIface = "rmnet_data0";
         final Integer ethIfIndex = 100;
         final Integer mobileIfIndex = 101;
-        coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface);
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
 
         final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfLimitMap,
                 mBpfStatsMap);
@@ -1124,48 +1388,56 @@
 
         // [1] Adding rules on the upstream Ethernet.
         // Note that the default data limit is applied after the first rule is added.
-        final Ipv6ForwardingRule ethernetRuleA = buildTestForwardingRule(
+        final Ipv6UpstreamRule ethernetUpstreamRule = buildTestUpstreamRule(
+                ethIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+        final Ipv6DownstreamRule ethernetRuleA = buildTestDownstreamRule(
                 ethIfIndex, NEIGH_A, MAC_A);
-        final Ipv6ForwardingRule ethernetRuleB = buildTestForwardingRule(
+        final Ipv6DownstreamRule ethernetRuleB = buildTestDownstreamRule(
                 ethIfIndex, NEIGH_B, MAC_B);
 
-        coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleA);
-        verifyTetherOffloadRuleAdd(inOrder, ethernetRuleA);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, ethIfIndex, ethIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
-        verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, ethIfIndex);
-        coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB);
-        verifyTetherOffloadRuleAdd(inOrder, ethernetRuleB);
+        verifyAddUpstreamRule(inOrder, ethernetUpstreamRule);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
+        verifyAddDownstreamRule(inOrder, ethernetRuleA);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_B, NUD_REACHABLE, MAC_B);
+        verifyAddDownstreamRule(inOrder, ethernetRuleB);
 
         // [2] Update the existing rules from Ethernet to cellular.
-        final Ipv6ForwardingRule mobileRuleA = buildTestForwardingRule(
+        final Ipv6UpstreamRule mobileUpstreamRule = buildTestUpstreamRule(
+                mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+        final Ipv6UpstreamRule mobileUpstreamRule2 = buildTestUpstreamRule(
+                mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX2, DOWNSTREAM_MAC);
+        final Ipv6DownstreamRule mobileRuleA = buildTestDownstreamRule(
                 mobileIfIndex, NEIGH_A, MAC_A);
-        final Ipv6ForwardingRule mobileRuleB = buildTestForwardingRule(
+        final Ipv6DownstreamRule mobileRuleB = buildTestDownstreamRule(
                 mobileIfIndex, NEIGH_B, MAC_B);
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40));
 
         // Update the existing rules for upstream changes. The rules are removed and re-added one
-        // by one for updating upstream interface index by #tetherOffloadRuleUpdate.
-        coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex);
-        verifyTetherOffloadRuleRemove(inOrder, ethernetRuleA);
-        verifyTetherOffloadRuleRemove(inOrder, ethernetRuleB);
-        verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+        // by one for updating upstream interface index and prefixes by #tetherOffloadRuleUpdate.
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES2);
+        verifyRemoveDownstreamRule(inOrder, ethernetRuleA);
+        verifyRemoveDownstreamRule(inOrder, ethernetRuleB);
+        verifyRemoveUpstreamRule(inOrder, ethernetUpstreamRule);
         verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex);
-        verifyTetherOffloadRuleAdd(inOrder, mobileRuleA);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
-        verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
-                mobileIfIndex);
-        verifyTetherOffloadRuleAdd(inOrder, mobileRuleB);
+        verifyAddUpstreamRules(inOrder, Set.of(mobileUpstreamRule, mobileUpstreamRule2));
+        verifyAddDownstreamRule(inOrder, mobileRuleA);
+        verifyAddDownstreamRule(inOrder, mobileRuleB);
 
         // [3] Clear all rules for a given IpServer.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80));
-        coordinator.tetherOffloadRuleClear(mIpServer);
-        verifyTetherOffloadRuleRemove(inOrder, mobileRuleA);
-        verifyTetherOffloadRuleRemove(inOrder, mobileRuleB);
-        verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+        coordinator.clearAllIpv6Rules(mIpServer);
+        verifyRemoveDownstreamRule(inOrder, mobileRuleA);
+        verifyRemoveDownstreamRule(inOrder, mobileRuleB);
+        verifyRemoveUpstreamRules(inOrder, Set.of(mobileUpstreamRule, mobileUpstreamRule2));
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
 
         // [4] Force pushing stats update to verify that the last diff of stats is reported on all
@@ -1185,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);
@@ -1195,43 +1466,44 @@
         // The interface name lookup table can't be added.
         final String iface = "rmnet_data0";
         final Integer ifIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(ifIndex, iface);
+        coordinator.maybeAddUpstreamToLookupTable(ifIndex, iface);
         assertEquals(0, coordinator.getInterfaceNamesForTesting().size());
 
         // The rule can't be added.
         final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
         final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(ifIndex, neigh, mac);
-        coordinator.tetherOffloadRuleAdd(mIpServer, rule);
-        verifyNeverTetherOffloadRuleAdd();
-        LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules =
-                coordinator.getForwardingRulesForTesting().get(mIpServer);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(ifIndex, neigh, mac);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, neigh, NUD_REACHABLE, mac);
+        verifyNeverAddDownstreamRule();
+        LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
+                coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
         assertNull(rules);
 
         // The rule can't be removed. This is not a realistic case because adding rule is not
         // allowed. That implies no rule could be removed, cleared or updated. Verify these
         // cases just in case.
-        rules = new LinkedHashMap<Inet6Address, Ipv6ForwardingRule>();
+        rules = new LinkedHashMap<Inet6Address, Ipv6DownstreamRule>();
         rules.put(rule.address, rule);
-        coordinator.getForwardingRulesForTesting().put(mIpServer, rules);
-        coordinator.tetherOffloadRuleRemove(mIpServer, rule);
-        verifyNeverTetherOffloadRuleRemove();
-        rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
+        coordinator.getIpv6DownstreamRulesForTesting().put(mIpServer, rules);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, neigh, NUD_STALE, mac);
+        verifyNeverRemoveDownstreamRule();
+        rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
         assertNotNull(rules);
         assertEquals(1, rules.size());
 
         // The rule can't be cleared.
-        coordinator.tetherOffloadRuleClear(mIpServer);
-        verifyNeverTetherOffloadRuleRemove();
-        rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
+        coordinator.clearAllIpv6Rules(mIpServer);
+        verifyNeverRemoveDownstreamRule();
+        rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
         assertNotNull(rules);
         assertEquals(1, rules.size());
 
         // The rule can't be updated.
-        coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */);
-        verifyNeverTetherOffloadRuleRemove();
-        verifyNeverTetherOffloadRuleAdd();
-        rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
+        coordinator.updateIpv6UpstreamInterface(mIpServer, rule.upstreamIfindex + 1 /* new */,
+                UPSTREAM_PREFIXES);
+        verifyNeverRemoveDownstreamRule();
+        verifyNeverAddDownstreamRule();
+        rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
         assertNotNull(rules);
         assertEquals(1, rules.size());
     }
@@ -1405,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.
@@ -1424,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());
@@ -1452,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();
     }
 
@@ -1475,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();
     }
 
@@ -1489,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();
     }
 
@@ -1524,12 +1791,12 @@
     //
     // @param coordinator BpfCoordinator instance.
     // @param upstreamIfindex upstream interface index. can be the following values.
-    //        INVALID_IFINDEX: no upstream interface
+    //        NO_UPSTREAM: no upstream interface
     //        UPSTREAM_IFINDEX: CELLULAR (raw ip interface)
     //        UPSTREAM_IFINDEX2: WIFI (ethernet interface)
     private void setUpstreamInformationTo(final BpfCoordinator coordinator,
             @Nullable Integer upstreamIfindex) {
-        if (upstreamIfindex == INVALID_IFINDEX) {
+        if (upstreamIfindex == NO_UPSTREAM) {
             coordinator.updateUpstreamNetworkState(null);
             return;
         }
@@ -1543,7 +1810,7 @@
         // interface index.
         doReturn(upstreamInfo.interfaceParams).when(mDeps).getInterfaceParams(
                 upstreamInfo.interfaceParams.name);
-        coordinator.addUpstreamNameToLookupTable(upstreamInfo.interfaceParams.index,
+        coordinator.maybeAddUpstreamToLookupTable(upstreamInfo.interfaceParams.index,
                 upstreamInfo.interfaceParams.name);
 
         final LinkProperties lp = new LinkProperties();
@@ -1668,19 +1935,23 @@
     public void testAddDevMapRule6() throws Exception {
         final BpfCoordinator coordinator = makeBpfCoordinator();
 
-        coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
-        final Ipv6ForwardingRule ruleA = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
-        final Ipv6ForwardingRule ruleB = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B);
-
-        coordinator.tetherOffloadRuleAdd(mIpServer, ruleA);
+        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)),
                 eq(new TetherDevValue(DOWNSTREAM_IFINDEX)));
         clearInvocations(mBpfDevMap);
 
-        coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
-        verify(mBpfDevMap, never()).updateEntry(any(), any());
+        // Adding the second downstream, only the second downstream ifindex is added to DevMap,
+        // the existing upstream ifindex won't be added again.
+        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)),
+                eq(new TetherDevValue(UPSTREAM_IFINDEX)));
     }
 
     @Test
@@ -1735,7 +2006,6 @@
                 .startMocking();
         try {
             final BpfCoordinator coordinator = makeBpfCoordinator();
-            coordinator.startPolling();
             bpfMap.insertEntry(tcpKey, tcpValue);
             bpfMap.insertEntry(udpKey, udpValue);
 
@@ -1764,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));
@@ -1957,6 +2227,10 @@
                 100 /* nonzero, CT_NEW */);
     }
 
+    private static byte[] prefixToIp64(IpPrefix prefix) {
+        return Arrays.copyOf(prefix.getRawAddress(), 8);
+    }
+
     void checkRule4ExistInUpstreamDownstreamMap() throws Exception {
         assertEquals(UPSTREAM4_RULE_VALUE_A, mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_A));
         assertEquals(DOWNSTREAM4_RULE_VALUE_A, mBpfDownstream4Map.getValue(
@@ -2024,7 +2298,7 @@
         assertNull(mTetherClients.get(mIpServer2));
     }
 
-    private void asseertClientInfoExist(@NonNull IpServer ipServer,
+    private void assertClientInfoExists(@NonNull IpServer ipServer,
             @NonNull ClientInfo clientInfo) {
         HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
         assertNotNull(clients);
@@ -2033,16 +2307,16 @@
 
     // Although either ClientInfo for a given downstream (IpServer) is not found or a given
     // client address is not found on a given downstream can be treated "ClientInfo not
-    // exist", we still want to know the real reason exactly. For example, we don't the
+    // exist", we still want to know the real reason exactly. For example, we don't know the
     // exact reason in the following:
-    //   assertNull(clients == null ? clients : clients.get(clientInfo.clientAddress));
+    //   assertNull(clients == null ? clients : clients.get(clientAddress));
     // This helper only verifies the case that the downstream still has at least one client.
     // In other words, ClientInfo for a given IpServer has not been removed yet.
-    private void asseertClientInfoNotExist(@NonNull IpServer ipServer,
-            @NonNull ClientInfo clientInfo) {
+    private void assertClientInfoDoesNotExist(@NonNull IpServer ipServer,
+            @NonNull Inet4Address clientAddress) {
         HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
         assertNotNull(clients);
-        assertNull(clients.get(clientInfo.clientAddress));
+        assertNull(clients.get(clientAddress));
     }
 
     @Test
@@ -2074,12 +2348,12 @@
 
         // [3] Switch upstream from the first upstream (rawip, bpf supported) to no upstream. Clear
         // all rules.
-        setUpstreamInformationTo(coordinator, INVALID_IFINDEX);
+        setUpstreamInformationTo(coordinator, NO_UPSTREAM);
         checkRule4NotExistInUpstreamDownstreamMap();
 
         // Client information should be not deleted.
-        asseertClientInfoExist(mIpServer, CLIENT_INFO_A);
-        asseertClientInfoExist(mIpServer2, CLIENT_INFO_B);
+        assertClientInfoExists(mIpServer, CLIENT_INFO_A);
+        assertClientInfoExists(mIpServer2, CLIENT_INFO_B);
     }
 
     @Test
@@ -2094,8 +2368,8 @@
                 PRIVATE_ADDR2, MAC_B);
         coordinator.tetherOffloadClientAdd(mIpServer, clientA);
         coordinator.tetherOffloadClientAdd(mIpServer, clientB);
-        asseertClientInfoExist(mIpServer, clientA);
-        asseertClientInfoExist(mIpServer, clientB);
+        assertClientInfoExists(mIpServer, clientA);
+        assertClientInfoExists(mIpServer, clientB);
 
         // Add the rules for client A and client B.
         final Tether4Key upstream4KeyA = makeUpstream4Key(
@@ -2119,8 +2393,8 @@
         // [2] Remove client information A. Only the rules on client A should be removed and
         // the rules on client B should exist.
         coordinator.tetherOffloadClientRemove(mIpServer, clientA);
-        asseertClientInfoNotExist(mIpServer, clientA);
-        asseertClientInfoExist(mIpServer, clientB);
+        assertClientInfoDoesNotExist(mIpServer, clientA.clientAddress);
+        assertClientInfoExists(mIpServer, clientB);
         assertNull(mBpfUpstream4Map.getValue(upstream4KeyA));
         assertNull(mBpfDownstream4Map.getValue(downstream4KeyA));
         assertEquals(upstream4ValueB, mBpfUpstream4Map.getValue(upstream4KeyB));
@@ -2128,9 +2402,9 @@
 
         // [3] Remove client information B. The rules on client B should be removed.
         // Exactly, ClientInfo for a given IpServer is removed because the last client B
-        // has been removed from the downstream. Can't use the helper #asseertClientInfoExist
+        // has been removed from the downstream. Can't use the helper #assertClientInfoExists
         // to check because the container ClientInfo for a given downstream has been removed.
-        // See #asseertClientInfoExist.
+        // See #assertClientInfoExists.
         coordinator.tetherOffloadClientRemove(mIpServer, clientB);
         assertNull(mTetherClients.get(mIpServer));
         assertNull(mBpfUpstream4Map.getValue(upstream4KeyB));
@@ -2139,9 +2413,17 @@
 
     @Test
     public void testIpv6ForwardingRuleToString() throws Exception {
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
-        assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, address: 2001:db8::1, "
-                + "srcMac: 12:34:56:78:90:ab, dstMac: 00:00:00:00:00:0a", rule.toString());
+        final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A,
+                MAC_A);
+        assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, address: 2001:db8:0:1234::1, "
+                + "srcMac: 12:34:56:78:90:ab, dstMac: 00:00:00:00:00:0a",
+                downstreamRule.toString());
+        final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(
+                UPSTREAM_IFINDEX, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+        assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, "
+                + "sourcePrefix: 2001:db8:0:1234::/64, inDstMac: 12:34:56:78:90:ab, "
+                + "outSrcMac: 00:00:00:00:00:00, outDstMac: 00:00:00:00:00:00",
+                upstreamRule.toString());
     }
 
     private void verifyDump(@NonNull final BpfCoordinator coordinator) {
@@ -2177,7 +2459,7 @@
         // - dumpCounters
         //   * mBpfErrorMap
         // - dumpIpv6ForwardingRulesByDownstream
-        //   * mIpv6ForwardingRules
+        //   * mIpv6DownstreamRules
 
         // dumpBpfForwardingRulesIpv4
         mBpfDownstream4Map.insertEntry(
@@ -2188,11 +2470,12 @@
                 new TestUpstream4Value.Builder().build());
 
         // dumpBpfForwardingRulesIpv6
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
         mBpfDownstream6Map.insertEntry(rule.makeTetherDownstream6Key(), rule.makeTether6Value());
 
+        final byte[] prefix64 = prefixToIp64(UPSTREAM_PREFIX);
         final TetherUpstream6Key upstream6Key = new TetherUpstream6Key(DOWNSTREAM_IFINDEX,
-                DOWNSTREAM_MAC);
+                DOWNSTREAM_MAC, prefix64);
         final Tether6Value upstream6Value = new Tether6Value(UPSTREAM_IFINDEX,
                 MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
                 ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
@@ -2206,7 +2489,7 @@
                         0L /* txPackets */, 0L /* txBytes */, 0L /* txErrors */));
 
         // dumpDevmap
-        coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+        coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
         mBpfDevMap.insertEntry(
                 new TetherDevKey(UPSTREAM_IFINDEX),
                 new TetherDevValue(UPSTREAM_IFINDEX));
@@ -2218,12 +2501,12 @@
                 new S32(1000 /* count */));
 
         // dumpIpv6ForwardingRulesByDownstream
-        final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>>
-                ipv6ForwardingRules = coordinator.getForwardingRulesForTesting();
-        final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> addressRuleMap =
+        final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6DownstreamRule>>
+                ipv6DownstreamRules = coordinator.getIpv6DownstreamRulesForTesting();
+        final LinkedHashMap<Inet6Address, Ipv6DownstreamRule> addressRuleMap =
                 new LinkedHashMap<>();
         addressRuleMap.put(rule.address, rule);
-        ipv6ForwardingRules.put(mIpServer, addressRuleMap);
+        ipv6DownstreamRules.put(mIpServer, addressRuleMap);
 
         verifyDump(coordinator);
     }
@@ -2375,7 +2658,7 @@
         // +-------+-------+-------+-------+-------+
 
         // [1] Mobile IPv4 only
-        coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+        coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
         doReturn(UPSTREAM_IFACE_PARAMS).when(mDeps).getInterfaceParams(UPSTREAM_IFACE);
         final UpstreamNetworkState mobileIPv4UpstreamState = new UpstreamNetworkState(
                 buildUpstreamLinkProperties(UPSTREAM_IFACE,
@@ -2427,7 +2710,7 @@
         verifyIpv4Upstream(ipv4UpstreamIndices, interfaceNames);
 
         // Mobile IPv6 and xlat
-        // IpServer doesn't add xlat interface mapping via #addUpstreamNameToLookupTable on
+        // IpServer doesn't add xlat interface mapping via #maybeAddUpstreamToLookupTable on
         // S and T devices.
         coordinator.updateUpstreamNetworkState(mobile464xlatUpstreamState);
         // Upstream IPv4 address mapping is removed because xlat interface is not supported.
@@ -2442,7 +2725,7 @@
 
         // [6] Wifi IPv4 and IPv6
         // Expect that upstream index map is cleared because ether ip is not supported.
-        coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2, UPSTREAM_IFACE2);
+        coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2, UPSTREAM_IFACE2);
         doReturn(UPSTREAM_IFACE_PARAMS2).when(mDeps).getInterfaceParams(UPSTREAM_IFACE2);
         final UpstreamNetworkState wifiDualStackUpstreamState = new UpstreamNetworkState(
                 buildUpstreamLinkProperties(UPSTREAM_IFACE2,
@@ -2461,4 +2744,292 @@
     public void testUpdateUpstreamNetworkState() throws Exception {
         verifyUpdateUpstreamNetworkState();
     }
+
+    @NonNull
+    private static TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) {
+        TetherStatsParcel parcel = new TetherStatsParcel();
+        parcel.ifIndex = ifIndex;
+        return parcel;
+    }
+
+    private void resetNetdAndBpfMaps() throws Exception {
+        reset(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+        // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
+        // potentially crash the test) if the stats map is empty.
+        when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
+        when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX))
+                .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX));
+        when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX2))
+                .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX2));
+        // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
+        // potentially crash the test) if the stats map is empty.
+        final TetherStatsValue allZeros = new TetherStatsValue(0, 0, 0, 0, 0, 0);
+        when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX))).thenReturn(allZeros);
+        when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX2))).thenReturn(allZeros);
+    }
+
+    @Test
+    public void addRemoveIpv6ForwardingRules() throws Exception {
+        final int myIfindex = DOWNSTREAM_IFINDEX;
+        final int notMyIfindex = myIfindex - 1;
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
+        resetNetdAndBpfMaps();
+        verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+        // TODO: Perhaps verify the interaction of tetherOffloadSetInterfaceQuota and
+        // tetherOffloadGetAndClearStats in netd while the rules are changed.
+
+        // Events on other interfaces are ignored.
+        recvNewNeigh(notMyIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+        verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+        // Events on this interface are received and sent to BpfCoordinator.
+        recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+        final Ipv6DownstreamRule ruleA = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
+        verifyAddDownstreamRule(ruleA);
+        resetNetdAndBpfMaps();
+
+        recvNewNeigh(myIfindex, NEIGH_B, NUD_REACHABLE, MAC_B);
+        final Ipv6DownstreamRule ruleB = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B);
+        verifyAddDownstreamRule(ruleB);
+        resetNetdAndBpfMaps();
+
+        // Link-local and multicast neighbors are ignored.
+        recvNewNeigh(myIfindex, NEIGH_LL, NUD_REACHABLE, MAC_A);
+        verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+        recvNewNeigh(myIfindex, NEIGH_MC, NUD_REACHABLE, MAC_A);
+        verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+        // A neighbor that is no longer valid causes the rule to be removed.
+        // NUD_FAILED events do not have a MAC address.
+        recvNewNeigh(myIfindex, NEIGH_A, NUD_FAILED, null);
+        final Ipv6DownstreamRule ruleANull = buildTestDownstreamRule(
+                UPSTREAM_IFINDEX, NEIGH_A, MAC_NULL);
+        verifyRemoveDownstreamRule(ruleANull);
+        resetNetdAndBpfMaps();
+
+        // A neighbor that is deleted causes the rule to be removed.
+        recvDelNeigh(myIfindex, NEIGH_B, NUD_STALE, MAC_B);
+        final Ipv6DownstreamRule ruleBNull = buildTestDownstreamRule(
+                UPSTREAM_IFINDEX, NEIGH_B, MAC_NULL);
+        verifyRemoveDownstreamRule(ruleBNull);
+        resetNetdAndBpfMaps();
+
+        // Upstream interface changes result in updating the rules.
+        recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+        recvNewNeigh(myIfindex, NEIGH_B, NUD_REACHABLE, MAC_B);
+        resetNetdAndBpfMaps();
+
+        InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES);
+        final Ipv6DownstreamRule ruleA2 = buildTestDownstreamRule(
+                UPSTREAM_IFINDEX2, NEIGH_A, MAC_A);
+        final Ipv6DownstreamRule ruleB2 = buildTestDownstreamRule(
+                UPSTREAM_IFINDEX2, NEIGH_B, MAC_B);
+        verifyRemoveDownstreamRule(inOrder, ruleA);
+        verifyRemoveDownstreamRule(inOrder, ruleB);
+        verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES);
+        verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
+        verifyAddDownstreamRule(inOrder, ruleA2);
+        verifyAddDownstreamRule(inOrder, ruleB2);
+        verifyNoUpstreamIpv6ForwardingChange(inOrder);
+        resetNetdAndBpfMaps();
+
+        // 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);
+        verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+        verifyAddDownstreamRule(inOrder, ruleA2);
+        verifyAddDownstreamRule(inOrder, ruleB2);
+        resetNetdAndBpfMaps();
+
+        // When the upstream is lost, rules are removed.
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
+        verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES2);
+        verifyRemoveDownstreamRule(ruleA2);
+        verifyRemoveDownstreamRule(ruleB2);
+        // Upstream lost doesn't clear the downstream rules from the maps.
+        // Do that here.
+        recvDelNeigh(myIfindex, NEIGH_A, NUD_STALE, MAC_A);
+        recvDelNeigh(myIfindex, NEIGH_B, NUD_STALE, MAC_B);
+        resetNetdAndBpfMaps();
+
+        // If the upstream is IPv4-only, no IPv6 rules are added to BPF map.
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
+        resetNetdAndBpfMaps();
+        recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+        verifyNoUpstreamIpv6ForwardingChange(null);
+        // Downstream rules are only added to BpfCoordinator but not BPF map.
+        verifyNeverAddDownstreamRule();
+        verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+        // Rules can be added again once upstream IPv6 connectivity is available. The existing rules
+        // with an upstream of NO_UPSTREAM are reapplied.
+        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);
+        verifyAddDownstreamRule(ruleB);
+
+        // If upstream IPv6 connectivity is lost, rules are removed.
+        resetNetdAndBpfMaps();
+        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.
+        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.
+        // 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);
+        verify(mIpNeighborMonitor).stop();
+        resetNetdAndBpfMaps();
+    }
+
+    @Test
+    public void enableDisableUsingBpfOffload() throws Exception {
+        final int myIfindex = DOWNSTREAM_IFINDEX;
+
+        // Expect that rules can be only added/removed when the BPF offload config is enabled.
+        // Note that the BPF offload disabled case is not a realistic test case. Because IP
+        // neighbor monitor doesn't start if BPF offload is disabled, there should have no
+        // neighbor event listening. This is used for testing the protection check just in case.
+        // TODO: Perhaps remove the BPF offload disabled case test once this check isn't needed
+        // anymore.
+
+        // [1] Enable BPF offload.
+        // A neighbor that is added or deleted causes the rule to be added or removed.
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
+        resetNetdAndBpfMaps();
+
+        recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
+        verifyAddDownstreamRule(rule);
+        resetNetdAndBpfMaps();
+
+        recvDelNeigh(myIfindex, NEIGH_A, NUD_STALE, MAC_A);
+        final Ipv6DownstreamRule ruleNull = buildTestDownstreamRule(
+                UPSTREAM_IFINDEX, NEIGH_A, MAC_NULL);
+        verifyRemoveDownstreamRule(ruleNull);
+        resetNetdAndBpfMaps();
+
+        // Upstream IPv6 connectivity change causes upstream rules change.
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES2);
+        verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+        resetNetdAndBpfMaps();
+
+        // [2] Disable BPF offload.
+        // 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();
+        verifyNoUpstreamIpv6ForwardingChange(null);
+        resetNetdAndBpfMaps();
+
+        recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+        verifyNeverAddDownstreamRule();
+        resetNetdAndBpfMaps();
+
+        recvDelNeigh(myIfindex, NEIGH_A, NUD_STALE, MAC_A);
+        verifyNeverRemoveDownstreamRule();
+        resetNetdAndBpfMaps();
+
+        // Upstream IPv6 connectivity change doesn't cause the rule to be added or removed.
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer2, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, NO_PREFIXES);
+        verifyNoUpstreamIpv6ForwardingChange(null);
+        verifyNeverRemoveDownstreamRule();
+        resetNetdAndBpfMaps();
+    }
+
+    @Test
+    public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception {
+        when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+
+        // IP neighbor monitor doesn't start if BPF offload is disabled.
+        verify(mIpNeighborMonitor, never()).start();
+    }
+
+    @Test
+    public void testSkipVirtualNetworkInBpf() throws Exception {
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+
+        resetNetdAndBpfMaps();
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, IPSEC_IFINDEX, IPSEC_IFACE, UPSTREAM_PREFIXES);
+        verifyNeverAddUpstreamRule();
+
+        recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
+        verifyNeverAddDownstreamRule();
+    }
+
+    @Test
+    public void addRemoveTetherClient() throws Exception {
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+        final int myIfindex = DOWNSTREAM_IFINDEX;
+        final int notMyIfindex = myIfindex - 1;
+
+        final InetAddress neighA = InetAddresses.parseNumericAddress("192.168.80.1");
+        final InetAddress neighB = InetAddresses.parseNumericAddress("192.168.80.2");
+        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(mIpServer));
+
+        // Events on this interface are received and sent to BpfCoordinator.
+        recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, MAC_A);
+        assertClientInfoExists(mIpServer,
+                new ClientInfo(myIfindex, DOWNSTREAM_MAC, (Inet4Address) neighA, MAC_A));
+
+        recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, MAC_B);
+        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(mIpServer, (Inet4Address) neighLL);
+        recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, MAC_A);
+        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(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(mIpServer));
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
index 9e287a0..087be26 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
@@ -28,9 +28,8 @@
     FakeTetheringConfiguration(Context ctx, SharedLog log, int id) {
         super(ctx, log, id, new Dependencies() {
             @Override
-            boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
-                    @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) {
-                return defaultEnabled;
+            boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) {
+                return false;
             }
 
             @Override
@@ -38,6 +37,11 @@
                     boolean defaultValue) {
                 return defaultValue;
             }
+
+            @Override
+            boolean isTetherForceUpstreamAutomaticFeatureEnabled() {
+                return false;
+            }
         });
     }
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
index b1f875b..4413d26 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
@@ -20,6 +20,8 @@
 import static android.system.OsConstants.AF_UNIX;
 import static android.system.OsConstants.SOCK_STREAM;
 
+import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_DESTROY;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_NEW;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_AIDL;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_HIDL_1_0;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_HIDL_1_1;
@@ -34,6 +36,7 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -57,7 +60,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.io.FileDescriptor;
@@ -75,8 +77,9 @@
     private OffloadHardwareInterface mOffloadHw;
     private OffloadHalCallback mOffloadHalCallback;
 
-    @Mock private IOffloadHal mIOffload;
-    @Mock private NativeHandle mNativeHandle;
+    private IOffloadHal mIOffload;
+    private NativeHandle mNativeHandle1;
+    private NativeHandle mNativeHandle2;
 
     // Random values to test Netlink message.
     private static final short TEST_TYPE = 184;
@@ -97,7 +100,9 @@
 
         @Override
         public NativeHandle createConntrackSocket(final int groups) {
-            return mNativeHandle;
+            return groups == (NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY)
+                    ? mNativeHandle1
+                    : mNativeHandle2;
         }
     }
 
@@ -105,45 +110,89 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mOffloadHalCallback = new OffloadHalCallback();
-        when(mIOffload.initOffload(any(NativeHandle.class), any(NativeHandle.class),
-                any(OffloadHalCallback.class))).thenReturn(true);
+        mIOffload = mock(IOffloadHal.class);
     }
 
-    private void startOffloadHardwareInterface(int offloadHalVersion)
+    private void startOffloadHardwareInterface(int offloadHalVersion, boolean isHalInitSuccess)
             throws Exception {
+        startOffloadHardwareInterface(offloadHalVersion, isHalInitSuccess, mock(NativeHandle.class),
+                mock(NativeHandle.class));
+    }
+
+    private void startOffloadHardwareInterface(int offloadHalVersion, boolean isHalInitSuccess,
+            NativeHandle handle1, NativeHandle handle2) throws Exception {
         final SharedLog log = new SharedLog("test");
         final Handler handler = new Handler(mTestLooper.getLooper());
-        final int num = offloadHalVersion != OFFLOAD_HAL_VERSION_NONE ? 1 : 0;
+        final boolean hasNullHandle = handle1 == null || handle2 == null;
+        // If offloadHalVersion is OFFLOAD_HAL_VERSION_NONE or it has null NativeHandle arguments,
+        // mIOffload.initOffload() shouldn't be called.
+        final int initNum = (offloadHalVersion != OFFLOAD_HAL_VERSION_NONE && !hasNullHandle)
+                ? 1
+                : 0;
+        // If it is HIDL or has null NativeHandle argument, NativeHandles should be closed.
+        final int handleCloseNum = (hasNullHandle
+                || offloadHalVersion == OFFLOAD_HAL_VERSION_HIDL_1_0
+                || offloadHalVersion == OFFLOAD_HAL_VERSION_HIDL_1_1) ? 1 : 0;
+        mNativeHandle1 = handle1;
+        mNativeHandle2 = handle2;
+        when(mIOffload.initOffload(any(NativeHandle.class), any(NativeHandle.class),
+                any(OffloadHalCallback.class))).thenReturn(isHalInitSuccess);
         mOffloadHw = new OffloadHardwareInterface(handler, log,
                 new MyDependencies(handler, log, offloadHalVersion));
-        assertEquals(offloadHalVersion, mOffloadHw.initOffload(mOffloadHalCallback));
-        verify(mIOffload, times(num)).initOffload(any(NativeHandle.class), any(NativeHandle.class),
-                eq(mOffloadHalCallback));
+        assertEquals(isHalInitSuccess && !hasNullHandle
+                ? offloadHalVersion
+                : OFFLOAD_HAL_VERSION_NONE,
+                mOffloadHw.initOffload(mOffloadHalCallback));
+        verify(mIOffload, times(initNum)).initOffload(any(NativeHandle.class),
+                any(NativeHandle.class), eq(mOffloadHalCallback));
+        if (mNativeHandle1 != null) verify(mNativeHandle1, times(handleCloseNum)).close();
+        if (mNativeHandle2 != null) verify(mNativeHandle2, times(handleCloseNum)).close();
     }
 
     @Test
     public void testInitFailureWithNoHal() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_NONE);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_NONE, true);
     }
 
     @Test
     public void testInitSuccessWithAidl() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_AIDL);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_AIDL, true);
     }
 
     @Test
     public void testInitSuccessWithHidl_1_0() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
     }
 
     @Test
     public void testInitSuccessWithHidl_1_1() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_1);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_1, true);
+    }
+
+    @Test
+    public void testInitFailWithAidl() throws Exception {
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_AIDL, false);
+    }
+
+    @Test
+    public void testInitFailWithHidl_1_0() throws Exception {
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, false);
+    }
+
+    @Test
+    public void testInitFailWithHidl_1_1() throws Exception {
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_1, false);
+    }
+
+    @Test
+    public void testInitFailDueToNullHandles() throws Exception {
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_AIDL, true, mock(NativeHandle.class),
+                null);
     }
 
     @Test
     public void testGetForwardedStats() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         ForwardedStats stats = new ForwardedStats(12345, 56780);
         when(mIOffload.getForwardedStats(anyString())).thenReturn(stats);
         assertEquals(mOffloadHw.getForwardedStats(RMNET0), stats);
@@ -152,7 +201,7 @@
 
     @Test
     public void testSetLocalPrefixes() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         final ArrayList<String> localPrefixes = new ArrayList<>();
         localPrefixes.add("127.0.0.0/8");
         localPrefixes.add("fe80::/64");
@@ -165,7 +214,7 @@
 
     @Test
     public void testSetDataLimit() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         final long limit = 12345;
         when(mIOffload.setDataLimit(anyString(), anyLong())).thenReturn(true);
         assertTrue(mOffloadHw.setDataLimit(RMNET0, limit));
@@ -177,7 +226,7 @@
     @Test
     public void testSetDataWarningAndLimitFailureWithHidl_1_0() throws Exception {
         // Verify V1.0 control HAL would reject the function call with exception.
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         final long warning = 12345;
         final long limit = 67890;
         assertThrows(UnsupportedOperationException.class,
@@ -187,7 +236,7 @@
     @Test
     public void testSetDataWarningAndLimit() throws Exception {
         // Verify V1.1 control HAL could receive this function call.
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_1);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_1, true);
         final long warning = 12345;
         final long limit = 67890;
         when(mIOffload.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(true);
@@ -199,7 +248,7 @@
 
     @Test
     public void testSetUpstreamParameters() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         final String v4addr = "192.168.10.1";
         final String v4gateway = "192.168.10.255";
         final ArrayList<String> v6gws = new ArrayList<>(0);
@@ -220,7 +269,7 @@
 
     @Test
     public void testUpdateDownstream() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         final String ifName = "wlan1";
         final String prefix = "192.168.43.0/24";
         when(mIOffload.addDownstream(anyString(), anyString())).thenReturn(true);
@@ -237,7 +286,7 @@
 
     @Test
     public void testSendIpv4NfGenMsg() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         FileDescriptor writeSocket = new FileDescriptor();
         FileDescriptor readSocket = new FileDescriptor();
         try {
@@ -246,9 +295,9 @@
             fail();
             return;
         }
-        when(mNativeHandle.getFileDescriptor()).thenReturn(writeSocket);
+        when(mNativeHandle1.getFileDescriptor()).thenReturn(writeSocket);
 
-        mOffloadHw.sendIpv4NfGenMsg(mNativeHandle, TEST_TYPE, TEST_FLAGS);
+        mOffloadHw.sendIpv4NfGenMsg(mNativeHandle1, TEST_TYPE, TEST_FLAGS);
 
         ByteBuffer buffer = ByteBuffer.allocate(9823);  // Arbitrary value > expectedLen.
         buffer.order(ByteOrder.nativeOrder());
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 91b092a..2298a1a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -30,6 +30,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
@@ -104,6 +105,7 @@
         when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mConnectivityMgr);
         when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks);
         when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(false);
+        when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(false);
         setUpIpServers();
         mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
     }
@@ -126,16 +128,17 @@
 
         final LinkAddress newAddress = requestDownstreamAddress(mHotspotIpServer,
                 CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
-        final IpPrefix testDupRequest = asIpPrefix(newAddress);
-        assertNotEquals(hotspotPrefix, testDupRequest);
-        assertNotEquals(bluetoothPrefix, testDupRequest);
-        mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
+        final IpPrefix newHotspotPrefix = asIpPrefix(newAddress);
+        assertNotEquals(hotspotPrefix, newHotspotPrefix);
+        assertNotEquals(bluetoothPrefix, newHotspotPrefix);
 
         final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
                 CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         final IpPrefix usbPrefix = asIpPrefix(usbAddress);
         assertNotEquals(usbPrefix, bluetoothPrefix);
-        assertNotEquals(usbPrefix, hotspotPrefix);
+        assertNotEquals(usbPrefix, newHotspotPrefix);
+
+        mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
         mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
     }
 
@@ -571,4 +574,41 @@
         assertEquals("Wrong local hotspot prefix: ", new LinkAddress("192.168.134.5/24"),
                 localHotspotAddress);
     }
+
+    @Test
+    public void testStartedPrefixRange() throws Exception {
+        when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(true);
+
+        startedPrefixBaseTest("192.168.0.0/16", 0);
+
+        startedPrefixBaseTest("192.168.0.0/16", 1);
+
+        startedPrefixBaseTest("192.168.0.0/16", 0xffff);
+
+        startedPrefixBaseTest("172.16.0.0/12", 0x10000);
+
+        startedPrefixBaseTest("172.16.0.0/12", 0x11111);
+
+        startedPrefixBaseTest("172.16.0.0/12", 0xfffff);
+
+        startedPrefixBaseTest("10.0.0.0/8", 0x100000);
+
+        startedPrefixBaseTest("10.0.0.0/8", 0x1fffff);
+
+        startedPrefixBaseTest("10.0.0.0/8", 0xffffff);
+
+        startedPrefixBaseTest("192.168.0.0/16", 0x1000000);
+    }
+
+    private void startedPrefixBaseTest(final String expected, final int randomIntForPrefixBase)
+            throws Exception {
+        mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
+        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomIntForPrefixBase);
+        final LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
+        final IpPrefix prefixBase = new IpPrefix(expected);
+        assertTrue(address + " is not part of " + prefixBase,
+                prefixBase.containsPrefix(asIpPrefix(address)));
+
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index 3382af8..dd51c7a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -155,9 +155,8 @@
         private ArrayMap<String, Boolean> mMockFlags = new ArrayMap<>();
 
         @Override
-        boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
-                @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) {
-            return isMockFlagEnabled(name, defaultEnabled);
+        boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) {
+            return isMockFlagEnabled(name, false /* defaultEnabled */);
         }
 
         @Override
@@ -172,6 +171,12 @@
             return isMockFlagEnabled(name, defaultValue);
         }
 
+        @Override
+        boolean isTetherForceUpstreamAutomaticFeatureEnabled() {
+            return isMockFlagEnabled(TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION,
+                    false /* defaultEnabled */);
+        }
+
         private boolean isMockFlagEnabled(@NonNull String name, boolean defaultEnabled) {
             final Boolean flag = mMockFlags.getOrDefault(name, defaultEnabled);
             // Value in the map can also be null
@@ -749,4 +754,27 @@
                 new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
         assertEquals(p2pLeasesSubnetPrefixLength, p2pCfg.getP2pLeasesSubnetPrefixLength());
     }
+
+    private void setTetherEnableSyncSMFlagEnabled(Boolean enabled) {
+        mDeps.setFeatureEnabled(TetheringConfiguration.TETHER_ENABLE_SYNC_SM, enabled);
+        new TetheringConfiguration(
+                mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps).readEnableSyncSM(mMockContext);
+    }
+
+    private void assertEnableSyncSM(boolean value) {
+        assertEquals(value, TetheringConfiguration.USE_SYNC_SM);
+    }
+
+    @Test
+    public void testEnableSyncSMFlag() throws Exception {
+        // Test default disabled
+        setTetherEnableSyncSMFlagEnabled(null);
+        assertEnableSyncSM(false);
+
+        setTetherEnableSyncSMFlagEnabled(true);
+        assertEnableSyncSM(true);
+
+        setTetherEnableSyncSMFlagEnabled(false);
+        assertEnableSyncSM(false);
+    }
 }
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 c15b85e..3e0d8bc 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -68,6 +68,7 @@
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
 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.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_HIDL_1_0;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_NONE;
 import static com.android.networkstack.tethering.TestConnectivityManager.BROADCAST_FIRST;
@@ -157,7 +158,6 @@
 import android.net.ip.DadProxy;
 import android.net.ip.IpServer;
 import android.net.ip.RouterAdvertisementDaemon;
-import android.net.util.NetworkConstants;
 import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiClient;
 import android.net.wifi.WifiManager;
@@ -186,11 +186,11 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.internal.util.StateMachine;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 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.RoutingCoordinatorManager;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.ip.IpNeighborMonitor;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -240,6 +240,7 @@
     private static final String TEST_RNDIS_IFNAME = "test_rndis0";
     private static final String TEST_WIFI_IFNAME = "test_wlan0";
     private static final String TEST_WLAN_IFNAME = "test_wlan1";
+    private static final String TEST_WLAN2_IFNAME = "test_wlan2";
     private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0";
     private static final String TEST_NCM_IFNAME = "test_ncm0";
     private static final String TEST_ETH_IFNAME = "test_eth0";
@@ -290,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());
@@ -299,7 +301,7 @@
     // Like so many Android system APIs, these cannot be mocked because it is marked final.
     // We have to use the real versions.
     private final PersistableBundle mCarrierConfig = new PersistableBundle();
-    private final TestLooper mLooper = new TestLooper();
+    private TestLooper mLooper;
 
     private Vector<Intent> mIntents;
     private BroadcastInterceptingContext mServiceContext;
@@ -307,6 +309,7 @@
     private BroadcastReceiver mBroadcastReceiver;
     private Tethering mTethering;
     private TestTetheringEventCallback mTetheringEventCallback;
+    private Tethering.TetherMainSM mTetherMainSM;
     private PhoneStateListener mPhoneStateListener;
     private InterfaceConfigurationParcel mInterfaceConfiguration;
     private TetheringConfiguration mConfig;
@@ -316,6 +319,7 @@
     private SoftApCallback mSoftApCallback;
     private SoftApCallback mLocalOnlyHotspotCallback;
     private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+    private UpstreamNetworkMonitor.EventListener mEventListener;
     private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
 
     private TestConnectivityManager mCm;
@@ -392,6 +396,7 @@
             assertTrue("Non-mocked interface " + ifName,
                     ifName.equals(TEST_RNDIS_IFNAME)
                             || ifName.equals(TEST_WLAN_IFNAME)
+                            || ifName.equals(TEST_WLAN2_IFNAME)
                             || ifName.equals(TEST_WIFI_IFNAME)
                             || ifName.equals(TEST_MOBILE_IFNAME)
                             || ifName.equals(TEST_DUN_IFNAME)
@@ -400,8 +405,9 @@
                             || ifName.equals(TEST_ETH_IFNAME)
                             || ifName.equals(TEST_BT_IFNAME));
             final String[] ifaces = new String[] {
-                    TEST_RNDIS_IFNAME, TEST_WLAN_IFNAME, TEST_WIFI_IFNAME, TEST_MOBILE_IFNAME,
-                    TEST_DUN_IFNAME, TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME};
+                    TEST_RNDIS_IFNAME, TEST_WLAN_IFNAME, TEST_WLAN2_IFNAME, TEST_WIFI_IFNAME,
+                    TEST_MOBILE_IFNAME, TEST_DUN_IFNAME, TEST_P2P_IFNAME, TEST_NCM_IFNAME,
+                    TEST_ETH_IFNAME};
             return new InterfaceParams(ifName,
                     CollectionUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET,
                     MacAddress.ALL_ZEROS_ADDRESS);
@@ -427,59 +433,64 @@
     }
 
     public class MockTetheringDependencies extends TetheringDependencies {
-        StateMachine mUpstreamNetworkMonitorSM;
-        ArrayList<IpServer> mIpv6CoordinatorNotifyList;
+        ArrayList<IpServer> mAllDownstreams;
 
         @Override
-        public BpfCoordinator getBpfCoordinator(
+        public BpfCoordinator makeBpfCoordinator(
                 BpfCoordinator.Dependencies deps) {
             return mBpfCoordinator;
         }
 
         @Override
-        public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
+        public OffloadHardwareInterface makeOffloadHardwareInterface(Handler h, SharedLog log) {
             return mOffloadHardwareInterface;
         }
 
         @Override
-        public OffloadController getOffloadController(Handler h, SharedLog log,
+        public OffloadController makeOffloadController(Handler h, SharedLog log,
                 OffloadController.Dependencies deps) {
-            mOffloadCtrl = spy(super.getOffloadController(h, log, deps));
+            mOffloadCtrl = spy(super.makeOffloadController(h, log, deps));
             // Return real object here instead of mock because
             // testReportFailCallbackIfOffloadNotSupported depend on real OffloadController object.
             return mOffloadCtrl;
         }
 
         @Override
-        public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx,
-                StateMachine target, SharedLog log, int what) {
+        public UpstreamNetworkMonitor makeUpstreamNetworkMonitor(Context ctx, Handler h,
+                SharedLog log, UpstreamNetworkMonitor.EventListener listener) {
             // Use a real object instead of a mock so that some tests can use a real UNM and some
             // can use a mock.
-            mUpstreamNetworkMonitorSM = target;
-            mUpstreamNetworkMonitor = spy(super.getUpstreamNetworkMonitor(ctx, target, log, what));
+            mEventListener = listener;
+            mUpstreamNetworkMonitor = spy(super.makeUpstreamNetworkMonitor(ctx, h, log, listener));
             return mUpstreamNetworkMonitor;
         }
 
         @Override
-        public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
+        public IPv6TetheringCoordinator makeIPv6TetheringCoordinator(
                 ArrayList<IpServer> notifyList, SharedLog log) {
-            mIpv6CoordinatorNotifyList = notifyList;
+            mAllDownstreams = notifyList;
             return mIPv6TetheringCoordinator;
         }
 
         @Override
-        public IpServer.Dependencies getIpServerDependencies() {
+        public IpServer.Dependencies makeIpServerDependencies() {
             return mIpServerDependencies;
         }
 
         @Override
-        public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log,
+        public EntitlementManager makeEntitlementManager(Context ctx, Handler h, SharedLog log,
                 Runnable callback) {
-            mEntitleMgr = spy(super.getEntitlementManager(ctx, h, log, callback));
+            mEntitleMgr = spy(super.makeEntitlementManager(ctx, h, log, callback));
             return mEntitleMgr;
         }
 
         @Override
+        public RoutingCoordinatorManager getRoutingCoordinator(final Context context,
+                SharedLog log) {
+            return mRoutingCoordinatorManager;
+        }
+
+        @Override
         public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
                 int subId) {
             mConfig = spy(new FakeTetheringConfiguration(ctx, log, subId));
@@ -487,12 +498,12 @@
         }
 
         @Override
-        public INetd getINetd(Context context) {
+        public INetd getINetd(Context context, SharedLog log) {
             return mNetd;
         }
 
         @Override
-        public Looper getTetheringLooper() {
+        public Looper makeTetheringLooper() {
             return mLooper.getLooper();
         }
 
@@ -507,7 +518,7 @@
         }
 
         @Override
-        public TetheringNotificationUpdater getNotificationUpdater(Context ctx, Looper looper) {
+        public TetheringNotificationUpdater makeNotificationUpdater(Context ctx, Looper looper) {
             return mNotificationUpdater;
         }
 
@@ -517,19 +528,19 @@
         }
 
         @Override
-        public TetheringMetrics getTetheringMetrics() {
+        public TetheringMetrics makeTetheringMetrics(Context ctx) {
             return mTetheringMetrics;
         }
 
         @Override
-        public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
+        public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx,
                 TetheringConfiguration cfg) {
-            mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg);
+            mPrivateAddressCoordinator = super.makePrivateAddressCoordinator(ctx, cfg);
             return mPrivateAddressCoordinator;
         }
 
         @Override
-        public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
+        public BluetoothPanShim makeBluetoothPanShim(BluetoothPan pan) {
             try {
                 when(mBluetoothPanShim.requestTetheredInterface(
                         any(), any())).thenReturn(mTetheredInterfaceRequestShim);
@@ -556,7 +567,7 @@
             prop.addDnsServer(InetAddresses.parseNumericAddress("2001:db8::2"));
             prop.addLinkAddress(
                     new LinkAddress(InetAddresses.parseNumericAddress("2001:db8::"),
-                            NetworkConstants.RFC7421_PREFIX_LENGTH));
+                            RFC7421_PREFIX_LENGTH));
             prop.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0),
                     InetAddresses.parseNumericAddress("2001:db8::1"),
                     interfaceName, RTN_UNICAST));
@@ -642,8 +653,8 @@
                 false);
         when(mNetd.interfaceGetList())
                 .thenReturn(new String[] {
-                        TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_RNDIS_IFNAME, TEST_P2P_IFNAME,
-                        TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME});
+                        TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_WLAN2_IFNAME, TEST_RNDIS_IFNAME,
+                        TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME});
         when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
         mInterfaceConfiguration = new InterfaceConfigurationParcel();
         mInterfaceConfiguration.flags = new String[0];
@@ -669,7 +680,15 @@
 
         mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class)));
 
-        mTethering = makeTethering();
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
+    }
+
+    // In order to interact with syncSM from the test, tethering must be created in test thread.
+    private void initTetheringOnTestThread() throws Exception {
+        mLooper = new TestLooper();
+        mTethering = new Tethering(mTetheringDependencies);
+        mTetherMainSM = mTethering.getTetherMainSMForTesting();
         verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any());
         verify(mNetd).registerUnsolicitedEventListener(any());
         verifyDefaultNetworkRequestFiled();
@@ -693,9 +712,6 @@
                     localOnlyCallbackCaptor.capture());
             mLocalOnlyHotspotCallback = localOnlyCallbackCaptor.getValue();
         }
-
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
     }
 
     private void setTetheringSupported(final boolean supported) {
@@ -727,10 +743,6 @@
         doReturn(upstreamState).when(mUpstreamNetworkMonitor).selectPreferredUpstreamType(any());
     }
 
-    private Tethering makeTethering() {
-        return new Tethering(mTetheringDependencies);
-    }
-
     private TetheringRequestParcel createTetheringRequestParcel(final int type) {
         return createTetheringRequestParcel(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
     }
@@ -874,6 +886,7 @@
 
     public void failingLocalOnlyHotspotLegacyApBroadcast(
             boolean emulateInterfaceStatusChanged) throws Exception {
+        initTetheringOnTestThread();
         // Emulate externally-visible WifiManager effects, causing the
         // per-interface state machine to start up, and telling us that
         // hotspot mode is to be started.
@@ -925,6 +938,7 @@
 
     @Test
     public void testUsbConfiguredBroadcastStartsTethering() throws Exception {
+        initTetheringOnTestThread();
         UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         initTetheringUpstream(upstreamState);
         prepareUsbTethering();
@@ -1001,6 +1015,7 @@
 
     public void workingLocalOnlyHotspotEnrichedApBroadcast(
             boolean emulateInterfaceStatusChanged) throws Exception {
+        initTetheringOnTestThread();
         // Emulate externally-visible WifiManager effects, causing the
         // per-interface state machine to start up, and telling us that
         // hotspot mode is to be started.
@@ -1026,7 +1041,7 @@
      */
     private void sendIPv6TetherUpdates(UpstreamNetworkState upstreamState) {
         // IPv6TetheringCoordinator must have been notified of downstream
-        for (IpServer ipSrv : mTetheringDependencies.mIpv6CoordinatorNotifyList) {
+        for (IpServer ipSrv : mTetheringDependencies.mAllDownstreams) {
             UpstreamNetworkState ipv6OnlyState = buildMobileUpstreamState(false, true, false);
             ipSrv.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0,
                     upstreamState.linkProperties.isIpv6Provisioned()
@@ -1064,11 +1079,12 @@
 
     @Test
     public void workingMobileUsbTethering_IPv4() throws Exception {
+        initTetheringOnTestThread();
         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 */);
@@ -1078,7 +1094,8 @@
     }
 
     @Test
-    public void workingMobileUsbTethering_IPv4LegacyDhcp() {
+    public void workingMobileUsbTethering_IPv4LegacyDhcp() throws Exception {
+        initTetheringOnTestThread();
         when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
                 true);
         sendConfigurationChanged();
@@ -1091,11 +1108,12 @@
 
     @Test
     public void workingMobileUsbTethering_IPv6() throws Exception {
+        initTetheringOnTestThread();
         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.
@@ -1106,11 +1124,12 @@
 
     @Test
     public void workingMobileUsbTethering_DualStack() throws Exception {
+        initTetheringOnTestThread();
         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());
@@ -1123,16 +1142,16 @@
 
     @Test
     public void workingMobileUsbTethering_MultipleUpstreams() throws Exception {
+        initTetheringOnTestThread();
         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 */);
@@ -1142,6 +1161,7 @@
 
     @Test
     public void workingMobileUsbTethering_v6Then464xlat() throws Exception {
+        initTetheringOnTestThread();
         when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
                 TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
         when(mResources.getStringArray(R.array.config_tether_usb_regexs))
@@ -1152,30 +1172,26 @@
         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();
         initTetheringUpstream(upstreamState);
 
         // Upstream LinkProperties changed: UpstreamNetworkMonitor sends EVENT_ON_LINKPROPERTIES.
-        mTetheringDependencies.mUpstreamNetworkMonitorSM.sendMessage(
-                Tethering.TetherMainSM.EVENT_UPSTREAM_CALLBACK,
-                UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
-                0,
+        mEventListener.onUpstreamEvent(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
                 upstreamState);
         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());
@@ -1183,6 +1199,7 @@
 
     @Test
     public void configTetherUpstreamAutomaticIgnoresConfigTetherUpstreamTypes() throws Exception {
+        initTetheringOnTestThread();
         when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true);
         sendConfigurationChanged();
 
@@ -1231,6 +1248,7 @@
     }
 
     private void verifyAutomaticUpstreamSelection(boolean configAutomatic) throws Exception {
+        initTetheringOnTestThread();
         TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
         TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
         InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1330,6 +1348,7 @@
     @Test
     @IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
     public void testLegacyUpstreamSelection() throws Exception {
+        initTetheringOnTestThread();
         TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
         TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
         InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1480,6 +1499,7 @@
     // +-------+-------+-------+-------+-------+
     //
     private void verifyChooseDunUpstreamByAutomaticMode(boolean configAutomatic) throws Exception {
+        initTetheringOnTestThread();
         // Enable automatic upstream selection.
         TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
         TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
@@ -1540,6 +1560,7 @@
     //
     @Test
     public void testChooseDunUpstreamByAutomaticMode_defaultNetworkWifi() throws Exception {
+        initTetheringOnTestThread();
         TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
         TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
         TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
@@ -1591,6 +1612,7 @@
     //
     @Test
     public void testChooseDunUpstreamByAutomaticMode_loseDefaultNetworkWifi() throws Exception {
+        initTetheringOnTestThread();
         TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
         TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
         final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1632,6 +1654,7 @@
     //
     @Test
     public void testChooseDunUpstreamByAutomaticMode_defaultNetworkCell() throws Exception {
+        initTetheringOnTestThread();
         TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
         TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
         final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1676,6 +1699,7 @@
     //
     @Test
     public void testChooseDunUpstreamByAutomaticMode_loseAndRegainDun() throws Exception {
+        initTetheringOnTestThread();
         TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
         final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
         setupDunUpstreamTest(true /* configAutomatic */, inOrder);
@@ -1717,6 +1741,7 @@
     @Test
     public void testChooseDunUpstreamByAutomaticMode_switchDefaultFromWifiToCell()
             throws Exception {
+        initTetheringOnTestThread();
         TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
         TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
         TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
@@ -1754,6 +1779,7 @@
     @Test
     @IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
     public void testChooseDunUpstreamByLegacyMode() throws Exception {
+        initTetheringOnTestThread();
         // Enable Legacy upstream selection.
         TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
         TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
@@ -1846,6 +1872,7 @@
 
     @Test
     public void workingNcmTethering() throws Exception {
+        initTetheringOnTestThread();
         runNcmTethering();
 
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
@@ -1853,7 +1880,8 @@
     }
 
     @Test
-    public void workingNcmTethering_LegacyDhcp() {
+    public void workingNcmTethering_LegacyDhcp() throws Exception {
+        initTetheringOnTestThread();
         when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
                 true);
         sendConfigurationChanged();
@@ -1875,6 +1903,7 @@
     // TODO: Test with and without interfaceStatusChanged().
     @Test
     public void failingWifiTetheringLegacyApBroadcast() throws Exception {
+        initTetheringOnTestThread();
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
 
         // Emulate pressing the WiFi tethering button.
@@ -1903,6 +1932,7 @@
     // TODO: Test with and without interfaceStatusChanged().
     @Test
     public void workingWifiTetheringEnrichedApBroadcast() throws Exception {
+        initTetheringOnTestThread();
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
 
         // Emulate pressing the WiFi tethering button.
@@ -1951,6 +1981,7 @@
     // TODO: Test with and without interfaceStatusChanged().
     @Test
     public void failureEnablingIpForwarding() throws Exception {
+        initTetheringOnTestThread();
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
         doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
 
@@ -2098,7 +2129,8 @@
     }
 
     @Test
-    public void testUntetherUsbWhenRestrictionIsOn() {
+    public void testUntetherUsbWhenRestrictionIsOn() throws Exception {
+        initTetheringOnTestThread();
         // Start usb tethering and check that usb interface is tethered.
         final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         runUsbTethering(upstreamState);
@@ -2275,6 +2307,7 @@
 
     @Test
     public void testRegisterTetheringEventCallback() throws Exception {
+        initTetheringOnTestThread();
         TestTetheringEventCallback callback = new TestTetheringEventCallback();
         TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
         final TetheringInterface wifiIface = new TetheringInterface(
@@ -2339,6 +2372,7 @@
 
     @Test
     public void testReportFailCallbackIfOffloadNotSupported() throws Exception {
+        initTetheringOnTestThread();
         final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
         TestTetheringEventCallback callback = new TestTetheringEventCallback();
         mTethering.registerTetheringEventCallback(callback);
@@ -2378,6 +2412,7 @@
 
     @Test
     public void testMultiSimAware() throws Exception {
+        initTetheringOnTestThread();
         final TetheringConfiguration initailConfig = mTethering.getTetheringConfiguration();
         assertEquals(INVALID_SUBSCRIPTION_ID, initailConfig.activeDataSubId);
 
@@ -2390,6 +2425,7 @@
 
     @Test
     public void testNoDuplicatedEthernetRequest() throws Exception {
+        initTetheringOnTestThread();
         final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
         when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
@@ -2410,6 +2446,7 @@
 
     private void workingWifiP2pGroupOwner(
             boolean emulateInterfaceStatusChanged) throws Exception {
+        initTetheringOnTestThread();
         if (emulateInterfaceStatusChanged) {
             mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
         }
@@ -2449,6 +2486,7 @@
 
     private void workingWifiP2pGroupClient(
             boolean emulateInterfaceStatusChanged) throws Exception {
+        initTetheringOnTestThread();
         if (emulateInterfaceStatusChanged) {
             mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
         }
@@ -2489,6 +2527,7 @@
 
     private void workingWifiP2pGroupOwnerLegacyMode(
             boolean emulateInterfaceStatusChanged) throws Exception {
+        initTetheringOnTestThread();
         // change to legacy mode and update tethering information by chaning SIM
         when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
                 .thenReturn(new String[]{});
@@ -2538,7 +2577,8 @@
     }
 
     @Test
-    public void testDataSaverChanged() {
+    public void testDataSaverChanged() throws Exception {
+        initTetheringOnTestThread();
         // Start Tethering.
         final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         runUsbTethering(upstreamState);
@@ -2593,6 +2633,7 @@
 
     @Test
     public void testMultipleStartTethering() throws Exception {
+        initTetheringOnTestThread();
         final LinkAddress serverLinkAddr = new LinkAddress("192.168.20.1/24");
         final LinkAddress clientLinkAddr = new LinkAddress("192.168.20.42/24");
         final String serverAddr = "192.168.20.1";
@@ -2636,6 +2677,7 @@
 
     @Test
     public void testRequestStaticIp() throws Exception {
+        initTetheringOnTestThread();
         when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
                 TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
         when(mResources.getStringArray(R.array.config_tether_usb_regexs))
@@ -2665,15 +2707,14 @@
     }
 
     @Test
-    public void testUpstreamNetworkChanged() {
-        final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
-                mTetheringDependencies.mUpstreamNetworkMonitorSM;
+    public void testUpstreamNetworkChanged() throws Exception {
+        initTetheringOnTestThread();
         final InOrder inOrder = inOrder(mNotificationUpdater);
 
         // Gain upstream.
         final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         initTetheringUpstream(upstreamState);
-        stateMachine.chooseUpstreamType(true);
+        mTetherMainSM.chooseUpstreamType(true);
         mTetheringEventCallback.expectUpstreamChanged(upstreamState.network);
         inOrder.verify(mNotificationUpdater)
                 .onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
@@ -2681,43 +2722,43 @@
         // Set the upstream with the same network ID but different object and the same capability.
         final UpstreamNetworkState upstreamState2 = buildMobileIPv4UpstreamState();
         initTetheringUpstream(upstreamState2);
-        stateMachine.chooseUpstreamType(true);
-        // Bug: duplicated upstream change event.
-        mTetheringEventCallback.expectUpstreamChanged(upstreamState2.network);
-        inOrder.verify(mNotificationUpdater)
-                .onUpstreamCapabilitiesChanged(upstreamState2.networkCapabilities);
+        mTetherMainSM.chooseUpstreamType(true);
+        // Expect that no upstream change event and capabilities changed event.
+        mTetheringEventCallback.assertNoUpstreamChangeCallback();
+        inOrder.verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
 
         // Set the upstream with the same network ID but different object and different capability.
         final UpstreamNetworkState upstreamState3 = buildMobileIPv4UpstreamState();
         assertFalse(upstreamState3.networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED));
         upstreamState3.networkCapabilities.addCapability(NET_CAPABILITY_VALIDATED);
         initTetheringUpstream(upstreamState3);
-        stateMachine.chooseUpstreamType(true);
-        // Bug: duplicated upstream change event.
-        mTetheringEventCallback.expectUpstreamChanged(upstreamState3.network);
+        mTetherMainSM.chooseUpstreamType(true);
+        // Expect that no upstream change event and capabilities changed event.
+        mTetheringEventCallback.assertNoUpstreamChangeCallback();
+        mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState3);
         inOrder.verify(mNotificationUpdater)
                 .onUpstreamCapabilitiesChanged(upstreamState3.networkCapabilities);
 
+
         // Lose upstream.
         initTetheringUpstream(null);
-        stateMachine.chooseUpstreamType(true);
+        mTetherMainSM.chooseUpstreamType(true);
         mTetheringEventCallback.expectUpstreamChanged(NULL_NETWORK);
         inOrder.verify(mNotificationUpdater).onUpstreamCapabilitiesChanged(null);
     }
 
     @Test
-    public void testUpstreamCapabilitiesChanged() {
-        final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
-                mTetheringDependencies.mUpstreamNetworkMonitorSM;
+    public void testUpstreamCapabilitiesChanged() throws Exception {
+        initTetheringOnTestThread();
         final InOrder inOrder = inOrder(mNotificationUpdater);
         final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         initTetheringUpstream(upstreamState);
 
-        stateMachine.chooseUpstreamType(true);
+        mTetherMainSM.chooseUpstreamType(true);
         inOrder.verify(mNotificationUpdater)
                 .onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
 
-        stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
+        mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
         inOrder.verify(mNotificationUpdater)
                 .onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
 
@@ -2726,7 +2767,7 @@
         // Expect that capability is changed with new capability VALIDATED.
         assertFalse(upstreamState.networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED));
         upstreamState.networkCapabilities.addCapability(NET_CAPABILITY_VALIDATED);
-        stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
+        mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
         inOrder.verify(mNotificationUpdater)
                 .onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
 
@@ -2735,12 +2776,13 @@
         final UpstreamNetworkState upstreamState2 = new UpstreamNetworkState(
                 upstreamState.linkProperties, upstreamState.networkCapabilities,
                 new Network(WIFI_NETID));
-        stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2);
+        mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2);
         inOrder.verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
     }
 
     @Test
     public void testUpstreamCapabilitiesChanged_startStopTethering() throws Exception {
+        initTetheringOnTestThread();
         final TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
 
         // Start USB tethering with no current upstream.
@@ -2762,19 +2804,19 @@
 
     @Test
     public void testDumpTetheringLog() throws Exception {
+        initTetheringOnTestThread();
         final FileDescriptor mockFd = mock(FileDescriptor.class);
         final PrintWriter mockPw = mock(PrintWriter.class);
         runUsbTethering(null);
-        mLooper.startAutoDispatch();
         mTethering.dump(mockFd, mockPw, new String[0]);
         verify(mConfig).dump(any());
         verify(mEntitleMgr).dump(any());
         verify(mOffloadCtrl).dump(any());
-        mLooper.stopAutoDispatch();
     }
 
     @Test
     public void testExemptFromEntitlementCheck() throws Exception {
+        initTetheringOnTestThread();
         setupForRequiredProvisioning();
         final TetheringRequestParcel wifiNotExemptRequest =
                 createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
@@ -2855,41 +2897,48 @@
             final String iface, final int transportType) {
         final UpstreamNetworkState upstream = buildV4UpstreamState(ipv4Address, network, iface,
                 transportType);
-        mTetheringDependencies.mUpstreamNetworkMonitorSM.sendMessage(
-                Tethering.TetherMainSM.EVENT_UPSTREAM_CALLBACK,
-                UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
-                0,
-                upstream);
+        mEventListener.onUpstreamEvent(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES, upstream);
         mLooper.dispatchAll();
     }
 
     @Test
     public void testHandleIpConflict() throws Exception {
+        initTetheringOnTestThread();
         final Network wifiNetwork = new Network(200);
         final Network[] allNetworks = { wifiNetwork };
         doReturn(allNetworks).when(mCm).getAllNetworks();
+        InOrder inOrder = inOrder(mUsbManager, mNetd);
         runUsbTethering(null);
+
+        inOrder.verify(mNetd).tetherInterfaceAdd(TEST_RNDIS_IFNAME);
+
         final ArgumentCaptor<InterfaceConfigurationParcel> ifaceConfigCaptor =
                 ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
         verify(mNetd).interfaceSetCfg(ifaceConfigCaptor.capture());
         final String ipv4Address = ifaceConfigCaptor.getValue().ipv4Addr;
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
-        reset(mUsbManager);
 
         // Cause a prefix conflict by assigning a /30 out of the downstream's /24 to the upstream.
         updateV4Upstream(new LinkAddress(InetAddresses.parseNumericAddress(ipv4Address), 30),
                 wifiNetwork, TEST_WIFI_IFNAME, TRANSPORT_WIFI);
         // verify turn off usb tethering
-        verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+        inOrder.verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
         sendUsbBroadcast(true, true, -1 /* function */);
         mLooper.dispatchAll();
+        inOrder.verify(mNetd).tetherInterfaceRemove(TEST_RNDIS_IFNAME);
+
         // verify restart usb tethering
-        verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+        inOrder.verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+
+        sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
+        mLooper.dispatchAll();
+        inOrder.verify(mNetd).tetherInterfaceAdd(TEST_RNDIS_IFNAME);
     }
 
     @Test
     public void testNoAddressAvailable() throws Exception {
+        initTetheringOnTestThread();
         final Network wifiNetwork = new Network(200);
         final Network btNetwork = new Network(201);
         final Network mobileNetwork = new Network(202);
@@ -2951,6 +3000,7 @@
 
     @Test
     public void testProvisioningNeededButUnavailable() throws Exception {
+        initTetheringOnTestThread();
         assertTrue(mTethering.isTetheringSupported());
         verify(mPackageManager, never()).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES);
 
@@ -2968,6 +3018,7 @@
 
     @Test
     public void testUpdateConnectedClients() throws Exception {
+        initTetheringOnTestThread();
         TestTetheringEventCallback callback = new TestTetheringEventCallback();
         runAsShell(NETWORK_SETTINGS, () -> {
             mTethering.registerTetheringEventCallback(callback);
@@ -2988,9 +3039,9 @@
         final MacAddress testMac1 = MacAddress.fromString("11:11:11:11:11:11");
         final DhcpLeaseParcelable p2pLease = createDhcpLeaseParcelable("clientId1", testMac1,
                 "192.168.50.24", 24, Long.MAX_VALUE, "test1");
-        final List<TetheredClient> p2pClients = notifyDhcpLeasesChanged(TETHERING_WIFI_P2P,
+        final List<TetheredClient> connectedClients = notifyDhcpLeasesChanged(TETHERING_WIFI_P2P,
                 eventCallbacks, p2pLease);
-        callback.expectTetheredClientChanged(p2pClients);
+        callback.expectTetheredClientChanged(connectedClients);
         reset(mDhcpServer);
 
         // Run wifi tethering.
@@ -2999,21 +3050,11 @@
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
                 any(), dhcpEventCbsCaptor.capture());
         eventCallbacks = dhcpEventCbsCaptor.getValue();
-        // Update mac address from softAp callback before getting dhcp lease.
         final MacAddress testMac2 = MacAddress.fromString("22:22:22:22:22:22");
-        final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac2,
-                false /* isLocalOnly */);
-        final List<TetheredClient> p2pAndNoAddrClients = new ArrayList<>(p2pClients);
-        p2pAndNoAddrClients.add(noAddrClient);
-        callback.expectTetheredClientChanged(p2pAndNoAddrClients);
-
-        // Update dhcp lease for wifi tethering.
         final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId2", testMac2,
                 "192.168.43.24", 24, Long.MAX_VALUE, "test2");
-        final List<TetheredClient> p2pAndWifiClients = new ArrayList<>(p2pClients);
-        p2pAndWifiClients.addAll(notifyDhcpLeasesChanged(TETHERING_WIFI,
-                eventCallbacks, wifiLease));
-        callback.expectTetheredClientChanged(p2pAndWifiClients);
+        verifyHotspotClientUpdate(false /* isLocalOnly */, testMac2, wifiLease, connectedClients,
+                eventCallbacks, callback);
 
         // Test onStarted callback that register second callback when tethering is running.
         TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
@@ -3021,12 +3062,13 @@
             mTethering.registerTetheringEventCallback(callback2);
             mLooper.dispatchAll();
         });
-        callback2.expectTetheredClientChanged(p2pAndWifiClients);
+        callback2.expectTetheredClientChanged(connectedClients);
     }
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testUpdateConnectedClientsForLocalOnlyHotspot() throws Exception {
+        initTetheringOnTestThread();
         TestTetheringEventCallback callback = new TestTetheringEventCallback();
         runAsShell(NETWORK_SETTINGS, () -> {
             mTethering.registerTetheringEventCallback(callback);
@@ -3043,26 +3085,87 @@
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
                 any(), dhcpEventCbsCaptor.capture());
         final IDhcpEventCallbacks eventCallbacks = dhcpEventCbsCaptor.getValue();
-        // Update mac address from softAp callback before getting dhcp lease.
-        final MacAddress testMac = MacAddress.fromString("22:22:22:22:22:22");
-        final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac,
-                true /* isLocalOnly */);
-        final List<TetheredClient> noAddrLocalOnlyClients = new ArrayList<>();
-        noAddrLocalOnlyClients.add(noAddrClient);
-        callback.expectTetheredClientChanged(noAddrLocalOnlyClients);
 
-        // Update dhcp lease for local only hotspot.
+        final List<TetheredClient> connectedClients = new ArrayList<>();
+        final MacAddress testMac = MacAddress.fromString("22:22:22:22:22:22");
         final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId", testMac,
                 "192.168.43.24", 24, Long.MAX_VALUE, "test");
-        final List<TetheredClient> localOnlyClients = notifyDhcpLeasesChanged(TETHERING_WIFI,
-                eventCallbacks, wifiLease);
-        callback.expectTetheredClientChanged(localOnlyClients);
+        verifyHotspotClientUpdate(true /* isLocalOnly */, testMac, wifiLease, connectedClients,
+                eventCallbacks, callback);
 
         // Client disconnect from local only hotspot.
         mLocalOnlyHotspotCallback.onConnectedClientsChanged(Collections.emptyList());
         callback.expectTetheredClientChanged(Collections.emptyList());
     }
 
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testConnectedClientsForSapAndLohsConcurrency() throws Exception {
+        initTetheringOnTestThread();
+        TestTetheringEventCallback callback = new TestTetheringEventCallback();
+        runAsShell(NETWORK_SETTINGS, () -> {
+            mTethering.registerTetheringEventCallback(callback);
+            mLooper.dispatchAll();
+        });
+        callback.expectTetheredClientChanged(Collections.emptyList());
+
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+        final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
+                 ArgumentCaptor.forClass(IDhcpEventCallbacks.class);
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+                any(), dhcpEventCbsCaptor.capture());
+        IDhcpEventCallbacks eventCallbacks = dhcpEventCbsCaptor.getValue();
+        final List<TetheredClient> connectedClients = new ArrayList<>();
+        final MacAddress wifiMac = MacAddress.fromString("11:11:11:11:11:11");
+        final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId", wifiMac,
+                "192.168.2.12", 24, Long.MAX_VALUE, "test");
+        verifyHotspotClientUpdate(false /* isLocalOnly */, wifiMac, wifiLease, connectedClients,
+                eventCallbacks, callback);
+        reset(mDhcpServer);
+
+        mTethering.interfaceStatusChanged(TEST_WLAN2_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN2_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
+
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+                any(), dhcpEventCbsCaptor.capture());
+        eventCallbacks = dhcpEventCbsCaptor.getValue();
+        final MacAddress localOnlyMac = MacAddress.fromString("22:22:22:22:22:22");
+        final DhcpLeaseParcelable localOnlyLease = createDhcpLeaseParcelable("clientId",
+                localOnlyMac, "192.168.43.24", 24, Long.MAX_VALUE, "test");
+        verifyHotspotClientUpdate(true /* isLocalOnly */, localOnlyMac, localOnlyLease,
+                connectedClients, eventCallbacks, callback);
+
+        assertTrue(isIpServerActive(TETHERING_WIFI, TEST_WLAN_IFNAME, IpServer.STATE_TETHERED));
+        assertTrue(isIpServerActive(TETHERING_WIFI, TEST_WLAN2_IFNAME, IpServer.STATE_LOCAL_ONLY));
+    }
+
+    private boolean isIpServerActive(int type, String ifName, int mode) {
+        for (IpServer ipSrv : mTetheringDependencies.mAllDownstreams) {
+            if (ipSrv.interfaceType() == type && ipSrv.interfaceName().equals(ifName)
+                    && ipSrv.servingMode() == mode) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private void verifyHotspotClientUpdate(final boolean isLocalOnly, final MacAddress testMac,
+            final DhcpLeaseParcelable dhcpLease, final List<TetheredClient> currentClients,
+            final IDhcpEventCallbacks dhcpCallback, final TestTetheringEventCallback callback)
+            throws Exception {
+        // Update mac address from softAp callback before getting dhcp lease.
+        final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac, isLocalOnly);
+        final List<TetheredClient> withNoAddrClients = new ArrayList<>(currentClients);
+        withNoAddrClients.add(noAddrClient);
+        callback.expectTetheredClientChanged(withNoAddrClients);
+
+        // Update dhcp lease for hotspot.
+        currentClients.addAll(notifyDhcpLeasesChanged(TETHERING_WIFI, dhcpCallback, dhcpLease));
+        callback.expectTetheredClientChanged(currentClients);
+    }
+
     private TetheredClient notifyConnectedWifiClientsChanged(final MacAddress mac,
             boolean isLocalOnly) throws Exception {
         final ArrayList<WifiClient> wifiClients = new ArrayList<>();
@@ -3124,6 +3227,7 @@
 
     @Test
     public void testBluetoothTethering() throws Exception {
+        initTetheringOnTestThread();
         // Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP.
         assumeTrue(isAtLeastT());
 
@@ -3160,6 +3264,7 @@
 
     @Test
     public void testBluetoothTetheringBeforeT() throws Exception {
+        initTetheringOnTestThread();
         // Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP.
         assumeFalse(isAtLeastT());
 
@@ -3207,6 +3312,7 @@
 
     @Test
     public void testBluetoothServiceDisconnects() throws Exception {
+        initTetheringOnTestThread();
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
@@ -3328,8 +3434,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(
@@ -3361,6 +3466,7 @@
 
     @Test
     public void testUsbFunctionConfigurationChange() throws Exception {
+        initTetheringOnTestThread();
         // Run TETHERING_NCM.
         runNcmTethering();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
@@ -3419,6 +3525,7 @@
 
     @Test
     public void testTetheringSupported() throws Exception {
+        initTetheringOnTestThread();
         final ArraySet<Integer> expectedTypes = getAllSupportedTetheringTypes();
         // Check tethering is supported after initialization.
         TestTetheringEventCallback callback = new TestTetheringEventCallback();
@@ -3488,6 +3595,66 @@
         assertEquals(expectedTypes.size() > 0, mTethering.isTetheringSupported());
         callback.expectSupportedTetheringTypes(expectedTypes);
     }
+
+    @Test
+    public void testIpv4AddressForSapAndLohsConcurrency() throws Exception {
+        initTetheringOnTestThread();
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+
+        ArgumentCaptor<InterfaceConfigurationParcel> ifaceConfigCaptor =
+                ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
+        verify(mNetd).interfaceSetCfg(ifaceConfigCaptor.capture());
+        InterfaceConfigurationParcel ifaceConfig = ifaceConfigCaptor.getValue();
+        final IpPrefix sapPrefix = new IpPrefix(
+                InetAddresses.parseNumericAddress(ifaceConfig.ipv4Addr), ifaceConfig.prefixLength);
+
+        mTethering.interfaceStatusChanged(TEST_WLAN2_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN2_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
+
+        ifaceConfigCaptor = ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
+        verify(mNetd, times(2)).interfaceSetCfg(ifaceConfigCaptor.capture());
+        ifaceConfig = ifaceConfigCaptor.getValue();
+        final IpPrefix lohsPrefix = new IpPrefix(
+                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(createTetheringRequestParcel(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/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index e756bd3..90fd709 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -30,10 +30,12 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -51,27 +53,23 @@
 import android.net.NetworkRequest;
 import android.os.Build;
 import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
 import android.os.test.TestLooper;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.internal.util.State;
-import com.android.internal.util.StateMachine;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.tethering.TestConnectivityManager.NetworkRequestInfo;
 import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 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;
 
@@ -84,8 +82,6 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class UpstreamNetworkMonitorTest {
-    private static final int EVENT_UNM_UPDATE = 1;
-
     private static final boolean INCLUDES = true;
     private static final boolean EXCLUDES = false;
 
@@ -102,32 +98,24 @@
     @Mock private EntitlementManager mEntitleMgr;
     @Mock private IConnectivityManager mCS;
     @Mock private SharedLog mLog;
+    @Mock private UpstreamNetworkMonitor.EventListener mListener;
 
-    private TestStateMachine mSM;
     private TestConnectivityManager mCM;
     private UpstreamNetworkMonitor mUNM;
 
     private final TestLooper mLooper = new TestLooper();
+    private InOrder mCallbackOrder;
 
     @Before public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        reset(mContext);
-        reset(mCS);
-        reset(mLog);
         when(mLog.forSubComponent(anyString())).thenReturn(mLog);
         when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
 
+        mCallbackOrder = inOrder(mListener);
         mCM = spy(new TestConnectivityManager(mContext, mCS));
         when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))).thenReturn(mCM);
-        mSM = new TestStateMachine(mLooper.getLooper());
-        mUNM = new UpstreamNetworkMonitor(mContext, mSM, mLog, EVENT_UNM_UPDATE);
-    }
-
-    @After public void tearDown() throws Exception {
-        if (mSM != null) {
-            mSM.quit();
-            mSM = null;
-        }
+        mUNM = new UpstreamNetworkMonitor(mContext, new Handler(mLooper.getLooper()), mLog,
+                mListener);
     }
 
     @Test
@@ -603,14 +591,17 @@
         mCM.makeDefaultNetwork(cellAgent);
         mLooper.dispatchAll();
         verifyCurrentLinkProperties(cellAgent);
-        int messageIndex = mSM.messages.size() - 1;
+        verifyNotifyNetworkCapabilitiesChange(cellAgent.networkCapabilities);
+        verifyNotifyLinkPropertiesChange(cellLp);
+        verifyNotifyDefaultSwitch(cellAgent);
+        verifyNoMoreInteractions(mListener);
 
         addLinkAddresses(cellLp, ipv6Addr1);
         mCM.sendLinkProperties(cellAgent, false /* updateDefaultFirst */);
         mLooper.dispatchAll();
         verifyCurrentLinkProperties(cellAgent);
-        verifyNotifyLinkPropertiesChange(messageIndex);
-        messageIndex = mSM.messages.size() - 1;
+        verifyNotifyLinkPropertiesChange(cellLp);
+        verifyNoMoreInteractions(mListener);
 
         removeLinkAddresses(cellLp, ipv6Addr1);
         addLinkAddresses(cellLp, ipv6Addr2);
@@ -618,7 +609,8 @@
         mLooper.dispatchAll();
         assertEquals(cellAgent.linkProperties, mUNM.getCurrentPreferredUpstream().linkProperties);
         verifyCurrentLinkProperties(cellAgent);
-        verifyNotifyLinkPropertiesChange(messageIndex);
+        verifyNotifyLinkPropertiesChange(cellLp);
+        verifyNoMoreInteractions(mListener);
     }
 
     private void verifyCurrentLinkProperties(TestNetworkAgent agent) {
@@ -626,12 +618,33 @@
         assertEquals(agent.linkProperties, mUNM.getCurrentPreferredUpstream().linkProperties);
     }
 
-    private void verifyNotifyLinkPropertiesChange(int lastMessageIndex) {
-        assertEquals(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
-                mSM.messages.get(++lastMessageIndex).arg1);
-        assertEquals(UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES,
-                mSM.messages.get(++lastMessageIndex).arg1);
-        assertEquals(lastMessageIndex + 1, mSM.messages.size());
+    private void verifyNotifyNetworkCapabilitiesChange(final NetworkCapabilities cap) {
+        mCallbackOrder.verify(mListener).onUpstreamEvent(
+                eq(UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES),
+                argThat(uns -> uns instanceof UpstreamNetworkState
+                    && cap.equals(((UpstreamNetworkState) uns).networkCapabilities)));
+
+    }
+
+    private void verifyNotifyLinkPropertiesChange(final LinkProperties lp) {
+        mCallbackOrder.verify(mListener).onUpstreamEvent(
+                eq(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES),
+                argThat(uns -> uns instanceof UpstreamNetworkState
+                    && lp.equals(((UpstreamNetworkState) uns).linkProperties)));
+
+        mCallbackOrder.verify(mListener).onUpstreamEvent(
+                eq(UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES), any());
+    }
+
+    private void verifyNotifyDefaultSwitch(TestNetworkAgent agent) {
+        mCallbackOrder.verify(mListener).onUpstreamEvent(
+                eq(UpstreamNetworkMonitor.EVENT_DEFAULT_SWITCHED),
+                argThat(uns ->
+                    uns instanceof UpstreamNetworkState
+                    && agent.networkId.equals(((UpstreamNetworkState) uns).network)
+                    && agent.linkProperties.equals(((UpstreamNetworkState) uns).linkProperties)
+                    && agent.networkCapabilities.equals(
+                        ((UpstreamNetworkState) uns).networkCapabilities)));
     }
 
     private void addLinkAddresses(LinkProperties lp, String... addrs) {
@@ -673,33 +686,6 @@
         return false;
     }
 
-    public static class TestStateMachine extends StateMachine {
-        public final ArrayList<Message> messages = new ArrayList<>();
-        private final State mLoggingState = new LoggingState();
-
-        class LoggingState extends State {
-            @Override public void enter() {
-                messages.clear();
-            }
-
-            @Override public void exit() {
-                messages.clear();
-            }
-
-            @Override public boolean processMessage(Message msg) {
-                messages.add(msg);
-                return true;
-            }
-        }
-
-        public TestStateMachine(Looper looper) {
-            super("UpstreamNetworkMonitor.TestStateMachine", looper);
-            addState(mLoggingState);
-            setInitialState(mLoggingState);
-            super.start();
-        }
-    }
-
     static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, String... expected) {
         final Set<String> expectedSet = new HashSet<>();
         Collections.addAll(expectedSet, expected);
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
new file mode 100644
index 0000000..2417385
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
@@ -0,0 +1,136 @@
+/**
+ * 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.networkstack.tethering.util
+
+import android.os.Looper
+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 kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class StateMachineShimTest {
+    private val mSyncSM = mock(SyncStateMachine::class.java)
+    private val mAsyncSM = mock(AsyncStateMachine::class.java)
+    private val mState1 = mock(State::class.java)
+    private val mState2 = mock(State::class.java)
+
+    inner class MyDependencies() : Dependencies() {
+
+        override fun makeSyncStateMachine(name: String, thread: Thread) = mSyncSM
+
+        override fun makeAsyncStateMachine(name: String, looper: Looper) = mAsyncSM
+    }
+
+    @Test
+    fun testUsingSyncStateMachine() {
+        val inOrder = inOrder(mSyncSM, mAsyncSM)
+        val shimUsingSyncSM = StateMachineShim("ShimTest", null, MyDependencies())
+        shimUsingSyncSM.start(mState1)
+        inOrder.verify(mSyncSM).start(mState1)
+
+        val allStates = ArrayList<StateInfo>()
+        allStates.add(StateInfo(mState1, null))
+        allStates.add(StateInfo(mState2, mState1))
+        shimUsingSyncSM.addAllStates(allStates)
+        inOrder.verify(mSyncSM).addAllStates(allStates)
+
+        shimUsingSyncSM.transitionTo(mState1)
+        inOrder.verify(mSyncSM).transitionTo(mState1)
+
+        val what = 10
+        shimUsingSyncSM.sendMessage(what)
+        inOrder.verify(mSyncSM).processMessage(what, 0, 0, null)
+        val obj = Object()
+        shimUsingSyncSM.sendMessage(what, obj)
+        inOrder.verify(mSyncSM).processMessage(what, 0, 0, obj)
+        val arg1 = 11
+        shimUsingSyncSM.sendMessage(what, arg1)
+        inOrder.verify(mSyncSM).processMessage(what, arg1, 0, null)
+        val arg2 = 12
+        shimUsingSyncSM.sendMessage(what, arg1, arg2, obj)
+        inOrder.verify(mSyncSM).processMessage(what, arg1, arg2, obj)
+
+        assertFailsWith(IllegalStateException::class) {
+            shimUsingSyncSM.sendMessageDelayedToAsyncSM(what, 1000 /* delayMillis */)
+        }
+
+        assertFailsWith(IllegalStateException::class) {
+            shimUsingSyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+        }
+
+        shimUsingSyncSM.sendSelfMessageToSyncSM(what, obj)
+        inOrder.verify(mSyncSM).sendSelfMessage(what, 0, 0, obj)
+
+        verifyNoMoreInteractions(mSyncSM, mAsyncSM)
+    }
+
+    @Test
+    fun testUsingAsyncStateMachine() {
+        val inOrder = inOrder(mSyncSM, mAsyncSM)
+        val shimUsingAsyncSM = StateMachineShim("ShimTest", mock(Looper::class.java),
+                MyDependencies())
+        shimUsingAsyncSM.start(mState1)
+        inOrder.verify(mAsyncSM).setInitialState(mState1)
+        inOrder.verify(mAsyncSM).start()
+
+        val allStates = ArrayList<StateInfo>()
+        allStates.add(StateInfo(mState1, null))
+        allStates.add(StateInfo(mState2, mState1))
+        shimUsingAsyncSM.addAllStates(allStates)
+        inOrder.verify(mAsyncSM).addState(mState1, null)
+        inOrder.verify(mAsyncSM).addState(mState2, mState1)
+
+        shimUsingAsyncSM.transitionTo(mState1)
+        inOrder.verify(mAsyncSM).transitionTo(mState1)
+
+        val what = 10
+        shimUsingAsyncSM.sendMessage(what)
+        inOrder.verify(mAsyncSM).sendMessage(what, 0, 0, null)
+        val obj = Object()
+        shimUsingAsyncSM.sendMessage(what, obj)
+        inOrder.verify(mAsyncSM).sendMessage(what, 0, 0, obj)
+        val arg1 = 11
+        shimUsingAsyncSM.sendMessage(what, arg1)
+        inOrder.verify(mAsyncSM).sendMessage(what, arg1, 0, null)
+        val arg2 = 12
+        shimUsingAsyncSM.sendMessage(what, arg1, arg2, obj)
+        inOrder.verify(mAsyncSM).sendMessage(what, arg1, arg2, obj)
+
+        shimUsingAsyncSM.sendMessageDelayedToAsyncSM(what, 1000 /* delayMillis */)
+        inOrder.verify(mAsyncSM).sendMessageDelayed(what, 1000)
+
+        shimUsingAsyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+        inOrder.verify(mAsyncSM).sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+
+        assertFailsWith(IllegalStateException::class) {
+            shimUsingAsyncSM.sendSelfMessageToSyncSM(what, obj)
+        }
+
+        verifyNoMoreInteractions(mSyncSM, mAsyncSM)
+    }
+}
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index b3f8ed6..1958aa8 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -18,6 +18,7 @@
 // struct definitions shared with JNI
 //
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -45,6 +46,7 @@
         "com.android.tethering",
     ],
     visibility: [
+        "//packages/modules/Connectivity/DnsResolver",
         "//packages/modules/Connectivity/netd",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/service/native/libs/libclat",
@@ -92,13 +94,13 @@
 }
 
 bpf {
-    name: "offload@btf.o",
-    srcs: ["offload@btf.c"],
+    name: "offload@mainline.o",
+    srcs: ["offload@mainline.c"],
     btf: true,
     cflags: [
         "-Wall",
         "-Werror",
-        "-DBTF",
+        "-DMAINLINE",
     ],
 }
 
@@ -112,13 +114,13 @@
 }
 
 bpf {
-    name: "test@btf.o",
-    srcs: ["test@btf.c"],
+    name: "test@mainline.o",
+    srcs: ["test@mainline.c"],
     btf: true,
     cflags: [
         "-Wall",
         "-Werror",
-        "-DBTF",
+        "-DMAINLINE",
     ],
 }
 
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index f2a3e62..152dda6 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -19,13 +19,13 @@
 #include <netinet/in.h>
 #include <stdint.h>
 
-// The resulting .o needs to load on the Android T beta 3 bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
 #include "bpf_helpers.h"
 
-#define ALLOW 1
-#define DISALLOW 0
+static const int ALLOW = 1;
+static const int DISALLOW = 0;
 
 DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
         1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
@@ -57,17 +57,22 @@
     return ALLOW;
 }
 
-DEFINE_BPF_PROG_KVER("bind4/block_port", AID_ROOT, AID_SYSTEM,
-                     bind4_block_port, KVER(5, 4, 0))
+// the program need to be accessible/loadable by netd (from netd updatable plugin)
+#define DEFINE_NETD_RO_BPF_PROG(SECTION_NAME, the_prog, min_kver) \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, min_kver, KVER_INF,  \
+                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+                        "", "netd_readonly/", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+DEFINE_NETD_RO_BPF_PROG("bind4/block_port", bind4_block_port, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return block_port(ctx);
 }
 
-DEFINE_BPF_PROG_KVER("bind6/block_port", AID_ROOT, AID_SYSTEM,
-                     bind6_block_port, KVER(5, 4, 0))
+DEFINE_NETD_RO_BPF_PROG("bind6/block_port", bind6_block_port, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return block_port(ctx);
 }
 
 LICENSE("Apache 2.0");
 CRITICAL("ConnectivityNative");
+DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index ed33cc9..1511ee5 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;
 
@@ -87,29 +88,18 @@
     if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
 }
 
-// constants for passing in to 'bool egress'
-static const bool INGRESS = false;
-static const bool EGRESS = true;
+struct egress_bool { bool egress; };
+#define INGRESS ((struct egress_bool){ .egress = false })
+#define EGRESS ((struct egress_bool){ .egress = true })
 
-// constants for passing in to 'bool downstream'
-static const bool UPSTREAM = false;
-static const bool DOWNSTREAM = true;
+struct stream_bool { bool down; };
+#define UPSTREAM ((struct stream_bool){ .down = false })
+#define DOWNSTREAM ((struct stream_bool){ .down = true })
 
-// constants for passing in to 'bool is_ethernet'
-static const bool RAWIP = false;
-static const bool ETHER = true;
+struct rawip_bool { bool rawip; };
+#define ETHER ((struct rawip_bool){ .rawip = false })
+#define RAWIP ((struct rawip_bool){ .rawip = true })
 
-// constants for passing in to 'bool updatetime'
-static const bool NO_UPDATETIME = false;
-static const bool UPDATETIME = true;
-
-// constants for passing in to ignore_on_eng / ignore_on_user / ignore_on_userdebug
-// define's instead of static const due to tm-mainline-prod compiler static_assert limitations
-#define LOAD_ON_ENG false
-#define LOAD_ON_USER false
-#define LOAD_ON_USERDEBUG false
-#define IGNORE_ON_ENG true
-#define IGNORE_ON_USER true
-#define IGNORE_ON_USERDEBUG true
-
-#define KVER_4_14 KVER(4, 14, 0)
+struct updatetime_bool { bool updatetime; };
+#define NO_UPDATETIME ((struct updatetime_bool){ .updatetime = false })
+#define UPDATETIME ((struct updatetime_bool){ .updatetime = true })
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index f05b93e..f83e5ae 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 beta 3 bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_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"
@@ -55,8 +55,10 @@
 DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
 
 static inline __always_inline int nat64(struct __sk_buff* skb,
-                                        const bool is_ethernet,
-                                        const unsigned kver) {
+                                        const struct rawip_bool rawip,
+                                        const struct kver_uint kver) {
+    const bool is_ethernet = !rawip.rawip;
+
     // Require ethernet dst mac address to be our unicast address.
     if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
 
@@ -115,7 +117,7 @@
 
     if (proto == IPPROTO_FRAGMENT) {
         // Fragment handling requires bpf_skb_adjust_room which is 4.14+
-        if (kver < KVER_4_14) return TC_ACT_PIPE;
+        if (!KVER_IS_AT_LEAST(kver, 4, 14, 0)) return TC_ACT_PIPE;
 
         // Must have (ethernet and) ipv6 header and ipv6 fragment extension header
         if (data + l2_header_size + sizeof(*ip6) + sizeof(struct frag_hdr) > data_end)
@@ -138,10 +140,11 @@
     }
 
     switch (proto) {
-        case IPPROTO_TCP:  // For TCP & UDP the checksum neutrality of the chosen IPv6
-        case IPPROTO_UDP:  // address means there is no need to update their checksums.
-        case IPPROTO_GRE:  // We do not need to bother looking at GRE/ESP headers,
-        case IPPROTO_ESP:  // since there is never a checksum to update.
+        case IPPROTO_TCP:      // For TCP, UDP & UDPLITE the checksum neutrality of the chosen
+        case IPPROTO_UDP:      // IPv6 address means there is no need to update their checksums.
+        case IPPROTO_UDPLITE:  //
+        case IPPROTO_GRE:      // We do not need to bother looking at GRE/ESP headers,
+        case IPPROTO_ESP:      // since there is never a checksum to update.
             break;
 
         default:  // do not know how to handle anything else
@@ -232,13 +235,15 @@
     //
     // Note: we currently have no TreeHugger coverage for 4.9-T devices (there are no such
     // Pixel or cuttlefish devices), so likely you won't notice for months if this breaks...
-    if (kver >= KVER_4_14 && frag_off != htons(IP_DF)) {
+    if (KVER_IS_AT_LEAST(kver, 4, 14, 0) && frag_off != htons(IP_DF)) {
         // If we're converting an IPv6 Fragment, we need to trim off 8 more bytes
         // We're beyond recovery on error here... but hard to imagine how this could fail.
         if (bpf_skb_adjust_room(skb, -(__s32)sizeof(struct frag_hdr), BPF_ADJ_ROOM_NET, /*flags*/0))
             return TC_ACT_SHOT;
     }
 
+    try_make_writable(skb, l2_header_size + sizeof(struct iphdr));
+
     // bpf_skb_change_proto() invalidates all pointers - reload them.
     data = (void*)(long)skb->data;
     data_end = (void*)(long)skb->data_end;
@@ -260,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);
 
@@ -328,12 +337,13 @@
     if (ip4->frag_off & ~htons(IP_DF)) return TC_ACT_PIPE;
 
     switch (ip4->protocol) {
-        case IPPROTO_TCP:  // For TCP & UDP the checksum neutrality of the chosen IPv6
-        case IPPROTO_GRE:  // address means there is no need to update their checksums.
-        case IPPROTO_ESP:  // We do not need to bother looking at GRE/ESP headers,
-            break;         // since there is never a checksum to update.
+        case IPPROTO_TCP:      // For TCP, UDP & UDPLITE the checksum neutrality of the chosen
+        case IPPROTO_UDPLITE:  // IPv6 address means there is no need to update their checksums.
+        case IPPROTO_GRE:      // We do not need to bother looking at GRE/ESP headers,
+        case IPPROTO_ESP:      // since there is never a checksum to update.
+            break;
 
-        case IPPROTO_UDP:  // See above comment, but must also have UDP header...
+        case IPPROTO_UDP:      // See above comment, but must also have UDP header...
             if (data + sizeof(*ip4) + sizeof(struct udphdr) > data_end) return TC_ACT_PIPE;
             const struct udphdr* uh = (const struct udphdr*)(ip4 + 1);
             // If IPv4/UDP checksum is 0 then fallback to clatd so it can calculate the
@@ -410,9 +420,14 @@
     // 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 */);
 }
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity");
+DISABLE_BTF_ON_USER_BUILDS();
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 72f63c6..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 beta 3 bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_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"
@@ -222,7 +222,7 @@
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM, schedcls_set_dscp_ether,
-                     KVER(5, 15, 0))
+                     KVER_5_15)
 (struct __sk_buff* skb) {
     if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
 
@@ -238,3 +238,4 @@
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity");
+DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 39dff7f..b3cde45 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 Beta 3 bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_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>
@@ -56,19 +56,21 @@
 // see include/uapi/linux/tcp.h
 #define TCP_FLAG32_OFF 12
 
+#define TCP_FLAG8_OFF (TCP_FLAG32_OFF + 1)
+
 // For maps netd does not need to access
-#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries)      \
-    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,              \
-                       AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false, \
-                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG,       \
-                       LOAD_ON_USER, LOAD_ON_USERDEBUG)
+#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,         \
+                       AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "",   \
+                       PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER,              \
+                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 // For maps netd only needs read only access to
-#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries)         \
-    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,                 \
-                       AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false, \
-                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG,       \
-                       LOAD_ON_USER, LOAD_ON_USERDEBUG)
+#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries)  \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,          \
+                       AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", \
+                       PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER,               \
+                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 // For maps netd needs to be able to read and write
 #define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
@@ -87,27 +89,34 @@
 DEFINE_BPF_MAP_RW_NETD(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE)
-DEFINE_BPF_MAP_RW_NETD(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
+DEFINE_BPF_MAP_RO_NETD(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
 DEFINE_BPF_MAP_RO_NETD(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE)
-DEFINE_BPF_MAP_NO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
-DEFINE_BPF_MAP_RW_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
+DEFINE_BPF_MAP_RO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
+DEFINE_BPF_MAP_RO_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
+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", "", false,
-                   BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
-                   IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+                   AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
+                   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. This map will only be loaded
-// on eng and userdebug devices. User devices won't load this to save memory.
+// 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", "", false,
-                       BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
-                       IGNORE_ON_USER, LOAD_ON_USERDEBUG);
+                       AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
+                       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,
+                       DATA_SAVER_ENABLED_MAP_SIZE)
 
 // iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
 // selinux contexts, because even non-xt_bpf iptables mutations are implemented as
@@ -124,8 +133,8 @@
 // which is loaded into netd and thus runs as netd uid/gid/selinux context)
 #define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV, maxKV) \
     DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog,                               \
-                        minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, false,                \
-                        "fs_bpf_netd_readonly", "", false, false, false)
+                        minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY,            \
+                        "fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 #define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
     DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF)
@@ -133,17 +142,16 @@
 #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,  \
-                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, false, "fs_bpf_net_shared", \
-                        "", false, false, false)
-
-static __always_inline int 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);
-}
+                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+                        "fs_bpf_net_shared", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 /*
  * Note: this blindly assumes an MTU of 1500, and that packets > MTU are always TCP,
@@ -175,8 +183,8 @@
 #define DEFINE_UPDATE_STATS(the_stats_map, TypeOfKey)                                            \
     static __always_inline inline void update_##the_stats_map(const struct __sk_buff* const skb, \
                                                               const TypeOfKey* const key,        \
-                                                              const bool egress,                 \
-                                                              const unsigned kver) {             \
+                                                              const struct egress_bool egress,   \
+                                                              const struct kver_uint kver) {     \
         StatsValue* value = bpf_##the_stats_map##_lookup_elem(key);                              \
         if (!value) {                                                                            \
             StatsValue newValue = {};                                                            \
@@ -196,7 +204,7 @@
                 packets = (payload + mss - 1) / mss;                                             \
                 bytes = tcp_overhead * packets + payload;                                        \
             }                                                                                    \
-            if (egress) {                                                                        \
+            if (egress.egress) {                                                                 \
                 __sync_fetch_and_add(&value->txPackets, packets);                                \
                 __sync_fetch_and_add(&value->txBytes, bytes);                                    \
             } else {                                                                             \
@@ -216,7 +224,7 @@
                                                          const int L3_off,
                                                          void* const to,
                                                          const int len,
-                                                         const unsigned kver) {
+                                                         const struct kver_uint kver) {
     // 'kver' (here and throughout) is the compile time guaranteed minimum kernel version,
     // ie. we're building (a version of) the bpf program for kver (or newer!) kernels.
     //
@@ -233,16 +241,16 @@
     //
     // For similar reasons this will fail with non-offloaded VLAN tags on < 4.19 kernels,
     // since those extend the ethernet header from 14 to 18 bytes.
-    return kver >= KVER(4, 19, 0)
+    return KVER_IS_AT_LEAST(kver, 4, 19, 0)
         ? bpf_skb_load_bytes_relative(skb, L3_off, to, len, BPF_HDR_START_NET)
         : bpf_skb_load_bytes(skb, L3_off, to, len);
 }
 
 static __always_inline inline void do_packet_tracing(
-        const struct __sk_buff* const skb, const bool egress, const uint32_t uid,
-        const uint32_t tag, const bool enable_tracing, const unsigned kver) {
+        const struct __sk_buff* const skb, const struct egress_bool egress, const uint32_t uid,
+        const uint32_t tag, const bool enable_tracing, const struct kver_uint kver) {
     if (!enable_tracing) return;
-    if (kver < KVER(5, 8, 0)) return;
+    if (!KVER_IS_AT_LEAST(kver, 5, 8, 0)) return;
 
     uint32_t mapKey = 0;
     bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
@@ -268,17 +276,41 @@
         (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(nexthdr), &proto, sizeof(proto), kver);
         L4_off = sizeof(struct ipv6hdr);
         ipVersion = 6;
+        // skip over a *single* HOPOPTS or DSTOPTS extension header (if present)
+        if (proto == IPPROTO_HOPOPTS || proto == IPPROTO_DSTOPTS) {
+            struct {
+                uint8_t proto, len;
+            } ext_hdr;
+            if (!bpf_skb_load_bytes_net(skb, L4_off, &ext_hdr, sizeof(ext_hdr), kver)) {
+                proto = ext_hdr.proto;
+                L4_off += (ext_hdr.len + 1) * 8;
+            }
+        }
     }
 
     uint8_t flags = 0;
     __be16 sport = 0, dport = 0;
-    if (proto == IPPROTO_TCP && L4_off >= 20) {
-        (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_FLAG32_OFF + 1, &flags, sizeof(flags), kver);
-        (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_OFFSET(source), &sport, sizeof(sport), kver);
-        (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_OFFSET(dest), &dport, sizeof(dport), kver);
-    } else if (proto == IPPROTO_UDP && L4_off >= 20) {
-        (void)bpf_skb_load_bytes_net(skb, L4_off + UDP_OFFSET(source), &sport, sizeof(sport), kver);
-        (void)bpf_skb_load_bytes_net(skb, L4_off + UDP_OFFSET(dest), &dport, sizeof(dport), kver);
+    if (L4_off >= 20) {
+      switch (proto) {
+        case IPPROTO_TCP:
+          (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_FLAG8_OFF, &flags, sizeof(flags), kver);
+          // fallthrough
+        case IPPROTO_DCCP:
+        case IPPROTO_UDP:
+        case IPPROTO_UDPLITE:
+        case IPPROTO_SCTP:
+          // all of these L4 protocols start with be16 src & dst port
+          (void)bpf_skb_load_bytes_net(skb, L4_off + 0, &sport, sizeof(sport), kver);
+          (void)bpf_skb_load_bytes_net(skb, L4_off + 2, &dport, sizeof(dport), kver);
+          break;
+        case IPPROTO_ICMP:
+        case IPPROTO_ICMPV6:
+          // Both IPv4 and IPv6 icmp start with u8 type & code, which we store in the bottom
+          // (ie. second) byte of sport/dport (which are be16s), the top byte is already zero.
+          (void)bpf_skb_load_bytes_net(skb, L4_off + 0, (char *)&sport + 1, 1, kver); //type
+          (void)bpf_skb_load_bytes_net(skb, L4_off + 1, (char *)&dport + 1, 1, kver); //code
+          break;
+      }
     }
 
     pkt->timestampNs = bpf_ktime_get_boot_ns();
@@ -290,7 +322,8 @@
     pkt->sport = sport;
     pkt->dport = dport;
 
-    pkt->egress = egress;
+    pkt->egress = egress.egress;
+    pkt->wakeup = !egress.egress && (skb->mark & 0x80000000);  // Fwmark.ingress_cpu_wakeup
     pkt->ipProto = proto;
     pkt->tcpFlags = flags;
     pkt->ipVersion = ipVersion;
@@ -298,8 +331,9 @@
     bpf_packet_trace_ringbuf_submit(pkt);
 }
 
-static __always_inline inline bool skip_owner_match(struct __sk_buff* skb, bool egress,
-                                                    const unsigned kver) {
+static __always_inline inline bool skip_owner_match(struct __sk_buff* skb,
+                                                    const struct egress_bool egress,
+                                                    const struct kver_uint kver) {
     uint32_t flag = 0;
     if (skb->protocol == htons(ETH_P_IP)) {
         uint8_t proto;
@@ -330,7 +364,7 @@
         return false;
     }
     // Always allow RST's, and additionally allow ingress FINs
-    return flag & (TCP_FLAG_RST | (egress ? 0 : TCP_FLAG_FIN));  // false on read failure
+    return flag & (TCP_FLAG_RST | (egress.egress ? 0 : TCP_FLAG_FIN));  // false on read failure
 }
 
 static __always_inline inline BpfConfig getConfig(uint32_t configKey) {
@@ -343,31 +377,55 @@
     return *config;
 }
 
-// DROP_IF_SET is set of rules that DROP if rule is globally enabled, and per-uid bit is set
-#define DROP_IF_SET (STANDBY_MATCH | OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)
-// DROP_IF_UNSET is set of rules that should DROP if globally enabled, and per-uid bit is NOT set
-#define DROP_IF_UNSET (DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH | LOW_POWER_STANDBY_MATCH)
+static __always_inline inline bool ingress_should_discard(struct __sk_buff* skb,
+                                                          const struct kver_uint kver) {
+    // Require 4.19, since earlier kernels don't have bpf_skb_load_bytes_relative() which
+    // provides relative to L3 header reads.  Without that we could fetch the wrong bytes.
+    // Additionally earlier bpf verifiers are much harder to please.
+    if (!KVER_IS_AT_LEAST(kver, 4, 19, 0)) return false;
+
+    IngressDiscardKey k = {};
+    if (skb->protocol == htons(ETH_P_IP)) {
+        k.daddr.s6_addr32[2] = htonl(0xFFFF);
+        (void)bpf_skb_load_bytes_net(skb, IP4_OFFSET(daddr), &k.daddr.s6_addr32[3], 4, kver);
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(daddr), &k.daddr, sizeof(k.daddr), kver);
+    } else {
+        return false; // non IPv4/IPv6, so no IP to match on
+    }
+
+    // we didn't check for load success, because destination bytes will be zeroed if
+    // bpf_skb_load_bytes_net() fails, instead we rely on daddr of '::' and '::ffff:0.0.0.0'
+    // never being present in the map itself
+
+    IngressDiscardValue* v = bpf_ingress_discard_map_lookup_elem(&k);
+    if (!v) return false;  // lookup failure -> no protection in place -> allow
+    // if (skb->ifindex == 1) return false;  // allow 'lo', but can't happen - see callsite
+    if (skb->ifindex == v->iif[0]) return false;  // allowed interface
+    if (skb->ifindex == v->iif[1]) return false;  // allowed interface
+    return true;  // disallowed interface
+}
 
 static __always_inline inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid,
-                                                  bool egress, const unsigned kver) {
+                                                  const struct egress_bool egress,
+                                                  const struct kver_uint kver) {
     if (is_system_uid(uid)) return PASS;
 
     if (skip_owner_match(skb, egress, kver)) return PASS;
 
     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;
 
-    // Warning: funky bit-wise arithmetic: in parallel, for all DROP_IF_SET/UNSET rules
-    // check whether the rules are globally enabled, and if so whether the rules are
-    // set/unset for the specific uid.  DROP if that is the case for ANY of the rules.
-    // We achieve this by masking out only the bits/rules we're interested in checking,
-    // and negating (via bit-wise xor) the bits/rules that should drop if unset.
-    if (enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET)) return DROP;
+    if (isBlockedByUidRules(enabledRules, uidRules)) return DROP;
 
-    if (!egress && skb->ifindex != 1) {
+    if (!egress.egress && skb->ifindex != 1) {
+        if (ingress_should_discard(skb, kver)) return DROP;
         if (uidRules & IIF_MATCH) {
             if (allowed_iif && skb->ifindex != allowed_iif) {
                 // Drops packets not coming from lo nor the allowed interface
@@ -386,8 +444,8 @@
 static __always_inline inline void update_stats_with_config(const uint32_t selectedMap,
                                                             const struct __sk_buff* const skb,
                                                             const StatsKey* const key,
-                                                            const bool egress,
-                                                            const unsigned kver) {
+                                                            const struct egress_bool egress,
+                                                            const struct kver_uint kver) {
     if (selectedMap == SELECT_MAP_A) {
         update_stats_map_A(skb, key, egress, kver);
     } else {
@@ -395,11 +453,22 @@
     }
 }
 
-static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, bool egress,
+static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb,
+                                                      const struct egress_bool egress,
                                                       const bool enable_tracing,
-                                                      const unsigned kver) {
+                                                      const struct kver_uint kver) {
+    // sock_uid will be 'overflowuid' if !sk_fullsock(sk_to_full_sk(skb->sk))
     uint32_t sock_uid = bpf_get_socket_uid(skb);
-    uint64_t cookie = bpf_get_socket_cookie(skb);
+
+    // kernel's DEFAULT_OVERFLOWUID is 65534, this is the overflow 'nobody' uid,
+    // usually this being returned means that skb->sk is NULL during RX
+    // (early decap socket lookup failure), which commonly happens for incoming
+    // packets to an unconnected udp socket.
+    // But it can also happen for egress from a timewait socket.
+    // Let's treat such cases as 'root' which is_system_uid()
+    if (sock_uid == 65534) sock_uid = 0;
+
+    uint64_t cookie = bpf_get_socket_cookie(skb);  // 0 iff !skb->sk
     UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
     uint32_t uid, tag;
     if (utag) {
@@ -412,10 +481,9 @@
 
     // Always allow and never count clat traffic. Only the IPv4 traffic on the stacked
     // interface is accounted for and subject to usage restrictions.
-    // TODO: remove sock_uid check once Nat464Xlat javaland adds the socket tag AID_CLAT for clat.
-    if (sock_uid == AID_CLAT || uid == AID_CLAT) {
-        return PASS;
-    }
+    // CLAT IPv6 TX sockets are *always* tagged with CLAT uid, see tagSocketAsClat()
+    // CLAT daemon receives via an untagged AF_PACKET socket.
+    if (egress.egress && uid == AID_CLAT) return PASS;
 
     int match = bpf_owner_match(skb, sock_uid, egress, kver);
 
@@ -431,7 +499,7 @@
     }
 
     // If an outbound packet is going to be dropped, we do not count that traffic.
-    if (egress && (match == DROP)) return DROP;
+    if (egress.egress && (match == DROP)) return DROP;
 
     StatsKey key = {.uid = uid, .tag = tag, .counterSet = 0, .ifaceIndex = skb->ifindex};
 
@@ -441,57 +509,81 @@
     uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY;
     uint32_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
 
-    // Use asm("%0 &= 1" : "+r"(match)) before return match,
-    // to help kernel's bpf verifier, so that it can be 100% certain
-    // that the returned value is always BPF_NOMATCH(0) or BPF_MATCH(1).
-    if (!selectedMap) {
-        asm("%0 &= 1" : "+r"(match));
-        return match;
-    }
+    if (!selectedMap) return PASS;  // cannot happen, needed to keep bpf verifier happy
 
     do_packet_tracing(skb, egress, uid, tag, enable_tracing, kver);
     update_stats_with_config(*selectedMap, skb, &key, egress, kver);
     update_app_uid_stats_map(skb, &uid, egress, kver);
+
+    // We've already handled DROP_UNLESS_DNS up above, thus when we reach here the only
+    // possible values of match are DROP(0) or PASS(1), however we need to use
+    // "match &= 1" before 'return match' to help the kernel's bpf verifier,
+    // so that it can be 100% certain that the returned value is always 0 or 1.
+    // We use assembly so that it cannot be optimized out by a too smart compiler.
     asm("%0 &= 1" : "+r"(match));
     return match;
 }
 
-DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
-                    bpf_cgroup_ingress_trace, KVER(5, 8, 0), KVER_INF,
-                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, false,
-                    "fs_bpf_netd_readonly", "", false, true, false)
+// 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_MAINLINE_U_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, 0));
+    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.
+DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
+                    bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
+                    BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+                    "fs_bpf_netd_readonly", "",
+                    LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
-                                bpf_cgroup_ingress_4_19, KVER(4, 19, 0), KVER_INF)
+                                bpf_cgroup_ingress_4_19, KVER_4_19, KVER_INF)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER(4, 19, 0));
+    return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_4_19);
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
-                                bpf_cgroup_ingress_4_14, KVER_NONE, KVER(4, 19, 0))
+                                bpf_cgroup_ingress_4_14, KVER_NONE, KVER_4_19)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
 }
 
-DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
-                    bpf_cgroup_egress_trace, KVER(5, 8, 0), KVER_INF,
-                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, false,
-                    "fs_bpf_netd_readonly", "", false, true, false)
+// 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_MAINLINE_U_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, 0));
+    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.
+DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
+                    bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
+                    BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+                    "fs_bpf_netd_readonly", "",
+                    LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
-                                bpf_cgroup_egress_4_19, KVER(4, 19, 0), KVER_INF)
+                                bpf_cgroup_egress_4_19, KVER_4_19, KVER_INF)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER(4, 19, 0));
+    return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_4_19);
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
-                                bpf_cgroup_egress_4_14, KVER_NONE, KVER(4, 19, 0))
+                                bpf_cgroup_egress_4_14, KVER_NONE, KVER_4_19)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
 }
@@ -502,9 +594,8 @@
     // Clat daemon does not generate new traffic, all its traffic is accounted for already
     // on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
     // but that can be corrected for later when merging v4-foo stats into interface foo's).
-    // TODO: remove sock_uid check once Nat464Xlat javaland adds the socket tag AID_CLAT for clat.
+    // CLAT sockets are created by system server and tagged as uid CLAT, see tagSocketAsClat()
     uint32_t sock_uid = bpf_get_socket_uid(skb);
-    if (sock_uid == AID_CLAT) return BPF_NOMATCH;
     if (sock_uid == AID_SYSTEM) {
         uint64_t cookie = bpf_get_socket_cookie(skb);
         UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
@@ -546,12 +637,13 @@
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     if (is_system_uid(sock_uid)) return BPF_MATCH;
 
-    // 65534 is the overflow 'nobody' uid, usually this being returned means
-    // that skb->sk is NULL during RX (early decap socket lookup failure),
-    // which commonly happens for incoming packets to an unconnected udp socket.
-    // Additionally bpf_get_socket_cookie() returns 0 if skb->sk is NULL
-    if ((sock_uid == 65534) && !bpf_get_socket_cookie(skb) && is_received_skb(skb))
-        return BPF_MATCH;
+    // kernel's DEFAULT_OVERFLOWUID is 65534, this is the overflow 'nobody' uid,
+    // usually this being returned means that skb->sk is NULL during RX
+    // (early decap socket lookup failure), which commonly happens for incoming
+    // packets to an unconnected udp socket.
+    // But it can also happen for egress from a timewait socket.
+    // Let's treat such cases as 'root' which is_system_uid()
+    if (sock_uid == 65534) return BPF_MATCH;
 
     UidOwnerValue* allowlistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
     if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
@@ -563,13 +655,12 @@
 (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;
 }
 
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
-                          KVER(4, 14, 0))
-(struct bpf_sock* sk) {
+static __always_inline inline uint8_t get_app_permissions() {
     uint64_t gid_uid = bpf_get_current_uid_gid();
     /*
      * A given app is guaranteed to have the same app ID in all the profiles in
@@ -579,14 +670,90 @@
      */
     uint32_t appId = (gid_uid & 0xffffffff) % AID_USER_OFFSET;  // == PER_USER_RANGE == 100000
     uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
-    if (!permissions) {
-        // UID not in map. Default to just INTERNET permission.
-        return 1;
-    }
+    // if UID not in map, then default to just INTERNET permission.
+    return permissions ? *permissions : BPF_PERMISSION_INTERNET;
+}
 
+DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", AID_ROOT, AID_ROOT, inet_socket_create,
+                          KVER_4_14)
+(struct bpf_sock* sk) {
     // A return value of 1 means allow, everything else means deny.
-    return (*permissions & BPF_PERMISSION_INTERNET) == BPF_PERMISSION_INTERNET;
+    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(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 be604f9..4877a4b 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include <cutils/android_filesystem_config.h>
 #include <linux/if.h>
 #include <linux/if_ether.h>
 #include <linux/in.h>
@@ -55,21 +56,22 @@
 } StatsValue;
 STRUCT_SIZE(StatsValue, 4 * 8);  // 32
 
+#ifdef __cplusplus
+static inline StatsValue& operator+=(StatsValue& lhs, const StatsValue& rhs) {
+    lhs.rxPackets += rhs.rxPackets;
+    lhs.rxBytes += rhs.rxBytes;
+    lhs.txPackets += rhs.txPackets;
+    lhs.txBytes += rhs.txBytes;
+    return lhs;
+}
+#endif
+
 typedef struct {
     char name[IFNAMSIZ];
 } IfaceValue;
 STRUCT_SIZE(IfaceValue, 16);
 
 typedef struct {
-    uint64_t rxBytes;
-    uint64_t rxPackets;
-    uint64_t txBytes;
-    uint64_t txPackets;
-    uint64_t tcpRxPackets;
-    uint64_t tcpTxPackets;
-} Stats;
-
-typedef struct {
   uint64_t timestampNs;
   uint32_t ifindex;
   uint32_t length;
@@ -80,7 +82,8 @@
   __be16 sport;
   __be16 dport;
 
-  bool egress;
+  bool egress:1,
+       wakeup:1;
   uint8_t ipProto;
   uint8_t tcpFlags;
   uint8_t ipVersion; // 4=IPv4, 6=IPv6, 0=unknown
@@ -121,7 +124,9 @@
 static const int IFACE_STATS_MAP_SIZE = 1000;
 static const int CONFIGURATION_MAP_SIZE = 2;
 static const int UID_OWNER_MAP_SIZE = 4000;
+static const int INGRESS_DISCARD_MAP_SIZE = 100;
 static const int PACKET_TRACE_BUF_SIZE = 32 * 1024;
+static const int DATA_SAVER_ENABLED_MAP_SIZE = 1;
 
 #ifdef __cplusplus
 
@@ -150,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
@@ -165,16 +179,18 @@
 #define CONFIGURATION_MAP_PATH BPF_NETD_PATH "map_netd_configuration_map"
 #define UID_OWNER_MAP_PATH BPF_NETD_PATH "map_netd_uid_owner_map"
 #define UID_PERMISSION_MAP_PATH BPF_NETD_PATH "map_netd_uid_permission_map"
+#define INGRESS_DISCARD_MAP_PATH BPF_NETD_PATH "map_netd_ingress_discard_map"
 #define PACKET_TRACE_RINGBUF_PATH BPF_NETD_PATH "map_netd_packet_trace_ringbuf"
 #define PACKET_TRACE_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_packet_trace_enabled_map"
+#define DATA_SAVER_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_data_saver_enabled_map"
 
 #endif // __cplusplus
 
 // LINT.IfChange(match_type)
-enum UidOwnerMatchType {
+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),
@@ -185,17 +201,19 @@
     OEM_DENY_1_MATCH = (1 << 9),
     OEM_DENY_2_MATCH = (1 << 10),
     OEM_DENY_3_MATCH = (1 << 11),
+    BACKGROUND_MATCH = (1 << 12),
+    PENALTY_BOX_ADMIN_MATCH = (1 << 13),
 };
-// LINT.ThenChange(packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java)
+// LINT.ThenChange(../framework/src/android/net/BpfNetMapsConstants.java)
 
-enum BpfPermissionMatch {
+enum BpfPermissionMatch : uint8_t {
     BPF_PERMISSION_INTERNET = 1 << 2,
     BPF_PERMISSION_UPDATE_DEVICE_STATS = 1 << 3,
 };
 // In production we use two identical stats maps to record per uid stats and
 // do swap and clean based on the configuration specified here. The statsMapType
 // value in configuration map specified which map is currently in use.
-enum StatsMapType {
+enum StatsMapType : uint32_t {
     SELECT_MAP_A,
     SELECT_MAP_B,
 };
@@ -213,9 +231,44 @@
 } UidOwnerValue;
 STRUCT_SIZE(UidOwnerValue, 2 * 4);  // 8
 
+typedef struct {
+    // The destination ip of the incoming packet.  IPv4 uses IPv4-mapped IPv6 address format.
+    struct in6_addr daddr;
+} IngressDiscardKey;
+STRUCT_SIZE(IngressDiscardKey, 16);  // 16
+
+typedef struct {
+    // Allowed interface indexes.  Use same value multiple times if you just want to match 1 value.
+    uint32_t iif[2];
+} IngressDiscardValue;
+STRUCT_SIZE(IngressDiscardValue, 2 * 4);  // 8
+
 // Entry in the configuration map that stores which UID rules are enabled.
 #define UID_RULES_CONFIGURATION_KEY 0
 // Entry in the configuration map that stores which stats map is currently in use.
 #define CURRENT_STATS_MAP_CONFIGURATION_KEY 1
+// Entry in the data saver enabled map that stores whether data saver is enabled or not.
+#define DATA_SAVER_ENABLED_KEY 0
 
 #undef STRUCT_SIZE
+
+// DROP_IF_SET is set of rules that DROP if rule is globally enabled, and per-uid bit is set
+#define DROP_IF_SET (STANDBY_MATCH | OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)
+// DROP_IF_UNSET is set of rules that should DROP if globally enabled, and per-uid bit is NOT set
+#define DROP_IF_UNSET (DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH \
+                        | LOW_POWER_STANDBY_MATCH | BACKGROUND_MATCH)
+
+// Warning: funky bit-wise arithmetic: in parallel, for all DROP_IF_SET/UNSET rules
+// check whether the rules are globally enabled, and if so whether the rules are
+// set/unset for the specific uid.  DROP if that is the case for ANY of the rules.
+// We achieve this by masking out only the bits/rules we're interested in checking,
+// and negating (via bit-wise xor) the bits/rules that should drop if unset.
+static inline bool isBlockedByUidRules(BpfConfig enabledRules, uint32_t uidRules) {
+    return enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET);
+}
+
+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_USER_OFFSET) < AID_APP_START);
+}
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 80d1a41..4f152bf 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
@@ -124,8 +124,12 @@
 DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
                    TETHERING_GID)
 
-static inline __always_inline int do_forward6(struct __sk_buff* skb, const bool is_ethernet,
-        const bool downstream, const unsigned kver) {
+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) {
+    const bool is_ethernet = !rawip.rawip;
+
     // Must be meta-ethernet IPv6 frame
     if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_PIPE;
 
@@ -184,7 +188,7 @@
         TC_PUNT(NON_GLOBAL_DST);
 
     // In the upstream direction do not forward traffic within the same /64 subnet.
-    if (!downstream && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
+    if (!stream.down && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
         TC_PUNT(LOCAL_SRC_DST);
 
     TetherDownstream6Key kd = {
@@ -194,16 +198,18 @@
 
     TetherUpstream6Key ku = {
             .iif = skb->ifindex,
+            // Retrieve the first 64 bits of the source IPv6 address in network order
+            .src64 = *(uint64_t*)&(ip6->saddr.s6_addr32[0]),
     };
-    if (is_ethernet) __builtin_memcpy(downstream ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
+    if (is_ethernet) __builtin_memcpy(stream.down ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
 
-    Tether6Value* v = downstream ? bpf_tether_downstream6_map_lookup_elem(&kd)
-                                 : bpf_tether_upstream6_map_lookup_elem(&ku);
+    Tether6Value* v = stream.down ? bpf_tether_downstream6_map_lookup_elem(&kd)
+                                  : bpf_tether_upstream6_map_lookup_elem(&ku);
 
     // If we don't find any offload information then simply let the core stack handle it...
     if (!v) return TC_ACT_PIPE;
 
-    uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+    uint32_t stat_and_limit_k = stream.down ? skb->ifindex : v->oif;
 
     TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
 
@@ -248,7 +254,7 @@
         // We do this even if TX interface is RAWIP and thus does not need an ethernet header,
         // because this is easier and the kernel will strip extraneous ethernet header.
         if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
-            __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+            __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
             TC_PUNT(CHANGE_HEAD_FAILED);
         }
 
@@ -260,7 +266,7 @@
 
         // I do not believe this can ever happen, but keep the verifier happy...
         if (data + sizeof(struct ethhdr) + sizeof(*ip6) > data_end) {
-            __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+            __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
             TC_DROP(TOO_SHORT);
         }
     };
@@ -280,8 +286,8 @@
     // (-ENOTSUPP) if it isn't.
     bpf_csum_update(skb, 0xFFFF - ntohs(old_hl) + ntohs(new_hl));
 
-    __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
-    __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
+    __sync_fetch_and_add(stream.down ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+    __sync_fetch_and_add(stream.down ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
 
     // Overwrite any mac header with the new one
     // For a rawip tx interface it will simply be a bunch of zeroes and later stripped.
@@ -323,26 +329,26 @@
 //
 // Hence, these mandatory (must load successfully) implementations for 4.14+ kernels:
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_downstream6_rawip_4_14, KVER(4, 14, 0))
+                     sched_cls_tether_downstream6_rawip_4_14, KVER_4_14)
 (struct __sk_buff* skb) {
-    return do_forward6(skb, RAWIP, DOWNSTREAM, KVER(4, 14, 0));
+    return do_forward6(skb, RAWIP, DOWNSTREAM, KVER_4_14);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_upstream6_rawip_4_14, KVER(4, 14, 0))
+                     sched_cls_tether_upstream6_rawip_4_14, KVER_4_14)
 (struct __sk_buff* skb) {
-    return do_forward6(skb, RAWIP, UPSTREAM, KVER(4, 14, 0));
+    return do_forward6(skb, RAWIP, UPSTREAM, KVER_4_14);
 }
 
 // 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, 0))
+                           sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER_4_14)
 (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, 0))
+                           sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER_4_14)
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
@@ -355,9 +361,10 @@
 
 static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
         const int l2_header_size, void* data, const void* data_end,
-        struct ethhdr* eth, struct iphdr* ip, const bool is_ethernet,
-        const bool downstream, const bool updatetime, const bool is_tcp,
-        const unsigned kver) {
+        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_ethernet = !rawip.rawip;
     struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
     struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
 
@@ -415,13 +422,13 @@
     };
     if (is_ethernet) __builtin_memcpy(k.dstMac, eth->h_dest, ETH_ALEN);
 
-    Tether4Value* v = downstream ? bpf_tether_downstream4_map_lookup_elem(&k)
-                                 : bpf_tether_upstream4_map_lookup_elem(&k);
+    Tether4Value* v = stream.down ? bpf_tether_downstream4_map_lookup_elem(&k)
+                                  : bpf_tether_upstream4_map_lookup_elem(&k);
 
     // If we don't find any offload information then simply let the core stack handle it...
     if (!v) return TC_ACT_PIPE;
 
-    uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+    uint32_t stat_and_limit_k = stream.down ? skb->ifindex : v->oif;
 
     TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
 
@@ -466,7 +473,7 @@
         // We do this even if TX interface is RAWIP and thus does not need an ethernet header,
         // because this is easier and the kernel will strip extraneous ethernet header.
         if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
-            __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+            __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
             TC_PUNT(CHANGE_HEAD_FAILED);
         }
 
@@ -480,7 +487,7 @@
 
         // I do not believe this can ever happen, but keep the verifier happy...
         if (data + sizeof(struct ethhdr) + sizeof(*ip) + (is_tcp ? sizeof(*tcph) : sizeof(*udph)) > data_end) {
-            __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+            __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
             TC_DROP(TOO_SHORT);
         }
     };
@@ -532,10 +539,10 @@
 
     // This requires the bpf_ktime_get_boot_ns() helper which was added in 5.8,
     // and backported to all Android Common Kernel 4.14+ trees.
-    if (updatetime) v->last_used = bpf_ktime_get_boot_ns();
+    if (updatetime.updatetime) v->last_used = bpf_ktime_get_boot_ns();
 
-    __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
-    __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
+    __sync_fetch_and_add(stream.down ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+    __sync_fetch_and_add(stream.down ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
 
     // Redirect to forwarded interface.
     //
@@ -546,8 +553,13 @@
     return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
 }
 
-static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
-        const bool downstream, const bool updatetime, const unsigned kver) {
+static inline __always_inline int do_forward4(struct __sk_buff* skb,
+                                              const struct rawip_bool rawip,
+                                              const struct stream_bool stream,
+                                              const struct updatetime_bool updatetime,
+                                              const struct kver_uint kver) {
+    const bool is_ethernet = !rawip.rawip;
+
     // Require ethernet dst mac address to be our unicast address.
     if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
 
@@ -605,16 +617,16 @@
     // in such a situation we can only support TCP.  This also has the added nice benefit of
     // using a separate error counter, and thus making it obvious which version of the program
     // is loaded.
-    if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+    if (!updatetime.updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
 
     // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
     // but no need to check this if !updatetime due to check immediately above.
-    if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+    if (updatetime.updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
         TC_PUNT(NON_TCP_UDP);
 
     // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
     // out all the non-tcp logic.  Also note that at this point is_udp === !is_tcp.
-    const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+    const bool is_tcp = !updatetime.updatetime || (ip->protocol == IPPROTO_TCP);
 
     // This is a bit of a hack to make things easier on the bpf verifier.
     // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
@@ -635,37 +647,37 @@
     // if the underlying requisite kernel support (bpf_ktime_get_boot_ns) was backported.
     if (is_tcp) {
       return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
-                                is_ethernet, downstream, updatetime, /* is_tcp */ true, kver);
+                                rawip, stream, updatetime, /* is_tcp */ true, kver);
     } else {
       return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
-                                is_ethernet, downstream, updatetime, /* is_tcp */ false, kver);
+                                rawip, stream, updatetime, /* is_tcp */ false, kver);
     }
 }
 
 // Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_downstream4_rawip_5_8, KVER(5, 8, 0))
+                     sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER(5, 8, 0));
+    return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_upstream4_rawip_5_8, KVER(5, 8, 0))
+                     sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER(5, 8, 0));
+    return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0))
+                     sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER(5, 8, 0));
+    return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0))
+                     sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER(5, 8, 0));
+    return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
 }
 
 // Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
@@ -674,33 +686,33 @@
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_downstream4_rawip_opt,
-                                    KVER(4, 14, 0), KVER(5, 8, 0))
+                                    KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_4_14);
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_upstream4_rawip_opt,
-                                    KVER(4, 14, 0), KVER(5, 8, 0))
+                                    KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_4_14);
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_downstream4_ether_opt,
-                                    KVER(4, 14, 0), KVER(5, 8, 0))
+                                    KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_4_14);
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_upstream4_ether_opt,
-                                    KVER(4, 14, 0), KVER(5, 8, 0))
+                                    KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_4_14);
 }
 
 // Partial (TCP-only: will not update 'last_used' field) implementations for 4.14+ kernels.
@@ -718,15 +730,15 @@
 // RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+                           sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER(5, 4, 0));
+    return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_upstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+                           sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER(5, 4, 0));
+    return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
 }
 
 // RAWIP: Optional for 4.14/4.19 (R) kernels -- which support bpf_skb_change_head().
@@ -735,31 +747,31 @@
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_downstream4_rawip_4_14,
-                                    KVER(4, 14, 0), KVER(5, 4, 0))
+                                    KVER_4_14, KVER_5_4)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_upstream4_rawip_4_14,
-                                    KVER(4, 14, 0), KVER(5, 4, 0))
+                                    KVER_4_14, KVER_5_4)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
 // ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+                           sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_upstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+                           sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
 // Placeholder (no-op) implementations for older Q kernels
@@ -767,13 +779,13 @@
 // RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
 
 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, 0))
+                           sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER_5_4)
 (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, 0))
+                           sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER_5_4)
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
@@ -781,13 +793,13 @@
 // ETHER: 4.9-P/Q kernel
 
 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, 0))
+                           sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER_4_14)
 (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, 0))
+                           sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER_4_14)
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
@@ -796,17 +808,18 @@
 
 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 bool is_ethernet,
-        const bool downstream) {
+static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const struct rawip_bool rawip,
+        const struct stream_bool stream) {
     return XDP_PASS;
 }
 
-static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const bool is_ethernet,
-        const bool downstream) {
+static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const struct rawip_bool rawip,
+        const struct stream_bool stream) {
     return XDP_PASS;
 }
 
-static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx, const bool downstream) {
+static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx,
+                                                       const struct stream_bool stream) {
     const void* data = (void*)(long)ctx->data;
     const void* data_end = (void*)(long)ctx->data_end;
     const struct ethhdr* eth = data;
@@ -815,15 +828,16 @@
     if ((void*)(eth + 1) > data_end) return XDP_PASS;
 
     if (eth->h_proto == htons(ETH_P_IPV6))
-        return do_xdp_forward6(ctx, ETHER, downstream);
+        return do_xdp_forward6(ctx, ETHER, stream);
     if (eth->h_proto == htons(ETH_P_IP))
-        return do_xdp_forward4(ctx, ETHER, downstream);
+        return do_xdp_forward4(ctx, ETHER, stream);
 
     // Anything else we don't know how to handle...
     return XDP_PASS;
 }
 
-static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx, const bool downstream) {
+static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx,
+                                                       const struct stream_bool stream) {
     const void* data = (void*)(long)ctx->data;
     const void* data_end = (void*)(long)ctx->data_end;
 
@@ -831,15 +845,15 @@
     if (data_end - data < 1) return XDP_PASS;
     const uint8_t v = (*(uint8_t*)data) >> 4;
 
-    if (v == 6) return do_xdp_forward6(ctx, RAWIP, downstream);
-    if (v == 4) return do_xdp_forward4(ctx, RAWIP, downstream);
+    if (v == 6) return do_xdp_forward6(ctx, RAWIP, stream);
+    if (v == 4) return do_xdp_forward4(ctx, RAWIP, stream);
 
     // Anything else we don't know how to handle...
     return XDP_PASS;
 }
 
 #define DEFINE_XDP_PROG(str, func) \
-    DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER(5, 9, 0))(struct xdp_md *ctx)
+    DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER_5_9)(struct xdp_md *ctx)
 
 DEFINE_XDP_PROG("xdp/tether_downstream_ether",
                  xdp_tether_downstream_ether) {
@@ -863,3 +877,4 @@
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity (Tethering)");
+DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/offload.h b/bpf_progs/offload.h
index 9dae6c9..1e28f01 100644
--- a/bpf_progs/offload.h
+++ b/bpf_progs/offload.h
@@ -135,10 +135,10 @@
 typedef struct {
     uint32_t iif;              // The input interface index
     uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
-    uint8_t zero[2];           // zero pad for 8 byte alignment
-                               // TODO: extend this to include src ip /64 subnet
+    uint8_t zero[6];           // zero pad for 8 byte alignment
+    uint64_t src64;            // Top 64-bits of the src ip
 } TetherUpstream6Key;
-STRUCT_SIZE(TetherUpstream6Key, 12);
+STRUCT_SIZE(TetherUpstream6Key, 4 + 6 + 6 + 8);  // 24
 
 typedef struct {
     uint32_t iif;              // The input interface index
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 091743c..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,11 +45,15 @@
 // 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)
 
 DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", TETHERING_UID, TETHERING_GID,
-                      xdp_test, KVER(5, 9, 0))
+                      xdp_test, KVER_5_9)
 (struct xdp_md *ctx) {
     void *data = (void *)(long)ctx->data;
     void *data_end = (void *)(long)ctx->data_end;
@@ -67,3 +71,4 @@
 }
 
 LICENSE("Apache 2.0");
+DISABLE_BTF_ON_USER_BUILDS();
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/clatd/.clang-format b/clatd/.clang-format
new file mode 100644
index 0000000..f1debbd
--- /dev/null
+++ b/clatd/.clang-format
@@ -0,0 +1,8 @@
+BasedOnStyle: Google
+AlignConsecutiveAssignments: true
+AlignEscapedNewlines: Right
+ColumnLimit: 100
+CommentPragmas: NOLINT:.*
+ContinuationIndentWidth: 2
+Cpp11BracedListStyle: false
+TabWidth: 2
diff --git a/clatd/Android.bp b/clatd/Android.bp
new file mode 100644
index 0000000..43eb2d8
--- /dev/null
+++ b/clatd/Android.bp
@@ -0,0 +1,119 @@
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["external_android-clat_license"],
+}
+
+// Added automatically by a large-scale-change
+//
+// large-scale-change included anything that looked like it might be a license
+// text as a license_text. e.g. LICENSE, NOTICE, COPYING etc.
+//
+// Please consider removing redundant or irrelevant files from 'license_text:'.
+// See: http://go/android-license-faq
+license {
+    name: "external_android-clat_license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "SPDX-license-identifier-Apache-2.0",
+    ],
+    license_text: [
+        "LICENSE",
+        "NOTICE",
+    ],
+}
+
+cc_defaults {
+    name: "clatd_defaults",
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wunused-parameter",
+
+        // Bug: http://b/33566695
+        "-Wno-address-of-packed-member",
+    ],
+}
+
+// Code used both by the daemon and by unit tests.
+filegroup {
+    name: "clatd_common",
+    srcs: [
+        "clatd.c",
+        "dump.c",
+        "icmp.c",
+        "ipv4.c",
+        "ipv6.c",
+        "logging.c",
+        "translate.c",
+    ],
+}
+
+// The clat daemon.
+cc_binary {
+    name: "clatd",
+    defaults: ["clatd_defaults"],
+    srcs: [
+        ":clatd_common",
+        "main.c",
+    ],
+    static_libs: [
+        "libip_checksum",
+    ],
+    shared_libs: [
+        "liblog",
+    ],
+    relative_install_path: "for-system",
+
+    // Static libc++ for smaller apex size while shipping clatd in the mainline module.
+    // See b/213123047
+    stl: "libc++_static",
+
+    // Only enable clang-tidy for the daemon, not the tests, because enabling it for the
+    // tests substantially increases build/compile cycle times and doesn't really provide a
+    // security benefit.
+    tidy: true,
+    tidy_checks: [
+        "-*",
+        "cert-*",
+        "clang-analyzer-security*",
+        // b/2043314, warnings on memcpy_s, memset_s, snprintf_s calls
+        // are blocking the migration from gnu99 to gnu11.
+        // Until those warnings are fixed, disable these checks.
+        "-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling",
+        "android-*",
+    ],
+    tidy_checks_as_errors: [
+        "clang-analyzer-security*",
+        "cert-*",
+        "android-*",
+    ],
+
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    min_sdk_version: "30",
+}
+
+// Unit tests.
+cc_test {
+    name: "clatd_test",
+    defaults: ["clatd_defaults"],
+    srcs: [
+        ":clatd_common",
+        "clatd_test.cpp",
+    ],
+    static_libs: [
+        "libbase",
+        "libip_checksum",
+        "libnetd_test_tun_interface",
+    ],
+    shared_libs: [
+        "libcutils",
+        "liblog",
+        "libnetutils",
+    ],
+    test_suites: ["device-tests"],
+    require_root: true,
+}
diff --git a/clatd/BUGS b/clatd/BUGS
new file mode 100644
index 0000000..24e6639
--- /dev/null
+++ b/clatd/BUGS
@@ -0,0 +1,5 @@
+known problems/assumptions:
+ - does not handle protocols other than ICMP, UDP, TCP and GRE/ESP
+ - assumes the handset has its own (routed) /64 ipv6 subnet
+ - assumes the /128 ipv6 subnet it generates can use the nat64 gateway
+ - assumes the nat64 gateway has the ipv4 address in the last 32 bits of the ipv6 address (that it uses a /96 plat subnet)
diff --git a/clatd/LICENSE b/clatd/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/clatd/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/clatd/METADATA b/clatd/METADATA
new file mode 100644
index 0000000..d97975c
--- /dev/null
+++ b/clatd/METADATA
@@ -0,0 +1,3 @@
+third_party {
+  license_type: NOTICE
+}
diff --git a/Tethering/apex/in-process b/clatd/MODULE_LICENSE_APACHE2
similarity index 100%
copy from Tethering/apex/in-process
copy to clatd/MODULE_LICENSE_APACHE2
diff --git a/clatd/NOTICE b/clatd/NOTICE
new file mode 100644
index 0000000..5943b54
--- /dev/null
+++ b/clatd/NOTICE
@@ -0,0 +1,189 @@
+   Copyright (c) 2010-2012, Daniel Drown
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/clatd/PREUPLOAD.cfg b/clatd/PREUPLOAD.cfg
new file mode 100644
index 0000000..c8dbf77
--- /dev/null
+++ b/clatd/PREUPLOAD.cfg
@@ -0,0 +1,5 @@
+[Builtin Hooks]
+clang_format = true
+
+[Builtin Hooks Options]
+clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
diff --git a/clatd/TEST_MAPPING b/clatd/TEST_MAPPING
new file mode 100644
index 0000000..d36908a
--- /dev/null
+++ b/clatd/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+  "presubmit": [
+    { "name": "clatd_test" },
+    { "name": "netd_integration_test" },
+    { "name": "netd_unit_test" },
+    { "name": "netdutils_test" },
+    { "name": "resolv_integration_test" },
+    { "name": "resolv_unit_test" }
+  ]
+}
diff --git a/clatd/clatd.c b/clatd/clatd.c
new file mode 100644
index 0000000..bac8b1d
--- /dev/null
+++ b/clatd/clatd.c
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2012 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * clatd.c - tun interface setup and main event loop
+ */
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <linux/filter.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/if_tun.h>
+#include <linux/virtio_net.h>
+#include <net/if.h>
+#include <sys/uio.h>
+
+#include "clatd.h"
+#include "checksum.h"
+#include "config.h"
+#include "dump.h"
+#include "logging.h"
+#include "translate.h"
+
+struct clat_config Global_Clatd_Config;
+
+volatile sig_atomic_t running = 1;
+
+// reads IPv6 packet from AF_PACKET socket, translates to IPv4, writes to tun
+void process_packet_6_to_4(struct tun_data *tunnel) {
+  // ethernet header is 14 bytes, plus 4 for a normal VLAN tag or 8 for Q-in-Q
+  // we don't really support vlans (or especially Q-in-Q)...
+  // but a few bytes of extra buffer space doesn't hurt...
+  struct {
+    struct virtio_net_hdr vnet;
+    uint8_t payload[22 + MAXMTU];
+    char pad; // +1 to make packet truncation obvious
+  } buf;
+  struct iovec iov = {
+    .iov_base = &buf,
+    .iov_len = sizeof(buf),
+  };
+  char cmsg_buf[CMSG_SPACE(sizeof(struct tpacket_auxdata))];
+  struct msghdr msgh = {
+    .msg_iov = &iov,
+    .msg_iovlen = 1,
+    .msg_control = cmsg_buf,
+    .msg_controllen = sizeof(cmsg_buf),
+  };
+  ssize_t readlen = recvmsg(tunnel->read_fd6, &msgh, /*flags*/ 0);
+
+  if (readlen < 0) {
+    if (errno != EAGAIN) {
+      logmsg(ANDROID_LOG_WARN, "%s: read error: %s", __func__, strerror(errno));
+    }
+    return;
+  } else if (readlen == 0) {
+    logmsg(ANDROID_LOG_WARN, "%s: packet socket removed?", __func__);
+    running = 0;
+    return;
+  } else if (readlen >= sizeof(buf)) {
+    logmsg(ANDROID_LOG_WARN, "%s: read truncation - ignoring pkt", __func__);
+    return;
+  }
+
+  bool ok = false;
+  __u32 tp_status = 0;
+  __u16 tp_net = 0;
+
+  for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msgh); cmsg != NULL; cmsg = CMSG_NXTHDR(&msgh,cmsg)) {
+    if (cmsg->cmsg_level == SOL_PACKET && cmsg->cmsg_type == PACKET_AUXDATA) {
+      struct tpacket_auxdata *aux = (struct tpacket_auxdata *)CMSG_DATA(cmsg);
+      ok = true;
+      tp_status = aux->tp_status;
+      tp_net = aux->tp_net;
+      break;
+    }
+  }
+
+  if (!ok) {
+    // theoretically this should not happen...
+    static bool logged = false;
+    if (!logged) {
+      logmsg(ANDROID_LOG_ERROR, "%s: failed to fetch tpacket_auxdata cmsg", __func__);
+      logged = true;
+    }
+  }
+
+  const int payload_offset = offsetof(typeof(buf), payload);
+  if (readlen < payload_offset + tp_net) {
+    logmsg(ANDROID_LOG_WARN, "%s: ignoring %zd byte pkt shorter than %d+%u L2 header",
+           __func__, readlen, payload_offset, tp_net);
+    return;
+  }
+
+  const int pkt_len = readlen - payload_offset;
+
+  // This will detect a skb->ip_summed == CHECKSUM_PARTIAL packet with non-final L4 checksum
+  if (tp_status & TP_STATUS_CSUMNOTREADY) {
+    static bool logged = false;
+    if (!logged) {
+      logmsg(ANDROID_LOG_WARN, "%s: L4 checksum calculation required", __func__);
+      logged = true;
+    }
+
+    // These are non-negative by virtue of csum_start/offset being u16
+    const int cs_start = buf.vnet.csum_start;
+    const int cs_offset = cs_start + buf.vnet.csum_offset;
+    if (cs_start > pkt_len) {
+      logmsg(ANDROID_LOG_ERROR, "%s: out of range - checksum start %d > %d",
+             __func__, cs_start, pkt_len);
+    } else if (cs_offset + 1 >= pkt_len) {
+      logmsg(ANDROID_LOG_ERROR, "%s: out of range - checksum offset %d + 1 >= %d",
+             __func__, cs_offset, pkt_len);
+    } else {
+      uint16_t csum = ip_checksum(buf.payload + cs_start, pkt_len - cs_start);
+      if (!csum) csum = 0xFFFF;  // required fixup for UDP, TCP must live with it
+      buf.payload[cs_offset] = csum & 0xFF;
+      buf.payload[cs_offset + 1] = csum >> 8;
+    }
+  }
+
+  translate_packet(tunnel->fd4, 0 /* to_ipv6 */, buf.payload + tp_net, pkt_len - tp_net);
+}
+
+// reads TUN_PI + L3 IPv4 packet from tun, translates to IPv6, writes to AF_INET6/RAW socket
+void process_packet_4_to_6(struct tun_data *tunnel) {
+  struct {
+    struct tun_pi pi;
+    uint8_t payload[MAXMTU];
+    char pad; // +1 byte to make packet truncation obvious
+  } buf;
+  ssize_t readlen = read(tunnel->fd4, &buf, sizeof(buf));
+
+  if (readlen < 0) {
+    if (errno != EAGAIN) {
+      logmsg(ANDROID_LOG_WARN, "%s: read error: %s", __func__, strerror(errno));
+    }
+    return;
+  } else if (readlen == 0) {
+    logmsg(ANDROID_LOG_WARN, "%s: tun interface removed", __func__);
+    running = 0;
+    return;
+  } else if (readlen >= sizeof(buf)) {
+    logmsg(ANDROID_LOG_WARN, "%s: read truncation - ignoring pkt", __func__);
+    return;
+  }
+
+  const int payload_offset = offsetof(typeof(buf), payload);
+
+  if (readlen < payload_offset) {
+    logmsg(ANDROID_LOG_WARN, "%s: short read: got %ld bytes", __func__, readlen);
+    return;
+  }
+
+  const int pkt_len = readlen - payload_offset;
+
+  uint16_t proto = ntohs(buf.pi.proto);
+  if (proto != ETH_P_IP) {
+    logmsg(ANDROID_LOG_WARN, "%s: unknown packet type = 0x%x", __func__, proto);
+    return;
+  }
+
+  if (buf.pi.flags != 0) {
+    logmsg(ANDROID_LOG_WARN, "%s: unexpected flags = %d", __func__, buf.pi.flags);
+  }
+
+  translate_packet(tunnel->write_fd6, 1 /* to_ipv6 */, buf.payload, pkt_len);
+}
+
+// IPv6 DAD packet format:
+//   Ethernet header (if needed) will be added by the kernel:
+//     u8[6] src_mac; u8[6] dst_mac '33:33:ff:XX:XX:XX'; be16 ethertype '0x86DD'
+//   IPv6 header:
+//     be32 0x60000000 - ipv6, tclass 0, flowlabel 0
+//     be16 payload_length '32'; u8 nxt_hdr ICMPv6 '58'; u8 hop limit '255'
+//     u128 src_ip6 '::'
+//     u128 dst_ip6 'ff02::1:ffXX:XXXX'
+//   ICMPv6 header:
+//     u8 type '135'; u8 code '0'; u16 icmp6 checksum; u32 reserved '0'
+//   ICMPv6 neighbour solicitation payload:
+//     u128 tgt_ip6
+//   ICMPv6 ND options:
+//     u8 opt nr '14'; u8 length '1'; u8[6] nonce '6 random bytes'
+void send_dad(int fd, const struct in6_addr* tgt) {
+  struct {
+    struct ip6_hdr ip6h;
+    struct nd_neighbor_solicit ns;
+    uint8_t ns_opt_nr;
+    uint8_t ns_opt_len;
+    uint8_t ns_opt_nonce[6];
+  } dad_pkt = {
+    .ip6h = {
+      .ip6_flow = htonl(6 << 28),  // v6, 0 tclass, 0 flowlabel
+      .ip6_plen = htons(sizeof(dad_pkt) - sizeof(struct ip6_hdr)),  // payload length, ie. 32
+      .ip6_nxt = IPPROTO_ICMPV6,  // 58
+      .ip6_hlim = 255,
+      .ip6_src = {},  // ::
+      .ip6_dst.s6_addr = {
+        0xFF, 0x02, 0, 0,
+        0, 0, 0, 0,
+        0, 0, 0, 1,
+        0xFF, tgt->s6_addr[13], tgt->s6_addr[14], tgt->s6_addr[15],
+      },  // ff02::1:ffXX:XXXX - multicast group address derived from bottom 24-bits of tgt
+    },
+    .ns = {
+      .nd_ns_type = ND_NEIGHBOR_SOLICIT,  // 135
+      .nd_ns_code = 0,
+      .nd_ns_cksum = 0,  // will be calculated later
+      .nd_ns_reserved = 0,
+      .nd_ns_target = *tgt,
+    },
+    .ns_opt_nr = 14,  // icmp6 option 'nonce' from RFC3971
+    .ns_opt_len = 1,  // in units of 8 bytes, including option nr and len
+    .ns_opt_nonce = {},  // opt_len *8 - sizeof u8(opt_nr) - sizeof u8(opt_len) = 6 ranodmized bytes
+  };
+  arc4random_buf(&dad_pkt.ns_opt_nonce, sizeof(dad_pkt.ns_opt_nonce));
+
+  // 40 byte IPv6 header + 8 byte ICMPv6 header + 16 byte ipv6 target address + 8 byte nonce option
+  _Static_assert(sizeof(dad_pkt) == 40 + 8 + 16 + 8, "sizeof dad packet != 72");
+
+  // IPv6 header checksum is standard negated 16-bit one's complement sum over the icmpv6 pseudo
+  // header (which includes payload length, nextheader, and src/dst ip) and the icmpv6 payload.
+  //
+  // Src/dst ip immediately prefix the icmpv6 header itself, so can be handled along
+  // with the payload.  We thus only need to manually account for payload len & next header.
+  //
+  // The magic '8' is simply the offset of the ip6_src field in the ipv6 header,
+  // ie. we're skipping over the ipv6 version, tclass, flowlabel, payload length, next header
+  // and hop limit fields, because they're not quite where we want them to be.
+  //
+  // ip6_plen is already in network order, while ip6_nxt is a single byte and thus needs htons().
+  uint32_t csum = dad_pkt.ip6h.ip6_plen + htons(dad_pkt.ip6h.ip6_nxt);
+  csum = ip_checksum_add(csum, &dad_pkt.ip6h.ip6_src, sizeof(dad_pkt) - 8);
+  dad_pkt.ns.nd_ns_cksum = ip_checksum_finish(csum);
+
+  const struct sockaddr_in6 dst = {
+    .sin6_family = AF_INET6,
+    .sin6_addr = dad_pkt.ip6h.ip6_dst,
+    .sin6_scope_id = if_nametoindex(Global_Clatd_Config.native_ipv6_interface),
+  };
+
+  sendto(fd, &dad_pkt, sizeof(dad_pkt), 0 /*flags*/, (const struct sockaddr *)&dst, sizeof(dst));
+}
+
+/* function: event_loop
+ * reads packets from the tun network interface and passes them down the stack
+ *   tunnel - tun device data
+ */
+void event_loop(struct tun_data *tunnel) {
+  // Apparently some network gear will refuse to perform NS for IPs that aren't DAD'ed,
+  // this would then result in an ipv6-only network with working native ipv6, working
+  // IPv4 via DNS64, but non-functioning IPv4 via CLAT (ie. IPv4 literals + IPv4 only apps).
+  // The kernel itself doesn't do DAD for anycast ips (but does handle IPV6 MLD and handle ND).
+  // So we'll spoof dad here, and yeah, we really should check for a response and in
+  // case of failure pick a different IP.  Seeing as 48-bits of the IP are utterly random
+  // (with the other 16 chosen to guarantee checksum neutrality) this seems like a remote
+  // concern...
+  // TODO: actually perform true DAD
+  send_dad(tunnel->write_fd6, &Global_Clatd_Config.ipv6_local_subnet);
+
+  struct pollfd wait_fd[] = {
+    { tunnel->read_fd6, POLLIN, 0 },
+    { tunnel->fd4, POLLIN, 0 },
+  };
+
+  while (running) {
+    if (poll(wait_fd, ARRAY_SIZE(wait_fd), -1) == -1) {
+      if (errno != EINTR) {
+        logmsg(ANDROID_LOG_WARN, "event_loop/poll returned an error: %s", strerror(errno));
+      }
+    } else {
+      // Call process_packet if the socket has data to be read, but also if an
+      // error is waiting. If we don't call read() after getting POLLERR, a
+      // subsequent poll() will return immediately with POLLERR again,
+      // causing this code to spin in a loop. Calling read() will clear the
+      // socket error flag instead.
+      if (wait_fd[0].revents) process_packet_6_to_4(tunnel);
+      if (wait_fd[1].revents) process_packet_4_to_6(tunnel);
+    }
+  }
+}
diff --git a/clatd/clatd.h b/clatd/clatd.h
new file mode 100644
index 0000000..e170c58
--- /dev/null
+++ b/clatd/clatd.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * clatd.h - main routines used by clatd
+ */
+#ifndef __CLATD_H__
+#define __CLATD_H__
+
+#include <signal.h>
+#include <stdlib.h>
+#include <sys/uio.h>
+
+struct tun_data;
+
+// IPv4 header has a u16 total length field, for maximum L3 mtu of 0xFFFF.
+//
+// Translating IPv4 to IPv6 requires removing the IPv4 header (20) and adding
+// an IPv6 header (40), possibly with an extra ipv6 fragment extension header (8).
+//
+// As such the maximum IPv4 L3 mtu size is 0xFFFF (by u16 tot_len field)
+// and the maximum IPv6 L3 mtu size is 0xFFFF + 28 (which is larger)
+//
+// A received non-jumbogram IPv6 frame could potentially be u16 payload_len = 0xFFFF
+// + sizeof ipv6 header = 40, bytes in size.  But such a packet cannot be meaningfully
+// converted to IPv4 (it's too large).  As such the restriction is the same: 0xFFFF + 28
+//
+// (since there's no jumbogram support in IPv4, IPv6 jumbograms cannot be meaningfully
+// converted to IPv4 anyway, and are thus entirely unsupported)
+#define MAXMTU (0xFFFF + 28)
+
+// logcat_hexdump() maximum binary data length, this is the maximum packet size
+// plus some extra space for various headers:
+//   struct tun_pi (4 bytes)
+//   struct virtio_net_hdr (10 bytes)
+//   ethernet (14 bytes), potentially including vlan tag (4) or tags (8 or 12)
+// plus some extra just-in-case headroom, because it doesn't hurt.
+#define MAXDUMPLEN (64 + MAXMTU)
+
+#define CLATD_VERSION "1.7"
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+
+extern volatile sig_atomic_t running;
+
+void event_loop(struct tun_data *tunnel);
+
+/* function: parse_int
+ * parses a string as a decimal/hex/octal signed integer
+ *   str - the string to parse
+ *   out - the signed integer to write to, gets clobbered on failure
+ */
+static inline int parse_int(const char *str, int *out) {
+  char *end_ptr;
+  *out = strtol(str, &end_ptr, 0);
+  return *str && !*end_ptr;
+}
+
+/* function: parse_unsigned
+ * parses a string as a decimal/hex/octal unsigned integer
+ *   str - the string to parse
+ *   out - the unsigned integer to write to, gets clobbered on failure
+ */
+static inline int parse_unsigned(const char *str, unsigned *out) {
+  char *end_ptr;
+  *out = strtoul(str, &end_ptr, 0);
+  return *str && !*end_ptr;
+}
+
+#endif /* __CLATD_H__ */
diff --git a/clatd/clatd_test.cpp b/clatd/clatd_test.cpp
new file mode 100644
index 0000000..0ed5f28
--- /dev/null
+++ b/clatd/clatd_test.cpp
@@ -0,0 +1,835 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * clatd_test.cpp - unit tests for clatd
+ */
+
+#include <iostream>
+
+#include <arpa/inet.h>
+#include <linux/if_packet.h>
+#include <netinet/in6.h>
+#include <stdio.h>
+#include <sys/uio.h>
+
+#include <gtest/gtest.h>
+
+#include "netutils/ifc.h"
+#include "tun_interface.h"
+
+extern "C" {
+#include "checksum.h"
+#include "clatd.h"
+#include "config.h"
+#include "translate.h"
+}
+
+// For convenience.
+#define ARRAYSIZE(x) sizeof((x)) / sizeof((x)[0])
+
+using android::net::TunInterface;
+
+// Default translation parameters.
+static const char kIPv4LocalAddr[]  = "192.0.0.4";
+static const char kIPv6LocalAddr[]  = "2001:db8:0:b11::464";
+static const char kIPv6PlatSubnet[] = "64:ff9b::";
+
+// clang-format off
+// Test packet portions. Defined as macros because it's easy to concatenate them to make packets.
+#define IPV4_HEADER(p, c1, c2) \
+    0x45, 0x00,    0,   41,  /* Version=4, IHL=5, ToS=0x80, len=41 */     \
+    0x00, 0x00, 0x40, 0x00,  /* ID=0x0000, flags=IP_DF, offset=0 */       \
+      55,  (p), (c1), (c2),  /* TTL=55, protocol=p, checksum=c1,c2 */     \
+     192,    0,    0,    4,  /* Src=192.0.0.4 */                          \
+       8,    8,    8,    8,  /* Dst=8.8.8.8 */
+#define IPV4_UDP_HEADER IPV4_HEADER(IPPROTO_UDP, 0x73, 0xb0)
+#define IPV4_ICMP_HEADER IPV4_HEADER(IPPROTO_ICMP, 0x73, 0xc0)
+
+#define IPV6_HEADER(p) \
+    0x60, 0x00,    0,    0,  /* Version=6, tclass=0x00, flowlabel=0 */    \
+       0,   21,  (p),   55,  /* plen=11, nxthdr=p, hlim=55 */             \
+    0x20, 0x01, 0x0d, 0xb8,  /* Src=2001:db8:0:b11::464 */                \
+    0x00, 0x00, 0x0b, 0x11,                                               \
+    0x00, 0x00, 0x00, 0x00,                                               \
+    0x00, 0x00, 0x04, 0x64,                                               \
+    0x00, 0x64, 0xff, 0x9b,  /* Dst=64:ff9b::8.8.8.8 */                   \
+    0x00, 0x00, 0x00, 0x00,                                               \
+    0x00, 0x00, 0x00, 0x00,                                               \
+    0x08, 0x08, 0x08, 0x08,
+#define IPV6_UDP_HEADER IPV6_HEADER(IPPROTO_UDP)
+#define IPV6_ICMPV6_HEADER IPV6_HEADER(IPPROTO_ICMPV6)
+
+#define UDP_LEN 21
+#define UDP_HEADER \
+    0xc8, 0x8b,    0,   53,  /* Port 51339->53 */                         \
+    0x00, UDP_LEN, 0,    0,  /* Length 21, checksum empty for now */
+
+#define PAYLOAD 'H', 'e', 'l', 'l', 'o', ' ', 0x4e, 0xb8, 0x96, 0xe7, 0x95, 0x8c, 0x00
+
+#define IPV4_PING \
+    0x08, 0x00, 0x88, 0xd0,  /* Type 8, code 0, checksum 0x88d0 */        \
+    0xd0, 0x0d, 0x00, 0x03,  /* ID=0xd00d, seq=3 */
+
+#define IPV6_PING \
+    0x80, 0x00, 0xc3, 0x42,  /* Type 128, code 0, checksum 0xc342 */      \
+    0xd0, 0x0d, 0x00, 0x03,  /* ID=0xd00d, seq=3 */
+
+// Macros to return pseudo-headers from packets.
+#define IPV4_PSEUDOHEADER(ip, tlen)                                  \
+  ip[12], ip[13], ip[14], ip[15],        /* Source address      */   \
+  ip[16], ip[17], ip[18], ip[19],        /* Destination address */   \
+  0, ip[9],                              /* 0, protocol         */   \
+  ((tlen) >> 16) & 0xff, (tlen) & 0xff,  /* Transport length */
+
+#define IPV6_PSEUDOHEADER(ip6, protocol, tlen)                       \
+  ip6[8],  ip6[9],  ip6[10], ip6[11],  /* Source address */          \
+  ip6[12], ip6[13], ip6[14], ip6[15],                                \
+  ip6[16], ip6[17], ip6[18], ip6[19],                                \
+  ip6[20], ip6[21], ip6[22], ip6[23],                                \
+  ip6[24], ip6[25], ip6[26], ip6[27],  /* Destination address */     \
+  ip6[28], ip6[29], ip6[30], ip6[31],                                \
+  ip6[32], ip6[33], ip6[34], ip6[35],                                \
+  ip6[36], ip6[37], ip6[38], ip6[39],                                \
+  ((tlen) >> 24) & 0xff,               /* Transport length */        \
+  ((tlen) >> 16) & 0xff,                                             \
+  ((tlen) >> 8) & 0xff,                                              \
+  (tlen) & 0xff,                                                     \
+  0, 0, 0, (protocol),
+
+// A fragmented DNS request.
+static const uint8_t kIPv4Frag1[] = {
+    0x45, 0x00, 0x00, 0x24, 0xfe, 0x47, 0x20, 0x00, 0x40, 0x11,
+    0x8c, 0x6d, 0xc0, 0x00, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08,
+    0x14, 0x5d, 0x00, 0x35, 0x00, 0x29, 0x68, 0xbb, 0x50, 0x47,
+    0x01, 0x00, 0x00, 0x01, 0x00, 0x00
+};
+static const uint8_t kIPv4Frag2[] = {
+    0x45, 0x00, 0x00, 0x24, 0xfe, 0x47, 0x20, 0x02, 0x40, 0x11,
+    0x8c, 0x6b, 0xc0, 0x00, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08,
+    0x00, 0x00, 0x00, 0x00, 0x04, 0x69, 0x70, 0x76, 0x34, 0x06,
+    0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65
+};
+static const uint8_t kIPv4Frag3[] = {
+    0x45, 0x00, 0x00, 0x1d, 0xfe, 0x47, 0x00, 0x04, 0x40, 0x11,
+    0xac, 0x70, 0xc0, 0x00, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08,
+    0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01
+};
+static const uint8_t *kIPv4Fragments[] = { kIPv4Frag1, kIPv4Frag2, kIPv4Frag3 };
+static const size_t kIPv4FragLengths[] = { sizeof(kIPv4Frag1), sizeof(kIPv4Frag2),
+                                           sizeof(kIPv4Frag3) };
+
+static const uint8_t kIPv6Frag1[] = {
+    0x60, 0x00, 0x00, 0x00, 0x00, 0x18, 0x2c, 0x40, 0x20, 0x01,
+    0x0d, 0xb8, 0x00, 0x00, 0x0b, 0x11, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x04, 0x64, 0x00, 0x64, 0xff, 0x9b, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08,
+    0x11, 0x00, 0x00, 0x01, 0x00, 0x00, 0xfe, 0x47, 0x14, 0x5d,
+    0x00, 0x35, 0x00, 0x29, 0xeb, 0x91, 0x50, 0x47, 0x01, 0x00,
+    0x00, 0x01, 0x00, 0x00
+};
+
+static const uint8_t kIPv6Frag2[] = {
+    0x60, 0x00, 0x00, 0x00, 0x00, 0x18, 0x2c, 0x40, 0x20, 0x01,
+    0x0d, 0xb8, 0x00, 0x00, 0x0b, 0x11, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x04, 0x64, 0x00, 0x64, 0xff, 0x9b, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08,
+    0x11, 0x00, 0x00, 0x11, 0x00, 0x00, 0xfe, 0x47, 0x00, 0x00,
+    0x00, 0x00, 0x04, 0x69, 0x70, 0x76, 0x34, 0x06, 0x67, 0x6f,
+    0x6f, 0x67, 0x6c, 0x65
+};
+
+static const uint8_t kIPv6Frag3[] = {
+    0x60, 0x00, 0x00, 0x00, 0x00, 0x11, 0x2c, 0x40, 0x20, 0x01,
+    0x0d, 0xb8, 0x00, 0x00, 0x0b, 0x11, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x04, 0x64, 0x00, 0x64, 0xff, 0x9b, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08,
+    0x11, 0x00, 0x00, 0x20, 0x00, 0x00, 0xfe, 0x47, 0x03, 0x63,
+    0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01
+};
+static const uint8_t *kIPv6Fragments[] = { kIPv6Frag1, kIPv6Frag2, kIPv6Frag3 };
+static const size_t kIPv6FragLengths[] = { sizeof(kIPv6Frag1), sizeof(kIPv6Frag2),
+                                           sizeof(kIPv6Frag3) };
+
+static const uint8_t kReassembledIPv4[] = {
+    0x45, 0x00, 0x00, 0x3d, 0xfe, 0x47, 0x00, 0x00, 0x40, 0x11,
+    0xac, 0x54, 0xc0, 0x00, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08,
+    0x14, 0x5d, 0x00, 0x35, 0x00, 0x29, 0x68, 0xbb, 0x50, 0x47,
+    0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x04, 0x69, 0x70, 0x76, 0x34, 0x06, 0x67, 0x6f, 0x6f, 0x67,
+    0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00,
+    0x01
+};
+// clang-format on
+
+// Expected checksums.
+static const uint32_t kUdpPartialChecksum     = 0xd5c8;
+static const uint32_t kPayloadPartialChecksum = 0x31e9c;
+static const uint16_t kUdpV4Checksum          = 0xd0c7;
+static const uint16_t kUdpV6Checksum          = 0xa74a;
+
+uint8_t ip_version(const uint8_t *packet) {
+  uint8_t version = packet[0] >> 4;
+  return version;
+}
+
+int is_ipv4_fragment(struct iphdr *ip) {
+  // A packet is a fragment if its fragment offset is nonzero or if the MF flag is set.
+  return ntohs(ip->frag_off) & (IP_OFFMASK | IP_MF);
+}
+
+int is_ipv6_fragment(struct ip6_hdr *ip6, size_t len) {
+  if (ip6->ip6_nxt != IPPROTO_FRAGMENT) {
+    return 0;
+  }
+  struct ip6_frag *frag = (struct ip6_frag *)(ip6 + 1);
+  return len >= sizeof(*ip6) + sizeof(*frag) &&
+         (frag->ip6f_offlg & (IP6F_OFF_MASK | IP6F_MORE_FRAG));
+}
+
+int ipv4_fragment_offset(struct iphdr *ip) {
+  return ntohs(ip->frag_off) & IP_OFFMASK;
+}
+
+int ipv6_fragment_offset(struct ip6_frag *frag) {
+  return ntohs((frag->ip6f_offlg & IP6F_OFF_MASK) >> 3);
+}
+
+void check_packet(const uint8_t *packet, size_t len, const char *msg) {
+  void *payload;
+  size_t payload_length    = 0;
+  uint32_t pseudo_checksum = 0;
+  uint8_t protocol         = 0;
+  int version              = ip_version(packet);
+  switch (version) {
+    case 4: {
+      struct iphdr *ip = (struct iphdr *)packet;
+      ASSERT_GE(len, sizeof(*ip)) << msg << ": IPv4 packet shorter than IPv4 header\n";
+      EXPECT_EQ(5, ip->ihl) << msg << ": Unsupported IP header length\n";
+      EXPECT_EQ(len, ntohs(ip->tot_len)) << msg << ": Incorrect IPv4 length\n";
+      EXPECT_EQ(0, ip_checksum(ip, sizeof(*ip))) << msg << ": Incorrect IP checksum\n";
+      protocol = ip->protocol;
+      payload  = ip + 1;
+      if (!is_ipv4_fragment(ip)) {
+        payload_length  = len - sizeof(*ip);
+        pseudo_checksum = ipv4_pseudo_header_checksum(ip, payload_length);
+      }
+      ASSERT_TRUE(protocol == IPPROTO_TCP || protocol == IPPROTO_UDP || protocol == IPPROTO_ICMP)
+        << msg << ": Unsupported IPv4 protocol " << protocol << "\n";
+      break;
+    }
+    case 6: {
+      struct ip6_hdr *ip6 = (struct ip6_hdr *)packet;
+      ASSERT_GE(len, sizeof(*ip6)) << msg << ": IPv6 packet shorter than IPv6 header\n";
+      EXPECT_EQ(len - sizeof(*ip6), htons(ip6->ip6_plen)) << msg << ": Incorrect IPv6 length\n";
+
+      if (ip6->ip6_nxt == IPPROTO_FRAGMENT) {
+        struct ip6_frag *frag = (struct ip6_frag *)(ip6 + 1);
+        ASSERT_GE(len, sizeof(*ip6) + sizeof(*frag))
+          << msg << ": IPv6 fragment: short fragment header\n";
+        protocol = frag->ip6f_nxt;
+        payload  = frag + 1;
+        // Even though the packet has a Fragment header, it might not be a fragment.
+        if (!is_ipv6_fragment(ip6, len)) {
+          payload_length = len - sizeof(*ip6) - sizeof(*frag);
+        }
+      } else {
+        // Since there are no extension headers except Fragment, this must be the payload.
+        protocol       = ip6->ip6_nxt;
+        payload        = ip6 + 1;
+        payload_length = len - sizeof(*ip6);
+      }
+      ASSERT_TRUE(protocol == IPPROTO_TCP || protocol == IPPROTO_UDP || protocol == IPPROTO_ICMPV6)
+        << msg << ": Unsupported IPv6 next header " << protocol;
+      if (payload_length) {
+        pseudo_checksum = ipv6_pseudo_header_checksum(ip6, payload_length, protocol);
+      }
+      break;
+    }
+    default:
+      FAIL() << msg << ": Unsupported IP version " << version << "\n";
+      return;
+  }
+
+  // If we understand the payload, verify the checksum.
+  if (payload_length) {
+    uint16_t checksum;
+    switch (protocol) {
+      case IPPROTO_UDP:
+      case IPPROTO_TCP:
+      case IPPROTO_ICMPV6:
+        checksum = ip_checksum_finish(ip_checksum_add(pseudo_checksum, payload, payload_length));
+        break;
+      case IPPROTO_ICMP:
+        checksum = ip_checksum(payload, payload_length);
+        break;
+      default:
+        checksum = 0;  // Don't check.
+        break;
+    }
+    EXPECT_EQ(0, checksum) << msg << ": Incorrect transport checksum\n";
+  }
+
+  if (protocol == IPPROTO_UDP) {
+    struct udphdr *udp = (struct udphdr *)payload;
+    EXPECT_NE(0, udp->check) << msg << ": UDP checksum 0 should be 0xffff";
+    // If this is not a fragment, check the UDP length field.
+    if (payload_length) {
+      EXPECT_EQ(payload_length, ntohs(udp->len)) << msg << ": Incorrect UDP length\n";
+    }
+  }
+}
+
+void reassemble_packet(const uint8_t **fragments, const size_t lengths[], int numpackets,
+                       uint8_t *reassembled, size_t *reassembled_len, const char *msg) {
+  struct iphdr *ip    = nullptr;
+  struct ip6_hdr *ip6 = nullptr;
+  size_t total_length, pos = 0;
+  uint8_t protocol = 0;
+  uint8_t version  = ip_version(fragments[0]);
+
+  for (int i = 0; i < numpackets; i++) {
+    const uint8_t *packet = fragments[i];
+    int len               = lengths[i];
+    int headersize, payload_offset;
+
+    ASSERT_EQ(ip_version(packet), version) << msg << ": Inconsistent fragment versions\n";
+    check_packet(packet, len, "Fragment sanity check");
+
+    switch (version) {
+      case 4: {
+        struct iphdr *ip_orig = (struct iphdr *)packet;
+        headersize            = sizeof(*ip_orig);
+        ASSERT_TRUE(is_ipv4_fragment(ip_orig))
+          << msg << ": IPv4 fragment #" << i + 1 << " not a fragment\n";
+        ASSERT_EQ(pos, ipv4_fragment_offset(ip_orig) * 8 + ((i != 0) ? sizeof(*ip) : 0))
+          << msg << ": IPv4 fragment #" << i + 1 << ": inconsistent offset\n";
+
+        headersize     = sizeof(*ip_orig);
+        payload_offset = headersize;
+        if (pos == 0) {
+          ip = (struct iphdr *)reassembled;
+        }
+        break;
+      }
+      case 6: {
+        struct ip6_hdr *ip6_orig = (struct ip6_hdr *)packet;
+        struct ip6_frag *frag    = (struct ip6_frag *)(ip6_orig + 1);
+        ASSERT_TRUE(is_ipv6_fragment(ip6_orig, len))
+          << msg << ": IPv6 fragment #" << i + 1 << " not a fragment\n";
+        ASSERT_EQ(pos, ipv6_fragment_offset(frag) * 8 + ((i != 0) ? sizeof(*ip6) : 0))
+          << msg << ": IPv6 fragment #" << i + 1 << ": inconsistent offset\n";
+
+        headersize     = sizeof(*ip6_orig);
+        payload_offset = sizeof(*ip6_orig) + sizeof(*frag);
+        if (pos == 0) {
+          ip6      = (struct ip6_hdr *)reassembled;
+          protocol = frag->ip6f_nxt;
+        }
+        break;
+      }
+      default:
+        FAIL() << msg << ": Invalid IP version << " << version;
+    }
+
+    // If this is the first fragment, copy the header.
+    if (pos == 0) {
+      ASSERT_LT(headersize, (int)*reassembled_len) << msg << ": Reassembly buffer too small\n";
+      memcpy(reassembled, packet, headersize);
+      total_length = headersize;
+      pos += headersize;
+    }
+
+    // Copy the payload.
+    int payload_length = len - payload_offset;
+    total_length += payload_length;
+    ASSERT_LT(total_length, *reassembled_len) << msg << ": Reassembly buffer too small\n";
+    memcpy(reassembled + pos, packet + payload_offset, payload_length);
+    pos += payload_length;
+  }
+
+  // Fix up the reassembled headers to reflect fragmentation and length (and IPv4 checksum).
+  ASSERT_EQ(total_length, pos) << msg << ": Reassembled packet length incorrect\n";
+  if (ip) {
+    ip->frag_off &= ~htons(IP_MF);
+    ip->tot_len = htons(total_length);
+    ip->check   = 0;
+    ip->check   = ip_checksum(ip, sizeof(*ip));
+    ASSERT_FALSE(is_ipv4_fragment(ip)) << msg << ": reassembled IPv4 packet is a fragment!\n";
+  }
+  if (ip6) {
+    ip6->ip6_nxt  = protocol;
+    ip6->ip6_plen = htons(total_length - sizeof(*ip6));
+    ASSERT_FALSE(is_ipv6_fragment(ip6, ip6->ip6_plen))
+      << msg << ": reassembled IPv6 packet is a fragment!\n";
+  }
+
+  *reassembled_len = total_length;
+}
+
+void check_data_matches(const void *expected, const void *actual, size_t len, const char *msg) {
+  if (memcmp(expected, actual, len)) {
+    // Hex dump, 20 bytes per line, one space between bytes (1 byte = 3 chars), indented by 4.
+    int hexdump_len = len * 3 + (len / 20 + 1) * 5;
+    char expected_hexdump[hexdump_len], actual_hexdump[hexdump_len];
+    unsigned pos = 0;
+    for (unsigned i = 0; i < len; i++) {
+      if (i % 20 == 0) {
+        snprintf(expected_hexdump + pos, hexdump_len - pos, "\n   ");
+        snprintf(actual_hexdump + pos, hexdump_len - pos, "\n   ");
+        pos += 4;
+      }
+      snprintf(expected_hexdump + pos, hexdump_len - pos, " %02x", ((uint8_t *)expected)[i]);
+      snprintf(actual_hexdump + pos, hexdump_len - pos, " %02x", ((uint8_t *)actual)[i]);
+      pos += 3;
+    }
+    FAIL() << msg << ": Data doesn't match"
+           << "\n  Expected:" << (char *) expected_hexdump
+           << "\n  Actual:" << (char *) actual_hexdump << "\n";
+  }
+}
+
+void fix_udp_checksum(uint8_t *packet) {
+  uint32_t pseudo_checksum;
+  uint8_t version = ip_version(packet);
+  struct udphdr *udp;
+  switch (version) {
+    case 4: {
+      struct iphdr *ip = (struct iphdr *)packet;
+      udp              = (struct udphdr *)(ip + 1);
+      pseudo_checksum  = ipv4_pseudo_header_checksum(ip, ntohs(udp->len));
+      break;
+    }
+    case 6: {
+      struct ip6_hdr *ip6 = (struct ip6_hdr *)packet;
+      udp                 = (struct udphdr *)(ip6 + 1);
+      pseudo_checksum     = ipv6_pseudo_header_checksum(ip6, ntohs(udp->len), IPPROTO_UDP);
+      break;
+    }
+    default:
+      FAIL() << "unsupported IP version" << version << "\n";
+      return;
+  }
+
+  udp->check = 0;
+  udp->check = ip_checksum_finish(ip_checksum_add(pseudo_checksum, udp, ntohs(udp->len)));
+}
+
+// Testing stub for send_rawv6. The real version uses sendmsg() with a
+// destination IPv6 address, and attempting to call that on our test socketpair
+// fd results in EINVAL.
+extern "C" void send_rawv6(int fd, clat_packet out, int iov_len) { writev(fd, out, iov_len); }
+
+void do_translate_packet(const uint8_t *original, size_t original_len, uint8_t *out, size_t *outlen,
+                         const char *msg) {
+  int fds[2];
+  if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK, 0, fds)) {
+    abort();
+  }
+
+  char foo[512];
+  snprintf(foo, sizeof(foo), "%s: Invalid original packet", msg);
+  check_packet(original, original_len, foo);
+
+  int read_fd, write_fd;
+  uint16_t expected_proto;
+  int version = ip_version(original);
+  switch (version) {
+    case 4:
+      expected_proto = htons(ETH_P_IPV6);
+      read_fd        = fds[1];
+      write_fd       = fds[0];
+      break;
+    case 6:
+      expected_proto = htons(ETH_P_IP);
+      read_fd        = fds[0];
+      write_fd       = fds[1];
+      break;
+    default:
+      FAIL() << msg << ": Unsupported IP version " << version << "\n";
+      break;
+  }
+
+  translate_packet(write_fd, (version == 4), original, original_len);
+
+  snprintf(foo, sizeof(foo), "%s: Invalid translated packet", msg);
+  if (version == 6) {
+    // Translating to IPv4. Expect a tun header.
+    struct tun_pi new_tun_header;
+    struct iovec iov[] = {
+      { &new_tun_header, sizeof(new_tun_header) },
+      { out, *outlen },
+    };
+
+    int len = readv(read_fd, iov, 2);
+    if (len > (int)sizeof(new_tun_header)) {
+      ASSERT_LT((size_t)len, *outlen) << msg << ": Translated packet buffer too small\n";
+      EXPECT_EQ(expected_proto, new_tun_header.proto) << msg << "Unexpected tun proto\n";
+      *outlen = len - sizeof(new_tun_header);
+      check_packet(out, *outlen, msg);
+    } else {
+      FAIL() << msg << ": Packet was not translated: len=" << len;
+      *outlen = 0;
+    }
+  } else {
+    // Translating to IPv6. Expect raw packet.
+    *outlen = read(read_fd, out, *outlen);
+    check_packet(out, *outlen, msg);
+  }
+}
+
+void check_translated_packet(const uint8_t *original, size_t original_len, const uint8_t *expected,
+                             size_t expected_len, const char *msg) {
+  uint8_t translated[MAXMTU];
+  size_t translated_len = sizeof(translated);
+  do_translate_packet(original, original_len, translated, &translated_len, msg);
+  EXPECT_EQ(expected_len, translated_len) << msg << ": Translated packet length incorrect\n";
+  check_data_matches(expected, translated, translated_len, msg);
+}
+
+void check_fragment_translation(const uint8_t *original[], const size_t original_lengths[],
+                                const uint8_t *expected[], const size_t expected_lengths[],
+                                int numfragments, const char *msg) {
+  for (int i = 0; i < numfragments; i++) {
+    // Check that each of the fragments translates as expected.
+    char frag_msg[512];
+    snprintf(frag_msg, sizeof(frag_msg), "%s: fragment #%d", msg, i + 1);
+    check_translated_packet(original[i], original_lengths[i], expected[i], expected_lengths[i],
+                            frag_msg);
+  }
+
+  // Sanity check that reassembling the original and translated fragments produces valid packets.
+  uint8_t reassembled[MAXMTU];
+  size_t reassembled_len = sizeof(reassembled);
+  reassemble_packet(original, original_lengths, numfragments, reassembled, &reassembled_len, msg);
+  check_packet(reassembled, reassembled_len, msg);
+
+  uint8_t translated[MAXMTU];
+  size_t translated_len = sizeof(translated);
+  do_translate_packet(reassembled, reassembled_len, translated, &translated_len, msg);
+  check_packet(translated, translated_len, msg);
+}
+
+int get_transport_checksum(const uint8_t *packet) {
+  struct iphdr *ip;
+  struct ip6_hdr *ip6;
+  uint8_t protocol;
+  const void *payload;
+
+  int version = ip_version(packet);
+  switch (version) {
+    case 4:
+      ip = (struct iphdr *)packet;
+      if (is_ipv4_fragment(ip)) {
+        return -1;
+      }
+      protocol = ip->protocol;
+      payload  = ip + 1;
+      break;
+    case 6:
+      ip6      = (struct ip6_hdr *)packet;
+      protocol = ip6->ip6_nxt;
+      payload  = ip6 + 1;
+      break;
+    default:
+      return -1;
+  }
+
+  switch (protocol) {
+    case IPPROTO_UDP:
+      return ((struct udphdr *)payload)->check;
+
+    case IPPROTO_TCP:
+      return ((struct tcphdr *)payload)->check;
+
+    case IPPROTO_FRAGMENT:
+    default:
+      return -1;
+  }
+}
+
+class ClatdTest : public ::testing::Test {
+ protected:
+  static TunInterface sTun;
+
+  virtual void SetUp() {
+    inet_pton(AF_INET, kIPv4LocalAddr, &Global_Clatd_Config.ipv4_local_subnet);
+    inet_pton(AF_INET6, kIPv6PlatSubnet, &Global_Clatd_Config.plat_subnet);
+    memset(&Global_Clatd_Config.ipv6_local_subnet, 0, sizeof(in6_addr));
+    Global_Clatd_Config.native_ipv6_interface = const_cast<char *>(sTun.name().c_str());
+  }
+
+  // Static because setting up the tun interface takes about 40ms.
+  static void SetUpTestCase() { ASSERT_EQ(0, sTun.init()); }
+
+  // Closing the socket removes the interface and IP addresses.
+  static void TearDownTestCase() { sTun.destroy(); }
+};
+
+TunInterface ClatdTest::sTun;
+
+void expect_ipv6_addr_equal(struct in6_addr *expected, struct in6_addr *actual) {
+  if (!IN6_ARE_ADDR_EQUAL(expected, actual)) {
+    char expected_str[INET6_ADDRSTRLEN], actual_str[INET6_ADDRSTRLEN];
+    inet_ntop(AF_INET6, expected, expected_str, sizeof(expected_str));
+    inet_ntop(AF_INET6, actual, actual_str, sizeof(actual_str));
+    FAIL()
+        << "Unexpected IPv6 address:: "
+        << "\n  Expected: " << expected_str
+        << "\n  Actual:   " << actual_str
+        << "\n";
+  }
+}
+
+TEST_F(ClatdTest, TestIPv6PrefixEqual) {
+  EXPECT_TRUE(ipv6_prefix_equal(&Global_Clatd_Config.plat_subnet,
+                                &Global_Clatd_Config.plat_subnet));
+  EXPECT_FALSE(ipv6_prefix_equal(&Global_Clatd_Config.plat_subnet,
+                                 &Global_Clatd_Config.ipv6_local_subnet));
+
+  struct in6_addr subnet2 = Global_Clatd_Config.ipv6_local_subnet;
+  EXPECT_TRUE(ipv6_prefix_equal(&Global_Clatd_Config.ipv6_local_subnet, &subnet2));
+  EXPECT_TRUE(ipv6_prefix_equal(&subnet2, &Global_Clatd_Config.ipv6_local_subnet));
+
+  subnet2.s6_addr[6] = 0xff;
+  EXPECT_FALSE(ipv6_prefix_equal(&Global_Clatd_Config.ipv6_local_subnet, &subnet2));
+  EXPECT_FALSE(ipv6_prefix_equal(&subnet2, &Global_Clatd_Config.ipv6_local_subnet));
+}
+
+TEST_F(ClatdTest, DataSanitycheck) {
+  // Sanity checks the data.
+  uint8_t v4_header[] = { IPV4_UDP_HEADER };
+  ASSERT_EQ(sizeof(struct iphdr), sizeof(v4_header)) << "Test IPv4 header: incorrect length\n";
+
+  uint8_t v6_header[] = { IPV6_UDP_HEADER };
+  ASSERT_EQ(sizeof(struct ip6_hdr), sizeof(v6_header)) << "Test IPv6 header: incorrect length\n";
+
+  uint8_t udp_header[] = { UDP_HEADER };
+  ASSERT_EQ(sizeof(struct udphdr), sizeof(udp_header)) << "Test UDP header: incorrect length\n";
+
+  // Sanity checks check_packet.
+  struct udphdr *udp;
+  uint8_t v4_udp_packet[] = { IPV4_UDP_HEADER UDP_HEADER PAYLOAD };
+  udp                     = (struct udphdr *)(v4_udp_packet + sizeof(struct iphdr));
+  fix_udp_checksum(v4_udp_packet);
+  ASSERT_EQ(kUdpV4Checksum, udp->check) << "UDP/IPv4 packet checksum sanity check\n";
+  check_packet(v4_udp_packet, sizeof(v4_udp_packet), "UDP/IPv4 packet sanity check");
+
+  uint8_t v6_udp_packet[] = { IPV6_UDP_HEADER UDP_HEADER PAYLOAD };
+  udp                     = (struct udphdr *)(v6_udp_packet + sizeof(struct ip6_hdr));
+  fix_udp_checksum(v6_udp_packet);
+  ASSERT_EQ(kUdpV6Checksum, udp->check) << "UDP/IPv6 packet checksum sanity check\n";
+  check_packet(v6_udp_packet, sizeof(v6_udp_packet), "UDP/IPv6 packet sanity check");
+
+  uint8_t ipv4_ping[] = { IPV4_ICMP_HEADER IPV4_PING PAYLOAD };
+  check_packet(ipv4_ping, sizeof(ipv4_ping), "IPv4 ping sanity check");
+
+  uint8_t ipv6_ping[] = { IPV6_ICMPV6_HEADER IPV6_PING PAYLOAD };
+  check_packet(ipv6_ping, sizeof(ipv6_ping), "IPv6 ping sanity check");
+
+  // Sanity checks reassemble_packet.
+  uint8_t reassembled[MAXMTU];
+  size_t total_length = sizeof(reassembled);
+  reassemble_packet(kIPv4Fragments, kIPv4FragLengths, ARRAYSIZE(kIPv4Fragments), reassembled,
+                    &total_length, "Reassembly sanity check");
+  check_packet(reassembled, total_length, "IPv4 Reassembled packet is valid");
+  ASSERT_EQ(sizeof(kReassembledIPv4), total_length) << "IPv4 reassembly sanity check: length\n";
+  ASSERT_TRUE(!is_ipv4_fragment((struct iphdr *)reassembled))
+    << "Sanity check: reassembled packet is a fragment!\n";
+  check_data_matches(kReassembledIPv4, reassembled, total_length, "IPv4 reassembly sanity check");
+
+  total_length = sizeof(reassembled);
+  reassemble_packet(kIPv6Fragments, kIPv6FragLengths, ARRAYSIZE(kIPv6Fragments), reassembled,
+                    &total_length, "IPv6 reassembly sanity check");
+  ASSERT_TRUE(!is_ipv6_fragment((struct ip6_hdr *)reassembled, total_length))
+    << "Sanity check: reassembled packet is a fragment!\n";
+  check_packet(reassembled, total_length, "IPv6 Reassembled packet is valid");
+}
+
+TEST_F(ClatdTest, PseudoChecksum) {
+  uint32_t pseudo_checksum;
+
+  uint8_t v4_header[]        = { IPV4_UDP_HEADER };
+  uint8_t v4_pseudo_header[] = { IPV4_PSEUDOHEADER(v4_header, UDP_LEN) };
+  pseudo_checksum            = ipv4_pseudo_header_checksum((struct iphdr *)v4_header, UDP_LEN);
+  EXPECT_EQ(ip_checksum_finish(pseudo_checksum),
+            ip_checksum(v4_pseudo_header, sizeof(v4_pseudo_header)))
+    << "ipv4_pseudo_header_checksum incorrect\n";
+
+  uint8_t v6_header[]        = { IPV6_UDP_HEADER };
+  uint8_t v6_pseudo_header[] = { IPV6_PSEUDOHEADER(v6_header, IPPROTO_UDP, UDP_LEN) };
+  pseudo_checksum = ipv6_pseudo_header_checksum((struct ip6_hdr *)v6_header, UDP_LEN, IPPROTO_UDP);
+  EXPECT_EQ(ip_checksum_finish(pseudo_checksum),
+            ip_checksum(v6_pseudo_header, sizeof(v6_pseudo_header)))
+    << "ipv6_pseudo_header_checksum incorrect\n";
+}
+
+TEST_F(ClatdTest, TransportChecksum) {
+  uint8_t udphdr[]  = { UDP_HEADER };
+  uint8_t payload[] = { PAYLOAD };
+  EXPECT_EQ(kUdpPartialChecksum, ip_checksum_add(0, udphdr, sizeof(udphdr)))
+    << "UDP partial checksum\n";
+  EXPECT_EQ(kPayloadPartialChecksum, ip_checksum_add(0, payload, sizeof(payload)))
+    << "Payload partial checksum\n";
+
+  uint8_t ip[]             = { IPV4_UDP_HEADER };
+  uint8_t ip6[]            = { IPV6_UDP_HEADER };
+  uint32_t ipv4_pseudo_sum = ipv4_pseudo_header_checksum((struct iphdr *)ip, UDP_LEN);
+  uint32_t ipv6_pseudo_sum =
+    ipv6_pseudo_header_checksum((struct ip6_hdr *)ip6, UDP_LEN, IPPROTO_UDP);
+
+  EXPECT_NE(0, ipv4_pseudo_sum);
+  EXPECT_NE(0, ipv6_pseudo_sum);
+  EXPECT_EQ(0x3ad0U, ipv4_pseudo_sum % 0xFFFF) << "IPv4 pseudo-checksum sanity check\n";
+  EXPECT_EQ(0x644dU, ipv6_pseudo_sum % 0xFFFF) << "IPv6 pseudo-checksum sanity check\n";
+  EXPECT_EQ(
+      kUdpV4Checksum,
+      ip_checksum_finish(ipv4_pseudo_sum + kUdpPartialChecksum + kPayloadPartialChecksum))
+      << "Unexpected UDP/IPv4 checksum\n";
+  EXPECT_EQ(
+      kUdpV6Checksum,
+      ip_checksum_finish(ipv6_pseudo_sum + kUdpPartialChecksum + kPayloadPartialChecksum))
+      << "Unexpected UDP/IPv6 checksum\n";
+
+  EXPECT_EQ(kUdpV6Checksum,
+      ip_checksum_adjust(kUdpV4Checksum, ipv4_pseudo_sum, ipv6_pseudo_sum))
+      << "Adjust IPv4/UDP checksum to IPv6\n";
+  EXPECT_EQ(kUdpV4Checksum,
+      ip_checksum_adjust(kUdpV6Checksum, ipv6_pseudo_sum, ipv4_pseudo_sum))
+      << "Adjust IPv6/UDP checksum to IPv4\n";
+}
+
+TEST_F(ClatdTest, AdjustChecksum) {
+  struct checksum_data {
+    uint16_t checksum;
+    uint32_t old_hdr_sum;
+    uint32_t new_hdr_sum;
+    uint16_t result;
+  } DATA[] = {
+    { 0x1423, 0xb8ec, 0x2d757, 0xf5b5 },
+    { 0xf5b5, 0x2d757, 0xb8ec, 0x1423 },
+    { 0xdd2f, 0x5555, 0x3285, 0x0000 },
+    { 0x1215, 0x5560, 0x15560 + 20, 0x1200 },
+    { 0xd0c7, 0x3ad0, 0x2644b, 0xa74a },
+  };
+  unsigned i = 0;
+
+  for (i = 0; i < ARRAYSIZE(DATA); i++) {
+    struct checksum_data *data = DATA + i;
+    uint16_t result = ip_checksum_adjust(data->checksum, data->old_hdr_sum, data->new_hdr_sum);
+    EXPECT_EQ(result, data->result)
+        << "Incorrect checksum" << std::showbase << std::hex
+        << "\n  Expected: " << data->result
+        << "\n  Actual:   " << result
+        << "\n    checksum=" << data->checksum
+        << " old_sum=" << data->old_hdr_sum << " new_sum=" << data->new_hdr_sum << "\n";
+  }
+}
+
+TEST_F(ClatdTest, Translate) {
+  // This test uses hardcoded packets so the clatd address must be fixed.
+  inet_pton(AF_INET6, kIPv6LocalAddr, &Global_Clatd_Config.ipv6_local_subnet);
+
+  uint8_t udp_ipv4[] = { IPV4_UDP_HEADER UDP_HEADER PAYLOAD };
+  uint8_t udp_ipv6[] = { IPV6_UDP_HEADER UDP_HEADER PAYLOAD };
+  fix_udp_checksum(udp_ipv4);
+  fix_udp_checksum(udp_ipv6);
+  check_translated_packet(udp_ipv4, sizeof(udp_ipv4), udp_ipv6, sizeof(udp_ipv6),
+                          "UDP/IPv4 -> UDP/IPv6 translation");
+  check_translated_packet(udp_ipv6, sizeof(udp_ipv6), udp_ipv4, sizeof(udp_ipv4),
+                          "UDP/IPv6 -> UDP/IPv4 translation");
+
+  uint8_t ipv4_ping[] = { IPV4_ICMP_HEADER IPV4_PING PAYLOAD };
+  uint8_t ipv6_ping[] = { IPV6_ICMPV6_HEADER IPV6_PING PAYLOAD };
+  check_translated_packet(ipv4_ping, sizeof(ipv4_ping), ipv6_ping, sizeof(ipv6_ping),
+                          "ICMP->ICMPv6 translation");
+  check_translated_packet(ipv6_ping, sizeof(ipv6_ping), ipv4_ping, sizeof(ipv4_ping),
+                          "ICMPv6->ICMP translation");
+}
+
+TEST_F(ClatdTest, Fragmentation) {
+  // This test uses hardcoded packets so the clatd address must be fixed.
+  inet_pton(AF_INET6, kIPv6LocalAddr, &Global_Clatd_Config.ipv6_local_subnet);
+
+  check_fragment_translation(kIPv4Fragments, kIPv4FragLengths, kIPv6Fragments, kIPv6FragLengths,
+                             ARRAYSIZE(kIPv4Fragments), "IPv4->IPv6 fragment translation");
+
+  check_fragment_translation(kIPv6Fragments, kIPv6FragLengths, kIPv4Fragments, kIPv4FragLengths,
+                             ARRAYSIZE(kIPv6Fragments), "IPv6->IPv4 fragment translation");
+}
+
+// picks a random interface ID that is checksum neutral with the IPv4 address and the NAT64 prefix
+void gen_random_iid(struct in6_addr *myaddr, struct in_addr *ipv4_local_subnet,
+                    struct in6_addr *plat_subnet) {
+  // Fill last 8 bytes of IPv6 address with random bits.
+  arc4random_buf(&myaddr->s6_addr[8], 8);
+
+  // Make the IID checksum-neutral. That is, make it so that:
+  //   checksum(Local IPv4 | Remote IPv4) = checksum(Local IPv6 | Remote IPv6)
+  // in other words (because remote IPv6 = NAT64 prefix | Remote IPv4):
+  //   checksum(Local IPv4) = checksum(Local IPv6 | NAT64 prefix)
+  // Do this by adjusting the two bytes in the middle of the IID.
+
+  uint16_t middlebytes = (myaddr->s6_addr[11] << 8) + myaddr->s6_addr[12];
+
+  uint32_t c1 = ip_checksum_add(0, ipv4_local_subnet, sizeof(*ipv4_local_subnet));
+  uint32_t c2 = ip_checksum_add(0, plat_subnet, sizeof(*plat_subnet)) +
+                ip_checksum_add(0, myaddr, sizeof(*myaddr));
+
+  uint16_t delta      = ip_checksum_adjust(middlebytes, c1, c2);
+  myaddr->s6_addr[11] = delta >> 8;
+  myaddr->s6_addr[12] = delta & 0xff;
+}
+
+void check_translate_checksum_neutral(const uint8_t *original, size_t original_len,
+                                      size_t expected_len, const char *msg) {
+  uint8_t translated[MAXMTU];
+  size_t translated_len = sizeof(translated);
+  do_translate_packet(original, original_len, translated, &translated_len, msg);
+  EXPECT_EQ(expected_len, translated_len) << msg << ": Translated packet length incorrect\n";
+  // do_translate_packet already checks packets for validity and verifies the checksum.
+  int original_check   = get_transport_checksum(original);
+  int translated_check = get_transport_checksum(translated);
+  ASSERT_NE(-1, original_check);
+  ASSERT_NE(-1, translated_check);
+  ASSERT_EQ(original_check, translated_check)
+    << "Not checksum neutral: original and translated checksums differ\n";
+}
+
+TEST_F(ClatdTest, TranslateChecksumNeutral) {
+  // Generate a random clat IPv6 address and check that translation is checksum-neutral.
+  ASSERT_TRUE(inet_pton(AF_INET6, "2001:db8:1:2:f076:ae99:124e:aa54",
+                        &Global_Clatd_Config.ipv6_local_subnet));
+
+  gen_random_iid(&Global_Clatd_Config.ipv6_local_subnet, &Global_Clatd_Config.ipv4_local_subnet,
+                 &Global_Clatd_Config.plat_subnet);
+
+  ASSERT_NE(htonl((uint32_t)0x00000464), Global_Clatd_Config.ipv6_local_subnet.s6_addr32[3]);
+  ASSERT_NE((uint32_t)0, Global_Clatd_Config.ipv6_local_subnet.s6_addr32[3]);
+
+  // Check that translating UDP packets is checksum-neutral. First, IPv4.
+  uint8_t udp_ipv4[] = { IPV4_UDP_HEADER UDP_HEADER PAYLOAD };
+  fix_udp_checksum(udp_ipv4);
+  check_translate_checksum_neutral(udp_ipv4, sizeof(udp_ipv4), sizeof(udp_ipv4) + 20,
+                                   "UDP/IPv4 -> UDP/IPv6 checksum neutral");
+
+  // Now try IPv6.
+  uint8_t udp_ipv6[] = { IPV6_UDP_HEADER UDP_HEADER PAYLOAD };
+  // The test packet uses the static IID, not the random IID. Fix up the source address.
+  struct ip6_hdr *ip6 = (struct ip6_hdr *)udp_ipv6;
+  memcpy(&ip6->ip6_src, &Global_Clatd_Config.ipv6_local_subnet, sizeof(ip6->ip6_src));
+  fix_udp_checksum(udp_ipv6);
+  check_translate_checksum_neutral(udp_ipv4, sizeof(udp_ipv4), sizeof(udp_ipv4) + 20,
+                                   "UDP/IPv4 -> UDP/IPv6 checksum neutral");
+}
diff --git a/clatd/common.h b/clatd/common.h
new file mode 100644
index 0000000..e9551ee
--- /dev/null
+++ b/clatd/common.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 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.
+ *
+ * common.h - common definitions
+ */
+#ifndef __CLATD_COMMON_H__
+#define __CLATD_COMMON_H__
+
+#include <sys/uio.h>
+
+// A clat_packet is an array of iovec structures representing a packet that we are translating.
+// The CLAT_POS_XXX constants represent the array indices within the clat_packet that contain
+// specific parts of the packet. The packet_* functions operate on all the packet segments past a
+// given position.
+typedef enum {
+  CLAT_POS_TUNHDR,
+  CLAT_POS_IPHDR,
+  CLAT_POS_FRAGHDR,
+  CLAT_POS_TRANSPORTHDR,
+  CLAT_POS_ICMPERR_IPHDR,
+  CLAT_POS_ICMPERR_FRAGHDR,
+  CLAT_POS_ICMPERR_TRANSPORTHDR,
+  CLAT_POS_PAYLOAD,
+  CLAT_POS_MAX
+} clat_packet_index;
+typedef struct iovec clat_packet[CLAT_POS_MAX];
+
+#endif /* __CLATD_COMMON_H__ */
diff --git a/clatd/config.h b/clatd/config.h
new file mode 100644
index 0000000..9612192
--- /dev/null
+++ b/clatd/config.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * config.h - configuration settings
+ */
+#ifndef __CONFIG_H__
+#define __CONFIG_H__
+
+#include <linux/if.h>
+#include <netinet/in.h>
+
+struct tun_data {
+  char device4[IFNAMSIZ];
+  int read_fd6, write_fd6, fd4;
+};
+
+struct clat_config {
+  struct in6_addr ipv6_local_subnet;
+  struct in_addr ipv4_local_subnet;
+  struct in6_addr plat_subnet;
+  const char *native_ipv6_interface;
+};
+
+extern struct clat_config Global_Clatd_Config;
+
+/* function: ipv6_prefix_equal
+ * compares the /64 prefixes of two ipv6 addresses.
+ *   a1 - first address
+ *   a2 - second address
+ *   returns: 0 if the subnets are different, 1 if they are the same.
+ */
+static inline int ipv6_prefix_equal(struct in6_addr *a1, struct in6_addr *a2) {
+  return !memcmp(a1, a2, 8);
+}
+
+#endif /* __CONFIG_H__ */
diff --git a/clatd/debug.h b/clatd/debug.h
new file mode 100644
index 0000000..8e09672
--- /dev/null
+++ b/clatd/debug.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * debug.h - debug settings
+ */
+#ifndef __DEBUG_H__
+#define __DEBUG_H__
+
+// set to 1 to enable debug logging and packet dumping.
+#define CLAT_DEBUG 0
+
+#endif /* __DEBUG_H__ */
diff --git a/clatd/dump.c b/clatd/dump.c
new file mode 100644
index 0000000..dff3d5e
--- /dev/null
+++ b/clatd/dump.c
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * dump.c - print various headers for debugging
+ */
+#include "dump.h"
+
+#include <arpa/inet.h>
+#include <linux/icmp.h>
+#include <linux/if_tun.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "checksum.h"
+#include "clatd.h"
+#include "debug.h"
+#include "logging.h"
+
+#if CLAT_DEBUG
+
+/* print ip header */
+void dump_ip(struct iphdr *header) {
+  u_int16_t frag_flags;
+  char addrstr[INET6_ADDRSTRLEN];
+
+  frag_flags = ntohs(header->frag_off);
+
+  printf("IP packet\n");
+  printf("header_len = %x\n", header->ihl);
+  printf("version = %x\n", header->version);
+  printf("tos = %x\n", header->tos);
+  printf("tot_len = %x\n", ntohs(header->tot_len));
+  printf("id = %x\n", ntohs(header->id));
+  printf("frag: ");
+  if (frag_flags & IP_RF) {
+    printf("(RF) ");
+  }
+  if (frag_flags & IP_DF) {
+    printf("DF ");
+  }
+  if (frag_flags & IP_MF) {
+    printf("MF ");
+  }
+  printf("offset = %x\n", frag_flags & IP_OFFMASK);
+  printf("ttl = %x\n", header->ttl);
+  printf("protocol = %x\n", header->protocol);
+  printf("checksum = %x\n", ntohs(header->check));
+  inet_ntop(AF_INET, &header->saddr, addrstr, sizeof(addrstr));
+  printf("saddr = %s\n", addrstr);
+  inet_ntop(AF_INET, &header->daddr, addrstr, sizeof(addrstr));
+  printf("daddr = %s\n", addrstr);
+}
+
+/* print ip6 header */
+void dump_ip6(struct ip6_hdr *header) {
+  char addrstr[INET6_ADDRSTRLEN];
+
+  printf("ipv6\n");
+  printf("version = %x\n", header->ip6_vfc >> 4);
+  printf("traffic class = %x\n", header->ip6_flow >> 20);
+  printf("flow label = %x\n", ntohl(header->ip6_flow & 0x000fffff));
+  printf("payload len = %x\n", ntohs(header->ip6_plen));
+  printf("next header = %x\n", header->ip6_nxt);
+  printf("hop limit = %x\n", header->ip6_hlim);
+
+  inet_ntop(AF_INET6, &header->ip6_src, addrstr, sizeof(addrstr));
+  printf("source = %s\n", addrstr);
+
+  inet_ntop(AF_INET6, &header->ip6_dst, addrstr, sizeof(addrstr));
+  printf("dest = %s\n", addrstr);
+}
+
+/* print icmp header */
+void dump_icmp(struct icmphdr *icmp) {
+  printf("ICMP\n");
+
+  printf("icmp.type = %x ", icmp->type);
+  if (icmp->type == ICMP_ECHOREPLY) {
+    printf("echo reply");
+  } else if (icmp->type == ICMP_ECHO) {
+    printf("echo request");
+  } else {
+    printf("other");
+  }
+  printf("\n");
+  printf("icmp.code = %x\n", icmp->code);
+  printf("icmp.checksum = %x\n", ntohs(icmp->checksum));
+  if (icmp->type == ICMP_ECHOREPLY || icmp->type == ICMP_ECHO) {
+    printf("icmp.un.echo.id = %x\n", ntohs(icmp->un.echo.id));
+    printf("icmp.un.echo.sequence = %x\n", ntohs(icmp->un.echo.sequence));
+  }
+}
+
+/* print icmp6 header */
+void dump_icmp6(struct icmp6_hdr *icmp6) {
+  printf("ICMP6\n");
+  printf("type = %x", icmp6->icmp6_type);
+  if (icmp6->icmp6_type == ICMP6_ECHO_REQUEST) {
+    printf("(echo request)");
+  } else if (icmp6->icmp6_type == ICMP6_ECHO_REPLY) {
+    printf("(echo reply)");
+  }
+  printf("\n");
+  printf("code = %x\n", icmp6->icmp6_code);
+
+  printf("checksum = %x\n", icmp6->icmp6_cksum);
+
+  if ((icmp6->icmp6_type == ICMP6_ECHO_REQUEST) || (icmp6->icmp6_type == ICMP6_ECHO_REPLY)) {
+    printf("icmp6_id = %x\n", icmp6->icmp6_id);
+    printf("icmp6_seq = %x\n", icmp6->icmp6_seq);
+  }
+}
+
+/* print udp header */
+void dump_udp_generic(const struct udphdr *udp, uint32_t temp_checksum, const uint8_t *payload,
+                      size_t payload_size) {
+  uint16_t my_checksum;
+
+  temp_checksum = ip_checksum_add(temp_checksum, udp, sizeof(struct udphdr));
+  temp_checksum = ip_checksum_add(temp_checksum, payload, payload_size);
+  my_checksum   = ip_checksum_finish(temp_checksum);
+
+  printf("UDP\n");
+  printf("source = %x\n", ntohs(udp->source));
+  printf("dest = %x\n", ntohs(udp->dest));
+  printf("len = %x\n", ntohs(udp->len));
+  printf("check = %x (mine %x)\n", udp->check, my_checksum);
+}
+
+/* print ipv4/udp header */
+void dump_udp(const struct udphdr *udp, const struct iphdr *ip, const uint8_t *payload,
+              size_t payload_size) {
+  uint32_t temp_checksum;
+  temp_checksum = ipv4_pseudo_header_checksum(ip, sizeof(*udp) + payload_size);
+  dump_udp_generic(udp, temp_checksum, payload, payload_size);
+}
+
+/* print ipv6/udp header */
+void dump_udp6(const struct udphdr *udp, const struct ip6_hdr *ip6, const uint8_t *payload,
+               size_t payload_size) {
+  uint32_t temp_checksum;
+  temp_checksum = ipv6_pseudo_header_checksum(ip6, sizeof(*udp) + payload_size, IPPROTO_UDP);
+  dump_udp_generic(udp, temp_checksum, payload, payload_size);
+}
+
+/* print tcp header */
+void dump_tcp_generic(const struct tcphdr *tcp, const uint8_t *options, size_t options_size,
+                      uint32_t temp_checksum, const uint8_t *payload, size_t payload_size) {
+  uint16_t my_checksum;
+
+  temp_checksum = ip_checksum_add(temp_checksum, tcp, sizeof(struct tcphdr));
+  if (options) {
+    temp_checksum = ip_checksum_add(temp_checksum, options, options_size);
+  }
+  temp_checksum = ip_checksum_add(temp_checksum, payload, payload_size);
+  my_checksum   = ip_checksum_finish(temp_checksum);
+
+  printf("TCP\n");
+  printf("source = %x\n", ntohs(tcp->source));
+  printf("dest = %x\n", ntohs(tcp->dest));
+  printf("seq = %x\n", ntohl(tcp->seq));
+  printf("ack = %x\n", ntohl(tcp->ack_seq));
+  printf("d_off = %x\n", tcp->doff);
+  printf("res1 = %x\n", tcp->res1);
+#ifdef __BIONIC__
+  printf("CWR = %x\n", tcp->cwr);
+  printf("ECE = %x\n", tcp->ece);
+#else
+  printf("CWR/ECE = %x\n", tcp->res2);
+#endif
+  printf("urg = %x  ack = %x  psh = %x  rst = %x  syn = %x  fin = %x\n", tcp->urg, tcp->ack,
+         tcp->psh, tcp->rst, tcp->syn, tcp->fin);
+  printf("window = %x\n", ntohs(tcp->window));
+  printf("check = %x [mine %x]\n", tcp->check, my_checksum);
+  printf("urgent = %x\n", tcp->urg_ptr);
+
+  if (options) {
+    size_t i;
+
+    printf("options: ");
+    for (i = 0; i < options_size; i++) {
+      printf("%x ", *(options + i));
+    }
+    printf("\n");
+  }
+}
+
+/* print ipv4/tcp header */
+void dump_tcp(const struct tcphdr *tcp, const struct iphdr *ip, const uint8_t *payload,
+              size_t payload_size, const uint8_t *options, size_t options_size) {
+  uint32_t temp_checksum;
+
+  temp_checksum = ipv4_pseudo_header_checksum(ip, sizeof(*tcp) + options_size + payload_size);
+  dump_tcp_generic(tcp, options, options_size, temp_checksum, payload, payload_size);
+}
+
+/* print ipv6/tcp header */
+void dump_tcp6(const struct tcphdr *tcp, const struct ip6_hdr *ip6, const uint8_t *payload,
+               size_t payload_size, const uint8_t *options, size_t options_size) {
+  uint32_t temp_checksum;
+
+  temp_checksum =
+    ipv6_pseudo_header_checksum(ip6, sizeof(*tcp) + options_size + payload_size, IPPROTO_TCP);
+  dump_tcp_generic(tcp, options, options_size, temp_checksum, payload, payload_size);
+}
+
+/* generic hex dump */
+void logcat_hexdump(const char *info, const uint8_t *data, size_t len) {
+  char output[MAXDUMPLEN * 3 + 2];
+  size_t i;
+
+  output[0] = '\0';
+  for (i = 0; i < len && i < MAXDUMPLEN; i++) {
+    snprintf(output + i * 3, 4, " %02x", data[i]);
+  }
+  output[len * 3 + 3] = '\0';
+
+  logmsg(ANDROID_LOG_WARN, "info %s len %d data%s", info, len, output);
+}
+
+void dump_iovec(const struct iovec *iov, int iov_len) {
+  int i;
+  char *str;
+  for (i = 0; i < iov_len; i++) {
+    asprintf(&str, "iov[%d]: ", i);
+    logcat_hexdump(str, iov[i].iov_base, iov[i].iov_len);
+    free(str);
+  }
+}
+#endif  // CLAT_DEBUG
diff --git a/clatd/dump.h b/clatd/dump.h
new file mode 100644
index 0000000..6b96cd2
--- /dev/null
+++ b/clatd/dump.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * dump.h - debug functions
+ */
+#ifndef __DUMP_H__
+#define __DUMP_H__
+
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
+
+void dump_ip(struct iphdr *header);
+void dump_icmp(struct icmphdr *icmp);
+void dump_udp(const struct udphdr *udp, const struct iphdr *ip, const uint8_t *payload,
+              size_t payload_size);
+void dump_tcp(const struct tcphdr *tcp, const struct iphdr *ip, const uint8_t *payload,
+              size_t payload_size, const uint8_t *options, size_t options_size);
+
+void dump_ip6(struct ip6_hdr *header);
+void dump_icmp6(struct icmp6_hdr *icmp6);
+void dump_udp6(const struct udphdr *udp, const struct ip6_hdr *ip6, const uint8_t *payload,
+               size_t payload_size);
+void dump_tcp6(const struct tcphdr *tcp, const struct ip6_hdr *ip6, const uint8_t *payload,
+               size_t payload_size, const uint8_t *options, size_t options_size);
+
+void logcat_hexdump(const char *info, const uint8_t *data, size_t len);
+void dump_iovec(const struct iovec *iov, int iov_len);
+
+#endif /* __DUMP_H__ */
diff --git a/clatd/icmp.c b/clatd/icmp.c
new file mode 100644
index 0000000..f9ba113
--- /dev/null
+++ b/clatd/icmp.c
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * icmp.c - convenience functions for translating ICMP and ICMPv6 packets.
+ */
+
+#include <linux/icmp.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip_icmp.h>
+
+#include "icmp.h"
+#include "logging.h"
+
+/* function: icmp_guess_ttl
+ * Guesses the number of hops a received packet has traversed based on its TTL.
+ * ttl - the ttl of the received packet.
+ */
+uint8_t icmp_guess_ttl(uint8_t ttl) {
+  if (ttl > 128) {
+    return 255 - ttl;
+  } else if (ttl > 64) {
+    return 128 - ttl;
+  } else if (ttl > 32) {
+    return 64 - ttl;
+  } else {
+    return 32 - ttl;
+  }
+}
+
+/* function: is_icmp_error
+ * Determines whether an ICMP type is an error message.
+ * type: the ICMP type
+ */
+int is_icmp_error(uint8_t type) { return type == 3 || type == 11 || type == 12; }
+
+/* function: is_icmp6_error
+ * Determines whether an ICMPv6 type is an error message.
+ * type: the ICMPv6 type
+ */
+int is_icmp6_error(uint8_t type) { return type < 128; }
+
+/* function: icmp_to_icmp6_type
+ * Maps ICMP types to ICMPv6 types. Partial implementation of RFC 6145, section 4.2.
+ * type - the ICMPv6 type
+ */
+uint8_t icmp_to_icmp6_type(uint8_t type, uint8_t code) {
+  switch (type) {
+    case ICMP_ECHO:
+      return ICMP6_ECHO_REQUEST;
+
+    case ICMP_ECHOREPLY:
+      return ICMP6_ECHO_REPLY;
+
+    case ICMP_TIME_EXCEEDED:
+      return ICMP6_TIME_EXCEEDED;
+
+    case ICMP_DEST_UNREACH:
+      // These two types need special translation which we don't support yet.
+      if (code != ICMP_UNREACH_PROTOCOL && code != ICMP_UNREACH_NEEDFRAG) {
+        return ICMP6_DST_UNREACH;
+      }
+  }
+
+  // We don't understand this ICMP type. Return parameter problem so the caller will bail out.
+  logmsg_dbg(ANDROID_LOG_DEBUG, "icmp_to_icmp6_type: unhandled ICMP type %d", type);
+  return ICMP6_PARAM_PROB;
+}
+
+/* function: icmp_to_icmp6_code
+ * Maps ICMP codes to ICMPv6 codes. Partial implementation of RFC 6145, section 4.2.
+ * type - the ICMP type
+ * code - the ICMP code
+ */
+uint8_t icmp_to_icmp6_code(uint8_t type, uint8_t code) {
+  switch (type) {
+    case ICMP_ECHO:
+    case ICMP_ECHOREPLY:
+      return 0;
+
+    case ICMP_TIME_EXCEEDED:
+      return code;
+
+    case ICMP_DEST_UNREACH:
+      switch (code) {
+        case ICMP_UNREACH_NET:
+        case ICMP_UNREACH_HOST:
+          return ICMP6_DST_UNREACH_NOROUTE;
+
+        case ICMP_UNREACH_PORT:
+          return ICMP6_DST_UNREACH_NOPORT;
+
+        case ICMP_UNREACH_NET_PROHIB:
+        case ICMP_UNREACH_HOST_PROHIB:
+        case ICMP_UNREACH_FILTER_PROHIB:
+        case ICMP_UNREACH_PRECEDENCE_CUTOFF:
+          return ICMP6_DST_UNREACH_ADMIN;
+
+          // Otherwise, we don't understand this ICMP type/code combination. Fall through.
+      }
+  }
+  logmsg_dbg(ANDROID_LOG_DEBUG, "icmp_to_icmp6_code: unhandled ICMP type/code %d/%d", type, code);
+  return 0;
+}
+
+/* function: icmp6_to_icmp_type
+ * Maps ICMPv6 types to ICMP types. Partial implementation of RFC 6145, section 5.2.
+ * type - the ICMP type
+ */
+uint8_t icmp6_to_icmp_type(uint8_t type, uint8_t code) {
+  switch (type) {
+    case ICMP6_ECHO_REQUEST:
+      return ICMP_ECHO;
+
+    case ICMP6_ECHO_REPLY:
+      return ICMP_ECHOREPLY;
+
+    case ICMP6_DST_UNREACH:
+      return ICMP_DEST_UNREACH;
+
+    case ICMP6_TIME_EXCEEDED:
+      return ICMP_TIME_EXCEEDED;
+  }
+
+  // We don't understand this ICMP type. Return parameter problem so the caller will bail out.
+  logmsg_dbg(ANDROID_LOG_DEBUG, "icmp6_to_icmp_type: unhandled ICMP type/code %d/%d", type, code);
+  return ICMP_PARAMETERPROB;
+}
+
+/* function: icmp6_to_icmp_code
+ * Maps ICMPv6 codes to ICMP codes. Partial implementation of RFC 6145, section 5.2.
+ * type - the ICMPv6 type
+ * code - the ICMPv6 code
+ */
+uint8_t icmp6_to_icmp_code(uint8_t type, uint8_t code) {
+  switch (type) {
+    case ICMP6_ECHO_REQUEST:
+    case ICMP6_ECHO_REPLY:
+    case ICMP6_TIME_EXCEEDED:
+      return code;
+
+    case ICMP6_DST_UNREACH:
+      switch (code) {
+        case ICMP6_DST_UNREACH_NOROUTE:
+          return ICMP_UNREACH_HOST;
+
+        case ICMP6_DST_UNREACH_ADMIN:
+          return ICMP_UNREACH_HOST_PROHIB;
+
+        case ICMP6_DST_UNREACH_BEYONDSCOPE:
+          return ICMP_UNREACH_HOST;
+
+        case ICMP6_DST_UNREACH_ADDR:
+          return ICMP_HOST_UNREACH;
+
+        case ICMP6_DST_UNREACH_NOPORT:
+          return ICMP_UNREACH_PORT;
+
+          // Otherwise, we don't understand this ICMPv6 type/code combination. Fall through.
+      }
+  }
+
+  logmsg_dbg(ANDROID_LOG_DEBUG, "icmp6_to_icmp_code: unhandled ICMP type/code %d/%d", type, code);
+  return 0;
+}
diff --git a/clatd/icmp.h b/clatd/icmp.h
new file mode 100644
index 0000000..632e92d
--- /dev/null
+++ b/clatd/icmp.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * icmp.c - convenience functions for translating ICMP and ICMPv6 packets.
+ */
+
+#ifndef __ICMP_H__
+#define __ICMP_H__
+
+#include <stdint.h>
+
+// Guesses the number of hops a received packet has traversed based on its TTL.
+uint8_t icmp_guess_ttl(uint8_t ttl);
+
+// Determines whether an ICMP type is an error message.
+int is_icmp_error(uint8_t type);
+
+// Determines whether an ICMPv6 type is an error message.
+int is_icmp6_error(uint8_t type);
+
+// Maps ICMP types to ICMPv6 types. Partial implementation of RFC 6145, section 4.2.
+uint8_t icmp_to_icmp6_type(uint8_t type, uint8_t code);
+
+// Maps ICMP codes to ICMPv6 codes. Partial implementation of RFC 6145, section 4.2.
+uint8_t icmp_to_icmp6_code(uint8_t type, uint8_t code);
+
+// Maps ICMPv6 types to ICMP types. Partial implementation of RFC 6145, section 5.2.
+uint8_t icmp6_to_icmp_type(uint8_t type, uint8_t code);
+
+// Maps ICMPv6 codes to ICMP codes. Partial implementation of RFC 6145, section 5.2.
+uint8_t icmp6_to_icmp_code(uint8_t type, uint8_t code);
+
+#endif /* __ICMP_H__ */
diff --git a/clatd/ipv4.c b/clatd/ipv4.c
new file mode 100644
index 0000000..2be02e3
--- /dev/null
+++ b/clatd/ipv4.c
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * ipv4.c - takes ipv4 packets, finds their headers, and then calls translation functions on them
+ */
+#include <string.h>
+
+#include "checksum.h"
+#include "debug.h"
+#include "dump.h"
+#include "logging.h"
+#include "translate.h"
+
+/* function: icmp_packet
+ * translates an icmp packet
+ * out      - output packet
+ * icmp     - pointer to icmp header in packet
+ * checksum - pseudo-header checksum
+ * len      - size of ip payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int icmp_packet(clat_packet out, clat_packet_index pos, const struct icmphdr *icmp,
+                uint32_t checksum, size_t len) {
+  const uint8_t *payload;
+  size_t payload_size;
+
+  if (len < sizeof(struct icmphdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "icmp_packet/(too small)");
+    return 0;
+  }
+
+  payload      = (const uint8_t *)(icmp + 1);
+  payload_size = len - sizeof(struct icmphdr);
+
+  return icmp_to_icmp6(out, pos, icmp, checksum, payload, payload_size);
+}
+
+/* function: ipv4_packet
+ * translates an ipv4 packet
+ * out    - output packet
+ * packet - packet data
+ * len    - size of packet
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int ipv4_packet(clat_packet out, clat_packet_index pos, const uint8_t *packet, size_t len) {
+  const struct iphdr *header = (struct iphdr *)packet;
+  struct ip6_hdr *ip6_targ   = (struct ip6_hdr *)out[pos].iov_base;
+  struct ip6_frag *frag_hdr;
+  size_t frag_hdr_len;
+  uint8_t nxthdr;
+  const uint8_t *next_header;
+  size_t len_left;
+  uint32_t old_sum, new_sum;
+  int iov_len;
+
+  if (len < sizeof(struct iphdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "ip_packet/too short for an ip header");
+    return 0;
+  }
+
+  if (header->ihl < 5) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "ip_packet/ip header length set to less than 5: %x", header->ihl);
+    return 0;
+  }
+
+  if ((size_t)header->ihl * 4 > len) {  // ip header length larger than entire packet
+    logmsg_dbg(ANDROID_LOG_ERROR, "ip_packet/ip header length set too large: %x", header->ihl);
+    return 0;
+  }
+
+  if (header->version != 4) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "ip_packet/ip header version not 4: %x", header->version);
+    return 0;
+  }
+
+  /* rfc6145 - If any IPv4 options are present in the IPv4 packet, they MUST be
+   * ignored and the packet translated normally; there is no attempt to
+   * translate the options.
+   */
+
+  next_header = packet + header->ihl * 4;
+  len_left    = len - header->ihl * 4;
+
+  nxthdr = header->protocol;
+  if (nxthdr == IPPROTO_ICMP) {
+    // ICMP and ICMPv6 have different protocol numbers.
+    nxthdr = IPPROTO_ICMPV6;
+  }
+
+  /* Fill in the IPv6 header. We need to do this before we translate the packet because TCP and
+   * UDP include parts of the IP header in the checksum. Set the length to zero because we don't
+   * know it yet.
+   */
+  fill_ip6_header(ip6_targ, 0, nxthdr, header);
+  out[pos].iov_len = sizeof(struct ip6_hdr);
+
+  /* Calculate the pseudo-header checksum.
+   * Technically, the length that is used in the pseudo-header checksum is the transport layer
+   * length, which is not the same as len_left in the case of fragmented packets. But since
+   * translation does not change the transport layer length, the checksum is unaffected.
+   */
+  old_sum = ipv4_pseudo_header_checksum(header, len_left);
+  new_sum = ipv6_pseudo_header_checksum(ip6_targ, len_left, nxthdr);
+
+  // If the IPv4 packet is fragmented, add a Fragment header.
+  frag_hdr             = (struct ip6_frag *)out[pos + 1].iov_base;
+  frag_hdr_len         = maybe_fill_frag_header(frag_hdr, ip6_targ, header);
+  out[pos + 1].iov_len = frag_hdr_len;
+
+  if (frag_hdr_len && frag_hdr->ip6f_offlg & IP6F_OFF_MASK) {
+    // Non-first fragment. Copy the rest of the packet as is.
+    iov_len = generic_packet(out, pos + 2, next_header, len_left);
+  } else if (nxthdr == IPPROTO_ICMPV6) {
+    iov_len = icmp_packet(out, pos + 2, (const struct icmphdr *)next_header, new_sum, len_left);
+  } else if (nxthdr == IPPROTO_TCP) {
+    iov_len =
+      tcp_packet(out, pos + 2, (const struct tcphdr *)next_header, old_sum, new_sum, len_left);
+  } else if (nxthdr == IPPROTO_UDP) {
+    iov_len =
+      udp_packet(out, pos + 2, (const struct udphdr *)next_header, old_sum, new_sum, len_left);
+  } else if (nxthdr == IPPROTO_GRE || nxthdr == IPPROTO_ESP) {
+    iov_len = generic_packet(out, pos + 2, next_header, len_left);
+  } else {
+#if CLAT_DEBUG
+    logmsg_dbg(ANDROID_LOG_ERROR, "ip_packet/unknown protocol: %x", header->protocol);
+    logcat_hexdump("ipv4/protocol", packet, len);
+#endif
+    return 0;
+  }
+
+  // Set the length.
+  ip6_targ->ip6_plen = htons(packet_length(out, pos));
+  return iov_len;
+}
diff --git a/clatd/ipv6.c b/clatd/ipv6.c
new file mode 100644
index 0000000..05cd3ab
--- /dev/null
+++ b/clatd/ipv6.c
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * ipv6.c - takes ipv6 packets, finds their headers, and then calls translation functions on them
+ */
+#include <arpa/inet.h>
+#include <string.h>
+
+#include "checksum.h"
+#include "config.h"
+#include "debug.h"
+#include "dump.h"
+#include "logging.h"
+#include "translate.h"
+
+/* function: icmp6_packet
+ * takes an icmp6 packet and sets it up for translation
+ * out      - output packet
+ * icmp6    - pointer to icmp6 header in packet
+ * checksum - pseudo-header checksum (unused)
+ * len      - size of ip payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int icmp6_packet(clat_packet out, clat_packet_index pos, const struct icmp6_hdr *icmp6,
+                 size_t len) {
+  const uint8_t *payload;
+  size_t payload_size;
+
+  if (len < sizeof(struct icmp6_hdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "icmp6_packet/(too small)");
+    return 0;
+  }
+
+  payload      = (const uint8_t *)(icmp6 + 1);
+  payload_size = len - sizeof(struct icmp6_hdr);
+
+  return icmp6_to_icmp(out, pos, icmp6, payload, payload_size);
+}
+
+/* function: log_bad_address
+ * logs a bad address to android's log buffer if debugging is turned on
+ * fmt     - printf-style format, use %s to place the address
+ * badaddr - the bad address in question
+ */
+#if CLAT_DEBUG
+void log_bad_address(const char *fmt, const struct in6_addr *src, const struct in6_addr *dst) {
+  char srcstr[INET6_ADDRSTRLEN];
+  char dststr[INET6_ADDRSTRLEN];
+
+  inet_ntop(AF_INET6, src, srcstr, sizeof(srcstr));
+  inet_ntop(AF_INET6, dst, dststr, sizeof(dststr));
+  logmsg_dbg(ANDROID_LOG_ERROR, fmt, srcstr, dststr);
+}
+#else
+#define log_bad_address(fmt, src, dst)
+#endif
+
+/* function: ipv6_packet
+ * takes an ipv6 packet and hands it off to the layer 4 protocol function
+ * out    - output packet
+ * packet - packet data
+ * len    - size of packet
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int ipv6_packet(clat_packet out, clat_packet_index pos, const uint8_t *packet, size_t len) {
+  const struct ip6_hdr *ip6 = (struct ip6_hdr *)packet;
+  struct iphdr *ip_targ     = (struct iphdr *)out[pos].iov_base;
+  struct ip6_frag *frag_hdr = NULL;
+  uint8_t protocol;
+  const uint8_t *next_header;
+  size_t len_left;
+  uint32_t old_sum, new_sum;
+  int iov_len;
+
+  if (len < sizeof(struct ip6_hdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "ipv6_packet/too short for an ip6 header: %d", len);
+    return 0;
+  }
+
+  if (IN6_IS_ADDR_MULTICAST(&ip6->ip6_dst)) {
+    log_bad_address("ipv6_packet/multicast %s->%s", &ip6->ip6_src, &ip6->ip6_dst);
+    return 0;  // silently ignore
+  }
+
+  // If the packet is not from the plat subnet to the local subnet, or vice versa, drop it, unless
+  // it's an ICMP packet (which can come from anywhere). We do not send IPv6 packets from the plat
+  // subnet to the local subnet, but these can appear as inner packets in ICMP errors, so we need
+  // to translate them. We accept third-party ICMPv6 errors, even though their source addresses
+  // cannot be translated, so that things like unreachables and traceroute will work. fill_ip_header
+  // takes care of faking a source address for them.
+  if (!(is_in_plat_subnet(&ip6->ip6_src) &&
+        IN6_ARE_ADDR_EQUAL(&ip6->ip6_dst, &Global_Clatd_Config.ipv6_local_subnet)) &&
+      !(is_in_plat_subnet(&ip6->ip6_dst) &&
+        IN6_ARE_ADDR_EQUAL(&ip6->ip6_src, &Global_Clatd_Config.ipv6_local_subnet)) &&
+      ip6->ip6_nxt != IPPROTO_ICMPV6) {
+    log_bad_address("ipv6_packet/wrong source address: %s->%s", &ip6->ip6_src, &ip6->ip6_dst);
+    return 0;
+  }
+
+  next_header = packet + sizeof(struct ip6_hdr);
+  len_left    = len - sizeof(struct ip6_hdr);
+
+  protocol = ip6->ip6_nxt;
+
+  /* Fill in the IPv4 header. We need to do this before we translate the packet because TCP and
+   * UDP include parts of the IP header in the checksum. Set the length to zero because we don't
+   * know it yet.
+   */
+  fill_ip_header(ip_targ, 0, protocol, ip6);
+  out[pos].iov_len = sizeof(struct iphdr);
+
+  // If there's a Fragment header, parse it and decide what the next header is.
+  // Do this before calculating the pseudo-header checksum because it updates the next header value.
+  if (protocol == IPPROTO_FRAGMENT) {
+    frag_hdr = (struct ip6_frag *)next_header;
+    if (len_left < sizeof(*frag_hdr)) {
+      logmsg_dbg(ANDROID_LOG_ERROR, "ipv6_packet/too short for fragment header: %d", len);
+      return 0;
+    }
+
+    next_header += sizeof(*frag_hdr);
+    len_left -= sizeof(*frag_hdr);
+
+    protocol = parse_frag_header(frag_hdr, ip_targ);
+  }
+
+  // ICMP and ICMPv6 have different protocol numbers.
+  if (protocol == IPPROTO_ICMPV6) {
+    protocol          = IPPROTO_ICMP;
+    ip_targ->protocol = IPPROTO_ICMP;
+  }
+
+  /* Calculate the pseudo-header checksum.
+   * Technically, the length that is used in the pseudo-header checksum is the transport layer
+   * length, which is not the same as len_left in the case of fragmented packets. But since
+   * translation does not change the transport layer length, the checksum is unaffected.
+   */
+  old_sum = ipv6_pseudo_header_checksum(ip6, len_left, protocol);
+  new_sum = ipv4_pseudo_header_checksum(ip_targ, len_left);
+
+  // Does not support IPv6 extension headers except Fragment.
+  if (frag_hdr && (frag_hdr->ip6f_offlg & IP6F_OFF_MASK)) {
+    iov_len = generic_packet(out, pos + 2, next_header, len_left);
+  } else if (protocol == IPPROTO_ICMP) {
+    iov_len = icmp6_packet(out, pos + 2, (const struct icmp6_hdr *)next_header, len_left);
+  } else if (protocol == IPPROTO_TCP) {
+    iov_len =
+      tcp_packet(out, pos + 2, (const struct tcphdr *)next_header, old_sum, new_sum, len_left);
+  } else if (protocol == IPPROTO_UDP) {
+    iov_len =
+      udp_packet(out, pos + 2, (const struct udphdr *)next_header, old_sum, new_sum, len_left);
+  } else if (protocol == IPPROTO_GRE || protocol == IPPROTO_ESP) {
+    iov_len = generic_packet(out, pos + 2, next_header, len_left);
+  } else {
+#if CLAT_DEBUG
+    logmsg(ANDROID_LOG_ERROR, "ipv6_packet/unknown next header type: %x", ip6->ip6_nxt);
+    logcat_hexdump("ipv6/nxthdr", packet, len);
+#endif
+    return 0;
+  }
+
+  // Set the length and calculate the checksum.
+  ip_targ->tot_len = htons(ntohs(ip_targ->tot_len) + packet_length(out, pos));
+  ip_targ->check   = ip_checksum(ip_targ, sizeof(struct iphdr));
+  return iov_len;
+}
diff --git a/clatd/logging.c b/clatd/logging.c
new file mode 100644
index 0000000..79d98e1
--- /dev/null
+++ b/clatd/logging.c
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * logging.c - print a log message
+ */
+
+#include <android/log.h>
+#include <stdarg.h>
+
+#include "debug.h"
+#include "logging.h"
+
+/* function: logmsg
+ * prints a log message to android's log buffer
+ * prio - the log message priority
+ * fmt  - printf format specifier
+ * ...  - printf format arguments
+ */
+void logmsg(int prio, const char *fmt, ...) {
+  va_list ap;
+
+  va_start(ap, fmt);
+  __android_log_vprint(prio, "clatd", fmt, ap);
+  va_end(ap);
+}
+
+/* function: logmsg_dbg
+ * prints a log message to android's log buffer if CLAT_DEBUG is set
+ * prio - the log message priority
+ * fmt  - printf format specifier
+ * ...  - printf format arguments
+ */
+#if CLAT_DEBUG
+void logmsg_dbg(int prio, const char *fmt, ...) {
+  va_list ap;
+
+  va_start(ap, fmt);
+  __android_log_vprint(prio, "clatd", fmt, ap);
+  va_end(ap);
+}
+#else
+void logmsg_dbg(__attribute__((unused)) int prio, __attribute__((unused)) const char *fmt, ...) {}
+#endif
diff --git a/clatd/logging.h b/clatd/logging.h
new file mode 100644
index 0000000..1f4b6b6
--- /dev/null
+++ b/clatd/logging.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * logging.h - print a log message
+ */
+
+#ifndef __LOGGING_H__
+#define __LOGGING_H__
+// for the priorities
+#include <android/log.h>
+
+void logmsg(int prio, const char *fmt, ...);
+void logmsg_dbg(int prio, const char *fmt, ...);
+
+#endif
diff --git a/clatd/main.c b/clatd/main.c
new file mode 100644
index 0000000..f888041
--- /dev/null
+++ b/clatd/main.c
@@ -0,0 +1,207 @@
+/*
+ * Copyright 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.
+ *
+ * main.c - main function
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/personality.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+
+#include "clatd.h"
+#include "common.h"
+#include "config.h"
+#include "logging.h"
+
+#define DEVICEPREFIX "v4-"
+
+/* function: stop_loop
+ * signal handler: stop the event loop
+ */
+static void stop_loop() { running = 0; };
+
+/* function: print_help
+ * in case the user is running this on the command line
+ */
+void print_help() {
+  printf("android-clat arguments:\n");
+  printf("-i [uplink interface]\n");
+  printf("-p [plat prefix]\n");
+  printf("-4 [IPv4 address]\n");
+  printf("-6 [IPv6 address]\n");
+  printf("-t [tun file descriptor number]\n");
+  printf("-r [read socket descriptor number]\n");
+  printf("-w [write socket descriptor number]\n");
+}
+
+/* function: main
+ * allocate and setup the tun device, then run the event loop
+ */
+int main(int argc, char **argv) {
+  struct tun_data tunnel;
+  int opt;
+  char *uplink_interface = NULL, *plat_prefix = NULL;
+  char *v4_addr = NULL, *v6_addr = NULL, *tunfd_str = NULL, *read_sock_str = NULL,
+       *write_sock_str = NULL;
+  unsigned len;
+
+  while ((opt = getopt(argc, argv, "i:p:4:6:t:r:w:h")) != -1) {
+    switch (opt) {
+      case 'i':
+        uplink_interface = optarg;
+        break;
+      case 'p':
+        plat_prefix = optarg;
+        break;
+      case '4':
+        v4_addr = optarg;
+        break;
+      case '6':
+        v6_addr = optarg;
+        break;
+      case 't':
+        tunfd_str = optarg;
+        break;
+      case 'r':
+        read_sock_str = optarg;
+        break;
+      case 'w':
+        write_sock_str = optarg;
+        break;
+      case 'h':
+        print_help();
+        exit(0);
+      default:
+        logmsg(ANDROID_LOG_FATAL, "Unknown option -%c. Exiting.", (char)optopt);
+        exit(1);
+    }
+  }
+
+  if (uplink_interface == NULL) {
+    logmsg(ANDROID_LOG_FATAL, "clatd called without an interface");
+    exit(1);
+  }
+
+  if (tunfd_str != NULL && !parse_int(tunfd_str, &tunnel.fd4)) {
+    logmsg(ANDROID_LOG_FATAL, "invalid tunfd %s", tunfd_str);
+    exit(1);
+  }
+  if (!tunnel.fd4) {
+    logmsg(ANDROID_LOG_FATAL, "no tunfd specified on commandline.");
+    exit(1);
+  }
+
+  if (read_sock_str != NULL && !parse_int(read_sock_str, &tunnel.read_fd6)) {
+    logmsg(ANDROID_LOG_FATAL, "invalid read socket %s", read_sock_str);
+    exit(1);
+  }
+  if (!tunnel.read_fd6) {
+    logmsg(ANDROID_LOG_FATAL, "no read_fd6 specified on commandline.");
+    exit(1);
+  }
+
+  if (write_sock_str != NULL && !parse_int(write_sock_str, &tunnel.write_fd6)) {
+    logmsg(ANDROID_LOG_FATAL, "invalid write socket %s", write_sock_str);
+    exit(1);
+  }
+  if (!tunnel.write_fd6) {
+    logmsg(ANDROID_LOG_FATAL, "no write_fd6 specified on commandline.");
+    exit(1);
+  }
+
+  len = snprintf(tunnel.device4, sizeof(tunnel.device4), "%s%s", DEVICEPREFIX, uplink_interface);
+  if (len >= sizeof(tunnel.device4)) {
+    logmsg(ANDROID_LOG_FATAL, "interface name too long '%s'", tunnel.device4);
+    exit(1);
+  }
+
+  Global_Clatd_Config.native_ipv6_interface = uplink_interface;
+  if (!plat_prefix || inet_pton(AF_INET6, plat_prefix, &Global_Clatd_Config.plat_subnet) <= 0) {
+    logmsg(ANDROID_LOG_FATAL, "invalid IPv6 address specified for plat prefix: %s", plat_prefix);
+    exit(1);
+  }
+
+  if (!v4_addr || !inet_pton(AF_INET, v4_addr, &Global_Clatd_Config.ipv4_local_subnet.s_addr)) {
+    logmsg(ANDROID_LOG_FATAL, "Invalid IPv4 address %s", v4_addr);
+    exit(1);
+  }
+
+  if (!v6_addr || !inet_pton(AF_INET6, v6_addr, &Global_Clatd_Config.ipv6_local_subnet)) {
+    logmsg(ANDROID_LOG_FATAL, "Invalid source address %s", v6_addr);
+    exit(1);
+  }
+
+  logmsg(ANDROID_LOG_INFO, "Starting clat version %s on %s plat=%s v4=%s v6=%s", CLATD_VERSION,
+         uplink_interface, plat_prefix ? plat_prefix : "(none)", v4_addr ? v4_addr : "(none)",
+         v6_addr ? v6_addr : "(none)");
+
+  {
+    // Compile time detection of 32 vs 64-bit build. (note: C does not have 'constexpr')
+    // Avoid use of preprocessor macros to get compile time syntax checking even on 64-bit.
+    const int user_bits = sizeof(void*) * 8;
+    const bool user32 = (user_bits == 32);
+
+    // Note that on 64-bit all this personality related code simply compile optimizes out.
+    // 32-bit: fetch current personality (see 'man personality': 0xFFFFFFFF means retrieve only)
+    // On Linux fetching personality cannot fail.
+    const int prev_personality = user32 ? personality(0xFFFFFFFFuL) : PER_LINUX;
+    // 32-bit: attempt to get rid of kernel spoofing of 'uts.machine' architecture,
+    // In theory this cannot fail, as PER_LINUX should always be supported.
+    if (user32) (void)personality((prev_personality & ~PER_MASK) | PER_LINUX);
+    // 64-bit: this will compile time evaluate to false.
+    const bool was_linux32 = (prev_personality & PER_MASK) == PER_LINUX32;
+
+    struct utsname uts = {};
+    if (uname(&uts)) exit(1); // only possible error is EFAULT, but 'uts' is on stack
+
+    // sysname is likely 'Linux', release is 'kver', machine is kernel's *true* architecture
+    logmsg(ANDROID_LOG_INFO, "%d-bit userspace on %s kernel %s for %s%s.", user_bits,
+           uts.sysname, uts.release, uts.machine, was_linux32 ? " (was spoofed)" : "");
+
+    // 32-bit: try to return to the 'default' personality
+    // In theory this cannot fail, because it was already previously in use.
+    if (user32) (void)personality(prev_personality);
+  }
+
+  // Loop until someone sends us a signal or brings down the tun interface.
+  if (signal(SIGTERM, stop_loop) == SIG_ERR) {
+    logmsg(ANDROID_LOG_FATAL, "sigterm handler failed: %s", strerror(errno));
+    exit(1);
+  }
+
+  event_loop(&tunnel);
+
+  logmsg(ANDROID_LOG_INFO, "Shutting down clat on %s", uplink_interface);
+
+  if (running) {
+    logmsg(ANDROID_LOG_INFO, "Clatd on %s waiting for SIGTERM", uplink_interface);
+    // let's give higher level java code 15 seconds to kill us,
+    // but eventually terminate anyway, in case system server forgets about us...
+    // sleep() should be interrupted by SIGTERM, the handler should clear running
+    sleep(15);
+    logmsg(ANDROID_LOG_INFO, "Clatd on %s %s SIGTERM", uplink_interface,
+           running ? "timed out waiting for" : "received");
+  } else {
+    logmsg(ANDROID_LOG_INFO, "Clatd on %s already received SIGTERM", uplink_interface);
+  }
+  return 0;
+}
diff --git a/clatd/translate.c b/clatd/translate.c
new file mode 100644
index 0000000..22830d89
--- /dev/null
+++ b/clatd/translate.c
@@ -0,0 +1,529 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * translate.c - CLAT functions / partial implementation of rfc6145
+ */
+#include "translate.h"
+
+#include <string.h>
+
+#include "checksum.h"
+#include "clatd.h"
+#include "common.h"
+#include "config.h"
+#include "debug.h"
+#include "icmp.h"
+#include "logging.h"
+
+/* function: packet_checksum
+ * calculates the checksum over all the packet components starting from pos
+ * checksum - checksum of packet components before pos
+ * packet   - packet to calculate the checksum of
+ * pos      - position to start counting from
+ * returns  - the completed 16-bit checksum, ready to write into a checksum header field
+ */
+uint16_t packet_checksum(uint32_t checksum, clat_packet packet, clat_packet_index pos) {
+  int i;
+  for (i = pos; i < CLAT_POS_MAX; i++) {
+    if (packet[i].iov_len > 0) {
+      checksum = ip_checksum_add(checksum, packet[i].iov_base, packet[i].iov_len);
+    }
+  }
+  return ip_checksum_finish(checksum);
+}
+
+/* function: packet_length
+ * returns the total length of all the packet components after pos
+ * packet - packet to calculate the length of
+ * pos    - position to start counting after
+ * returns: the total length of the packet components after pos
+ */
+uint16_t packet_length(clat_packet packet, clat_packet_index pos) {
+  size_t len = 0;
+  int i;
+  for (i = pos + 1; i < CLAT_POS_MAX; i++) {
+    len += packet[i].iov_len;
+  }
+  return len;
+}
+
+/* function: is_in_plat_subnet
+ * returns true iff the given IPv6 address is in the plat subnet.
+ * addr - IPv6 address
+ */
+int is_in_plat_subnet(const struct in6_addr *addr6) {
+  // Assumes a /96 plat subnet.
+  return (addr6 != NULL) && (memcmp(addr6, &Global_Clatd_Config.plat_subnet, 12) == 0);
+}
+
+/* function: ipv6_addr_to_ipv4_addr
+ * return the corresponding ipv4 address for the given ipv6 address
+ * addr6 - ipv6 address
+ * returns: the IPv4 address
+ */
+uint32_t ipv6_addr_to_ipv4_addr(const struct in6_addr *addr6) {
+  if (is_in_plat_subnet(addr6)) {
+    // Assumes a /96 plat subnet.
+    return addr6->s6_addr32[3];
+  } else if (IN6_ARE_ADDR_EQUAL(addr6, &Global_Clatd_Config.ipv6_local_subnet)) {
+    // Special-case our own address.
+    return Global_Clatd_Config.ipv4_local_subnet.s_addr;
+  } else {
+    // Third party packet. Let the caller deal with it.
+    return INADDR_NONE;
+  }
+}
+
+/* function: ipv4_addr_to_ipv6_addr
+ * return the corresponding ipv6 address for the given ipv4 address
+ * addr4 - ipv4 address
+ */
+struct in6_addr ipv4_addr_to_ipv6_addr(uint32_t addr4) {
+  struct in6_addr addr6;
+  // Both addresses are in network byte order (addr4 comes from a network packet, and the config
+  // file entry is read using inet_ntop).
+  if (addr4 == Global_Clatd_Config.ipv4_local_subnet.s_addr) {
+    return Global_Clatd_Config.ipv6_local_subnet;
+  } else {
+    // Assumes a /96 plat subnet.
+    addr6              = Global_Clatd_Config.plat_subnet;
+    addr6.s6_addr32[3] = addr4;
+    return addr6;
+  }
+}
+
+/* function: fill_tun_header
+ * fill in the header for the tun fd
+ * tun_header - tunnel header, already allocated
+ * proto      - ethernet protocol id: ETH_P_IP(ipv4) or ETH_P_IPV6(ipv6)
+ */
+void fill_tun_header(struct tun_pi *tun_header, uint16_t proto) {
+  tun_header->flags = 0;
+  tun_header->proto = htons(proto);
+}
+
+/* function: fill_ip_header
+ * generate an ipv4 header from an ipv6 header
+ * ip_targ     - (ipv4) target packet header, source: original ipv4 addr, dest: local subnet addr
+ * payload_len - length of other data inside packet
+ * protocol    - protocol number (tcp, udp, etc)
+ * old_header  - (ipv6) source packet header, source: nat64 prefix, dest: local subnet prefix
+ */
+void fill_ip_header(struct iphdr *ip, uint16_t payload_len, uint8_t protocol,
+                    const struct ip6_hdr *old_header) {
+  int ttl_guess;
+  memset(ip, 0, sizeof(struct iphdr));
+
+  ip->ihl      = 5;
+  ip->version  = 4;
+  ip->tos      = 0;
+  ip->tot_len  = htons(sizeof(struct iphdr) + payload_len);
+  ip->id       = 0;
+  ip->frag_off = htons(IP_DF);
+  ip->ttl      = old_header->ip6_hlim;
+  ip->protocol = protocol;
+  ip->check    = 0;
+
+  ip->saddr = ipv6_addr_to_ipv4_addr(&old_header->ip6_src);
+  ip->daddr = ipv6_addr_to_ipv4_addr(&old_header->ip6_dst);
+
+  // Third-party ICMPv6 message. This may have been originated by an native IPv6 address.
+  // In that case, the source IPv6 address can't be translated and we need to make up an IPv4
+  // source address. For now, use 255.0.0.<ttl>, which at least looks useful in traceroute.
+  if ((uint32_t)ip->saddr == INADDR_NONE) {
+    ttl_guess = icmp_guess_ttl(old_header->ip6_hlim);
+    ip->saddr = htonl((0xff << 24) + ttl_guess);
+  }
+}
+
+/* function: fill_ip6_header
+ * generate an ipv6 header from an ipv4 header
+ * ip6         - (ipv6) target packet header, source: local subnet prefix, dest: nat64 prefix
+ * payload_len - length of other data inside packet
+ * protocol    - protocol number (tcp, udp, etc)
+ * old_header  - (ipv4) source packet header, source: local subnet addr, dest: internet's ipv4 addr
+ */
+void fill_ip6_header(struct ip6_hdr *ip6, uint16_t payload_len, uint8_t protocol,
+                     const struct iphdr *old_header) {
+  memset(ip6, 0, sizeof(struct ip6_hdr));
+
+  ip6->ip6_vfc  = 6 << 4;
+  ip6->ip6_plen = htons(payload_len);
+  ip6->ip6_nxt  = protocol;
+  ip6->ip6_hlim = old_header->ttl;
+
+  ip6->ip6_src = ipv4_addr_to_ipv6_addr(old_header->saddr);
+  ip6->ip6_dst = ipv4_addr_to_ipv6_addr(old_header->daddr);
+}
+
+/* function: maybe_fill_frag_header
+ * fills a fragmentation header
+ * generate an ipv6 fragment header from an ipv4 header
+ * frag_hdr    - target (ipv6) fragmentation header
+ * ip6_targ    - target (ipv6) header
+ * old_header  - (ipv4) source packet header
+ * returns: the length of the fragmentation header if present, or zero if not present
+ */
+size_t maybe_fill_frag_header(struct ip6_frag *frag_hdr, struct ip6_hdr *ip6_targ,
+                              const struct iphdr *old_header) {
+  uint16_t frag_flags = ntohs(old_header->frag_off);
+  uint16_t frag_off   = frag_flags & IP_OFFMASK;
+  if (frag_off == 0 && (frag_flags & IP_MF) == 0) {
+    // Not a fragment.
+    return 0;
+  }
+
+  frag_hdr->ip6f_nxt      = ip6_targ->ip6_nxt;
+  frag_hdr->ip6f_reserved = 0;
+  // In IPv4, the offset is the bottom 13 bits; in IPv6 it's the top 13 bits.
+  frag_hdr->ip6f_offlg = htons(frag_off << 3);
+  if (frag_flags & IP_MF) {
+    frag_hdr->ip6f_offlg |= IP6F_MORE_FRAG;
+  }
+  frag_hdr->ip6f_ident = htonl(ntohs(old_header->id));
+  ip6_targ->ip6_nxt    = IPPROTO_FRAGMENT;
+
+  return sizeof(*frag_hdr);
+}
+
+/* function: parse_frag_header
+ * return the length of the fragmentation header if present, or zero if not present
+ * generate an ipv6 fragment header from an ipv4 header
+ * frag_hdr    - (ipv6) fragmentation header
+ * ip_targ     - target (ipv4) header
+ * returns: the next header value
+ */
+uint8_t parse_frag_header(const struct ip6_frag *frag_hdr, struct iphdr *ip_targ) {
+  uint16_t frag_off = (ntohs(frag_hdr->ip6f_offlg & IP6F_OFF_MASK) >> 3);
+  if (frag_hdr->ip6f_offlg & IP6F_MORE_FRAG) {
+    frag_off |= IP_MF;
+  }
+  ip_targ->frag_off = htons(frag_off);
+  ip_targ->id       = htons(ntohl(frag_hdr->ip6f_ident) & 0xffff);
+  ip_targ->protocol = frag_hdr->ip6f_nxt;
+  return frag_hdr->ip6f_nxt;
+}
+
+/* function: icmp_to_icmp6
+ * translate ipv4 icmp to ipv6 icmp
+ * out          - output packet
+ * icmp         - source packet icmp header
+ * checksum     - pseudo-header checksum
+ * payload      - icmp payload
+ * payload_size - size of payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int icmp_to_icmp6(clat_packet out, clat_packet_index pos, const struct icmphdr *icmp,
+                  uint32_t checksum, const uint8_t *payload, size_t payload_size) {
+  struct icmp6_hdr *icmp6_targ = out[pos].iov_base;
+  uint8_t icmp6_type;
+  int clat_packet_len;
+
+  memset(icmp6_targ, 0, sizeof(struct icmp6_hdr));
+
+  icmp6_type             = icmp_to_icmp6_type(icmp->type, icmp->code);
+  icmp6_targ->icmp6_type = icmp6_type;
+  icmp6_targ->icmp6_code = icmp_to_icmp6_code(icmp->type, icmp->code);
+
+  out[pos].iov_len = sizeof(struct icmp6_hdr);
+
+  if (pos == CLAT_POS_TRANSPORTHDR && is_icmp_error(icmp->type) && icmp6_type != ICMP6_PARAM_PROB) {
+    // An ICMP error we understand, one level deep.
+    // Translate the nested packet (the one that caused the error).
+    clat_packet_len = ipv4_packet(out, pos + 1, payload, payload_size);
+
+    // The pseudo-header checksum was calculated on the transport length of the original IPv4
+    // packet that we were asked to translate. This transport length is 20 bytes smaller than it
+    // needs to be, because the ICMP error contains an IPv4 header, which we will be translating to
+    // an IPv6 header, which is 20 bytes longer. Fix it up here.
+    // We only need to do this for ICMP->ICMPv6, not ICMPv6->ICMP, because ICMP does not use the
+    // pseudo-header when calculating its checksum (as the IPv4 header has its own checksum).
+    checksum = checksum + htons(20);
+  } else if (icmp6_type == ICMP6_ECHO_REQUEST || icmp6_type == ICMP6_ECHO_REPLY) {
+    // Ping packet.
+    icmp6_targ->icmp6_id           = icmp->un.echo.id;
+    icmp6_targ->icmp6_seq          = icmp->un.echo.sequence;
+    out[CLAT_POS_PAYLOAD].iov_base = (uint8_t *)payload;
+    out[CLAT_POS_PAYLOAD].iov_len  = payload_size;
+    clat_packet_len                = CLAT_POS_PAYLOAD + 1;
+  } else {
+    // Unknown type/code. The type/code conversion functions have already logged an error.
+    return 0;
+  }
+
+  icmp6_targ->icmp6_cksum = 0;  // Checksum field must be 0 when calculating checksum.
+  icmp6_targ->icmp6_cksum = packet_checksum(checksum, out, pos);
+
+  return clat_packet_len;
+}
+
+/* function: icmp6_to_icmp
+ * translate ipv6 icmp to ipv4 icmp
+ * out          - output packet
+ * icmp6        - source packet icmp6 header
+ * payload      - icmp6 payload
+ * payload_size - size of payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int icmp6_to_icmp(clat_packet out, clat_packet_index pos, const struct icmp6_hdr *icmp6,
+                  const uint8_t *payload, size_t payload_size) {
+  struct icmphdr *icmp_targ = out[pos].iov_base;
+  uint8_t icmp_type;
+  int clat_packet_len;
+
+  memset(icmp_targ, 0, sizeof(struct icmphdr));
+
+  icmp_type       = icmp6_to_icmp_type(icmp6->icmp6_type, icmp6->icmp6_code);
+  icmp_targ->type = icmp_type;
+  icmp_targ->code = icmp6_to_icmp_code(icmp6->icmp6_type, icmp6->icmp6_code);
+
+  out[pos].iov_len = sizeof(struct icmphdr);
+
+  if (pos == CLAT_POS_TRANSPORTHDR && is_icmp6_error(icmp6->icmp6_type) &&
+      icmp_type != ICMP_PARAMETERPROB) {
+    // An ICMPv6 error we understand, one level deep.
+    // Translate the nested packet (the one that caused the error).
+    clat_packet_len = ipv6_packet(out, pos + 1, payload, payload_size);
+  } else if (icmp_type == ICMP_ECHO || icmp_type == ICMP_ECHOREPLY) {
+    // Ping packet.
+    icmp_targ->un.echo.id          = icmp6->icmp6_id;
+    icmp_targ->un.echo.sequence    = icmp6->icmp6_seq;
+    out[CLAT_POS_PAYLOAD].iov_base = (uint8_t *)payload;
+    out[CLAT_POS_PAYLOAD].iov_len  = payload_size;
+    clat_packet_len                = CLAT_POS_PAYLOAD + 1;
+  } else {
+    // Unknown type/code. The type/code conversion functions have already logged an error.
+    return 0;
+  }
+
+  icmp_targ->checksum = 0;  // Checksum field must be 0 when calculating checksum.
+  icmp_targ->checksum = packet_checksum(0, out, pos);
+
+  return clat_packet_len;
+}
+
+/* function: generic_packet
+ * takes a generic IP packet and sets it up for translation
+ * out      - output packet
+ * pos      - position in the output packet of the transport header
+ * payload  - pointer to IP payload
+ * len      - size of ip payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int generic_packet(clat_packet out, clat_packet_index pos, const uint8_t *payload, size_t len) {
+  out[pos].iov_len               = 0;
+  out[CLAT_POS_PAYLOAD].iov_base = (uint8_t *)payload;
+  out[CLAT_POS_PAYLOAD].iov_len  = len;
+
+  return CLAT_POS_PAYLOAD + 1;
+}
+
+/* function: udp_packet
+ * takes a udp packet and sets it up for translation
+ * out      - output packet
+ * udp      - pointer to udp header in packet
+ * old_sum  - pseudo-header checksum of old header
+ * new_sum  - pseudo-header checksum of new header
+ * len      - size of ip payload
+ */
+int udp_packet(clat_packet out, clat_packet_index pos, const struct udphdr *udp, uint32_t old_sum,
+               uint32_t new_sum, size_t len) {
+  const uint8_t *payload;
+  size_t payload_size;
+
+  if (len < sizeof(struct udphdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "udp_packet/(too small)");
+    return 0;
+  }
+
+  payload      = (const uint8_t *)(udp + 1);
+  payload_size = len - sizeof(struct udphdr);
+
+  return udp_translate(out, pos, udp, old_sum, new_sum, payload, payload_size);
+}
+
+/* function: tcp_packet
+ * takes a tcp packet and sets it up for translation
+ * out      - output packet
+ * tcp      - pointer to tcp header in packet
+ * checksum - pseudo-header checksum
+ * len      - size of ip payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int tcp_packet(clat_packet out, clat_packet_index pos, const struct tcphdr *tcp, uint32_t old_sum,
+               uint32_t new_sum, size_t len) {
+  const uint8_t *payload;
+  size_t payload_size, header_size;
+
+  if (len < sizeof(struct tcphdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "tcp_packet/(too small)");
+    return 0;
+  }
+
+  if (tcp->doff < 5) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "tcp_packet/tcp header length set to less than 5: %x", tcp->doff);
+    return 0;
+  }
+
+  if ((size_t)tcp->doff * 4 > len) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "tcp_packet/tcp header length set too large: %x", tcp->doff);
+    return 0;
+  }
+
+  header_size  = tcp->doff * 4;
+  payload      = ((const uint8_t *)tcp) + header_size;
+  payload_size = len - header_size;
+
+  return tcp_translate(out, pos, tcp, header_size, old_sum, new_sum, payload, payload_size);
+}
+
+/* function: udp_translate
+ * common between ipv4/ipv6 - setup checksum and send udp packet
+ * out          - output packet
+ * udp          - udp header
+ * old_sum      - pseudo-header checksum of old header
+ * new_sum      - pseudo-header checksum of new header
+ * payload      - tcp payload
+ * payload_size - size of payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int udp_translate(clat_packet out, clat_packet_index pos, const struct udphdr *udp,
+                  uint32_t old_sum, uint32_t new_sum, const uint8_t *payload, size_t payload_size) {
+  struct udphdr *udp_targ = out[pos].iov_base;
+
+  memcpy(udp_targ, udp, sizeof(struct udphdr));
+
+  out[pos].iov_len               = sizeof(struct udphdr);
+  out[CLAT_POS_PAYLOAD].iov_base = (uint8_t *)payload;
+  out[CLAT_POS_PAYLOAD].iov_len  = payload_size;
+
+  if (udp_targ->check) {
+    udp_targ->check = ip_checksum_adjust(udp->check, old_sum, new_sum);
+  } else {
+    // Zero checksums are special. RFC 768 says, "An all zero transmitted checksum value means that
+    // the transmitter generated no checksum (for debugging or for higher level protocols that
+    // don't care)." However, in IPv6 zero UDP checksums were only permitted by RFC 6935 (2013). So
+    // for safety we recompute it.
+    udp_targ->check = 0;  // Checksum field must be 0 when calculating checksum.
+    udp_targ->check = packet_checksum(new_sum, out, pos);
+  }
+
+  // RFC 768: "If the computed checksum is zero, it is transmitted as all ones (the equivalent
+  // in one's complement arithmetic)."
+  if (!udp_targ->check) {
+    udp_targ->check = 0xffff;
+  }
+
+  return CLAT_POS_PAYLOAD + 1;
+}
+
+/* function: tcp_translate
+ * common between ipv4/ipv6 - setup checksum and send tcp packet
+ * out          - output packet
+ * tcp          - tcp header
+ * header_size  - size of tcp header including options
+ * checksum     - partial checksum covering ipv4/ipv6 header
+ * payload      - tcp payload
+ * payload_size - size of payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int tcp_translate(clat_packet out, clat_packet_index pos, const struct tcphdr *tcp,
+                  size_t header_size, uint32_t old_sum, uint32_t new_sum, const uint8_t *payload,
+                  size_t payload_size) {
+  struct tcphdr *tcp_targ = out[pos].iov_base;
+  out[pos].iov_len        = header_size;
+
+  if (header_size > MAX_TCP_HDR) {
+    // A TCP header cannot be more than MAX_TCP_HDR bytes long because it's a 4-bit field that
+    // counts in 4-byte words. So this can never happen unless there is a bug in the caller.
+    logmsg(ANDROID_LOG_ERROR, "tcp_translate: header too long %d > %d, truncating", header_size,
+           MAX_TCP_HDR);
+    header_size = MAX_TCP_HDR;
+  }
+
+  memcpy(tcp_targ, tcp, header_size);
+
+  out[CLAT_POS_PAYLOAD].iov_base = (uint8_t *)payload;
+  out[CLAT_POS_PAYLOAD].iov_len  = payload_size;
+
+  tcp_targ->check = ip_checksum_adjust(tcp->check, old_sum, new_sum);
+
+  return CLAT_POS_PAYLOAD + 1;
+}
+
+// Weak symbol so we can override it in the unit test.
+void send_rawv6(int fd, clat_packet out, int iov_len) __attribute__((weak));
+
+void send_rawv6(int fd, clat_packet out, int iov_len) {
+  // A send on a raw socket requires a destination address to be specified even if the socket's
+  // protocol is IPPROTO_RAW. This is the address that will be used in routing lookups; the
+  // destination address in the packet header only affects what appears on the wire, not where the
+  // packet is sent to.
+  static struct sockaddr_in6 sin6 = { AF_INET6, 0, 0, { { { 0, 0, 0, 0 } } }, 0 };
+  static struct msghdr msg        = {
+    .msg_name    = &sin6,
+    .msg_namelen = sizeof(sin6),
+  };
+
+  msg.msg_iov = out, msg.msg_iovlen = iov_len,
+  sin6.sin6_addr = ((struct ip6_hdr *)out[CLAT_POS_IPHDR].iov_base)->ip6_dst;
+  sendmsg(fd, &msg, 0);
+}
+
+/* function: translate_packet
+ * takes a packet, translates it, and writes it to fd
+ * fd         - fd to write translated packet to
+ * to_ipv6    - true if translating to ipv6, false if translating to ipv4
+ * packet     - packet
+ * packetsize - size of packet
+ */
+void translate_packet(int fd, int to_ipv6, const uint8_t *packet, size_t packetsize) {
+  int iov_len = 0;
+
+  // Allocate buffers for all packet headers.
+  struct tun_pi tun_targ;
+  char iphdr[sizeof(struct ip6_hdr)];
+  char fraghdr[sizeof(struct ip6_frag)];
+  char transporthdr[MAX_TCP_HDR];
+  char icmp_iphdr[sizeof(struct ip6_hdr)];
+  char icmp_fraghdr[sizeof(struct ip6_frag)];
+  char icmp_transporthdr[MAX_TCP_HDR];
+
+  // iovec of the packets we'll send. This gets passed down to the translation functions.
+  clat_packet out = {
+    { &tun_targ, 0 },          // Tunnel header.
+    { iphdr, 0 },              // IP header.
+    { fraghdr, 0 },            // Fragment header.
+    { transporthdr, 0 },       // Transport layer header.
+    { icmp_iphdr, 0 },         // ICMP error inner IP header.
+    { icmp_fraghdr, 0 },       // ICMP error fragmentation header.
+    { icmp_transporthdr, 0 },  // ICMP error transport layer header.
+    { NULL, 0 },               // Payload. No buffer, it's a pointer to the original payload.
+  };
+
+  if (to_ipv6) {
+    iov_len = ipv4_packet(out, CLAT_POS_IPHDR, packet, packetsize);
+    if (iov_len > 0) {
+      send_rawv6(fd, out, iov_len);
+    }
+  } else {
+    iov_len = ipv6_packet(out, CLAT_POS_IPHDR, packet, packetsize);
+    if (iov_len > 0) {
+      fill_tun_header(&tun_targ, ETH_P_IP);
+      out[CLAT_POS_TUNHDR].iov_len = sizeof(tun_targ);
+      writev(fd, out, iov_len);
+    }
+  }
+}
diff --git a/clatd/translate.h b/clatd/translate.h
new file mode 100644
index 0000000..0e520f7
--- /dev/null
+++ b/clatd/translate.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * translate.h - translate from one version of ip to another
+ */
+#ifndef __TRANSLATE_H__
+#define __TRANSLATE_H__
+
+#include <linux/icmp.h>
+#include <linux/if_tun.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
+
+#include "clatd.h"
+#include "common.h"
+
+#define MAX_TCP_HDR (15 * 4)  // Data offset field is 4 bits and counts in 32-bit words.
+
+// Calculates the checksum over all the packet components starting from pos.
+uint16_t packet_checksum(uint32_t checksum, clat_packet packet, clat_packet_index pos);
+
+// Returns the total length of the packet components after pos.
+uint16_t packet_length(clat_packet packet, clat_packet_index pos);
+
+// Returns true iff the given IPv6 address is in the plat subnet.
+int is_in_plat_subnet(const struct in6_addr *addr6);
+
+// Functions to create tun, IPv4, and IPv6 headers.
+void fill_tun_header(struct tun_pi *tun_header, uint16_t proto);
+void fill_ip_header(struct iphdr *ip_targ, uint16_t payload_len, uint8_t protocol,
+                    const struct ip6_hdr *old_header);
+void fill_ip6_header(struct ip6_hdr *ip6, uint16_t payload_len, uint8_t protocol,
+                     const struct iphdr *old_header);
+
+// Translate and send packets.
+void translate_packet(int fd, int to_ipv6, const uint8_t *packet, size_t packetsize);
+
+// Translate IPv4 and IPv6 packets.
+int ipv4_packet(clat_packet out, clat_packet_index pos, const uint8_t *packet, size_t len);
+int ipv6_packet(clat_packet out, clat_packet_index pos, const uint8_t *packet, size_t len);
+
+// Deal with fragmented packets.
+size_t maybe_fill_frag_header(struct ip6_frag *frag_hdr, struct ip6_hdr *ip6_targ,
+                              const struct iphdr *old_header);
+uint8_t parse_frag_header(const struct ip6_frag *frag_hdr, struct iphdr *ip_targ);
+
+// Deal with fragmented packets.
+size_t maybe_fill_frag_header(struct ip6_frag *frag_hdr, struct ip6_hdr *ip6_targ,
+                              const struct iphdr *old_header);
+uint8_t parse_frag_header(const struct ip6_frag *frag_hdr, struct iphdr *ip_targ);
+
+// Translate ICMP packets.
+int icmp_to_icmp6(clat_packet out, clat_packet_index pos, const struct icmphdr *icmp,
+                  uint32_t checksum, const uint8_t *payload, size_t payload_size);
+int icmp6_to_icmp(clat_packet out, clat_packet_index pos, const struct icmp6_hdr *icmp6,
+                  const uint8_t *payload, size_t payload_size);
+
+// Translate generic IP packets.
+int generic_packet(clat_packet out, clat_packet_index pos, const uint8_t *payload, size_t len);
+
+// Translate TCP and UDP packets.
+int tcp_packet(clat_packet out, clat_packet_index pos, const struct tcphdr *tcp, uint32_t old_sum,
+               uint32_t new_sum, size_t len);
+int udp_packet(clat_packet out, clat_packet_index pos, const struct udphdr *udp, uint32_t old_sum,
+               uint32_t new_sum, size_t len);
+
+int tcp_translate(clat_packet out, clat_packet_index pos, const struct tcphdr *tcp,
+                  size_t header_size, uint32_t old_sum, uint32_t new_sum, const uint8_t *payload,
+                  size_t payload_size);
+int udp_translate(clat_packet out, clat_packet_index pos, const struct udphdr *udp,
+                  uint32_t old_sum, uint32_t new_sum, const uint8_t *payload, size_t payload_size);
+
+#endif /* __TRANSLATE_H__ */
diff --git a/common/Android.bp b/common/Android.bp
index 729ef32..5fabf41 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -15,17 +15,25 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+build = ["FlaggedApi.bp"]
+
+// This is a placeholder comment to avoid merge conflicts
+// as the above target may not exist
+// depending on the branch
+
+// The library requires the final artifact to contain net-utils-device-common-struct-base.
 java_library {
     name: "connectivity-net-module-utils-bpf",
     srcs: [
         "src/com/android/net/module/util/bpf/*.java",
     ],
     sdk_version: "module_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     visibility: [
         // Do not add any lib. This library is only shared inside connectivity module
         // and its tests.
@@ -34,12 +42,15 @@
     libs: [
         "androidx.annotation_annotation",
         "framework-connectivity.stubs.module_lib",
-    ],
-    static_libs: [
-        "net-utils-device-common-struct",
+        // 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-base",
     ],
     apex_available: [
         "com.android.tethering",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+    },
 }
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
new file mode 100644
index 0000000..21be1d3
--- /dev/null
+++ b/common/FlaggedApi.bp
@@ -0,0 +1,39 @@
+//
+// 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.
+//
+
+aconfig_declarations {
+    name: "com.android.net.flags-aconfig",
+    package: "com.android.net.flags",
+    container: "com.android.tethering",
+    srcs: ["flags.aconfig"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+aconfig_declarations {
+    name: "com.android.net.thread.flags-aconfig",
+    package: "com.android.net.thread.flags",
+    container: "com.android.tethering",
+    srcs: ["thread_flags.aconfig"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+aconfig_declarations {
+    name: "nearby_flags",
+    package: "com.android.nearby.flags",
+    container: "com.android.tethering",
+    srcs: ["nearby_flags.aconfig"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/common/OWNERS b/common/OWNERS
new file mode 100644
index 0000000..e7f5d11
--- /dev/null
+++ b/common/OWNERS
@@ -0,0 +1 @@
+per-file thread_flags.aconfig = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/common/flags.aconfig b/common/flags.aconfig
new file mode 100644
index 0000000..b320b61
--- /dev/null
+++ b/common/flags.aconfig
@@ -0,0 +1,125 @@
+package: "com.android.net.flags"
+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"
+}
+
+flag {
+  name: "support_is_uid_networking_blocked"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "This flag controls whether isUidNetworkingBlocked is supported"
+  bug: "297836825"
+}
+
+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"
+}
+
+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"
+}
+
+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"
+}
+
+flag {
+  name: "request_restricted_wifi"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for API to support requesting restricted wifi"
+  bug: "315835605"
+}
+
+flag {
+  name: "net_capability_local_network"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for local network capability API"
+  bug: "313000440"
+}
+
+flag {
+  name: "support_transport_satellite"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for satellite transport API"
+  bug: "320514105"
+}
+
+flag {
+  name: "nsd_subtypes_support_enabled"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for API to support nsd subtypes"
+  bug: "265095929"
+}
+
+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"
+}
diff --git a/common/nearby_flags.aconfig b/common/nearby_flags.aconfig
new file mode 100644
index 0000000..55a865b
--- /dev/null
+++ b/common/nearby_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.nearby.flags"
+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/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/src/com/android/net/module/util/bpf/IngressDiscardKey.java b/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
new file mode 100644
index 0000000..9fefb52
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
@@ -0,0 +1,45 @@
+/*
+ * 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.net.module.util.bpf;
+
+import com.android.net.module.util.InetAddressUtils;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+/** Key type for ingress discard map */
+public class IngressDiscardKey extends Struct {
+    // The destination ip of the incoming packet. IPv4 uses IPv4-mapped IPv6 address.
+    @Field(order = 0, type = Type.Ipv6Address)
+    public final Inet6Address dstAddr;
+
+    public IngressDiscardKey(final Inet6Address dstAddr) {
+        this.dstAddr = dstAddr;
+    }
+
+    private static Inet6Address getInet6Address(final InetAddress addr) {
+        return (addr instanceof Inet4Address)
+                ? InetAddressUtils.v4MappedV6Address((Inet4Address) addr)
+                : (Inet6Address) addr;
+    }
+
+    public IngressDiscardKey(final InetAddress dstAddr) {
+        this(getInet6Address(dstAddr));
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java b/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java
new file mode 100644
index 0000000..7df3620
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java
@@ -0,0 +1,34 @@
+/*
+ * 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.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+
+/** Value type for ingress discard map */
+public class IngressDiscardValue extends Struct {
+    // Allowed interface indexes.
+    // Use the same value for iif1 and iif2 if there is only a single allowed interface index.
+    @Field(order = 0, type = Type.S32)
+    public final int iif1;
+    @Field(order = 1, type = Type.S32)
+    public final int iif2;
+
+    public IngressDiscardValue(final int iif1, final int iif2) {
+        this.iif1 = iif1;
+        this.iif2 = iif2;
+    }
+}
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
new file mode 100644
index 0000000..43acd1b
--- /dev/null
+++ b/common/thread_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.net.thread.flags"
+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 ffa2857..f076f5b 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -15,10 +15,19 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+framework_remoteauth_srcs = [":framework-remoteauth-java-sources"]
+framework_remoteauth_api_srcs = []
+
+java_defaults {
+    name: "enable-remoteauth-targets",
+    enabled: true,
+}
+
 // Include build rules from Sources.bp
 build = ["Sources.bp"]
 
@@ -26,6 +35,7 @@
     name: "enable-framework-connectivity-t-targets",
     enabled: true,
 }
+
 // The above defaults can be used to disable framework-connectivity t
 // targets while minimizing merge conflicts in the build rules.
 
@@ -42,12 +52,17 @@
     srcs: [
         ":framework-connectivity-tiramisu-updatable-sources",
         ":framework-nearby-java-sources",
+        ":framework-thread-sources",
     ],
     libs: [
         "unsupportedappusage",
         "app-compat-annotations",
         "androidx.annotation_annotation",
     ],
+    static_libs: [
+        // Cannot go to framework-connectivity because mid_sdk checks require 31.
+        "modules-utils-binary-xml",
+    ],
     impl_only_libs: [
         // The build system will use framework-bluetooth module_current stubs, because
         // of sdk_version: "module_current" above.
@@ -82,6 +97,20 @@
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
+// The filegroup lists files that are necessary for verifying building mdns as a standalone,
+// for use with service-connectivity-mdns-standalone-build-test
+// This filegroup should never be included in anywhere in the module build. It is only used for
+// building service-connectivity-mdns-standalone-build-test target. The files will be renamed by
+// copybara to prevent them from being shadowed by the bootclasspath copies.
+filegroup {
+    name: "framework-connectivity-t-mdns-standalone-build-sources",
+    srcs: [
+        "src/android/net/nsd/OffloadEngine.java",
+        "src/android/net/nsd/OffloadServiceInfo.java",
+    ],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
 java_library {
     name: "framework-connectivity-t-pre-jarjar",
     defaults: ["framework-connectivity-t-defaults"],
@@ -89,6 +118,7 @@
         "framework-bluetooth",
         "framework-wifi",
         "framework-connectivity-pre-jarjar",
+        "framework-location.stubs.module_lib",
     ],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
@@ -111,6 +141,7 @@
         "sdk_module-lib_current_framework-connectivity",
     ],
     libs: [
+        "framework-location.stubs.module_lib",
         "sdk_module-lib_current_framework-connectivity",
     ],
     permitted_packages: [
@@ -141,6 +172,7 @@
         "//packages/modules/Connectivity/service", // For R8 only
         "//packages/modules/Connectivity/service-t",
         "//packages/modules/Connectivity/nearby:__subpackages__",
+        "//packages/modules/Connectivity/remoteauth:__subpackages__",
         "//frameworks/base",
 
         // Tests using hidden APIs
@@ -150,18 +182,28 @@
         "//frameworks/base/core/tests/bandwidthtests",
         "//frameworks/base/core/tests/benchmarks",
         "//frameworks/base/core/tests/utillib",
+        "//frameworks/base/services/tests/VpnTests",
         "//frameworks/base/tests/vcn",
-        "//frameworks/libs/net/common/testutils",
-        "//frameworks/libs/net/common/tests:__subpackages__",
         "//frameworks/opt/net/ethernet/tests:__subpackages__",
         "//frameworks/opt/telephony/tests/telephonytests",
         "//packages/modules/CaptivePortalLogin/tests",
+        "//packages/modules/Connectivity/staticlibs/testutils",
+        "//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
         "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Connectivity/thread/tests:__subpackages__",
         "//packages/modules/IPsec/tests/iketests",
         "//packages/modules/NetworkStack/tests:__subpackages__",
         "//packages/modules/Wifi/service/tests/wifitests",
     ],
+    aconfig_declarations: [
+        "com.android.net.flags-aconfig",
+        "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/OWNERS b/framework-t/api/OWNERS
index de0f905..8ef735c 100644
--- a/framework-t/api/OWNERS
+++ b/framework-t/api/OWNERS
@@ -1 +1,3 @@
-file:platform/packages/modules/Connectivity:master:/nearby/OWNERS
+file:platform/packages/modules/Connectivity:main:/nearby/OWNERS
+file:platform/packages/modules/Connectivity:main:/remoteauth/OWNERS
+file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 86745d4..9ae0cf7 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -127,6 +127,7 @@
 
   public final class IpSecTransform implements java.lang.AutoCloseable {
     method public void close();
+    method @FlaggedApi("com.android.net.flags.ipsec_transform_state") public void requestIpSecTransformState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.IpSecTransformState,java.lang.RuntimeException>);
   }
 
   public static class IpSecTransform.Builder {
@@ -138,6 +139,29 @@
     method @NonNull public android.net.IpSecTransform.Builder setIpv4Encapsulation(@NonNull android.net.IpSecManager.UdpEncapsulationSocket, int);
   }
 
+  @FlaggedApi("com.android.net.flags.ipsec_transform_state") public final class IpSecTransformState implements android.os.Parcelable {
+    method public int describeContents();
+    method public long getByteCount();
+    method public long getPacketCount();
+    method @NonNull public byte[] getReplayBitmap();
+    method public long getRxHighestSequenceNumber();
+    method public long getTimestampMillis();
+    method public long getTxHighestSequenceNumber();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpSecTransformState> CREATOR;
+  }
+
+  @FlaggedApi("com.android.net.flags.ipsec_transform_state") public static final class IpSecTransformState.Builder {
+    ctor public IpSecTransformState.Builder();
+    method @NonNull public android.net.IpSecTransformState build();
+    method @NonNull public android.net.IpSecTransformState.Builder setByteCount(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setPacketCount(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setReplayBitmap(@NonNull byte[]);
+    method @NonNull public android.net.IpSecTransformState.Builder setRxHighestSequenceNumber(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setTimestampMillis(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setTxHighestSequenceNumber(long);
+  }
+
   public class TrafficStats {
     ctor public TrafficStats();
     method public static void clearThreadStatsTag();
@@ -186,9 +210,26 @@
 
 package android.net.nsd {
 
+  @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public final class DiscoveryRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.net.Network getNetwork();
+    method @NonNull public String getServiceType();
+    method @Nullable public String getSubtype();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.DiscoveryRequest> CREATOR;
+  }
+
+  public static final class DiscoveryRequest.Builder {
+    ctor public DiscoveryRequest.Builder(@NonNull String);
+    method @NonNull public android.net.nsd.DiscoveryRequest build();
+    method @NonNull public android.net.nsd.DiscoveryRequest.Builder setNetwork(@Nullable android.net.Network);
+    method @NonNull public android.net.nsd.DiscoveryRequest.Builder setSubtype(@Nullable String);
+  }
+
   public final class NsdManager {
     method public void discoverServices(String, int, android.net.nsd.NsdManager.DiscoveryListener);
     method public void discoverServices(@NonNull String, int, @Nullable android.net.Network, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
+    method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void discoverServices(@NonNull android.net.nsd.DiscoveryRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void discoverServices(@NonNull String, int, @NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
     method public void registerService(android.net.nsd.NsdServiceInfo, int, android.net.nsd.NsdManager.RegistrationListener);
     method public void registerService(@NonNull android.net.nsd.NsdServiceInfo, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.RegistrationListener);
@@ -251,6 +292,7 @@
     method public int getPort();
     method public String getServiceName();
     method public String getServiceType();
+    method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") @NonNull public java.util.Set<java.lang.String> getSubtypes();
     method public void removeAttribute(String);
     method public void setAttribute(String, String);
     method @Deprecated public void setHost(java.net.InetAddress);
@@ -259,6 +301,7 @@
     method public void setPort(int);
     method public void setServiceName(String);
     method public void setServiceType(String);
+    method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void setSubtypes(@NonNull java.util.Set<java.lang.String>);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.NsdServiceInfo> CREATOR;
   }
diff --git a/framework-t/api/module-lib-lint-baseline.txt b/framework-t/api/module-lib-lint-baseline.txt
index 3158bd4..6f954df 100644
--- a/framework-t/api/module-lib-lint-baseline.txt
+++ b/framework-t/api/module-lib-lint-baseline.txt
@@ -5,3 +5,17 @@
     Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
 BannedThrow: android.app.usage.NetworkStatsManager#queryTaggedSummary(android.net.NetworkTemplate, long, long):
     Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+
+
+MissingPermission: android.net.IpSecManager#startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress):
+    Feature field FEATURE_IPSEC_TUNNEL_MIGRATION required by method android.net.IpSecManager.startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress) is hidden or removed
+
+
+RequiresPermission: android.app.usage.NetworkStatsManager#registerUsageCallback(android.net.NetworkTemplate, long, java.util.concurrent.Executor, android.app.usage.NetworkStatsManager.UsageCallback):
+    Method 'registerUsageCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#disableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+    Method 'disableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#enableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+    Method 'enableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#updateConfiguration(String, android.net.EthernetNetworkUpdateRequest, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+    Method 'updateConfiguration' documentation mentions permissions already declared by @RequiresPermission
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 6613ee6..1f1953c 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -59,11 +59,17 @@
   }
 
   public class NearbyManager {
+    method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getPoweredOffFindingMode();
     method public void queryOffloadCapability(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.nearby.OffloadCapability>);
+    method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingEphemeralIds(@NonNull java.util.List<byte[]>);
+    method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingMode(int);
     method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void startBroadcast(@NonNull android.nearby.BroadcastRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.BroadcastCallback);
     method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int startScan(@NonNull android.nearby.ScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.ScanCallback);
     method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopBroadcast(@NonNull android.nearby.BroadcastCallback);
     method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopScan(@NonNull android.nearby.ScanCallback);
+    field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1; // 0x1
+    field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2; // 0x2
+    field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0; // 0x0
   }
 
   public final class OffloadCapability implements android.os.Parcelable {
@@ -305,6 +311,7 @@
     ctor public NetworkStats(long, int);
     method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
     method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
+    method public android.net.NetworkStats clone();
     method public int describeContents();
     method @NonNull public java.util.Iterator<android.net.NetworkStats.Entry> iterator();
     method @NonNull public android.net.NetworkStats subtract(@NonNull android.net.NetworkStats);
@@ -374,3 +381,176 @@
 
 }
 
+package android.net.nsd {
+
+  public final class NsdManager {
+    method @FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api") @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void registerOffloadEngine(@NonNull String, long, long, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.OffloadEngine);
+    method @FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api") @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void unregisterOffloadEngine(@NonNull android.net.nsd.OffloadEngine);
+  }
+
+  @FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api") public interface OffloadEngine {
+    method public void onOffloadServiceRemoved(@NonNull android.net.nsd.OffloadServiceInfo);
+    method public void onOffloadServiceUpdated(@NonNull android.net.nsd.OffloadServiceInfo);
+    field public static final int OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK = 1; // 0x1
+    field public static final int OFFLOAD_TYPE_FILTER_QUERIES = 2; // 0x2
+    field public static final int OFFLOAD_TYPE_FILTER_REPLIES = 4; // 0x4
+    field public static final int OFFLOAD_TYPE_REPLY = 1; // 0x1
+  }
+
+  @FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api") public final class OffloadServiceInfo implements android.os.Parcelable {
+    ctor public OffloadServiceInfo(@NonNull android.net.nsd.OffloadServiceInfo.Key, @NonNull java.util.List<java.lang.String>, @NonNull String, @Nullable byte[], @IntRange(from=0, to=java.lang.Integer.MAX_VALUE) int, long);
+    method public int describeContents();
+    method @NonNull public String getHostname();
+    method @NonNull public android.net.nsd.OffloadServiceInfo.Key getKey();
+    method @Nullable public byte[] getOffloadPayload();
+    method public long getOffloadType();
+    method public int getPriority();
+    method @NonNull public java.util.List<java.lang.String> getSubtypes();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.OffloadServiceInfo> CREATOR;
+  }
+
+  public static final class OffloadServiceInfo.Key implements android.os.Parcelable {
+    ctor public OffloadServiceInfo.Key(@NonNull String, @NonNull String);
+    method public int describeContents();
+    method @NonNull public String getServiceName();
+    method @NonNull public String getServiceType();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.OffloadServiceInfo.Key> CREATOR;
+  }
+
+}
+
+package android.net.thread {
+
+  @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ActiveOperationalDataset implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public static android.net.thread.ActiveOperationalDataset fromThreadTlvs(@NonNull byte[]);
+    method @NonNull public android.net.thread.OperationalDatasetTimestamp getActiveTimestamp();
+    method @IntRange(from=0, to=65535) public int getChannel();
+    method @NonNull @Size(min=1) public android.util.SparseArray<byte[]> getChannelMask();
+    method @IntRange(from=0, to=255) public int getChannelPage();
+    method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) public byte[] getExtendedPanId();
+    method @NonNull public android.net.IpPrefix getMeshLocalPrefix();
+    method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) public byte[] getNetworkKey();
+    method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) public String getNetworkName();
+    method @IntRange(from=0, to=65534) public int getPanId();
+    method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) public byte[] getPskc();
+    method @NonNull public android.net.thread.ActiveOperationalDataset.SecurityPolicy getSecurityPolicy();
+    method @NonNull public byte[] toThreadTlvs();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int CHANNEL_MAX_24_GHZ = 26; // 0x1a
+    field public static final int CHANNEL_MIN_24_GHZ = 11; // 0xb
+    field public static final int CHANNEL_PAGE_24_GHZ = 0; // 0x0
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ActiveOperationalDataset> CREATOR;
+    field public static final int LENGTH_EXTENDED_PAN_ID = 8; // 0x8
+    field public static final int LENGTH_MAX_DATASET_TLVS = 254; // 0xfe
+    field public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16; // 0x10
+    field public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64; // 0x40
+    field public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1; // 0x1
+    field public static final int LENGTH_NETWORK_KEY = 16; // 0x10
+    field public static final int LENGTH_PSKC = 16; // 0x10
+  }
+
+  public static final class ActiveOperationalDataset.Builder {
+    ctor public ActiveOperationalDataset.Builder(@NonNull android.net.thread.ActiveOperationalDataset);
+    ctor public ActiveOperationalDataset.Builder();
+    method @NonNull public android.net.thread.ActiveOperationalDataset build();
+    method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setActiveTimestamp(@NonNull android.net.thread.OperationalDatasetTimestamp);
+    method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannel(@IntRange(from=0, to=255) int, @IntRange(from=0, to=65535) int);
+    method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannelMask(@NonNull @Size(min=1) android.util.SparseArray<byte[]>);
+    method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setExtendedPanId(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) byte[]);
+    method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setMeshLocalPrefix(@NonNull android.net.IpPrefix);
+    method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkKey(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) byte[]);
+    method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkName(@NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) String);
+    method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPanId(@IntRange(from=0, to=65534) int);
+    method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPskc(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) byte[]);
+    method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setSecurityPolicy(@NonNull android.net.thread.ActiveOperationalDataset.SecurityPolicy);
+  }
+
+  public static final class ActiveOperationalDataset.SecurityPolicy {
+    ctor public ActiveOperationalDataset.SecurityPolicy(@IntRange(from=1, to=65535) int, @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[]);
+    method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) public byte[] getFlags();
+    method @IntRange(from=1, to=65535) public int getRotationTimeHours();
+    field public static final int DEFAULT_ROTATION_TIME_HOURS = 672; // 0x2a0
+    field public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1; // 0x1
+  }
+
+  @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class OperationalDatasetTimestamp {
+    ctor public OperationalDatasetTimestamp(@IntRange(from=0, to=281474976710655L) long, @IntRange(from=0, to=32767) int, boolean);
+    method @NonNull public static android.net.thread.OperationalDatasetTimestamp fromInstant(@NonNull java.time.Instant);
+    method @IntRange(from=0, to=281474976710655L) public long getSeconds();
+    method @IntRange(from=0, to=32767) public int getTicks();
+    method public boolean isAuthoritativeSource();
+    method @NonNull public java.time.Instant toInstant();
+  }
+
+  @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class PendingOperationalDataset implements android.os.Parcelable {
+    ctor public PendingOperationalDataset(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull android.net.thread.OperationalDatasetTimestamp, @NonNull java.time.Duration);
+    method public int describeContents();
+    method @NonNull public static android.net.thread.PendingOperationalDataset fromThreadTlvs(@NonNull byte[]);
+    method @NonNull public android.net.thread.ActiveOperationalDataset getActiveOperationalDataset();
+    method @NonNull public java.time.Duration getDelayTimer();
+    method @NonNull public android.net.thread.OperationalDatasetTimestamp getPendingTimestamp();
+    method @NonNull public byte[] toThreadTlvs();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> 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 @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 @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
+    field public static final int DEVICE_ROLE_DETACHED = 1; // 0x1
+    field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
+    field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
+    field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
+    field public static final int STATE_DISABLED = 0; // 0x0
+    field public static final int STATE_DISABLING = 2; // 0x2
+    field public static final int STATE_ENABLED = 1; // 0x1
+    field public static final int THREAD_VERSION_1_3 = 4; // 0x4
+  }
+
+  public static interface ThreadNetworkController.OperationalDatasetCallback {
+    method public void onActiveOperationalDatasetChanged(@Nullable android.net.thread.ActiveOperationalDataset);
+    method public default void onPendingOperationalDatasetChanged(@Nullable android.net.thread.PendingOperationalDataset);
+  }
+
+  public static interface ThreadNetworkController.StateCallback {
+    method public void onDeviceRoleChanged(int);
+    method public default void onPartitionIdChanged(long);
+    method public default void onThreadEnableStateChanged(int);
+  }
+
+  @FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkException extends java.lang.Exception {
+    ctor public ThreadNetworkException(int, @NonNull String);
+    method public int getErrorCode();
+    field public static final int ERROR_ABORTED = 2; // 0x2
+    field public static final int ERROR_BUSY = 5; // 0x5
+    field public static final int ERROR_FAILED_PRECONDITION = 6; // 0x6
+    field public static final int ERROR_INTERNAL_ERROR = 1; // 0x1
+    field public static final int ERROR_REJECTED_BY_PEER = 8; // 0x8
+    field public static final int ERROR_RESOURCE_EXHAUSTED = 10; // 0xa
+    field public static final int ERROR_RESPONSE_BAD_FORMAT = 9; // 0x9
+    field public static final int ERROR_THREAD_DISABLED = 12; // 0xc
+    field public static final int ERROR_TIMEOUT = 3; // 0x3
+    field public static final int ERROR_UNAVAILABLE = 4; // 0x4
+    field public static final int ERROR_UNKNOWN = 11; // 0xb
+    field public static final int ERROR_UNSUPPORTED_CHANNEL = 7; // 0x7
+  }
+
+  @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkManager {
+    method @NonNull public java.util.List<android.net.thread.ThreadNetworkController> getAllThreadNetworkControllers();
+  }
+
+}
+
diff --git a/framework-t/api/system-lint-baseline.txt b/framework-t/api/system-lint-baseline.txt
index 9baf991..4f7af87 100644
--- a/framework-t/api/system-lint-baseline.txt
+++ b/framework-t/api/system-lint-baseline.txt
@@ -5,3 +5,161 @@
 
 GenericException: android.net.IpSecManager.IpSecTunnelInterface#finalize():
     Methods must not throw generic exceptions (`java.lang.Throwable`)
+
+
+MissingPermission: android.net.IpSecManager#startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress):
+    Feature field FEATURE_IPSEC_TUNNEL_MIGRATION required by method android.net.IpSecManager.startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress) is hidden or removed
+
+
+RequiresPermission: android.net.EthernetManager#disableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+    Method 'disableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#enableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+    Method 'enableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#updateConfiguration(String, android.net.EthernetNetworkUpdateRequest, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+    Method 'updateConfiguration' documentation mentions permissions already declared by @RequiresPermission
+
+
+UnflaggedApi: android.nearby.CredentialElement#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.CredentialElement.equals(Object)
+UnflaggedApi: android.nearby.CredentialElement#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.CredentialElement.hashCode()
+UnflaggedApi: android.nearby.DataElement#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.DataElement.equals(Object)
+UnflaggedApi: android.nearby.DataElement#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.DataElement.hashCode()
+UnflaggedApi: android.nearby.NearbyDevice#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.NearbyDevice.equals(Object)
+UnflaggedApi: android.nearby.NearbyDevice#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.NearbyDevice.hashCode()
+UnflaggedApi: android.nearby.NearbyDevice#toString():
+    New API must be flagged with @FlaggedApi: method android.nearby.NearbyDevice.toString()
+UnflaggedApi: android.nearby.OffloadCapability#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.OffloadCapability.equals(Object)
+UnflaggedApi: android.nearby.OffloadCapability#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.OffloadCapability.hashCode()
+UnflaggedApi: android.nearby.OffloadCapability#toString():
+    New API must be flagged with @FlaggedApi: method android.nearby.OffloadCapability.toString()
+UnflaggedApi: android.nearby.PresenceCredential#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.PresenceCredential.equals(Object)
+UnflaggedApi: android.nearby.PresenceCredential#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.PresenceCredential.hashCode()
+UnflaggedApi: android.nearby.PublicCredential#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.PublicCredential.equals(Object)
+UnflaggedApi: android.nearby.PublicCredential#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.PublicCredential.hashCode()
+UnflaggedApi: android.nearby.ScanRequest#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.ScanRequest.equals(Object)
+UnflaggedApi: android.nearby.ScanRequest#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.ScanRequest.hashCode()
+UnflaggedApi: android.nearby.ScanRequest#toString():
+    New API must be flagged with @FlaggedApi: method android.nearby.ScanRequest.toString()
+UnflaggedApi: android.net.EthernetNetworkManagementException#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkManagementException.equals(Object)
+UnflaggedApi: android.net.EthernetNetworkManagementException#hashCode():
+    New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkManagementException.hashCode()
+UnflaggedApi: android.net.EthernetNetworkUpdateRequest#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkUpdateRequest.equals(Object)
+UnflaggedApi: android.net.EthernetNetworkUpdateRequest#hashCode():
+    New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkUpdateRequest.hashCode()
+UnflaggedApi: android.net.EthernetNetworkUpdateRequest#toString():
+    New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkUpdateRequest.toString()
+UnflaggedApi: android.net.IpSecManager.IpSecTunnelInterface#finalize():
+    New API must be flagged with @FlaggedApi: method android.net.IpSecManager.IpSecTunnelInterface.finalize()
+UnflaggedApi: android.net.IpSecManager.IpSecTunnelInterface#toString():
+    New API must be flagged with @FlaggedApi: method android.net.IpSecManager.IpSecTunnelInterface.toString()
+UnflaggedApi: android.net.IpSecTransform.Builder#buildTunnelModeTransform(java.net.InetAddress, android.net.IpSecManager.SecurityParameterIndex):
+    New API must be flagged with @FlaggedApi: method android.net.IpSecTransform.Builder.buildTunnelModeTransform(java.net.InetAddress,android.net.IpSecManager.SecurityParameterIndex)
+UnflaggedApi: android.net.NetworkStats.Entry#toString():
+    New API must be flagged with @FlaggedApi: method android.net.NetworkStats.Entry.toString()
+UnflaggedApi: android.net.nsd.NsdManager#registerOffloadEngine(String, long, long, java.util.concurrent.Executor, android.net.nsd.OffloadEngine):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.NsdManager.registerOffloadEngine(String,long,long,java.util.concurrent.Executor,android.net.nsd.OffloadEngine)
+UnflaggedApi: android.net.nsd.NsdManager#unregisterOffloadEngine(android.net.nsd.OffloadEngine):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.NsdManager.unregisterOffloadEngine(android.net.nsd.OffloadEngine)
+UnflaggedApi: android.net.nsd.OffloadEngine:
+    New API must be flagged with @FlaggedApi: class android.net.nsd.OffloadEngine
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_TYPE_FILTER_QUERIES:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_TYPE_FILTER_QUERIES
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_TYPE_FILTER_REPLIES:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_TYPE_FILTER_REPLIES
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_TYPE_REPLY:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_TYPE_REPLY
+UnflaggedApi: android.net.nsd.OffloadEngine#onOffloadServiceRemoved(android.net.nsd.OffloadServiceInfo):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadEngine.onOffloadServiceRemoved(android.net.nsd.OffloadServiceInfo)
+UnflaggedApi: android.net.nsd.OffloadEngine#onOffloadServiceUpdated(android.net.nsd.OffloadServiceInfo):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadEngine.onOffloadServiceUpdated(android.net.nsd.OffloadServiceInfo)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo:
+    New API must be flagged with @FlaggedApi: class android.net.nsd.OffloadServiceInfo
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#CONTENTS_FILE_DESCRIPTOR:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.CONTENTS_FILE_DESCRIPTOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#CREATOR:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.CREATOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#OffloadServiceInfo(android.net.nsd.OffloadServiceInfo.Key, java.util.List<java.lang.String>, String, byte[], int, long):
+    New API must be flagged with @FlaggedApi: constructor android.net.nsd.OffloadServiceInfo(android.net.nsd.OffloadServiceInfo.Key,java.util.List<java.lang.String>,String,byte[],int,long)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#PARCELABLE_STABILITY_LOCAL:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.PARCELABLE_STABILITY_LOCAL
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#PARCELABLE_STABILITY_VINTF:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.PARCELABLE_STABILITY_VINTF
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#PARCELABLE_WRITE_RETURN_VALUE:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.PARCELABLE_WRITE_RETURN_VALUE
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#describeContents():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.describeContents()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.equals(Object)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getHostname():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getHostname()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getKey():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getKey()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getOffloadPayload():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getOffloadPayload()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getOffloadType():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getOffloadType()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getPriority():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getPriority()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getSubtypes():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getSubtypes()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#hashCode():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.hashCode()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#toString():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.toString()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#writeToParcel(android.os.Parcel, int):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.writeToParcel(android.os.Parcel,int)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key:
+    New API must be flagged with @FlaggedApi: class android.net.nsd.OffloadServiceInfo.Key
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#CONTENTS_FILE_DESCRIPTOR:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.CONTENTS_FILE_DESCRIPTOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#CREATOR:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.CREATOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#Key(String, String):
+    New API must be flagged with @FlaggedApi: constructor android.net.nsd.OffloadServiceInfo.Key(String,String)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#PARCELABLE_STABILITY_LOCAL:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.PARCELABLE_STABILITY_LOCAL
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#PARCELABLE_STABILITY_VINTF:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.PARCELABLE_STABILITY_VINTF
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#PARCELABLE_WRITE_RETURN_VALUE:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.PARCELABLE_WRITE_RETURN_VALUE
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#describeContents():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.describeContents()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.equals(Object)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#getServiceName():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.getServiceName()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#getServiceType():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.getServiceType()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#hashCode():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.hashCode()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#toString():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.toString()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#writeToParcel(android.os.Parcel, int):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.writeToParcel(android.os.Parcel,int)
+UnflaggedApi: android.net.thread.ThreadNetworkController:
+    New API must be flagged with @FlaggedApi: class android.net.thread.ThreadNetworkController
+UnflaggedApi: android.net.thread.ThreadNetworkController#THREAD_VERSION_1_3:
+    New API must be flagged with @FlaggedApi: field android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3
+UnflaggedApi: android.net.thread.ThreadNetworkController#getThreadVersion():
+    New API must be flagged with @FlaggedApi: method android.net.thread.ThreadNetworkController.getThreadVersion()
+UnflaggedApi: android.net.thread.ThreadNetworkManager:
+    New API must be flagged with @FlaggedApi: class android.net.thread.ThreadNetworkManager
+UnflaggedApi: android.net.thread.ThreadNetworkManager#getAllThreadNetworkControllers():
+    New API must be flagged with @FlaggedApi: method android.net.thread.ThreadNetworkManager.getAllThreadNetworkControllers()
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 d139544..18c839f 100644
--- a/framework-t/src/android/app/usage/NetworkStatsManager.java
+++ b/framework-t/src/android/app/usage/NetworkStatsManager.java
@@ -510,6 +510,27 @@
      * Query network usage statistics details for a given uid.
      * This may take a long time, and apps should avoid calling this on their main thread.
      *
+     * @param networkType As defined in {@link ConnectivityManager}, e.g.
+     *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+     *            etc.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param startTime Start of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param uid UID of app
+     * @return Statistics which is described above.
+     * @throws SecurityException if permissions are insufficient to read network statistics.
      * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
      */
     @NonNull
@@ -731,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/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
index d9c9d74..d7cff2c 100644
--- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
+++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -24,6 +24,10 @@
 import android.net.nsd.INsdManager;
 import android.net.nsd.MDnsManager;
 import android.net.nsd.NsdManager;
+import android.net.thread.IThreadNetworkManager;
+import android.net.thread.ThreadNetworkManager;
+
+import com.android.modules.utils.build.SdkLevel;
 
 /**
  * Class for performing registration for Connectivity services which are exposed via updatable APIs
@@ -81,13 +85,25 @@
                 }
         );
 
-        SystemServiceRegistry.registerStaticService(
-                MDnsManager.MDNS_SERVICE,
-                MDnsManager.class,
-                (serviceBinder) -> {
-                    IMDns service = IMDns.Stub.asInterface(serviceBinder);
-                    return new MDnsManager(service);
-                }
-        );
+        // mdns service is removed from Netd from Android V.
+        if (!SdkLevel.isAtLeastV()) {
+            SystemServiceRegistry.registerStaticService(
+                    MDnsManager.MDNS_SERVICE,
+                    MDnsManager.class,
+                    (serviceBinder) -> {
+                        IMDns service = IMDns.Stub.asInterface(serviceBinder);
+                        return new MDnsManager(service);
+                    }
+            );
+        }
+
+        SystemServiceRegistry.registerContextAwareService(
+                ThreadNetworkManager.SERVICE_NAME,
+                ThreadNetworkManager.class,
+                (context, serviceBinder) -> {
+                    IThreadNetworkManager managerService =
+                            IThreadNetworkManager.Stub.asInterface(serviceBinder);
+                    return new ThreadNetworkManager(context, managerService);
+                });
     }
 }
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/EthernetNetworkSpecifier.java b/framework-t/src/android/net/EthernetNetworkSpecifier.java
index e4d6e24..90c0361 100644
--- a/framework-t/src/android/net/EthernetNetworkSpecifier.java
+++ b/framework-t/src/android/net/EthernetNetworkSpecifier.java
@@ -26,8 +26,6 @@
 
 /**
  * A {@link NetworkSpecifier} used to identify ethernet interfaces.
- *
- * @see EthernetManager
  */
 public final class EthernetNetworkSpecifier extends NetworkSpecifier implements Parcelable {
 
diff --git a/framework-t/src/android/net/IIpSecService.aidl b/framework-t/src/android/net/IIpSecService.aidl
index 88ffd0e..f972ab9 100644
--- a/framework-t/src/android/net/IIpSecService.aidl
+++ b/framework-t/src/android/net/IIpSecService.aidl
@@ -22,6 +22,7 @@
 import android.net.IpSecUdpEncapResponse;
 import android.net.IpSecSpiResponse;
 import android.net.IpSecTransformResponse;
+import android.net.IpSecTransformState;
 import android.net.IpSecTunnelInterfaceResponse;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -74,6 +75,8 @@
 
     void deleteTransform(int transformId);
 
+    IpSecTransformState getTransformState(int transformId);
+
     void applyTransportModeTransform(
             in ParcelFileDescriptor socket, int direction, int transformId);
 
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/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 3afa6ef..3f74e1c 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -65,6 +65,13 @@
 public class IpSecManager {
     private static final String TAG = "IpSecManager";
 
+    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+    // available here
+    /** @hide */
+    public static class Flags {
+        static final String IPSEC_TRANSFORM_STATE = "com.android.net.flags.ipsec_transform_state";
+    }
+
     /**
      * Feature flag to declare the kernel support of updating IPsec SAs.
      *
@@ -1084,6 +1091,12 @@
         }
     }
 
+    /** @hide */
+    public IpSecTransformState getTransformState(int transformId)
+            throws IllegalStateException, RemoteException {
+        return mService.getTransformState(transformId);
+    }
+
     /**
      * Construct an instance of IpSecManager within an application context.
      *
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index c236b6c..70c9bc8 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -15,8 +15,11 @@
  */
 package android.net;
 
+import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
 import static android.net.IpSecManager.INVALID_RESOURCE_ID;
 
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -26,6 +29,8 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Binder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.util.Log;
 
@@ -38,6 +43,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.net.InetAddress;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * This class represents a transform, which roughly corresponds to an IPsec Security Association.
@@ -118,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());
@@ -158,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();
+            }
         }
     }
 
@@ -190,14 +199,56 @@
     }
 
     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;
+        }
+    }
+
+    /**
+     * Retrieve the current state of this IpSecTransform.
+     *
+     * @param executor The {@link Executor} on which to call the supplied callback.
+     * @param callback Callback that's called after the transform state is ready or when an error
+     *     occurs.
+     * @see IpSecTransformState
+     */
+    @FlaggedApi(IPSEC_TRANSFORM_STATE)
+    public void requestIpSecTransformState(
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        // TODO: Consider adding check to prevent DDoS attack.
+
+        try {
+            IpSecTransformState ipSecTransformState;
+            synchronized(mLock) {
+                ipSecTransformState = getIpSecManager(mContext).getTransformState(mResourceId);
+            }
+            executor.execute(
+                    () -> {
+                        callback.onResult(ipSecTransformState);
+                    });
+        } catch (IllegalStateException e) {
+            executor.execute(
+                    () -> {
+                        callback.onError(e);
+                    });
+        } catch (RemoteException e) {
+            executor.execute(
+                    () -> {
+                        callback.onError(e.rethrowFromSystemServer());
+                    });
+        }
     }
 
     /**
diff --git a/framework-t/src/android/net/IpSecTransformState.aidl b/framework-t/src/android/net/IpSecTransformState.aidl
new file mode 100644
index 0000000..69cce28
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformState.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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;
+
+/** @hide */
+parcelable IpSecTransformState;
\ No newline at end of file
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
new file mode 100644
index 0000000..5b80ae2
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -0,0 +1,295 @@
+/*
+ * 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.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HexDump;
+
+import java.util.Objects;
+
+/**
+ * This class represents a snapshot of the state of an IpSecTransform
+ *
+ * <p>This class provides the current state of an IpSecTransform, enabling link metric analysis by
+ * the caller. Use cases include understanding transform usage, such as packet and byte counts, as
+ * well as observing out-of-order delivery by checking the bitmap. Additionally, callers can query
+ * IpSecTransformStates at two timestamps. By comparing the changes in packet counts and sequence
+ * numbers, callers can estimate IPsec data loss in the inbound direction.
+ */
+@FlaggedApi(IPSEC_TRANSFORM_STATE)
+public final class IpSecTransformState implements Parcelable {
+    private final long mTimestamp;
+    private final long mTxHighestSequenceNumber;
+    private final long mRxHighestSequenceNumber;
+    private final long mPacketCount;
+    private final long mByteCount;
+    private final byte[] mReplayBitmap;
+
+    private IpSecTransformState(
+            long timestamp,
+            long txHighestSequenceNumber,
+            long rxHighestSequenceNumber,
+            long packetCount,
+            long byteCount,
+            byte[] replayBitmap) {
+        mTimestamp = timestamp;
+        mTxHighestSequenceNumber = txHighestSequenceNumber;
+        mRxHighestSequenceNumber = rxHighestSequenceNumber;
+        mPacketCount = packetCount;
+        mByteCount = byteCount;
+
+        Objects.requireNonNull(replayBitmap, "replayBitmap is null");
+        mReplayBitmap = replayBitmap.clone();
+
+        validate();
+    }
+
+    private void validate() {
+        Objects.requireNonNull(mReplayBitmap, "mReplayBitmap is null");
+    }
+
+    /**
+     * Deserializes a IpSecTransformState from a PersistableBundle.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public IpSecTransformState(@NonNull Parcel in) {
+        Objects.requireNonNull(in, "The input PersistableBundle is null");
+        mTimestamp = in.readLong();
+        mTxHighestSequenceNumber = in.readLong();
+        mRxHighestSequenceNumber = in.readLong();
+        mPacketCount = in.readLong();
+        mByteCount = in.readLong();
+        mReplayBitmap = HexDump.hexStringToByteArray(in.readString());
+
+        validate();
+    }
+
+    // Parcelable methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeLong(mTimestamp);
+        out.writeLong(mTxHighestSequenceNumber);
+        out.writeLong(mRxHighestSequenceNumber);
+        out.writeLong(mPacketCount);
+        out.writeLong(mByteCount);
+        out.writeString(HexDump.toHexString(mReplayBitmap));
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<IpSecTransformState> CREATOR =
+            new Parcelable.Creator<IpSecTransformState>() {
+                @NonNull
+                public IpSecTransformState createFromParcel(Parcel in) {
+                    return new IpSecTransformState(in);
+                }
+
+                @NonNull
+                public IpSecTransformState[] newArray(int size) {
+                    return new IpSecTransformState[size];
+                }
+            };
+
+    /**
+     * Retrieve the timestamp (milliseconds) when this state was created, as per {@link
+     * SystemClock#elapsedRealtime}
+     *
+     * @see Builder#setTimestampMillis(long)
+     */
+    public long getTimestampMillis() {
+        return mTimestamp;
+    }
+
+    /**
+     * Retrieve the highest sequence number sent so far as an unsigned long
+     *
+     * @see Builder#setTxHighestSequenceNumber(long)
+     */
+    public long getTxHighestSequenceNumber() {
+        return mTxHighestSequenceNumber;
+    }
+
+    /**
+     * Retrieve the highest sequence number received so far as an unsigned long
+     *
+     * @see Builder#setRxHighestSequenceNumber(long)
+     */
+    public long getRxHighestSequenceNumber() {
+        return mRxHighestSequenceNumber;
+    }
+
+    /**
+     * Retrieve the number of packets processed so far as an unsigned long.
+     *
+     * <p>The packet count direction (inbound or outbound) aligns with the direction in which the
+     * IpSecTransform is applied to.
+     *
+     * @see Builder#setPacketCount(long)
+     */
+    public long getPacketCount() {
+        return mPacketCount;
+    }
+
+    /**
+     * Retrieve the number of bytes processed so far as an unsigned long
+     *
+     * <p>The byte count direction (inbound or outbound) aligns with the direction in which the
+     * IpSecTransform is applied to.
+     *
+     * @see Builder#setByteCount(long)
+     */
+    public long getByteCount() {
+        return mByteCount;
+    }
+
+    /**
+     * Retrieve the replay bitmap
+     *
+     * <p>This bitmap represents a replay window, allowing the caller to observe out-of-order
+     * delivery. The last bit represents the highest sequence number received so far and bits for
+     * the received packets will be marked as true.
+     *
+     * <p>The size of a replay bitmap will never change over the lifetime of an IpSecTransform
+     *
+     * <p>The replay bitmap is solely useful for inbound IpSecTransforms. For outbound
+     * IpSecTransforms, all bits will be unchecked.
+     *
+     * @see Builder#setReplayBitmap(byte[])
+     */
+    @NonNull
+    public byte[] getReplayBitmap() {
+        return mReplayBitmap.clone();
+    }
+
+    /**
+     * Builder class for testing purposes
+     *
+     * <p>Except for testing, IPsec callers normally do not instantiate {@link IpSecTransformState}
+     * themselves but instead get a reference via {@link IpSecTransformState}
+     */
+    @FlaggedApi(IPSEC_TRANSFORM_STATE)
+    public static final class Builder {
+        private long mTimestamp;
+        private long mTxHighestSequenceNumber;
+        private long mRxHighestSequenceNumber;
+        private long mPacketCount;
+        private long mByteCount;
+        private byte[] mReplayBitmap;
+
+        public Builder() {
+            mTimestamp = SystemClock.elapsedRealtime();
+        }
+
+        /**
+         * Set the timestamp (milliseconds) when this state was created
+         *
+         * @see IpSecTransformState#getTimestampMillis()
+         */
+        @NonNull
+        public Builder setTimestampMillis(long timestamp) {
+            mTimestamp = timestamp;
+            return this;
+        }
+
+        /**
+         * Set the highest sequence number sent so far as an unsigned long
+         *
+         * @see IpSecTransformState#getTxHighestSequenceNumber()
+         */
+        @NonNull
+        public Builder setTxHighestSequenceNumber(long seqNum) {
+            mTxHighestSequenceNumber = seqNum;
+            return this;
+        }
+
+        /**
+         * Set the highest sequence number received so far as an unsigned long
+         *
+         * @see IpSecTransformState#getRxHighestSequenceNumber()
+         */
+        @NonNull
+        public Builder setRxHighestSequenceNumber(long seqNum) {
+            mRxHighestSequenceNumber = seqNum;
+            return this;
+        }
+
+        /**
+         * Set the number of packets processed so far as an unsigned long
+         *
+         * @see IpSecTransformState#getPacketCount()
+         */
+        @NonNull
+        public Builder setPacketCount(long packetCount) {
+            mPacketCount = packetCount;
+            return this;
+        }
+
+        /**
+         * Set the number of bytes processed so far as an unsigned long
+         *
+         * @see IpSecTransformState#getByteCount()
+         */
+        @NonNull
+        public Builder setByteCount(long byteCount) {
+            mByteCount = byteCount;
+            return this;
+        }
+
+        /**
+         * Set the replay bitmap
+         *
+         * @see IpSecTransformState#getReplayBitmap()
+         */
+        @NonNull
+        public Builder setReplayBitmap(@NonNull byte[] bitMap) {
+            mReplayBitmap = bitMap.clone();
+            return this;
+        }
+
+        /**
+         * Build and validate the IpSecTransformState
+         *
+         * @return an immutable IpSecTransformState instance
+         */
+        @NonNull
+        public IpSecTransformState build() {
+            return new IpSecTransformState(
+                    mTimestamp,
+                    mTxHighestSequenceNumber,
+                    mRxHighestSequenceNumber,
+                    mPacketCount,
+                    mByteCount,
+                    mReplayBitmap);
+        }
+    }
+}
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
index 8719960..e9a3f58 100644
--- a/framework-t/src/android/net/NetworkStats.java
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -32,6 +32,7 @@
 import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 
 import libcore.util.EmptyArray;
@@ -46,6 +47,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.function.Function;
 import java.util.function.Predicate;
 
 /**
@@ -455,6 +457,41 @@
             return operations;
         }
 
+        /**
+         * Set Key fields for this entry.
+         *
+         * @return this object.
+         * @hide
+         */
+        private Entry setKeys(@Nullable String iface, int uid, @State int set,
+                int tag, @Meteredness int metered, @Roaming int roaming,
+                @DefaultNetwork int defaultNetwork) {
+            this.iface = iface;
+            this.uid = uid;
+            this.set = set;
+            this.tag = tag;
+            this.metered = metered;
+            this.roaming = roaming;
+            this.defaultNetwork = defaultNetwork;
+            return this;
+        }
+
+        /**
+         * Set Value fields for this entry.
+         *
+         * @return this object.
+         * @hide
+         */
+        private Entry setValues(long rxBytes, long rxPackets, long txBytes, long txPackets,
+                long operations) {
+            this.rxBytes = rxBytes;
+            this.rxPackets = rxPackets;
+            this.txBytes = txBytes;
+            this.txPackets = txPackets;
+            this.operations = operations;
+            return this;
+        }
+
         @Override
         public String toString() {
             final StringBuilder builder = new StringBuilder();
@@ -1111,7 +1148,8 @@
             entry.txPackets = left.txPackets[i];
             entry.operations = left.operations[i];
 
-            // find remote row that matches, and subtract
+            // Find the remote row that matches and subtract.
+            // The returned row must be uniquely matched.
             final int j = right.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag,
                     entry.metered, entry.roaming, entry.defaultNetwork, i);
             if (j != -1) {
@@ -1208,64 +1246,46 @@
      * Return total statistics grouped by {@link #iface}; doesn't mutate the
      * original structure.
      * @hide
+     * @deprecated Use {@link #mapKeysNotNull(Function)} instead.
      */
+    @Deprecated
     public NetworkStats groupedByIface() {
-        final NetworkStats stats = new NetworkStats(elapsedRealtime, 10);
-
-        final Entry entry = new Entry();
-        entry.uid = UID_ALL;
-        entry.set = SET_ALL;
-        entry.tag = TAG_NONE;
-        entry.metered = METERED_ALL;
-        entry.roaming = ROAMING_ALL;
-        entry.defaultNetwork = DEFAULT_NETWORK_ALL;
-        entry.operations = 0L;
-
-        for (int i = 0; i < size; i++) {
-            // skip specific tags, since already counted in TAG_NONE
-            if (tag[i] != TAG_NONE) continue;
-
-            entry.iface = iface[i];
-            entry.rxBytes = rxBytes[i];
-            entry.rxPackets = rxPackets[i];
-            entry.txBytes = txBytes[i];
-            entry.txPackets = txPackets[i];
-            stats.combineValues(entry);
+        if (SdkLevel.isAtLeastV()) {
+            throw new UnsupportedOperationException("groupedByIface is not supported");
         }
+        // Keep backward compatibility where the method filtered out tagged stats and keep the
+        // operation counts as 0. The method used to deal with uid snapshot where tagged and
+        // non-tagged stats were mixed. And this method was also in Android O API list,
+        // so it is possible OEM can access it.
+        final Entry temp = new Entry();
+        final NetworkStats mappedStats = this.mapKeysNotNull(entry -> entry.getTag() != TAG_NONE
+                ? null : temp.setKeys(entry.getIface(), UID_ALL,
+                SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL));
 
-        return stats;
+        for (int i = 0; i < mappedStats.size; i++) {
+            mappedStats.operations[i] = 0L;
+        }
+        return mappedStats;
     }
 
     /**
      * Return total statistics grouped by {@link #uid}; doesn't mutate the
      * original structure.
      * @hide
+     * @deprecated Use {@link #mapKeysNotNull(Function)} instead.
      */
+    @Deprecated
     public NetworkStats groupedByUid() {
-        final NetworkStats stats = new NetworkStats(elapsedRealtime, 10);
-
-        final Entry entry = new Entry();
-        entry.iface = IFACE_ALL;
-        entry.set = SET_ALL;
-        entry.tag = TAG_NONE;
-        entry.metered = METERED_ALL;
-        entry.roaming = ROAMING_ALL;
-        entry.defaultNetwork = DEFAULT_NETWORK_ALL;
-
-        for (int i = 0; i < size; i++) {
-            // skip specific tags, since already counted in TAG_NONE
-            if (tag[i] != TAG_NONE) continue;
-
-            entry.uid = uid[i];
-            entry.rxBytes = rxBytes[i];
-            entry.rxPackets = rxPackets[i];
-            entry.txBytes = txBytes[i];
-            entry.txPackets = txPackets[i];
-            entry.operations = operations[i];
-            stats.combineValues(entry);
+        if (SdkLevel.isAtLeastV()) {
+            throw new UnsupportedOperationException("groupedByUid is not supported");
         }
-
-        return stats;
+        // Keep backward compatibility where the method filtered out tagged stats. The method used
+        // to deal with uid snapshot where tagged and non-tagged stats were mixed. And
+        // this method is also in Android O API list, so it is possible OEM can access it.
+        final Entry temp = new Entry();
+        return this.mapKeysNotNull(entry ->  entry.getTag() != TAG_NONE
+                ? null : temp.setKeys(IFACE_ALL, entry.getUid(),
+                SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL));
     }
 
     /**
@@ -1291,14 +1311,41 @@
     }
 
     /**
-     * Removes the interface name from all entries.
-     * This mutates the original structure in place.
+     * Returns a copy of this NetworkStats, replacing iface with IFACE_ALL in all entries.
+     *
      * @hide
      */
-    public void clearInterfaces() {
-        for (int i = 0; i < size; i++) {
-            iface[i] = null;
+    @NonNull
+    public NetworkStats withoutInterfaces() {
+        final Entry temp = new Entry();
+        return mapKeysNotNull(entry -> temp.setKeys(IFACE_ALL, entry.getUid(), entry.getSet(),
+                entry.getTag(), entry.getMetered(), entry.getRoaming(), entry.getDefaultNetwork()));
+    }
+
+    /**
+     * Returns a new NetworkStats object where entries are transformed.
+     *
+     * Note that because NetworkStats is more akin to a map than to a list,
+     * the entries will be grouped after they are mapped by the key fields,
+     * e.g. uid, set, tag, defaultNetwork.
+     * Only the key returned by the function is used ; values will be forcefully
+     * copied from the original entry. Entries that map to the same set of keys
+     * will be added together.
+     */
+    @NonNull
+    private NetworkStats mapKeysNotNull(@NonNull Function<Entry, Entry> f) {
+        final NetworkStats ret = new NetworkStats(0, 1);
+        for (Entry e : this) {
+            final NetworkStats.Entry transformed = f.apply(e);
+            if (transformed == null) continue;
+            if (transformed == e) {
+                throw new IllegalStateException("A new entry must be created.");
+            }
+            transformed.setValues(e.getRxBytes(), e.getRxPackets(), e.getTxBytes(),
+                    e.getTxPackets(), e.getOperations());
+            ret.combineValues(transformed);
         }
+        return ret;
     }
 
     /**
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 23902dc..7c9b3ec 100644
--- a/framework-t/src/android/net/NetworkStatsAccess.java
+++ b/framework-t/src/android/net/NetworkStatsAccess.java
@@ -23,6 +23,7 @@
 
 import android.Manifest;
 import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.app.AppOpsManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
@@ -109,7 +110,7 @@
 
     /** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
     public static @NetworkStatsAccess.Level int checkAccessLevel(
-            Context context, int callingPid, int callingUid, String callingPackage) {
+            Context context, int callingPid, int callingUid, @Nullable String callingPackage) {
         final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class);
         final TelephonyManager tm = (TelephonyManager)
                 context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -127,7 +128,7 @@
 
         final int appId = UserHandle.getAppId(callingUid);
 
-        final boolean isNetworkStack = PermissionUtils.checkAnyPermissionOf(
+        final boolean isNetworkStack = PermissionUtils.hasAnyPermissionOf(
                 context, callingPid, callingUid, android.Manifest.permission.NETWORK_STACK,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
 
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index 20c5f30..934b4c6 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -26,6 +26,7 @@
 import static android.net.NetworkStats.ROAMING_YES;
 import static android.net.NetworkStats.SET_ALL;
 import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
@@ -50,6 +51,7 @@
 import android.telephony.SubscriptionPlan;
 import android.text.format.DateUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
@@ -58,6 +60,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FileRotator;
+import com.android.modules.utils.FastDataInput;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.NetworkStatsUtils;
 
@@ -114,15 +117,28 @@
     private long mEndMillis;
     private long mTotalBytes;
     private boolean mDirty;
+    private final boolean mUseFastDataInput;
 
     /**
      * Construct a {@link NetworkStatsCollection} object.
      *
-     * @param bucketDuration duration of the buckets in this object, in milliseconds.
+     * @param bucketDurationMillis duration of the buckets in this object, in milliseconds.
      * @hide
      */
     public NetworkStatsCollection(long bucketDurationMillis) {
+        this(bucketDurationMillis, false /* useFastDataInput */);
+    }
+
+    /**
+     * Construct a {@link NetworkStatsCollection} object.
+     *
+     * @param bucketDurationMillis duration of the buckets in this object, in milliseconds.
+     * @param useFastDataInput true if using {@link FastDataInput} is preferred. Otherwise, false.
+     * @hide
+     */
+    public NetworkStatsCollection(long bucketDurationMillis, boolean useFastDataInput) {
         mBucketDurationMillis = bucketDurationMillis;
+        mUseFastDataInput = useFastDataInput;
         reset();
     }
 
@@ -481,7 +497,11 @@
     /** @hide */
     @Override
     public void read(InputStream in) throws IOException {
-        read((DataInput) new DataInputStream(in));
+        if (mUseFastDataInput) {
+            read(FastDataInput.obtain(in));
+        } else {
+            read((DataInput) new DataInputStream(in));
+        }
     }
 
     private void read(DataInput in) throws IOException {
@@ -924,6 +944,100 @@
         }
     }
 
+
+    private static String str(NetworkStatsCollection.Key key) {
+        StringBuilder sb = new StringBuilder()
+                .append(key.ident.toString())
+                .append(" uid=").append(key.uid);
+        if (key.set != SET_FOREGROUND) {
+            sb.append(" set=").append(key.set);
+        }
+        if (key.tag != 0) {
+            sb.append(" tag=").append(key.tag);
+        }
+        return sb.toString();
+    }
+
+    // The importer will modify some keys when importing them.
+    // In order to keep the comparison code simple, add such special cases here and simply
+    // ignore them. This should not impact fidelity much because the start/end checks and the total
+    // bytes check still need to pass.
+    private static boolean couldKeyChangeOnImport(NetworkStatsCollection.Key key) {
+        if (key.ident.isEmpty()) return false;
+        final NetworkIdentity firstIdent = key.ident.iterator().next();
+
+        // Non-mobile network with non-empty RAT type.
+        // This combination is invalid and the NetworkIdentity.Builder will throw if it is passed
+        // in, but it looks like it was previously possible to persist it to disk. The importer sets
+        // the RAT type to NETWORK_TYPE_ALL.
+        if (firstIdent.getType() != ConnectivityManager.TYPE_MOBILE
+                && firstIdent.getRatType() != NetworkTemplate.NETWORK_TYPE_ALL) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Compare two {@link NetworkStatsCollection} instances and returning a human-readable
+     * string description of difference for debugging purpose.
+     *
+     * @hide
+     */
+    @Nullable
+    public static String compareStats(NetworkStatsCollection migrated,
+                                      NetworkStatsCollection legacy, boolean allowKeyChange) {
+        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
+                migrated.getEntries();
+        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
+
+        final ArraySet<NetworkStatsCollection.Key> unmatchedLegKeys =
+                new ArraySet<>(legEntries.keySet());
+
+        for (NetworkStatsCollection.Key legKey : legEntries.keySet()) {
+            final NetworkStatsHistory legHistory = legEntries.get(legKey);
+            final NetworkStatsHistory migHistory = migEntries.get(legKey);
+
+            if (migHistory == null && allowKeyChange && couldKeyChangeOnImport(legKey)) {
+                unmatchedLegKeys.remove(legKey);
+                continue;
+            }
+
+            if (migHistory == null) {
+                return "Missing migrated history for legacy key " + str(legKey)
+                        + ", legacy history was " + legHistory;
+            }
+            if (!migHistory.isSameAs(legHistory)) {
+                return "Difference in history for key " + legKey + "; legacy history " + legHistory
+                        + ", migrated history " + migHistory;
+            }
+            unmatchedLegKeys.remove(legKey);
+        }
+
+        if (!unmatchedLegKeys.isEmpty()) {
+            final NetworkStatsHistory first = legEntries.get(unmatchedLegKeys.valueAt(0));
+            return "Found unmatched legacy keys: count=" + unmatchedLegKeys.size()
+                    + ", first unmatched collection " + first;
+        }
+
+        if (migrated.getStartMillis() != legacy.getStartMillis()
+                || migrated.getEndMillis() != legacy.getEndMillis()) {
+            return "Start / end of the collections "
+                    + migrated.getStartMillis() + "/" + legacy.getStartMillis() + " and "
+                    + migrated.getEndMillis() + "/" + legacy.getEndMillis()
+                    + " don't match";
+        }
+
+        if (migrated.getTotalBytes() != legacy.getTotalBytes()) {
+            return "Total bytes " + migrated.getTotalBytes() + " and " + legacy.getTotalBytes()
+                    + " don't match for collections with start/end "
+                    + migrated.getStartMillis()
+                    + "/" + legacy.getStartMillis();
+        }
+
+        return null;
+    }
+
     /**
      * the identifier that associate with the {@link NetworkStatsHistory} object to identify
      * a certain record in the {@link NetworkStatsCollection} object.
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index dc4ac55..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;
@@ -683,33 +684,34 @@
     /** {@hide} */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static long getMobileTcpRxPackets() {
-        long total = 0;
-        for (String iface : getMobileIfaces()) {
-            long stat = UNSUPPORTED;
-            try {
-                stat = getStatsService().getIfaceStats(iface, TYPE_TCP_RX_PACKETS);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
-            total += addIfSupported(stat);
-        }
-        return total;
+        return UNSUPPORTED;
     }
 
     /** {@hide} */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static long getMobileTcpTxPackets() {
-        long total = 0;
-        for (String iface : getMobileIfaces()) {
-            long stat = UNSUPPORTED;
-            try {
-                stat = getStatsService().getIfaceStats(iface, TYPE_TCP_TX_PACKETS);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
-            total += addIfSupported(stat);
+        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 total;
     }
 
     /**
@@ -1141,8 +1143,4 @@
     public static final int TYPE_TX_BYTES = 2;
     /** {@hide} */
     public static final int TYPE_TX_PACKETS = 3;
-    /** {@hide} */
-    public static final int TYPE_TCP_RX_PACKETS = 4;
-    /** {@hide} */
-    public static final int TYPE_TCP_TX_PACKETS = 5;
 }
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
new file mode 100644
index 0000000..6afb2d5
--- /dev/null
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -0,0 +1,241 @@
+/*
+ * 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.nsd;
+
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * Encapsulates parameters for {@link NsdManager#registerService}.
+ * @hide
+ */
+//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+public final class AdvertisingRequest implements Parcelable {
+
+    /**
+     * Only update the registration without sending exit and re-announcement.
+     */
+    public static final long NSD_ADVERTISING_UPDATE_ONLY = 1;
+
+
+    @NonNull
+    public static final Creator<AdvertisingRequest> CREATOR =
+            new Creator<>() {
+                @Override
+                public AdvertisingRequest createFromParcel(Parcel in) {
+                    final NsdServiceInfo serviceInfo = in.readParcelable(
+                            NsdServiceInfo.class.getClassLoader(), NsdServiceInfo.class);
+                    final int protocolType = in.readInt();
+                    final long advertiseConfig = in.readLong();
+                    final long ttlSeconds = in.readLong();
+                    final Duration ttl = ttlSeconds < 0 ? null : Duration.ofSeconds(ttlSeconds);
+                    return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig, ttl);
+                }
+
+                @Override
+                public AdvertisingRequest[] newArray(int size) {
+                    return new AdvertisingRequest[size];
+                }
+            };
+    @NonNull
+    private final NsdServiceInfo mServiceInfo;
+    private final int mProtocolType;
+    // Bitmask of @AdvertisingConfig flags. Uses a long to allow 64 possible flags in the future.
+    private final long mAdvertisingConfig;
+
+    @Nullable
+    private final Duration mTtl;
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(flag = true, prefix = {"NSD_ADVERTISING"}, value = {
+            NSD_ADVERTISING_UPDATE_ONLY,
+    })
+    @interface AdvertisingConfig {}
+
+    /**
+     * The constructor for the advertiseRequest
+     */
+    private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+            long advertisingConfig, @NonNull Duration ttl) {
+        mServiceInfo = serviceInfo;
+        mProtocolType = protocolType;
+        mAdvertisingConfig = advertisingConfig;
+        mTtl = ttl;
+    }
+
+    /**
+     * Returns the {@link NsdServiceInfo}
+     */
+    @NonNull
+    public NsdServiceInfo getServiceInfo() {
+        return mServiceInfo;
+    }
+
+    /**
+     * Returns the service advertise protocol
+     */
+    public int getProtocolType() {
+        return mProtocolType;
+    }
+
+    /**
+     * Returns the advertising config.
+     */
+    public long getAdvertisingConfig() {
+        return mAdvertisingConfig;
+    }
+
+    /**
+     * 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
+     */
+    // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+    @Nullable
+    public Duration getTtl() {
+        return mTtl;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("serviceInfo: ").append(mServiceInfo)
+                .append(", protocolType: ").append(mProtocolType)
+                .append(", advertisingConfig: ").append(mAdvertisingConfig)
+                .append(", ttl: ").append(mTtl);
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (!(other instanceof AdvertisingRequest)) {
+            return false;
+        } else {
+            final AdvertisingRequest otherRequest = (AdvertisingRequest) other;
+            return mServiceInfo.equals(otherRequest.mServiceInfo)
+                    && mProtocolType == otherRequest.mProtocolType
+                    && mAdvertisingConfig == otherRequest.mAdvertisingConfig
+                    && Objects.equals(mTtl, otherRequest.mTtl);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig, mTtl);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mServiceInfo, flags);
+        dest.writeInt(mProtocolType);
+        dest.writeLong(mAdvertisingConfig);
+        dest.writeLong(mTtl == null ? -1L : mTtl.getSeconds());
+    }
+
+//    @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+    /**
+     * The builder for creating new {@link AdvertisingRequest} objects.
+     * @hide
+     */
+    public static final class Builder {
+        @NonNull
+        private final NsdServiceInfo mServiceInfo;
+        private final int mProtocolType;
+        private long mAdvertisingConfig;
+        @Nullable
+        private Duration mTtl;
+        /**
+         * Creates a new {@link Builder} object.
+         */
+        public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) {
+            mServiceInfo = serviceInfo;
+            mProtocolType = protocolType;
+        }
+
+        /**
+         * Sets advertising configuration flags.
+         *
+         * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags.
+         */
+        @NonNull
+        public Builder setAdvertisingConfig(long advertisingConfigFlags) {
+            mAdvertisingConfig = advertisingConfigFlags;
+            return this;
+        }
+
+        /**
+         * Sets the time interval that the resource records may be cached on a DNS resolver.
+         *
+         * If this method is not called or {@code ttl} is {@code null}, default TTL values
+         * will be used for the service when it's registered. Otherwise, the {@code ttl}
+         * will be used for all resource records of this service.
+         *
+         * When registering a service, {@link NsdManager#FAILURE_BAD_PARAMETERS} will be returned
+         * if {@code ttl} is smaller than 30 seconds.
+         *
+         * 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
+         *
+         * @see AdvertisingRequest#getTtl
+         * @hide
+         */
+        // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+        @NonNull
+        public Builder setTtl(@Nullable Duration ttl) {
+            if (ttl == null) {
+                mTtl = null;
+                return this;
+            }
+            final long ttlSeconds = ttl.getSeconds();
+            if (ttlSeconds < 0 || ttlSeconds > 0xffffffffL) {
+                throw new IllegalArgumentException(
+                        "ttlSeconds exceeds the allowed range (value = " + ttlSeconds
+                                + ", allowedRanged = [0, 0xffffffffL])");
+            }
+            mTtl = Duration.ofSeconds(ttlSeconds);
+            return this;
+        }
+
+        /** Creates a new {@link AdvertisingRequest} object. */
+        @NonNull
+        public AdvertisingRequest build() {
+            return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig, mTtl);
+        }
+    }
+}
diff --git a/framework-t/src/android/net/nsd/DiscoveryRequest.java b/framework-t/src/android/net/nsd/DiscoveryRequest.java
new file mode 100644
index 0000000..b0b71ea
--- /dev/null
+++ b/framework-t/src/android/net/nsd/DiscoveryRequest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.nsd;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Network;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates parameters for {@link NsdManager#discoverServices}.
+ */
+@FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+public final class DiscoveryRequest implements Parcelable {
+    private final int mProtocolType;
+
+    @NonNull
+    private final String mServiceType;
+
+    @Nullable
+    private final String mSubtype;
+
+    @Nullable
+    private final Network mNetwork;
+
+    // TODO: add mDiscoveryConfig for more fine-grained discovery behavior control
+
+    @NonNull
+    public static final Creator<DiscoveryRequest> CREATOR =
+            new Creator<>() {
+                @Override
+                public DiscoveryRequest createFromParcel(Parcel in) {
+                    int protocolType = in.readInt();
+                    String serviceType = in.readString();
+                    String subtype = in.readString();
+                    Network network =
+                            in.readParcelable(Network.class.getClassLoader(), Network.class);
+                    return new DiscoveryRequest(protocolType, serviceType, subtype, network);
+                }
+
+                @Override
+                public DiscoveryRequest[] newArray(int size) {
+                    return new DiscoveryRequest[size];
+                }
+            };
+
+    private DiscoveryRequest(int protocolType, @NonNull String serviceType,
+            @Nullable String subtype, @Nullable Network network) {
+        mProtocolType = protocolType;
+        mServiceType = serviceType;
+        mSubtype = subtype;
+        mNetwork = network;
+    }
+
+    /**
+     * Returns the service type in format of dot-joint string of two labels.
+     *
+     * For example, "_ipp._tcp" for internet printer and "_matter._tcp" for <a
+     * href="https://csa-iot.org/all-solutions/matter">Matter</a> operational device.
+     */
+    @NonNull
+    public String getServiceType() {
+        return mServiceType;
+    }
+
+    /**
+     * Returns the subtype without the trailing "._sub" label or {@code null} if no subtype is
+     * specified.
+     *
+     * For example, the return value will be "_printer" for subtype "_printer._sub".
+     */
+    @Nullable
+    public String getSubtype() {
+        return mSubtype;
+    }
+
+    /**
+     * Returns the service discovery protocol.
+     *
+     * @hide
+     */
+    public int getProtocolType() {
+        return mProtocolType;
+    }
+
+    /**
+     * Returns the {@link Network} on which the query should be sent or {@code null} if no
+     * network is specified.
+     */
+    @Nullable
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(", protocolType: ").append(mProtocolType)
+            .append(", serviceType: ").append(mServiceType)
+            .append(", subtype: ").append(mSubtype)
+            .append(", network: ").append(mNetwork);
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (!(other instanceof DiscoveryRequest)) {
+            return false;
+        } else {
+            DiscoveryRequest otherRequest = (DiscoveryRequest) other;
+            return mProtocolType == otherRequest.mProtocolType
+                    && Objects.equals(mServiceType, otherRequest.mServiceType)
+                    && Objects.equals(mSubtype, otherRequest.mSubtype)
+                    && Objects.equals(mNetwork, otherRequest.mNetwork);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mProtocolType, mServiceType, mSubtype, mNetwork);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mProtocolType);
+        dest.writeString(mServiceType);
+        dest.writeString(mSubtype);
+        dest.writeParcelable(mNetwork, flags);
+    }
+
+    /** The builder for creating new {@link DiscoveryRequest} objects. */
+    public static final class Builder {
+        private final int mProtocolType;
+
+        @NonNull
+        private String mServiceType;
+
+        @Nullable
+        private String mSubtype;
+
+        @Nullable
+        private Network mNetwork;
+
+        /**
+         * Creates a new default {@link Builder} object with given service type.
+         *
+         * @throws IllegalArgumentException if {@code serviceType} is {@code null} or an empty
+         * string
+         */
+        public Builder(@NonNull String serviceType) {
+            this(NsdManager.PROTOCOL_DNS_SD, serviceType);
+        }
+
+        /** @hide */
+        public Builder(int protocolType, @NonNull String serviceType) {
+            NsdManager.checkProtocol(protocolType);
+            mProtocolType = protocolType;
+            setServiceType(serviceType);
+        }
+
+        /**
+         * Sets the service type to be discovered or {@code null} if no services should be queried.
+         *
+         * The {@code serviceType} must be a dot-joint string of two labels. For example,
+         * "_ipp._tcp" for internet printer. Additionally, the first label must start with
+         * underscore ('_') and the second label must be either "_udp" or "_tcp". Otherwise, {@link
+         * NsdManager#discoverServices} will fail with {@link NsdManager#FAILURE_BAD_PARAMETER}.
+         *
+         * @throws IllegalArgumentException if {@code serviceType} is {@code null} or an empty
+         * string
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setServiceType(@NonNull String serviceType) {
+            if (TextUtils.isEmpty(serviceType)) {
+                throw new IllegalArgumentException("Service type cannot be empty");
+            }
+            mServiceType = serviceType;
+            return this;
+        }
+
+        /**
+         * Sets the optional subtype of the services to be discovered.
+         *
+         * If a non-empty {@code subtype} is specified, it must start with underscore ('_') and
+         * have the trailing "._sub" removed. Otherwise, {@link NsdManager#discoverServices} will
+         * fail with {@link NsdManager#FAILURE_BAD_PARAMETER}. For example, {@code subtype} should
+         * be "_printer" for DNS name "_printer._sub._http._tcp". In this case, only services with
+         * this {@code subtype} will be queried, rather than all services of the base service type.
+         *
+         * Note that a non-empty service type must be specified with {@link #setServiceType} if a
+         * non-empty subtype is specified by this method.
+         */
+        @NonNull
+        public Builder setSubtype(@Nullable String subtype) {
+            mSubtype = subtype;
+            return this;
+        }
+
+        /**
+         * Sets the {@link Network} on which the discovery queries should be sent.
+         *
+         * @param network the discovery network or {@code null} if the query should be sent on
+         * all supported networks
+         */
+        @NonNull
+        public Builder setNetwork(@Nullable Network network) {
+            mNetwork = network;
+            return this;
+        }
+
+        /**
+         * Creates a new {@link DiscoveryRequest} object.
+         */
+        @NonNull
+        public DiscoveryRequest build() {
+            return new DiscoveryRequest(mProtocolType, mServiceType, mSubtype, mNetwork);
+        }
+    }
+}
diff --git a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
index d89bfa9..55820ec 100644
--- a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
+++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
@@ -17,6 +17,7 @@
 package android.net.nsd;
 
 import android.os.Messenger;
+import android.net.nsd.DiscoveryRequest;
 import android.net.nsd.NsdServiceInfo;
 
 /**
@@ -24,7 +25,7 @@
  * @hide
  */
 oneway interface INsdManagerCallback {
-    void onDiscoverServicesStarted(int listenerKey, in NsdServiceInfo info);
+    void onDiscoverServicesStarted(int listenerKey, in DiscoveryRequest discoveryRequest);
     void onDiscoverServicesFailed(int listenerKey, int error);
     void onServiceFound(int listenerKey, in NsdServiceInfo info);
     void onServiceLost(int listenerKey, in NsdServiceInfo info);
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index 5533154..9a31278 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -16,7 +16,10 @@
 
 package android.net.nsd;
 
+import android.net.nsd.AdvertisingRequest;
+import android.net.nsd.DiscoveryRequest;
 import android.net.nsd.INsdManagerCallback;
+import android.net.nsd.IOffloadEngine;
 import android.net.nsd.NsdServiceInfo;
 import android.os.Messenger;
 
@@ -26,13 +29,15 @@
  * {@hide}
  */
 interface INsdServiceConnector {
-    void registerService(int listenerKey, in NsdServiceInfo serviceInfo);
+    void registerService(int listenerKey, in AdvertisingRequest advertisingRequest);
     void unregisterService(int listenerKey);
-    void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo);
+    void discoverServices(int listenerKey, in DiscoveryRequest discoveryRequest);
     void stopDiscovery(int listenerKey);
     void resolveService(int listenerKey, in NsdServiceInfo serviceInfo);
     void startDaemon();
     void stopResolution(int listenerKey);
     void registerServiceInfoCallback(int listenerKey, in NsdServiceInfo serviceInfo);
     void unregisterServiceInfoCallback(int listenerKey);
-}
\ No newline at end of file
+    void registerOffloadEngine(String ifaceName, in IOffloadEngine cb, long offloadCapabilities, long offloadType);
+    void unregisterOffloadEngine(in IOffloadEngine cb);
+}
diff --git a/framework-t/src/android/net/nsd/IOffloadEngine.aidl b/framework-t/src/android/net/nsd/IOffloadEngine.aidl
new file mode 100644
index 0000000..2af733d
--- /dev/null
+++ b/framework-t/src/android/net/nsd/IOffloadEngine.aidl
@@ -0,0 +1,29 @@
+/**
+ * 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.nsd;
+
+import android.net.nsd.OffloadServiceInfo;
+
+/**
+ * Callbacks from NsdService to inform providers of packet offload.
+ *
+ * @hide
+ */
+oneway interface IOffloadEngine {
+    void onOffloadServiceUpdated(in OffloadServiceInfo info);
+    void onOffloadServiceRemoved(in OffloadServiceInfo info);
+}
diff --git a/framework-t/src/android/net/nsd/MDnsManager.java b/framework-t/src/android/net/nsd/MDnsManager.java
index c11e60c..c7ded25 100644
--- a/framework-t/src/android/net/nsd/MDnsManager.java
+++ b/framework-t/src/android/net/nsd/MDnsManager.java
@@ -51,7 +51,7 @@
     public void startDaemon() {
         try {
             mMdns.startDaemon();
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Start mdns failed.", e);
         }
     }
@@ -62,7 +62,7 @@
     public void stopDaemon() {
         try {
             mMdns.stopDaemon();
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Stop mdns failed.", e);
         }
     }
@@ -85,7 +85,7 @@
                 registrationType, port, txtRecord, interfaceIdx);
         try {
             mMdns.registerService(info);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Register service failed.", e);
             return false;
         }
@@ -105,7 +105,7 @@
                 registrationType, "" /* domainName */, interfaceIdx, NETID_UNSET);
         try {
             mMdns.discover(info);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Discover service failed.", e);
             return false;
         }
@@ -129,7 +129,7 @@
                 new byte[0] /* txtRecord */, interfaceIdx);
         try {
             mMdns.resolve(info);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Resolve service failed.", e);
             return false;
         }
@@ -149,7 +149,7 @@
                 "" /* address */, interfaceIdx, NETID_UNSET);
         try {
             mMdns.getServiceAddress(info);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Get service address failed.", e);
             return false;
         }
@@ -165,7 +165,7 @@
     public boolean stopOperation(int id) {
         try {
             mMdns.stopOperation(id);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Stop operation failed.", e);
             return false;
         }
@@ -180,7 +180,7 @@
     public void registerEventListener(@NonNull IMDnsEventListener listener) {
         try {
             mMdns.registerEventListener(listener);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Register listener failed.", e);
         }
     }
@@ -193,7 +193,7 @@
     public void unregisterEventListener(@NonNull IMDnsEventListener listener) {
         try {
             mMdns.unregisterEventListener(listener);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
             Log.e(TAG, "Unregister listener failed.", e);
         }
     }
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 2930cbd..b21e22a 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -16,24 +16,29 @@
 
 package android.net.nsd;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_STACK;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_PLATFORM_MDNS_BACKEND;
 import static android.net.connectivity.ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.app.compat.CompatChanges;
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityThread;
 import android.net.Network;
 import android.net.NetworkRequest;
 import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
@@ -41,15 +46,21 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.Pair;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.CollectionUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * The Network Service Discovery Manager class provides the API to discover services
@@ -137,6 +148,70 @@
     private static final String TAG = NsdManager.class.getSimpleName();
     private static final boolean DBG = false;
 
+    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+    // available here
+    /** @hide */
+    public static class Flags {
+        static final String REGISTER_NSD_OFFLOAD_ENGINE_API =
+                "com.android.net.flags.register_nsd_offload_engine_api";
+        static final String NSD_SUBTYPES_SUPPORT_ENABLED =
+                "com.android.net.flags.nsd_subtypes_support_enabled";
+        static final String ADVERTISE_REQUEST_API =
+                "com.android.net.flags.advertise_request_api";
+        static final String NSD_CUSTOM_HOSTNAME_ENABLED =
+                "com.android.net.flags.nsd_custom_hostname_enabled";
+        static final String NSD_CUSTOM_TTL_ENABLED =
+                "com.android.net.flags.nsd_custom_ttl_enabled";
+    }
+
+    /**
+     * A regex for the acceptable format of a type or subtype label.
+     * @hide
+     */
+    public static final String TYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
+    /**
+     * A regex for the acceptable format of a subtype label.
+     *
+     * As per RFC 6763 7.1, "Subtype strings are not required to begin with an underscore, though
+     * they often do.", and "Subtype strings [...] may be constructed using arbitrary 8-bit data
+     * values.  In many cases these data values may be UTF-8 [RFC3629] representations of text, or
+     * even (as in the example above) plain ASCII [RFC20], but they do not have to be.".
+     *
+     * This regex is overly conservative as it mandates the underscore and only allows printable
+     * ASCII characters (codes 0x20 to 0x7e, space to tilde), except for comma (0x2c) and dot
+     * (0x2e); so the NsdManager API does not allow everything the RFC allows. This may be revisited
+     * in the future, but using arbitrary bytes makes logging and testing harder, and using other
+     * characters would probably be a bad idea for interoperability for apps.
+     * @hide
+     */
+    public static final String SUBTYPE_LABEL_REGEX = "_["
+            + "\\x20-\\x2b"
+            + "\\x2d"
+            + "\\x2f-\\x7e"
+            + "]{1,62}";
+
+    /**
+     * A regex for the acceptable format of a service type specification.
+     *
+     * When it matches, matcher group 1 is an optional leading subtype when using legacy dot syntax
+     * (_subtype._type._tcp). Matcher group 2 is the actual type, and matcher group 3 contains
+     * optional comma-separated subtypes.
+     * @hide
+     */
+    public static final String TYPE_REGEX =
+            // Optional leading subtype (_subtype._type._tcp)
+            // (?: xxx) is a non-capturing parenthesis, don't capture the dot
+            "^(?:(" + SUBTYPE_LABEL_REGEX + ")\\.)?"
+                    // Actual type (_type._tcp.local)
+                    + "(" + TYPE_LABEL_REGEX + "\\._(?:tcp|udp))"
+                    // Drop '.' at the end of service type that is compatible with old backend.
+                    // e.g. allow "_type._tcp.local."
+                    + "\\.?"
+                    // Optional subtype after comma, for "_type._tcp,_subtype1,_subtype2" format
+                    + "((?:," + SUBTYPE_LABEL_REGEX + ")*)"
+                    + "$";
+
     /**
      * Broadcast intent action to indicate whether network service discovery is
      * enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
@@ -246,10 +321,28 @@
     public static final int UNREGISTER_SERVICE_CALLBACK             = 31;
     /** @hide */
     public static final int UNREGISTER_SERVICE_CALLBACK_SUCCEEDED   = 32;
+    /** @hide */
+    public static final int REGISTER_OFFLOAD_ENGINE                 = 33;
+    /** @hide */
+    public static final int UNREGISTER_OFFLOAD_ENGINE               = 34;
 
     /** Dns based service discovery protocol */
     public static final int PROTOCOL_DNS_SD = 0x0001;
 
+    /**
+     * The minimum TTL seconds which is allowed for a service registration.
+     *
+     * @hide
+     */
+    public static final long TTL_SECONDS_MIN = 30L;
+
+    /**
+     * The maximum TTL seconds which is allowed for a service registration.
+     *
+     * @hide
+     */
+    public static final long TTL_SECONDS_MAX = 10 * 3600L;
+
     private static final SparseArray<String> EVENT_NAMES = new SparseArray<>();
     static {
         EVENT_NAMES.put(DISCOVER_SERVICES, "DISCOVER_SERVICES");
@@ -296,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;
@@ -306,6 +400,8 @@
     @GuardedBy("mMapLock")
     private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>();
     @GuardedBy("mMapLock")
+    private final SparseArray<DiscoveryRequest> mDiscoveryMap = new SparseArray<>();
+    @GuardedBy("mMapLock")
     private final SparseArray<Executor> mExecutorMap = new SparseArray<>();
     private final Object mMapLock = new Object();
     // Map of listener key sent by client -> per-network discovery tracker
@@ -313,8 +409,109 @@
     private final ArrayMap<Integer, PerNetworkDiscoveryTracker>
             mPerNetworkDiscoveryMap = new ArrayMap<>();
 
+    @GuardedBy("mOffloadEngines")
+    private final ArrayList<OffloadEngineProxy> mOffloadEngines = new ArrayList<>();
     private final ServiceHandler mHandler;
 
+    private static class OffloadEngineProxy extends IOffloadEngine.Stub {
+        private final Executor mExecutor;
+        private final OffloadEngine mEngine;
+
+        private OffloadEngineProxy(@NonNull Executor executor, @NonNull OffloadEngine appCb) {
+            mExecutor = executor;
+            mEngine = appCb;
+        }
+
+        @Override
+        public void onOffloadServiceUpdated(OffloadServiceInfo info) {
+            mExecutor.execute(() -> mEngine.onOffloadServiceUpdated(info));
+        }
+
+        @Override
+        public void onOffloadServiceRemoved(OffloadServiceInfo info) {
+            mExecutor.execute(() -> mEngine.onOffloadServiceRemoved(info));
+        }
+    }
+
+    /**
+     * Registers an OffloadEngine with NsdManager.
+     *
+     * A caller can register itself as an OffloadEngine if it supports mDns hardware offload.
+     * The caller must implement the {@link OffloadEngine} interface and update hardware offload
+     * state property when the {@link OffloadEngine#onOffloadServiceUpdated} and
+     * {@link OffloadEngine#onOffloadServiceRemoved} callback are called. Multiple engines may be
+     * registered for the same interface, and that the same engine cannot be registered twice.
+     *
+     * @param ifaceName  indicates which network interface the hardware offload runs on
+     * @param offloadType    the type of offload that the offload engine support
+     * @param offloadCapability    the capabilities of the offload engine
+     * @param executor   the executor on which to receive the offload callbacks
+     * @param engine     the OffloadEngine that will receive the offload callbacks
+     * @throws IllegalStateException if the engine is already registered.
+     *
+     * @hide
+     */
+    @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
+    @SystemApi
+    @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
+            NETWORK_STACK})
+    public void registerOffloadEngine(@NonNull String ifaceName,
+            @OffloadEngine.OffloadType long offloadType,
+            @OffloadEngine.OffloadCapability long offloadCapability, @NonNull Executor executor,
+            @NonNull OffloadEngine engine) {
+        Objects.requireNonNull(ifaceName);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(engine);
+        final OffloadEngineProxy cbImpl = new OffloadEngineProxy(executor, engine);
+        synchronized (mOffloadEngines) {
+            if (CollectionUtils.contains(mOffloadEngines, impl -> impl.mEngine == engine)) {
+                throw new IllegalStateException("This engine is already registered");
+            }
+            mOffloadEngines.add(cbImpl);
+        }
+        try {
+            mService.registerOffloadEngine(ifaceName, cbImpl, offloadCapability, offloadType);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
+     * Unregisters an OffloadEngine from NsdService.
+     *
+     * A caller can unregister itself as an OffloadEngine when it doesn't want to receive the
+     * callback anymore. The OffloadEngine must have been previously registered with the system
+     * using the {@link NsdManager#registerOffloadEngine} method.
+     *
+     * @param engine OffloadEngine object to be removed from NsdService
+     * @throws IllegalStateException if the engine is not registered.
+     *
+     * @hide
+     */
+    @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
+    @SystemApi
+    @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
+            NETWORK_STACK})
+    public void unregisterOffloadEngine(@NonNull OffloadEngine engine) {
+        Objects.requireNonNull(engine);
+        final OffloadEngineProxy cbImpl;
+        synchronized (mOffloadEngines) {
+            final int index = CollectionUtils.indexOf(mOffloadEngines,
+                    impl -> impl.mEngine == engine);
+            if (index < 0) {
+                throw new IllegalStateException("This engine is not registered");
+            }
+            cbImpl = mOffloadEngines.remove(index);
+        }
+
+        try {
+            mService.unregisterOffloadEngine(cbImpl);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
     private class PerNetworkDiscoveryTracker {
         final String mServiceType;
         final int mProtocolType;
@@ -523,10 +720,9 @@
      */
     public NsdManager(Context context, INsdManager service) {
         mContext = context;
-
-        HandlerThread t = new HandlerThread("NsdManager");
-        t.start();
-        mHandler = new ServiceHandler(t.getLooper());
+        // Use a common singleton thread ConnectivityThread to be shared among all nsd tasks.
+        // Instead of launching separate threads to handle tasks from the various instances.
+        mHandler = new ServiceHandler(ConnectivityThread.getInstanceLooper());
 
         try {
             mService = service.connect(new NsdCallbackImpl(mHandler), CompatChanges.isChangeEnabled(
@@ -535,9 +731,12 @@
             throw new RuntimeException("Failed to connect to NsdService");
         }
 
-        // Only proactively start the daemon if the target SDK < S, otherwise the internal service
-        // would automatically start/stop the native daemon as needed.
-        if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)) {
+        // Only proactively start the daemon if the target SDK < S AND platform < V, For target
+        // SDK >= S AND platform < V, the internal service would automatically start/stop the native
+        // daemon as needed. For platform >= V, no action is required because the native daemon is
+        // completely removed.
+        if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
+                && !SdkLevel.isAtLeastV()) {
             try {
                 mService.startDaemon();
             } catch (RemoteException e) {
@@ -558,6 +757,12 @@
             mServHandler.sendMessage(mServHandler.obtainMessage(message, 0, listenerKey, info));
         }
 
+        private void sendDiscoveryRequest(
+                int message, int listenerKey, DiscoveryRequest discoveryRequest) {
+            mServHandler.sendMessage(
+                    mServHandler.obtainMessage(message, 0, listenerKey, discoveryRequest));
+        }
+
         private void sendError(int message, int listenerKey, int error) {
             mServHandler.sendMessage(mServHandler.obtainMessage(message, error, listenerKey));
         }
@@ -567,8 +772,8 @@
         }
 
         @Override
-        public void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
-            sendInfo(DISCOVER_SERVICES_STARTED, listenerKey, info);
+        public void onDiscoverServicesStarted(int listenerKey, DiscoveryRequest discoveryRequest) {
+            sendDiscoveryRequest(DISCOVER_SERVICES_STARTED, listenerKey, discoveryRequest);
         }
 
         @Override
@@ -846,10 +1051,12 @@
             final Object obj = message.obj;
             final Object listener;
             final NsdServiceInfo ns;
+            final DiscoveryRequest discoveryRequest;
             final Executor executor;
             synchronized (mMapLock) {
                 listener = mListenerMap.get(key);
                 ns = mServiceMap.get(key);
+                discoveryRequest = mDiscoveryMap.get(key);
                 executor = mExecutorMap.get(key);
             }
             if (listener == null) {
@@ -857,17 +1064,22 @@
                 return;
             }
             if (DBG) {
-                Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
+                if (discoveryRequest != null) {
+                    Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", discovery "
+                            + discoveryRequest);
+                } else {
+                    Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
+                }
             }
             switch (what) {
                 case DISCOVER_SERVICES_STARTED:
-                    final String s = getNsdServiceInfoType((NsdServiceInfo) obj);
+                    final String s = getNsdServiceInfoType((DiscoveryRequest) obj);
                     executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStarted(s));
                     break;
                 case DISCOVER_SERVICES_FAILED:
                     removeListener(key);
                     executor.execute(() -> ((DiscoveryListener) listener).onStartDiscoveryFailed(
-                            getNsdServiceInfoType(ns), errorCode));
+                            getNsdServiceInfoType(discoveryRequest), errorCode));
                     break;
                 case SERVICE_FOUND:
                     executor.execute(() -> ((DiscoveryListener) listener).onServiceFound(
@@ -882,12 +1094,12 @@
                     // the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED
                     removeListener(key);
                     executor.execute(() -> ((DiscoveryListener) listener).onStopDiscoveryFailed(
-                            getNsdServiceInfoType(ns), errorCode));
+                            getNsdServiceInfoType(discoveryRequest), errorCode));
                     break;
                 case STOP_DISCOVERY_SUCCEEDED:
                     removeListener(key);
                     executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStopped(
-                            getNsdServiceInfoType(ns)));
+                            getNsdServiceInfoType(discoveryRequest)));
                     break;
                 case REGISTER_SERVICE_FAILED:
                     removeListener(key);
@@ -960,17 +1172,39 @@
         return mListenerKey;
     }
 
-    // Assert that the listener is not in the map, then add it and returns its key
-    private int putListener(Object listener, Executor e, NsdServiceInfo s) {
-        checkListener(listener);
-        final int key;
+    private int putListener(Object listener, Executor e, NsdServiceInfo serviceInfo) {
         synchronized (mMapLock) {
-            int valueIndex = mListenerMap.indexOfValue(listener);
+            return putListener(listener, e, mServiceMap, serviceInfo);
+        }
+    }
+
+    private int putListener(Object listener, Executor e, DiscoveryRequest discoveryRequest) {
+        synchronized (mMapLock) {
+            return putListener(listener, e, mDiscoveryMap, discoveryRequest);
+        }
+    }
+
+    // Assert that the listener is not in the map, then add it and returns its key
+    private <T> int putListener(Object listener, Executor e, SparseArray<T> map, T value) {
+        synchronized (mMapLock) {
+            checkListener(listener);
+            final int key;
+            final int valueIndex = mListenerMap.indexOfValue(listener);
             if (valueIndex != -1) {
                 throw new IllegalArgumentException("listener already in use");
             }
             key = nextListenerKey();
             mListenerMap.put(key, listener);
+            map.put(key, value);
+            mExecutorMap.put(key, e);
+            return key;
+        }
+    }
+
+    private int updateRegisteredListener(Object listener, Executor e, NsdServiceInfo s) {
+        final int key;
+        synchronized (mMapLock) {
+            key = getListenerKey(listener);
             mServiceMap.put(key, s);
             mExecutorMap.put(key, e);
         }
@@ -981,6 +1215,7 @@
         synchronized (mMapLock) {
             mListenerMap.remove(key);
             mServiceMap.remove(key);
+            mDiscoveryMap.remove(key);
             mExecutorMap.remove(key);
         }
     }
@@ -996,9 +1231,9 @@
         }
     }
 
-    private static String getNsdServiceInfoType(NsdServiceInfo s) {
-        if (s == null) return "?";
-        return s.getServiceType();
+    private static String getNsdServiceInfoType(DiscoveryRequest r) {
+        if (r == null) return "?";
+        return r.getServiceType();
     }
 
     /**
@@ -1041,14 +1276,111 @@
      */
     public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType,
             @NonNull Executor executor, @NonNull RegistrationListener listener) {
-        if (serviceInfo.getPort() <= 0) {
-            throw new IllegalArgumentException("Invalid port number");
-        }
-        checkServiceInfo(serviceInfo);
+        checkServiceInfoForRegistration(serviceInfo);
         checkProtocol(protocolType);
-        int key = putListener(listener, executor, serviceInfo);
+        final AdvertisingRequest.Builder builder = new AdvertisingRequest.Builder(serviceInfo,
+                protocolType);
+        // Optionally assume that the request is an update request if it uses subtypes and the same
+        // listener. This is not documented behavior as support for advertising subtypes via
+        // "_servicename,_sub1,_sub2" has never been documented in the first place, and using
+        // multiple subtypes was broken in T until a later module update. Subtype registration is
+        // documented in the NsdServiceInfo.setSubtypes API instead, but this provides a limited
+        // option for users of the older undocumented behavior, only for subtype changes.
+        if (isSubtypeUpdateRequest(serviceInfo, listener)) {
+            builder.setAdvertisingConfig(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
+        }
+        registerService(builder.build(), executor, listener);
+    }
+
+    private boolean isSubtypeUpdateRequest(@NonNull NsdServiceInfo serviceInfo, @NonNull
+            RegistrationListener listener) {
+        // If the listener is the same object, serviceInfo is for the same service name and
+        // type (outside of subtypes), and either of them use subtypes, treat the request as a
+        // subtype update request.
+        synchronized (mMapLock) {
+            int valueIndex = mListenerMap.indexOfValue(listener);
+            if (valueIndex == -1) {
+                return false;
+            }
+            final int key = mListenerMap.keyAt(valueIndex);
+            NsdServiceInfo existingService = mServiceMap.get(key);
+            if (existingService == null) {
+                return false;
+            }
+            final Pair<String, String> existingTypeSubtype = getTypeAndSubtypes(
+                    existingService.getServiceType());
+            final Pair<String, String> newTypeSubtype = getTypeAndSubtypes(
+                    serviceInfo.getServiceType());
+            if (existingTypeSubtype == null || newTypeSubtype == null) {
+                return false;
+            }
+            final boolean existingHasNoSubtype = TextUtils.isEmpty(existingTypeSubtype.second);
+            final boolean updatedHasNoSubtype = TextUtils.isEmpty(newTypeSubtype.second);
+            if (existingHasNoSubtype && updatedHasNoSubtype) {
+                // Only allow subtype changes when subtypes are used. This ensures that this
+                // behavior does not affect most requests.
+                return false;
+            }
+
+            return Objects.equals(existingService.getServiceName(), serviceInfo.getServiceName())
+                    && Objects.equals(existingTypeSubtype.first, newTypeSubtype.first);
+        }
+    }
+
+    /**
+     * Get the base type from a type specification with "_type._tcp,sub1,sub2" syntax.
+     *
+     * <p>This rejects specifications using dot syntax to specify subtypes ("_sub1._type._tcp").
+     *
+     * @return Type and comma-separated list of subtypes, or null if invalid format.
+     */
+    @Nullable
+    private static Pair<String, String> getTypeAndSubtypes(@Nullable String typeWithSubtype) {
+        if (typeWithSubtype == null) {
+            return null;
+        }
+        final Matcher matcher = Pattern.compile(TYPE_REGEX).matcher(typeWithSubtype);
+        if (!matcher.matches()) return null;
+        // Reject specifications using leading subtypes with a dot
+        if (!TextUtils.isEmpty(matcher.group(1))) return null;
+        return new Pair<>(matcher.group(2), matcher.group(3));
+    }
+
+    /**
+     * Register a service to be discovered by other services.
+     *
+     * <p> The function call immediately returns after sending a request to register service
+     * to the framework. The application is notified of a successful registration
+     * through the callback {@link RegistrationListener#onServiceRegistered} or a failure
+     * through {@link RegistrationListener#onRegistrationFailed}.
+     *
+     * <p> The application should call {@link #unregisterService} when the service
+     * registration is no longer required, and/or whenever the application is stopped.
+     * @param  advertisingRequest service being registered
+     * @param executor Executor to run listener callbacks with
+     * @param listener The listener notifies of a successful registration and is used to
+     * unregister this service through a call on {@link #unregisterService}. Cannot be null.
+     *
+     * @hide
+     */
+//    @FlaggedApi(Flags.ADVERTISE_REQUEST_API)
+    public void registerService(@NonNull AdvertisingRequest advertisingRequest,
+            @NonNull Executor executor,
+            @NonNull RegistrationListener listener) {
+        final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
+        final int protocolType = advertisingRequest.getProtocolType();
+        checkServiceInfoForRegistration(serviceInfo);
+        checkProtocol(protocolType);
+        final int key;
+        // For update only request, the old listener has to be reused
+        if ((advertisingRequest.getAdvertisingConfig()
+                & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) {
+            key = updateRegisteredListener(listener, executor, serviceInfo);
+        } else {
+            key = putListener(listener, executor, serviceInfo);
+        }
         try {
-            mService.registerService(key, serviceInfo);
+            mService.registerService(key, advertisingRequest);
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
@@ -1142,15 +1474,44 @@
         if (TextUtils.isEmpty(serviceType)) {
             throw new IllegalArgumentException("Service type cannot be empty");
         }
-        checkProtocol(protocolType);
+        DiscoveryRequest request = new DiscoveryRequest.Builder(protocolType, serviceType)
+                .setNetwork(network).build();
+        discoverServices(request, executor, listener);
+    }
 
-        NsdServiceInfo s = new NsdServiceInfo();
-        s.setServiceType(serviceType);
-        s.setNetwork(network);
-
-        int key = putListener(listener, executor, s);
+    /**
+     * Initiates service discovery to browse for instances of a service type. Service discovery
+     * consumes network bandwidth and will continue until the application calls
+     * {@link #stopServiceDiscovery}.
+     *
+     * <p> The function call immediately returns after sending a request to start service
+     * discovery to the framework. The application is notified of a success to initiate
+     * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure
+     * through {@link DiscoveryListener#onStartDiscoveryFailed}.
+     *
+     * <p> Upon successful start, application is notified when a service is found with
+     * {@link DiscoveryListener#onServiceFound} or when a service is lost with
+     * {@link DiscoveryListener#onServiceLost}.
+     *
+     * <p> Upon failure to start, service discovery is not active and application does
+     * not need to invoke {@link #stopServiceDiscovery}
+     *
+     * <p> The application should call {@link #stopServiceDiscovery} when discovery of this
+     * service type is no longer required, and/or whenever the application is paused or
+     * stopped.
+     *
+     * @param discoveryRequest the {@link DiscoveryRequest} object which specifies the discovery
+     * parameters such as service type, subtype and network
+     * @param executor Executor to run listener callbacks with
+     * @param listener  The listener notifies of a successful discovery and is used
+     * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
+     */
+    @FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    public void discoverServices(@NonNull DiscoveryRequest discoveryRequest,
+            @NonNull Executor executor, @NonNull DiscoveryListener listener) {
+        int key = putListener(listener, executor, discoveryRequest);
         try {
-            mService.discoverServices(key, s);
+            mService.discoverServices(key, discoveryRequest);
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
@@ -1201,12 +1562,10 @@
             throw new IllegalArgumentException("Service type cannot be empty");
         }
         Objects.requireNonNull(networkRequest, "NetworkRequest cannot be null");
-        checkProtocol(protocolType);
+        DiscoveryRequest discoveryRequest =
+                new DiscoveryRequest.Builder(protocolType, serviceType).build();
 
-        NsdServiceInfo s = new NsdServiceInfo();
-        s.setServiceType(serviceType);
-
-        final int baseListenerKey = putListener(listener, executor, s);
+        final int baseListenerKey = putListener(listener, executor, discoveryRequest);
 
         final PerNetworkDiscoveryTracker discoveryInfo = new PerNetworkDiscoveryTracker(
                 serviceType, protocolType, executor, listener);
@@ -1287,7 +1646,7 @@
     @Deprecated
     public void resolveService(@NonNull NsdServiceInfo serviceInfo,
             @NonNull Executor executor, @NonNull ResolveListener listener) {
-        checkServiceInfo(serviceInfo);
+        checkServiceInfoForResolution(serviceInfo);
         int key = putListener(listener, executor, serviceInfo);
         try {
             mService.resolveService(key, serviceInfo);
@@ -1338,9 +1697,10 @@
      * @param executor Executor to run callbacks with
      * @param listener to receive callback upon service update
      */
+    // TODO: use {@link DiscoveryRequest} to specify the service to be subscribed
     public void registerServiceInfoCallback(@NonNull NsdServiceInfo serviceInfo,
             @NonNull Executor executor, @NonNull ServiceInfoCallback listener) {
-        checkServiceInfo(serviceInfo);
+        checkServiceInfoForResolution(serviceInfo);
         int key = putListener(listener, executor, serviceInfo);
         try {
             mService.registerServiceInfoCallback(key, serviceInfo);
@@ -1379,13 +1739,13 @@
         Objects.requireNonNull(listener, "listener cannot be null");
     }
 
-    private static void checkProtocol(int protocolType) {
+    static void checkProtocol(int protocolType) {
         if (protocolType != PROTOCOL_DNS_SD) {
             throw new IllegalArgumentException("Unsupported protocol");
         }
     }
 
-    private static void checkServiceInfo(NsdServiceInfo serviceInfo) {
+    private static void checkServiceInfoForResolution(NsdServiceInfo serviceInfo) {
         Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
         if (TextUtils.isEmpty(serviceInfo.getServiceName())) {
             throw new IllegalArgumentException("Service name cannot be empty");
@@ -1394,4 +1754,133 @@
             throw new IllegalArgumentException("Service type cannot be empty");
         }
     }
+
+    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.
+     *
+     * <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.
+     *
+     * <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");
+
+        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 (publicKeyValidation == PublicKeyValidationType.NO_KEY) {
+            if (serviceValidation == ServiceValidationType.HAS_SERVICE_ZERO_PORT) {
+                throw new IllegalArgumentException("The port is missing");
+            }
+            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 caeecdd..d8cccb2 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,6 +16,9 @@
 
 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;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -24,6 +27,7 @@
 import android.os.Parcelable;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.net.module.util.InetAddressUtils;
@@ -31,10 +35,14 @@
 import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
 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
@@ -44,30 +52,67 @@
 
     private static final String TAG = "NsdServiceInfo";
 
+    @Nullable
     private String mServiceName;
 
+    @Nullable
     private String mServiceType;
 
-    private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
+    private final Set<String> mSubtypes;
 
-    private final List<InetAddress> mHostAddresses = new ArrayList<>();
+    private final ArrayMap<String, byte[]> mTxtRecord;
+
+    private final List<InetAddress> mHostAddresses;
+
+    @Nullable
+    private String mHostname;
 
     private int mPort;
 
     @Nullable
+    private byte[] mPublicKey;
+
+    @Nullable
     private Network mNetwork;
 
     private int mInterfaceIndex;
 
+    // The timestamp that one or more resource records associated with this service are considered
+    // invalid.
+    @Nullable
+    private Instant mExpirationTime;
+
     public NsdServiceInfo() {
+        mSubtypes = new ArraySet<>();
+        mTxtRecord = new ArrayMap<>();
+        mHostAddresses = new ArrayList<>();
     }
 
     /** @hide */
     public NsdServiceInfo(String sn, String rt) {
+        this();
         mServiceName = sn;
         mServiceType = rt;
     }
 
+    /**
+     * Creates a copy of {@code other}.
+     *
+     * @hide
+     */
+    public NsdServiceInfo(@NonNull NsdServiceInfo other) {
+        mServiceName = other.getServiceName();
+        mServiceType = other.getServiceType();
+        mSubtypes = new ArraySet<>(other.getSubtypes());
+        mTxtRecord = new ArrayMap<>(other.mTxtRecord);
+        mHostAddresses = new ArrayList<>(other.getHostAddresses());
+        mHostname = other.getHostname();
+        mPort = other.getPort();
+        mNetwork = other.getNetwork();
+        mInterfaceIndex = other.getInterfaceIndex();
+        mExpirationTime = other.getExpirationTime();
+    }
+
     /** Get the service name */
     public String getServiceName() {
         return mServiceName;
@@ -135,13 +180,94 @@
         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);
     }
 
     /**
+     * Get the hostname.
+     *
+     * <p>When a service is resolved, it returns the hostname of the resolved service . The top
+     * level domain ".local." is omitted.
+     *
+     * <p>For example, it returns "MyHost" when the service's hostname is "MyHost.local.".
+     *
+     * @hide
+     */
+//    @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
+    @Nullable
+    public String getHostname() {
+        return mHostname;
+    }
+
+    /**
+     * Set a custom hostname for this service instance for registration.
+     *
+     * <p>A hostname must be in ".local." domain. The ".local." must be omitted when calling this
+     * method.
+     *
+     * <p>For example, you should call setHostname("MyHost") to use the hostname "MyHost.local.".
+     *
+     * <p>If a hostname is set with this method, the addresses set with {@link #setHostAddresses}
+     * will be registered with the hostname.
+     *
+     * <p>If the hostname is null (which is the default for a new {@link NsdServiceInfo}), a random
+     * hostname is used and the addresses of this device will be registered.
+     *
+     * @hide
+     */
+//    @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
+    public void setHostname(@Nullable String hostname) {
+        mHostname = hostname;
+    }
+
+    /**
+     * 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.
@@ -391,20 +517,125 @@
         mInterfaceIndex = interfaceIndex;
     }
 
+    /**
+     * Sets the subtypes to be advertised for this service instance.
+     *
+     * The elements in {@code subtypes} should be the subtype identifiers which have the trailing
+     * "._sub" removed. For example, the subtype should be "_printer" for
+     * "_printer._sub._http._tcp.local".
+     *
+     * Only one subtype will be registered if multiple elements of {@code subtypes} have the same
+     * case-insensitive value.
+     */
+    @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    public void setSubtypes(@NonNull Set<String> subtypes) {
+        mSubtypes.clear();
+        mSubtypes.addAll(subtypes);
+    }
+
+    /**
+     * Returns subtypes of this service instance.
+     *
+     * When this object is returned by the service discovery/browse APIs (etc. {@link
+     * NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this
+     * service.
+     */
+    @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    @NonNull
+    public Set<String> getSubtypes() {
+        return Collections.unmodifiableSet(mSubtypes);
+    }
+
+    /**
+     * Sets the timestamp after when this service is expired.
+     *
+     * 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
+     */
+    public void setExpirationTime(@Nullable Instant expirationTime) {
+        if (expirationTime == null) {
+            mExpirationTime = null;
+        } else {
+            mExpirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
+        }
+    }
+
+    /**
+     * Returns the timestamp after when this service is expired or {@code null} if it's unknown.
+     *
+     * A service is considered expired if any of its DNS record is expired.
+     *
+     * Clients that are depending on the refreshness of the service information should not continue
+     * use this service after the returned timestamp. Instead, clients may re-send queries for the
+     * service to get updated the service information.
+     *
+     * @hide
+     */
+    // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+    @Nullable
+    public Instant getExpirationTime() {
+        return mExpirationTime;
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
         sb.append("name: ").append(mServiceName)
                 .append(", type: ").append(mServiceType)
+                .append(", subtypes: ").append(TextUtils.join(", ", mSubtypes))
                 .append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
+                .append(", hostname: ").append(mHostname)
                 .append(", port: ").append(mPort)
-                .append(", network: ").append(mNetwork);
+                .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;
@@ -414,6 +645,7 @@
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeString(mServiceName);
         dest.writeString(mServiceType);
+        dest.writeStringList(new ArrayList<>(mSubtypes));
         dest.writeInt(mPort);
 
         // TXT record key/value pairs.
@@ -436,6 +668,9 @@
         for (InetAddress address : mHostAddresses) {
             InetAddressUtils.parcelInetAddress(dest, address, flags);
         }
+        dest.writeString(mHostname);
+        dest.writeLong(mExpirationTime != null ? mExpirationTime.getEpochSecond() : -1);
+        dest.writeByteArray(mPublicKey);
     }
 
     /** Implement the Parcelable interface */
@@ -445,6 +680,7 @@
                 NsdServiceInfo info = new NsdServiceInfo();
                 info.mServiceName = in.readString();
                 info.mServiceType = in.readString();
+                info.setSubtypes(new ArraySet<>(in.createStringArrayList()));
                 info.mPort = in.readInt();
 
                 // TXT record key/value pairs.
@@ -464,6 +700,10 @@
                 for (int i = 0; i < size; i++) {
                     info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
                 }
+                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-t/src/android/net/nsd/OffloadEngine.java b/framework-t/src/android/net/nsd/OffloadEngine.java
new file mode 100644
index 0000000..9015985
--- /dev/null
+++ b/framework-t/src/android/net/nsd/OffloadEngine.java
@@ -0,0 +1,94 @@
+/*
+ * 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.nsd;
+
+import android.annotation.FlaggedApi;
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * OffloadEngine is an interface for mDns hardware offloading.
+ *
+ * An offloading engine can interact with the firmware code to instruct the hardware to
+ * offload some of mDns network traffic before it reached android OS. This can improve the
+ * power consumption performance of the host system by not always waking up the OS to handle
+ * the mDns packet when the device is in low power mode.
+ *
+ * @hide
+ */
+@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
+@SystemApi
+public interface OffloadEngine {
+    /**
+     * Indicates that the OffloadEngine can generate replies to mDns queries.
+     *
+     * @see OffloadServiceInfo#getOffloadPayload()
+     */
+    int OFFLOAD_TYPE_REPLY = 1;
+    /**
+     * Indicates that the OffloadEngine can filter and drop mDns queries.
+     */
+    int OFFLOAD_TYPE_FILTER_QUERIES = 1 << 1;
+    /**
+     * Indicates that the OffloadEngine can filter and drop mDns replies. It can allow mDns packets
+     * to be received even when no app holds a {@link android.net.wifi.WifiManager.MulticastLock}.
+     */
+    int OFFLOAD_TYPE_FILTER_REPLIES = 1 << 2;
+
+    /**
+     * Indicates that the OffloadEngine can bypass multicast lock.
+     */
+    int OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK = 1;
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(flag = true, prefix = {"OFFLOAD_TYPE"}, value = {
+            OFFLOAD_TYPE_REPLY,
+            OFFLOAD_TYPE_FILTER_QUERIES,
+            OFFLOAD_TYPE_FILTER_REPLIES,
+    })
+    @interface OffloadType {}
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(flag = true, prefix = {"OFFLOAD_CAPABILITY"}, value = {
+            OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK
+    })
+    @interface OffloadCapability {}
+
+    /**
+     * To be called when the OffloadServiceInfo is added or updated.
+     *
+     * @param info The OffloadServiceInfo to add or update.
+     */
+    void onOffloadServiceUpdated(@NonNull OffloadServiceInfo info);
+
+    /**
+     * To be called when the OffloadServiceInfo is removed.
+     *
+     * @param info The OffloadServiceInfo to remove.
+     */
+    void onOffloadServiceRemoved(@NonNull OffloadServiceInfo info);
+}
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
new file mode 100644
index 0000000..98dc83a
--- /dev/null
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -0,0 +1,336 @@
+/*
+ * 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.nsd;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresApi;
+import android.annotation.SystemApi;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.module.util.HexDump;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The OffloadServiceInfo class contains all the necessary information the OffloadEngine needs to
+ * know about how to offload an mDns service. The OffloadServiceInfo is keyed on
+ * {@link OffloadServiceInfo.Key} which is a (serviceName, serviceType) pair.
+ *
+ * @hide
+ */
+@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
+@SystemApi
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public final class OffloadServiceInfo implements Parcelable {
+    @NonNull
+    private final Key mKey;
+    @NonNull
+    private final String mHostname;
+    @NonNull final List<String> mSubtypes;
+    @Nullable
+    private final byte[] mOffloadPayload;
+    private final int mPriority;
+    private final long mOffloadType;
+
+    /**
+     * Creates a new OffloadServiceInfo object with the specified parameters.
+     *
+     * @param key The key of the service.
+     * @param subtypes The list of subTypes of the service.
+     * @param hostname The name of the host offering the service. It is meaningful only when
+     *                 offloadType contains OFFLOAD_REPLY.
+     * @param offloadPayload The raw udp payload for hardware offloading.
+     * @param priority The priority of the service, @see #getPriority.
+     * @param offloadType The type of the service offload, @see #getOffloadType.
+     */
+    public OffloadServiceInfo(@NonNull Key key,
+            @NonNull List<String> subtypes, @NonNull String hostname,
+            @Nullable byte[] offloadPayload,
+            @IntRange(from = 0, to = Integer.MAX_VALUE) int priority,
+            @OffloadEngine.OffloadType long offloadType) {
+        Objects.requireNonNull(key);
+        Objects.requireNonNull(subtypes);
+        Objects.requireNonNull(hostname);
+        mKey = key;
+        mSubtypes = subtypes;
+        mHostname = hostname;
+        mOffloadPayload = offloadPayload;
+        mPriority = priority;
+        mOffloadType = offloadType;
+    }
+
+    /**
+     * Creates a new OffloadServiceInfo object from a Parcel.
+     *
+     * @param in The Parcel to read the object from.
+     *
+     * @hide
+     */
+    public OffloadServiceInfo(@NonNull Parcel in) {
+        mKey = in.readParcelable(Key.class.getClassLoader(),
+                Key.class);
+        mSubtypes = in.createStringArrayList();
+        mHostname = in.readString();
+        mOffloadPayload = in.createByteArray();
+        mPriority = in.readInt();
+        mOffloadType = in.readLong();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mKey, flags);
+        dest.writeStringList(mSubtypes);
+        dest.writeString(mHostname);
+        dest.writeByteArray(mOffloadPayload);
+        dest.writeInt(mPriority);
+        dest.writeLong(mOffloadType);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Creator<OffloadServiceInfo> CREATOR = new Creator<OffloadServiceInfo>() {
+        @Override
+        public OffloadServiceInfo createFromParcel(Parcel in) {
+            return new OffloadServiceInfo(in);
+        }
+
+        @Override
+        public OffloadServiceInfo[] newArray(int size) {
+            return new OffloadServiceInfo[size];
+        }
+    };
+
+    /**
+     * Get the {@link Key}.
+     */
+    @NonNull
+    public Key getKey() {
+        return mKey;
+    }
+
+    /**
+     * Get the host name. (e.g. "Android.local" )
+     */
+    @NonNull
+    public String getHostname() {
+        return mHostname;
+    }
+
+    /**
+     * Get the service subtypes. (e.g. ["_ann"] )
+     */
+    @NonNull
+    public List<String> getSubtypes() {
+        return Collections.unmodifiableList(mSubtypes);
+    }
+
+    /**
+     * Get the raw udp payload that the OffloadEngine can use to directly reply the incoming query.
+     * <p>
+     * It is null if the OffloadEngine can not handle transmit. The packet must be sent as-is when
+     * replying to query.
+     */
+    @Nullable
+    public byte[] getOffloadPayload() {
+        if (mOffloadPayload == null) {
+            return null;
+        } else {
+            return mOffloadPayload.clone();
+        }
+    }
+
+    /**
+     * Create a new OffloadServiceInfo with payload updated.
+     *
+     * @hide
+     */
+    @NonNull
+    public OffloadServiceInfo withOffloadPayload(@NonNull byte[] offloadPayload) {
+        return new OffloadServiceInfo(
+                this.getKey(),
+                this.getSubtypes(),
+                this.getHostname(),
+                offloadPayload,
+                this.getPriority(),
+                this.getOffloadType()
+        );
+    }
+
+    /**
+     * Get the offloadType.
+     * <p>
+     * For example, if the {@link com.android.server.NsdService} requests the OffloadEngine to both
+     * filter the mDNS queries and replies, the {@link #mOffloadType} =
+     * ({@link OffloadEngine#OFFLOAD_TYPE_FILTER_QUERIES} |
+     * {@link OffloadEngine#OFFLOAD_TYPE_FILTER_REPLIES}).
+     */
+    @OffloadEngine.OffloadType public long getOffloadType() {
+        return mOffloadType;
+    }
+
+    /**
+     * Get the priority for the OffloadServiceInfo.
+     * <p>
+     * When OffloadEngine don't have enough resource
+     * (e.g. not enough memory) to offload all the OffloadServiceInfo. The OffloadServiceInfo
+     * having lower priority values should be handled by the OffloadEngine first.
+     */
+    public int getPriority() {
+        return mPriority;
+    }
+
+    /**
+     * Only for debug purpose, the string can be long as the raw packet is dump in the string.
+     */
+    @Override
+    public String toString() {
+        return String.format(
+                "OffloadServiceInfo{ mOffloadServiceInfoKey=%s, mHostName=%s, "
+                        + "mOffloadPayload=%s, mPriority=%d, mOffloadType=%d, mSubTypes=%s }",
+                mKey,
+                mHostname, HexDump.dumpHexString(mOffloadPayload), mPriority,
+                mOffloadType, mSubtypes.toString());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof OffloadServiceInfo)) return false;
+        OffloadServiceInfo that = (OffloadServiceInfo) o;
+        return mPriority == that.mPriority && mOffloadType == that.mOffloadType
+                && mKey.equals(that.mKey)
+                && mHostname.equals(
+                that.mHostname) && Arrays.equals(mOffloadPayload,
+                that.mOffloadPayload)
+                && mSubtypes.equals(that.mSubtypes);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = Objects.hash(mKey, mHostname, mPriority,
+                mOffloadType, mSubtypes);
+        result = 31 * result + Arrays.hashCode(mOffloadPayload);
+        return result;
+    }
+
+    /**
+     * The {@link OffloadServiceInfo.Key} is the (serviceName, serviceType) pair.
+     */
+    public static final class Key implements Parcelable {
+        @NonNull
+        private final String mServiceName;
+        @NonNull
+        private final String mServiceType;
+
+        /**
+         * Creates a new OffloadServiceInfoKey object with the specified parameters.
+         *
+         * @param serviceName The name of the service.
+         * @param serviceType The type of the service.
+         */
+        public Key(@NonNull String serviceName, @NonNull String serviceType) {
+            Objects.requireNonNull(serviceName);
+            Objects.requireNonNull(serviceType);
+            mServiceName = serviceName;
+            mServiceType = serviceType;
+        }
+
+        /**
+         * Creates a new OffloadServiceInfoKey object from a Parcel.
+         *
+         * @param in The Parcel to read the object from.
+         *
+         * @hide
+         */
+        public Key(@NonNull Parcel in) {
+            mServiceName = in.readString();
+            mServiceType = in.readString();
+        }
+        /**
+         * Get the service name. (e.g. "NsdChat")
+         */
+        @NonNull
+        public String getServiceName() {
+            return mServiceName;
+        }
+
+        /**
+         * Get the service type. (e.g. "_http._tcp.local" )
+         */
+        @NonNull
+        public String getServiceType() {
+            return mServiceType;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            dest.writeString(mServiceName);
+            dest.writeString(mServiceType);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @NonNull
+        public static final Creator<Key> CREATOR =
+                new Creator<Key>() {
+            @Override
+            public Key createFromParcel(Parcel in) {
+                return new Key(in);
+            }
+
+            @Override
+            public Key[] newArray(int size) {
+                return new Key[size];
+            }
+        };
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof Key)) return false;
+            Key that = (Key) o;
+            return Objects.equals(mServiceName, that.mServiceName) && Objects.equals(
+                    mServiceType, that.mServiceType);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mServiceName, mServiceType);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("OffloadServiceInfoKey{ mServiceName=%s, mServiceType=%s }",
+                    mServiceName, mServiceType);
+        }
+    }
+}
diff --git a/framework/Android.bp b/framework/Android.bp
index d7eaf9b..4eda0aa 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
@@ -80,17 +81,22 @@
     impl_only_libs: [
         // TODO: figure out why just using "framework-tethering" uses the stubs, even though both
         // framework-connectivity and framework-tethering are in the same APEX.
+        "framework-location.stubs.module_lib",
         "framework-tethering.impl",
         "framework-wifi.stubs.module_lib",
-        "net-utils-device-common",
     ],
     static_libs: [
-        "mdns_aidl_interface-lateststable-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",
         "framework-connectivity-javastream-protos",
     ],
+    impl_only_static_libs: [
+        "net-utils-framework-connectivity",
+    ],
     libs: [
         "androidx.annotation_annotation",
         "app-compat-annotations",
@@ -100,24 +106,45 @@
     apex_available: [
         "com.android.tethering",
     ],
-    lint: { strict_updatability_linting: true },
 }
 
 java_library {
     name: "framework-connectivity-pre-jarjar",
     defaults: [
         "framework-connectivity-defaults",
-        "CronetJavaPrejarjarDefaults",
-     ],
+    ],
+    static_libs: [
+        "httpclient_api",
+        "httpclient_impl",
+        // 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-framework-connectivity",
+    ],
     libs: [
         // This cannot be in the defaults clause above because if it were, it would be used
         // to generate the connectivity stubs. That would create a circular dependency
         // because the tethering impl depend on the connectivity stubs (e.g.,
         // TetheringRequest depends on LinkAddress).
+        "framework-location.stubs.module_lib",
         "framework-tethering.impl",
         "framework-wifi.stubs.module_lib",
     ],
-    visibility: ["//packages/modules/Connectivity:__subpackages__"]
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+java_defaults {
+    name: "CronetJavaDefaults",
+    srcs: [":httpclient_api_sources"],
+    libs: [
+        "androidx.annotation_annotation",
+    ],
+    impl_only_static_libs: [
+        "httpclient_impl",
+    ],
 }
 
 java_sdk_library {
@@ -136,30 +163,36 @@
         "//packages/modules/Connectivity/framework-t",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/service-t",
-        "//frameworks/base/packages/Connectivity/service",
         "//frameworks/base",
 
         // Tests using hidden APIs
         "//cts/tests/netlegacy22.api",
         "//cts/tests/tests/app.usage", // NetworkUsageStatsTest
         "//external/sl4a:__subpackages__",
-        "//frameworks/base/packages/Connectivity/tests:__subpackages__",
         "//frameworks/base/core/tests/bandwidthtests",
         "//frameworks/base/core/tests/benchmarks",
         "//frameworks/base/core/tests/utillib",
+        "//frameworks/base/services/tests/VpnTests",
         "//frameworks/base/tests/vcn",
-        "//frameworks/libs/net/common/testutils",
-        "//frameworks/libs/net/common/tests:__subpackages__",
         "//frameworks/opt/net/ethernet/tests:__subpackages__",
         "//frameworks/opt/telephony/tests/telephonytests",
         "//packages/modules/CaptivePortalLogin/tests",
+        "//packages/modules/Connectivity/staticlibs/testutils",
+        "//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
         "//packages/modules/Connectivity/Cronet/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
         "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Connectivity/thread/tests:__subpackages__",
         "//packages/modules/IPsec/tests/iketests",
         "//packages/modules/NetworkStack/tests:__subpackages__",
         "//packages/modules/Wifi/service/tests/wifitests",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
+    aconfig_declarations: [
+        "com.android.net.flags-aconfig",
+    ],
 }
 
 platform_compat_config {
@@ -229,6 +262,7 @@
         ":framework-connectivity-t-pre-jarjar{.jar}",
         ":framework-connectivity.stubs.module_lib{.jar}",
         ":framework-connectivity-t.stubs.module_lib{.jar}",
+        ":framework-connectivity-module-api-stubs-including-flagged{.jar}",
         "jarjar-excludes.txt",
     ],
     tools: [
@@ -241,6 +275,7 @@
         "--prefix android.net.connectivity " +
         "--apistubs $(location :framework-connectivity.stubs.module_lib{.jar}) " +
         "--apistubs $(location :framework-connectivity-t.stubs.module_lib{.jar}) " +
+        "--apistubs $(location :framework-connectivity-module-api-stubs-including-flagged{.jar}) " +
         // Make a ":"-separated list. There will be an extra ":" but empty items are ignored.
         "--unsupportedapi $$(printf ':%s' $(locations :connectivity-hiddenapi-files)) " +
         "--excludes $(location jarjar-excludes.txt) " +
@@ -252,21 +287,50 @@
     ],
 }
 
+droidstubs {
+    name: "framework-connectivity-module-api-stubs-including-flagged-droidstubs",
+    srcs: [
+        ":framework-connectivity-sources",
+        ":framework-connectivity-tiramisu-updatable-sources",
+        ":framework-nearby-java-sources",
+        ":framework-thread-sources",
+    ],
+    flags: [
+        "--show-for-stub-purposes-annotation android.annotation.SystemApi" +
+            "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
+        "--show-for-stub-purposes-annotation android.annotation.SystemApi" +
+            "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
+    ],
+    aidl: {
+        include_dirs: [
+            "packages/modules/Connectivity/framework/aidl-export",
+            "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+        ],
+    },
+}
+
+java_library {
+    name: "framework-connectivity-module-api-stubs-including-flagged",
+    srcs: [":framework-connectivity-module-api-stubs-including-flagged-droidstubs"],
+}
+
 // Library providing limited APIs within the connectivity module, so that R+ components like
 // Tethering have a controlled way to depend on newer components like framework-connectivity that
 // are not loaded on R.
+// Note that this target needs to have access to hidden classes, and as such needs to list
+// the full libraries instead of the .impl lib (which only expose API classes).
 java_library {
     name: "connectivity-internal-api-util",
     sdk_version: "module_current",
     libs: [
         "androidx.annotation_annotation",
-        "framework-connectivity.impl",
+        "framework-connectivity-pre-jarjar",
     ],
     jarjar_rules: ":framework-connectivity-jarjar-rules",
     srcs: [
-        // Files listed here MUST all be annotated with @RequiresApi(Build.VERSION_CODES.TIRAMISU),
-        // so that API checks are enforced for R+ users of this library
-        "src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java",
+        // 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/connectivity/ConnectivityInternalApiUtil.java",
     ],
     visibility: [
         "//packages/modules/Connectivity/Tethering:__subpackages__",
diff --git a/framework/aidl-export/android/net/LocalNetworkConfig.aidl b/framework/aidl-export/android/net/LocalNetworkConfig.aidl
new file mode 100644
index 0000000..e2829a5
--- /dev/null
+++ b/framework/aidl-export/android/net/LocalNetworkConfig.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * 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;
+
+@JavaOnlyStableParcelable parcelable LocalNetworkConfig;
diff --git a/framework/aidl-export/android/net/LocalNetworkInfo.aidl b/framework/aidl-export/android/net/LocalNetworkInfo.aidl
new file mode 100644
index 0000000..fa0bc41
--- /dev/null
+++ b/framework/aidl-export/android/net/LocalNetworkInfo.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * 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;
+
+parcelable LocalNetworkInfo;
diff --git a/framework/aidl-export/android/net/TetheringManager.aidl b/framework/aidl-export/android/net/TetheringManager.aidl
new file mode 100644
index 0000000..1235722
--- /dev/null
+++ b/framework/aidl-export/android/net/TetheringManager.aidl
@@ -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.
+ */
+
+package android.net;
+
+parcelable TetheringManager.TetheringRequest;
diff --git a/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
new file mode 100644
index 0000000..2848074
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.nsd;
+
+@JavaOnlyStableParcelable parcelable AdvertisingRequest;
\ No newline at end of file
diff --git a/framework/aidl-export/android/net/nsd/DiscoveryRequest.aidl b/framework/aidl-export/android/net/nsd/DiscoveryRequest.aidl
new file mode 100644
index 0000000..481a066
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/DiscoveryRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.nsd;
+
+@JavaOnlyStableParcelable parcelable DiscoveryRequest;
diff --git a/framework/aidl-export/android/net/nsd/OffloadServiceInfo.aidl b/framework/aidl-export/android/net/nsd/OffloadServiceInfo.aidl
new file mode 100644
index 0000000..aa7aef2
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/OffloadServiceInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.nsd;
+
+@JavaOnlyStableParcelable parcelable OffloadServiceInfo;
\ No newline at end of file
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 6860c3c..7bc0cf3 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -315,6 +315,7 @@
     method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
     method public int getOwnerUid();
     method public int getSignalStrength();
+    method @FlaggedApi("com.android.net.flags.request_restricted_wifi") @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
     method @Nullable public android.net.TransportInfo getTransportInfo();
     method public boolean hasCapability(int);
     method public boolean hasEnterpriseId(int);
@@ -332,9 +333,11 @@
     field public static final int NET_CAPABILITY_IA = 7; // 0x7
     field public static final int NET_CAPABILITY_IMS = 4; // 0x4
     field public static final int NET_CAPABILITY_INTERNET = 12; // 0xc
+    field @FlaggedApi("com.android.net.flags.net_capability_local_network") public static final int NET_CAPABILITY_LOCAL_NETWORK = 36; // 0x24
     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
@@ -360,6 +363,7 @@
     field public static final int TRANSPORT_CELLULAR = 0; // 0x0
     field public static final int TRANSPORT_ETHERNET = 3; // 0x3
     field public static final int TRANSPORT_LOWPAN = 6; // 0x6
+    field @FlaggedApi("com.android.net.flags.support_transport_satellite") public static final int TRANSPORT_SATELLITE = 10; // 0xa
     field public static final int TRANSPORT_THREAD = 9; // 0x9
     field public static final int TRANSPORT_USB = 8; // 0x8
     field public static final int TRANSPORT_VPN = 4; // 0x4
@@ -418,6 +422,7 @@
     method public int describeContents();
     method @NonNull public int[] getCapabilities();
     method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
+    method @FlaggedApi("com.android.net.flags.request_restricted_wifi") @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
     method @NonNull public int[] getTransportTypes();
     method public boolean hasCapability(int);
     method public boolean hasTransport(int);
@@ -437,6 +442,7 @@
     method @NonNull public android.net.NetworkRequest.Builder setIncludeOtherUidNetworks(boolean);
     method @Deprecated public android.net.NetworkRequest.Builder setNetworkSpecifier(String);
     method public android.net.NetworkRequest.Builder setNetworkSpecifier(android.net.NetworkSpecifier);
+    method @FlaggedApi("com.android.net.flags.request_restricted_wifi") @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
   }
 
   public class ParseException extends java.lang.RuntimeException {
diff --git a/framework/api/lint-baseline.txt b/framework/api/lint-baseline.txt
index 2f4004a..4465bcb 100644
--- a/framework/api/lint-baseline.txt
+++ b/framework/api/lint-baseline.txt
@@ -1,4 +1,19 @@
 // Baseline format: 1.0
+BroadcastBehavior: android.net.ConnectivityManager#ACTION_BACKGROUND_DATA_SETTING_CHANGED:
+    Field 'ACTION_BACKGROUND_DATA_SETTING_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.app.PendingIntent):
+    Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback):
+    Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getOwnerUid():
+    Method 'getOwnerUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.BidirectionalStream.Builder#setTrafficStatsUid(int):
+    Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.UrlRequest.Builder#setTrafficStatsUid(int):
+    Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+
+
 VisiblySynchronized: android.net.NetworkInfo#toString():
     Internal locks must not be exposed (synchronizing on this or class is still
-    externally observable): method android.net.NetworkInfo.toString()
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 193bd92..cd7307f 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -14,6 +14,7 @@
     method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange();
     method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.LinkProperties getRedactedLinkPropertiesForPackage(@NonNull android.net.LinkProperties, int, @NonNull String);
     method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.NetworkCapabilities getRedactedNetworkCapabilitiesForPackage(@NonNull android.net.NetworkCapabilities, int, @NonNull String);
+    method @FlaggedApi("com.android.net.flags.support_is_uid_networking_blocked") @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public boolean isUidNetworkingBlocked(int, boolean);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerDefaultNetworkCallbackForUid(int, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS}) public void registerSystemDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkAllowList(int);
@@ -24,6 +25,7 @@
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptPartialConnectivity(@NonNull android.net.Network, boolean, boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptUnvalidated(@NonNull android.net.Network, boolean, boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAvoidUnvalidated(@NonNull android.net.Network);
+    method @FlaggedApi("com.android.net.flags.set_data_saver_via_cm") @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setDataSaverEnabled(boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setFirewallChainEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setGlobalProxy(@Nullable android.net.ProxyInfo);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setLegacyLockdownVpnEnabled(boolean);
@@ -43,15 +45,22 @@
     field public static final int BLOCKED_METERED_REASON_DATA_SAVER = 65536; // 0x10000
     field public static final int BLOCKED_METERED_REASON_MASK = -65536; // 0xffff0000
     field public static final int BLOCKED_METERED_REASON_USER_RESTRICTED = 131072; // 0x20000
+    field @FlaggedApi("com.android.net.flags.basic_background_restrictions_enabled") public static final int BLOCKED_REASON_APP_BACKGROUND = 64; // 0x40
     field public static final int BLOCKED_REASON_APP_STANDBY = 4; // 0x4
     field public static final int BLOCKED_REASON_BATTERY_SAVER = 1; // 0x1
     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
@@ -231,6 +240,7 @@
 
   public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
     ctor @Deprecated public VpnTransportInfo(int, @Nullable String);
+    method public long getApplicableRedactions();
     method @Nullable public String getSessionId();
     method @NonNull public android.net.VpnTransportInfo makeCopy(long);
   }
diff --git a/framework/api/module-lib-lint-baseline.txt b/framework/api/module-lib-lint-baseline.txt
new file mode 100644
index 0000000..53a8c5e
--- /dev/null
+++ b/framework/api/module-lib-lint-baseline.txt
@@ -0,0 +1,33 @@
+// Baseline format: 1.0
+BroadcastBehavior: android.net.ConnectivityManager#ACTION_BACKGROUND_DATA_SETTING_CHANGED:
+    Field 'ACTION_BACKGROUND_DATA_SETTING_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.ConnectivityManager#isTetheringSupported():
+    Method 'isTetheringSupported' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.app.PendingIntent):
+    Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback):
+    Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestRouteToHostAddress(int, java.net.InetAddress):
+    Method 'requestRouteToHostAddress' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalApiUrl():
+    Method 'getCaptivePortalApiUrl' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalData():
+    Method 'getCaptivePortalData' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getOwnerUid():
+    Method 'getOwnerUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getUnderlyingNetworks():
+    Method 'getUnderlyingNetworks' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setAllowedUids(java.util.Set<java.lang.Integer>):
+    Method 'setAllowedUids' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setSignalStrength(int):
+    Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setUnderlyingNetworks(java.util.List<android.net.Network>):
+    Method 'setUnderlyingNetworks' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkRequest.Builder#setSignalStrength(int):
+    Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.http.BidirectionalStream.Builder#setTrafficStatsUid(int):
+    Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.UrlRequest.Builder#setTrafficStatsUid(int):
+    Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 4a2ed8a..bef29a4 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -94,6 +94,7 @@
   }
 
   public final class DscpPolicy implements android.os.Parcelable {
+    method public int describeContents();
     method @Nullable public java.net.InetAddress getDestinationAddress();
     method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange();
     method public int getDscpValue();
@@ -101,6 +102,7 @@
     method public int getProtocol();
     method @Nullable public java.net.InetAddress getSourceAddress();
     method public int getSourcePort();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
     field public static final int PROTOCOL_ANY = -1; // 0xffffffff
     field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
@@ -305,7 +307,6 @@
     method @NonNull public int[] getAdministratorUids();
     method @Nullable public static String getCapabilityCarrierName(int);
     method @Nullable public String getSsid();
-    method @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
     method @NonNull public int[] getTransportTypes();
     method @Nullable public java.util.List<android.net.Network> getUnderlyingNetworks();
     method public boolean isPrivateDnsBroken();
@@ -371,7 +372,6 @@
 
   public static class NetworkRequest.Builder {
     method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkRequest.Builder setSignalStrength(int);
-    method @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
   }
 
   public final class NetworkScore implements android.os.Parcelable {
diff --git a/framework/api/system-lint-baseline.txt b/framework/api/system-lint-baseline.txt
index 9a97707..3ac97c0 100644
--- a/framework/api/system-lint-baseline.txt
+++ b/framework/api/system-lint-baseline.txt
@@ -1 +1,29 @@
 // Baseline format: 1.0
+BroadcastBehavior: android.net.ConnectivityManager#ACTION_BACKGROUND_DATA_SETTING_CHANGED:
+    Field 'ACTION_BACKGROUND_DATA_SETTING_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.ConnectivityManager#isTetheringSupported():
+    Method 'isTetheringSupported' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.app.PendingIntent):
+    Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback):
+    Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalApiUrl():
+    Method 'getCaptivePortalApiUrl' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalData():
+    Method 'getCaptivePortalData' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getOwnerUid():
+    Method 'getOwnerUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getUnderlyingNetworks():
+    Method 'getUnderlyingNetworks' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setSignalStrength(int):
+    Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setUnderlyingNetworks(java.util.List<android.net.Network>):
+    Method 'setUnderlyingNetworks' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkRequest.Builder#setSignalStrength(int):
+    Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.http.BidirectionalStream.Builder#setTrafficStatsUid(int):
+    Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.UrlRequest.Builder#setTrafficStatsUid(int):
+    Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
diff --git a/framework/cronet_disabled/api/current.txt b/framework/cronet_disabled/api/current.txt
deleted file mode 100644
index 672e3e2..0000000
--- a/framework/cronet_disabled/api/current.txt
+++ /dev/null
@@ -1,527 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public class CaptivePortal implements android.os.Parcelable {
-    method public int describeContents();
-    method public void ignoreNetwork();
-    method public void reportCaptivePortalDismissed();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortal> CREATOR;
-  }
-
-  public class ConnectivityDiagnosticsManager {
-    method public void registerConnectivityDiagnosticsCallback(@NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback);
-    method public void unregisterConnectivityDiagnosticsCallback(@NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback);
-  }
-
-  public abstract static class ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback {
-    ctor public ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback();
-    method public void onConnectivityReportAvailable(@NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityReport);
-    method public void onDataStallSuspected(@NonNull android.net.ConnectivityDiagnosticsManager.DataStallReport);
-    method public void onNetworkConnectivityReported(@NonNull android.net.Network, boolean);
-  }
-
-  public static final class ConnectivityDiagnosticsManager.ConnectivityReport implements android.os.Parcelable {
-    ctor public ConnectivityDiagnosticsManager.ConnectivityReport(@NonNull android.net.Network, long, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkCapabilities, @NonNull android.os.PersistableBundle);
-    method public int describeContents();
-    method @NonNull public android.os.PersistableBundle getAdditionalInfo();
-    method @NonNull public android.net.LinkProperties getLinkProperties();
-    method @NonNull public android.net.Network getNetwork();
-    method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities();
-    method public long getReportTimestamp();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ConnectivityDiagnosticsManager.ConnectivityReport> CREATOR;
-    field public static final String KEY_NETWORK_PROBES_ATTEMPTED_BITMASK = "networkProbesAttempted";
-    field public static final String KEY_NETWORK_PROBES_SUCCEEDED_BITMASK = "networkProbesSucceeded";
-    field public static final String KEY_NETWORK_VALIDATION_RESULT = "networkValidationResult";
-    field public static final int NETWORK_PROBE_DNS = 4; // 0x4
-    field public static final int NETWORK_PROBE_FALLBACK = 32; // 0x20
-    field public static final int NETWORK_PROBE_HTTP = 8; // 0x8
-    field public static final int NETWORK_PROBE_HTTPS = 16; // 0x10
-    field public static final int NETWORK_PROBE_PRIVATE_DNS = 64; // 0x40
-    field public static final int NETWORK_VALIDATION_RESULT_INVALID = 0; // 0x0
-    field public static final int NETWORK_VALIDATION_RESULT_PARTIALLY_VALID = 2; // 0x2
-    field public static final int NETWORK_VALIDATION_RESULT_SKIPPED = 3; // 0x3
-    field public static final int NETWORK_VALIDATION_RESULT_VALID = 1; // 0x1
-  }
-
-  public static final class ConnectivityDiagnosticsManager.DataStallReport implements android.os.Parcelable {
-    ctor public ConnectivityDiagnosticsManager.DataStallReport(@NonNull android.net.Network, long, int, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkCapabilities, @NonNull android.os.PersistableBundle);
-    method public int describeContents();
-    method public int getDetectionMethod();
-    method @NonNull public android.net.LinkProperties getLinkProperties();
-    method @NonNull public android.net.Network getNetwork();
-    method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities();
-    method public long getReportTimestamp();
-    method @NonNull public android.os.PersistableBundle getStallDetails();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ConnectivityDiagnosticsManager.DataStallReport> CREATOR;
-    field public static final int DETECTION_METHOD_DNS_EVENTS = 1; // 0x1
-    field public static final int DETECTION_METHOD_TCP_METRICS = 2; // 0x2
-    field public static final String KEY_DNS_CONSECUTIVE_TIMEOUTS = "dnsConsecutiveTimeouts";
-    field public static final String KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS = "tcpMetricsCollectionPeriodMillis";
-    field public static final String KEY_TCP_PACKET_FAIL_RATE = "tcpPacketFailRate";
-  }
-
-  public class ConnectivityManager {
-    method public void addDefaultNetworkActiveListener(android.net.ConnectivityManager.OnNetworkActiveListener);
-    method public boolean bindProcessToNetwork(@Nullable android.net.Network);
-    method @NonNull public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull android.net.IpSecManager.UdpEncapsulationSocket, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
-    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.Network getActiveNetwork();
-    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getActiveNetworkInfo();
-    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo[] getAllNetworkInfo();
-    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.Network[] getAllNetworks();
-    method @Deprecated public boolean getBackgroundDataSetting();
-    method @Nullable public android.net.Network getBoundNetworkForProcess();
-    method public int getConnectionOwnerUid(int, @NonNull java.net.InetSocketAddress, @NonNull java.net.InetSocketAddress);
-    method @Nullable public android.net.ProxyInfo getDefaultProxy();
-    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.LinkProperties getLinkProperties(@Nullable android.net.Network);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public int getMultipathPreference(@Nullable android.net.Network);
-    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkCapabilities getNetworkCapabilities(@Nullable android.net.Network);
-    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getNetworkInfo(int);
-    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getNetworkInfo(@Nullable android.net.Network);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public int getNetworkPreference();
-    method @Nullable public byte[] getNetworkWatchlistConfigHash();
-    method @Deprecated @Nullable public static android.net.Network getProcessDefaultNetwork();
-    method public int getRestrictBackgroundStatus();
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public boolean isActiveNetworkMetered();
-    method public boolean isDefaultNetworkActive();
-    method @Deprecated public static boolean isNetworkTypeValid(int);
-    method public void registerBestMatchingNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
-    method public void releaseNetworkRequest(@NonNull android.app.PendingIntent);
-    method public void removeDefaultNetworkActiveListener(@NonNull android.net.ConnectivityManager.OnNetworkActiveListener);
-    method @Deprecated public void reportBadNetwork(@Nullable android.net.Network);
-    method public void reportNetworkConnectivity(@Nullable android.net.Network, boolean);
-    method public boolean requestBandwidthUpdate(@NonNull android.net.Network);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, int);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler, int);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
-    method @Deprecated public void setNetworkPreference(int);
-    method @Deprecated public static boolean setProcessDefaultNetwork(@Nullable android.net.Network);
-    method public void unregisterNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
-    method public void unregisterNetworkCallback(@NonNull android.app.PendingIntent);
-    field @Deprecated public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED = "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED";
-    field public static final String ACTION_CAPTIVE_PORTAL_SIGN_IN = "android.net.conn.CAPTIVE_PORTAL";
-    field public static final String ACTION_RESTRICT_BACKGROUND_CHANGED = "android.net.conn.RESTRICT_BACKGROUND_CHANGED";
-    field @Deprecated public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
-    field @Deprecated public static final int DEFAULT_NETWORK_PREFERENCE = 1; // 0x1
-    field public static final String EXTRA_CAPTIVE_PORTAL = "android.net.extra.CAPTIVE_PORTAL";
-    field public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL";
-    field @Deprecated public static final String EXTRA_EXTRA_INFO = "extraInfo";
-    field @Deprecated public static final String EXTRA_IS_FAILOVER = "isFailover";
-    field public static final String EXTRA_NETWORK = "android.net.extra.NETWORK";
-    field @Deprecated public static final String EXTRA_NETWORK_INFO = "networkInfo";
-    field public static final String EXTRA_NETWORK_REQUEST = "android.net.extra.NETWORK_REQUEST";
-    field @Deprecated public static final String EXTRA_NETWORK_TYPE = "networkType";
-    field public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity";
-    field @Deprecated public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
-    field public static final String EXTRA_REASON = "reason";
-    field public static final int MULTIPATH_PREFERENCE_HANDOVER = 1; // 0x1
-    field public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 4; // 0x4
-    field public static final int MULTIPATH_PREFERENCE_RELIABILITY = 2; // 0x2
-    field public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; // 0x1
-    field public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; // 0x3
-    field public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; // 0x2
-    field @Deprecated public static final int TYPE_BLUETOOTH = 7; // 0x7
-    field @Deprecated public static final int TYPE_DUMMY = 8; // 0x8
-    field @Deprecated public static final int TYPE_ETHERNET = 9; // 0x9
-    field @Deprecated public static final int TYPE_MOBILE = 0; // 0x0
-    field @Deprecated public static final int TYPE_MOBILE_DUN = 4; // 0x4
-    field @Deprecated public static final int TYPE_MOBILE_HIPRI = 5; // 0x5
-    field @Deprecated public static final int TYPE_MOBILE_MMS = 2; // 0x2
-    field @Deprecated public static final int TYPE_MOBILE_SUPL = 3; // 0x3
-    field @Deprecated public static final int TYPE_VPN = 17; // 0x11
-    field @Deprecated public static final int TYPE_WIFI = 1; // 0x1
-    field @Deprecated public static final int TYPE_WIMAX = 6; // 0x6
-  }
-
-  public static class ConnectivityManager.NetworkCallback {
-    ctor public ConnectivityManager.NetworkCallback();
-    ctor public ConnectivityManager.NetworkCallback(int);
-    method public void onAvailable(@NonNull android.net.Network);
-    method public void onBlockedStatusChanged(@NonNull android.net.Network, boolean);
-    method public void onCapabilitiesChanged(@NonNull android.net.Network, @NonNull android.net.NetworkCapabilities);
-    method public void onLinkPropertiesChanged(@NonNull android.net.Network, @NonNull android.net.LinkProperties);
-    method public void onLosing(@NonNull android.net.Network, int);
-    method public void onLost(@NonNull android.net.Network);
-    method public void onUnavailable();
-    field public static final int FLAG_INCLUDE_LOCATION_INFO = 1; // 0x1
-  }
-
-  public static interface ConnectivityManager.OnNetworkActiveListener {
-    method public void onNetworkActive();
-  }
-
-  public class DhcpInfo implements android.os.Parcelable {
-    ctor public DhcpInfo();
-    method public int describeContents();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.DhcpInfo> CREATOR;
-    field public int dns1;
-    field public int dns2;
-    field public int gateway;
-    field public int ipAddress;
-    field public int leaseDuration;
-    field public int netmask;
-    field public int serverAddress;
-  }
-
-  public final class DnsResolver {
-    method @NonNull public static android.net.DnsResolver getInstance();
-    method public void query(@Nullable android.net.Network, @NonNull String, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super java.util.List<java.net.InetAddress>>);
-    method public void query(@Nullable android.net.Network, @NonNull String, int, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super java.util.List<java.net.InetAddress>>);
-    method public void rawQuery(@Nullable android.net.Network, @NonNull byte[], int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super byte[]>);
-    method public void rawQuery(@Nullable android.net.Network, @NonNull String, int, int, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super byte[]>);
-    field public static final int CLASS_IN = 1; // 0x1
-    field public static final int ERROR_PARSE = 0; // 0x0
-    field public static final int ERROR_SYSTEM = 1; // 0x1
-    field public static final int FLAG_EMPTY = 0; // 0x0
-    field public static final int FLAG_NO_CACHE_LOOKUP = 4; // 0x4
-    field public static final int FLAG_NO_CACHE_STORE = 2; // 0x2
-    field public static final int FLAG_NO_RETRY = 1; // 0x1
-    field public static final int TYPE_A = 1; // 0x1
-    field public static final int TYPE_AAAA = 28; // 0x1c
-  }
-
-  public static interface DnsResolver.Callback<T> {
-    method public void onAnswer(@NonNull T, int);
-    method public void onError(@NonNull android.net.DnsResolver.DnsException);
-  }
-
-  public static class DnsResolver.DnsException extends java.lang.Exception {
-    ctor public DnsResolver.DnsException(int, @Nullable Throwable);
-    field public final int code;
-  }
-
-  public class InetAddresses {
-    method public static boolean isNumericAddress(@NonNull String);
-    method @NonNull public static java.net.InetAddress parseNumericAddress(@NonNull String);
-  }
-
-  public final class IpConfiguration implements android.os.Parcelable {
-    method public int describeContents();
-    method @Nullable public android.net.ProxyInfo getHttpProxy();
-    method @Nullable public android.net.StaticIpConfiguration getStaticIpConfiguration();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpConfiguration> CREATOR;
-  }
-
-  public static final class IpConfiguration.Builder {
-    ctor public IpConfiguration.Builder();
-    method @NonNull public android.net.IpConfiguration build();
-    method @NonNull public android.net.IpConfiguration.Builder setHttpProxy(@Nullable android.net.ProxyInfo);
-    method @NonNull public android.net.IpConfiguration.Builder setStaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
-  }
-
-  public final class IpPrefix implements android.os.Parcelable {
-    ctor public IpPrefix(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int);
-    method public boolean contains(@NonNull java.net.InetAddress);
-    method public int describeContents();
-    method @NonNull public java.net.InetAddress getAddress();
-    method @IntRange(from=0, to=128) public int getPrefixLength();
-    method @NonNull public byte[] getRawAddress();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR;
-  }
-
-  public class LinkAddress implements android.os.Parcelable {
-    method public int describeContents();
-    method public java.net.InetAddress getAddress();
-    method public int getFlags();
-    method @IntRange(from=0, to=128) public int getPrefixLength();
-    method public int getScope();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.LinkAddress> CREATOR;
-  }
-
-  public final class LinkProperties implements android.os.Parcelable {
-    ctor public LinkProperties();
-    method public boolean addRoute(@NonNull android.net.RouteInfo);
-    method public void clear();
-    method public int describeContents();
-    method @Nullable public java.net.Inet4Address getDhcpServerAddress();
-    method @NonNull public java.util.List<java.net.InetAddress> getDnsServers();
-    method @Nullable public String getDomains();
-    method @Nullable public android.net.ProxyInfo getHttpProxy();
-    method @Nullable public String getInterfaceName();
-    method @NonNull public java.util.List<android.net.LinkAddress> getLinkAddresses();
-    method public int getMtu();
-    method @Nullable public android.net.IpPrefix getNat64Prefix();
-    method @Nullable public String getPrivateDnsServerName();
-    method @NonNull public java.util.List<android.net.RouteInfo> getRoutes();
-    method public boolean isPrivateDnsActive();
-    method public boolean isWakeOnLanSupported();
-    method public void setDhcpServerAddress(@Nullable java.net.Inet4Address);
-    method public void setDnsServers(@NonNull java.util.Collection<java.net.InetAddress>);
-    method public void setDomains(@Nullable String);
-    method public void setHttpProxy(@Nullable android.net.ProxyInfo);
-    method public void setInterfaceName(@Nullable String);
-    method public void setLinkAddresses(@NonNull java.util.Collection<android.net.LinkAddress>);
-    method public void setMtu(int);
-    method public void setNat64Prefix(@Nullable android.net.IpPrefix);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.LinkProperties> CREATOR;
-  }
-
-  public final class MacAddress implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public static android.net.MacAddress fromBytes(@NonNull byte[]);
-    method @NonNull public static android.net.MacAddress fromString(@NonNull String);
-    method public int getAddressType();
-    method @Nullable public java.net.Inet6Address getLinkLocalIpv6FromEui48Mac();
-    method public boolean isLocallyAssigned();
-    method public boolean matches(@NonNull android.net.MacAddress, @NonNull android.net.MacAddress);
-    method @NonNull public byte[] toByteArray();
-    method @NonNull public String toOuiString();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.net.MacAddress BROADCAST_ADDRESS;
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.MacAddress> CREATOR;
-    field public static final int TYPE_BROADCAST = 3; // 0x3
-    field public static final int TYPE_MULTICAST = 2; // 0x2
-    field public static final int TYPE_UNICAST = 1; // 0x1
-  }
-
-  public class Network implements android.os.Parcelable {
-    method public void bindSocket(java.net.DatagramSocket) throws java.io.IOException;
-    method public void bindSocket(java.net.Socket) throws java.io.IOException;
-    method public void bindSocket(java.io.FileDescriptor) throws java.io.IOException;
-    method public int describeContents();
-    method public static android.net.Network fromNetworkHandle(long);
-    method public java.net.InetAddress[] getAllByName(String) throws java.net.UnknownHostException;
-    method public java.net.InetAddress getByName(String) throws java.net.UnknownHostException;
-    method public long getNetworkHandle();
-    method public javax.net.SocketFactory getSocketFactory();
-    method public java.net.URLConnection openConnection(java.net.URL) throws java.io.IOException;
-    method public java.net.URLConnection openConnection(java.net.URL, java.net.Proxy) throws java.io.IOException;
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.Network> CREATOR;
-  }
-
-  public final class NetworkCapabilities implements android.os.Parcelable {
-    ctor public NetworkCapabilities();
-    ctor public NetworkCapabilities(android.net.NetworkCapabilities);
-    method public int describeContents();
-    method @NonNull public int[] getCapabilities();
-    method @NonNull public int[] getEnterpriseIds();
-    method public int getLinkDownstreamBandwidthKbps();
-    method public int getLinkUpstreamBandwidthKbps();
-    method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
-    method public int getOwnerUid();
-    method public int getSignalStrength();
-    method @Nullable public android.net.TransportInfo getTransportInfo();
-    method public boolean hasCapability(int);
-    method public boolean hasEnterpriseId(int);
-    method public boolean hasTransport(int);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkCapabilities> CREATOR;
-    field public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17; // 0x11
-    field public static final int NET_CAPABILITY_CBS = 5; // 0x5
-    field public static final int NET_CAPABILITY_DUN = 2; // 0x2
-    field public static final int NET_CAPABILITY_EIMS = 10; // 0xa
-    field public static final int NET_CAPABILITY_ENTERPRISE = 29; // 0x1d
-    field public static final int NET_CAPABILITY_FOREGROUND = 19; // 0x13
-    field public static final int NET_CAPABILITY_FOTA = 3; // 0x3
-    field public static final int NET_CAPABILITY_HEAD_UNIT = 32; // 0x20
-    field public static final int NET_CAPABILITY_IA = 7; // 0x7
-    field public static final int NET_CAPABILITY_IMS = 4; // 0x4
-    field public static final int NET_CAPABILITY_INTERNET = 12; // 0xc
-    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 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
-    field public static final int NET_CAPABILITY_NOT_ROAMING = 18; // 0x12
-    field public static final int NET_CAPABILITY_NOT_SUSPENDED = 21; // 0x15
-    field public static final int NET_CAPABILITY_NOT_VPN = 15; // 0xf
-    field public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35; // 0x23
-    field public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22
-    field public static final int NET_CAPABILITY_RCS = 8; // 0x8
-    field public static final int NET_CAPABILITY_SUPL = 1; // 0x1
-    field public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25; // 0x19
-    field public static final int NET_CAPABILITY_TRUSTED = 14; // 0xe
-    field public static final int NET_CAPABILITY_VALIDATED = 16; // 0x10
-    field public static final int NET_CAPABILITY_WIFI_P2P = 6; // 0x6
-    field public static final int NET_CAPABILITY_XCAP = 9; // 0x9
-    field public static final int NET_ENTERPRISE_ID_1 = 1; // 0x1
-    field public static final int NET_ENTERPRISE_ID_2 = 2; // 0x2
-    field public static final int NET_ENTERPRISE_ID_3 = 3; // 0x3
-    field public static final int NET_ENTERPRISE_ID_4 = 4; // 0x4
-    field public static final int NET_ENTERPRISE_ID_5 = 5; // 0x5
-    field public static final int SIGNAL_STRENGTH_UNSPECIFIED = -2147483648; // 0x80000000
-    field public static final int TRANSPORT_BLUETOOTH = 2; // 0x2
-    field public static final int TRANSPORT_CELLULAR = 0; // 0x0
-    field public static final int TRANSPORT_ETHERNET = 3; // 0x3
-    field public static final int TRANSPORT_LOWPAN = 6; // 0x6
-    field public static final int TRANSPORT_THREAD = 9; // 0x9
-    field public static final int TRANSPORT_USB = 8; // 0x8
-    field public static final int TRANSPORT_VPN = 4; // 0x4
-    field public static final int TRANSPORT_WIFI = 1; // 0x1
-    field public static final int TRANSPORT_WIFI_AWARE = 5; // 0x5
-  }
-
-  @Deprecated public class NetworkInfo implements android.os.Parcelable {
-    ctor @Deprecated public NetworkInfo(int, int, @Nullable String, @Nullable String);
-    method @Deprecated public int describeContents();
-    method @Deprecated @NonNull public android.net.NetworkInfo.DetailedState getDetailedState();
-    method @Deprecated public String getExtraInfo();
-    method @Deprecated public String getReason();
-    method @Deprecated public android.net.NetworkInfo.State getState();
-    method @Deprecated public int getSubtype();
-    method @Deprecated public String getSubtypeName();
-    method @Deprecated public int getType();
-    method @Deprecated public String getTypeName();
-    method @Deprecated public boolean isAvailable();
-    method @Deprecated public boolean isConnected();
-    method @Deprecated public boolean isConnectedOrConnecting();
-    method @Deprecated public boolean isFailover();
-    method @Deprecated public boolean isRoaming();
-    method @Deprecated public void setDetailedState(@NonNull android.net.NetworkInfo.DetailedState, @Nullable String, @Nullable String);
-    method @Deprecated public void writeToParcel(android.os.Parcel, int);
-    field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkInfo> CREATOR;
-  }
-
-  @Deprecated public enum NetworkInfo.DetailedState {
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState AUTHENTICATING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState BLOCKED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CAPTIVE_PORTAL_CHECK;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState DISCONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState DISCONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState FAILED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState IDLE;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState OBTAINING_IPADDR;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState SCANNING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState SUSPENDED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState VERIFYING_POOR_LINK;
-  }
-
-  @Deprecated public enum NetworkInfo.State {
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State CONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State CONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State DISCONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State DISCONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State SUSPENDED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State UNKNOWN;
-  }
-
-  public class NetworkRequest implements android.os.Parcelable {
-    method public boolean canBeSatisfiedBy(@Nullable android.net.NetworkCapabilities);
-    method public int describeContents();
-    method @NonNull public int[] getCapabilities();
-    method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
-    method @NonNull public int[] getTransportTypes();
-    method public boolean hasCapability(int);
-    method public boolean hasTransport(int);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkRequest> CREATOR;
-  }
-
-  public static class NetworkRequest.Builder {
-    ctor public NetworkRequest.Builder();
-    ctor public NetworkRequest.Builder(@NonNull android.net.NetworkRequest);
-    method public android.net.NetworkRequest.Builder addCapability(int);
-    method public android.net.NetworkRequest.Builder addTransportType(int);
-    method public android.net.NetworkRequest build();
-    method @NonNull public android.net.NetworkRequest.Builder clearCapabilities();
-    method public android.net.NetworkRequest.Builder removeCapability(int);
-    method public android.net.NetworkRequest.Builder removeTransportType(int);
-    method @NonNull public android.net.NetworkRequest.Builder setIncludeOtherUidNetworks(boolean);
-    method @Deprecated public android.net.NetworkRequest.Builder setNetworkSpecifier(String);
-    method public android.net.NetworkRequest.Builder setNetworkSpecifier(android.net.NetworkSpecifier);
-  }
-
-  public class ParseException extends java.lang.RuntimeException {
-    ctor public ParseException(@NonNull String);
-    ctor public ParseException(@NonNull String, @NonNull Throwable);
-    field public String response;
-  }
-
-  public class ProxyInfo implements android.os.Parcelable {
-    ctor public ProxyInfo(@Nullable android.net.ProxyInfo);
-    method public static android.net.ProxyInfo buildDirectProxy(String, int);
-    method public static android.net.ProxyInfo buildDirectProxy(String, int, java.util.List<java.lang.String>);
-    method public static android.net.ProxyInfo buildPacProxy(android.net.Uri);
-    method @NonNull public static android.net.ProxyInfo buildPacProxy(@NonNull android.net.Uri, int);
-    method public int describeContents();
-    method public String[] getExclusionList();
-    method public String getHost();
-    method public android.net.Uri getPacFileUrl();
-    method public int getPort();
-    method public boolean isValid();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ProxyInfo> CREATOR;
-  }
-
-  public final class RouteInfo implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public android.net.IpPrefix getDestination();
-    method @Nullable public java.net.InetAddress getGateway();
-    method @Nullable public String getInterface();
-    method public int getType();
-    method public boolean hasGateway();
-    method public boolean isDefaultRoute();
-    method public boolean matches(java.net.InetAddress);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.RouteInfo> CREATOR;
-    field public static final int RTN_THROW = 9; // 0x9
-    field public static final int RTN_UNICAST = 1; // 0x1
-    field public static final int RTN_UNREACHABLE = 7; // 0x7
-  }
-
-  public abstract class SocketKeepalive implements java.lang.AutoCloseable {
-    method public final void close();
-    method public final void start(@IntRange(from=0xa, to=0xe10) int);
-    method public final void stop();
-    field public static final int ERROR_HARDWARE_ERROR = -31; // 0xffffffe1
-    field public static final int ERROR_INSUFFICIENT_RESOURCES = -32; // 0xffffffe0
-    field public static final int ERROR_INVALID_INTERVAL = -24; // 0xffffffe8
-    field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb
-    field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9
-    field public static final int ERROR_INVALID_NETWORK = -20; // 0xffffffec
-    field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea
-    field public static final int ERROR_INVALID_SOCKET = -25; // 0xffffffe7
-    field public static final int ERROR_SOCKET_NOT_IDLE = -26; // 0xffffffe6
-    field public static final int ERROR_UNSUPPORTED = -30; // 0xffffffe2
-  }
-
-  public static class SocketKeepalive.Callback {
-    ctor public SocketKeepalive.Callback();
-    method public void onDataReceived();
-    method public void onError(int);
-    method public void onStarted();
-    method public void onStopped();
-  }
-
-  public final class StaticIpConfiguration implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public java.util.List<java.net.InetAddress> getDnsServers();
-    method @Nullable public String getDomains();
-    method @Nullable public java.net.InetAddress getGateway();
-    method @NonNull public android.net.LinkAddress getIpAddress();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.StaticIpConfiguration> CREATOR;
-  }
-
-  public static final class StaticIpConfiguration.Builder {
-    ctor public StaticIpConfiguration.Builder();
-    method @NonNull public android.net.StaticIpConfiguration build();
-    method @NonNull public android.net.StaticIpConfiguration.Builder setDnsServers(@NonNull Iterable<java.net.InetAddress>);
-    method @NonNull public android.net.StaticIpConfiguration.Builder setDomains(@Nullable String);
-    method @NonNull public android.net.StaticIpConfiguration.Builder setGateway(@Nullable java.net.InetAddress);
-    method @NonNull public android.net.StaticIpConfiguration.Builder setIpAddress(@NonNull android.net.LinkAddress);
-  }
-
-  public interface TransportInfo {
-  }
-
-}
-
diff --git a/framework/cronet_disabled/api/lint-baseline.txt b/framework/cronet_disabled/api/lint-baseline.txt
deleted file mode 100644
index 2f4004a..0000000
--- a/framework/cronet_disabled/api/lint-baseline.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-// Baseline format: 1.0
-VisiblySynchronized: android.net.NetworkInfo#toString():
-    Internal locks must not be exposed (synchronizing on this or class is still
-    externally observable): method android.net.NetworkInfo.toString()
diff --git a/framework/cronet_disabled/api/module-lib-current.txt b/framework/cronet_disabled/api/module-lib-current.txt
deleted file mode 100644
index 193bd92..0000000
--- a/framework/cronet_disabled/api/module-lib-current.txt
+++ /dev/null
@@ -1,239 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public final class ConnectivityFrameworkInitializer {
-    method public static void registerServiceWrappers();
-  }
-
-  public class ConnectivityManager {
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void addUidToMeteredNetworkAllowList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void addUidToMeteredNetworkDenyList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void factoryReset();
-    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public java.util.List<android.net.NetworkStateSnapshot> getAllNetworkStateSnapshots();
-    method @Nullable public android.net.ProxyInfo getGlobalProxy();
-    method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange();
-    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.LinkProperties getRedactedLinkPropertiesForPackage(@NonNull android.net.LinkProperties, int, @NonNull String);
-    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.NetworkCapabilities getRedactedNetworkCapabilitiesForPackage(@NonNull android.net.NetworkCapabilities, int, @NonNull String);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerDefaultNetworkCallbackForUid(int, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS}) public void registerSystemDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkAllowList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkDenyList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void replaceFirewallChain(int, @NonNull int[]);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void requestBackgroundNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @Deprecated public boolean requestRouteToHostAddress(int, java.net.InetAddress);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptPartialConnectivity(@NonNull android.net.Network, boolean, boolean);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptUnvalidated(@NonNull android.net.Network, boolean, boolean);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAvoidUnvalidated(@NonNull android.net.Network);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setFirewallChainEnabled(int, boolean);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setGlobalProxy(@Nullable android.net.ProxyInfo);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setLegacyLockdownVpnEnabled(boolean);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreference(@NonNull android.os.UserHandle, int, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreferences(@NonNull android.os.UserHandle, @NonNull java.util.List<android.net.ProfileNetworkPreference>, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setRequireVpnForUids(boolean, @NonNull java.util.Collection<android.util.Range<java.lang.Integer>>);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setUidFirewallRule(int, int, int);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setVpnDefaultForUids(@NonNull String, @NonNull java.util.Collection<android.util.Range<java.lang.Integer>>);
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_TEST_NETWORKS, android.Manifest.permission.NETWORK_STACK}) public void simulateDataStall(int, long, @NonNull android.net.Network, @NonNull android.os.PersistableBundle);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void startCaptivePortalApp(@NonNull android.net.Network);
-    method public void systemReady();
-    field public static final String ACTION_CLEAR_DNS_CACHE = "android.net.action.CLEAR_DNS_CACHE";
-    field public static final String ACTION_PROMPT_LOST_VALIDATION = "android.net.action.PROMPT_LOST_VALIDATION";
-    field public static final String ACTION_PROMPT_PARTIAL_CONNECTIVITY = "android.net.action.PROMPT_PARTIAL_CONNECTIVITY";
-    field public static final String ACTION_PROMPT_UNVALIDATED = "android.net.action.PROMPT_UNVALIDATED";
-    field public static final int BLOCKED_METERED_REASON_ADMIN_DISABLED = 262144; // 0x40000
-    field public static final int BLOCKED_METERED_REASON_DATA_SAVER = 65536; // 0x10000
-    field public static final int BLOCKED_METERED_REASON_MASK = -65536; // 0xffff0000
-    field public static final int BLOCKED_METERED_REASON_USER_RESTRICTED = 131072; // 0x20000
-    field public static final int BLOCKED_REASON_APP_STANDBY = 4; // 0x4
-    field public static final int BLOCKED_REASON_BATTERY_SAVER = 1; // 0x1
-    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 public static final int BLOCKED_REASON_NONE = 0; // 0x0
-    field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
-    field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
-    field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5
-    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
-    field public static final int FIREWALL_CHAIN_POWERSAVE = 3; // 0x3
-    field public static final int FIREWALL_CHAIN_RESTRICTED = 4; // 0x4
-    field public static final int FIREWALL_CHAIN_STANDBY = 2; // 0x2
-    field public static final int FIREWALL_RULE_ALLOW = 1; // 0x1
-    field public static final int FIREWALL_RULE_DEFAULT = 0; // 0x0
-    field public static final int FIREWALL_RULE_DENY = 2; // 0x2
-    field public static final int PROFILE_NETWORK_PREFERENCE_DEFAULT = 0; // 0x0
-    field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE = 1; // 0x1
-    field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE_BLOCKING = 3; // 0x3
-    field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK = 2; // 0x2
-  }
-
-  public static class ConnectivityManager.NetworkCallback {
-    method public void onBlockedStatusChanged(@NonNull android.net.Network, int);
-  }
-
-  public class ConnectivitySettingsManager {
-    method public static void clearGlobalProxy(@NonNull android.content.Context);
-    method @Nullable public static String getCaptivePortalHttpUrl(@NonNull android.content.Context);
-    method public static int getCaptivePortalMode(@NonNull android.content.Context, int);
-    method @NonNull public static java.time.Duration getConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method @NonNull public static android.util.Range<java.lang.Integer> getDnsResolverSampleRanges(@NonNull android.content.Context);
-    method @NonNull public static java.time.Duration getDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static int getDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, int);
-    method @Nullable public static android.net.ProxyInfo getGlobalProxy(@NonNull android.content.Context);
-    method public static long getIngressRateLimitInBytesPerSecond(@NonNull android.content.Context);
-    method @NonNull public static java.time.Duration getMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static boolean getMobileDataAlwaysOn(@NonNull android.content.Context, boolean);
-    method @NonNull public static java.util.Set<java.lang.Integer> getMobileDataPreferredUids(@NonNull android.content.Context);
-    method public static int getNetworkAvoidBadWifi(@NonNull android.content.Context);
-    method @Nullable public static String getNetworkMeteredMultipathPreference(@NonNull android.content.Context);
-    method public static int getNetworkSwitchNotificationMaximumDailyCount(@NonNull android.content.Context, int);
-    method @NonNull public static java.time.Duration getNetworkSwitchNotificationRateDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method @NonNull public static String getPrivateDnsDefaultMode(@NonNull android.content.Context);
-    method @Nullable public static String getPrivateDnsHostname(@NonNull android.content.Context);
-    method public static int getPrivateDnsMode(@NonNull android.content.Context);
-    method @NonNull public static java.util.Set<java.lang.Integer> getUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context);
-    method public static boolean getWifiAlwaysRequested(@NonNull android.content.Context, boolean);
-    method @NonNull public static java.time.Duration getWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setCaptivePortalHttpUrl(@NonNull android.content.Context, @Nullable String);
-    method public static void setCaptivePortalMode(@NonNull android.content.Context, int);
-    method public static void setConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setDnsResolverSampleRanges(@NonNull android.content.Context, @NonNull android.util.Range<java.lang.Integer>);
-    method public static void setDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, @IntRange(from=0, to=100) int);
-    method public static void setGlobalProxy(@NonNull android.content.Context, @NonNull android.net.ProxyInfo);
-    method public static void setIngressRateLimitInBytesPerSecond(@NonNull android.content.Context, @IntRange(from=-1L, to=4294967295L) long);
-    method public static void setMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setMobileDataAlwaysOn(@NonNull android.content.Context, boolean);
-    method public static void setMobileDataPreferredUids(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>);
-    method public static void setNetworkAvoidBadWifi(@NonNull android.content.Context, int);
-    method public static void setNetworkMeteredMultipathPreference(@NonNull android.content.Context, @NonNull String);
-    method public static void setNetworkSwitchNotificationMaximumDailyCount(@NonNull android.content.Context, @IntRange(from=0) int);
-    method public static void setNetworkSwitchNotificationRateDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setPrivateDnsDefaultMode(@NonNull android.content.Context, @NonNull int);
-    method public static void setPrivateDnsHostname(@NonNull android.content.Context, @Nullable String);
-    method public static void setPrivateDnsMode(@NonNull android.content.Context, int);
-    method public static void setUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>);
-    method public static void setWifiAlwaysRequested(@NonNull android.content.Context, boolean);
-    method public static void setWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    field public static final int CAPTIVE_PORTAL_MODE_AVOID = 2; // 0x2
-    field public static final int CAPTIVE_PORTAL_MODE_IGNORE = 0; // 0x0
-    field public static final int CAPTIVE_PORTAL_MODE_PROMPT = 1; // 0x1
-    field public static final int NETWORK_AVOID_BAD_WIFI_AVOID = 2; // 0x2
-    field public static final int NETWORK_AVOID_BAD_WIFI_IGNORE = 0; // 0x0
-    field public static final int NETWORK_AVOID_BAD_WIFI_PROMPT = 1; // 0x1
-    field public static final int PRIVATE_DNS_MODE_OFF = 1; // 0x1
-    field public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC = 2; // 0x2
-    field public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3; // 0x3
-  }
-
-  public final class DhcpOption implements android.os.Parcelable {
-    ctor public DhcpOption(byte, @Nullable byte[]);
-    method public int describeContents();
-    method public byte getType();
-    method @Nullable public byte[] getValue();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.DhcpOption> CREATOR;
-  }
-
-  public final class NetworkAgentConfig implements android.os.Parcelable {
-    method @Nullable public String getSubscriberId();
-    method public boolean isBypassableVpn();
-    method public boolean isVpnValidationRequired();
-  }
-
-  public static final class NetworkAgentConfig.Builder {
-    method @NonNull public android.net.NetworkAgentConfig.Builder setBypassableVpn(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLocalRoutesExcludedForVpn(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setSubscriberId(@Nullable String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setVpnRequiresValidation(boolean);
-  }
-
-  public final class NetworkCapabilities implements android.os.Parcelable {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public java.util.Set<java.lang.Integer> getAllowedUids();
-    method @Nullable public java.util.Set<android.util.Range<java.lang.Integer>> getUids();
-    method public boolean hasForbiddenCapability(int);
-    field public static final long REDACT_ALL = -1L; // 0xffffffffffffffffL
-    field public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1L; // 0x1L
-    field public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 2L; // 0x2L
-    field public static final long REDACT_FOR_NETWORK_SETTINGS = 4L; // 0x4L
-    field public static final long REDACT_NONE = 0L; // 0x0L
-    field public static final int TRANSPORT_TEST = 7; // 0x7
-  }
-
-  public static final class NetworkCapabilities.Builder {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAllowedUids(@NonNull java.util.Set<java.lang.Integer>);
-    method @NonNull public android.net.NetworkCapabilities.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
-  }
-
-  public class NetworkRequest implements android.os.Parcelable {
-    method @NonNull public int[] getEnterpriseIds();
-    method @NonNull public int[] getForbiddenCapabilities();
-    method public boolean hasEnterpriseId(int);
-    method public boolean hasForbiddenCapability(int);
-  }
-
-  public static class NetworkRequest.Builder {
-    method @NonNull public android.net.NetworkRequest.Builder addForbiddenCapability(int);
-    method @NonNull public android.net.NetworkRequest.Builder removeForbiddenCapability(int);
-    method @NonNull public android.net.NetworkRequest.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
-  }
-
-  public final class ProfileNetworkPreference implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public int[] getExcludedUids();
-    method @NonNull public int[] getIncludedUids();
-    method public int getPreference();
-    method public int getPreferenceEnterpriseId();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ProfileNetworkPreference> CREATOR;
-  }
-
-  public static final class ProfileNetworkPreference.Builder {
-    ctor public ProfileNetworkPreference.Builder();
-    method @NonNull public android.net.ProfileNetworkPreference build();
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setExcludedUids(@NonNull int[]);
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setIncludedUids(@NonNull int[]);
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setPreference(int);
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setPreferenceEnterpriseId(int);
-  }
-
-  public final class TestNetworkInterface implements android.os.Parcelable {
-    ctor public TestNetworkInterface(@NonNull android.os.ParcelFileDescriptor, @NonNull String);
-    method public int describeContents();
-    method @NonNull public android.os.ParcelFileDescriptor getFileDescriptor();
-    method @NonNull public String getInterfaceName();
-    method @Nullable public android.net.MacAddress getMacAddress();
-    method public int getMtu();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkInterface> CREATOR;
-  }
-
-  public class TestNetworkManager {
-    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTapInterface();
-    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTunInterface(@NonNull java.util.Collection<android.net.LinkAddress>);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void setupTestNetwork(@NonNull String, @NonNull android.os.IBinder);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void teardownTestNetwork(@NonNull android.net.Network);
-    field public static final String TEST_TAP_PREFIX = "testtap";
-  }
-
-  public final class TestNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
-    ctor public TestNetworkSpecifier(@NonNull String);
-    method public int describeContents();
-    method @Nullable public String getInterfaceName();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkSpecifier> CREATOR;
-  }
-
-  public interface TransportInfo {
-    method public default long getApplicableRedactions();
-    method @NonNull public default android.net.TransportInfo makeCopy(long);
-  }
-
-  public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
-    ctor @Deprecated public VpnTransportInfo(int, @Nullable String);
-    method @Nullable public String getSessionId();
-    method @NonNull public android.net.VpnTransportInfo makeCopy(long);
-  }
-
-}
-
diff --git a/framework/cronet_disabled/api/module-lib-removed.txt b/framework/cronet_disabled/api/module-lib-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/framework/cronet_disabled/api/module-lib-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/framework/cronet_disabled/api/removed.txt b/framework/cronet_disabled/api/removed.txt
deleted file mode 100644
index 303a1e6..0000000
--- a/framework/cronet_disabled/api/removed.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public class ConnectivityManager {
-    method @Deprecated public boolean requestRouteToHost(int, int);
-    method @Deprecated public int startUsingNetworkFeature(int, String);
-    method @Deprecated public int stopUsingNetworkFeature(int, String);
-  }
-
-}
-
diff --git a/framework/cronet_disabled/api/system-current.txt b/framework/cronet_disabled/api/system-current.txt
deleted file mode 100644
index 4a2ed8a..0000000
--- a/framework/cronet_disabled/api/system-current.txt
+++ /dev/null
@@ -1,544 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public class CaptivePortal implements android.os.Parcelable {
-    method @Deprecated public void logEvent(int, @NonNull String);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void reevaluateNetwork();
-    method public void useNetwork();
-    field public static final int APP_REQUEST_REEVALUATION_REQUIRED = 100; // 0x64
-    field public static final int APP_RETURN_DISMISSED = 0; // 0x0
-    field public static final int APP_RETURN_UNWANTED = 1; // 0x1
-    field public static final int APP_RETURN_WANTED_AS_IS = 2; // 0x2
-  }
-
-  public final class CaptivePortalData implements android.os.Parcelable {
-    method public int describeContents();
-    method public long getByteLimit();
-    method public long getExpiryTimeMillis();
-    method public long getRefreshTimeMillis();
-    method @Nullable public android.net.Uri getUserPortalUrl();
-    method public int getUserPortalUrlSource();
-    method @Nullable public CharSequence getVenueFriendlyName();
-    method @Nullable public android.net.Uri getVenueInfoUrl();
-    method public int getVenueInfoUrlSource();
-    method public boolean isCaptive();
-    method public boolean isSessionExtendable();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field public static final int CAPTIVE_PORTAL_DATA_SOURCE_OTHER = 0; // 0x0
-    field public static final int CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT = 1; // 0x1
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortalData> CREATOR;
-  }
-
-  public static class CaptivePortalData.Builder {
-    ctor public CaptivePortalData.Builder();
-    ctor public CaptivePortalData.Builder(@Nullable android.net.CaptivePortalData);
-    method @NonNull public android.net.CaptivePortalData build();
-    method @NonNull public android.net.CaptivePortalData.Builder setBytesRemaining(long);
-    method @NonNull public android.net.CaptivePortalData.Builder setCaptive(boolean);
-    method @NonNull public android.net.CaptivePortalData.Builder setExpiryTime(long);
-    method @NonNull public android.net.CaptivePortalData.Builder setRefreshTime(long);
-    method @NonNull public android.net.CaptivePortalData.Builder setSessionExtendable(boolean);
-    method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri);
-    method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri, int);
-    method @NonNull public android.net.CaptivePortalData.Builder setVenueFriendlyName(@Nullable CharSequence);
-    method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri);
-    method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri, int);
-  }
-
-  public class ConnectivityManager {
-    method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createNattKeepalive(@NonNull android.net.Network, @NonNull android.os.ParcelFileDescriptor, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
-    method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull java.net.Socket, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public String getCaptivePortalServerUrl();
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void getLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEntitlementResultListener);
-    method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public boolean isTetheringSupported();
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_FACTORY}) public int registerNetworkProvider(@NonNull android.net.NetworkProvider);
-    method public void registerQosCallback(@NonNull android.net.QosSocketInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.QosCallback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEventCallback);
-    method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void requestNetwork(@NonNull android.net.NetworkRequest, int, int, @NonNull android.os.Handler, @NonNull android.net.ConnectivityManager.NetworkCallback);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_AIRPLANE_MODE, android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void setAirplaneMode(boolean);
-    method @RequiresPermission(android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE) public void setOemNetworkPreference(@NonNull android.net.OemNetworkPreferences, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public boolean shouldAvoidBadWifi();
-    method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void startCaptivePortalApp(@NonNull android.net.Network, @NonNull android.os.Bundle);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback, android.os.Handler);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopTethering(int);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_FACTORY}) public void unregisterNetworkProvider(@NonNull android.net.NetworkProvider);
-    method public void unregisterQosCallback(@NonNull android.net.QosCallback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void unregisterTetheringEventCallback(@NonNull android.net.ConnectivityManager.OnTetheringEventCallback);
-    field public static final String EXTRA_CAPTIVE_PORTAL_PROBE_SPEC = "android.net.extra.CAPTIVE_PORTAL_PROBE_SPEC";
-    field public static final String EXTRA_CAPTIVE_PORTAL_USER_AGENT = "android.net.extra.CAPTIVE_PORTAL_USER_AGENT";
-    field public static final int TETHERING_BLUETOOTH = 2; // 0x2
-    field public static final int TETHERING_USB = 1; // 0x1
-    field public static final int TETHERING_WIFI = 0; // 0x0
-    field @Deprecated public static final int TETHER_ERROR_ENTITLEMENT_UNKONWN = 13; // 0xd
-    field @Deprecated public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
-    field @Deprecated public static final int TETHER_ERROR_PROVISION_FAILED = 11; // 0xb
-    field public static final int TYPE_NONE = -1; // 0xffffffff
-    field @Deprecated public static final int TYPE_PROXY = 16; // 0x10
-    field @Deprecated public static final int TYPE_WIFI_P2P = 13; // 0xd
-  }
-
-  @Deprecated public abstract static class ConnectivityManager.OnStartTetheringCallback {
-    ctor @Deprecated public ConnectivityManager.OnStartTetheringCallback();
-    method @Deprecated public void onTetheringFailed();
-    method @Deprecated public void onTetheringStarted();
-  }
-
-  @Deprecated public static interface ConnectivityManager.OnTetheringEntitlementResultListener {
-    method @Deprecated public void onTetheringEntitlementResult(int);
-  }
-
-  @Deprecated public abstract static class ConnectivityManager.OnTetheringEventCallback {
-    ctor @Deprecated public ConnectivityManager.OnTetheringEventCallback();
-    method @Deprecated public void onUpstreamChanged(@Nullable android.net.Network);
-  }
-
-  public final class DscpPolicy implements android.os.Parcelable {
-    method @Nullable public java.net.InetAddress getDestinationAddress();
-    method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange();
-    method public int getDscpValue();
-    method public int getPolicyId();
-    method public int getProtocol();
-    method @Nullable public java.net.InetAddress getSourceAddress();
-    method public int getSourcePort();
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
-    field public static final int PROTOCOL_ANY = -1; // 0xffffffff
-    field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
-  }
-
-  public static final class DscpPolicy.Builder {
-    ctor public DscpPolicy.Builder(int, int);
-    method @NonNull public android.net.DscpPolicy build();
-    method @NonNull public android.net.DscpPolicy.Builder setDestinationAddress(@NonNull java.net.InetAddress);
-    method @NonNull public android.net.DscpPolicy.Builder setDestinationPortRange(@NonNull android.util.Range<java.lang.Integer>);
-    method @NonNull public android.net.DscpPolicy.Builder setProtocol(int);
-    method @NonNull public android.net.DscpPolicy.Builder setSourceAddress(@NonNull java.net.InetAddress);
-    method @NonNull public android.net.DscpPolicy.Builder setSourcePort(int);
-  }
-
-  public final class InvalidPacketException extends java.lang.Exception {
-    ctor public InvalidPacketException(int);
-    method public int getError();
-    field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb
-    field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9
-    field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea
-  }
-
-  public final class IpConfiguration implements android.os.Parcelable {
-    ctor public IpConfiguration();
-    ctor public IpConfiguration(@NonNull android.net.IpConfiguration);
-    method @NonNull public android.net.IpConfiguration.IpAssignment getIpAssignment();
-    method @NonNull public android.net.IpConfiguration.ProxySettings getProxySettings();
-    method public void setHttpProxy(@Nullable android.net.ProxyInfo);
-    method public void setIpAssignment(@NonNull android.net.IpConfiguration.IpAssignment);
-    method public void setProxySettings(@NonNull android.net.IpConfiguration.ProxySettings);
-    method public void setStaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
-  }
-
-  public enum IpConfiguration.IpAssignment {
-    enum_constant public static final android.net.IpConfiguration.IpAssignment DHCP;
-    enum_constant public static final android.net.IpConfiguration.IpAssignment STATIC;
-    enum_constant public static final android.net.IpConfiguration.IpAssignment UNASSIGNED;
-  }
-
-  public enum IpConfiguration.ProxySettings {
-    enum_constant public static final android.net.IpConfiguration.ProxySettings NONE;
-    enum_constant public static final android.net.IpConfiguration.ProxySettings PAC;
-    enum_constant public static final android.net.IpConfiguration.ProxySettings STATIC;
-    enum_constant public static final android.net.IpConfiguration.ProxySettings UNASSIGNED;
-  }
-
-  public final class IpPrefix implements android.os.Parcelable {
-    ctor public IpPrefix(@NonNull String);
-  }
-
-  public class KeepalivePacketData {
-    ctor protected KeepalivePacketData(@NonNull java.net.InetAddress, @IntRange(from=0, to=65535) int, @NonNull java.net.InetAddress, @IntRange(from=0, to=65535) int, @NonNull byte[]) throws android.net.InvalidPacketException;
-    method @NonNull public java.net.InetAddress getDstAddress();
-    method public int getDstPort();
-    method @NonNull public byte[] getPacket();
-    method @NonNull public java.net.InetAddress getSrcAddress();
-    method public int getSrcPort();
-  }
-
-  public class LinkAddress implements android.os.Parcelable {
-    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int);
-    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int, long, long);
-    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int);
-    ctor public LinkAddress(@NonNull String);
-    ctor public LinkAddress(@NonNull String, int, int);
-    method public long getDeprecationTime();
-    method public long getExpirationTime();
-    method public boolean isGlobalPreferred();
-    method public boolean isIpv4();
-    method public boolean isIpv6();
-    method public boolean isSameAddressAs(@Nullable android.net.LinkAddress);
-    field public static final long LIFETIME_PERMANENT = 9223372036854775807L; // 0x7fffffffffffffffL
-    field public static final long LIFETIME_UNKNOWN = -1L; // 0xffffffffffffffffL
-  }
-
-  public final class LinkProperties implements android.os.Parcelable {
-    ctor public LinkProperties(@Nullable android.net.LinkProperties);
-    ctor public LinkProperties(@Nullable android.net.LinkProperties, boolean);
-    method public boolean addDnsServer(@NonNull java.net.InetAddress);
-    method public boolean addLinkAddress(@NonNull android.net.LinkAddress);
-    method public boolean addPcscfServer(@NonNull java.net.InetAddress);
-    method @NonNull public java.util.List<java.net.InetAddress> getAddresses();
-    method @NonNull public java.util.List<java.lang.String> getAllInterfaceNames();
-    method @NonNull public java.util.List<android.net.LinkAddress> getAllLinkAddresses();
-    method @NonNull public java.util.List<android.net.RouteInfo> getAllRoutes();
-    method @Nullable public android.net.Uri getCaptivePortalApiUrl();
-    method @Nullable public android.net.CaptivePortalData getCaptivePortalData();
-    method @NonNull public java.util.List<java.net.InetAddress> getPcscfServers();
-    method @Nullable public String getTcpBufferSizes();
-    method @NonNull public java.util.List<java.net.InetAddress> getValidatedPrivateDnsServers();
-    method public boolean hasGlobalIpv6Address();
-    method public boolean hasIpv4Address();
-    method public boolean hasIpv4DefaultRoute();
-    method public boolean hasIpv4DnsServer();
-    method public boolean hasIpv6DefaultRoute();
-    method public boolean hasIpv6DnsServer();
-    method public boolean isIpv4Provisioned();
-    method public boolean isIpv6Provisioned();
-    method public boolean isProvisioned();
-    method public boolean isReachable(@NonNull java.net.InetAddress);
-    method public boolean removeDnsServer(@NonNull java.net.InetAddress);
-    method public boolean removeLinkAddress(@NonNull android.net.LinkAddress);
-    method public boolean removeRoute(@NonNull android.net.RouteInfo);
-    method public void setCaptivePortalApiUrl(@Nullable android.net.Uri);
-    method public void setCaptivePortalData(@Nullable android.net.CaptivePortalData);
-    method public void setPcscfServers(@NonNull java.util.Collection<java.net.InetAddress>);
-    method public void setPrivateDnsServerName(@Nullable String);
-    method public void setTcpBufferSizes(@Nullable String);
-    method public void setUsePrivateDns(boolean);
-    method public void setValidatedPrivateDnsServers(@NonNull java.util.Collection<java.net.InetAddress>);
-  }
-
-  public final class NattKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable {
-    ctor public NattKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[]) throws android.net.InvalidPacketException;
-    method public int describeContents();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NattKeepalivePacketData> CREATOR;
-  }
-
-  public class Network implements android.os.Parcelable {
-    ctor public Network(@NonNull android.net.Network);
-    method public int getNetId();
-    method @NonNull public android.net.Network getPrivateDnsBypassingCopy();
-  }
-
-  public abstract class NetworkAgent {
-    ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, int, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider);
-    ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkScore, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider);
-    method @Nullable public android.net.Network getNetwork();
-    method public void markConnected();
-    method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData);
-    method public void onAutomaticReconnectDisabled();
-    method public void onBandwidthUpdateRequested();
-    method public void onDscpPolicyStatusUpdated(int, int);
-    method public void onNetworkCreated();
-    method public void onNetworkDestroyed();
-    method public void onNetworkUnwanted();
-    method public void onQosCallbackRegistered(int, @NonNull android.net.QosFilter);
-    method public void onQosCallbackUnregistered(int);
-    method public void onRemoveKeepalivePacketFilter(int);
-    method public void onSaveAcceptUnvalidated(boolean);
-    method public void onSignalStrengthThresholdsUpdated(@NonNull int[]);
-    method public void onStartSocketKeepalive(int, @NonNull java.time.Duration, @NonNull android.net.KeepalivePacketData);
-    method public void onStopSocketKeepalive(int);
-    method public void onValidationStatus(int, @Nullable android.net.Uri);
-    method @NonNull public android.net.Network register();
-    method public void sendAddDscpPolicy(@NonNull android.net.DscpPolicy);
-    method public void sendLinkProperties(@NonNull android.net.LinkProperties);
-    method public void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
-    method public void sendNetworkScore(@NonNull android.net.NetworkScore);
-    method public void sendNetworkScore(@IntRange(from=0, to=99) int);
-    method public final void sendQosCallbackError(int, int);
-    method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
-    method public final void sendQosSessionLost(int, int, int);
-    method public void sendRemoveAllDscpPolicies();
-    method public void sendRemoveDscpPolicy(int);
-    method public final void sendSocketKeepaliveEvent(int, int);
-    method @Deprecated public void setLegacySubtype(int, @NonNull String);
-    method public void setLingerDuration(@NonNull java.time.Duration);
-    method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int);
-    method public void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
-    method public void unregister();
-    method public void unregisterAfterReplacement(@IntRange(from=0, to=0x1388) int);
-    field public static final int DSCP_POLICY_STATUS_DELETED = 4; // 0x4
-    field public static final int DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3; // 0x3
-    field public static final int DSCP_POLICY_STATUS_POLICY_NOT_FOUND = 5; // 0x5
-    field public static final int DSCP_POLICY_STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2; // 0x2
-    field public static final int DSCP_POLICY_STATUS_REQUEST_DECLINED = 1; // 0x1
-    field public static final int DSCP_POLICY_STATUS_SUCCESS = 0; // 0x0
-    field public static final int VALIDATION_STATUS_NOT_VALID = 2; // 0x2
-    field public static final int VALIDATION_STATUS_VALID = 1; // 0x1
-  }
-
-  public final class NetworkAgentConfig implements android.os.Parcelable {
-    method public int describeContents();
-    method public int getLegacyType();
-    method @NonNull public String getLegacyTypeName();
-    method public boolean isExplicitlySelected();
-    method public boolean isPartialConnectivityAcceptable();
-    method public boolean isUnvalidatedConnectivityAcceptable();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkAgentConfig> CREATOR;
-  }
-
-  public static final class NetworkAgentConfig.Builder {
-    ctor public NetworkAgentConfig.Builder();
-    method @NonNull public android.net.NetworkAgentConfig build();
-    method @NonNull public android.net.NetworkAgentConfig.Builder setExplicitlySelected(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyExtraInfo(@NonNull String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubType(int);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubTypeName(@NonNull String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyType(int);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyTypeName(@NonNull String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setNat64DetectionEnabled(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setPartialConnectivityAcceptable(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setProvisioningNotificationEnabled(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setUnvalidatedConnectivityAcceptable(boolean);
-  }
-
-  public final class NetworkCapabilities implements android.os.Parcelable {
-    method @NonNull public int[] getAdministratorUids();
-    method @Nullable public static String getCapabilityCarrierName(int);
-    method @Nullable public String getSsid();
-    method @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
-    method @NonNull public int[] getTransportTypes();
-    method @Nullable public java.util.List<android.net.Network> getUnderlyingNetworks();
-    method public boolean isPrivateDnsBroken();
-    method public boolean satisfiedByNetworkCapabilities(@Nullable android.net.NetworkCapabilities);
-    field public static final int NET_CAPABILITY_BIP = 31; // 0x1f
-    field public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28; // 0x1c
-    field public static final int NET_CAPABILITY_OEM_PAID = 22; // 0x16
-    field public static final int NET_CAPABILITY_OEM_PRIVATE = 26; // 0x1a
-    field public static final int NET_CAPABILITY_PARTIAL_CONNECTIVITY = 24; // 0x18
-    field public static final int NET_CAPABILITY_VEHICLE_INTERNAL = 27; // 0x1b
-    field public static final int NET_CAPABILITY_VSIM = 30; // 0x1e
-  }
-
-  public static final class NetworkCapabilities.Builder {
-    ctor public NetworkCapabilities.Builder();
-    ctor public NetworkCapabilities.Builder(@NonNull android.net.NetworkCapabilities);
-    method @NonNull public android.net.NetworkCapabilities.Builder addCapability(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseId(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder addTransportType(int);
-    method @NonNull public android.net.NetworkCapabilities build();
-    method @NonNull public android.net.NetworkCapabilities.Builder removeCapability(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseId(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder removeTransportType(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAdministratorUids(@NonNull int[]);
-    method @NonNull public android.net.NetworkCapabilities.Builder setLinkDownstreamBandwidthKbps(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder setLinkUpstreamBandwidthKbps(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder setNetworkSpecifier(@Nullable android.net.NetworkSpecifier);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setOwnerUid(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setRequestorPackageName(@Nullable String);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setRequestorUid(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkCapabilities.Builder setSignalStrength(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setSsid(@Nullable String);
-    method @NonNull public android.net.NetworkCapabilities.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
-    method @NonNull public android.net.NetworkCapabilities.Builder setTransportInfo(@Nullable android.net.TransportInfo);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
-    method @NonNull public static android.net.NetworkCapabilities.Builder withoutDefaultCapabilities();
-  }
-
-  public class NetworkProvider {
-    ctor public NetworkProvider(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void declareNetworkRequestUnfulfillable(@NonNull android.net.NetworkRequest);
-    method public int getProviderId();
-    method public void onNetworkRequestWithdrawn(@NonNull android.net.NetworkRequest);
-    method public void onNetworkRequested(@NonNull android.net.NetworkRequest, @IntRange(from=0, to=99) int, int);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void registerNetworkOffer(@NonNull android.net.NetworkScore, @NonNull android.net.NetworkCapabilities, @NonNull java.util.concurrent.Executor, @NonNull android.net.NetworkProvider.NetworkOfferCallback);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void unregisterNetworkOffer(@NonNull android.net.NetworkProvider.NetworkOfferCallback);
-    field public static final int ID_NONE = -1; // 0xffffffff
-  }
-
-  public static interface NetworkProvider.NetworkOfferCallback {
-    method public void onNetworkNeeded(@NonNull android.net.NetworkRequest);
-    method public void onNetworkUnneeded(@NonNull android.net.NetworkRequest);
-  }
-
-  public class NetworkReleasedException extends java.lang.Exception {
-    ctor public NetworkReleasedException();
-  }
-
-  public class NetworkRequest implements android.os.Parcelable {
-    method @Nullable public String getRequestorPackageName();
-    method public int getRequestorUid();
-  }
-
-  public static class NetworkRequest.Builder {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkRequest.Builder setSignalStrength(int);
-    method @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
-  }
-
-  public final class NetworkScore implements android.os.Parcelable {
-    method public int describeContents();
-    method public int getKeepConnectedReason();
-    method public int getLegacyInt();
-    method public boolean isExiting();
-    method public boolean isTransportPrimary();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkScore> CREATOR;
-    field public static final int KEEP_CONNECTED_FOR_HANDOVER = 1; // 0x1
-    field public static final int KEEP_CONNECTED_NONE = 0; // 0x0
-  }
-
-  public static final class NetworkScore.Builder {
-    ctor public NetworkScore.Builder();
-    method @NonNull public android.net.NetworkScore build();
-    method @NonNull public android.net.NetworkScore.Builder setExiting(boolean);
-    method @NonNull public android.net.NetworkScore.Builder setKeepConnectedReason(int);
-    method @NonNull public android.net.NetworkScore.Builder setLegacyInt(int);
-    method @NonNull public android.net.NetworkScore.Builder setTransportPrimary(boolean);
-  }
-
-  public final class OemNetworkPreferences implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public java.util.Map<java.lang.String,java.lang.Integer> getNetworkPreferences();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.OemNetworkPreferences> CREATOR;
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID = 1; // 0x1
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK = 2; // 0x2
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY = 3; // 0x3
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY = 4; // 0x4
-    field public static final int OEM_NETWORK_PREFERENCE_UNINITIALIZED = 0; // 0x0
-  }
-
-  public static final class OemNetworkPreferences.Builder {
-    ctor public OemNetworkPreferences.Builder();
-    ctor public OemNetworkPreferences.Builder(@NonNull android.net.OemNetworkPreferences);
-    method @NonNull public android.net.OemNetworkPreferences.Builder addNetworkPreference(@NonNull String, int);
-    method @NonNull public android.net.OemNetworkPreferences build();
-    method @NonNull public android.net.OemNetworkPreferences.Builder clearNetworkPreference(@NonNull String);
-  }
-
-  public abstract class QosCallback {
-    ctor public QosCallback();
-    method public void onError(@NonNull android.net.QosCallbackException);
-    method public void onQosSessionAvailable(@NonNull android.net.QosSession, @NonNull android.net.QosSessionAttributes);
-    method public void onQosSessionLost(@NonNull android.net.QosSession);
-  }
-
-  public static class QosCallback.QosCallbackRegistrationException extends java.lang.RuntimeException {
-  }
-
-  public final class QosCallbackException extends java.lang.Exception {
-    ctor public QosCallbackException(@NonNull String);
-    ctor public QosCallbackException(@NonNull Throwable);
-  }
-
-  public abstract class QosFilter {
-    method @NonNull public abstract android.net.Network getNetwork();
-    method public abstract boolean matchesLocalAddress(@NonNull java.net.InetAddress, int, int);
-    method public boolean matchesProtocol(int);
-    method public abstract boolean matchesRemoteAddress(@NonNull java.net.InetAddress, int, int);
-  }
-
-  public final class QosSession implements android.os.Parcelable {
-    ctor public QosSession(int, int);
-    method public int describeContents();
-    method public int getSessionId();
-    method public int getSessionType();
-    method public long getUniqueId();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSession> CREATOR;
-    field public static final int TYPE_EPS_BEARER = 1; // 0x1
-    field public static final int TYPE_NR_BEARER = 2; // 0x2
-  }
-
-  public interface QosSessionAttributes {
-  }
-
-  public final class QosSocketInfo implements android.os.Parcelable {
-    ctor public QosSocketInfo(@NonNull android.net.Network, @NonNull java.net.Socket) throws java.io.IOException;
-    ctor public QosSocketInfo(@NonNull android.net.Network, @NonNull java.net.DatagramSocket) throws java.io.IOException;
-    method public int describeContents();
-    method @NonNull public java.net.InetSocketAddress getLocalSocketAddress();
-    method @NonNull public android.net.Network getNetwork();
-    method @Nullable public java.net.InetSocketAddress getRemoteSocketAddress();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSocketInfo> CREATOR;
-  }
-
-  public final class RouteInfo implements android.os.Parcelable {
-    ctor public RouteInfo(@Nullable android.net.IpPrefix, @Nullable java.net.InetAddress, @Nullable String, int);
-    ctor public RouteInfo(@Nullable android.net.IpPrefix, @Nullable java.net.InetAddress, @Nullable String, int, int);
-    method public int getMtu();
-  }
-
-  public abstract class SocketKeepalive implements java.lang.AutoCloseable {
-    method public final void start(@IntRange(from=0xa, to=0xe10) int, int, @Nullable android.net.Network);
-    field public static final int ERROR_NO_SUCH_SLOT = -33; // 0xffffffdf
-    field public static final int FLAG_AUTOMATIC_ON_OFF = 1; // 0x1
-    field public static final int SUCCESS = 0; // 0x0
-  }
-
-  public class SocketLocalAddressChangedException extends java.lang.Exception {
-    ctor public SocketLocalAddressChangedException();
-  }
-
-  public class SocketNotBoundException extends java.lang.Exception {
-    ctor public SocketNotBoundException();
-  }
-
-  public class SocketNotConnectedException extends java.lang.Exception {
-    ctor public SocketNotConnectedException();
-  }
-
-  public class SocketRemoteAddressChangedException extends java.lang.Exception {
-    ctor public SocketRemoteAddressChangedException();
-  }
-
-  public final class StaticIpConfiguration implements android.os.Parcelable {
-    ctor public StaticIpConfiguration();
-    ctor public StaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
-    method public void addDnsServer(@NonNull java.net.InetAddress);
-    method public void clear();
-    method @NonNull public java.util.List<android.net.RouteInfo> getRoutes(@Nullable String);
-  }
-
-  public final class TcpKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable {
-    ctor public TcpKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[], int, int, int, int, int, int) throws android.net.InvalidPacketException;
-    method public int describeContents();
-    method public int getIpTos();
-    method public int getIpTtl();
-    method public int getTcpAck();
-    method public int getTcpSeq();
-    method public int getTcpWindow();
-    method public int getTcpWindowScale();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TcpKeepalivePacketData> CREATOR;
-  }
-
-  public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
-    ctor public VpnTransportInfo(int, @Nullable String, boolean, boolean);
-    method public boolean areLongLivedTcpConnectionsExpensive();
-    method public int describeContents();
-    method public int getType();
-    method public boolean isBypassable();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.VpnTransportInfo> CREATOR;
-  }
-
-}
-
-package android.net.apf {
-
-  public final class ApfCapabilities implements android.os.Parcelable {
-    ctor public ApfCapabilities(int, int, int);
-    method public int describeContents();
-    method public static boolean getApfDrop8023Frames();
-    method @NonNull public static int[] getApfEtherTypeBlackList();
-    method public boolean hasDataAccess();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<android.net.apf.ApfCapabilities> CREATOR;
-    field public final int apfPacketFormat;
-    field public final int apfVersionSupported;
-    field public final int maximumApfProgramSize;
-  }
-
-}
-
diff --git a/framework/cronet_disabled/api/system-lint-baseline.txt b/framework/cronet_disabled/api/system-lint-baseline.txt
deleted file mode 100644
index 9a97707..0000000
--- a/framework/cronet_disabled/api/system-lint-baseline.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Baseline format: 1.0
diff --git a/framework/cronet_disabled/api/system-removed.txt b/framework/cronet_disabled/api/system-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/framework/cronet_disabled/api/system-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
index 9b48d57..09abd17 100644
--- a/framework/jarjar-excludes.txt
+++ b/framework/jarjar-excludes.txt
@@ -14,6 +14,16 @@
 # TODO: move files to android.net.connectivity.visiblefortesting
 android\.net\.IConnectivityDiagnosticsCallback(\$.+)?
 
+# Classes used by tethering as a hidden API are compiled as a lib in target
+# connectivity-internal-api-util. Because it's used by tethering, it can't
+# be jarjared. Classes in android.net.connectivity are exempt from being
+# listed here because they are already in the target package and as such
+# are already not jarjared.
+# Because Tethering can be installed on R without Connectivity, any use
+# of these classes must be protected by a check for >= S SDK.
+# It's unlikely anybody else declares a hidden class with this name ?
+android\.net\.RoutingCoordinatorManager(\$.+)?
+android\.net\.LocalNetworkInfo(\$.+)?
 
 # KeepaliveUtils is used by ConnectivityManager CTS
 # TODO: move into service-connectivity so framework-connectivity stops using
@@ -27,4 +37,4 @@
 # Don't touch anything that's already under android.net.http (cronet)
 # This is required since android.net.http contains api classes and hidden classes.
 # TODO: Remove this after hidden classes are moved to different package
-android\.net\.http\..+
\ No newline at end of file
+android\.net\.http\..+
diff --git a/framework/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp
index ca297e5..3779a00 100644
--- a/framework/jni/android_net_NetworkUtils.cpp
+++ b/framework/jni/android_net_NetworkUtils.cpp
@@ -24,8 +24,10 @@
 #include <string.h>
 
 #include <bpf/BpfClassic.h>
+#include <bpf/KernelUtils.h>
 #include <DnsProxydProtocol.h> // NETID_USE_LOCAL_NAMESERVERS
 #include <nativehelper/JNIPlatformHelp.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
 #include <utils/Log.h>
 
 #include "jni.h"
@@ -240,6 +242,23 @@
             trw.rcv_wnd, trw.rcv_wup, tcpinfo.tcpi_rcv_wscale);
 }
 
+static void android_net_utils_setsockoptBytes(JNIEnv *env, jclass clazz, jobject javaFd,
+        jint level, jint option, jbyteArray javaBytes) {
+    int sock = AFileDescriptor_getFd(env, javaFd);
+    ScopedByteArrayRO value(env, javaBytes);
+    if (setsockopt(sock, level, option, value.get(), value.size()) != 0) {
+        jniThrowErrnoException(env, "setsockoptBytes", errno);
+    }
+}
+
+static jboolean android_net_utils_isKernel64Bit(JNIEnv *env, jclass clazz) {
+    return bpf::isKernel64Bit();
+}
+
+static jboolean android_net_utils_isKernelX86(JNIEnv *env, jclass clazz) {
+    return bpf::isX86();
+}
+
 // ----------------------------------------------------------------------------
 
 /*
@@ -260,6 +279,10 @@
     { "resNetworkResult", "(Ljava/io/FileDescriptor;)Landroid/net/DnsResolver$DnsResponse;", (void*) android_net_utils_resNetworkResult },
     { "resNetworkCancel", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_resNetworkCancel },
     { "getDnsNetwork", "()Landroid/net/Network;", (void*) android_net_utils_getDnsNetwork },
+    { "setsockoptBytes", "(Ljava/io/FileDescriptor;II[B)V",
+    (void*) android_net_utils_setsockoptBytes},
+    { "isKernel64Bit", "()Z", (void*) android_net_utils_isKernel64Bit },
+    { "isKernelX86", "()Z", (void*) android_net_utils_isKernelX86 },
 };
 // clang-format on
 
diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml
index f68aad7..dddabef 100644
--- a/framework/lint-baseline.xml
+++ b/framework/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
@@ -8,78 +8,177 @@
         errorLine2="                                                                      ~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
-            line="2456"
+            line="2490"
             column="71"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.Proxy#setHttpProxyConfiguration`"
-        errorLine1="                Proxy.setHttpProxyConfiguration(getInstance().getDefaultProxy());"
-        errorLine2="                      ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
-            line="5323"
-            column="23"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
-        errorLine1="            if (!Build.isDebuggable()) {"
-        errorLine2="                       ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java"
-            line="1072"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
-        errorLine1="        final int end = nextUser.getUid(0 /* appId */) - 1;"
-        errorLine2="                                 ~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
-            line="50"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
-        errorLine1="        final int start = user.getUid(0 /* appId */);"
-        errorLine2="                               ~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
-            line="49"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="NewApi"
         message="Call requires API level 31 (current min is 30): `android.provider.Settings#checkAndNoteWriteSettingsOperation`"
         errorLine1="        return Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,"
         errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
-            line="2799"
+            line="2853"
             column="25"/>
     </issue>
 
     <issue
         id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.Proxy#setHttpProxyConfiguration`"
+        errorLine1="                Proxy.setHttpProxyConfiguration(getInstance().getDefaultProxy());"
+        errorLine2="                      ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+            line="5422"
+            column="23"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 31 (current min is 30): `java.net.InetAddress#clearDnsCache`"
         errorLine1="            InetAddress.clearDnsCache();"
         errorLine2="                        ~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
-            line="5329"
+            line="5428"
             column="25"/>
     </issue>
 
     <issue
         id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#dispatchNetworkConfigurationChange`"
+        errorLine1="            NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
+        errorLine2="                                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+            line="5431"
+            column="50"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#getInstance`"
+        errorLine1="            NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
+        errorLine2="                                   ~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+            line="5431"
+            column="36"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+        errorLine1="            if (!Build.isDebuggable()) {"
+        errorLine2="                       ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java"
+            line="1095"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 31 (current min is 30): `android.system.OsConstants#ENONET`"
+        errorLine1='                    new DnsException(ERROR_SYSTEM, new ErrnoException("resNetworkQuery", ENONET))));'
+        errorLine2="                                                                                         ~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/DnsResolver.java"
+            line="367"
+            column="90"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="            IoUtils.closeQuietly(socket);"
+        errorLine2="                    ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
+            line="181"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="            IoUtils.closeQuietly(socket);"
+        errorLine2="                    ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
+            line="373"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="                        IoUtils.closeQuietly(is);"
+        errorLine2="                                ~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+            line="171"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="            IoUtils.closeQuietly(zos);"
+        errorLine2="                    ~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+            line="178"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="            IoUtils.closeQuietly(bis);"
+        errorLine2="                    ~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+            line="401"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="            IoUtils.closeQuietly(bos);"
+        errorLine2="                    ~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+            line="416"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#isNumericAddress`"
+        errorLine1="        return InetAddressUtils.isNumericAddress(address);"
+        errorLine2="                                ~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
+            line="46"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#parseNumericAddress`"
+        errorLine1="        return InetAddressUtils.parseNumericAddress(address);"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
+            line="63"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 31 (current min is 30): `java.net.InetAddress#getAllByNameOnNet`"
         errorLine1="        return InetAddress.getAllByNameOnNet(host, getNetIdForResolv());"
         errorLine2="                           ~~~~~~~~~~~~~~~~~">
@@ -103,17 +202,6 @@
     <issue
         id="NewApi"
         message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="                        IoUtils.closeQuietly(is);"
-        errorLine2="                                ~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
-            line="168"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
         errorLine1="                        if (failed) IoUtils.closeQuietly(socket);"
         errorLine2="                                            ~~~~~~~~~~~~">
         <location
@@ -157,105 +245,6 @@
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="            IoUtils.closeQuietly(bis);"
-        errorLine2="                    ~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
-            line="391"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="            IoUtils.closeQuietly(bos);"
-        errorLine2="                    ~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
-            line="406"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="            IoUtils.closeQuietly(socket);"
-        errorLine2="                    ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
-            line="181"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="            IoUtils.closeQuietly(socket);"
-        errorLine2="                    ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
-            line="373"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="            IoUtils.closeQuietly(zos);"
-        errorLine2="                    ~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
-            line="175"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#isNumericAddress`"
-        errorLine1="        return InetAddressUtils.isNumericAddress(address);"
-        errorLine2="                                ~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
-            line="46"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#parseNumericAddress`"
-        errorLine1="        return InetAddressUtils.parseNumericAddress(address);"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
-            line="63"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#dispatchNetworkConfigurationChange`"
-        errorLine1="            NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
-        errorLine2="                                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
-            line="5332"
-            column="50"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#getInstance`"
-        errorLine1="            NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
-        errorLine2="                                   ~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
-            line="5332"
-            column="36"/>
-    </issue>
-
-    <issue
-        id="NewApi"
         message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#createInstance`"
         errorLine1="        HttpURLConnectionFactory urlConnectionFactory = HttpURLConnectionFactory.createInstance();"
         errorLine2="                                                                                 ~~~~~~~~~~~~~~">
@@ -267,17 +256,6 @@
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#openConnection`"
-        errorLine1="        return urlConnectionFactory.openConnection(url, socketFactory, proxy);"
-        errorLine2="                                    ~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/Network.java"
-            line="372"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="NewApi"
         message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#setDns`"
         errorLine1="        urlConnectionFactory.setDns(dnsLookup); // Let traffic go via dnsLookup"
         errorLine2="                             ~~~~~~">
@@ -300,35 +278,13 @@
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
-        errorLine1="                    return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
-        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#openConnection`"
+        errorLine1="        return urlConnectionFactory.openConnection(url, socketFactory, proxy);"
+        errorLine2="                                    ~~~~~~~~~~~~~~">
         <location
-            file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
-            line="525"
-            column="48"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
-        errorLine1="                    return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
-        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
-            line="525"
-            column="48"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.data.EpsBearerQosSessionAttributes`"
-        errorLine1="                    (EpsBearerQosSessionAttributes)attributes));"
-        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
-            line="1421"
-            column="22"/>
+            file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+            line="372"
+            column="37"/>
     </issue>
 
     <issue
@@ -338,18 +294,18 @@
         errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
-            line="1418"
+            line="1462"
             column="35"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.data.NrQosSessionAttributes`"
-        errorLine1="                    (NrQosSessionAttributes)attributes));"
-        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~">
+        message="Class requires API level 31 (current min is 30): `android.telephony.data.EpsBearerQosSessionAttributes`"
+        errorLine1="                    (EpsBearerQosSessionAttributes)attributes));"
+        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
-            line="1425"
+            line="1465"
             column="22"/>
     </issue>
 
@@ -360,8 +316,338 @@
         errorLine2="                                         ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
-            line="1422"
+            line="1466"
             column="42"/>
     </issue>
 
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.telephony.data.NrQosSessionAttributes`"
+        errorLine1="                    (NrQosSessionAttributes)attributes));"
+        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+            line="1469"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
+        errorLine1="                    return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
+        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+            line="553"
+            column="48"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+        errorLine1="                    return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
+        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+            line="553"
+            column="48"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+        errorLine1="        final int start = user.getUid(0 /* appId */);"
+        errorLine2="                               ~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
+            line="49"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+        errorLine1="        final int end = nextUser.getUid(0 /* appId */) - 1;"
+        errorLine2="                                 ~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
+            line="50"
+            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
new file mode 100644
index 0000000..f3773de
--- /dev/null
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -0,0 +1,150 @@
+/*
+ * 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.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 android.util.Pair;
+
+import com.android.net.module.util.Struct;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * BpfNetMaps related constants that can be shared among modules.
+ *
+ * @hide
+ */
+// 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.
+public class BpfNetMapsConstants {
+    // Prevent this class from being accidental instantiated.
+    private BpfNetMapsConstants() {}
+
+    public static final String CONFIGURATION_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_configuration_map";
+    public static final String UID_OWNER_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_uid_owner_map";
+    public static final String UID_PERMISSION_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_uid_permission_map";
+    public static final String COOKIE_TAG_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
+    public static final String DATA_SAVER_ENABLED_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_data_saver_enabled_map";
+    public static final String INGRESS_DISCARD_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_ingress_discard_map";
+    public static final Struct.S32 UID_RULES_CONFIGURATION_KEY = new Struct.S32(0);
+    public static final Struct.S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new Struct.S32(1);
+    public static final Struct.S32 DATA_SAVER_ENABLED_KEY = new Struct.S32(0);
+
+    public static final short DATA_SAVER_DISABLED = 0;
+    public static final short DATA_SAVER_ENABLED = 1;
+
+    // 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_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);
+    public static final long RESTRICTED_MATCH = (1 << 5);
+    public static final long LOW_POWER_STANDBY_MATCH = (1 << 6);
+    public static final long IIF_MATCH = (1 << 7);
+    public static final long LOCKDOWN_VPN_MATCH = (1 << 8);
+    public static final long OEM_DENY_1_MATCH = (1 << 9);
+    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_USER_MATCH, "PENALTY_BOX_USER_MATCH"),
+            Pair.create(DOZABLE_MATCH, "DOZABLE_MATCH"),
+            Pair.create(STANDBY_MATCH, "STANDBY_MATCH"),
+            Pair.create(POWERSAVE_MATCH, "POWERSAVE_MATCH"),
+            Pair.create(RESTRICTED_MATCH, "RESTRICTED_MATCH"),
+            Pair.create(LOW_POWER_STANDBY_MATCH, "LOW_POWER_STANDBY_MATCH"),
+            Pair.create(IIF_MATCH, "IIF_MATCH"),
+            Pair.create(LOCKDOWN_VPN_MATCH, "LOCKDOWN_VPN_MATCH"),
+            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(PENALTY_BOX_ADMIN_MATCH, "PENALTY_BOX_ADMIN_MATCH")
+    );
+
+    /**
+     * 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.
+     */
+    public static final List<Integer> ALLOW_CHAINS = List.of(
+            FIREWALL_CHAIN_DOZABLE,
+            FIREWALL_CHAIN_POWERSAVE,
+            FIREWALL_CHAIN_RESTRICTED,
+            FIREWALL_CHAIN_LOW_POWER_STANDBY,
+            FIREWALL_CHAIN_BACKGROUND
+    );
+
+    /**
+     * 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.
+     */
+    public static final List<Integer> DENY_CHAINS = List.of(
+            FIREWALL_CHAIN_STANDBY,
+            FIREWALL_CHAIN_OEM_DENY_1,
+            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/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
new file mode 100644
index 0000000..282a11e
--- /dev/null
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -0,0 +1,375 @@
+/*
+ * 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.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;
+
+/**
+ * The classes and the methods for BpfNetMaps utilization.
+ *
+ * @hide
+ */
+// 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() {}
+
+    /**
+     * Get corresponding match from firewall chain.
+     */
+    public static long getMatchByFirewallChain(final int chain) {
+        switch (chain) {
+            case FIREWALL_CHAIN_DOZABLE:
+                return DOZABLE_MATCH;
+            case FIREWALL_CHAIN_STANDBY:
+                return STANDBY_MATCH;
+            case FIREWALL_CHAIN_POWERSAVE:
+                return POWERSAVE_MATCH;
+            case FIREWALL_CHAIN_RESTRICTED:
+                return RESTRICTED_MATCH;
+            case FIREWALL_CHAIN_BACKGROUND:
+                return BACKGROUND_MATCH;
+            case FIREWALL_CHAIN_LOW_POWER_STANDBY:
+                return LOW_POWER_STANDBY_MATCH;
+            case FIREWALL_CHAIN_OEM_DENY_1:
+                return OEM_DENY_1_MATCH;
+            case FIREWALL_CHAIN_OEM_DENY_2:
+                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);
+        }
+    }
+
+    /**
+     * Get whether the chain is an allow-list or a deny-list.
+     *
+     * ALLOWLIST means the firewall denies all by default, uids must be explicitly allowed
+     * 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) || METERED_ALLOW_CHAINS.contains(chain)) {
+            return true;
+        } else if (DENY_CHAINS.contains(chain) || METERED_DENY_CHAINS.contains(chain)) {
+            return false;
+        }
+        throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
+    }
+
+    /**
+     * Get match string representation from the given match bitmap.
+     */
+    public static String matchToString(long matchMask) {
+        if (matchMask == NO_MATCH) {
+            return "NO_MATCH";
+        }
+
+        final StringJoiner sj = new StringJoiner(" ");
+        for (final Pair<Long, String> match : MATCH_LIST) {
+            final long matchFlag = match.first;
+            final String matchName = match.second;
+            if ((matchMask & matchFlag) != 0) {
+                sj.add(matchName);
+                matchMask &= ~matchFlag;
+            }
+        }
+        if (matchMask != 0) {
+            sj.add("UNKNOWN_MATCH(" + matchMask + ")");
+        }
+        return sj.toString();
+    }
+
+    /**
+     * Throw UnsupportedOperationException if SdkLevel is before T.
+     */
+    public static void throwIfPreT(final String msg) {
+        if (!SdkLevel.isAtLeastT()) {
+            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
+    ) {
+        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");
+
+        // 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 false;
+        }
+
+        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 2315521..a6a967b 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -26,9 +26,12 @@
 import static android.net.QosCallback.QosCallbackRegistrationException;
 
 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;
 import android.annotation.RequiresPermission;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
@@ -68,22 +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;
@@ -115,6 +125,24 @@
     private static final String TAG = "ConnectivityManager";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
+    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+    // available here
+    /** @hide */
+    public static class Flags {
+        static final String SET_DATA_SAVER_VIA_CM =
+                "com.android.net.flags.set_data_saver_via_cm";
+        static final String SUPPORT_IS_UID_NETWORKING_BLOCKED =
+                "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";
+    }
+
     /**
      * A change in network connectivity has occurred. A default connection has either
      * been established or lost. The NetworkInfo for the affected network is
@@ -886,6 +914,40 @@
     public static final int BLOCKED_REASON_LOW_POWER_STANDBY = 1 << 5;
 
     /**
+     * Flag to indicate that an app is subject to default background restrictions that would
+     * result in its network access being blocked.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    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.
      *
@@ -924,6 +986,9 @@
             BLOCKED_REASON_RESTRICTED_MODE,
             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,
@@ -941,7 +1006,6 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     private final IConnectivityManager mService;
 
-    // LINT.IfChange(firewall_chain)
     /**
      * Firewall chain for device idle (doze mode).
      * Allowlist of apps that have network access in device idle.
@@ -983,6 +1047,16 @@
     public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5;
 
     /**
+     * Firewall chain used for always-on default background restrictions.
+     * Allowlist of apps that have access because either they are in the foreground or they are
+     * exempted for specific situations while in the background.
+     * @hide
+     */
+    @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_CHAIN_BACKGROUND = 6;
+
+    /**
      * Firewall chain used for OEM-specific application restrictions.
      *
      * Denylist of apps that will not have network access due to OEM-specific restrictions. If an
@@ -1033,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 = {
@@ -1041,12 +1170,15 @@
         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,
+        FIREWALL_CHAIN_METERED_ALLOW,
+        FIREWALL_CHAIN_METERED_DENY_USER,
+        FIREWALL_CHAIN_METERED_DENY_ADMIN
     })
     public @interface FirewallChain {}
-    // LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h)
 
     /**
      * A firewall rule which allows or drops packets depending on existing policy.
@@ -1082,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.
@@ -1095,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) {
@@ -3811,11 +3974,28 @@
     @RequiresPermission(anyOf = {
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
             android.Manifest.permission.NETWORK_FACTORY})
-    public Network registerNetworkAgent(INetworkAgent na, NetworkInfo ni, LinkProperties lp,
-            NetworkCapabilities nc, @NonNull NetworkScore score, NetworkAgentConfig config,
-            int providerId) {
+    public Network registerNetworkAgent(@NonNull INetworkAgent na, @NonNull NetworkInfo ni,
+            @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+            @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, int providerId) {
+        return registerNetworkAgent(na, ni, lp, nc, null /* localNetworkConfig */, score, config,
+                providerId);
+    }
+
+    /**
+     * @hide
+     * Register a NetworkAgent with ConnectivityService.
+     * @return Network corresponding to NetworkAgent.
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_FACTORY})
+    public Network registerNetworkAgent(@NonNull INetworkAgent na, @NonNull NetworkInfo ni,
+            @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+            @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+            @NonNull NetworkAgentConfig config, int providerId) {
         try {
-            return mService.registerNetworkAgent(na, ni, lp, nc, score, config, providerId);
+            return mService.registerNetworkAgent(na, ni, lp, nc, score, localNetworkConfig, config,
+                    providerId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -3835,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
          */
@@ -3897,6 +4126,7 @@
                 throw new IllegalArgumentException("Invalid flags");
             }
             mFlags = flags;
+            mConstructorWasCalled = true;
         }
 
         /**
@@ -3914,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.
@@ -3924,18 +4156,29 @@
          * @param network The {@link Network} of the satisfying network.
          * @param networkCapabilities The {@link NetworkCapabilities} of the satisfying network.
          * @param linkProperties The {@link LinkProperties} of the satisfying network.
+         * @param localInfo The {@link LocalNetworkInfo} of the satisfying network, or null
+         *                  if this network is not a local network.
          * @param blocked Whether access to the {@link Network} is blocked due to system policy.
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONAVAILABLE_5ARGS,
+                calledByCallbackId = CALLBACK_AVAILABLE,
+                mayCall = { METHOD_ONAVAILABLE_4ARGS,
+                        METHOD_ONLOCALNETWORKINFOCHANGED,
+                        METHOD_ONBLOCKEDSTATUSCHANGED_INT })
         public final void onAvailable(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities,
-                @NonNull LinkProperties linkProperties, @BlockedReason int blocked) {
+                @NonNull LinkProperties linkProperties,
+                @Nullable LocalNetworkInfo localInfo,
+                @BlockedReason int blocked) {
             // Internally only this method is called when a new network is available, and
             // it calls the callback in the same way and order that older versions used
             // to call so as not to change the behavior.
             onAvailable(network, networkCapabilities, linkProperties, blocked != 0);
+            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.
@@ -3948,9 +4191,17 @@
          *
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONAVAILABLE_4ARGS,
+                calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY,
+                mayCall = { METHOD_ONAVAILABLE_1ARG,
+                        METHOD_ONNETWORKSUSPENDED,
+                        METHOD_ONCAPABILITIESCHANGED,
+                        METHOD_ONLINKPROPERTIESCHANGED
+                })
         public void onAvailable(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities,
-                @NonNull LinkProperties linkProperties, boolean blocked) {
+                @NonNull LinkProperties linkProperties,
+                boolean blocked) {
             onAvailable(network);
             if (!networkCapabilities.hasCapability(
                     NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)) {
@@ -3960,6 +4211,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.
@@ -3990,7 +4242,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
@@ -4009,7 +4264,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
@@ -4030,7 +4287,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
@@ -4040,7 +4299,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
@@ -4057,8 +4318,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}.
@@ -4073,8 +4337,27 @@
          * @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.
+         *
+         * This is only called for local networks, that is those with the
+         * NET_CAPABILITY_LOCAL_NETWORK network capability.
+         *
+         * @param network the {@link Network} whose local network info has changed.
+         * @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
@@ -4093,7 +4376,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
@@ -4107,7 +4393,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.
@@ -4120,7 +4408,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
@@ -4138,10 +4429,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;
@@ -4169,32 +4464,38 @@
         }
     }
 
+    private static final int CALLBACK_TRANSITIVE_CALLS_ONLY     = 0;
     /** @hide */
-    public static final int CALLBACK_PRECHECK            = 1;
+    public static final int CALLBACK_PRECHECK                   = 1;
     /** @hide */
-    public static final int CALLBACK_AVAILABLE           = 2;
+    public static final int CALLBACK_AVAILABLE                  = 2;
     /** @hide arg1 = TTL */
-    public static final int CALLBACK_LOSING              = 3;
+    public static final int CALLBACK_LOSING                     = 3;
     /** @hide */
-    public static final int CALLBACK_LOST                = 4;
+    public static final int CALLBACK_LOST                       = 4;
     /** @hide */
-    public static final int CALLBACK_UNAVAIL             = 5;
+    public static final int CALLBACK_UNAVAIL                    = 5;
     /** @hide */
-    public static final int CALLBACK_CAP_CHANGED         = 6;
+    public static final int CALLBACK_CAP_CHANGED                = 6;
     /** @hide */
-    public static final int CALLBACK_IP_CHANGED          = 7;
+    public static final int CALLBACK_IP_CHANGED                 = 7;
     /** @hide obj = NetworkCapabilities, arg1 = seq number */
-    private static final int EXPIRE_LEGACY_REQUEST       = 8;
+    private static final int EXPIRE_LEGACY_REQUEST              = 8;
     /** @hide */
-    public static final int CALLBACK_SUSPENDED           = 9;
+    public static final int CALLBACK_SUSPENDED                  = 9;
     /** @hide */
-    public static final int CALLBACK_RESUMED             = 10;
+    public static final int CALLBACK_RESUMED                    = 10;
     /** @hide */
-    public static final int CALLBACK_BLK_CHANGED         = 11;
+    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";
@@ -4206,11 +4507,74 @@
             case CALLBACK_SUSPENDED:    return "CALLBACK_SUSPENDED";
             case CALLBACK_RESUMED:      return "CALLBACK_RESUMED";
             case CALLBACK_BLK_CHANGED:  return "CALLBACK_BLK_CHANGED";
+            case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: return "CALLBACK_LOCAL_NETWORK_INFO_CHANGED";
             default:
                 return Integer.toString(whichCallback);
         }
     }
 
+    /** @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;
@@ -4260,7 +4624,8 @@
                 case CALLBACK_AVAILABLE: {
                     NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
                     LinkProperties lp = getObject(message, LinkProperties.class);
-                    callback.onAvailable(network, cap, lp, message.arg1);
+                    LocalNetworkInfo lni = getObject(message, LocalNetworkInfo.class);
+                    callback.onAvailable(network, cap, lp, lni, message.arg1);
                     break;
                 }
                 case CALLBACK_LOSING: {
@@ -4285,6 +4650,11 @@
                     callback.onLinkPropertiesChanged(network, lp);
                     break;
                 }
+                case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
+                    final LocalNetworkInfo info = getObject(message, LocalNetworkInfo.class);
+                    callback.onLocalNetworkInfoChanged(network, info);
+                    break;
+                }
                 case CALLBACK_SUSPENDED: {
                     callback.onNetworkSuspended(network);
                     break;
@@ -4324,6 +4694,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 {
@@ -4340,11 +4718,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);
@@ -4359,6 +4738,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,
@@ -5941,6 +6436,35 @@
     }
 
     /**
+     * Sets data saver switch.
+     *
+     * <p>This API configures the bandwidth control, and filling data saver status in BpfMap,
+     * which is intended for internal use by the network stack to optimize performance
+     * when frequently checking data saver status for multiple uids without doing IPC.
+     * It does not directly control the global data saver mode that users manage in settings.
+     * To query the comprehensive data saver status for a specific UID, including allowlist
+     * considerations, use {@link #getRestrictBackgroundStatus}.
+     *
+     * @param enable True if enable.
+     * @throws IllegalStateException if failed.
+     * @hide
+     */
+    @FlaggedApi(Flags.SET_DATA_SAVER_VIA_CM)
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    public void setDataSaverEnabled(final boolean enable) {
+        try {
+            mService.setDataSaverEnabled(enable);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Adds the specified UID to the list of UIds that are allowed to use data on metered networks
      * even when background data is restricted. The deny list takes precedence over the allow list.
      *
@@ -5956,7 +6480,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();
         }
@@ -5979,7 +6503,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();
         }
@@ -5989,10 +6513,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,
@@ -6001,7 +6532,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();
         }
@@ -6012,10 +6543,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,
@@ -6024,7 +6562,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();
         }
@@ -6082,6 +6620,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.
@@ -6149,6 +6691,44 @@
         }
     }
 
+    /**
+     * Return whether the network is blocked for the given uid and metered condition.
+     *
+     * Similar to {@link NetworkPolicyManager#isUidNetworkingBlocked}, but directly reads the BPF
+     * maps and therefore considerably faster. For use by the NetworkStack process only.
+     *
+     * @param uid The target uid.
+     * @param isNetworkMetered Whether the target network is metered.
+     *
+     * @return True if all networking with the given condition is blocked. Otherwise, false.
+     * @throws IllegalStateException if the map cannot be opened.
+     * @throws ServiceSpecificException if the read fails.
+     * @hide
+     */
+    // This isn't protected by a standard Android permission since it can't
+    // afford to do IPC for performance reasons. Instead, the access control
+    // is provided by linux file group permission AID_NET_BW_ACCT and the
+    // selinux context fs_bpf_net*.
+    // Only the system server process and the network stack have access.
+    @FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
+    @SystemApi(client = MODULE_LIBRARIES)
+    // Note b/326143935 kernel bug can trigger crash on some T device.
+    @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+    public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
+        if (!SdkLevel.isAtLeastU()) {
+            throw new IllegalStateException(
+                    "isUidNetworkingBlocked is not supported on pre-U devices");
+        }
+        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);
+    }
+
     /** @hide */
     public IBinder getCompanionDeviceManagerProxyService() {
         try {
@@ -6157,4 +6737,14 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /** @hide */
+    @RequiresApi(Build.VERSION_CODES.S)
+    public IBinder getRoutingCoordinatorService() {
+        try {
+            return mService.getRoutingCoordinatorService();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java
index 822e67d..ba7df7f 100644
--- a/framework/src/android/net/ConnectivitySettingsManager.java
+++ b/framework/src/android/net/ConnectivitySettingsManager.java
@@ -28,6 +28,7 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.net.ConnectivityManager.MultipathPreference;
 import android.os.Binder;
 import android.os.Build;
@@ -36,6 +37,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import android.util.Log;
 import android.util.Range;
 
 import com.android.net.module.util.ConnectivitySettingsUtils;
@@ -55,6 +57,7 @@
  */
 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
 public class ConnectivitySettingsManager {
+    private static final String TAG = ConnectivitySettingsManager.class.getSimpleName();
 
     private ConnectivitySettingsManager() {}
 
@@ -173,7 +176,9 @@
 
     /**
      * When detecting a captive portal, immediately disconnect from the
-     * network and do not reconnect to that network in the future.
+     * network and do not reconnect to that network in the future; except
+     * on Wear platform companion proxy networks (transport BLUETOOTH)
+     * will stay behind captive portal.
      */
     public static final int CAPTIVE_PORTAL_MODE_AVOID = 2;
 
@@ -696,10 +701,20 @@
     /**
      * Set global http proxy settings from given {@link ProxyInfo}.
      *
+     * <p class="note">
+     * While a {@link ProxyInfo} for a PAC proxy can be specified, not all devices support
+     * PAC proxies. In particular, smaller devices like watches often do not have the capabilities
+     * necessary to interpret the PAC file. In such cases, calling this API with a PAC proxy
+     * results in undefined behavior, including possibly breaking networking for applications.
+     * You can test for this by checking for the presence of {@link PackageManager.FEATURE_WEBVIEW}.
+     * </p>
+     *
      * @param context The {@link Context} to set the setting.
      * @param proxyInfo The {@link ProxyInfo} for global http proxy settings which build from
      *                    {@link ProxyInfo#buildPacProxy(Uri)} or
      *                    {@link ProxyInfo#buildDirectProxy(String, int, List)}
+     * @throws UnsupportedOperationException if |proxyInfo| codes for a PAC proxy but the system
+     *                                       does not support PAC proxies.
      */
     public static void setGlobalProxy(@NonNull Context context, @NonNull ProxyInfo proxyInfo) {
         final String host = proxyInfo.getHost();
@@ -707,6 +722,14 @@
         final String exclusionList = proxyInfo.getExclusionListAsString();
         final String pacFileUrl = proxyInfo.getPacFileUrl().toString();
 
+
+        if (!TextUtils.isEmpty(pacFileUrl)) {
+            final PackageManager pm = context.getPackageManager();
+            if (null != pm && !pm.hasSystemFeature(PackageManager.FEATURE_WEBVIEW)) {
+                Log.wtf(TAG, "PAC proxy can't be installed on a device without FEATURE_WEBVIEW");
+            }
+        }
+
         if (TextUtils.isEmpty(pacFileUrl)) {
             Settings.Global.putString(context.getContentResolver(), GLOBAL_HTTP_PROXY_HOST, host);
             Settings.Global.putInt(context.getContentResolver(), GLOBAL_HTTP_PROXY_PORT, port);
diff --git a/framework/src/android/net/DnsResolver.java b/framework/src/android/net/DnsResolver.java
index c6034f1..5fefcd6 100644
--- a/framework/src/android/net/DnsResolver.java
+++ b/framework/src/android/net/DnsResolver.java
@@ -77,6 +77,15 @@
     @interface QueryType {}
     public static final int TYPE_A = 1;
     public static final int TYPE_AAAA = 28;
+    // TODO: add below constants as part of QueryType and the public API
+    /** @hide */
+    public static final int TYPE_PTR = 12;
+    /** @hide */
+    public static final int TYPE_TXT = 16;
+    /** @hide */
+    public static final int TYPE_SRV = 33;
+    /** @hide */
+    public static final int TYPE_ANY = 255;
 
     @IntDef(prefix = { "FLAG_" }, value = {
             FLAG_EMPTY,
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index ebe8bca..988cc92 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -27,6 +27,7 @@
 import android.net.IQosCallback;
 import android.net.ISocketKeepaliveCallback;
 import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
 import android.net.Network;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
@@ -146,12 +147,14 @@
     void declareNetworkRequestUnfulfillable(in NetworkRequest request);
 
     Network registerNetworkAgent(in INetworkAgent na, in NetworkInfo ni, in LinkProperties lp,
-            in NetworkCapabilities nc, in NetworkScore score, in NetworkAgentConfig config,
+            in NetworkCapabilities nc, in NetworkScore score,
+            in LocalNetworkConfig localNetworkConfig, in NetworkAgentConfig config,
             in int factorySerialNumber);
 
     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);
@@ -160,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,
@@ -238,9 +241,7 @@
 
     void setTestAllowBadWifiUntil(long timeMs);
 
-    void updateMeteredNetworkAllowList(int uid, boolean add);
-
-    void updateMeteredNetworkDenyList(int uid, boolean add);
+    void setDataSaverEnabled(boolean enable);
 
     void setUidFirewallRule(int chain, int uid, int rule);
 
@@ -257,4 +258,8 @@
     void setVpnNetworkPreference(String session, in UidRange[] ranges);
 
     void setTestLowTcpPollingTimerForKeepalive(long timeMs);
+
+    IBinder getRoutingCoordinatorService();
+
+    long getEnabledConnectivityManagerFeatures();
 }
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index b375b7b..61b27b5 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -17,6 +17,7 @@
 
 import android.net.DscpPolicy;
 import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
@@ -34,6 +35,7 @@
     void sendLinkProperties(in LinkProperties lp);
     // TODO: consider replacing this by "markConnected()" and removing
     void sendNetworkInfo(in NetworkInfo info);
+    void sendLocalNetworkConfig(in LocalNetworkConfig config);
     void sendScore(in NetworkScore score);
     void sendExplicitlySelected(boolean explicitlySelected, boolean acceptPartial);
     void sendSocketKeepaliveEvent(int slot, int reason);
diff --git a/framework/src/android/net/LinkAddress.java b/framework/src/android/net/LinkAddress.java
index 90f55b3..8376963 100644
--- a/framework/src/android/net/LinkAddress.java
+++ b/framework/src/android/net/LinkAddress.java
@@ -37,6 +37,8 @@
 import android.os.SystemClock;
 import android.util.Pair;
 
+import com.android.net.module.util.ConnectivityUtils;
+
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -146,11 +148,7 @@
      * Per RFC 4193 section 8, fc00::/7 identifies these addresses.
      */
     private boolean isIpv6ULA() {
-        if (isIpv6()) {
-            byte[] bytes = address.getAddress();
-            return ((bytes[0] & (byte)0xfe) == (byte)0xfc);
-        }
-        return false;
+        return ConnectivityUtils.isIPv6ULA(address);
     }
 
     /**
diff --git a/framework/src/android/net/LocalNetworkConfig.java b/framework/src/android/net/LocalNetworkConfig.java
new file mode 100644
index 0000000..17b1064
--- /dev/null
+++ b/framework/src/android/net/LocalNetworkConfig.java
@@ -0,0 +1,185 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A class to communicate configuration info about a local network through {@link NetworkAgent}.
+ * @hide
+ */
+// TODO : @SystemApi
+public final class LocalNetworkConfig implements Parcelable {
+    @Nullable
+    private final NetworkRequest mUpstreamSelector;
+
+    @NonNull
+    private final MulticastRoutingConfig mUpstreamMulticastRoutingConfig;
+
+    @NonNull
+    private final MulticastRoutingConfig mDownstreamMulticastRoutingConfig;
+
+    private LocalNetworkConfig(@Nullable final NetworkRequest upstreamSelector,
+            @Nullable final MulticastRoutingConfig upstreamConfig,
+            @Nullable final MulticastRoutingConfig downstreamConfig) {
+        mUpstreamSelector = upstreamSelector;
+        if (null != upstreamConfig) {
+            mUpstreamMulticastRoutingConfig = upstreamConfig;
+        } else {
+            mUpstreamMulticastRoutingConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+        }
+        if (null != downstreamConfig) {
+            mDownstreamMulticastRoutingConfig = downstreamConfig;
+        } else {
+            mDownstreamMulticastRoutingConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+        }
+    }
+
+    /**
+     * Get the request choosing which network traffic from this network is forwarded to and from.
+     *
+     * This may be null if the local network doesn't forward the traffic anywhere.
+     */
+    @Nullable
+    public NetworkRequest getUpstreamSelector() {
+        return mUpstreamSelector;
+    }
+
+    /**
+     * Get the upstream multicast routing config
+     */
+    @NonNull
+    public MulticastRoutingConfig getUpstreamMulticastRoutingConfig() {
+        return mUpstreamMulticastRoutingConfig;
+    }
+
+    /**
+     * Get the downstream multicast routing config
+     */
+    @NonNull
+    public MulticastRoutingConfig getDownstreamMulticastRoutingConfig() {
+        return mDownstreamMulticastRoutingConfig;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+        dest.writeParcelable(mUpstreamSelector, flags);
+        dest.writeParcelable(mUpstreamMulticastRoutingConfig, flags);
+        dest.writeParcelable(mDownstreamMulticastRoutingConfig, flags);
+    }
+
+    @Override
+    public String toString() {
+        return "LocalNetworkConfig{"
+                + "UpstreamSelector=" + mUpstreamSelector
+                + ", UpstreamMulticastConfig=" + mUpstreamMulticastRoutingConfig
+                + ", DownstreamMulticastConfig=" + mDownstreamMulticastRoutingConfig
+                + '}';
+    }
+
+    public static final @NonNull Creator<LocalNetworkConfig> CREATOR = new Creator<>() {
+        public LocalNetworkConfig createFromParcel(Parcel in) {
+            final NetworkRequest upstreamSelector = in.readParcelable(null);
+            final MulticastRoutingConfig upstreamConfig = in.readParcelable(null);
+            final MulticastRoutingConfig downstreamConfig = in.readParcelable(null);
+            return new LocalNetworkConfig(
+                    upstreamSelector, upstreamConfig, downstreamConfig);
+        }
+
+        @Override
+        public LocalNetworkConfig[] newArray(final int size) {
+            return new LocalNetworkConfig[size];
+        }
+    };
+
+
+    public static final class Builder {
+        @Nullable
+        private NetworkRequest mUpstreamSelector;
+
+        @Nullable
+        private MulticastRoutingConfig mUpstreamMulticastRoutingConfig;
+
+        @Nullable
+        private MulticastRoutingConfig mDownstreamMulticastRoutingConfig;
+
+        /**
+         * Create a Builder
+         */
+        public Builder() {
+        }
+
+        /**
+         * Set to choose where this local network should forward its traffic to.
+         *
+         * The system will automatically choose the best network matching the request as an
+         * upstream, and set up forwarding between this local network and the chosen upstream.
+         * If no network matches the request, there is no upstream and the traffic is not forwarded.
+         * The caller can know when this changes by listening to link properties changes of
+         * this network with the {@link android.net.LinkProperties#getForwardedNetwork()} getter.
+         *
+         * Set this to null if the local network shouldn't be forwarded. Default is null.
+         */
+        @NonNull
+        public Builder setUpstreamSelector(@Nullable NetworkRequest upstreamSelector) {
+            mUpstreamSelector = upstreamSelector;
+            return this;
+        }
+
+        /**
+         * Set the upstream multicast routing config.
+         *
+         * If null, don't route multicast packets upstream. This is equivalent to a
+         * MulticastRoutingConfig in mode FORWARD_NONE. The default is null.
+         */
+        @NonNull
+        public Builder setUpstreamMulticastRoutingConfig(@Nullable MulticastRoutingConfig cfg) {
+            mUpstreamMulticastRoutingConfig = cfg;
+            return this;
+        }
+
+        /**
+         * Set the downstream multicast routing config.
+         *
+         * If null, don't route multicast packets downstream. This is equivalent to a
+         * MulticastRoutingConfig in mode FORWARD_NONE. The default is null.
+         */
+        @NonNull
+        public Builder setDownstreamMulticastRoutingConfig(@Nullable MulticastRoutingConfig cfg) {
+            mDownstreamMulticastRoutingConfig = cfg;
+            return this;
+        }
+
+        /**
+         * Build the LocalNetworkConfig object.
+         */
+        @NonNull
+        public LocalNetworkConfig build() {
+            return new LocalNetworkConfig(mUpstreamSelector,
+                    mUpstreamMulticastRoutingConfig,
+                    mDownstreamMulticastRoutingConfig);
+        }
+    }
+}
diff --git a/framework/src/android/net/LocalNetworkInfo.java b/framework/src/android/net/LocalNetworkInfo.java
new file mode 100644
index 0000000..f945133
--- /dev/null
+++ b/framework/src/android/net/LocalNetworkInfo.java
@@ -0,0 +1,96 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+/**
+ * Information about a local network.
+ *
+ * This is sent to ConnectivityManager.NetworkCallback.
+ * @hide
+ */
+// TODO : make public
+public final class LocalNetworkInfo implements Parcelable {
+    @Nullable private final Network mUpstreamNetwork;
+
+    public LocalNetworkInfo(@Nullable final Network upstreamNetwork) {
+        this.mUpstreamNetwork = upstreamNetwork;
+    }
+
+    /**
+     * Return the upstream network, or null if none.
+     */
+    @Nullable
+    public Network getUpstreamNetwork() {
+        return mUpstreamNetwork;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+        dest.writeParcelable(mUpstreamNetwork, flags);
+    }
+
+    @Override
+    public String toString() {
+        return "LocalNetworkInfo { upstream=" + mUpstreamNetwork + " }";
+    }
+
+    public static final @NonNull Creator<LocalNetworkInfo> CREATOR = new Creator<>() {
+        public LocalNetworkInfo createFromParcel(Parcel in) {
+            final Network upstreamNetwork = in.readParcelable(null);
+            return new LocalNetworkInfo(upstreamNetwork);
+        }
+
+        @Override
+        public LocalNetworkInfo[] newArray(final int size) {
+            return new LocalNetworkInfo[size];
+        }
+    };
+
+    /**
+     * Builder for LocalNetworkInfo
+     */
+    public static final class Builder {
+        @Nullable private Network mUpstreamNetwork;
+
+        /**
+         * Set the upstream network, or null if none.
+         * @return the builder
+         */
+        @NonNull public Builder setUpstreamNetwork(@Nullable final Network network) {
+            mUpstreamNetwork = network;
+            return this;
+        }
+
+        /**
+         * Build the LocalNetworkInfo
+         */
+        @NonNull public LocalNetworkInfo build() {
+            return new LocalNetworkInfo(mUpstreamNetwork);
+        }
+    }
+}
diff --git a/framework/src/android/net/MacAddress.java b/framework/src/android/net/MacAddress.java
index 26a504a..049a425 100644
--- a/framework/src/android/net/MacAddress.java
+++ b/framework/src/android/net/MacAddress.java
@@ -127,7 +127,7 @@
     /**
      * Convert this MacAddress to a byte array.
      *
-     * The returned array is in network order. For example, if this MacAddress is 1:2:3:4:5:6,
+     * The returned array is in network order. For example, if this MacAddress is 01:02:03:04:05:06,
      * the returned array is [1, 2, 3, 4, 5, 6].
      *
      * @return a byte array representation of this MacAddress.
diff --git a/framework/src/android/net/MulticastRoutingConfig.java b/framework/src/android/net/MulticastRoutingConfig.java
new file mode 100644
index 0000000..4a3e1be
--- /dev/null
+++ b/framework/src/android/net/MulticastRoutingConfig.java
@@ -0,0 +1,335 @@
+/*
+ * 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 android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringJoiner;
+
+/**
+ * A class representing a configuration for multicast routing.
+ *
+ * Internal usage to Connectivity
+ * @hide
+ */
+// @SystemApi(client = MODULE_LIBRARIES)
+public final class MulticastRoutingConfig implements Parcelable {
+    private static final String TAG = MulticastRoutingConfig.class.getSimpleName();
+
+    /** Do not forward any multicast packets. */
+    public static final int FORWARD_NONE = 0;
+    /**
+     * Forward only multicast packets with destination in the list of listening addresses.
+     * Ignore the min scope.
+     */
+    public static final int FORWARD_SELECTED = 1;
+    /**
+     * Forward all multicast packets with scope greater or equal than the min scope.
+     * Ignore the list of listening addresses.
+     */
+    public static final int FORWARD_WITH_MIN_SCOPE = 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "FORWARD_" }, value = {
+            FORWARD_NONE,
+            FORWARD_SELECTED,
+            FORWARD_WITH_MIN_SCOPE
+    })
+    public @interface MulticastForwardingMode {}
+
+    /**
+     * Not a multicast scope, for configurations that do not use the min scope.
+     */
+    public static final int MULTICAST_SCOPE_NONE = -1;
+
+    /** @hide */
+    public static final MulticastRoutingConfig CONFIG_FORWARD_NONE =
+            new MulticastRoutingConfig(FORWARD_NONE, MULTICAST_SCOPE_NONE, null);
+
+    @MulticastForwardingMode
+    private final int mForwardingMode;
+
+    private final int mMinScope;
+
+    @NonNull
+    private final Set<Inet6Address> mListeningAddresses;
+
+    private MulticastRoutingConfig(@MulticastForwardingMode final int mode, final int scope,
+            @Nullable final Set<Inet6Address> addresses) {
+        mForwardingMode = mode;
+        mMinScope = scope;
+        if (null != addresses) {
+            mListeningAddresses = Collections.unmodifiableSet(new ArraySet<>(addresses));
+        } else {
+            mListeningAddresses = Collections.emptySet();
+        }
+    }
+
+    /**
+     * Returns the forwarding mode.
+     */
+    @MulticastForwardingMode
+    public int getForwardingMode() {
+        return mForwardingMode;
+    }
+
+    /**
+     * Returns the minimal group address scope that is allowed for forwarding.
+     * If the forwarding mode is not FORWARD_WITH_MIN_SCOPE, will be MULTICAST_SCOPE_NONE.
+     */
+    public int getMinimumScope() {
+        return mMinScope;
+    }
+
+    /**
+     * Returns the list of group addresses listened by the outgoing interface.
+     * The list will be empty if the forwarding mode is not FORWARD_SELECTED.
+     */
+    @NonNull
+    public Set<Inet6Address> getListeningAddresses() {
+        return mListeningAddresses;
+    }
+
+    private MulticastRoutingConfig(Parcel in) {
+        mForwardingMode = in.readInt();
+        mMinScope = in.readInt();
+        final int count = in.readInt();
+        final ArraySet<Inet6Address> listeningAddresses = new ArraySet<>(count);
+        final byte[] buffer = new byte[16]; // Size of an Inet6Address
+        for (int i = 0; i < count; ++i) {
+            in.readByteArray(buffer);
+            try {
+                listeningAddresses.add((Inet6Address) Inet6Address.getByAddress(buffer));
+            } catch (UnknownHostException e) {
+                Log.wtf(TAG, "Can't read inet6address : " + Arrays.toString(buffer));
+            }
+        }
+        mListeningAddresses = Collections.unmodifiableSet(listeningAddresses);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mForwardingMode);
+        dest.writeInt(mMinScope);
+        dest.writeInt(mListeningAddresses.size());
+        for (final Inet6Address addr : mListeningAddresses) {
+            dest.writeByteArray(addr.getAddress());
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Creator<MulticastRoutingConfig> CREATOR = new Creator<>() {
+        @Override
+        public MulticastRoutingConfig createFromParcel(@NonNull Parcel in) {
+            return new MulticastRoutingConfig(in);
+        }
+
+        @Override
+        public MulticastRoutingConfig[] newArray(int size) {
+            return new MulticastRoutingConfig[size];
+        }
+    };
+
+    private static String forwardingModeToString(final int forwardingMode) {
+        switch (forwardingMode) {
+            case FORWARD_NONE: return "NONE";
+            case FORWARD_SELECTED: return "SELECTED";
+            case FORWARD_WITH_MIN_SCOPE: return "WITH_MIN_SCOPE";
+            default: return "UNKNOWN";
+        }
+    }
+
+    public static final class Builder {
+        @MulticastForwardingMode
+        private final int mForwardingMode;
+        private int mMinScope;
+        private final ArraySet<Inet6Address> mListeningAddresses;
+
+        // The two constructors with runtime checks for the mode and scope are arguably
+        // less convenient than three static factory methods, but API guidelines mandates
+        // that Builders are built with a constructor and not factory methods.
+        /**
+         * Create a new builder for forwarding mode FORWARD_NONE or FORWARD_SELECTED.
+         *
+         * <p>On a Builder for FORWARD_NONE, no properties can be set.
+         * <p>On a Builder for FORWARD_SELECTED, listening addresses can be added and removed
+         * but the minimum scope can't be set.
+         *
+         * @param mode {@link #FORWARD_NONE} or {@link #FORWARD_SELECTED}. Any other
+         *             value will result in IllegalArgumentException.
+         * @see #Builder(int, int)
+         */
+        public Builder(@MulticastForwardingMode final int mode) {
+            if (FORWARD_NONE != mode && FORWARD_SELECTED != mode) {
+                if (FORWARD_WITH_MIN_SCOPE == mode) {
+                    throw new IllegalArgumentException("FORWARD_WITH_MIN_SCOPE requires "
+                            + "passing the scope as a second argument");
+                } else {
+                    throw new IllegalArgumentException("Unknown forwarding mode : " + mode);
+                }
+            }
+            mForwardingMode = mode;
+            mMinScope = MULTICAST_SCOPE_NONE;
+            mListeningAddresses = new ArraySet<>();
+        }
+
+        /**
+         * Create a new builder for forwarding mode FORWARD_WITH_MIN_SCOPE.
+         *
+         * <p>On this Builder the scope can be set with {@link #setMinimumScope}, but
+         * listening addresses can't be added or removed.
+         *
+         * @param mode Must be {@link #FORWARD_WITH_MIN_SCOPE}.
+         * @param scope the minimum scope for this multicast routing config.
+         * @see Builder#Builder(int)
+         */
+        public Builder(@MulticastForwardingMode final int mode, int scope) {
+            if (FORWARD_WITH_MIN_SCOPE != mode) {
+                throw new IllegalArgumentException("Forwarding with a min scope must "
+                        + "use forward mode FORWARD_WITH_MIN_SCOPE");
+            }
+            mForwardingMode = mode;
+            mMinScope = scope;
+            mListeningAddresses = new ArraySet<>();
+        }
+
+        /**
+         * Sets the minimum scope for this multicast routing config.
+         * This is only meaningful (indeed, allowed) for configs in FORWARD_WITH_MIN_SCOPE mode.
+         * @return this builder
+         */
+        @NonNull
+        public Builder setMinimumScope(final int scope) {
+            if (FORWARD_WITH_MIN_SCOPE != mForwardingMode) {
+                throw new IllegalArgumentException("Can't set the scope on a builder in mode "
+                        + modeToString(mForwardingMode));
+            }
+            mMinScope = scope;
+            return this;
+        }
+
+        /**
+         * Add an address to the set of listening addresses.
+         *
+         * This is only meaningful (indeed, allowed) for configs in FORWARD_SELECTED mode.
+         * If this address was already added, this is a no-op.
+         * @return this builder
+         */
+        @NonNull
+        public Builder addListeningAddress(@NonNull final Inet6Address address) {
+            if (FORWARD_SELECTED != mForwardingMode) {
+                throw new IllegalArgumentException("Can't add an address on a builder in mode "
+                        + modeToString(mForwardingMode));
+            }
+            // TODO : should we check that this is a multicast address ?
+            mListeningAddresses.add(address);
+            return this;
+        }
+
+        /**
+         * Remove an address from the set of listening addresses.
+         *
+         * This is only meaningful (indeed, allowed) for configs in FORWARD_SELECTED mode.
+         * If this address was not added, or was already removed, this is a no-op.
+         * @return this builder
+         */
+        @NonNull
+        public Builder clearListeningAddress(@NonNull final Inet6Address address) {
+            if (FORWARD_SELECTED != mForwardingMode) {
+                throw new IllegalArgumentException("Can't remove an address on a builder in mode "
+                        + modeToString(mForwardingMode));
+            }
+            mListeningAddresses.remove(address);
+            return this;
+        }
+
+        /**
+         * Build the config.
+         */
+        @NonNull
+        public MulticastRoutingConfig build() {
+            return new MulticastRoutingConfig(mForwardingMode, mMinScope, mListeningAddresses);
+        }
+    }
+
+    private static String modeToString(@MulticastForwardingMode final int mode) {
+        switch (mode) {
+            case FORWARD_NONE: return "FORWARD_NONE";
+            case FORWARD_SELECTED: return "FORWARD_SELECTED";
+            case FORWARD_WITH_MIN_SCOPE: return "FORWARD_WITH_MIN_SCOPE";
+            default: return "unknown multicast routing mode " + mode;
+        }
+    }
+
+    public boolean equals(Object other) {
+        if (other == this) {
+            return true;
+        } else if (!(other instanceof MulticastRoutingConfig)) {
+            return false;
+        } else {
+            final MulticastRoutingConfig otherConfig = (MulticastRoutingConfig) other;
+            return mForwardingMode == otherConfig.mForwardingMode
+                && mMinScope == otherConfig.mMinScope
+                && mListeningAddresses.equals(otherConfig.mListeningAddresses);
+        }
+    }
+
+    public int hashCode() {
+        return Objects.hash(mForwardingMode, mMinScope, mListeningAddresses);
+    }
+
+    public String toString() {
+        final StringJoiner resultJoiner = new StringJoiner(" ", "{", "}");
+
+        resultJoiner.add("ForwardingMode:");
+        resultJoiner.add(modeToString(mForwardingMode));
+
+        if (mForwardingMode == FORWARD_WITH_MIN_SCOPE) {
+            resultJoiner.add("MinScope:");
+            resultJoiner.add(Integer.toString(mMinScope));
+        }
+
+        if (mForwardingMode == FORWARD_SELECTED && !mListeningAddresses.isEmpty()) {
+            resultJoiner.add("ListeningAddresses: [");
+            resultJoiner.add(TextUtils.join(",", mListeningAddresses));
+            resultJoiner.add("]");
+        }
+
+        return resultJoiner.toString();
+    }
+}
diff --git a/framework/src/android/net/NattKeepalivePacketData.java b/framework/src/android/net/NattKeepalivePacketData.java
index c4f8fc2..9e6d80d 100644
--- a/framework/src/android/net/NattKeepalivePacketData.java
+++ b/framework/src/android/net/NattKeepalivePacketData.java
@@ -29,7 +29,9 @@
 import com.android.net.module.util.IpUtils;
 
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.Objects;
@@ -38,6 +40,7 @@
 @SystemApi
 public final class NattKeepalivePacketData extends KeepalivePacketData implements Parcelable {
     private static final int IPV4_HEADER_LENGTH = 20;
+    private static final int IPV6_HEADER_LENGTH = 40;
     private static final int UDP_HEADER_LENGTH = 8;
 
     // This should only be constructed via static factory methods, such as
@@ -55,39 +58,85 @@
     public static NattKeepalivePacketData nattKeepalivePacket(
             InetAddress srcAddress, int srcPort, InetAddress dstAddress, int dstPort)
             throws InvalidPacketException {
-
-        if (!(srcAddress instanceof Inet4Address) || !(dstAddress instanceof Inet4Address)) {
-            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
-        }
-
         if (dstPort != NattSocketKeepalive.NATT_PORT) {
             throw new InvalidPacketException(ERROR_INVALID_PORT);
         }
 
+        // Convert IPv4 mapped v6 address to v4 if any.
+        final InetAddress srcAddr, dstAddr;
+        try {
+            srcAddr = InetAddress.getByAddress(srcAddress.getAddress());
+            dstAddr = InetAddress.getByAddress(dstAddress.getAddress());
+        } catch (UnknownHostException e) {
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+
+        if (srcAddr instanceof Inet4Address && dstAddr instanceof Inet4Address) {
+            return nattKeepalivePacketv4(
+                    (Inet4Address) srcAddr, srcPort, (Inet4Address) dstAddr, dstPort);
+        } else if (srcAddr instanceof Inet6Address && dstAddr instanceof Inet6Address) {
+            return nattKeepalivePacketv6(
+                    (Inet6Address) srcAddr, srcPort, (Inet6Address) dstAddr, dstPort);
+        } else {
+            // Destination address and source address should be the same IP family.
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+    }
+
+    private static NattKeepalivePacketData nattKeepalivePacketv4(
+            Inet4Address srcAddress, int srcPort, Inet4Address dstAddress, int dstPort)
+            throws InvalidPacketException {
         int length = IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH + 1;
-        ByteBuffer buf = ByteBuffer.allocate(length);
+        final ByteBuffer buf = ByteBuffer.allocate(length);
         buf.order(ByteOrder.BIG_ENDIAN);
-        buf.putShort((short) 0x4500);             // IP version and TOS
+        buf.putShort((short) 0x4500);                       // IP version and TOS
         buf.putShort((short) length);
-        buf.putInt(0);                            // ID, flags, offset
-        buf.put((byte) 64);                       // TTL
+        buf.putShort((short) 0);                            // ID
+        buf.putShort((short) 0x4000);                       // Flags(DF), offset
+        // Technically speaking, this should be reading/using the v4 sysctl
+        // /proc/sys/net/ipv4/ip_default_ttl. Use hard-coded 64 for simplicity.
+        buf.put((byte) 64);                                 // TTL
         buf.put((byte) OsConstants.IPPROTO_UDP);
-        int ipChecksumOffset = buf.position();
-        buf.putShort((short) 0);                  // IP checksum
+        final int ipChecksumOffset = buf.position();
+        buf.putShort((short) 0);                            // IP checksum
         buf.put(srcAddress.getAddress());
         buf.put(dstAddress.getAddress());
         buf.putShort((short) srcPort);
         buf.putShort((short) dstPort);
-        buf.putShort((short) (length - 20));      // UDP length
-        int udpChecksumOffset = buf.position();
-        buf.putShort((short) 0);                  // UDP checksum
-        buf.put((byte) 0xff);                     // NAT-T keepalive
+        buf.putShort((short) (UDP_HEADER_LENGTH + 1));      // UDP length
+        final int udpChecksumOffset = buf.position();
+        buf.putShort((short) 0);                            // UDP checksum
+        buf.put((byte) 0xff);                               // NAT-T keepalive
         buf.putShort(ipChecksumOffset, IpUtils.ipChecksum(buf, 0));
         buf.putShort(udpChecksumOffset, IpUtils.udpChecksum(buf, 0, IPV4_HEADER_LENGTH));
 
         return new NattKeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, buf.array());
     }
 
+    private static NattKeepalivePacketData nattKeepalivePacketv6(
+            Inet6Address srcAddress, int srcPort, Inet6Address dstAddress, int dstPort)
+            throws InvalidPacketException {
+        final ByteBuffer buf = ByteBuffer.allocate(IPV6_HEADER_LENGTH + UDP_HEADER_LENGTH + 1);
+        buf.order(ByteOrder.BIG_ENDIAN);
+        buf.putInt(0x60000000);                         // IP version, traffic class and flow label
+        buf.putShort((short) (UDP_HEADER_LENGTH + 1));  // Payload length
+        buf.put((byte) OsConstants.IPPROTO_UDP);        // Next header
+        // For native ipv6, this hop limit value should use the per interface v6 hoplimit sysctl.
+        // For 464xlat, this value should use the v4 ttl sysctl.
+        // Either way, for simplicity, just hard code 64.
+        buf.put((byte) 64);                             // Hop limit
+        buf.put(srcAddress.getAddress());
+        buf.put(dstAddress.getAddress());
+        // UDP
+        buf.putShort((short) srcPort);
+        buf.putShort((short) dstPort);
+        buf.putShort((short) (UDP_HEADER_LENGTH + 1));  // UDP length = Payload length
+        final int udpChecksumOffset = buf.position();
+        buf.putShort((short) 0);                        // UDP checksum
+        buf.put((byte) 0xff);                           // NAT-T keepalive. 1 byte of data
+        buf.putShort(udpChecksumOffset, IpUtils.udpChecksum(buf, 0, IPV6_HEADER_LENGTH));
+        return new NattKeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, buf.array());
+    }
     /** Parcelable Implementation */
     public int describeContents() {
         return 0;
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 177f7e3..574ab2f 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -151,7 +151,7 @@
 
     /**
      * Sent by the NetworkAgent to ConnectivityService to pass the current
-     * NetworkCapabilties.
+     * NetworkCapabilities.
      * obj = NetworkCapabilities
      * @hide
      */
@@ -443,6 +443,14 @@
     public static final int EVENT_UNREGISTER_AFTER_REPLACEMENT = BASE + 29;
 
     /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the new value of the local
+     * network agent config.
+     * obj = {@code Pair<NetworkAgentInfo, LocalNetworkConfig>}
+     * @hide
+     */
+    public static final int EVENT_LOCAL_NETWORK_CONFIG_CHANGED = BASE + 30;
+
+    /**
      * DSCP policy was successfully added.
      */
     public static final int DSCP_POLICY_STATUS_SUCCESS = 0;
@@ -517,20 +525,47 @@
             @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
             @NonNull NetworkScore score, @NonNull NetworkAgentConfig config,
             @Nullable NetworkProvider provider) {
-        this(looper, context, logTag, nc, lp, score, config,
+        this(context, looper, logTag, nc, lp, null /* localNetworkConfig */, score, config,
+                provider);
+    }
+
+    /**
+     * Create a new network agent.
+     * @param context a {@link Context} to get system services from.
+     * @param looper the {@link Looper} on which to invoke the callbacks.
+     * @param logTag the tag for logs
+     * @param nc the initial {@link NetworkCapabilities} of this network. Update with
+     *           sendNetworkCapabilities.
+     * @param lp the initial {@link LinkProperties} of this network. Update with sendLinkProperties.
+     * @param localNetworkConfig the initial {@link LocalNetworkConfig} of this
+     *                                  network. Update with sendLocalNetworkConfig. Must be
+     *                                  non-null iff the nc have NET_CAPABILITY_LOCAL_NETWORK.
+     * @param score the initial score of this network. Update with sendNetworkScore.
+     * @param config an immutable {@link NetworkAgentConfig} for this agent.
+     * @param provider the {@link NetworkProvider} managing this agent.
+     * @hide
+     */
+    // TODO : expose
+    public NetworkAgent(@NonNull Context context, @NonNull Looper looper, @NonNull String logTag,
+            @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
+            @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+            @NonNull NetworkAgentConfig config, @Nullable NetworkProvider provider) {
+        this(looper, context, logTag, nc, lp, localNetworkConfig, score, config,
                 provider == null ? NetworkProvider.ID_NONE : provider.getProviderId(),
                 getLegacyNetworkInfo(config));
     }
 
     private static class InitialConfiguration {
-        public final Context context;
-        public final NetworkCapabilities capabilities;
-        public final LinkProperties properties;
-        public final NetworkScore score;
-        public final NetworkAgentConfig config;
-        public final NetworkInfo info;
+        @NonNull public final Context context;
+        @NonNull public final NetworkCapabilities capabilities;
+        @NonNull public final LinkProperties properties;
+        @NonNull public final NetworkScore score;
+        @NonNull public final NetworkAgentConfig config;
+        @NonNull public final NetworkInfo info;
+        @Nullable public final LocalNetworkConfig localNetworkConfig;
         InitialConfiguration(@NonNull Context context, @NonNull NetworkCapabilities capabilities,
-                @NonNull LinkProperties properties, @NonNull NetworkScore score,
+                @NonNull LinkProperties properties,
+                @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
                 @NonNull NetworkAgentConfig config, @NonNull NetworkInfo info) {
             this.context = context;
             this.capabilities = capabilities;
@@ -538,14 +573,15 @@
             this.score = score;
             this.config = config;
             this.info = info;
+            this.localNetworkConfig = localNetworkConfig;
         }
     }
     private volatile InitialConfiguration mInitialConfiguration;
 
     private NetworkAgent(@NonNull Looper looper, @NonNull Context context, @NonNull String logTag,
             @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
-            @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, int providerId,
-            @NonNull NetworkInfo ni) {
+            @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+            @NonNull NetworkAgentConfig config, int providerId, @NonNull NetworkInfo ni) {
         mHandler = new NetworkAgentHandler(looper);
         LOG_TAG = logTag;
         mNetworkInfo = new NetworkInfo(ni);
@@ -556,7 +592,7 @@
 
         mInitialConfiguration = new InitialConfiguration(context,
                 new NetworkCapabilities(nc, NetworkCapabilities.REDACT_NONE),
-                new LinkProperties(lp), score, config, ni);
+                new LinkProperties(lp), localNetworkConfig, score, config, ni);
     }
 
     private class NetworkAgentHandler extends Handler {
@@ -720,10 +756,20 @@
             }
             final ConnectivityManager cm = (ConnectivityManager) mInitialConfiguration.context
                     .getSystemService(Context.CONNECTIVITY_SERVICE);
-            mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
-                    new NetworkInfo(mInitialConfiguration.info),
-                    mInitialConfiguration.properties, mInitialConfiguration.capabilities,
-                    mInitialConfiguration.score, mInitialConfiguration.config, providerId);
+            if (mInitialConfiguration.localNetworkConfig == null) {
+                // Call registerNetworkAgent without localNetworkConfig argument to pass
+                // android.net.cts.NetworkAgentTest#testAgentStartsInConnecting in old cts
+                mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
+                        new NetworkInfo(mInitialConfiguration.info),
+                        mInitialConfiguration.properties, mInitialConfiguration.capabilities,
+                        mInitialConfiguration.score, mInitialConfiguration.config, providerId);
+            } else {
+                mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
+                        new NetworkInfo(mInitialConfiguration.info),
+                        mInitialConfiguration.properties, mInitialConfiguration.capabilities,
+                        mInitialConfiguration.localNetworkConfig, mInitialConfiguration.score,
+                        mInitialConfiguration.config, providerId);
+            }
             mInitialConfiguration = null; // All this memory can now be GC'd
         }
         return mNetwork;
@@ -1099,6 +1145,18 @@
     }
 
     /**
+     * Must be called by the agent when the network's {@link LocalNetworkConfig} changes.
+     * @param config the new LocalNetworkConfig
+     * @hide
+     */
+    public void sendLocalNetworkConfig(@NonNull LocalNetworkConfig config) {
+        Objects.requireNonNull(config);
+        // If the agent doesn't have NET_CAPABILITY_LOCAL_NETWORK, this will be ignored by
+        // ConnectivityService with a Log.wtf.
+        queueOrSendMessage(reg -> reg.sendLocalNetworkConfig(config));
+    }
+
+    /**
      * Must be called by the agent to update the score of this network.
      *
      * @param score the new score.
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 92e9599..6a14bde 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -20,6 +20,7 @@
 import static com.android.net.module.util.BitUtils.appendStringRepresentationOfBitMaskToStringBuilder;
 import static com.android.net.module.util.BitUtils.describeDifferences;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.LongDef;
 import android.annotation.NonNull;
@@ -35,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;
@@ -121,6 +124,22 @@
 public final class NetworkCapabilities implements Parcelable {
     private static final String TAG = "NetworkCapabilities";
 
+    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+    // available here
+    /** @hide */
+    public static class Flags {
+        static final String FLAG_FORBIDDEN_CAPABILITY =
+                "com.android.net.flags.forbidden_capability";
+        static final String FLAG_NET_CAPABILITY_LOCAL_NETWORK =
+                "com.android.net.flags.net_capability_local_network";
+        static final String REQUEST_RESTRICTED_WIFI =
+                "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";
+    }
+
     /**
      * Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific
      * app permissions.
@@ -260,6 +279,19 @@
     private int mEnterpriseId;
 
     /**
+     * Gets the enterprise IDs as an int. Internal callers only.
+     *
+     * DO NOT USE THIS if not immediately collapsing back into a scalar. Instead,
+     * prefer getEnterpriseIds/hasEnterpriseId.
+     *
+     * @return the internal, version-dependent int representing enterprise ids
+     * @hide
+     */
+    public int getEnterpriseIdsInternal() {
+        return mEnterpriseId;
+    }
+
+    /**
      * Get enteprise identifiers set.
      *
      * Get all the enterprise capabilities identifier set on this {@code NetworkCapability}
@@ -429,6 +461,8 @@
             NET_CAPABILITY_MMTEL,
             NET_CAPABILITY_PRIORITIZE_LATENCY,
             NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
+            NET_CAPABILITY_LOCAL_NETWORK,
+            NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED,
     })
     public @interface NetCapability { }
 
@@ -690,38 +724,71 @@
      */
     public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35;
 
-    private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
-    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
+    /**
+     * Indicates that this network is a local network.
+     *
+     * Local networks are networks where the device is not obtaining IP addresses from the
+     * network, but advertising IP addresses itself. Examples of local networks are:
+     * <ul>
+     * <li>USB tethering or Wi-Fi hotspot networks to which the device is sharing its Internet
+     * connectivity.
+     * <li>Thread networks where the current device is the Thread Border Router.
+     * <li>Wi-Fi P2P networks where the current device is the Group Owner.
+     * </ul>
+     *
+     * Networks used to obtain Internet access are never local networks.
+     *
+     * Apps that target an SDK before {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} will not see
+     * networks with this capability unless they explicitly set the NET_CAPABILITY_LOCAL_NETWORK
+     * in their NetworkRequests.
+     */
+    @FlaggedApi(Flags.FLAG_NET_CAPABILITY_LOCAL_NETWORK)
+    public static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
 
-    private static final int ALL_VALID_CAPABILITIES;
-    static {
-        int caps = 0;
-        for (int i = MIN_NET_CAPABILITY; i <= MAX_NET_CAPABILITY; ++i) {
-            caps |= 1 << i;
-        }
-        ALL_VALID_CAPABILITIES = caps;
-    }
+    /**
+     * 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;
 
     /**
      * 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
@@ -741,20 +808,28 @@
     /**
      * 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.
+     * @hide
      */
-    private static final long CONNECTIVITY_MANAGED_CAPABILITIES =
-            BitUtils.packBitList(
-                    NET_CAPABILITY_VALIDATED,
-                    NET_CAPABILITY_CAPTIVE_PORTAL,
-                    NET_CAPABILITY_FOREGROUND,
-                    NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+    @VisibleForTesting
+    public static final long CONNECTIVITY_MANAGED_CAPABILITIES =
+            (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
@@ -763,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
@@ -779,12 +855,18 @@
      * 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.
      * Note that when searching for a network to satisfy a request, all capabilities
      * requested must be satisfied.
+     * <p>
+     * If the capability was previously added to the list of forbidden capabilities (either
+     * by default or added using {@link #addForbiddenCapability(int)}), then it will be removed
+     * from the list of forbidden capabilities as well.
      *
      * @param capability the capability to be added.
      * @return This NetworkCapabilities instance, to facilitate chaining.
@@ -793,9 +875,11 @@
     public @NonNull NetworkCapabilities addCapability(@NetCapability int capability) {
         // 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: Consider adding forbidden capabilities to the public API and mention this
-        // in the documentation.
-        checkValidCapability(capability);
+        // TODO: Add forbidden capabilities to the public API
+        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);
@@ -816,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
     }
@@ -830,14 +917,17 @@
      * @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;
     }
 
     /**
-     * Removes (if found) the given forbidden capability from this {@code NetworkCapability}
+     * Removes (if found) the given forbidden capability from this {@link NetworkCapabilities}
      * instance that were added via addForbiddenCapability(int) or setCapabilities(int[], int[]).
      *
      * @param capability the capability to be removed.
@@ -845,12 +935,26 @@
      * @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;
     }
 
     /**
+     * Removes all forbidden capabilities from this {@link NetworkCapabilities} instance.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities removeAllForbiddenCapabilities() {
+        mForbiddenNetworkCapabilities = 0;
+        return this;
+    }
+
+    /**
      * Sets (or clears) the given capability on this {@link NetworkCapabilities}
      * instance.
      * @hide
@@ -866,6 +970,19 @@
     }
 
     /**
+     * Gets the capabilities as an int. Internal callers only.
+     *
+     * DO NOT USE THIS if not immediately collapsing back into a scalar. Instead,
+     * prefer getCapabilities/hasCapability.
+     *
+     * @return an internal, version-dependent int representing the capabilities
+     * @hide
+     */
+    public long getCapabilitiesInternal() {
+        return mNetworkCapabilities;
+    }
+
+    /**
      * Gets all the capabilities set on this {@code NetworkCapability} instance.
      *
      * @return an array of capability values for this instance.
@@ -880,6 +997,8 @@
      * @return an array of forbidden capability values for this instance.
      * @hide
      */
+    @NonNull
+    // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
     public @NetCapability int[] getForbiddenCapabilities() {
         return BitUtils.unpackBits(mForbiddenNetworkCapabilities);
     }
@@ -979,7 +1098,7 @@
     /**
      * Tests for the presence of a capability on this instance.
      *
-     * @param capability the capabilities to be tested for.
+     * @param capability the capability to be tested for.
      * @return {@code true} if set on this instance.
      */
     public boolean hasCapability(@NetCapability int capability) {
@@ -987,19 +1106,27 @@
                 && ((mNetworkCapabilities & (1L << capability)) != 0);
     }
 
-    /** @hide */
+    /**
+     * Tests for the presence of a forbidden capability on this instance.
+     *
+     * @param capability the capability to be tested for.
+     * @return {@code true} if this capability is set forbidden on this instance.
+     * @hide
+     */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
     public boolean hasForbiddenCapability(@NetCapability int capability) {
         return isValidCapability(capability)
                 && ((mForbiddenNetworkCapabilities & (1L << capability)) != 0);
     }
 
     /**
-     * Check if this NetworkCapabilities has system managed capabilities or not.
+     * Check if this NetworkCapabilities has connectivity-managed capabilities or not.
      * @hide
      */
     public boolean hasConnectivityManagedCapability() {
-        return ((mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0);
+        return (mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0
+                || mForbiddenNetworkCapabilities != 0;
     }
 
     /**
@@ -1093,7 +1220,7 @@
      * @hide
      */
     public void maybeMarkCapabilitiesRestricted() {
-        if (NetworkCapabilitiesUtils.inferRestrictedCapability(this)) {
+        if (NetworkCapabilitiesUtils.inferRestrictedCapability(mNetworkCapabilities)) {
             removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
         }
     }
@@ -1187,6 +1314,7 @@
             TRANSPORT_TEST,
             TRANSPORT_USB,
             TRANSPORT_THREAD,
+            TRANSPORT_SATELLITE,
     })
     public @interface Transport { }
 
@@ -1243,10 +1371,16 @@
      */
     public static final int TRANSPORT_THREAD = 9;
 
+    /**
+     * Indicates this network uses a Satellite transport.
+     */
+    @FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE)
+    public static final int TRANSPORT_SATELLITE = 10;
+
     /** @hide */
     public static final int MIN_TRANSPORT = TRANSPORT_CELLULAR;
     /** @hide */
-    public static final int MAX_TRANSPORT = TRANSPORT_THREAD;
+    public static final int MAX_TRANSPORT = TRANSPORT_SATELLITE;
 
     private static final int ALL_VALID_TRANSPORTS;
     static {
@@ -1273,18 +1407,18 @@
         "TEST",
         "USB",
         "THREAD",
+        "SATELLITE",
     };
 
     /**
      * 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.
@@ -1387,6 +1521,18 @@
         return mTransportTypes == (1 << transportType);
     }
 
+    /**
+     * Returns true iff this NC has the specified transport and no other, ignoring TRANSPORT_TEST.
+     *
+     * If this NC has the passed transport and no other, this method returns true.
+     * If this NC has the passed transport, TRANSPORT_TEST and no other, this method returns true.
+     * Otherwise, this method returns false.
+     * @hide
+     */
+    public boolean hasSingleTransportBesidesTest(@Transport int transportType) {
+        return (mTransportTypes & ~(1 << TRANSPORT_TEST)) == (1 << transportType);
+    }
+
     private boolean satisfiedByTransportTypes(NetworkCapabilities nc) {
         return ((this.mTransportTypes == 0)
                 || ((this.mTransportTypes & nc.mTransportTypes) != 0));
@@ -1669,9 +1815,12 @@
     public @NonNull NetworkCapabilities setNetworkSpecifier(
             @NonNull NetworkSpecifier networkSpecifier) {
         if (networkSpecifier != null
-                // Transport can be test, or test + a single other transport
+                // Transport can be test, or test + a single other transport or cellular + satellite
+                // transport. Note: cellular + satellite combination is allowed since both transport
+                // use the same specifier, TelephonyNetworkSpecifier.
                 && mTransportTypes != (1L << TRANSPORT_TEST)
-                && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1) {
+                && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1
+                && !specifierAcceptableForMultipleTransports(mTransportTypes)) {
             throw new IllegalStateException("Must have a single non-test transport specified to "
                     + "use setNetworkSpecifier");
         }
@@ -1681,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.
      *
@@ -2479,6 +2634,8 @@
             case NET_CAPABILITY_MMTEL:                return "MMTEL";
             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);
         }
     }
@@ -2519,13 +2676,7 @@
     }
 
     private static boolean isValidCapability(@NetworkCapabilities.NetCapability int capability) {
-        return capability >= MIN_NET_CAPABILITY && capability <= MAX_NET_CAPABILITY;
-    }
-
-    private static void checkValidCapability(@NetworkCapabilities.NetCapability int capability) {
-        if (!isValidCapability(capability)) {
-            throw new IllegalArgumentException("NetworkCapability " + capability + " out of range");
-        }
+        return capability >= 0 && capability <= MAX_NET_CAPABILITY;
     }
 
     private static boolean isValidEnterpriseId(
@@ -2711,10 +2862,9 @@
      * receiver holds the NETWORK_FACTORY permission. In all other cases, it will be the empty set.
      *
      * @return
-     * @hide
      */
     @NonNull
-    @SystemApi
+    @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
     public Set<Integer> getSubscriptionIds() {
         return new ArraySet<>(mSubIds);
     }
@@ -2868,6 +3018,44 @@
         }
 
         /**
+         * Adds the given capability to the list of forbidden capabilities.
+         *
+         * A network with a capability will not match a {@link NetworkCapabilities} or
+         * {@link NetworkRequest} which has said capability set as forbidden. For example, if
+         * a request has NET_CAPABILITY_INTERNET in the list of forbidden capabilities, networks
+         * with NET_CAPABILITY_INTERNET will not match the request.
+         *
+         * If the capability was previously added to the list of required capabilities (for
+         * example, it was there by default or added using {@link #addCapability(int)} method), then
+         * it will be removed from the list of required capabilities as well.
+         *
+         * @param capability the capability
+         * @return this builder
+         * @hide
+         */
+        @NonNull
+        // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
+        public Builder addForbiddenCapability(@NetCapability final int capability) {
+            mCaps.addForbiddenCapability(capability);
+            return this;
+        }
+
+        /**
+         * Removes the given capability from the list of forbidden capabilities.
+         *
+         * @see #addForbiddenCapability(int)
+         * @param capability the capability
+         * @return this builder
+         * @hide
+         */
+        @NonNull
+        // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
+        public Builder removeForbiddenCapability(@NetCapability final int capability) {
+            mCaps.removeForbiddenCapability(capability);
+            return this;
+        }
+
+        /**
          * Adds the given enterprise capability identifier.
          * Note that when searching for a network to satisfy a request, all capabilities identifier
          * requested must be satisfied. Enterprise capability identifier is applicable only
@@ -3214,4 +3402,4 @@
             return new NetworkCapabilities(mCaps);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 6c351d0..502ac6f 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -20,6 +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_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;
@@ -33,6 +34,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -142,6 +144,12 @@
  * Look up the specific capability to learn whether its usage requires this self-certification.
  */
 public class NetworkRequest implements Parcelable {
+
+    /** @hide */
+    public static class Flags {
+        static final String REQUEST_RESTRICTED_WIFI =
+                "com.android.net.flags.request_restricted_wifi";
+    }
     /**
      * The first requestId value that will be allocated.
      * @hide only used by ConnectivityService.
@@ -279,7 +287,8 @@
                 NET_CAPABILITY_PARTIAL_CONNECTIVITY,
                 NET_CAPABILITY_TEMPORARILY_NOT_METERED,
                 NET_CAPABILITY_TRUSTED,
-                NET_CAPABILITY_VALIDATED);
+                NET_CAPABILITY_VALIDATED,
+                NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
 
         private final NetworkCapabilities mNetworkCapabilities;
 
@@ -408,6 +417,7 @@
         @NonNull
         @SuppressLint("MissingGetterMatchingBuilder")
         @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
         public Builder addForbiddenCapability(@NetworkCapabilities.NetCapability int capability) {
             mNetworkCapabilities.addForbiddenCapability(capability);
             return this;
@@ -424,6 +434,7 @@
         @NonNull
         @SuppressLint("BuilderSetStyle")
         @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
         public Builder removeForbiddenCapability(
                 @NetworkCapabilities.NetCapability int capability) {
             mNetworkCapabilities.removeForbiddenCapability(capability);
@@ -433,6 +444,7 @@
         /**
          * Completely clears all the {@code NetworkCapabilities} from this builder instance,
          * removing even the capabilities that are set by default when the object is constructed.
+         * Also removes any set forbidden capabilities.
          *
          * @return The builder to facilitate chaining.
          */
@@ -602,10 +614,9 @@
          * NETWORK_FACTORY permission.
          *
          * @param subIds A {@code Set} that represents subscription IDs.
-         * @hide
          */
         @NonNull
-        @SystemApi
+        @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
         public Builder setSubscriptionIds(@NonNull Set<Integer> subIds) {
             mNetworkCapabilities.setSubscriptionIds(subIds);
             return this;
@@ -721,6 +732,7 @@
      * @hide
      */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public instead of @SystemApi
     public boolean hasForbiddenCapability(@NetCapability int capability) {
         return networkCapabilities.hasForbiddenCapability(capability);
     }
@@ -843,6 +855,7 @@
      */
     @NonNull
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public instead of @SystemApi
     public @NetCapability int[] getForbiddenCapabilities() {
         // No need to make a defensive copy here as NC#getForbiddenCapabilities() already returns
         // a new array.
@@ -860,4 +873,17 @@
         // a new array.
         return networkCapabilities.getTransportTypes();
     }
+
+    /**
+     * Gets all the subscription ids set on this {@code NetworkRequest} instance.
+     *
+     * @return Set of Integer values for this instance.
+     */
+    @NonNull
+    @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+    public Set<Integer> getSubscriptionIds() {
+        // No need to make a defensive copy here as NC#getSubscriptionIds() already returns
+        // a new set.
+        return networkCapabilities.getSubscriptionIds();
+    }
 }
diff --git a/framework/src/android/net/NetworkScore.java b/framework/src/android/net/NetworkScore.java
index 815e2b0..935dea1 100644
--- a/framework/src/android/net/NetworkScore.java
+++ b/framework/src/android/net/NetworkScore.java
@@ -44,7 +44,9 @@
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
             KEEP_CONNECTED_NONE,
-            KEEP_CONNECTED_FOR_HANDOVER
+            KEEP_CONNECTED_FOR_HANDOVER,
+            KEEP_CONNECTED_FOR_TEST,
+            KEEP_CONNECTED_LOCAL_NETWORK
     })
     public @interface KeepConnectedReason { }
 
@@ -57,6 +59,18 @@
      * is being considered for handover.
      */
     public static final int KEEP_CONNECTED_FOR_HANDOVER = 1;
+    /**
+     * Keep this network connected even if there is no outstanding request for it, because it
+     * is used in a test and it's not necessarily easy to file the right request for it.
+     * @hide
+     */
+    public static final int KEEP_CONNECTED_FOR_TEST = 2;
+    /**
+     * Keep this network connected even if there is no outstanding request for it, because
+     * it is a local network.
+     * @hide
+     */
+    public static final int KEEP_CONNECTED_LOCAL_NETWORK = 3;
 
     // Agent-managed policies
     // This network should lose to a wifi that has ever been validated
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/NetworkUtils.java b/framework/src/android/net/NetworkUtils.java
index 2679b62..18feb84 100644
--- a/framework/src/android/net/NetworkUtils.java
+++ b/framework/src/android/net/NetworkUtils.java
@@ -426,4 +426,21 @@
         return routedIPCount;
     }
 
+    /**
+     * Sets a socket option with byte array
+     *
+     * @param fd The socket file descriptor
+     * @param level The level at which the option is defined
+     * @param option The socket option for which the value is to be set
+     * @param value The option value to be set in byte array
+     * @throws ErrnoException if setsockopt fails
+     */
+    public static native void setsockoptBytes(FileDescriptor fd, int level, int option,
+            byte[] value) throws ErrnoException;
+
+    /** Returns whether the Linux Kernel is 64 bit */
+    public static native boolean isKernel64Bit();
+
+    /** Returns whether the Linux Kernel is x86 */
+    public static native boolean isKernelX86();
 }
diff --git a/framework/src/android/net/QosSession.java b/framework/src/android/net/QosSession.java
index 25f3965..d1edae9 100644
--- a/framework/src/android/net/QosSession.java
+++ b/framework/src/android/net/QosSession.java
@@ -22,6 +22,9 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Provides identifying information of a QoS session.  Sent to an application through
  * {@link QosCallback}.
@@ -107,6 +110,7 @@
             TYPE_EPS_BEARER,
             TYPE_NR_BEARER,
     })
+    @Retention(RetentionPolicy.SOURCE)
     @interface QosSessionType {}
 
     private QosSession(final Parcel in) {
diff --git a/framework/src/android/net/RouteInfo.java b/framework/src/android/net/RouteInfo.java
index df5f151..e8ebf81 100644
--- a/framework/src/android/net/RouteInfo.java
+++ b/framework/src/android/net/RouteInfo.java
@@ -584,7 +584,7 @@
             }
             RouteKey p = (RouteKey) o;
             // No need to do anything special for scoped addresses. Inet6Address#equals does not
-            // consider the scope ID, but the netd route IPCs (e.g., INetd#networkAddRouteParcel)
+            // consider the scope ID, but the route IPCs (e.g., RoutingCoordinatorManager#addRoute)
             // and the kernel ignore scoped addresses both in the prefix and in the nexthop and only
             // look at RTA_OIF.
             return Objects.equals(p.mDestination, mDestination)
diff --git a/framework/src/android/net/UidOwnerValue.java b/framework/src/android/net/UidOwnerValue.java
new file mode 100644
index 0000000..e8ae604
--- /dev/null
+++ b/framework/src/android/net/UidOwnerValue.java
@@ -0,0 +1,39 @@
+/*
+ * 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 com.android.net.module.util.Struct;
+
+/**
+ * Value type for per uid traffic control configuration map.
+ *
+ * @hide
+ */
+public class UidOwnerValue extends Struct {
+    // Allowed interface index. Only applicable if IIF_MATCH is set in the rule bitmask below.
+    @Field(order = 0, type = Type.S32)
+    public final int iif;
+
+    // A bitmask of match type.
+    @Field(order = 1, type = Type.U32)
+    public final long rule;
+
+    public UidOwnerValue(final int iif, final long rule) {
+        this.iif = iif;
+        this.rule = rule;
+    }
+}
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
new file mode 100644
index 0000000..6e87ed3
--- /dev/null
+++ b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
@@ -0,0 +1,60 @@
+/*
+ * 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.connectivity;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.os.Build;
+import android.os.IBinder;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * Utility providing limited access to module-internal APIs which are only available on Android T+,
+ * as this class is only in the bootclasspath on T+ as part of framework-connectivity.
+ *
+ * R+ module components like Tethering cannot depend on all hidden symbols from
+ * framework-connectivity. They only have access to stable API stubs where newer APIs can be
+ * accessed after an API level check (enforced by the linter), or to limited hidden symbols in this
+ * class which is also annotated with @RequiresApi (so API level checks are also enforced by the
+ * linter).
+ * @hide
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class ConnectivityInternalApiUtil {
+
+    /**
+     * Get a service binder token for
+     * {@link com.android.server.connectivity.wear.CompanionDeviceManagerProxyService}.
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public static IBinder getCompanionDeviceManagerProxyService(Context ctx) {
+        final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
+        return cm.getCompanionDeviceManagerProxyService();
+    }
+
+    /**
+     * Obtain a routing coordinator manager from a context, possibly cross-module.
+     * @param ctx the context
+     * @return an instance of the coordinator manager
+     */
+    @RequiresApi(Build.VERSION_CODES.S)
+    public static IBinder getRoutingCoordinator(Context ctx) {
+        final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
+        return cm.getRoutingCoordinatorService();
+    }
+}
diff --git a/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java b/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java
deleted file mode 100644
index d65858f..0000000
--- a/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java
+++ /dev/null
@@ -1,48 +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.connectivity;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.os.Build;
-import android.os.IBinder;
-
-import androidx.annotation.RequiresApi;
-
-/**
- * Utility providing limited access to module-internal APIs which are only available on Android T+,
- * as this class is only in the bootclasspath on T+ as part of framework-connectivity.
- *
- * R+ module components like Tethering cannot depend on all hidden symbols from
- * framework-connectivity. They only have access to stable API stubs where newer APIs can be
- * accessed after an API level check (enforced by the linter), or to limited hidden symbols in this
- * class which is also annotated with @RequiresApi (so API level checks are also enforced by the
- * linter).
- * @hide
- */
-@RequiresApi(Build.VERSION_CODES.TIRAMISU)
-public class TiramisuConnectivityInternalApiUtil {
-
-    /**
-     * Get a service binder token for
-     * {@link com.android.server.connectivity.wear.CompanionDeviceManagerProxyService}.
-     */
-    public static IBinder getCompanionDeviceManagerProxyService(Context ctx) {
-        final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
-        return cm.getCompanionDeviceManagerProxyService();
-    }
-}
diff --git a/nearby/README.md b/nearby/README.md
index 8451882..8dac61c 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -47,12 +47,29 @@
 ## Build and Install
 
 ```sh
-$ source build/envsetup.sh && lunch <TARGET>
-$ m com.google.android.tethering.next deapexer
-$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input \
-    ${ANDROID_PRODUCT_OUT}/system/apex/com.google.android.tethering.next.capex \
-    --output /tmp/tethering.apex
-$ adb install -r /tmp/tethering.apex
+For master on AOSP (Android) host
+$ source build/envsetup.sh
+$ lunch aosp_oriole-trunk_staging-userdebug
+$ m com.android.tethering
+$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/com.android.tethering.capex --output /tmp/tethering.apex
+$ adb install /tmp/tethering.apex
+$ adb reboot
+
+NOTE: Developers should use AOSP by default, udc-mainline-prod should not be used unless for Google internal features.
+For udc-mainline-prod on Google internal host
+Build unbundled module using banchan
+$ source build/envsetup.sh
+$ banchan com.google.android.tethering mainline_modules_arm64
+$ m apps_only dist
+$ adb install out/dist/com.google.android.tethering.apex
+$ adb reboot
+Ensure that the module you are installing is compatible with the module currently preloaded on the phone (in /system/apex/com.google.android.tethering.apex). Compatible means:
+
+1. Same package name
+2. Same keys used to sign the apex and the payload
+3. Higher version
+
+See go/mainline-local-build#build-install-local-module for more information
 ```
 
 ## Build and Install from tm-mainline-prod branch
@@ -63,7 +80,7 @@
 This is because the device is flashed with AOSP built from master or other branches, which has
 prebuilt APEX with higher version. We can use root access to replace the prebuilt APEX with the APEX
 built from tm-mainline-prod as below.
-1. adb root && adb remount; adb shell mount -orw,remount /system/apex
+1. adb root && adb remount -R
 2. cp tethering.next.apex com.google.android.tethering.apex
 3. adb push  com.google.android.tethering.apex  /system/apex/
 4. adb reboot
diff --git a/nearby/TEST_MAPPING b/nearby/TEST_MAPPING
index dbaca33..7e9a375 100644
--- a/nearby/TEST_MAPPING
+++ b/nearby/TEST_MAPPING
@@ -2,6 +2,11 @@
   "presubmit": [
     {
       "name": "NearbyUnitTests"
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "NearbyUnitTests"
     },
     {
       "name": "NearbyIntegrationPrivilegedTests"
@@ -9,11 +14,6 @@
     {
       "name": "NearbyIntegrationUntrustedTests"
     }
-  ],
-  "postsubmit": [
-    {
-      "name": "NearbyUnitTests"
-    }
   ]
   // TODO(b/193602229): uncomment once it's supported.
   //"mainline-presubmit": [
diff --git a/nearby/apex/Android.bp b/nearby/apex/Android.bp
index d7f063a..5fdf5c9 100644
--- a/nearby/apex/Android.bp
+++ b/nearby/apex/Android.bp
@@ -12,6 +12,7 @@
 // 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"],
 }
 
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index a8a6eaa..41a28a0 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -26,6 +27,7 @@
     ],
     path: "java",
     visibility: [
+        "//packages/modules/Connectivity/framework:__subpackages__",
         "//packages/modules/Connectivity/framework-t:__subpackages__",
     ],
 }
@@ -48,11 +50,15 @@
         "androidx.annotation_annotation",
         "framework-annotations-lib",
         "framework-bluetooth",
+        "framework-location.stubs.module_lib",
     ],
     static_libs: [
         "modules-utils-preconditions",
     ],
     visibility: [
-    "//packages/modules/Connectivity/nearby/tests:__subpackages__",
-    "//packages/modules/Connectivity/nearby/halfsheet:__subpackages__"],
+        "//packages/modules/Connectivity/nearby/tests:__subpackages__",
+    ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/nearby/framework/java/android/nearby/BroadcastRequest.java b/nearby/framework/java/android/nearby/BroadcastRequest.java
index 90f4d0f..6d6357d 100644
--- a/nearby/framework/java/android/nearby/BroadcastRequest.java
+++ b/nearby/framework/java/android/nearby/BroadcastRequest.java
@@ -88,6 +88,7 @@
      * @hide
      */
     @IntDef({MEDIUM_BLE})
+    @Retention(RetentionPolicy.SOURCE)
     public @interface Medium {}
 
     /**
diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java
index 10c1132..8f032bf 100644
--- a/nearby/framework/java/android/nearby/DataElement.java
+++ b/nearby/framework/java/android/nearby/DataElement.java
@@ -39,10 +39,15 @@
     private final int mKey;
     private final byte[] mValue;
 
-    /** @hide */
+    /**
+     * Note this interface is used for internal implementation only.
+     * We only keep those data element types used for encoding and decoding from the specs.
+     * Please read the nearby specs for learning more about each data type and use it as the only
+     * source.
+     *
+     * @hide
+     */
     @IntDef({
-            DataType.BLE_SERVICE_DATA,
-            DataType.BLE_ADDRESS,
             DataType.SALT,
             DataType.PRIVATE_IDENTITY,
             DataType.TRUSTED_IDENTITY,
@@ -50,20 +55,17 @@
             DataType.PROVISIONED_IDENTITY,
             DataType.TX_POWER,
             DataType.ACTION,
-            DataType.MODEL_ID,
-            DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER,
             DataType.ACCOUNT_KEY_DATA,
             DataType.CONNECTION_STATUS,
             DataType.BATTERY,
+            DataType.ENCRYPTION_INFO,
+            DataType.BLE_SERVICE_DATA,
+            DataType.BLE_ADDRESS,
             DataType.SCAN_MODE,
             DataType.TEST_DE_BEGIN,
             DataType.TEST_DE_END
     })
     public @interface DataType {
-        int BLE_SERVICE_DATA = 100;
-        int BLE_ADDRESS = 101;
-        // This is to indicate if the scan is offload only
-        int SCAN_MODE = 102;
         int SALT = 0;
         int PRIVATE_IDENTITY = 1;
         int TRUSTED_IDENTITY = 2;
@@ -71,11 +73,19 @@
         int PROVISIONED_IDENTITY = 4;
         int TX_POWER = 5;
         int ACTION = 6;
-        int MODEL_ID = 7;
-        int EDDYSTONE_EPHEMERAL_IDENTIFIER = 8;
         int ACCOUNT_KEY_DATA = 9;
         int CONNECTION_STATUS = 10;
         int BATTERY = 11;
+
+        int ENCRYPTION_INFO = 16;
+
+        // Not defined in the spec. Reserved for internal use only.
+        int BLE_SERVICE_DATA = 100;
+        int BLE_ADDRESS = 101;
+        // This is to indicate if the scan is offload only
+        int SCAN_MODE = 102;
+
+        int DEVICE_TYPE = 22;
         // Reserves test DE ranges from {@link DataElement.DataType#TEST_DE_BEGIN}
         // to {@link DataElement.DataType#TEST_DE_END}, inclusive.
         // Reserves 128 Test DEs.
@@ -84,27 +94,6 @@
     }
 
     /**
-     * @hide
-     */
-    public static boolean isValidType(int type) {
-        return type == DataType.BLE_SERVICE_DATA
-                || type == DataType.ACCOUNT_KEY_DATA
-                || type == DataType.BLE_ADDRESS
-                || type == DataType.SCAN_MODE
-                || type == DataType.SALT
-                || type == DataType.PRIVATE_IDENTITY
-                || type == DataType.TRUSTED_IDENTITY
-                || type == DataType.PUBLIC_IDENTITY
-                || type == DataType.PROVISIONED_IDENTITY
-                || type == DataType.TX_POWER
-                || type == DataType.ACTION
-                || type == DataType.MODEL_ID
-                || type == DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER
-                || type == DataType.CONNECTION_STATUS
-                || type == DataType.BATTERY;
-    }
-
-    /**
      * @return {@code true} if this is identity type.
      * @hide
      */
diff --git a/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java b/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java
deleted file mode 100644
index d42fbf4..0000000
--- a/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java
+++ /dev/null
@@ -1,183 +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 android.nearby;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
-
-/**
- * Class for metadata of a Fast Pair device associated with an account.
- *
- * @hide
- */
-public class FastPairAccountKeyDeviceMetadata {
-
-    FastPairAccountKeyDeviceMetadataParcel mMetadataParcel;
-
-    FastPairAccountKeyDeviceMetadata(FastPairAccountKeyDeviceMetadataParcel metadataParcel) {
-        this.mMetadataParcel = metadataParcel;
-    }
-
-    /**
-     * Get Device Account Key, which uniquely identifies a Fast Pair device associated with an
-     * account. AccountKey is 16 bytes: first byte is 0x04. Other 15 bytes are randomly generated.
-     *
-     * @return 16-byte Account Key.
-     * @hide
-     */
-    @Nullable
-    public byte[] getDeviceAccountKey() {
-        return mMetadataParcel.deviceAccountKey;
-    }
-
-    /**
-     * Get a hash value of device's account key and public bluetooth address without revealing the
-     * public bluetooth address. Sha256 hash value is 32 bytes.
-     *
-     * @return 32-byte Sha256 hash value.
-     * @hide
-     */
-    @Nullable
-    public byte[] getSha256DeviceAccountKeyPublicAddress() {
-        return mMetadataParcel.sha256DeviceAccountKeyPublicAddress;
-    }
-
-    /**
-     * Get metadata of a Fast Pair device type.
-     *
-     * @hide
-     */
-    @Nullable
-    public FastPairDeviceMetadata getFastPairDeviceMetadata() {
-        if (mMetadataParcel.metadata == null) {
-            return null;
-        }
-        return new FastPairDeviceMetadata(mMetadataParcel.metadata);
-    }
-
-    /**
-     * Get Fast Pair discovery item, which is tied to both the device type and the account.
-     *
-     * @hide
-     */
-    @Nullable
-    public FastPairDiscoveryItem getFastPairDiscoveryItem() {
-        if (mMetadataParcel.discoveryItem == null) {
-            return null;
-        }
-        return new FastPairDiscoveryItem(mMetadataParcel.discoveryItem);
-    }
-
-    /**
-     * Builder used to create FastPairAccountKeyDeviceMetadata.
-     *
-     * @hide
-     */
-    public static final class Builder {
-
-        private final FastPairAccountKeyDeviceMetadataParcel mBuilderParcel;
-
-        /**
-         * Default constructor of Builder.
-         *
-         * @hide
-         */
-        public Builder() {
-            mBuilderParcel = new FastPairAccountKeyDeviceMetadataParcel();
-            mBuilderParcel.deviceAccountKey = null;
-            mBuilderParcel.sha256DeviceAccountKeyPublicAddress = null;
-            mBuilderParcel.metadata = null;
-            mBuilderParcel.discoveryItem = null;
-        }
-
-        /**
-         * Set Account Key.
-         *
-         * @param deviceAccountKey Fast Pair device account key, which is 16 bytes: first byte is
-         *                         0x04. Next 15 bytes are randomly generated.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setDeviceAccountKey(@Nullable byte[] deviceAccountKey) {
-            mBuilderParcel.deviceAccountKey = deviceAccountKey;
-            return this;
-        }
-
-        /**
-         * Set sha256 hash value of account key and public bluetooth address.
-         *
-         * @param sha256DeviceAccountKeyPublicAddress 32-byte sha256 hash value of account key and
-         *                                            public bluetooth address.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setSha256DeviceAccountKeyPublicAddress(
-                @Nullable byte[] sha256DeviceAccountKeyPublicAddress) {
-            mBuilderParcel.sha256DeviceAccountKeyPublicAddress =
-                    sha256DeviceAccountKeyPublicAddress;
-            return this;
-        }
-
-
-        /**
-         * Set Fast Pair metadata.
-         *
-         * @param metadata Fast Pair metadata.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setFastPairDeviceMetadata(@Nullable FastPairDeviceMetadata metadata) {
-            if (metadata == null) {
-                mBuilderParcel.metadata = null;
-            } else {
-                mBuilderParcel.metadata = metadata.mMetadataParcel;
-            }
-            return this;
-        }
-
-        /**
-         * Set Fast Pair discovery item.
-         *
-         * @param discoveryItem Fast Pair discovery item.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setFastPairDiscoveryItem(@Nullable FastPairDiscoveryItem discoveryItem) {
-            if (discoveryItem == null) {
-                mBuilderParcel.discoveryItem = null;
-            } else {
-                mBuilderParcel.discoveryItem = discoveryItem.mMetadataParcel;
-            }
-            return this;
-        }
-
-        /**
-         * Build {@link FastPairAccountKeyDeviceMetadata} with the currently set configuration.
-         *
-         * @hide
-         */
-        @NonNull
-        public FastPairAccountKeyDeviceMetadata build() {
-            return new FastPairAccountKeyDeviceMetadata(mBuilderParcel);
-        }
-    }
-}
diff --git a/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java b/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java
deleted file mode 100644
index 74831d5..0000000
--- a/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java
+++ /dev/null
@@ -1,119 +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 android.nearby;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
-
-/**
- * Class for a type of registered Fast Pair device keyed by modelID, or antispoofKey.
- *
- * @hide
- */
-public class FastPairAntispoofKeyDeviceMetadata {
-
-    FastPairAntispoofKeyDeviceMetadataParcel mMetadataParcel;
-    FastPairAntispoofKeyDeviceMetadata(
-            FastPairAntispoofKeyDeviceMetadataParcel metadataParcel) {
-        this.mMetadataParcel = metadataParcel;
-    }
-
-    /**
-     * Get Antispoof public key.
-     *
-     * @hide
-     */
-    @Nullable
-    public byte[] getAntispoofPublicKey() {
-        return this.mMetadataParcel.antispoofPublicKey;
-    }
-
-    /**
-     * Get metadata of a Fast Pair device type.
-     *
-     * @hide
-     */
-    @Nullable
-    public FastPairDeviceMetadata getFastPairDeviceMetadata() {
-        if (this.mMetadataParcel.deviceMetadata == null) {
-            return null;
-        }
-        return new FastPairDeviceMetadata(this.mMetadataParcel.deviceMetadata);
-    }
-
-    /**
-     * Builder used to create FastPairAntispoofkeyDeviceMetadata.
-     *
-     * @hide
-     */
-    public static final class Builder {
-
-        private final FastPairAntispoofKeyDeviceMetadataParcel mBuilderParcel;
-
-        /**
-         * Default constructor of Builder.
-         *
-         * @hide
-         */
-        public Builder() {
-            mBuilderParcel = new FastPairAntispoofKeyDeviceMetadataParcel();
-            mBuilderParcel.antispoofPublicKey = null;
-            mBuilderParcel.deviceMetadata = null;
-        }
-
-        /**
-         * Set AntiSpoof public key, which uniquely identify a Fast Pair device type.
-         *
-         * @param antispoofPublicKey is 64 bytes, see <a href="https://developers.google.com/nearby/fast-pair/spec#data_format">Data Format</a>.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setAntispoofPublicKey(@Nullable byte[] antispoofPublicKey) {
-            mBuilderParcel.antispoofPublicKey = antispoofPublicKey;
-            return this;
-        }
-
-        /**
-         * Set Fast Pair metadata, which is the property of a Fast Pair device type, including
-         * device images and strings.
-         *
-         * @param metadata Fast Pair device meta data.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setFastPairDeviceMetadata(@Nullable FastPairDeviceMetadata metadata) {
-            if (metadata != null) {
-                mBuilderParcel.deviceMetadata = metadata.mMetadataParcel;
-            } else {
-                mBuilderParcel.deviceMetadata = null;
-            }
-            return this;
-        }
-
-        /**
-         * Build {@link FastPairAntispoofKeyDeviceMetadata} with the currently set configuration.
-         *
-         * @hide
-         */
-        @NonNull
-        public FastPairAntispoofKeyDeviceMetadata build() {
-            return new FastPairAntispoofKeyDeviceMetadata(mBuilderParcel);
-        }
-    }
-}
diff --git a/nearby/framework/java/android/nearby/FastPairDataProviderService.java b/nearby/framework/java/android/nearby/FastPairDataProviderService.java
deleted file mode 100644
index f1d5074..0000000
--- a/nearby/framework/java/android/nearby/FastPairDataProviderService.java
+++ /dev/null
@@ -1,714 +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 android.nearby;
-
-import android.accounts.Account;
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.Service;
-import android.content.Intent;
-import android.nearby.aidl.ByteArrayParcel;
-import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
-import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
-import android.nearby.aidl.FastPairEligibleAccountParcel;
-import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
-import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
-import android.nearby.aidl.FastPairManageAccountRequestParcel;
-import android.nearby.aidl.IFastPairAccountDevicesMetadataCallback;
-import android.nearby.aidl.IFastPairAntispoofKeyDeviceMetadataCallback;
-import android.nearby.aidl.IFastPairDataProvider;
-import android.nearby.aidl.IFastPairEligibleAccountsCallback;
-import android.nearby.aidl.IFastPairManageAccountCallback;
-import android.nearby.aidl.IFastPairManageAccountDeviceCallback;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-/**
- * A service class for fast pair data providers outside the system server.
- *
- * Fast pair providers should be wrapped in a non-exported service which returns the result of
- * {@link #getBinder()} from the service's {@link android.app.Service#onBind(Intent)} method. The
- * service should not be exported so that components other than the system server cannot bind to it.
- * Alternatively, the service may be guarded by a permission that only system server can obtain.
- *
- * <p>Fast Pair providers are identified by their UID / package name.
- *
- * @hide
- */
-public abstract class FastPairDataProviderService extends Service {
-    /**
-     * The action the wrapping service should have in its intent filter to implement the
-     * {@link android.nearby.FastPairDataProviderBase}.
-     *
-     * @hide
-     */
-    public static final String ACTION_FAST_PAIR_DATA_PROVIDER =
-            "android.nearby.action.FAST_PAIR_DATA_PROVIDER";
-
-    /**
-     * Manage request type to add, or opt-in.
-     *
-     * @hide
-     */
-    public static final int MANAGE_REQUEST_ADD = 0;
-
-    /**
-     * Manage request type to remove, or opt-out.
-     *
-     * @hide
-     */
-    public static final int MANAGE_REQUEST_REMOVE = 1;
-
-    /**
-     * @hide
-     */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(value = {
-            MANAGE_REQUEST_ADD,
-            MANAGE_REQUEST_REMOVE})
-    @interface ManageRequestType {}
-
-    /**
-     * Error code for bad request.
-     *
-     * @hide
-     */
-    public static final int ERROR_CODE_BAD_REQUEST = 0;
-
-    /**
-     * Error code for internal error.
-     *
-     * @hide
-     */
-    public static final int ERROR_CODE_INTERNAL_ERROR = 1;
-
-    /**
-     * @hide
-     */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(value = {
-            ERROR_CODE_BAD_REQUEST,
-            ERROR_CODE_INTERNAL_ERROR})
-    @interface ErrorCode {}
-
-    private final IBinder mBinder;
-    private final String mTag;
-
-    /**
-     * Constructor of FastPairDataProviderService.
-     *
-     * @param tag TAG for on device logging.
-     * @hide
-     */
-    public FastPairDataProviderService(@NonNull String tag) {
-        mBinder = new Service();
-        mTag = tag;
-    }
-
-    @Override
-    @NonNull
-    public final IBinder onBind(@NonNull Intent intent) {
-        return mBinder;
-    }
-
-    /**
-     * Callback to be invoked when an AntispoofKeyed device metadata is loaded.
-     *
-     * @hide
-     */
-    public interface FastPairAntispoofKeyDeviceMetadataCallback {
-
-        /**
-         * Invoked once the meta data is loaded.
-         *
-         * @hide
-         */
-        void onFastPairAntispoofKeyDeviceMetadataReceived(
-                @NonNull FastPairAntispoofKeyDeviceMetadata metadata);
-
-        /** Invoked in case of error.
-         *
-         * @hide
-         */
-        void onError(@ErrorCode int code, @Nullable String message);
-    }
-
-    /**
-     * Callback to be invoked when Fast Pair devices of a given account is loaded.
-     *
-     * @hide
-     */
-    public interface FastPairAccountDevicesMetadataCallback {
-
-        /**
-         * Should be invoked once the metadatas are loaded.
-         *
-         * @hide
-         */
-        void onFastPairAccountDevicesMetadataReceived(
-                @NonNull Collection<FastPairAccountKeyDeviceMetadata> metadatas);
-        /**
-         * Invoked in case of error.
-         *
-         * @hide
-         */
-        void onError(@ErrorCode int code, @Nullable String message);
-    }
-
-    /**
-     * Callback to be invoked when FastPair eligible accounts are loaded.
-     *
-     * @hide
-     */
-    public interface FastPairEligibleAccountsCallback {
-
-        /**
-         * Should be invoked once the eligible accounts are loaded.
-         *
-         * @hide
-         */
-        void onFastPairEligibleAccountsReceived(
-                @NonNull Collection<FastPairEligibleAccount> accounts);
-        /**
-         * Invoked in case of error.
-         *
-         * @hide
-         */
-        void onError(@ErrorCode int code, @Nullable String message);
-    }
-
-    /**
-     * Callback to be invoked when a management action is finished.
-     *
-     * @hide
-     */
-    public interface FastPairManageActionCallback {
-
-        /**
-         * Should be invoked once the manage action is successful.
-         *
-         * @hide
-         */
-        void onSuccess();
-        /**
-         * Invoked in case of error.
-         *
-         * @hide
-         */
-        void onError(@ErrorCode int code, @Nullable String message);
-    }
-
-    /**
-     * Fulfills the Fast Pair device metadata request by using callback to send back the
-     * device meta data of a given modelId.
-     *
-     * @hide
-     */
-    public abstract void onLoadFastPairAntispoofKeyDeviceMetadata(
-            @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
-            @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback);
-
-    /**
-     * Fulfills the account tied Fast Pair devices metadata request by using callback to send back
-     * all Fast Pair device's metadata of a given account.
-     *
-     * @hide
-     */
-    public abstract void onLoadFastPairAccountDevicesMetadata(
-            @NonNull FastPairAccountDevicesMetadataRequest request,
-            @NonNull FastPairAccountDevicesMetadataCallback callback);
-
-    /**
-     * Fulfills the Fast Pair eligible accounts request by using callback to send back Fast Pair
-     * eligible accounts.
-     *
-     * @hide
-     */
-    public abstract void onLoadFastPairEligibleAccounts(
-            @NonNull FastPairEligibleAccountsRequest request,
-            @NonNull FastPairEligibleAccountsCallback callback);
-
-    /**
-     * Fulfills the Fast Pair account management request by using callback to send back result.
-     *
-     * @hide
-     */
-    public abstract void onManageFastPairAccount(
-            @NonNull FastPairManageAccountRequest request,
-            @NonNull FastPairManageActionCallback callback);
-
-    /**
-     * Fulfills the request to manage device-account mapping by using callback to send back result.
-     *
-     * @hide
-     */
-    public abstract void onManageFastPairAccountDevice(
-            @NonNull FastPairManageAccountDeviceRequest request,
-            @NonNull FastPairManageActionCallback callback);
-
-    /**
-     * Class for reading FastPairAntispoofKeyDeviceMetadataRequest, which specifies the model ID of
-     * a Fast Pair device. To fulfill this request, corresponding
-     * {@link FastPairAntispoofKeyDeviceMetadata} should be fetched and returned.
-     *
-     * @hide
-     */
-    public static class FastPairAntispoofKeyDeviceMetadataRequest {
-
-        private final FastPairAntispoofKeyDeviceMetadataRequestParcel mMetadataRequestParcel;
-
-        private FastPairAntispoofKeyDeviceMetadataRequest(
-                final FastPairAntispoofKeyDeviceMetadataRequestParcel metaDataRequestParcel) {
-            this.mMetadataRequestParcel = metaDataRequestParcel;
-        }
-
-        /**
-         * Get modelId (24 bit), the key for FastPairAntispoofKeyDeviceMetadata in the same format
-         * returned by Google at device registration time.
-         *
-         * ModelId format is defined at device registration time, see
-         * <a href="https://developers.google.com/nearby/fast-pair/spec#model_id">Model ID</a>.
-         * @return raw bytes of modelId in the same format returned by Google at device registration
-         *         time.
-         * @hide
-         */
-        public @NonNull byte[] getModelId() {
-            return this.mMetadataRequestParcel.modelId;
-        }
-    }
-
-    /**
-     * Class for reading FastPairAccountDevicesMetadataRequest, which specifies the Fast Pair
-     * account and the allow list of the FastPair device keys saved to the account (i.e., FastPair
-     * accountKeys).
-     *
-     * A Fast Pair accountKey is created when a Fast Pair device is saved to an account. It is per
-     * Fast Pair device per account.
-     *
-     * To retrieve all Fast Pair accountKeys saved to an account, the caller needs to set
-     * account with an empty allow list.
-     *
-     * To retrieve metadata of a selected list of Fast Pair devices saved to an account, the caller
-     * needs to set account with a non-empty allow list.
-     * @hide
-     */
-    public static class FastPairAccountDevicesMetadataRequest {
-
-        private final FastPairAccountDevicesMetadataRequestParcel mMetadataRequestParcel;
-
-        private FastPairAccountDevicesMetadataRequest(
-                final FastPairAccountDevicesMetadataRequestParcel metaDataRequestParcel) {
-            this.mMetadataRequestParcel = metaDataRequestParcel;
-        }
-
-        /**
-         * Get FastPair account, whose Fast Pair devices' metadata is requested.
-         *
-         * @return a FastPair account.
-         * @hide
-         */
-        public @NonNull Account getAccount() {
-            return this.mMetadataRequestParcel.account;
-        }
-
-        /**
-         * Get allowlist of Fast Pair devices using a collection of deviceAccountKeys.
-         * Note that as a special case, empty list actually means all FastPair devices under the
-         * account instead of none.
-         *
-         * DeviceAccountKey is 16 bytes: first byte is 0x04. Other 15 bytes are randomly generated.
-         *
-         * @return allowlist of Fast Pair devices using a collection of deviceAccountKeys.
-         * @hide
-         */
-        public @NonNull Collection<byte[]> getDeviceAccountKeys()  {
-            if (this.mMetadataRequestParcel.deviceAccountKeys == null) {
-                return new ArrayList<byte[]>(0);
-            }
-            List<byte[]> deviceAccountKeys =
-                    new ArrayList<>(this.mMetadataRequestParcel.deviceAccountKeys.length);
-            for (ByteArrayParcel deviceAccountKey : this.mMetadataRequestParcel.deviceAccountKeys) {
-                deviceAccountKeys.add(deviceAccountKey.byteArray);
-            }
-            return deviceAccountKeys;
-        }
-    }
-
-    /**
-     *  Class for reading FastPairEligibleAccountsRequest. Upon receiving this request, Fast Pair
-     *  eligible accounts should be returned to bind Fast Pair devices.
-     *
-     * @hide
-     */
-    public static class FastPairEligibleAccountsRequest {
-        @SuppressWarnings("UnusedVariable")
-        private final FastPairEligibleAccountsRequestParcel mAccountsRequestParcel;
-
-        private FastPairEligibleAccountsRequest(
-                final FastPairEligibleAccountsRequestParcel accountsRequestParcel) {
-            this.mAccountsRequestParcel = accountsRequestParcel;
-        }
-    }
-
-    /**
-     * Class for reading FastPairManageAccountRequest. If the request type is MANAGE_REQUEST_ADD,
-     * the account is enabled to bind Fast Pair devices; If the request type is
-     * MANAGE_REQUEST_REMOVE, the account is disabled to bind more Fast Pair devices. Furthermore,
-     * all existing bounded Fast Pair devices are unbounded.
-     *
-     * @hide
-     */
-    public static class FastPairManageAccountRequest {
-
-        private final FastPairManageAccountRequestParcel mAccountRequestParcel;
-
-        private FastPairManageAccountRequest(
-                final FastPairManageAccountRequestParcel accountRequestParcel) {
-            this.mAccountRequestParcel = accountRequestParcel;
-        }
-
-        /**
-         * Get request type: MANAGE_REQUEST_ADD, or MANAGE_REQUEST_REMOVE.
-         *
-         * @hide
-         */
-        public @ManageRequestType int getRequestType() {
-            return this.mAccountRequestParcel.requestType;
-        }
-        /**
-         * Get account.
-         *
-         * @hide
-         */
-        public @NonNull Account getAccount() {
-            return this.mAccountRequestParcel.account;
-        }
-    }
-
-    /**
-     *  Class for reading FastPairManageAccountDeviceRequest. If the request type is
-     *  MANAGE_REQUEST_ADD, then a Fast Pair device is bounded to a Fast Pair account. If the
-     *  request type is MANAGE_REQUEST_REMOVE, then a Fast Pair device is removed from a Fast Pair
-     *  account.
-     *
-     * @hide
-     */
-    public static class FastPairManageAccountDeviceRequest {
-
-        private final FastPairManageAccountDeviceRequestParcel mRequestParcel;
-
-        private FastPairManageAccountDeviceRequest(
-                final FastPairManageAccountDeviceRequestParcel requestParcel) {
-            this.mRequestParcel = requestParcel;
-        }
-
-        /**
-         * Get request type: MANAGE_REQUEST_ADD, or MANAGE_REQUEST_REMOVE.
-         *
-         * @hide
-         */
-        public @ManageRequestType int getRequestType() {
-            return this.mRequestParcel.requestType;
-        }
-        /**
-         * Get account.
-         *
-         * @hide
-         */
-        public @NonNull Account getAccount() {
-            return this.mRequestParcel.account;
-        }
-        /**
-         * Get account key device metadata.
-         *
-         * @hide
-         */
-        public @NonNull FastPairAccountKeyDeviceMetadata getAccountKeyDeviceMetadata() {
-            return new FastPairAccountKeyDeviceMetadata(
-                    this.mRequestParcel.accountKeyDeviceMetadata);
-        }
-    }
-
-    /**
-     * Callback class that sends back FastPairAntispoofKeyDeviceMetadata.
-     */
-    private final class WrapperFastPairAntispoofKeyDeviceMetadataCallback implements
-            FastPairAntispoofKeyDeviceMetadataCallback {
-
-        private IFastPairAntispoofKeyDeviceMetadataCallback mCallback;
-
-        private WrapperFastPairAntispoofKeyDeviceMetadataCallback(
-                IFastPairAntispoofKeyDeviceMetadataCallback callback) {
-            mCallback = callback;
-        }
-
-        /**
-         * Sends back FastPairAntispoofKeyDeviceMetadata.
-         */
-        @Override
-        public void onFastPairAntispoofKeyDeviceMetadataReceived(
-                @NonNull FastPairAntispoofKeyDeviceMetadata metadata) {
-            try {
-                mCallback.onFastPairAntispoofKeyDeviceMetadataReceived(metadata.mMetadataParcel);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            } catch (RuntimeException e) {
-                Log.w(mTag, e);
-            }
-        }
-
-        @Override
-        public void onError(@ErrorCode int code, @Nullable String message) {
-            try {
-                mCallback.onError(code, message);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            } catch (RuntimeException e) {
-                Log.w(mTag, e);
-            }
-        }
-    }
-
-    /**
-     * Callback class that sends back collection of FastPairAccountKeyDeviceMetadata.
-     */
-    private final class WrapperFastPairAccountDevicesMetadataCallback implements
-            FastPairAccountDevicesMetadataCallback {
-
-        private IFastPairAccountDevicesMetadataCallback mCallback;
-
-        private WrapperFastPairAccountDevicesMetadataCallback(
-                IFastPairAccountDevicesMetadataCallback callback) {
-            mCallback = callback;
-        }
-
-        /**
-         * Sends back collection of FastPairAccountKeyDeviceMetadata.
-         */
-        @Override
-        public void onFastPairAccountDevicesMetadataReceived(
-                @NonNull Collection<FastPairAccountKeyDeviceMetadata> metadatas) {
-            FastPairAccountKeyDeviceMetadataParcel[] metadataParcels =
-                    new FastPairAccountKeyDeviceMetadataParcel[metadatas.size()];
-            int i = 0;
-            for (FastPairAccountKeyDeviceMetadata metadata : metadatas) {
-                metadataParcels[i] = metadata.mMetadataParcel;
-                i = i + 1;
-            }
-            try {
-                mCallback.onFastPairAccountDevicesMetadataReceived(metadataParcels);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            } catch (RuntimeException e) {
-                Log.w(mTag, e);
-            }
-        }
-
-        @Override
-        public void onError(@ErrorCode int code, @Nullable String message) {
-            try {
-                mCallback.onError(code, message);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            } catch (RuntimeException e) {
-                Log.w(mTag, e);
-            }
-        }
-    }
-
-    /**
-     * Callback class that sends back eligible Fast Pair accounts.
-     */
-    private final class WrapperFastPairEligibleAccountsCallback implements
-            FastPairEligibleAccountsCallback {
-
-        private IFastPairEligibleAccountsCallback mCallback;
-
-        private WrapperFastPairEligibleAccountsCallback(
-                IFastPairEligibleAccountsCallback callback) {
-            mCallback = callback;
-        }
-
-        /**
-         * Sends back the eligible Fast Pair accounts.
-         */
-        @Override
-        public void onFastPairEligibleAccountsReceived(
-                @NonNull Collection<FastPairEligibleAccount> accounts) {
-            int i = 0;
-            FastPairEligibleAccountParcel[] accountParcels =
-                    new FastPairEligibleAccountParcel[accounts.size()];
-            for (FastPairEligibleAccount account: accounts) {
-                accountParcels[i] = account.mAccountParcel;
-                i = i + 1;
-            }
-            try {
-                mCallback.onFastPairEligibleAccountsReceived(accountParcels);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            } catch (RuntimeException e) {
-                Log.w(mTag, e);
-            }
-        }
-
-        @Override
-        public void onError(@ErrorCode int code, @Nullable String message) {
-            try {
-                mCallback.onError(code, message);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            } catch (RuntimeException e) {
-                Log.w(mTag, e);
-            }
-        }
-    }
-
-    /**
-     * Callback class that sends back Fast Pair account management result.
-     */
-    private final class WrapperFastPairManageAccountCallback implements
-            FastPairManageActionCallback {
-
-        private IFastPairManageAccountCallback mCallback;
-
-        private WrapperFastPairManageAccountCallback(
-                IFastPairManageAccountCallback callback) {
-            mCallback = callback;
-        }
-
-        /**
-         * Sends back Fast Pair account opt in result.
-         */
-        @Override
-        public void onSuccess() {
-            try {
-                mCallback.onSuccess();
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            } catch (RuntimeException e) {
-                Log.w(mTag, e);
-            }
-        }
-
-        @Override
-        public void onError(@ErrorCode int code, @Nullable String message) {
-            try {
-                mCallback.onError(code, message);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            } catch (RuntimeException e) {
-                Log.w(mTag, e);
-            }
-        }
-    }
-
-    /**
-     * Call back class that sends back account-device mapping management result.
-     */
-    private final class WrapperFastPairManageAccountDeviceCallback implements
-            FastPairManageActionCallback {
-
-        private IFastPairManageAccountDeviceCallback mCallback;
-
-        private WrapperFastPairManageAccountDeviceCallback(
-                IFastPairManageAccountDeviceCallback callback) {
-            mCallback = callback;
-        }
-
-        /**
-         * Sends back the account-device mapping management result.
-         */
-        @Override
-        public void onSuccess() {
-            try {
-                mCallback.onSuccess();
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            } catch (RuntimeException e) {
-                Log.w(mTag, e);
-            }
-        }
-
-        @Override
-        public void onError(@ErrorCode int code, @Nullable String message) {
-            try {
-                mCallback.onError(code, message);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            } catch (RuntimeException e) {
-                Log.w(mTag, e);
-            }
-        }
-    }
-
-    private final class Service extends IFastPairDataProvider.Stub {
-
-        Service() {
-        }
-
-        @Override
-        public void loadFastPairAntispoofKeyDeviceMetadata(
-                @NonNull FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel,
-                IFastPairAntispoofKeyDeviceMetadataCallback callback) {
-            onLoadFastPairAntispoofKeyDeviceMetadata(
-                    new FastPairAntispoofKeyDeviceMetadataRequest(requestParcel),
-                    new WrapperFastPairAntispoofKeyDeviceMetadataCallback(callback));
-        }
-
-        @Override
-        public void loadFastPairAccountDevicesMetadata(
-                @NonNull FastPairAccountDevicesMetadataRequestParcel requestParcel,
-                IFastPairAccountDevicesMetadataCallback callback) {
-            onLoadFastPairAccountDevicesMetadata(
-                    new FastPairAccountDevicesMetadataRequest(requestParcel),
-                    new WrapperFastPairAccountDevicesMetadataCallback(callback));
-        }
-
-        @Override
-        public void loadFastPairEligibleAccounts(
-                @NonNull FastPairEligibleAccountsRequestParcel requestParcel,
-                IFastPairEligibleAccountsCallback callback) {
-            onLoadFastPairEligibleAccounts(new FastPairEligibleAccountsRequest(requestParcel),
-                    new WrapperFastPairEligibleAccountsCallback(callback));
-        }
-
-        @Override
-        public void manageFastPairAccount(
-                @NonNull FastPairManageAccountRequestParcel requestParcel,
-                IFastPairManageAccountCallback callback) {
-            onManageFastPairAccount(new FastPairManageAccountRequest(requestParcel),
-                    new WrapperFastPairManageAccountCallback(callback));
-        }
-
-        @Override
-        public void manageFastPairAccountDevice(
-                @NonNull FastPairManageAccountDeviceRequestParcel requestParcel,
-                IFastPairManageAccountDeviceCallback callback) {
-            onManageFastPairAccountDevice(new FastPairManageAccountDeviceRequest(requestParcel),
-                    new WrapperFastPairManageAccountDeviceCallback(callback));
-        }
-    }
-}
diff --git a/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java b/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java
deleted file mode 100644
index 0e2e79d..0000000
--- a/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java
+++ /dev/null
@@ -1,683 +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 android.nearby;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.nearby.aidl.FastPairDeviceMetadataParcel;
-
-/**
- * Class for the properties of a given type of Fast Pair device, including images and text.
- *
- * @hide
- */
-public class FastPairDeviceMetadata {
-
-    FastPairDeviceMetadataParcel mMetadataParcel;
-
-    FastPairDeviceMetadata(
-            FastPairDeviceMetadataParcel metadataParcel) {
-        this.mMetadataParcel = metadataParcel;
-    }
-
-    /**
-     * Get ImageUrl, which will be displayed in notification.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getImageUrl() {
-        return mMetadataParcel.imageUrl;
-    }
-
-    /**
-     * Get IntentUri, which will be launched to install companion app.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getIntentUri() {
-        return mMetadataParcel.intentUri;
-    }
-
-    /**
-     * Get BLE transmit power, as described in Fast Pair spec, see
-     * <a href="https://developers.google.com/nearby/fast-pair/spec#transmit_power">Transmit Power</a>
-     *
-     * @hide
-     */
-    public int getBleTxPower() {
-        return mMetadataParcel.bleTxPower;
-    }
-
-    /**
-     * Get Fast Pair Half Sheet trigger distance in meters.
-     *
-     * @hide
-     */
-    public float getTriggerDistance() {
-        return mMetadataParcel.triggerDistance;
-    }
-
-    /**
-     * Get Fast Pair device image, which is submitted at device registration time to display on
-     * notification. It is a 32-bit PNG with dimensions of 512px by 512px.
-     *
-     * @return Fast Pair device image in 32-bit PNG with dimensions of 512px by 512px.
-     * @hide
-     */
-    @Nullable
-    public byte[] getImage() {
-        return mMetadataParcel.image;
-    }
-
-    /**
-     * Get Fast Pair device type.
-     * DEVICE_TYPE_UNSPECIFIED = 0;
-     * HEADPHONES = 1;
-     * TRUE_WIRELESS_HEADPHONES = 7;
-     * @hide
-     */
-    public int getDeviceType() {
-        return mMetadataParcel.deviceType;
-    }
-
-    /**
-     * Get Fast Pair device name. e.g., "Pixel Buds A-Series".
-     *
-     * @hide
-     */
-    @Nullable
-    public String getName() {
-        return mMetadataParcel.name;
-    }
-
-    /**
-     * Get true wireless image url for left bud.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getTrueWirelessImageUrlLeftBud() {
-        return mMetadataParcel.trueWirelessImageUrlLeftBud;
-    }
-
-    /**
-     * Get true wireless image url for right bud.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getTrueWirelessImageUrlRightBud() {
-        return mMetadataParcel.trueWirelessImageUrlRightBud;
-    }
-
-    /**
-     * Get true wireless image url for case.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getTrueWirelessImageUrlCase() {
-        return mMetadataParcel.trueWirelessImageUrlCase;
-    }
-
-    /**
-     * Get InitialNotificationDescription, which is a translated string of
-     * "Tap to pair. Earbuds will be tied to %s" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getInitialNotificationDescription() {
-        return mMetadataParcel.initialNotificationDescription;
-    }
-
-    /**
-     * Get InitialNotificationDescriptionNoAccount, which is a translated string of
-     * "Tap to pair with this device" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getInitialNotificationDescriptionNoAccount() {
-        return mMetadataParcel.initialNotificationDescriptionNoAccount;
-    }
-
-    /**
-     * Get OpenCompanionAppDescription, which is a translated string of
-     * "Tap to finish setup" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getOpenCompanionAppDescription() {
-        return mMetadataParcel.openCompanionAppDescription;
-    }
-
-    /**
-     * Get UpdateCompanionAppDescription, which is a translated string of
-     * "Tap to update device settings and finish setup" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getUpdateCompanionAppDescription() {
-        return mMetadataParcel.updateCompanionAppDescription;
-    }
-
-    /**
-     * Get DownloadCompanionAppDescription, which is a translated string of
-     * "Tap to download device app on Google Play and see all features" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getDownloadCompanionAppDescription() {
-        return mMetadataParcel.downloadCompanionAppDescription;
-    }
-
-    /**
-     * Get UnableToConnectTitle, which is a translated string of
-     * "Unable to connect" based on locale.
-     */
-    @Nullable
-    public String getUnableToConnectTitle() {
-        return mMetadataParcel.unableToConnectTitle;
-    }
-
-    /**
-     * Get UnableToConnectDescription, which is a translated string of
-     * "Try manually pairing to the device" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getUnableToConnectDescription() {
-        return mMetadataParcel.unableToConnectDescription;
-    }
-
-    /**
-     * Get InitialPairingDescription, which is a translated string of
-     * "%s will appear on devices linked with %s" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getInitialPairingDescription() {
-        return mMetadataParcel.initialPairingDescription;
-    }
-
-    /**
-     * Get ConnectSuccessCompanionAppInstalled, which is a translated string of
-     * "Your device is ready to be set up" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getConnectSuccessCompanionAppInstalled() {
-        return mMetadataParcel.connectSuccessCompanionAppInstalled;
-    }
-
-    /**
-     * Get ConnectSuccessCompanionAppNotInstalled, which is a translated string of
-     * "Download the device app on Google Play to see all available features" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getConnectSuccessCompanionAppNotInstalled() {
-        return mMetadataParcel.connectSuccessCompanionAppNotInstalled;
-    }
-
-    /**
-     * Get SubsequentPairingDescription, which is a translated string of
-     * "Connect %s to this phone" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getSubsequentPairingDescription() {
-        return mMetadataParcel.subsequentPairingDescription;
-    }
-
-    /**
-     * Get RetroactivePairingDescription, which is a translated string of
-     * "Save device to %s for faster pairing to your other devices" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getRetroactivePairingDescription() {
-        return mMetadataParcel.retroactivePairingDescription;
-    }
-
-    /**
-     * Get WaitLaunchCompanionAppDescription, which is a translated string of
-     * "This will take a few moments" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getWaitLaunchCompanionAppDescription() {
-        return mMetadataParcel.waitLaunchCompanionAppDescription;
-    }
-
-    /**
-     * Get FailConnectGoToSettingsDescription, which is a translated string of
-     * "Try manually pairing to the device by going to Settings" based on locale.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getFailConnectGoToSettingsDescription() {
-        return mMetadataParcel.failConnectGoToSettingsDescription;
-    }
-
-    /**
-     * Builder used to create FastPairDeviceMetadata.
-     *
-     * @hide
-     */
-    public static final class Builder {
-
-        private final FastPairDeviceMetadataParcel mBuilderParcel;
-
-        /**
-         * Default constructor of Builder.
-         *
-         * @hide
-         */
-        public Builder() {
-            mBuilderParcel = new FastPairDeviceMetadataParcel();
-            mBuilderParcel.imageUrl = null;
-            mBuilderParcel.intentUri = null;
-            mBuilderParcel.name = null;
-            mBuilderParcel.bleTxPower = 0;
-            mBuilderParcel.triggerDistance = 0;
-            mBuilderParcel.image = null;
-            mBuilderParcel.deviceType = 0;  // DEVICE_TYPE_UNSPECIFIED
-            mBuilderParcel.trueWirelessImageUrlLeftBud = null;
-            mBuilderParcel.trueWirelessImageUrlRightBud = null;
-            mBuilderParcel.trueWirelessImageUrlCase = null;
-            mBuilderParcel.initialNotificationDescription = null;
-            mBuilderParcel.initialNotificationDescriptionNoAccount = null;
-            mBuilderParcel.openCompanionAppDescription = null;
-            mBuilderParcel.updateCompanionAppDescription = null;
-            mBuilderParcel.downloadCompanionAppDescription = null;
-            mBuilderParcel.unableToConnectTitle = null;
-            mBuilderParcel.unableToConnectDescription = null;
-            mBuilderParcel.initialPairingDescription = null;
-            mBuilderParcel.connectSuccessCompanionAppInstalled = null;
-            mBuilderParcel.connectSuccessCompanionAppNotInstalled = null;
-            mBuilderParcel.subsequentPairingDescription = null;
-            mBuilderParcel.retroactivePairingDescription = null;
-            mBuilderParcel.waitLaunchCompanionAppDescription = null;
-            mBuilderParcel.failConnectGoToSettingsDescription = null;
-        }
-
-        /**
-         * Set ImageUlr.
-         *
-         * @param imageUrl Image Ulr.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setImageUrl(@Nullable String imageUrl) {
-            mBuilderParcel.imageUrl = imageUrl;
-            return this;
-        }
-
-        /**
-         * Set IntentUri.
-         *
-         * @param intentUri Intent uri.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setIntentUri(@Nullable String intentUri) {
-            mBuilderParcel.intentUri = intentUri;
-            return this;
-        }
-
-        /**
-         * Set device name.
-         *
-         * @param name Device name.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setName(@Nullable String name) {
-            mBuilderParcel.name = name;
-            return this;
-        }
-
-        /**
-         * Set ble transmission power.
-         *
-         * @param bleTxPower Ble transmission power.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setBleTxPower(int bleTxPower) {
-            mBuilderParcel.bleTxPower = bleTxPower;
-            return this;
-        }
-
-        /**
-         * Set trigger distance.
-         *
-         * @param triggerDistance Fast Pair trigger distance.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setTriggerDistance(float triggerDistance) {
-            mBuilderParcel.triggerDistance = triggerDistance;
-            return this;
-        }
-
-        /**
-         * Set image.
-         *
-         * @param image Fast Pair device image, which is submitted at device registration time to
-         *              display on notification. It is a 32-bit PNG with dimensions of
-         *              512px by 512px.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setImage(@Nullable byte[] image) {
-            mBuilderParcel.image = image;
-            return this;
-        }
-
-        /**
-         * Set device type.
-         *
-         * @param deviceType Fast Pair device type.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setDeviceType(int deviceType) {
-            mBuilderParcel.deviceType = deviceType;
-            return this;
-        }
-
-        /**
-         * Set true wireless image url for left bud.
-         *
-         * @param trueWirelessImageUrlLeftBud True wireless image url for left bud.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setTrueWirelessImageUrlLeftBud(
-                @Nullable String trueWirelessImageUrlLeftBud) {
-            mBuilderParcel.trueWirelessImageUrlLeftBud = trueWirelessImageUrlLeftBud;
-            return this;
-        }
-
-        /**
-         * Set true wireless image url for right bud.
-         *
-         * @param trueWirelessImageUrlRightBud True wireless image url for right bud.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setTrueWirelessImageUrlRightBud(
-                @Nullable String trueWirelessImageUrlRightBud) {
-            mBuilderParcel.trueWirelessImageUrlRightBud = trueWirelessImageUrlRightBud;
-            return this;
-        }
-
-        /**
-         * Set true wireless image url for case.
-         *
-         * @param trueWirelessImageUrlCase True wireless image url for case.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setTrueWirelessImageUrlCase(@Nullable String trueWirelessImageUrlCase) {
-            mBuilderParcel.trueWirelessImageUrlCase = trueWirelessImageUrlCase;
-            return this;
-        }
-
-        /**
-         * Set InitialNotificationDescription.
-         *
-         * @param initialNotificationDescription Initial notification description.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setInitialNotificationDescription(
-                @Nullable String initialNotificationDescription) {
-            mBuilderParcel.initialNotificationDescription = initialNotificationDescription;
-            return this;
-        }
-
-        /**
-         * Set InitialNotificationDescriptionNoAccount.
-         *
-         * @param initialNotificationDescriptionNoAccount Initial notification description when
-         *                                                account is not present.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setInitialNotificationDescriptionNoAccount(
-                @Nullable String initialNotificationDescriptionNoAccount) {
-            mBuilderParcel.initialNotificationDescriptionNoAccount =
-                    initialNotificationDescriptionNoAccount;
-            return this;
-        }
-
-        /**
-         * Set OpenCompanionAppDescription.
-         *
-         * @param openCompanionAppDescription Description for opening companion app.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setOpenCompanionAppDescription(
-                @Nullable String openCompanionAppDescription) {
-            mBuilderParcel.openCompanionAppDescription = openCompanionAppDescription;
-            return this;
-        }
-
-        /**
-         * Set UpdateCompanionAppDescription.
-         *
-         * @param updateCompanionAppDescription Description for updating companion app.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setUpdateCompanionAppDescription(
-                @Nullable String updateCompanionAppDescription) {
-            mBuilderParcel.updateCompanionAppDescription = updateCompanionAppDescription;
-            return this;
-        }
-
-        /**
-         * Set DownloadCompanionAppDescription.
-         *
-         * @param downloadCompanionAppDescription Description for downloading companion app.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setDownloadCompanionAppDescription(
-                @Nullable String downloadCompanionAppDescription) {
-            mBuilderParcel.downloadCompanionAppDescription = downloadCompanionAppDescription;
-            return this;
-        }
-
-        /**
-         * Set UnableToConnectTitle.
-         *
-         * @param unableToConnectTitle Title when Fast Pair device is unable to be connected to.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setUnableToConnectTitle(@Nullable String unableToConnectTitle) {
-            mBuilderParcel.unableToConnectTitle = unableToConnectTitle;
-            return this;
-        }
-
-        /**
-         * Set UnableToConnectDescription.
-         *
-         * @param unableToConnectDescription Description when Fast Pair device is unable to be
-         *                                   connected to.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setUnableToConnectDescription(
-                @Nullable String unableToConnectDescription) {
-            mBuilderParcel.unableToConnectDescription = unableToConnectDescription;
-            return this;
-        }
-
-        /**
-         * Set InitialPairingDescription.
-         *
-         * @param initialPairingDescription Description for Fast Pair initial pairing.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setInitialPairingDescription(@Nullable String initialPairingDescription) {
-            mBuilderParcel.initialPairingDescription = initialPairingDescription;
-            return this;
-        }
-
-        /**
-         * Set ConnectSuccessCompanionAppInstalled.
-         *
-         * @param connectSuccessCompanionAppInstalled Description that let user open the companion
-         *                                            app.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setConnectSuccessCompanionAppInstalled(
-                @Nullable String connectSuccessCompanionAppInstalled) {
-            mBuilderParcel.connectSuccessCompanionAppInstalled =
-                    connectSuccessCompanionAppInstalled;
-            return this;
-        }
-
-        /**
-         * Set ConnectSuccessCompanionAppNotInstalled.
-         *
-         * @param connectSuccessCompanionAppNotInstalled Description that let user download the
-         *                                               companion app.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setConnectSuccessCompanionAppNotInstalled(
-                @Nullable String connectSuccessCompanionAppNotInstalled) {
-            mBuilderParcel.connectSuccessCompanionAppNotInstalled =
-                    connectSuccessCompanionAppNotInstalled;
-            return this;
-        }
-
-        /**
-         * Set SubsequentPairingDescription.
-         *
-         * @param subsequentPairingDescription Description that reminds user there is a paired
-         *                                     device nearby.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setSubsequentPairingDescription(
-                @Nullable String subsequentPairingDescription) {
-            mBuilderParcel.subsequentPairingDescription = subsequentPairingDescription;
-            return this;
-        }
-
-        /**
-         * Set RetroactivePairingDescription.
-         *
-         * @param retroactivePairingDescription Description that reminds users opt in their device.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setRetroactivePairingDescription(
-                @Nullable String retroactivePairingDescription) {
-            mBuilderParcel.retroactivePairingDescription = retroactivePairingDescription;
-            return this;
-        }
-
-        /**
-         * Set WaitLaunchCompanionAppDescription.
-         *
-         * @param waitLaunchCompanionAppDescription Description that indicates companion app is
-         *                                          about to launch.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setWaitLaunchCompanionAppDescription(
-                @Nullable String waitLaunchCompanionAppDescription) {
-            mBuilderParcel.waitLaunchCompanionAppDescription =
-                    waitLaunchCompanionAppDescription;
-            return this;
-        }
-
-        /**
-         * Set FailConnectGoToSettingsDescription.
-         *
-         * @param failConnectGoToSettingsDescription Description that indicates go to bluetooth
-         *                                           settings when connection fail.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setFailConnectGoToSettingsDescription(
-                @Nullable String failConnectGoToSettingsDescription) {
-            mBuilderParcel.failConnectGoToSettingsDescription =
-                    failConnectGoToSettingsDescription;
-            return this;
-        }
-
-        /**
-         * Build {@link FastPairDeviceMetadata} with the currently set configuration.
-         *
-         * @hide
-         */
-        @NonNull
-        public FastPairDeviceMetadata build() {
-            return new FastPairDeviceMetadata(mBuilderParcel);
-        }
-    }
-}
diff --git a/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java b/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java
deleted file mode 100644
index d8dfe29..0000000
--- a/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java
+++ /dev/null
@@ -1,529 +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 android.nearby;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.nearby.aidl.FastPairDiscoveryItemParcel;
-
-/**
- * Class for FastPairDiscoveryItem and its builder.
- *
- * @hide
- */
-public class FastPairDiscoveryItem {
-
-    FastPairDiscoveryItemParcel mMetadataParcel;
-
-    FastPairDiscoveryItem(
-            FastPairDiscoveryItemParcel metadataParcel) {
-        this.mMetadataParcel = metadataParcel;
-    }
-
-    /**
-     * Get Id.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getId() {
-        return mMetadataParcel.id;
-    }
-
-    /**
-     * Get MacAddress.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getMacAddress() {
-        return mMetadataParcel.macAddress;
-    }
-
-    /**
-     * Get ActionUrl.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getActionUrl() {
-        return mMetadataParcel.actionUrl;
-    }
-
-    /**
-     * Get DeviceName.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getDeviceName() {
-        return mMetadataParcel.deviceName;
-    }
-
-    /**
-     * Get Title.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getTitle() {
-        return mMetadataParcel.title;
-    }
-
-    /**
-     * Get Description.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getDescription() {
-        return mMetadataParcel.description;
-    }
-
-    /**
-     * Get DisplayUrl.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getDisplayUrl() {
-        return mMetadataParcel.displayUrl;
-    }
-
-    /**
-     * Get LastObservationTimestampMillis.
-     *
-     * @hide
-     */
-    public long getLastObservationTimestampMillis() {
-        return mMetadataParcel.lastObservationTimestampMillis;
-    }
-
-    /**
-     * Get FirstObservationTimestampMillis.
-     *
-     * @hide
-     */
-    public long getFirstObservationTimestampMillis() {
-        return mMetadataParcel.firstObservationTimestampMillis;
-    }
-
-    /**
-     * Get State.
-     *
-     * @hide
-     */
-    public int getState() {
-        return mMetadataParcel.state;
-    }
-
-    /**
-     * Get ActionUrlType.
-     *
-     * @hide
-     */
-    public int getActionUrlType() {
-        return mMetadataParcel.actionUrlType;
-    }
-
-    /**
-     * Get Rssi.
-     *
-     * @hide
-     */
-    public int getRssi() {
-        return mMetadataParcel.rssi;
-    }
-
-    /**
-     * Get PendingAppInstallTimestampMillis.
-     *
-     * @hide
-     */
-    public long getPendingAppInstallTimestampMillis() {
-        return mMetadataParcel.pendingAppInstallTimestampMillis;
-    }
-
-    /**
-     * Get TxPower.
-     *
-     * @hide
-     */
-    public int getTxPower() {
-        return mMetadataParcel.txPower;
-    }
-
-    /**
-     * Get AppName.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getAppName() {
-        return mMetadataParcel.appName;
-    }
-
-    /**
-     * Get PackageName.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getPackageName() {
-        return mMetadataParcel.packageName;
-    }
-
-    /**
-     * Get TriggerId.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getTriggerId() {
-        return mMetadataParcel.triggerId;
-    }
-
-    /**
-     * Get IconPng, which is submitted at device registration time to display on notification. It is
-     * a 32-bit PNG with dimensions of 512px by 512px.
-     *
-     * @return IconPng in 32-bit PNG with dimensions of 512px by 512px.
-     * @hide
-     */
-    @Nullable
-    public byte[] getIconPng() {
-        return mMetadataParcel.iconPng;
-    }
-
-    /**
-     * Get IconFifeUrl.
-     *
-     * @hide
-     */
-    @Nullable
-    public String getIconFfeUrl() {
-        return mMetadataParcel.iconFifeUrl;
-    }
-
-    /**
-     * Get authenticationPublicKeySecp256r1, which is same as AntiSpoof public key, see
-     * <a href="https://developers.google.com/nearby/fast-pair/spec#data_format">Data Format</a>.
-     *
-     * @return 64-byte authenticationPublicKeySecp256r1.
-     * @hide
-     */
-    @Nullable
-    public byte[] getAuthenticationPublicKeySecp256r1() {
-        return mMetadataParcel.authenticationPublicKeySecp256r1;
-    }
-
-    /**
-     * Builder used to create FastPairDiscoveryItem.
-     *
-     * @hide
-     */
-    public static final class Builder {
-
-        private final FastPairDiscoveryItemParcel mBuilderParcel;
-
-        /**
-         * Default constructor of Builder.
-         *
-         * @hide
-         */
-        public Builder() {
-            mBuilderParcel = new FastPairDiscoveryItemParcel();
-        }
-
-        /**
-         * Set Id.
-         *
-         * @param id Unique id.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         *
-         * @hide
-         */
-        @NonNull
-        public Builder setId(@Nullable String id) {
-            mBuilderParcel.id = id;
-            return this;
-        }
-
-        /**
-         * Set MacAddress.
-         *
-         * @param macAddress Fast Pair device rotating mac address.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setMacAddress(@Nullable String macAddress) {
-            mBuilderParcel.macAddress = macAddress;
-            return this;
-        }
-
-        /**
-         * Set ActionUrl.
-         *
-         * @param actionUrl Action Url of Fast Pair device.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setActionUrl(@Nullable String actionUrl) {
-            mBuilderParcel.actionUrl = actionUrl;
-            return this;
-        }
-
-        /**
-         * Set DeviceName.
-         * @param deviceName Fast Pair device name.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setDeviceName(@Nullable String deviceName) {
-            mBuilderParcel.deviceName = deviceName;
-            return this;
-        }
-
-        /**
-         * Set Title.
-         *
-         * @param title Title of Fast Pair device.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setTitle(@Nullable String title) {
-            mBuilderParcel.title = title;
-            return this;
-        }
-
-        /**
-         * Set Description.
-         *
-         * @param description Description of Fast Pair device.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setDescription(@Nullable String description) {
-            mBuilderParcel.description = description;
-            return this;
-        }
-
-        /**
-         * Set DisplayUrl.
-         *
-         * @param displayUrl Display Url of Fast Pair device.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setDisplayUrl(@Nullable String displayUrl) {
-            mBuilderParcel.displayUrl = displayUrl;
-            return this;
-        }
-
-        /**
-         * Set LastObservationTimestampMillis.
-         *
-         * @param lastObservationTimestampMillis Last observed timestamp of Fast Pair device, keyed
-         *                                       by a rotating id.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setLastObservationTimestampMillis(
-                long lastObservationTimestampMillis) {
-            mBuilderParcel.lastObservationTimestampMillis = lastObservationTimestampMillis;
-            return this;
-        }
-
-        /**
-         * Set FirstObservationTimestampMillis.
-         *
-         * @param firstObservationTimestampMillis First observed timestamp of Fast Pair device,
-         *                                        keyed by a rotating id.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setFirstObservationTimestampMillis(
-                long firstObservationTimestampMillis) {
-            mBuilderParcel.firstObservationTimestampMillis = firstObservationTimestampMillis;
-            return this;
-        }
-
-        /**
-         * Set State.
-         *
-         * @param state Item's current state. e.g. if the item is blocked.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setState(int state) {
-            mBuilderParcel.state = state;
-            return this;
-        }
-
-        /**
-         * Set ActionUrlType.
-         *
-         * @param actionUrlType The resolved url type for the action_url.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setActionUrlType(int actionUrlType) {
-            mBuilderParcel.actionUrlType = actionUrlType;
-            return this;
-        }
-
-        /**
-         * Set Rssi.
-         *
-         * @param rssi Beacon's RSSI value.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setRssi(int rssi) {
-            mBuilderParcel.rssi = rssi;
-            return this;
-        }
-
-        /**
-         * Set PendingAppInstallTimestampMillis.
-         *
-         * @param pendingAppInstallTimestampMillis The timestamp when the user is redirected to App
-         *                                         Store after clicking on the item.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setPendingAppInstallTimestampMillis(long pendingAppInstallTimestampMillis) {
-            mBuilderParcel.pendingAppInstallTimestampMillis = pendingAppInstallTimestampMillis;
-            return this;
-        }
-
-        /**
-         * Set TxPower.
-         *
-         * @param txPower Beacon's tx power.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setTxPower(int txPower) {
-            mBuilderParcel.txPower = txPower;
-            return this;
-        }
-
-        /**
-         * Set AppName.
-         *
-         * @param appName Human readable name of the app designated to open the uri.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setAppName(@Nullable String appName) {
-            mBuilderParcel.appName = appName;
-            return this;
-        }
-
-        /**
-         * Set PackageName.
-         *
-         * @param packageName Package name of the App that owns this item.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setPackageName(@Nullable String packageName) {
-            mBuilderParcel.packageName = packageName;
-            return this;
-        }
-
-        /**
-         * Set TriggerId.
-         *
-         * @param triggerId TriggerId identifies the trigger/beacon that is attached with a message.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setTriggerId(@Nullable String triggerId) {
-            mBuilderParcel.triggerId = triggerId;
-            return this;
-        }
-
-        /**
-         * Set IconPng.
-         *
-         * @param iconPng Bytes of item icon in PNG format displayed in Discovery item list.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setIconPng(@Nullable byte[] iconPng) {
-            mBuilderParcel.iconPng = iconPng;
-            return this;
-        }
-
-        /**
-         * Set IconFifeUrl.
-         *
-         * @param iconFifeUrl A FIFE URL of the item icon displayed in Discovery item list.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setIconFfeUrl(@Nullable String iconFifeUrl) {
-            mBuilderParcel.iconFifeUrl = iconFifeUrl;
-            return this;
-        }
-
-        /**
-         * Set authenticationPublicKeySecp256r1, which is same as AntiSpoof public key, see
-         * <a href="https://developers.google.com/nearby/fast-pair/spec#data_format">Data Format</a>
-         *
-         * @param authenticationPublicKeySecp256r1 64-byte Fast Pair device public key.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setAuthenticationPublicKeySecp256r1(
-                @Nullable byte[] authenticationPublicKeySecp256r1) {
-            mBuilderParcel.authenticationPublicKeySecp256r1 = authenticationPublicKeySecp256r1;
-            return this;
-        }
-
-        /**
-         * Build {@link FastPairDiscoveryItem} with the currently set configuration.
-         *
-         * @hide
-         */
-        @NonNull
-        public FastPairDiscoveryItem build() {
-            return new FastPairDiscoveryItem(mBuilderParcel);
-        }
-    }
-}
diff --git a/nearby/framework/java/android/nearby/FastPairEligibleAccount.java b/nearby/framework/java/android/nearby/FastPairEligibleAccount.java
deleted file mode 100644
index 8be4cca..0000000
--- a/nearby/framework/java/android/nearby/FastPairEligibleAccount.java
+++ /dev/null
@@ -1,112 +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 android.nearby;
-
-import android.accounts.Account;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.nearby.aidl.FastPairEligibleAccountParcel;
-
-/**
- * Class for FastPairEligibleAccount and its builder.
- *
- * @hide
- */
-public class FastPairEligibleAccount {
-
-    FastPairEligibleAccountParcel mAccountParcel;
-
-    FastPairEligibleAccount(FastPairEligibleAccountParcel accountParcel) {
-        this.mAccountParcel = accountParcel;
-    }
-
-    /**
-     * Get Account.
-     *
-     * @hide
-     */
-    @Nullable
-    public Account getAccount() {
-        return this.mAccountParcel.account;
-    }
-
-    /**
-     * Get OptIn Status.
-     *
-     * @hide
-     */
-    public boolean isOptIn() {
-        return this.mAccountParcel.optIn;
-    }
-
-    /**
-     * Builder used to create FastPairEligibleAccount.
-     *
-     * @hide
-     */
-    public static final class Builder {
-
-        private final FastPairEligibleAccountParcel mBuilderParcel;
-
-        /**
-         * Default constructor of Builder.
-         *
-         * @hide
-         */
-        public Builder() {
-            mBuilderParcel = new FastPairEligibleAccountParcel();
-            mBuilderParcel.account = null;
-            mBuilderParcel.optIn = false;
-        }
-
-        /**
-         * Set Account.
-         *
-         * @param account Fast Pair eligible account.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setAccount(@Nullable Account account) {
-            mBuilderParcel.account = account;
-            return this;
-        }
-
-        /**
-         * Set whether the account is opt into Fast Pair.
-         *
-         * @param optIn Whether the Fast Pair eligible account opts into Fast Pair.
-         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
-         * @hide
-         */
-        @NonNull
-        public Builder setOptIn(boolean optIn) {
-            mBuilderParcel.optIn = optIn;
-            return this;
-        }
-
-        /**
-         * Build {@link FastPairEligibleAccount} with the currently set configuration.
-         *
-         * @hide
-         */
-        @NonNull
-        public FastPairEligibleAccount build() {
-            return new FastPairEligibleAccount(mBuilderParcel);
-        }
-    }
-}
diff --git a/nearby/framework/java/android/nearby/FastPairStatusCallback.java b/nearby/framework/java/android/nearby/FastPairStatusCallback.java
deleted file mode 100644
index 1567828..0000000
--- a/nearby/framework/java/android/nearby/FastPairStatusCallback.java
+++ /dev/null
@@ -1,30 +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.nearby;
-
-import android.annotation.NonNull;
-
-/**
- * Reports the pair status for an ongoing pair with a {@link FastPairDevice}.
- * @hide
- */
-public interface FastPairStatusCallback {
-
-    /** Reports a pair status related metadata associated with a {@link FastPairDevice} */
-    void onPairUpdate(@NonNull FastPairDevice fastPairDevice,
-            PairStatusMetadata pairStatusMetadata);
-}
diff --git a/nearby/framework/java/android/nearby/IFastPairHalfSheetCallback.aidl b/nearby/framework/java/android/nearby/IFastPairHalfSheetCallback.aidl
deleted file mode 100644
index 2e6fc87..0000000
--- a/nearby/framework/java/android/nearby/IFastPairHalfSheetCallback.aidl
+++ /dev/null
@@ -1,25 +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 android.nearby;
-
-import android.content.Intent;
-/**
-  * Provides callback interface for halfsheet to send FastPair call back.
-  *
-  * {@hide}
-  */
-interface IFastPairHalfSheetCallback {
-     void onHalfSheetConnectionConfirm(in Intent intent);
- }
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 7af271e..21ae0ac 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -20,6 +20,7 @@
 import android.nearby.IScanListener;
 import android.nearby.BroadcastRequestParcelable;
 import android.nearby.ScanRequest;
+import android.nearby.PoweredOffFindingEphemeralId;
 import android.nearby.aidl.IOffloadCallback;
 
 /**
@@ -40,4 +41,10 @@
     void stopBroadcast(in IBroadcastListener callback, String packageName, @nullable String attributionTag);
 
     void queryOffloadCapability(in IOffloadCallback callback) ;
-}
\ No newline at end of file
+
+    void setPoweredOffFindingEphemeralIds(in List<PoweredOffFindingEphemeralId> eids);
+
+    void setPoweredOffModeEnabled(boolean enabled);
+
+    boolean getPoweredOffModeEnabled();
+}
diff --git a/nearby/framework/java/android/nearby/NearbyDevice.java b/nearby/framework/java/android/nearby/NearbyDevice.java
index e8fcc28..e7db0c5 100644
--- a/nearby/framework/java/android/nearby/NearbyDevice.java
+++ b/nearby/framework/java/android/nearby/NearbyDevice.java
@@ -25,6 +25,8 @@
 
 import com.android.internal.util.Preconditions;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -149,6 +151,7 @@
      * @hide
      */
     @IntDef({Medium.BLE, Medium.BLUETOOTH})
+    @Retention(RetentionPolicy.SOURCE)
     public @interface Medium {
         int BLE = 1;
         int BLUETOOTH = 2;
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index a70b303..cae653d 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -18,6 +18,7 @@
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -25,16 +26,22 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.bluetooth.BluetoothManager;
 import android.content.Context;
+import android.location.LocationManager;
 import android.nearby.aidl.IOffloadCallback;
 import android.os.RemoteException;
+import android.os.SystemProperties;
 import android.provider.Settings;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
+import java.util.List;
 import java.util.Objects;
 import java.util.WeakHashMap;
 import java.util.concurrent.Executor;
@@ -63,6 +70,7 @@
             ScanStatus.SUCCESS,
             ScanStatus.ERROR,
     })
+    @Retention(RetentionPolicy.SOURCE)
     public @interface ScanStatus {
         // The undetermined status, some modules may be initializing. Retry is suggested.
         int UNKNOWN = 0;
@@ -72,8 +80,51 @@
         int ERROR = 2;
     }
 
+    /**
+     * Return value of {@link #getPoweredOffFindingMode()} when this powered off finding is not
+     * supported the device.
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0;
+
+    /**
+     * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link
+     * #setPoweredOffFindingMode(int)} when powered off finding is supported but disabled. The
+     * device will not start to advertise when powered off.
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1;
+
+    /**
+     * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link
+     * #setPoweredOffFindingMode(int)} when powered off finding is enabled. The device will start to
+     * advertise when powered off.
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2;
+
+    /**
+     * Powered off finding modes.
+     *
+     * @hide
+     */
+    @IntDef(
+            prefix = {"POWERED_OFF_FINDING_MODE"},
+            value = {
+                    POWERED_OFF_FINDING_MODE_UNSUPPORTED,
+                    POWERED_OFF_FINDING_MODE_DISABLED,
+                    POWERED_OFF_FINDING_MODE_ENABLED,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PoweredOffFindingMode {}
+
     private static final String TAG = "NearbyManager";
 
+    private static final int POWERED_OFF_FINDING_EID_LENGTH = 20;
+
+    private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY =
+            "ro.bluetooth.finder.supported";
+
     /**
      * TODO(b/286137024): Remove this when CTS R5 is rolled out.
      * Whether allows Fast Pair to scan.
@@ -281,6 +332,8 @@
      */
     public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<OffloadCapability> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
         try {
             mService.queryOffloadCapability(new OffloadTransport(executor, callback));
         } catch (RemoteException e) {
@@ -451,4 +504,124 @@
                 "successfully %s Fast Pair scan", enable ? "enables" : "disables"));
     }
 
+    /**
+     * Sets the precomputed EIDs for advertising when the phone is powered off. The Bluetooth
+     * controller will store these EIDs in its memory, and will start advertising them in Find My
+     * Device network EID frames when powered off, only if the powered off finding mode was
+     * previously enabled by calling {@link #setPoweredOffFindingMode(int)}.
+     *
+     * <p>The EIDs are cryptographic ephemeral identifiers that change periodically, based on the
+     * Android clock at the time of the shutdown. They are used as the public part of asymmetric key
+     * pairs. Members of the Find My Device network can use them to encrypt the location of where
+     * they sight the advertising device. Only someone in possession of the private key (the device
+     * owner or someone that the device owner shared the key with) can decrypt this encrypted
+     * location.
+     *
+     * <p>Android will typically call this method during the shutdown process. Even after the
+     * method was called, it is still possible to call {#link setPoweredOffFindingMode() to disable
+     * the advertisement, for example to temporarily disable it for a single shutdown.
+     *
+     * <p>If called more than once, the EIDs of the most recent call overrides the EIDs from any
+     * previous call.
+     *
+     * @throws IllegalArgumentException if the length of one of the EIDs is not 20 bytes
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+    public void setPoweredOffFindingEphemeralIds(@NonNull List<byte[]> eids) {
+        Objects.requireNonNull(eids);
+        if (!isPoweredOffFindingSupported()) {
+            throw new UnsupportedOperationException(
+                    "Powered off finding is not supported on this device");
+        }
+        List<PoweredOffFindingEphemeralId> ephemeralIdList = eids.stream().map(
+                eid -> {
+                    Preconditions.checkArgument(eid.length == POWERED_OFF_FINDING_EID_LENGTH);
+                    PoweredOffFindingEphemeralId ephemeralId = new PoweredOffFindingEphemeralId();
+                    ephemeralId.bytes = eid;
+                    return ephemeralId;
+                }).toList();
+        try {
+            mService.setPoweredOffFindingEphemeralIds(ephemeralIdList);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+    }
+
+    /**
+     * Turns the powered off finding on or off. Power off finding will operate only if this method
+     * was called at least once since boot, and the value of the argument {@code
+     * poweredOffFindinMode} was {@link #POWERED_OFF_FINDING_MODE_ENABLED} the last time the method
+     * was called.
+     *
+     * <p>When an Android device with the powered off finding feature is turned off (either as part
+     * of a normal shutdown or due to dead battery), its Bluetooth chip starts to advertise Find My
+     * Device network EID frames with the EID payload that were provided by the last call to {@link
+     * #setPoweredOffFindingEphemeralIds(List)}. These EIDs can be sighted by other Android devices
+     * in BLE range that are part of the Find My Device network. The Android sighters use the EID to
+     * encrypt the location of the Android device and upload it to the server, in a way that only
+     * the owner of the advertising device, or people that the owner shared their encryption key
+     * with, can decrypt the location.
+     *
+     * @param poweredOffFindingMode {@link #POWERED_OFF_FINDING_MODE_ENABLED} or {@link
+     * #POWERED_OFF_FINDING_MODE_DISABLED}
+     *
+     * @throws IllegalStateException if called with {@link #POWERED_OFF_FINDING_MODE_ENABLED} when
+     * Bluetooth or location services are disabled
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+    public void setPoweredOffFindingMode(@PoweredOffFindingMode int poweredOffFindingMode) {
+        Preconditions.checkArgument(
+                poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED
+                        || poweredOffFindingMode == POWERED_OFF_FINDING_MODE_DISABLED,
+                "invalid poweredOffFindingMode");
+        if (!isPoweredOffFindingSupported()) {
+            throw new UnsupportedOperationException(
+                    "Powered off finding is not supported on this device");
+        }
+        if (poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED) {
+            Preconditions.checkState(areLocationAndBluetoothEnabled(),
+                    "Location services and Bluetooth must be on");
+        }
+        try {
+            mService.setPoweredOffModeEnabled(
+                    poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the state of the powered off finding feature.
+     *
+     * <p>{@link #POWERED_OFF_FINDING_MODE_UNSUPPORTED} if the feature is not supported by the
+     * device, {@link #POWERED_OFF_FINDING_MODE_DISABLED} if this was the last value set by {@link
+     * #setPoweredOffFindingMode(int)} or if no value was set since boot, {@link
+     * #POWERED_OFF_FINDING_MODE_ENABLED} if this was the last value set by {@link
+     * #setPoweredOffFindingMode(int)}
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+    public @PoweredOffFindingMode int getPoweredOffFindingMode() {
+        if (!isPoweredOffFindingSupported()) {
+            return POWERED_OFF_FINDING_MODE_UNSUPPORTED;
+        }
+        try {
+            return mService.getPoweredOffModeEnabled()
+                    ? POWERED_OFF_FINDING_MODE_ENABLED : POWERED_OFF_FINDING_MODE_DISABLED;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private boolean isPoweredOffFindingSupported() {
+        return Boolean.parseBoolean(SystemProperties.get(POWER_OFF_FINDING_SUPPORTED_PROPERTY));
+    }
+
+    private boolean areLocationAndBluetoothEnabled() {
+        return mContext.getSystemService(BluetoothManager.class).getAdapter().isEnabled()
+                && mContext.getSystemService(LocationManager.class).isLocationEnabled();
+    }
 }
diff --git a/nearby/framework/java/android/nearby/PairStatusMetadata.aidl b/nearby/framework/java/android/nearby/PairStatusMetadata.aidl
deleted file mode 100644
index 911a300..0000000
--- a/nearby/framework/java/android/nearby/PairStatusMetadata.aidl
+++ /dev/null
@@ -1,24 +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.nearby;
-
-/**
- * Metadata about an ongoing paring. Wraps transient data like status and progress.
- *
- * @hide
- */
-parcelable PairStatusMetadata;
diff --git a/nearby/framework/java/android/nearby/PairStatusMetadata.java b/nearby/framework/java/android/nearby/PairStatusMetadata.java
deleted file mode 100644
index 438cd6b..0000000
--- a/nearby/framework/java/android/nearby/PairStatusMetadata.java
+++ /dev/null
@@ -1,117 +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.nearby;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import java.util.Objects;
-
-/**
- * Metadata about an ongoing paring. Wraps transient data like status and progress.
- *
- * @hide
- */
-public final class PairStatusMetadata implements Parcelable {
-
-    @Status
-    private final int mStatus;
-
-    /** The status of the pairing. */
-    @IntDef({
-            Status.UNKNOWN,
-            Status.SUCCESS,
-            Status.FAIL,
-            Status.DISMISS
-    })
-    public @interface Status {
-        int UNKNOWN = 1000;
-        int SUCCESS = 1001;
-        int FAIL = 1002;
-        int DISMISS = 1003;
-    }
-
-    /** Converts the status to readable string. */
-    public static String statusToString(@Status int status) {
-        switch (status) {
-            case Status.SUCCESS:
-                return "SUCCESS";
-            case Status.FAIL:
-                return "FAIL";
-            case Status.DISMISS:
-                return "DISMISS";
-            case Status.UNKNOWN:
-            default:
-                return "UNKNOWN";
-        }
-    }
-
-    public int getStatus() {
-        return mStatus;
-    }
-
-    @Override
-    public String toString() {
-        return "PairStatusMetadata[ status=" + statusToString(mStatus) + "]";
-    }
-
-    @Override
-    public boolean equals(Object other) {
-        if (other instanceof PairStatusMetadata) {
-            return mStatus == ((PairStatusMetadata) other).mStatus;
-        }
-        return false;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(mStatus);
-    }
-
-    public PairStatusMetadata(@Status int status) {
-        mStatus = status;
-    }
-
-    public static final Creator<PairStatusMetadata> CREATOR = new Creator<PairStatusMetadata>() {
-        @Override
-        public PairStatusMetadata createFromParcel(Parcel in) {
-            return new PairStatusMetadata(in.readInt());
-        }
-
-        @Override
-        public PairStatusMetadata[] newArray(int size) {
-            return new PairStatusMetadata[size];
-        }
-    };
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public int getStability() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeInt(mStatus);
-    }
-}
diff --git a/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl b/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl
new file mode 100644
index 0000000..9f4bfef
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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.nearby;
+
+/**
+ * Find My Device network ephemeral ID for powered off finding.
+ *
+ * @hide
+ */
+parcelable PoweredOffFindingEphemeralId {
+    byte[20] bytes;
+}
diff --git a/nearby/framework/java/android/nearby/aidl/ByteArrayParcel.aidl b/nearby/framework/java/android/nearby/aidl/ByteArrayParcel.aidl
deleted file mode 100644
index 53c73bd..0000000
--- a/nearby/framework/java/android/nearby/aidl/ByteArrayParcel.aidl
+++ /dev/null
@@ -1,25 +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 android.nearby.aidl;
-
-/**
- * This is to support 2D byte arrays.
- * {@hide}
- */
-parcelable ByteArrayParcel {
-    byte[] byteArray;
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairAccountDevicesMetadataRequestParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairAccountDevicesMetadataRequestParcel.aidl
deleted file mode 100644
index fc3ba22..0000000
--- a/nearby/framework/java/android/nearby/aidl/FastPairAccountDevicesMetadataRequestParcel.aidl
+++ /dev/null
@@ -1,29 +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 android.nearby.aidl;
-
-import android.accounts.Account;
-import android.nearby.aidl.ByteArrayParcel;
-
-/**
- * Request details for Metadata of Fast Pair devices associated with an account.
- * {@hide}
- */
-parcelable FastPairAccountDevicesMetadataRequestParcel {
-    Account account;
-    ByteArrayParcel[] deviceAccountKeys;
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairAccountKeyDeviceMetadataParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairAccountKeyDeviceMetadataParcel.aidl
deleted file mode 100644
index 8014323..0000000
--- a/nearby/framework/java/android/nearby/aidl/FastPairAccountKeyDeviceMetadataParcel.aidl
+++ /dev/null
@@ -1,35 +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 android.nearby.aidl;
-
-import android.nearby.aidl.FastPairDeviceMetadataParcel;
-import android.nearby.aidl.FastPairDiscoveryItemParcel;
-
-/**
- * Metadata of a Fast Pair device associated with an account.
- * {@hide}
- */
- // TODO(b/204780849): remove unnecessary fields and polish comments.
-parcelable FastPairAccountKeyDeviceMetadataParcel {
-    // Key of the Fast Pair device associated with the account.
-    byte[] deviceAccountKey;
-    // Hash function of device account key and public bluetooth address.
-    byte[] sha256DeviceAccountKeyPublicAddress;
-    // Fast Pair device metadata for the Fast Pair device.
-    FastPairDeviceMetadataParcel metadata;
-    // Fast Pair discovery item tied to both the Fast Pair device and the
-    // account.
-    FastPairDiscoveryItemParcel discoveryItem;
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataParcel.aidl
deleted file mode 100644
index 4fd4d4b..0000000
--- a/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataParcel.aidl
+++ /dev/null
@@ -1,31 +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 android.nearby.aidl;
-
-import android.nearby.aidl.FastPairDeviceMetadataParcel;
-
-/**
- * Metadata of a Fast Pair device keyed by AntispoofKey,
- * Used by initial pairing without account association.
- *
- * {@hide}
- */
-parcelable FastPairAntispoofKeyDeviceMetadataParcel {
-    // Anti-spoof public key.
-    byte[] antispoofPublicKey;
-
-    // Fast Pair device metadata for the Fast Pair device.
-    FastPairDeviceMetadataParcel deviceMetadata;
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataRequestParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataRequestParcel.aidl
deleted file mode 100644
index afdcf15..0000000
--- a/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataRequestParcel.aidl
+++ /dev/null
@@ -1,26 +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 android.nearby.aidl;
-
-/**
- * Request details for metadata of a Fast Pair device keyed by either
- * antispoofKey or modelId.
- * {@hide}
- */
-parcelable FastPairAntispoofKeyDeviceMetadataRequestParcel {
-    byte[] modelId;
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairDeviceMetadataParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairDeviceMetadataParcel.aidl
deleted file mode 100644
index d90f6a1..0000000
--- a/nearby/framework/java/android/nearby/aidl/FastPairDeviceMetadataParcel.aidl
+++ /dev/null
@@ -1,99 +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 android.nearby.aidl;
-
-/**
- * Fast Pair Device Metadata for a given device model ID.
- * @hide
- */
-// TODO(b/204780849): remove unnecessary fields and polish comments.
-parcelable FastPairDeviceMetadataParcel {
-    // The image to show on the notification.
-    String imageUrl;
-
-    // The intent that will be launched via the notification.
-    String intentUri;
-
-    // The transmit power of the device's BLE chip.
-    int bleTxPower;
-
-    // The distance that the device must be within to show a notification.
-    // If no distance is set, we default to 0.6 meters. Only Nearby admins can
-    // change this.
-    float triggerDistance;
-
-    // The image icon that shows in the notification.
-    byte[] image;
-
-    // The name of the device.
-    String name;
-
-    int deviceType;
-
-    // The image urls for device with device type "true wireless".
-    String trueWirelessImageUrlLeftBud;
-    String trueWirelessImageUrlRightBud;
-    String trueWirelessImageUrlCase;
-
-    // The notification description for when the device is initially discovered.
-    String initialNotificationDescription;
-
-    // The notification description for when the device is initially discovered
-    // and no account is logged in.
-    String initialNotificationDescriptionNoAccount;
-
-    // The notification description for once we have finished pairing and the
-    // companion app has been opened. For Bisto devices, this String will point
-    // users to setting up the assistant.
-    String openCompanionAppDescription;
-
-    // The notification description for once we have finished pairing and the
-    // companion app needs to be updated before use.
-    String updateCompanionAppDescription;
-
-    // The notification description for once we have finished pairing and the
-    // companion app needs to be installed.
-    String downloadCompanionAppDescription;
-
-    // The notification title when a pairing fails.
-    String unableToConnectTitle;
-
-    // The notification summary when a pairing fails.
-    String unableToConnectDescription;
-
-    // The description that helps user initially paired with device.
-    String initialPairingDescription;
-
-    // The description that let user open the companion app.
-    String connectSuccessCompanionAppInstalled;
-
-    // The description that let user download the companion app.
-    String connectSuccessCompanionAppNotInstalled;
-
-    // The description that reminds user there is a paired device nearby.
-    String subsequentPairingDescription;
-
-    // The description that reminds users opt in their device.
-    String retroactivePairingDescription;
-
-    // The description that indicates companion app is about to launch.
-    String waitLaunchCompanionAppDescription;
-
-    // The description that indicates go to bluetooth settings when connection
-    // fail.
-    String failConnectGoToSettingsDescription;
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairDiscoveryItemParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairDiscoveryItemParcel.aidl
deleted file mode 100644
index 2cc2daa..0000000
--- a/nearby/framework/java/android/nearby/aidl/FastPairDiscoveryItemParcel.aidl
+++ /dev/null
@@ -1,93 +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 android.nearby.aidl;
-
-/**
- * Fast Pair Discovery Item.
- * @hide
- */
-// TODO(b/204780849): remove unnecessary fields and polish comments.
-parcelable FastPairDiscoveryItemParcel {
-  // Offline item: unique ID generated on client.
-  // Online item: unique ID generated on server.
-  String id;
-
-  // The most recent all upper case mac associated with this item.
-  // (Mac-to-DiscoveryItem is a many-to-many relationship)
-  String macAddress;
-
-  String actionUrl;
-
-  // The bluetooth device name from advertisement
-  String deviceName;
-
-  // Item's title
-  String title;
-
-  // Item's description.
-  String description;
-
-  // The URL for display
-  String displayUrl;
-
-  // Client timestamp when the beacon was last observed in BLE scan.
-  long lastObservationTimestampMillis;
-
-  // Client timestamp when the beacon was first observed in BLE scan.
-  long firstObservationTimestampMillis;
-
-  // Item's current state. e.g. if the item is blocked.
-  int state;
-
-  // The resolved url type for the action_url.
-  int actionUrlType;
-
-  // The timestamp when the user is redirected to Play Store after clicking on
-  // the item.
-  long pendingAppInstallTimestampMillis;
-
-  // Beacon's RSSI value
-  int rssi;
-
-  // Beacon's tx power
-  int txPower;
-
-  // Human readable name of the app designated to open the uri
-  // Used in the second line of the notification, "Open in {} app"
-  String appName;
-
-  // Package name of the App that owns this item.
-  String packageName;
-
-  // TriggerId identifies the trigger/beacon that is attached with a message.
-  // It's generated from server for online messages to synchronize formatting
-  // across client versions.
-  // Example:
-  // * BLE_UID: 3||deadbeef
-  // * BLE_URL: http://trigger.id
-  // See go/discovery-store-message-and-trigger-id for more details.
-  String triggerId;
-
-  // Bytes of item icon in PNG format displayed in Discovery item list.
-  byte[] iconPng;
-
-  // A FIFE URL of the item icon displayed in Discovery item list.
-  String iconFifeUrl;
-
-  // Fast Pair antispoof key.
-  byte[] authenticationPublicKeySecp256r1;
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountParcel.aidl
deleted file mode 100644
index 747758d..0000000
--- a/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountParcel.aidl
+++ /dev/null
@@ -1,29 +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 android.nearby.aidl;
-
-import android.accounts.Account;
-
-/**
- * Fast Pair Eligible Account.
- * {@hide}
- */
-parcelable FastPairEligibleAccountParcel {
-    Account account;
-    // Whether the account opts in Fast Pair.
-    boolean optIn;
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountsRequestParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountsRequestParcel.aidl
deleted file mode 100644
index 8db3356..0000000
--- a/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountsRequestParcel.aidl
+++ /dev/null
@@ -1,25 +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 android.nearby.aidl;
-
-/**
- * Request details for Fast Pair eligible accounts.
- * Empty place holder for future expansion.
- * {@hide}
- */
-parcelable FastPairEligibleAccountsRequestParcel {
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairManageAccountDeviceRequestParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairManageAccountDeviceRequestParcel.aidl
deleted file mode 100644
index 59834b2..0000000
--- a/nearby/framework/java/android/nearby/aidl/FastPairManageAccountDeviceRequestParcel.aidl
+++ /dev/null
@@ -1,34 +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 android.nearby.aidl;
-
-import android.accounts.Account;
-import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
-
-/**
- * Request details for managing Fast Pair device-account mapping.
- * {@hide}
- */
- // TODO(b/204780849): remove unnecessary fields and polish comments.
-parcelable FastPairManageAccountDeviceRequestParcel {
-    Account account;
-    // MANAGE_ACCOUNT_DEVICE_ADD: add Fast Pair device to the account.
-    // MANAGE_ACCOUNT_DEVICE_REMOVE: remove Fast Pair device from the account.
-    int requestType;
-    // Fast Pair account key-ed device metadata.
-    FastPairAccountKeyDeviceMetadataParcel accountKeyDeviceMetadata;
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairManageAccountRequestParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairManageAccountRequestParcel.aidl
deleted file mode 100644
index 3d92064..0000000
--- a/nearby/framework/java/android/nearby/aidl/FastPairManageAccountRequestParcel.aidl
+++ /dev/null
@@ -1,31 +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 android.nearby.aidl;
-
-import android.accounts.Account;
-
-/**
- * Request details for managing a Fast Pair account.
- *
- * {@hide}
- */
-parcelable FastPairManageAccountRequestParcel {
-    Account account;
-    // MANAGE_ACCOUNT_OPT_IN: opt account into Fast Pair.
-    // MANAGE_ACCOUNT_OPT_OUT: opt account out of Fast Pair.
-    int requestType;
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairAccountDevicesMetadataCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairAccountDevicesMetadataCallback.aidl
deleted file mode 100644
index 7db18d0..0000000
--- a/nearby/framework/java/android/nearby/aidl/IFastPairAccountDevicesMetadataCallback.aidl
+++ /dev/null
@@ -1,28 +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 android.nearby.aidl;
-
-import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
-
-/**
-  * Provides callback interface for OEMs to send back metadata of FastPair
-  * devices associated with an account.
-  *
-  * {@hide}
-  */
-interface IFastPairAccountDevicesMetadataCallback {
-     void onFastPairAccountDevicesMetadataReceived(in FastPairAccountKeyDeviceMetadataParcel[] accountDevicesMetadata);
-     void onError(int code, String message);
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairAntispoofKeyDeviceMetadataCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairAntispoofKeyDeviceMetadataCallback.aidl
deleted file mode 100644
index 38abba4..0000000
--- a/nearby/framework/java/android/nearby/aidl/IFastPairAntispoofKeyDeviceMetadataCallback.aidl
+++ /dev/null
@@ -1,27 +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 android.nearby.aidl;
-
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
-
-/**
-  * Provides callback interface for OEMs to send FastPair AntispoofKey Device metadata back.
-  *
-  * {@hide}
-  */
-interface IFastPairAntispoofKeyDeviceMetadataCallback {
-     void onFastPairAntispoofKeyDeviceMetadataReceived(in FastPairAntispoofKeyDeviceMetadataParcel metadata);
-     void onError(int code, String message);
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairDataProvider.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairDataProvider.aidl
deleted file mode 100644
index 2956211..0000000
--- a/nearby/framework/java/android/nearby/aidl/IFastPairDataProvider.aidl
+++ /dev/null
@@ -1,44 +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 android.nearby.aidl;
-
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
-import android.nearby.aidl.IFastPairAntispoofKeyDeviceMetadataCallback;
-import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
-import android.nearby.aidl.IFastPairAccountDevicesMetadataCallback;
-import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
-import android.nearby.aidl.IFastPairEligibleAccountsCallback;
-import android.nearby.aidl.FastPairManageAccountRequestParcel;
-import android.nearby.aidl.IFastPairManageAccountCallback;
-import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
-import android.nearby.aidl.IFastPairManageAccountDeviceCallback;
-
-/**
- * Interface for communicating with the fast pair providers.
- *
- * {@hide}
- */
-oneway interface IFastPairDataProvider {
-    void loadFastPairAntispoofKeyDeviceMetadata(in FastPairAntispoofKeyDeviceMetadataRequestParcel request,
-        in IFastPairAntispoofKeyDeviceMetadataCallback callback);
-    void loadFastPairAccountDevicesMetadata(in FastPairAccountDevicesMetadataRequestParcel request,
-        in IFastPairAccountDevicesMetadataCallback callback);
-    void loadFastPairEligibleAccounts(in FastPairEligibleAccountsRequestParcel request,
-        in IFastPairEligibleAccountsCallback callback);
-    void manageFastPairAccount(in FastPairManageAccountRequestParcel request,
-        in IFastPairManageAccountCallback callback);
-    void manageFastPairAccountDevice(in FastPairManageAccountDeviceRequestParcel request,
-        in IFastPairManageAccountDeviceCallback callback);
-}
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairEligibleAccountsCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairEligibleAccountsCallback.aidl
deleted file mode 100644
index 9990014..0000000
--- a/nearby/framework/java/android/nearby/aidl/IFastPairEligibleAccountsCallback.aidl
+++ /dev/null
@@ -1,28 +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 android.nearby.aidl;
-
-import android.accounts.Account;
-import android.nearby.aidl.FastPairEligibleAccountParcel;
-
-/**
-  * Provides callback interface for OEMs to return FastPair Eligible accounts.
-  *
-  * {@hide}
-  */
-interface IFastPairEligibleAccountsCallback {
-     void onFastPairEligibleAccountsReceived(in FastPairEligibleAccountParcel[] accounts);
-     void onError(int code, String message);
- }
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountCallback.aidl
deleted file mode 100644
index 6b4aaee..0000000
--- a/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountCallback.aidl
+++ /dev/null
@@ -1,25 +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 android.nearby.aidl;
-
-/**
-  * Provides callback interface to send response for account management request.
-  *
-  * {@hide}
-  */
-interface IFastPairManageAccountCallback {
-     void onSuccess();
-     void onError(int code, String message);
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountDeviceCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountDeviceCallback.aidl
deleted file mode 100644
index bffc533..0000000
--- a/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountDeviceCallback.aidl
+++ /dev/null
@@ -1,26 +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 android.nearby.aidl;
-
-/**
-  * Provides callback interface to send response for account-device mapping
-  * management request.
-  *
-  * {@hide}
-  */
-interface IFastPairManageAccountDeviceCallback {
-     void onSuccess();
-     void onError(int code, String message);
-}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairStatusCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairStatusCallback.aidl
deleted file mode 100644
index d844c06..0000000
--- a/nearby/framework/java/android/nearby/aidl/IFastPairStatusCallback.aidl
+++ /dev/null
@@ -1,32 +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.nearby.aidl;
-
-import android.nearby.FastPairDevice;
-import android.nearby.PairStatusMetadata;
-
-/**
- *
- * Provides callbacks for Fast Pair foreground activity to learn about paring status from backend.
- *
- * {@hide}
- */
-interface IFastPairStatusCallback {
-
-    /** Reports a pair status related metadata associated with a {@link FastPairDevice} */
-    void onPairUpdate(in FastPairDevice fastPairDevice, in PairStatusMetadata pairStatusMetadata);
-}
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairUiService.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairUiService.aidl
deleted file mode 100644
index 9200a9d..0000000
--- a/nearby/framework/java/android/nearby/aidl/IFastPairUiService.aidl
+++ /dev/null
@@ -1,37 +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.nearby.aidl;
-
-import android.nearby.aidl.IFastPairStatusCallback;
-import android.nearby.FastPairDevice;
-
-/**
- * 0p API for controlling Fast Pair. Used to talk between foreground activities
- * and background services.
- *
- * {@hide}
- */
-interface IFastPairUiService {
-
-    void registerCallback(in IFastPairStatusCallback fastPairStatusCallback);
-
-    void unregisterCallback(in IFastPairStatusCallback fastPairStatusCallback);
-
-    void connect(in FastPairDevice fastPairDevice);
-
-    void cancel(in FastPairDevice fastPairDevice);
-}
\ No newline at end of file
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/halfsheet/Android.bp b/nearby/halfsheet/Android.bp
deleted file mode 100644
index 6190d45..0000000
--- a/nearby/halfsheet/Android.bp
+++ /dev/null
@@ -1,56 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_app {
-    name: "HalfSheetUX",
-    defaults: ["platform_app_defaults"],
-    srcs: ["src/**/*.java"],
-    resource_dirs: ["res"],
-    sdk_version: "module_current",
-    // This is included in tethering apex, which uses min SDK 30
-    min_sdk_version: "30",
-    updatable: true,
-    certificate: ":com.android.nearby.halfsheetcertificate",
-    libs: [
-        "framework-bluetooth",
-        "framework-connectivity-t.impl",
-        "framework-nearby-static",
-    ],
-    static_libs: [
-        "androidx.annotation_annotation",
-        "androidx.fragment_fragment",
-        "androidx-constraintlayout_constraintlayout",
-        "androidx.localbroadcastmanager_localbroadcastmanager",
-        "androidx.core_core",
-        "androidx.appcompat_appcompat",
-        "androidx.recyclerview_recyclerview",
-        "androidx.lifecycle_lifecycle-runtime",
-        "androidx.lifecycle_lifecycle-extensions",
-        "com.google.android.material_material",
-        "fast-pair-lite-protos",
-    ],
-    manifest: "AndroidManifest.xml",
-    jarjar_rules: ":nearby-jarjar-rules",
-    apex_available: ["com.android.tethering",],
-    lint: { strict_updatability_linting: true }
-}
-
-android_app_certificate {
-    name: "com.android.nearby.halfsheetcertificate",
-    certificate: "apk-certs/com.android.nearby.halfsheet"
-}
diff --git a/nearby/halfsheet/AndroidManifest.xml b/nearby/halfsheet/AndroidManifest.xml
deleted file mode 100644
index 22987fb..0000000
--- a/nearby/halfsheet/AndroidManifest.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.nearby.halfsheet">
-    <application>
-        <activity
-            android:name="com.android.nearby.halfsheet.HalfSheetActivity"
-            android:exported="true"
-            android:launchMode="singleInstance"
-            android:theme="@style/HalfSheetStyle" >
-            <intent-filter>
-                <action android:name="android.nearby.SHOW_HALFSHEET"/>
-                <category android:name="android.intent.category.DEFAULT"/>
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
diff --git a/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.pk8 b/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.pk8
deleted file mode 100644
index 187d51e..0000000
--- a/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.pk8
+++ /dev/null
Binary files differ
diff --git a/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.x509.pem b/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.x509.pem
deleted file mode 100644
index 440c524..0000000
--- a/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.x509.pem
+++ /dev/null
@@ -1,34 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIF6zCCA9OgAwIBAgIUU5ATKevcNA5ZSurwgwGenwrr4c4wDQYJKoZIhvcNAQEL
-BQAwgYMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMQwwCgYDVQQH
-DANNVFYxDzANBgNVBAoMBkdvb2dsZTEPMA0GA1UECwwGbmVhcmJ5MQswCQYDVQQD
-DAJ3czEiMCAGCSqGSIb3DQEJARYTd2VpY2VzdW5AZ29vZ2xlLmNvbTAgFw0yMTEy
-MDgwMTMxMzFaGA80NzU5MTEwNDAxMzEzMVowgYMxCzAJBgNVBAYTAlVTMRMwEQYD
-VQQIDApDYWxpZm9ybmlhMQwwCgYDVQQHDANNVFYxDzANBgNVBAoMBkdvb2dsZTEP
-MA0GA1UECwwGbmVhcmJ5MQswCQYDVQQDDAJ3czEiMCAGCSqGSIb3DQEJARYTd2Vp
-Y2VzdW5AZ29vZ2xlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
-AO0JW1YZ5bKHZG5B9eputz3kGREmXcWZ97dg/ODDs3+op4ulBmgaYeo5yeCy29GI
-Sjgxo4G+9fNZ7Fejrk5/LLWovAoRvVxnkRxCkTfp15jZpKNnZjT2iTRLXzNz2O04
-cC0jB81mu5vJ9a8pt+EQkuSwjDMiUi6q4Sf6IRxtTCd5a1yn9eHf1y2BbCmU+Eys
-bs97HJl9PgMCp7hP+dYDxEtNTAESg5IpJ1i7uINgPNl8d0tvJ9rOEdy0IcdeGwt/
-t0L9fIoRCePttH+idKIyDjcNyp9WtX2/wZKlsGap83rGzLdL2PI4DYJ2Ytmy8W3a
-9qFJNrhl3Q3BYgPlcCg9qQOIKq6ZJgFFH3snVDKvtSFd8b9ofK7UzD5g2SllTqDA
-4YvrdK4GETQunSjG7AC/2PpvN/FdhHm7pBi0fkgwykMh35gv0h8mmb6pBISYgr85
-+GMBilNiNJ4G6j3cdOa72pvfDW5qn5dn5ks8cIgW2X1uF/GT8rR6Mb2rwhjY9eXk
-TaP0RykyzheMY/7dWeA/PdN3uMCEJEt72ZakDIswgQVPCIw8KQPIf6pl0d5hcLSV
-QzhqBaXudseVg0QlZ86iaobpZvCrW0KqQmMU5GVhEtDc2sPe5e+TCmUC/H+vo8F8
-1UYu3MJaBcpePFlgIsLhW0niUTfCq2FiNrPykOJT7U9NAgMBAAGjUzBRMB0GA1Ud
-DgQWBBQKSepRcKTv9hr8mmKjYCL7NeG2izAfBgNVHSMEGDAWgBQKSepRcKTv9hr8
-mmKjYCL7NeG2izAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQC/
-BoItafzvjYPzENY16BIkgRqJVU7IosWxGLczzg19NFu6HPa54alqkawp7RI1ZNVH
-bJjQma5ap0L+Y06/peIU9rvEtfbCkkYJwvIaSRlTlzrNwNEcj3yJMmGTr/wfIzq8
-PN1t0hihnqI8ZguOPC+sV6ARoC+ygkwaLU1oPbVvOGz9WplvSokE1mvtqKAyuDoL
-LZfWwbhxRAgwgCIEz6cPfEcgg3Xzc+L4OzmNhTTc7GNOAtvvW7Zqc2Lohb8nQMNw
-uY65yiHPNmjmc+xLHZk3jQg82tKv792JJRkVXPsIfQV087IzxFFjjvKy82rVfeaN
-F9g2EpUvdjtm8zx7K5tiDv9Es/Up7oOnoB5baLgnMAEVMTZY+4k/6BfVM5CVUu+H
-AO1yh2yeNWbzY8B+zxRef3C2Ax68lJHFyz8J1pfrGpWxML3rDmWiVDMtEk73t3g+
-lcyLYo7OW+iBn6BODRcINO4R640oyMjFz2wPSPAsU0Zj/MbgC6iaS+goS3QnyPQS
-O3hKWfwqQuA7BZ0la1n+plKH5PKxQESAbd37arzCsgQuktl33ONiwYOt6eUyHl/S
-E3ZdldkmGm9z0mcBYG9NczDBSYmtuZOGjEzIRqI5GFD2WixE+dqTzVP/kyBd4BLc
-OTmBynN/8D/qdUZNrT+tgs+mH/I2SsKYW9Zymwf7Qw==
------END CERTIFICATE-----
diff --git a/nearby/halfsheet/apk-certs/key.pem b/nearby/halfsheet/apk-certs/key.pem
deleted file mode 100644
index e9f4288..0000000
--- a/nearby/halfsheet/apk-certs/key.pem
+++ /dev/null
@@ -1,52 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDtCVtWGeWyh2Ru
-QfXqbrc95BkRJl3Fmfe3YPzgw7N/qKeLpQZoGmHqOcngstvRiEo4MaOBvvXzWexX
-o65Ofyy1qLwKEb1cZ5EcQpE36deY2aSjZ2Y09ok0S18zc9jtOHAtIwfNZrubyfWv
-KbfhEJLksIwzIlIuquEn+iEcbUwneWtcp/Xh39ctgWwplPhMrG7PexyZfT4DAqe4
-T/nWA8RLTUwBEoOSKSdYu7iDYDzZfHdLbyfazhHctCHHXhsLf7dC/XyKEQnj7bR/
-onSiMg43DcqfVrV9v8GSpbBmqfN6xsy3S9jyOA2CdmLZsvFt2vahSTa4Zd0NwWID
-5XAoPakDiCqumSYBRR97J1Qyr7UhXfG/aHyu1Mw+YNkpZU6gwOGL63SuBhE0Lp0o
-xuwAv9j6bzfxXYR5u6QYtH5IMMpDId+YL9IfJpm+qQSEmIK/OfhjAYpTYjSeBuo9
-3HTmu9qb3w1uap+XZ+ZLPHCIFtl9bhfxk/K0ejG9q8IY2PXl5E2j9EcpMs4XjGP+
-3VngPz3Td7jAhCRLe9mWpAyLMIEFTwiMPCkDyH+qZdHeYXC0lUM4agWl7nbHlYNE
-JWfOomqG6Wbwq1tCqkJjFORlYRLQ3NrD3uXvkwplAvx/r6PBfNVGLtzCWgXKXjxZ
-YCLC4VtJ4lE3wqthYjaz8pDiU+1PTQIDAQABAoICAQCt4R5CM+8enlka1IIbvann
-2cpVnUpOaNqhh6EZFBY5gDOfqafgd/H5yvh/P1UnCI5BWJBz3ew33nAT/fsglAPt
-ImEGFetNvJ9jFqXGWWCRPJ6cS35bPbp6RQwKB2JK6grH4ZmYoFLhPi5elwDPNcQ7
-xBKkc/nLSAiwtbjSTI7/qf8K0h752aTUOctpWWEnhZon00ywf4Ic3TbBatF/n/W/
-s20coEMp1cyKN/JrVQ5uD/LGwDyBModB2lWpFSxLrB14I9DWyxbxP28X7ckXLhbl
-ZdWMOyQZoa/S7n5PYT49g1Wq5BW54UpvuH5c6fpWtrgSqk1cyUR2EbTf3NAAhPLU
-PgPK8wbFMcMB3TpQDXl7USA7QX5wSv22OfhivPsHQ9szGM0f84mK0PhXYPWBiNUY
-Y8rrIjOijB4eFGDFnTIMTofAb07NxRThci710BYUqgBVTBG5N+avIesjwkikMjOI
-PwYukKSQSw/Tqxy5Z9l22xksGynBZFjEFs/WT5pDczPAktA4xW3CGxjkMsIYaOBs
-OCEujqc5+mHSywYvy8aN+nA+yPucJP5e5pLZ1qaU0tqyakCx8XeeOyP6Wfm3UAAV
-AYelBRcWcJxM51w4o5UnUnpBD+Uxiz1sRVlqa9bLJjP4M+wJNL+WaIn9D6WhPOvl
-+naDC+p29ou2JzyKFDsOQQKCAQEA+Jalm+xAAPc+t/gCdAqEDo0NMA2/NG8m9BRc
-CVZRRaWVyGPeg5ziT/7caGwy2jpOZEjK0OOTCAqF+sJRDj6DDIw7nDrlxNyaXnCF
-gguQHFIYaHcjKGTs5l0vgL3H7pMFHN2qVynf4xrTuBXyT1GJ4vdWKAJbooa02c8W
-XI2fjwZ7Y8wSWrm1tn3oTTBR3N6o1GyPY6/TrL0mhpWwgx5eJeLl3GuUxOhXY5R9
-y48ziS97Dqdq75MxUOHickofCNcm7p+jA8Hg+SxLMR/kUFsXOxawmvsBqdL1XzU5
-LTS7xAEY9iMuBcO6yIxcxqBx96idjsPXx1lgARo1CpaZYCzgPQKCAQEA9BqKMN/Y
-o+T+ac99St8x3TYkk5lkvLVqlPw+EQhEqrm9EEBPntxWM5FEIpPVmFm7taGTgPfN
-KKaaNxX5XyK9B2v1QqN7XrX0nF4+6x7ao64fdpRUParIuBVctqzQWWthme66eHrf
-L86T/tkt3o/7p+Hd4Z9UT3FaAew1ggWr00xz5PJ/4b3f3mRmtNmgeTYskWMxOpSj
-bEenom4Row7sfLNeXNSWDGlzJ/lf6svvbVM2X5h2uFsxlt/Frq9ooTA3wwhnbd1i
-cFifDQ6cxF5mBpz/V/hnlHVfuXlknEZa9EQXHNo/aC9y+bR+ai05FJyK/WgqleW8
-5PBmoTReWA2MUQKCAQAnnnLkh+GnhcBEN83ESszDOO3KI9a+d5yguAH3Jv+q9voJ
-Rwl2tnFHSJo+NkhgiXxm9UcFxc9wL6Us0v1yJLpkLJFvk9984Z/kv1A36rncGaV0
-ONCspnEvQdjJTvXnax0cfaOhYrYhDuyBYVYOGDO+rabYl4+dNpTqRdwNgjDU7baK
-sEKYnRJ99FEqxDG33vDPckHkJGi7FiZmusK4EwX0SdZSq/6450LORyNJZxhSm/Oj
-4UDkz/PDLU0W5ANQOGInE+A6QBMoA0w0lx2fRPVN4I7jFHAubcXXl7b2InpugbJF
-wFOcbZZ+UgiTS4z+aKw7zbC9P9xSMKgVeO0W6/ANAoIBABe0LA8q7YKczgfAWk5W
-9iShCVQ75QheJYdqJyzIPMLHXpChbhnjE4vWY2NoL6mnrQ6qLgSsC4QTCY6n15th
-aDG8Tgi2j1hXGvXEQR/b0ydp1SxSowuJ9gvKJ0Kl7WWBg+zKvdjNNbcSvFRXCpk+
-KhXXXRB3xFwiibb+FQQXQOQ33FkzIy/snDygS0jsiSS8Gf/UPgeOP4BYRPME9Tl8
-TYKeeF9TVW7HHqOXF7VZMFrRZcpKp9ynHl2kRTH9Xo+oewG5YzHL+a8nK+q8rIR1
-Fjs2K6WDPauw6ia8nwR94H8vzX7Dwrx/Pw74c/4jfhN+UBDjeJ8tu/YPUif9SdwL
-FMECggEALdCGKfQ4vPmqI6UdfVB5hdCPoM6tUsI2yrXFvlHjSGVanC/IG9x2mpRb
-4odamLYx4G4NjP1IJSY08LFT9VhLZtRM1W3fGeboW12LTEVNrI3lRBU84rAQ1ced
-l6/DvTKJjhfwTxb/W7sqmZY5hF3QuNxs67Z8x0pe4b58musa0qFCs4Sa8qTNZKRW
-fIbxIKuvu1HSNOKkZLu6Gq8km+XIlVAaSVA03Tt+EK74MFL6+pcd7/VkS00MAYUC
-gS4ic+QFzCl5P8zl/GoX8iUFsRZQCSJkZ75VwO13pEupVwCAW8WWJO83U4jBsnJs
-ayrX7pbsnW6jsNYBUlck+RYVYkVkxA==
------END PRIVATE KEY-----
diff --git a/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_enter.xml b/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_enter.xml
deleted file mode 100644
index 098dccb..0000000
--- a/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_enter.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:interpolator="@android:interpolator/decelerate_quint">
-    <translate android:fromYDelta="100%"
-               android:toYDelta="0"
-               android:duration="900"/>
-</set>
diff --git a/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_exit.xml b/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_exit.xml
deleted file mode 100644
index 1cf7401..0000000
--- a/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_exit.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:interpolator="@android:interpolator/decelerate_quint">
-    <translate android:fromYDelta="0"
-               android:toYDelta="100%"
-               android:duration="500"/>
-</set>
diff --git a/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_in.xml b/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_in.xml
deleted file mode 100644
index 9a51ddb..0000000
--- a/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_in.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    tools:targetApi="23"
-    android:duration="@integer/half_sheet_slide_in_duration"
-    android:interpolator="@android:interpolator/fast_out_slow_in">
-  <translate
-      android:fromYDelta="100%p"
-      android:toYDelta="0%p"/>
-
-  <alpha
-      android:fromAlpha="0.0"
-      android:toAlpha="1.0"/>
-</set>
diff --git a/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_out.xml b/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_out.xml
deleted file mode 100644
index c589482..0000000
--- a/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_out.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:duration="@integer/half_sheet_fade_out_duration"
-    android:interpolator="@android:interpolator/fast_out_slow_in">
-
-  <translate
-      android:fromYDelta="0%p"
-      android:toYDelta="100%p"/>
-
-  <alpha
-      android:fromAlpha="1.0"
-      android:toAlpha="0.0"/>
-
-</set>
diff --git a/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml b/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml
deleted file mode 100644
index 7d61d1c..0000000
--- a/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml
+++ /dev/null
@@ -1,25 +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.
-  -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24.0"
-    android:viewportHeight="24.0"
-    android:tint="@color/fast_pair_half_sheet_subtitle_color">
-    <path
-        android:fillColor="@color/fast_pair_half_sheet_subtitle_color"
-        android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
-</vector>
\ No newline at end of file
diff --git a/nearby/halfsheet/res/drawable/fastpair_outline.xml b/nearby/halfsheet/res/drawable/fastpair_outline.xml
deleted file mode 100644
index 6765e11..0000000
--- a/nearby/halfsheet/res/drawable/fastpair_outline.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="oval">
-  <stroke
-      android:width="1dp"
-      android:color="@color/fast_pair_notification_image_outline"/>
-</shape>
diff --git a/nearby/halfsheet/res/drawable/half_sheet_bg.xml b/nearby/halfsheet/res/drawable/half_sheet_bg.xml
deleted file mode 100644
index 7e7d8dd..0000000
--- a/nearby/halfsheet/res/drawable/half_sheet_bg.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    tools:targetApi="23">
-  <solid android:color="@color/fast_pair_half_sheet_background" />
-  <corners
-      android:topLeftRadius="16dp"
-      android:topRightRadius="16dp"
-      android:padding="8dp"/>
-</shape>
diff --git a/nearby/halfsheet/res/drawable/quantum_ic_devices_other_vd_theme_24.xml b/nearby/halfsheet/res/drawable/quantum_ic_devices_other_vd_theme_24.xml
deleted file mode 100644
index 3dcfdee..0000000
--- a/nearby/halfsheet/res/drawable/quantum_ic_devices_other_vd_theme_24.xml
+++ /dev/null
@@ -1,25 +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.
-  -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24.0"
-    android:viewportHeight="24.0"
-    android:tint="?attr/colorControlNormal">
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M3,6h18L21,4L3,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h4v-2L3,18L3,6zM13,12L9,12v1.78c-0.61,0.55 -1,1.33 -1,2.22s0.39,1.67 1,2.22L9,20h4v-1.78c0.61,-0.55 1,-1.34 1,-2.22s-0.39,-1.67 -1,-2.22L13,12zM11,17.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM22,8h-6c-0.5,0 -1,0.5 -1,1v10c0,0.5 0.5,1 1,1h6c0.5,0 1,-0.5 1,-1L23,9c0,-0.5 -0.5,-1 -1,-1zM21,18h-4v-8h4v8z"/>
-</vector>
diff --git a/nearby/halfsheet/res/layout-land/fast_pair_device_pairing_fragment.xml b/nearby/halfsheet/res/layout-land/fast_pair_device_pairing_fragment.xml
deleted file mode 100644
index 545f7fa..0000000
--- a/nearby/halfsheet/res/layout-land/fast_pair_device_pairing_fragment.xml
+++ /dev/null
@@ -1,148 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<ScrollView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    tools:ignore="RtlCompat"
-    android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="0dp"
-    android:layout_weight="1">
-
-  <androidx.constraintlayout.widget.ConstraintLayout
-      android:id="@+id/image_view"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:paddingTop="12dp"
-      android:paddingStart="12dp"
-      android:paddingEnd="12dp">
-
-    <TextView
-        android:id="@+id/header_subtitle"
-        android:textColor="@color/fast_pair_half_sheet_subtitle_color"
-        android:fontFamily="google-sans"
-        android:textSize="14sp"
-        android:gravity="center"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent" />
-
-    <ImageView
-        android:id="@+id/pairing_pic"
-        android:layout_width="@dimen/fast_pair_half_sheet_land_image_size"
-        android:layout_height="@dimen/fast_pair_half_sheet_land_image_size"
-        android:paddingTop="18dp"
-        android:paddingBottom="18dp"
-        android:importantForAccessibility="no"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/header_subtitle" />
-
-    <TextView
-        android:id="@+id/pin_code"
-        android:textColor="@color/fast_pair_half_sheet_subtitle_color"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/fast_pair_half_sheet_land_image_size"
-        android:paddingTop="18dp"
-        android:paddingBottom="18dp"
-        android:visibility="invisible"
-        android:textSize="50sp"
-        android:letterSpacing="0.2"
-        android:fontFamily="google-sans-medium"
-        android:gravity="center"
-        android:importantForAccessibility="yes"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/header_subtitle" />
-
-    <ProgressBar
-        android:id="@+id/connect_progressbar"
-        android:layout_width="@dimen/fast_pair_half_sheet_land_image_size"
-        android:layout_height="4dp"
-        android:indeterminate="true"
-        android:indeterminateTint="@color/fast_pair_progress_color"
-        android:indeterminateTintMode="src_in"
-        style="?android:attr/progressBarStyleHorizontal"
-        android:layout_marginBottom="6dp"
-        app:layout_constraintTop_toBottomOf="@+id/pairing_pic"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"/>
-
-    <RelativeLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal"
-        android:id="@+id/mid_part"
-        app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent">
-
-      <ImageView
-          android:id="@+id/info_icon"
-          android:layout_alignParentStart="true"
-          android:contentDescription="@null"
-          android:layout_centerInParent="true"
-          android:layout_marginEnd="10dp"
-          android:layout_toStartOf="@id/connect_btn"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          app:srcCompat="@drawable/fast_pair_ic_info"
-          android:visibility="invisible" />
-
-      <com.google.android.material.button.MaterialButton
-          android:id="@+id/connect_btn"
-          android:text="@string/paring_action_connect"
-          android:layout_height="wrap_content"
-          android:layout_width="@dimen/fast_pair_half_sheet_image_size"
-          android:layout_centerInParent="true"
-          style="@style/HalfSheetButton" />
-
-    </RelativeLayout>
-
-    <com.google.android.material.button.MaterialButton
-        android:id="@+id/settings_btn"
-        android:text="@string/paring_action_settings"
-        android:visibility="invisible"
-        android:layout_height="wrap_content"
-        android:layout_width="@dimen/fast_pair_half_sheet_land_image_size"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
-        style="@style/HalfSheetButton" />
-
-    <com.google.android.material.button.MaterialButton
-        android:id="@+id/cancel_btn"
-        android:text="@string/paring_action_done"
-        android:visibility="invisible"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/mid_part"
-        android:gravity="start|center_vertical"
-        android:paddingTop="6dp"
-        android:paddingBottom="6dp"
-        android:layout_marginBottom="10dp"
-        style="@style/HalfSheetButtonBorderless"/>
-
-    <com.google.android.material.button.MaterialButton
-        android:id="@+id/setup_btn"
-        android:text="@string/paring_action_launch"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/mid_part"
-        android:paddingTop="6dp"
-        android:paddingBottom="6dp"
-        android:layout_marginBottom="10dp"
-        android:visibility="invisible"
-        android:layout_height="@dimen/fast_pair_half_sheet_bottom_button_height"
-        android:layout_width="wrap_content"
-        style="@style/HalfSheetButton" />
-
-    <!--Empty place holder to prevent pairing button from being cut off by screen-->
-    <View
-        android:layout_width="match_parent"
-        android:layout_height="30dp"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/setup_btn"/>
-  </androidx.constraintlayout.widget.ConstraintLayout>
-
-</ScrollView>
diff --git a/nearby/halfsheet/res/layout-land/fast_pair_half_sheet.xml b/nearby/halfsheet/res/layout-land/fast_pair_half_sheet.xml
deleted file mode 100644
index e993536..0000000
--- a/nearby/halfsheet/res/layout-land/fast_pair_half_sheet.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    tools:ignore="RtlCompat, UselessParent, MergeRootFrame"
-    android:id="@+id/background"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-  <LinearLayout
-      android:id="@+id/card"
-      android:orientation="vertical"
-      android:transitionName="card"
-      android:layout_height="wrap_content"
-      android:layout_width="wrap_content"
-      android:layout_gravity= "center|bottom"
-      android:paddingLeft="12dp"
-      android:paddingRight="12dp"
-      android:background="@drawable/half_sheet_bg"
-      android:accessibilityLiveRegion="polite"
-      android:gravity="bottom">
-
-    <RelativeLayout
-        android:id="@+id/toolbar_wrapper"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:paddingLeft="20dp"
-        android:paddingRight="20dp">
-
-      <ImageView
-          android:layout_marginTop="9dp"
-          android:layout_marginBottom="9dp"
-          android:id="@+id/toolbar_image"
-          android:layout_width="42dp"
-          android:layout_height="42dp"
-          android:contentDescription="@null"
-          android:layout_toStartOf="@id/toolbar_title"
-          android:layout_centerHorizontal="true"
-          android:visibility="invisible"/>
-
-      <TextView
-          android:layout_marginTop="18dp"
-          android:layout_marginBottom="18dp"
-          android:layout_centerHorizontal="true"
-          android:id="@+id/toolbar_title"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:fontFamily="google-sans-medium"
-          android:maxLines="1"
-          android:ellipsize="end"
-          android:textSize="24sp"
-          android:textColor="@color/fast_pair_half_sheet_text_color"
-          style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
-    </RelativeLayout>
-
-    <FrameLayout
-        android:id="@+id/fragment_container"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-    />
-
-  </LinearLayout>
-
-</FrameLayout>
-
diff --git a/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml b/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml
deleted file mode 100644
index 77cd1ea..0000000
--- a/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml
+++ /dev/null
@@ -1,141 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:orientation="vertical"
-    tools:ignore="RtlCompat"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-  <androidx.constraintlayout.widget.ConstraintLayout
-      android:id="@+id/image_view"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:minHeight="340dp"
-      android:paddingStart="12dp"
-      android:paddingEnd="12dp"
-      android:paddingTop="12dp"
-      android:paddingBottom="12dp">
-
-    <TextView
-        android:id="@+id/header_subtitle"
-        android:textColor="@color/fast_pair_half_sheet_subtitle_color"
-        android:fontFamily="google-sans"
-        android:textSize="14sp"
-        android:maxLines="3"
-        android:gravity="center"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent" />
-
-    <ImageView
-        android:id="@+id/pairing_pic"
-        android:layout_width="@dimen/fast_pair_half_sheet_image_size"
-        android:layout_height="@dimen/fast_pair_half_sheet_image_size"
-        android:paddingTop="18dp"
-        android:paddingBottom="18dp"
-        android:importantForAccessibility="no"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/header_subtitle" />
-
-    <TextView
-        android:id="@+id/pin_code"
-        android:textColor="@color/fast_pair_half_sheet_subtitle_color"
-        android:layout_width="wrap_content"
-        android:layout_height="@dimen/fast_pair_half_sheet_image_size"
-        android:paddingTop="18dp"
-        android:paddingBottom="18dp"
-        android:visibility="invisible"
-        android:textSize="50sp"
-        android:letterSpacing="0.2"
-        android:fontFamily="google-sans-medium"
-        android:gravity="center"
-        android:importantForAccessibility="yes"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/header_subtitle" />
-
-    <ProgressBar
-        android:id="@+id/connect_progressbar"
-        android:layout_width="@dimen/fast_pair_half_sheet_image_size"
-        android:layout_height="2dp"
-        android:indeterminate="true"
-        android:indeterminateTint="@color/fast_pair_progress_color"
-        android:indeterminateTintMode="src_in"
-        style="?android:attr/progressBarStyleHorizontal"
-        android:layout_marginBottom="6dp"
-        app:layout_constraintTop_toBottomOf="@+id/pairing_pic"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"/>
-
-    <RelativeLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal"
-        app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent">
-
-      <ImageView
-          android:id="@+id/info_icon"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          app:srcCompat="@drawable/fast_pair_ic_info"
-          android:layout_centerInParent="true"
-          android:contentDescription="@null"
-          android:layout_marginEnd="10dp"
-          android:layout_toStartOf="@id/connect_btn"
-          android:visibility="invisible" />
-
-      <com.google.android.material.button.MaterialButton
-          android:id="@+id/connect_btn"
-          android:layout_width="@dimen/fast_pair_half_sheet_image_size"
-          android:layout_height="wrap_content"
-          android:text="@string/paring_action_connect"
-          android:layout_centerInParent="true"
-          style="@style/HalfSheetButton" />
-
-    </RelativeLayout>
-
-    <com.google.android.material.button.MaterialButton
-        android:id="@+id/settings_btn"
-        android:text="@string/paring_action_settings"
-        android:layout_height="wrap_content"
-        android:layout_width="@dimen/fast_pair_half_sheet_image_size"
-        app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        android:visibility="invisible"
-        style="@style/HalfSheetButton" />
-
-    <com.google.android.material.button.MaterialButton
-        android:id="@+id/cancel_btn"
-        android:text="@string/paring_action_done"
-        android:visibility="invisible"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:gravity="start|center_vertical"
-        android:layout_marginTop="6dp"
-        android:layout_marginBottom="16dp"
-        style="@style/HalfSheetButtonBorderless"/>
-
-    <com.google.android.material.button.MaterialButton
-        android:id="@+id/setup_btn"
-        android:text="@string/paring_action_launch"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:layout_marginTop="6dp"
-        android:layout_marginBottom="16dp"
-        android:background="@color/fast_pair_half_sheet_button_color"
-        android:visibility="invisible"
-        android:layout_height="@dimen/fast_pair_half_sheet_bottom_button_height"
-        android:layout_width="wrap_content"
-        style="@style/HalfSheetButton" />
-
-  </androidx.constraintlayout.widget.ConstraintLayout>
-
-</LinearLayout>
diff --git a/nearby/halfsheet/res/layout/fast_pair_half_sheet.xml b/nearby/halfsheet/res/layout/fast_pair_half_sheet.xml
deleted file mode 100644
index 705aa1b..0000000
--- a/nearby/halfsheet/res/layout/fast_pair_half_sheet.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    tools:ignore="RtlCompat, UselessParent, MergeRootFrame"
-    android:id="@+id/background"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-  <LinearLayout
-      android:id="@+id/card"
-      android:orientation="vertical"
-      android:transitionName="card"
-      android:layout_height="wrap_content"
-      android:layout_width="match_parent"
-      android:layout_gravity= "center|bottom"
-      android:paddingLeft="12dp"
-      android:paddingRight="12dp"
-      android:background="@drawable/half_sheet_bg"
-      android:accessibilityLiveRegion="polite"
-      android:gravity="bottom">
-
-    <RelativeLayout
-        android:id="@+id/toolbar_wrapper"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:paddingLeft="20dp"
-        android:paddingRight="20dp">
-
-      <ImageView
-          android:layout_marginTop="9dp"
-          android:layout_marginBottom="9dp"
-          android:id="@+id/toolbar_image"
-          android:layout_width="42dp"
-          android:layout_height="42dp"
-          android:contentDescription="@null"
-          android:layout_toStartOf="@id/toolbar_title"
-          android:layout_centerHorizontal="true"
-          android:visibility="invisible"/>
-
-      <TextView
-          android:layout_marginTop="18dp"
-          android:layout_marginBottom="18dp"
-          android:layout_centerHorizontal="true"
-          android:id="@+id/toolbar_title"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:fontFamily="google-sans-medium"
-          android:textSize="24sp"
-          android:textColor="@color/fast_pair_half_sheet_text_color"
-          style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
-    </RelativeLayout>
-
-    <FrameLayout
-        android:id="@+id/fragment_container"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-
-  </LinearLayout>
-
-</FrameLayout>
-
diff --git a/nearby/halfsheet/res/layout/fast_pair_heads_up_notification.xml b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification.xml
deleted file mode 100644
index 11b8343..0000000
--- a/nearby/halfsheet/res/layout/fast_pair_heads_up_notification.xml
+++ /dev/null
@@ -1,83 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="horizontal"
-    android:baselineAligned="false"
-    android:background="@color/fast_pair_notification_background"
-    tools:ignore="ContentDescription,UnusedAttribute,RtlCompat,Overdraw">
-
-  <LinearLayout
-      android:orientation="vertical"
-      android:layout_width="0dp"
-      android:layout_height="wrap_content"
-      android:layout_weight="1"
-      android:layout_marginTop="@dimen/fast_pair_notification_padding"
-      android:layout_marginStart="@dimen/fast_pair_notification_padding"
-      android:layout_marginEnd="@dimen/fast_pair_notification_padding">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal">
-
-      <TextView
-          android:id="@android:id/title"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:fontFamily="sans-serif-medium"
-          android:textSize="@dimen/fast_pair_notification_text_size"
-          android:textColor="@color/fast_pair_primary_text"
-          android:layout_marginBottom="2dp"
-          android:lines="1"/>
-
-      <TextView
-          android:id="@android:id/text2"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:textSize="@dimen/fast_pair_notification_text_size_small"
-          android:textColor="@color/fast_pair_primary_text"
-          android:layout_marginBottom="2dp"
-          android:layout_marginStart="4dp"
-          android:lines="1"/>
-    </LinearLayout>
-
-    <TextView
-        android:id="@android:id/text1"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:textSize="@dimen/fast_pair_notification_text_size"
-        android:textColor="@color/fast_pair_primary_text"
-        android:maxLines="2"
-        android:ellipsize="end"
-        android:breakStrategy="simple" />
-
-    <FrameLayout
-        android:id="@android:id/secondaryProgress"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="8dp"
-        android:layout_marginEnd="32dp"
-        android:orientation="horizontal"
-        android:visibility="gone">
-
-      <ProgressBar
-          android:id="@android:id/progress"
-          style="?android:attr/progressBarStyleHorizontal"
-          android:layout_width="match_parent"
-          android:layout_height="wrap_content"
-          android:layout_gravity="center_vertical"
-          android:indeterminateTint="@color/discovery_activity_accent"/>
-
-    </FrameLayout>
-
-  </LinearLayout>
-
-  <FrameLayout
-      android:id="@android:id/icon1"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"/>
-
-</LinearLayout>
diff --git a/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_large_image.xml b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_large_image.xml
deleted file mode 100644
index dd28947..0000000
--- a/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_large_image.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@android:id/icon"
-    android:layout_width="@dimen/fast_pair_notification_large_image_size"
-    android:layout_height="@dimen/fast_pair_notification_large_image_size"
-    android:scaleType="fitStart"
-    tools:ignore="ContentDescription"/>
diff --git a/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_small_image.xml b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_small_image.xml
deleted file mode 100644
index ee1d89f..0000000
--- a/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_small_image.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@android:id/icon"
-    android:layout_width="@dimen/fast_pair_notification_small_image_size"
-    android:layout_height="@dimen/fast_pair_notification_small_image_size"
-    android:layout_marginTop="@dimen/fast_pair_notification_padding"
-    android:layout_marginBottom="@dimen/fast_pair_notification_padding"
-    android:layout_marginStart="@dimen/fast_pair_notification_padding"
-    android:layout_marginEnd="@dimen/fast_pair_notification_padding"
-    android:scaleType="fitStart"
-    tools:ignore="ContentDescription,RtlCompat"/>
diff --git a/nearby/halfsheet/res/values-af/strings.xml b/nearby/halfsheet/res/values-af/strings.xml
deleted file mode 100644
index b0f5631..0000000
--- a/nearby/halfsheet/res/values-af/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Begin tans opstelling …"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Stel toestel op"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Toestel is gekoppel"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Gekoppel aan “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Kon nie koppel nie"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Kan nie koppel nie"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Probeer die toestel self saambind"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Probeer om die toestel in saambindmodus te sit"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Toestelle binne bereik"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Toestelle met jou rekening"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Jou gestoorde toestel is reg"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Naby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Toestelle"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Koppel tans …"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Klaar"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Stoor"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Koppel"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Stel op"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Instellings"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-am/strings.xml b/nearby/halfsheet/res/values-am/strings.xml
deleted file mode 100644
index 7c0aed4..0000000
--- a/nearby/halfsheet/res/values-am/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ማዋቀርን በመጀመር ላይ…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"መሣሪያ አዋቅር"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"መሣሪያ ተገናኝቷል"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"ከ«%s» ጋር ተገናኝቷል"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"መገናኘት አልተቻለም"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"መገናኘት አልተቻለም"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"በእጅ ከመሣሪያው ጋር ለማጣመር ይሞክሩ"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"መሣሪያውን ወደ ማጣመር ሁነታ ለማስገባት ይሞክሩ"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"በቅርብ ያሉ መሣሪያዎች"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ከመለያዎ ጋር መሣሪያዎች"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"የተቀመጠው መሣሪያዎ ይገኛል"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"በአቅራቢያ"</string>
-    <string name="common_devices" msgid="2635603125608104442">"መሣሪያዎች"</string>
-    <string name="common_connecting" msgid="160531481424245303">"በማገናኘት ላይ…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ባትሪ፦ %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"ተጠናቅቋል"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"አስቀምጥ"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"አገናኝ"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"አዋቅር"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"ቅንብሮች"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ar/strings.xml b/nearby/halfsheet/res/values-ar/strings.xml
deleted file mode 100644
index 089faaa..0000000
--- a/nearby/halfsheet/res/values-ar/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"جارٍ الإعداد…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"إعداد جهاز"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"تمّ إقران الجهاز"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"‏تم الربط بـ \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"تعذّر الربط"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"يتعذَّر الاتصال"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"جرِّب الإقران يدويًا بالجهاز."</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"جرِّب تشغيل الجهاز في وضع الإقران."</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"الأجهزة القريبة"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"الأجهزة المرتبطة بحسابك"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"جهازك المحفوظ متاح"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"مشاركة عن قرب"</string>
-    <string name="common_devices" msgid="2635603125608104442">"الأجهزة"</string>
-    <string name="common_connecting" msgid="160531481424245303">"جارٍ الاتصال…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"‏البطارية: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"تم"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"حفظ"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"ربط"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"إعداد"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"الإعدادات"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-as/strings.xml b/nearby/halfsheet/res/values-as/strings.xml
deleted file mode 100644
index bb9dfcc..0000000
--- a/nearby/halfsheet/res/values-as/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ছেটআপ আৰম্ভ কৰি থকা হৈছে…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"ডিভাইচ ছেট আপ কৰক"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"ডিভাইচ সংযোগ কৰা হ’ল"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s”ৰ সৈতে সংযোগ কৰা হ’ল"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"সংযোগ কৰিব পৰা নগ’ল"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"সংযোগ কৰিব পৰা নাই"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ডিভাইচটোৰ সৈতে মেনুৱেলী পেয়াৰ কৰক"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ডিভাইচক পেয়াৰ কৰা ম’ডত ৰাখি চাওক"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"সংযোগ সীমাত থকা ডিভাইচসমূহ"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"আপোনাৰ একাউণ্টত সংযোগ হোৱা ডিভাইচবোৰ"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"আপুনি ছেভ কৰা ডিভাইচ উপলব্ধ"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"নিকটৱৰ্তী"</string>
-    <string name="common_devices" msgid="2635603125608104442">"ডিভাইচ"</string>
-    <string name="common_connecting" msgid="160531481424245303">"সংযোগ কৰি থকা হৈছে…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"বেটাৰী: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"হ’ল"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"ছেভ কৰক"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"সংযোগ কৰক"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"ছেট আপ কৰক"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"ছেটিং"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-az/strings.xml b/nearby/halfsheet/res/values-az/strings.xml
deleted file mode 100644
index 844963b..0000000
--- a/nearby/halfsheet/res/values-az/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Ayarlama başladılır…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Cihazı quraşdırın"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Cihaz qoşulub"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” şəbəkəsinə qoşulub"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Qoşulmaq mümkün olmadı"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Qoşulmaq olmur"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Cihazı manual olaraq birləşdirin"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Cihazı qoşalaşdırma rejiminə qoymağa çalışın"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Əl altında olan cihazlar"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Hesabınızdakı cihazlar"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Yadda saxlanmış cihazınız əlçatandır"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Yaxınlıqda"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Cihazlar"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Qoşulur…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batareya: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Oldu"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Saxlayın"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Qoşun"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Ayarlayın"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Ayarlar"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-b+sr+Latn/strings.xml b/nearby/halfsheet/res/values-b+sr+Latn/strings.xml
deleted file mode 100644
index fcd1dc6..0000000
--- a/nearby/halfsheet/res/values-b+sr+Latn/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Podešavanje se pokreće…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Podesite uređaj"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Uređaj je povezan"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Povezani ste sa uređajem %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezivanje nije uspelo"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Povezivanje nije uspelo"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Probajte da uparite ručno sa uređajem"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Probajte da prebacite uređaj u režim uparivanja"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Uređaji u dometu"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Uređaji povezani sa nalogom"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Sačuvani uređaj je dostupan"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"U blizini"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Uređaji"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Povezuje se…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Baterija: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Gotovo"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Sačuvaj"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Podesi"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Podešavanja"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-be/strings.xml b/nearby/halfsheet/res/values-be/strings.xml
deleted file mode 100644
index f469922..0000000
--- a/nearby/halfsheet/res/values-be/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Пачынаецца наладжванне…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Наладзьце прыладу"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Прылада падключана"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Падключана да прылады \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Не ўдалося падключыцца"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Не ўдалося падключыцца"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Паспрабуйце спалучыць прыладу ўручную"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Перавядзіце прыладу ў рэжым спалучэння"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Прылады ў межах дасягальнасці"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Прылады з вашым уліковым запісам"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Ёсць захаваная вамі прылада"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Паблізу"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Прылады"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Падключэнне…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Узровень зараду: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Гатова"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Захаваць"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Падключыць"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Наладзіць"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Налады"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-bg/strings.xml b/nearby/halfsheet/res/values-bg/strings.xml
deleted file mode 100644
index a0c5103..0000000
--- a/nearby/halfsheet/res/values-bg/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Настройването се стартира…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Настройване на устройството"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Устройството е свързано"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Установена е връзка с(ъс) „%s“"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Свързването не бе успешно"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Не може да се установи връзка"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Опитайте да сдвоите устройството ръчно"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Опитайте да зададете режим на сдвояване за устройството"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Устройства в обхват"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Устройства с профила ви"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Запазеното ви у-во е налице"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"В близост"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Устройства"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Свързва се…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Батерия: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Запазване"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Свързване"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Настройване"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Настройки"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-bn/strings.xml b/nearby/halfsheet/res/values-bn/strings.xml
deleted file mode 100644
index 4d6afc0..0000000
--- a/nearby/halfsheet/res/values-bn/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"সেট-আপ করা শুরু হচ্ছে…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"ডিভাইস সেট-আপ করুন"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"ডিভাইস কানেক্ট হয়েছে"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s”-এ কানেক্ট করা হয়েছে"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"কানেক্ট করা যায়নি"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"কানেক্ট করা যায়নি"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ডিভাইসে ম্যানুয়ালি পেয়ার করার চেষ্টা করুন"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ডিভাইস \'যোগ করার\' মোডে রাখার চেষ্টা করুন"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"কাছে রয়েছে এমন ডিভাইস"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"আপনার অ্যাকাউন্টের সাথে কানেক্ট থাকা ডিভাইস"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"আপনার সেভ করা ডিভাইস উপলভ্য আছে"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"নিয়ারবাই"</string>
-    <string name="common_devices" msgid="2635603125608104442">"ডিভাইস"</string>
-    <string name="common_connecting" msgid="160531481424245303">"কানেক্ট করা হচ্ছে…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ব্যাটারি: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"হয়ে গেছে"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"সেভ করুন"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"কানেক্ট করুন"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"সেট-আপ করুন"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"সেটিংস"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-bs/strings.xml b/nearby/halfsheet/res/values-bs/strings.xml
deleted file mode 100644
index 47f13c3..0000000
--- a/nearby/halfsheet/res/values-bs/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Pokretanje postavljanja…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Postavi uređaj"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Uređaj je povezan"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Povezano s uređajem “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezivanje nije uspjelo"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nije moguće povezati"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Pokušajte ručno upariti uređaj"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Pokušajte staviti uređaj u način rada za uparivanje"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Uređaji u dometu"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Uređaji s vašim računom"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Sačuvani uređaj je dostupan"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"U blizini"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Uređaji"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Povezivanje…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Baterija: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Gotovo"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Sačuvaj"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Postavi"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Postavke"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ca/strings.xml b/nearby/halfsheet/res/values-ca/strings.xml
deleted file mode 100644
index 44ebc3e..0000000
--- a/nearby/halfsheet/res/values-ca/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciant la configuració…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configura el dispositiu"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"El dispositiu s\'ha connectat"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connectat a %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"No s\'ha pogut connectar"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"No es pot establir la connexió"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prova de vincular el dispositiu manualment"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prova d\'activar el mode de vinculació al dispositiu"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositius a l\'abast"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositius amb el teu compte"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispositiu desat disponible"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"A prop"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Dispositius"</string>
-    <string name="common_connecting" msgid="160531481424245303">"S\'està connectant…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Fet"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Desa"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Connecta"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configura"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Configuració"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-cs/strings.xml b/nearby/halfsheet/res/values-cs/strings.xml
deleted file mode 100644
index 872eef5..0000000
--- a/nearby/halfsheet/res/values-cs/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Zahajování nastavení…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Nastavení zařízení"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Zařízení připojeno"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Připojeno k zařízení %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nelze se připojit"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nepodařilo se připojit"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Zkuste zařízení spárovat ručně"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Přepněte zařízení do režimu párování"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Zařízení v dosahu"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Zařízení s vaším účtem"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Je dostupné uložené zařízení"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"V okolí"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Zařízení"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Připojování…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Baterie: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Hotovo"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Uložit"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Připojit"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Nastavit"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Nastavení"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-da/strings.xml b/nearby/halfsheet/res/values-da/strings.xml
deleted file mode 100644
index 89b221f..0000000
--- a/nearby/halfsheet/res/values-da/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Begynder konfiguration…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfigurer enhed"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Enheden er forbundet"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Der er oprettet forbindelse til \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Forbindelsen kan ikke oprettes"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Kunne ikke forbindes"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prøv at parre med enheden manuelt"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prøv at sætte enheden i parringstilstand"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Enheder inden for rækkevidde"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Enheder med din konto"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Din gemte enhed er tilgængelig"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Tæt på"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Enheder"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Forbinder…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batteri: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Luk"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Gem"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Opret forbindelse"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Konfigurer"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Indstillinger"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-de/strings.xml b/nearby/halfsheet/res/values-de/strings.xml
deleted file mode 100644
index de54114..0000000
--- a/nearby/halfsheet/res/values-de/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Einrichtung wird gestartet..."</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Gerät einrichten"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Gerät verbunden"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Mit „%s“ verbunden"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Verbindung nicht möglich"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Kein Verbindungsaufbau möglich"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Versuche, das Gerät manuell zu koppeln"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Versuche, das Gerät in den Kopplungsmodus zu versetzen"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Geräte in Reichweite"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Geräte mit deinem Konto"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Gespeichertes Gerät verfügbar"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Geräte"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Wird verbunden…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Akkustand: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Fertig"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Speichern"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Verbinden"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Einrichten"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Einstellungen"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-el/strings.xml b/nearby/halfsheet/res/values-el/strings.xml
deleted file mode 100644
index 1ea467a..0000000
--- a/nearby/halfsheet/res/values-el/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Έναρξη ρύθμισης…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Ρύθμιση συσκευής"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Η συσκευή συνδέθηκε"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Συνδέθηκε με τη συσκευή %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Αδυναμία σύνδεσης"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Δεν είναι δυνατή η σύνδεση"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Δοκιμάστε να κάνετε μη αυτόματη σύζευξη στη συσκευή"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Δοκιμάστε να θέσετε τη συσκευή σε λειτουργία σύζευξης"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Συσκευές εντός εμβέλειας"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Συσκευές με τον λογαριασμό σας"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Η αποθ. συσκ. είναι διαθέσιμη"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Κοντά"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Συσκευές"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Σύνδεση…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Μπαταρία: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Τέλος"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Αποθήκευση"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Σύνδεση"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Ρύθμιση"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Ρυθμίσεις"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-en-rAU/strings.xml b/nearby/halfsheet/res/values-en-rAU/strings.xml
deleted file mode 100644
index b7039a1..0000000
--- a/nearby/halfsheet/res/values-en-rAU/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting setup…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connected to \'%s\'"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Unable to connect"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Try pairing to the device manually"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Try putting the device into pairing mode"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Devices within reach"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Devices with your account"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Your saved device is available"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Devices"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Connecting…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Set up"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Settings"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-en-rCA/strings.xml b/nearby/halfsheet/res/values-en-rCA/strings.xml
deleted file mode 100644
index 06b3a5e..0000000
--- a/nearby/halfsheet/res/values-en-rCA/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting Setup…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connected to “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Unable to connect"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Try manually pairing to the device"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Try putting the device into pairing mode"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Devices within reach"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Devices with your account"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Your saved device is available"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Devices"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Connecting…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Set up"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Settings"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-en-rGB/strings.xml b/nearby/halfsheet/res/values-en-rGB/strings.xml
deleted file mode 100644
index b7039a1..0000000
--- a/nearby/halfsheet/res/values-en-rGB/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting setup…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connected to \'%s\'"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Unable to connect"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Try pairing to the device manually"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Try putting the device into pairing mode"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Devices within reach"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Devices with your account"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Your saved device is available"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Devices"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Connecting…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Set up"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Settings"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-en-rIN/strings.xml b/nearby/halfsheet/res/values-en-rIN/strings.xml
deleted file mode 100644
index b7039a1..0000000
--- a/nearby/halfsheet/res/values-en-rIN/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting setup…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connected to \'%s\'"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Unable to connect"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Try pairing to the device manually"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Try putting the device into pairing mode"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Devices within reach"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Devices with your account"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Your saved device is available"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Devices"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Connecting…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Set up"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Settings"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-en-rXC/strings.xml b/nearby/halfsheet/res/values-en-rXC/strings.xml
deleted file mode 100644
index c71272e..0000000
--- a/nearby/halfsheet/res/values-en-rXC/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‏‏‎‏‏‎‏‏‎‏‏‏‎‎‏‎‎‎‎‏‏‏‎‎‎‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‎‏‏‎‎‎‏‎‎‏‏‎‏‏‏‏‎Starting Setup…‎‏‎‎‏‎"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‎‎‎‏‎‏‎‏‎‏‏‎‏‎‏‎‏‎‏‏‎‏‎‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‎‎‏‏‎‏‎‏‎‎‏‎‏‏‏‏‎‎Set up device‎‏‎‎‏‎"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‎‎‏‎‎‏‎‏‏‎‏‎‎‎‏‎‏‎‎‎‏‎‏‏‎‎‎‎‏‏‏‏‏‎‎‎‎‏‎‏‏‎‎‎‏‎‎‏‎‏‏‎‎‏‏‎‏‎Device connected‎‏‎‎‏‎"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‎‏‏‏‎‏‏‏‎‏‎‏‎‏‎‎‏‏‎‏‏‏‏‏‎‏‏‎‎‎‏‏‏‏‎‏‎‎‎‎‎‎‎‏‎‏‎‎‎‎‏‏‎‏‏‏‏‎‎Connected to “%s”‎‏‎‎‏‎"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‏‎‎‏‎‎‏‎‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‎‏‎‏‏‎‎‏‏‏‏‏‏‎‎‎‎Couldn\'t connect‎‏‎‎‏‎"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‎‏‎‎‎‏‏‎‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‎‏‏‎‎‏‎‎‏‎‏‎‏‎‏‎‏‏‎‏‏‎‎‎‏‎‏‎‎‏‎Unable to connect‎‏‎‎‏‎"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‎‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‏‎‎‎‎‎‎‏‏‏‎‏‎‏‎‎‎‎‏‎‏‎‏‎‏‎‏‎‎‎‏‎‏‎‎‎‎‏‏‎Try manually pairing to the device‎‏‎‎‏‎"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‎‏‏‏‏‏‎‏‎‏‏‎‎‏‏‎‎‏‎‎‎‎‏‎‏‎‎‎‏‎‎‎‏‏‎‎‏‎‏‎‏‏‏‎‏‏‏‎‏‎‏‏‏‎‎Try putting the device into pairing mode‎‏‎‎‏‎"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‎‎‎‏‎‏‎‎‏‎‎‏‎‏‏‎‎‏‎‎‎‎‏‎‎‏‏‎‏‎‎‎‎‏‏‎‏‎‎‎‎‏‎‎‏‏‏‏‎‏‏‏‎‏‎‎‎‎Devices within reach‎‏‎‎‏‎"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‏‎‎‎‎‎‏‎‎‎‏‏‎‏‎‏‎‎‏‏‏‎‏‎‏‏‏‎‏‏‏‏‏‎‎‎‏‏‎‎‎‏‎‏‏‏‏‎‏‎‏‏‎‎Devices with your account‎‏‎‎‏‎"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‎‏‎‎‏‏‏‎‎‎‏‎‏‏‎‎‏‎‎‎‎‏‎‏‎‎‏‎‎‏‏‎‏‏‎‏‏‏‏‏‏‏‏‎‏‎‏‎‎‎‏‎‎‎‎Your saved device is available‎‏‎‎‏‎"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‎‎‎‎‎‏‏‏‎‎‎‎‎‎‏‏‎‎‎‎‏‏‏‎‎‎‎‏‏‎‎‎‏‎‏‎‏‏‏‏‎‏‎‎‏‏‎‏‏‎‏‏‎‎‏‏‏‎Nearby‎‏‎‎‏‎"</string>
-    <string name="common_devices" msgid="2635603125608104442">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‏‎‎‏‎‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‎‏‎‎‎‎‏‎‎‏‏‎‏‏‎‎‎‏‏‏‏‏‎‎‎‎‎‏‏‏‏‏‏‎‏‎‎Devices‎‏‎‎‏‎"</string>
-    <string name="common_connecting" msgid="160531481424245303">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‎‎‏‏‏‎‏‎‎‏‎‏‎‎‏‎‏‎‎‎‏‎‎‎‎‎‏‎‎‏‎‏‏‎‎‎‏‏‏‏‎‎‏‏‎‎‏‎‎‎‏‏‎‏‏‏‎Connecting…‎‏‎‎‏‎"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‎‎‎‏‏‏‎‎‏‏‎‏‎‎‏‏‎‎‏‎‏‏‏‏‎‏‏‏‎‏‏‏‎‏‏‎‏‏‏‎‏‏‏‏‎‎‎‎‏‎‏‎Battery: %d%%‎‏‎‎‏‎"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‎‎‏‏‎‏‎‎‎‏‏‎‎‏‏‎‏‏‏‎‏‎‏‎‏‎‏‏‏‎‎‏‎‎‏‏‎‏‏‎‏‎‏‏‏‎‎‎‏‎‎‏‎‏‏‎Done‎‏‎‎‏‎"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‏‎‏‏‏‎‏‏‎‏‏‎‏‎‎‎‏‏‎‏‏‏‎‎‎‎‏‏‎‎‎‏‎‏‎‎‏‏‏‎‏‎‏‏‎‎‎‏‏‎‎‏‎‎‎‎Save‎‏‎‎‏‎"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‎‏‎‎‎‎‎‏‏‏‏‎‎‎‏‏‎‏‎‏‏‏‏‏‎‏‎‏‏‎‏‎‏‏‎‏‏‎‏‎‎‎‏‏‏‏‎‏‎‎‏‏‏‎‏‎Connect‎‏‎‎‏‎"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‎‏‎‎‎‎‏‎‎‎‎‏‏‏‎‏‏‎‏‎‏‏‎‏‏‏‎‎‏‎‏‏‎‎‏‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‎Set up‎‏‎‎‏‎"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‏‏‏‏‎‎‏‎‏‎‏‏‏‎‏‏‎‎‎‏‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‎‏‎‏‏‏‏‏‎‏‎‏‎‏‎‏‎‏‏‏‎‎Settings‎‏‎‎‏‎"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-es-rUS/strings.xml b/nearby/halfsheet/res/values-es-rUS/strings.xml
deleted file mode 100644
index 05eb75d..0000000
--- a/nearby/halfsheet/res/values-es-rUS/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando la configuración…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configuración del dispositivo"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Se conectó el dispositivo"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Se estableció conexión con \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"No se pudo establecer conexión"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"No se pudo establecer conexión"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Intenta vincular el dispositivo manualmente"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prueba poner el dispositivo en el modo de vinculación"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos al alcance"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos con tu cuenta"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"El dispositivo está disponible"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Conectando…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batería: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Listo"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Guardar"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Configuración"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-es/strings.xml b/nearby/halfsheet/res/values-es/strings.xml
deleted file mode 100644
index 7142a1a..0000000
--- a/nearby/halfsheet/res/values-es/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando configuración…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurar el dispositivo"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo conectado"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Conectado a \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"No se ha podido conectar"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"No se ha podido conectar"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prueba a emparejar el dispositivo manualmente"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prueba a poner el dispositivo en modo Emparejamiento"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos al alcance"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos conectados con tu cuenta"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispositivo guardado disponible"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Conectando…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batería: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Hecho"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Guardar"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Ajustes"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-et/strings.xml b/nearby/halfsheet/res/values-et/strings.xml
deleted file mode 100644
index 20a46a5..0000000
--- a/nearby/halfsheet/res/values-et/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Seadistuse käivitamine …"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Seadistage seade"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Seade on ühendatud"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Ühendatud seadmega „%s“"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ühendamine ebaõnnestus"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Ühendust ei õnnestu luua"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Proovige seadmega käsitsi siduda"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Lülitage seade sidumisrežiimi"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Ühendusulatuses olevad seadmed"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Teie kontoga ühendatud seadmed"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Salvestatud seade on saadaval"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Läheduses"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Seadmed"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Ühendamine …"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Aku: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Valmis"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Salvesta"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Ühenda"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Seadistamine"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Seaded"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-eu/strings.xml b/nearby/halfsheet/res/values-eu/strings.xml
deleted file mode 100644
index cd6eb34..0000000
--- a/nearby/halfsheet/res/values-eu/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Konfigurazio-prozesua abiarazten…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfiguratu gailua"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Konektatu da gailua"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"%s gailura konektatuta"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ezin izan da konektatu"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Ezin da konektatu"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Saiatu gailua eskuz parekatzen"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Jarri gailua parekatzeko moduan"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Estaldura-eremuko gailuak"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Kontura konektatutako gailuak"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Gordetako gailua erabilgarri dago"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Gailuak"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Konektatzen…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %% %d"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Eginda"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Gorde"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Konektatu"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Konfiguratu"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Ezarpenak"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-fa/strings.xml b/nearby/halfsheet/res/values-fa/strings.xml
deleted file mode 100644
index 7490e0f..0000000
--- a/nearby/halfsheet/res/values-fa/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"درحال شروع راه‌اندازی…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"راه‌اندازی دستگاه"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"دستگاه متصل شد"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"‏به «%s» متصل شد"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"متصل نشد"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"اتصال برقرار نشد"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"مرتبط‌سازی با دستگاه را به‌صورت دستی امتحان کنید"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"دستگاه را در حالت مرتبط‌سازی قرار دهید"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"دستگاه‌های دردسترس"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"دستگاه‌های متصل به حسابتان"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"دستگاه ذخیره‌شده‌تان دردسترس است"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"اطراف"</string>
-    <string name="common_devices" msgid="2635603125608104442">"دستگاه‌ها"</string>
-    <string name="common_connecting" msgid="160531481424245303">"درحال اتصال…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"‏باتری: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"تمام"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"ذخیره"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"متصل کردن"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"راه‌اندازی"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"تنظیمات"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-fi/strings.xml b/nearby/halfsheet/res/values-fi/strings.xml
deleted file mode 100644
index b488b3e..0000000
--- a/nearby/halfsheet/res/values-fi/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Aloitetaan käyttöönottoa…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Määritä laite"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Laite on yhdistetty"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"%s yhdistetty"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ei yhteyttä"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Yhdistäminen epäonnistui"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Yritä yhdistää laitteet manuaalisesti"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Kokeile asettaa laite parinmuodostustilaan"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Kantoalueella olevat laitteet"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Tiliisi liitetyt laitteet"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Laitteesi on käytettävissä"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Lähellä"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Laitteet"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Yhdistetään…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Akku: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Valmis"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Tallenna"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Yhdistä"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Ota käyttöön"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Asetukset"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-fr-rCA/strings.xml b/nearby/halfsheet/res/values-fr-rCA/strings.xml
deleted file mode 100644
index 9a48890..0000000
--- a/nearby/halfsheet/res/values-fr-rCA/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Démarrage de la configuration…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurer l\'appareil"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Appareil associé"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connecté à l\'appareil suivant : « %s »"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Impossible d\'associer"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Échec de la connexion"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Essayez d\'associer manuellement l\'appareil"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Essayez de mettre l\'appareil en mode d\'association"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Appareils à portée"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Appareils connectés à votre compte"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Appareil enregistré accessible"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"À proximité"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Appareils"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Connexion en cours…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Pile : %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"OK"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Enregistrer"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Associer"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configurer"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Paramètres"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-fr/strings.xml b/nearby/halfsheet/res/values-fr/strings.xml
deleted file mode 100644
index f1263ab..0000000
--- a/nearby/halfsheet/res/values-fr/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Début de la configuration…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurer un appareil"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Appareil associé"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connecté à \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Impossible de se connecter"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Impossible de se connecter"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Essayez d\'associer manuellement l\'appareil"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Essayez de mettre l\'appareil en mode association"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Appareils à portée de main"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Appareils connectés à votre compte"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Appareil enregistré disponible"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"À proximité"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Appareils"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Connexion…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batterie : %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"OK"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Enregistrer"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Connecter"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configurer"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Paramètres"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-gl/strings.xml b/nearby/halfsheet/res/values-gl/strings.xml
deleted file mode 100644
index 91eac4f..0000000
--- a/nearby/halfsheet/res/values-gl/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando configuración…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configura o dispositivo"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Conectouse o dispositivo"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"O dispositivo conectouse a %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Non se puido conectar"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Non se puido conectar"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Proba a vincular o dispositivo manualmente"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Proba a poñer o dispositivo no modo de vinculación"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos dentro do alcance"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos conectados á túa conta"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispositivo gardado dispoñible"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Conectando…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batería: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Feito"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Gardar"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Configuración"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-gu/strings.xml b/nearby/halfsheet/res/values-gu/strings.xml
deleted file mode 100644
index a7a7a2b..0000000
--- a/nearby/halfsheet/res/values-gu/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"સેટઅપ શરૂ કરી રહ્યાં છીએ…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"ડિવાઇસનું સેટઅપ કરો"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"ડિવાઇસ કનેક્ટ કર્યું"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” સાથે કનેક્ટ કરેલું"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"કનેક્ટ કરી શક્યા નથી"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"કનેક્ટ કરી શકાયું નથી"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ડિવાઇસથી મેન્યૂઅલી જોડાણ બનાવવાનો પ્રયાસ કરો"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ડિવાઇસને જોડાણ બનાવવાના મોડમાં રાખવાનો પ્રયાસ કરો"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"પહોંચની અંદરના ડિવાઇસ"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"તમારા એકાઉન્ટ સાથેના ડિવાઇસ"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"તમારું સાચવેલું ડિવાઇસ ઉપલબ્ધ છે"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"શેરિંગ"</string>
-    <string name="common_devices" msgid="2635603125608104442">"ડિવાઇસ"</string>
-    <string name="common_connecting" msgid="160531481424245303">"કનેક્ટ કરી રહ્યાં છીએ…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"બૅટરી: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"થઈ ગયું"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"સાચવો"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"કનેક્ટ કરો"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"સેટઅપ કરો"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"સેટિંગ"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-hi/strings.xml b/nearby/halfsheet/res/values-hi/strings.xml
deleted file mode 100644
index dff9496..0000000
--- a/nearby/halfsheet/res/values-hi/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"सेट अप शुरू किया जा रहा है…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"डिवाइस सेट अप करें"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"डिवाइस कनेक्ट हो गया"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” से कनेक्ट हो गया है"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"कनेक्ट नहीं किया जा सका"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"कनेक्ट नहीं किया जा सका"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"डिवाइस को मैन्युअल तरीके से दूसरे डिवाइस से जोड़ने की कोशिश करें"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"डिवाइस को दूसरे डिवाइस से जोड़ने वाले मोड में रखें"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ऐसे डिवाइस जो रेंज में हैं"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"आपके खाते से जुड़े डिवाइस"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"सेव किया गया डिवाइस उपलब्ध है"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"आस-पास शेयरिंग"</string>
-    <string name="common_devices" msgid="2635603125608104442">"डिवाइस"</string>
-    <string name="common_connecting" msgid="160531481424245303">"कनेक्ट हो रहा है…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"बैटरी: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"हो गया"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"सेव करें"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"कनेक्ट करें"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"सेट अप करें"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"सेटिंग"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-hr/strings.xml b/nearby/halfsheet/res/values-hr/strings.xml
deleted file mode 100644
index 13952b8..0000000
--- a/nearby/halfsheet/res/values-hr/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Pokretanje postavljanja…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Postavi uređaj"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Uređaj je povezan"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Povezano s uređajem %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezivanje nije uspjelo"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Povezivanje nije uspjelo"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Pokušajte ručno upariti s uređajem"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Pokušajte staviti uređaj u način uparivanja"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Uređaji u dometu"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Uređaji s vašim računom"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Spremljeni je uređaj dostupan"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"U blizini"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Uređaji"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Povezivanje…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Baterija: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Gotovo"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Spremi"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Postavi"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Postavke"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-hu/strings.xml b/nearby/halfsheet/res/values-hu/strings.xml
deleted file mode 100644
index 3d810d4..0000000
--- a/nearby/halfsheet/res/values-hu/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Beállítás megkezdése…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Eszköz beállítása"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Eszköz csatlakoztatva"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Csatlakoztatva a következőhöz: %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nem sikerült csatlakozni"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nem sikerült csatlakozni"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Próbálkozzon az eszköz kézi párosításával"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Próbálja meg bekapcsolni az eszközön a párosítási módot"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Elérhető eszközök"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"A fiókjával társított eszközök"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"A mentett eszköze elérhető"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Közeli"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Eszközök"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Csatlakozás…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Akkumulátor: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Kész"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Mentés"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Csatlakozás"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Beállítás"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Beállítások"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-hy/strings.xml b/nearby/halfsheet/res/values-hy/strings.xml
deleted file mode 100644
index 57b9256..0000000
--- a/nearby/halfsheet/res/values-hy/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Կարգավորում…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Կարգավորեք սարքը"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Սարքը զուգակցվեց"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"«%s» սարքի հետ կապը հաստատվեց"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Չհաջողվեց միանալ"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Չհաջողվեց կապ հաստատել"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Փորձեք ձեռքով զուգակցել սարքը"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Փորձեք սարքում միացնել զուգակցման ռեժիմը"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Սարքեր հասանելիության տիրույթում"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Ձեր հաշվի հետ կապված սարքեր"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Պահված սարքը հասանելի է"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Մոտակայքում"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Սարքեր"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Միացում…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Մարտկոցի լիցքը՝ %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Պատրաստ է"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Պահել"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Միանալ"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Կարգավորել"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Կարգավորումներ"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-in/strings.xml b/nearby/halfsheet/res/values-in/strings.xml
deleted file mode 100644
index c665572..0000000
--- a/nearby/halfsheet/res/values-in/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Memulai Penyiapan …"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Siapkan perangkat"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Perangkat terhubung"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Terhubung ke “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Tidak dapat terhubung"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Tidak dapat terhubung"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Coba sambungkan ke perangkat secara manual"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Coba masukkan perangkat ke dalam mode penyambungan"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Perangkat dalam jangkauan"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Perangkat dengan akun Anda"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Perangkat tersimpan tersedia"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Berbagi Langsung"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Perangkat"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Menghubungkan …"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Daya baterai: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Selesai"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Simpan"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Hubungkan"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Siapkan"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Setelan"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-is/strings.xml b/nearby/halfsheet/res/values-is/strings.xml
deleted file mode 100644
index 04a4de4..0000000
--- a/nearby/halfsheet/res/values-is/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Ræsir uppsetningu…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Uppsetning tækis"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Tækið er tengt"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Tengt við „%s“"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Tenging mistókst"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Tenging tókst ekki"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prófaðu að para tækið handvirkt"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prófaðu að kveikja á pörunarstillingu í tækinu"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Nálæg tæki"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Tæki tengd reikningnum þínum"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Vistaða tækið er tiltækt"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nálægt"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Tæki"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Tengist…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Rafhlaða: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Lokið"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Vista"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Tengja"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Setja upp"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Stillingar"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-it/strings.xml b/nearby/halfsheet/res/values-it/strings.xml
deleted file mode 100644
index 2ffe268..0000000
--- a/nearby/halfsheet/res/values-it/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Avvio della configurazione…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configura dispositivo"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo connesso"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connesso a \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Impossibile connettere"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Impossibile connettersi"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prova a eseguire l\'accoppiamento manuale con il dispositivo"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prova a impostare il dispositivo sulla modalità di accoppiamento"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivi nelle vicinanze"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivi collegati all\'account"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Disposit. salvato disponibile"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nelle vicinanze"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Dispositivi"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Connessione in corso"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batteria: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Fine"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Salva"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Connetti"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configura"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Impostazioni"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-iw/strings.xml b/nearby/halfsheet/res/values-iw/strings.xml
deleted file mode 100644
index 61724f0..0000000
--- a/nearby/halfsheet/res/values-iw/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ההגדרה מתבצעת…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"הגדרת המכשיר"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"המכשיר מחובר"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"‏יש חיבור אל \'%s\'"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"לא ניתן להתחבר"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"לא ניתן להתחבר"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"כדאי לנסות לבצע התאמה ידנית למכשיר"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"כדאי לנסות להעביר את המכשיר למצב התאמה"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"מכשירים בהישג יד"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"מכשירים המחוברים לחשבון שלך"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"המכשיר ששמרת זמין"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"בקרבת מקום"</string>
-    <string name="common_devices" msgid="2635603125608104442">"מכשירים"</string>
-    <string name="common_connecting" msgid="160531481424245303">"מתבצעת התחברות…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"‏טעינת הסוללה: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"סיום"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"שמירה"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"התחברות"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"הגדרה"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"הגדרות"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ja/strings.xml b/nearby/halfsheet/res/values-ja/strings.xml
deleted file mode 100644
index 3168bda..0000000
--- a/nearby/halfsheet/res/values-ja/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"セットアップを開始中…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"デバイスのセットアップ"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"デバイス接続完了"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"「%s」に接続しました"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"接続エラー"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"接続できません"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"手動でデバイスとペア設定してみてください"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"デバイスをペア設定モードにしてください"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"近接するデバイス"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"アカウントと接続済みのデバイス"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"保存済みのデバイスがあります"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"周辺ユーザーとの共有"</string>
-    <string name="common_devices" msgid="2635603125608104442">"デバイス"</string>
-    <string name="common_connecting" msgid="160531481424245303">"接続しています…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"バッテリー: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"完了"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"保存"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"接続"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"セットアップ"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"設定"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ka/strings.xml b/nearby/halfsheet/res/values-ka/strings.xml
deleted file mode 100644
index a9ee648..0000000
--- a/nearby/halfsheet/res/values-ka/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"დაყენება იწყება…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"მოწყობილობის დაყენება"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"მოწყობილობა დაკავშირებულია"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"დაკავშირებულია „%s“-თან"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"დაკავშირება ვერ მოხერხდა"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"დაკავშირება ვერ ხერხდება"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ცადეთ მოწყობილობასთან ხელით დაწყვილება"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ცადეთ მოწყობილობის გადაყვანა დაწყვილების რეჟიმზე"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ხელმისაწვდომი მოწყობილობები"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"მოწყობილობები თქვენი ანგარიშით"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"შენახული მოწყობილობა ხელმისაწვდომია"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"ახლომახლო"</string>
-    <string name="common_devices" msgid="2635603125608104442">"მოწყობილობები"</string>
-    <string name="common_connecting" msgid="160531481424245303">"მიმდინარეობს დაკავშირება…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ბატარეა: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"მზადაა"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"შენახვა"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"დაკავშირება"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"დაყენება"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"პარამეტრები"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-kk/strings.xml b/nearby/halfsheet/res/values-kk/strings.xml
deleted file mode 100644
index 6e1a0bd..0000000
--- a/nearby/halfsheet/res/values-kk/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Реттеу басталуда…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Құрылғыны реттеу"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Құрылғы байланыстырылды"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\"%s\" құрылғысымен байланыстырылды"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Қосылмады"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Қосылу мүмкін емес"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Құрылғыны қолмен жұптап көріңіз."</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Құрылғыны жұптау режиміне қойып көріңіз."</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Қолжетімді құрылғылар"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Аккаунтпен байланыстырылған құрылғылар"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Сақталған құрылғы қолжетімді"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Маңайдағы"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Құрылғылар"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Жалғанып жатыр…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Батарея: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Дайын"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Сақтау"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Қосу"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Реттеу"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Параметрлер"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-km/strings.xml b/nearby/halfsheet/res/values-km/strings.xml
deleted file mode 100644
index deb6504..0000000
--- a/nearby/halfsheet/res/values-km/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"កំពុងចាប់ផ្ដើម​រៀបចំ…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"រៀបចំ​ឧបករណ៍"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"បានភ្ជាប់ឧបករណ៍"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"បានភ្ជាប់ជាមួយ “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"មិន​អាចភ្ជាប់​បានទេ"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"មិនអាច​ភ្ជាប់​បានទេ"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"សាកល្បង​ផ្គូផ្គង​ដោយ​ផ្ទាល់​ទៅឧបករណ៍"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"សាកល្បង​កំណត់​ឧបករណ៍​ឱ្យ​ប្រើ​មុខងារ​​ផ្គូផ្គង"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ឧបករណ៍​នៅ​ក្បែរដៃ"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ឧបករណ៍​ភ្ជាប់​ជាមួយ​គណនី​របស់អ្នក​"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"ឧបករណ៍ដែលអ្នកបានរក្សាទុកអាចប្រើបានហើយ"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"នៅ​ជិត"</string>
-    <string name="common_devices" msgid="2635603125608104442">"ឧបករណ៍"</string>
-    <string name="common_connecting" msgid="160531481424245303">"កំពុងភ្ជាប់…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ថ្ម៖ %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"រួចរាល់"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"រក្សាទុក"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"ភ្ជាប់"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"រៀបចំ"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"ការកំណត់"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-kn/strings.xml b/nearby/halfsheet/res/values-kn/strings.xml
deleted file mode 100644
index 87b4fe3..0000000
--- a/nearby/halfsheet/res/values-kn/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ಸೆಟಪ್ ಪ್ರಾರಂಭಿಸಲಾಗುತ್ತಿದೆ…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"ಸಾಧನವನ್ನು ಸೆಟಪ್ ಮಾಡಿ"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"ಸಾಧನವನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲಾಗಿದೆ"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” ಗೆ ಕನೆಕ್ಟ್ ಮಾಡಲಾಗಿದೆ"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ಸಾಧನಕ್ಕೆ ಹಸ್ತಚಾಲಿತವಾಗಿ ಜೋಡಿಸಲು ಪ್ರಯತ್ನಿಸಿ"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ಜೋಡಿಸುವಿಕೆ ಮೋಡ್‌ನಲ್ಲಿ ಸಾಧನವನ್ನು ಇರಿಸಲು ಪ್ರಯತ್ನಿಸಿ"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ವ್ಯಾಪ್ತಿಯಲ್ಲಿರುವ ಸಾಧನಗಳು"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ನಿಮ್ಮ ಖಾತೆಗೆ ಸಂಪರ್ಕಿತವಾಗಿರುವ ಸಾಧನಗಳು"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"ಉಳಿಸಲಾದ ನಿಮ್ಮ ಸಾಧನವು ಲಭ್ಯವಿದೆ"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"ಸಮೀಪದಲ್ಲಿರುವುದು"</string>
-    <string name="common_devices" msgid="2635603125608104442">"ಸಾಧನಗಳು"</string>
-    <string name="common_connecting" msgid="160531481424245303">"ಕನೆಕ್ಟ್ ಆಗುತ್ತಿದೆ…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ಬ್ಯಾಟರಿ: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"ಮುಗಿದಿದೆ"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"ಉಳಿಸಿ"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"ಕನೆಕ್ಟ್ ಮಾಡಿ"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"ಸೆಟಪ್ ಮಾಡಿ"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ko/strings.xml b/nearby/halfsheet/res/values-ko/strings.xml
deleted file mode 100644
index 37d50da..0000000
--- a/nearby/halfsheet/res/values-ko/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"설정을 시작하는 중…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"기기 설정"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"기기 연결됨"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\'%s\'에 연결됨"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"연결할 수 없음"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"연결할 수 없음"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"기기에 수동으로 페어링을 시도해 보세요."</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"기기를 페어링 모드로 전환하세요."</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"연결 가능 기기"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"내 계정에 연결된 기기"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"저장된 기기 사용 가능"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
-    <string name="common_devices" msgid="2635603125608104442">"기기"</string>
-    <string name="common_connecting" msgid="160531481424245303">"연결 중…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"배터리: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"완료"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"저장"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"연결"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"설정"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"설정"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ky/strings.xml b/nearby/halfsheet/res/values-ky/strings.xml
deleted file mode 100644
index b6aa409..0000000
--- a/nearby/halfsheet/res/values-ky/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Жөндөлүп баштады…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Түзмөктү жөндөө"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Түзмөк туташты"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” түзмөгүнө туташты"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Туташпай койду"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Туташпай жатат"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Түзмөккө кол менен жупташтырып көрүңүз"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Түзмөктү Байланыштыруу режимине коюп көрүңүз"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Жеткиликтүү түзмөктөр"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Аккаунтуңузга кирип турган түзмөктөр"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Сакталган түзмөгүңүз жеткиликтүү"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Жакын жерде"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Түзмөктөр"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Туташууда…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Батарея: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Бүттү"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Сактоо"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Туташуу"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Жөндөө"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Параметрлер"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-lo/strings.xml b/nearby/halfsheet/res/values-lo/strings.xml
deleted file mode 100644
index cbc7601..0000000
--- a/nearby/halfsheet/res/values-lo/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ກຳລັງເລີ່ມການຕັ້ງຄ່າ…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"ຕັ້ງຄ່າອຸປະກອນ"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"ເຊື່ອມຕໍ່ອຸປະກອນແລ້ວ"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"ເຊື່ອມຕໍ່ກັບ “%s” ແລ້ວ"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ລອງຈັບຄູ່ອຸປະກອນດ້ວຍຕົນເອງ"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ໃຫ້ລອງຕັ້ງອຸປະກອນເປັນໂໝດຈັບຄູ່"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ອຸປະກອນພ້ອມໃຫ້ເຂົ້າເຖິງ"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ອຸປະກອນທີ່ມີບັນຊີຂອງທ່ານ"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"ອຸປະກອນທີ່ບັນທຶກໄວ້ຂອງທ່ານສາມາດໃຊ້ໄດ້"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"ໃກ້ຄຽງ"</string>
-    <string name="common_devices" msgid="2635603125608104442">"ອຸປະກອນ"</string>
-    <string name="common_connecting" msgid="160531481424245303">"ກຳລັງເຊື່ອມຕໍ່…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ແບັດເຕີຣີ: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"ແລ້ວໆ"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"ບັນທຶກ"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"ເຊື່ອມຕໍ່"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"ຕັ້ງຄ່າ"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"ການຕັ້ງຄ່າ"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-lt/strings.xml b/nearby/halfsheet/res/values-lt/strings.xml
deleted file mode 100644
index 29a9bc5..0000000
--- a/nearby/halfsheet/res/values-lt/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Pradedama sąranka…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Įrenginio nustatymas"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Įrenginys prijungtas"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Prisijungta prie „%s“"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Prisijungti nepavyko"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nepavyko prisijungti"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Pabandykite neautomatiškai susieti įrenginį"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Pabandykite vėl įgalinti įrenginio susiejimo režimą"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Lengvai pasiekiami įrenginiai"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Įrenginiai su jūsų paskyra"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Išsaugotas įrenginys pasiekiamas"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Netoliese"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Įrenginiai"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Prisijungiama…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Akumuliatorius: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Atlikta"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Išsaugoti"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Prisijungti"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Nustatyti"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Nustatymai"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-lv/strings.xml b/nearby/halfsheet/res/values-lv/strings.xml
deleted file mode 100644
index 9573357..0000000
--- a/nearby/halfsheet/res/values-lv/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Tiek sākta iestatīšana…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Iestatiet ierīci"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Ierīce ir pievienota"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Izveidots savienojums ar ierīci “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nevarēja izveidot savienojumu"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nevar izveidot savienojumu."</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Mēģiniet manuāli izveidot savienojumu pārī ar ierīci."</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Ieslēdziet ierīcē režīmu savienošanai pārī"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Sasniedzamas ierīces"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Ierīces ar jūsu kontu"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Jūsu saglabātā ierīce pieejama"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Tuvumā"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Ierīces"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Savienojuma izveide…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Akumulatora uzlādes līmenis: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Gatavs"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Saglabāt"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Izveidot savienojumu"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Iestatīt"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Iestatījumi"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-mk/strings.xml b/nearby/halfsheet/res/values-mk/strings.xml
deleted file mode 100644
index 693f112..0000000
--- a/nearby/halfsheet/res/values-mk/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Се започнува со поставување…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Поставете го уредот"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Уредот е поврзан"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Поврзан со „%s“"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Не може да се поврзе"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Не може да се поврзе"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Обидете се рачно да се спарите со уредот"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Пробајте да го ставите уредот во режим на спарување"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Уреди на дофат"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Уреди поврзани со вашата сметка"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Вашиот зачуван уред е достапен"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Во близина"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Уреди"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Се поврзува…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Батерија: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Зачувај"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Поврзи"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Поставете"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Поставки"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ml/strings.xml b/nearby/halfsheet/res/values-ml/strings.xml
deleted file mode 100644
index 56a2db2..0000000
--- a/nearby/halfsheet/res/values-ml/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"സജ്ജീകരിക്കൽ ആരംഭിക്കുന്നു…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"ഉപകരണം സജ്ജീകരിക്കുക"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"ഉപകരണം കണക്റ്റ് ചെയ്‌തു"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” എന്നതിലേക്ക് കണക്റ്റ് ചെയ്‌തു"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"കണക്റ്റ് ചെയ്യാനായില്ല"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"കണക്റ്റ് ചെയ്യാനാകുന്നില്ല"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ഉപകരണം ജോടിയാക്കാൻ നേരിട്ട് ശ്രമിക്കുക"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ഉപകരണം ജോഡിയാക്കൽ മോഡിലേക്ക് മാറ്റിയ ശേഷം ശ്രമിക്കുക"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"പരിധിയിലുള്ള ഉപകരണങ്ങൾ"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"അക്കൗണ്ട് ഉപയോഗിക്കുന്ന ഉപകരണങ്ങൾ"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"സംരക്ഷിച്ച ഉപകരണം ലഭ്യമാണ്"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"സമീപമുള്ളവ"</string>
-    <string name="common_devices" msgid="2635603125608104442">"ഉപകരണങ്ങൾ"</string>
-    <string name="common_connecting" msgid="160531481424245303">"കണക്റ്റ് ചെയ്യുന്നു…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ബാറ്ററി: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"പൂർത്തിയായി"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"സംരക്ഷിക്കുക"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"കണക്റ്റ് ചെയ്യുക"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"സജ്ജീകരിക്കുക"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"ക്രമീകരണം"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-mn/strings.xml b/nearby/halfsheet/res/values-mn/strings.xml
deleted file mode 100644
index 5a72ce3..0000000
--- a/nearby/halfsheet/res/values-mn/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Тохируулгыг эхлүүлж байна…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Төхөөрөмж тохируулах"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Төхөөрөмж холбогдсон"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s”-д холбогдсон"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Холбогдож чадсангүй"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Холбогдох боломжгүй байна"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Төхөөрөмжийг гар аргаар хослуулна уу"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Төхөөрөмжийг хослуулах горимд оруулахаар оролдоно уу"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Хүрээн дэх төхөөрөмжүүд"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Таны бүртгэлтэй холбогдсон төхөөрөмж"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Таны хадгалсан төхөөрөмж бэлэн байна"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Ойролцоо"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Төхөөрөмж"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Холбогдож байна…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Батарей: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Болсон"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Хадгалах"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Холбох"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Тохируулах"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Тохиргоо"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-mr/strings.xml b/nearby/halfsheet/res/values-mr/strings.xml
deleted file mode 100644
index 3eeb0ec..0000000
--- a/nearby/halfsheet/res/values-mr/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"सेटअप सुरू करत आहे…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"डिव्हाइस सेट करा"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"डिव्हाइस कनेक्ट केले आहे"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\"%s\" शी कनेक्ट केले आहे"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"कनेक्ट करता आले नाही"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"कनेक्ट करता आले नाही"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"डिव्हाइसशी मॅन्युअली पेअर करण्याचा प्रयत्न करा"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"डिव्हाइसला पेअरिंग मोडमध्ये ठेवण्याचा प्रयत्न करा"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"पुरेसे जवळ असलेले डिव्हाइस"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"तुमच्या खात्‍यासह असलेली डिव्‍हाइस"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"सेव्ह केलेले डिव्हाइस उपलब्ध"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"जवळपास"</string>
-    <string name="common_devices" msgid="2635603125608104442">"डिव्हाइस"</string>
-    <string name="common_connecting" msgid="160531481424245303">"कनेक्ट करत आहे…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"बॅटरी: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"पूर्ण झाले"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"सेव्ह करा"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"कनेक्ट करा"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"सेट करा"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"सेटिंग्ज"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ms/strings.xml b/nearby/halfsheet/res/values-ms/strings.xml
deleted file mode 100644
index 0af903d..0000000
--- a/nearby/halfsheet/res/values-ms/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Memulakan Persediaan…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Sediakan peranti"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Peranti disambungkan"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Disambungkan kepada “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Tidak dapat menyambung"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Tidak dapat menyambung"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Cuba gandingkan dengan peranti secara manual"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Cuba letakkan peranti dalam mod penggandingan"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Peranti dalam capaian"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Peranti dengan akaun anda"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Peranti disimpan anda tersedia"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Berdekatan"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Peranti"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Menyambung…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Bateri: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Selesai"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Simpan"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Sambung"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Sediakan"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Tetapan"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-my/strings.xml b/nearby/halfsheet/res/values-my/strings.xml
deleted file mode 100644
index 306538f..0000000
--- a/nearby/halfsheet/res/values-my/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"စနစ်ထည့်သွင်းခြင်း စတင်နေသည်…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"စက်ကို စနစ်ထည့်သွင်းရန်"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"စက်ကို ချိတ်ဆက်လိုက်ပြီ"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” သို့ ချိတ်ဆက်ထားသည်"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"ချိတ်ဆက်၍မရပါ"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"ချိတ်ဆက်၍ မရပါ"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"စက်ကို ကိုယ်တိုင်တွဲချိတ်ကြည့်ပါ"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"စက်ကို တွဲချိတ်ခြင်းမုဒ်သို့ ထားကြည့်ပါ"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"လက်လှမ်းမီသည့် စက်များ"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"သင့်အကောင့်ရှိသည့် စက်များ"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"သင်သိမ်းထားသောစက် ရပါပြီ"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"အနီးတစ်ဝိုက်"</string>
-    <string name="common_devices" msgid="2635603125608104442">"စက်များ"</string>
-    <string name="common_connecting" msgid="160531481424245303">"ချိတ်ဆက်နေသည်…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ဘက်ထရီ− %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"ပြီးပြီ"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"သိမ်းရန်"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"ချိတ်ဆက်ရန်"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"စနစ်ထည့်သွင်းရန်"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"ဆက်တင်များ"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-nb/strings.xml b/nearby/halfsheet/res/values-nb/strings.xml
deleted file mode 100644
index 72a2ab7..0000000
--- a/nearby/halfsheet/res/values-nb/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starter konfigureringen …"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfigurer enheten"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Enheten er tilkoblet"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Koblet til «%s»"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Kunne ikke koble til"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Kan ikke koble til"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prøv manuell tilkobling til enheten"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prøv å sette enheten i tilkoblingsmodus"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Enheter innen rekkevidde"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Enheter med kontoen din"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Lagret enhet tilgjengelig"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"I nærheten"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Enheter"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Kobler til …"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batteri: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Ferdig"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Lagre"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Koble til"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Konfigurer"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Innstillinger"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ne/strings.xml b/nearby/halfsheet/res/values-ne/strings.xml
deleted file mode 100644
index 2956768..0000000
--- a/nearby/halfsheet/res/values-ne/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"सेटअप प्रक्रिया सुरु गरिँदै छ…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"डिभाइस सेटअप गर्नुहोस्"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"डिभाइस कनेक्ट गरियो"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\"%s\" मा कनेक्ट गरिएको छ"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"कनेक्ट गर्न सकिएन"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"कनेक्ट गर्न सकिएन"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"म्यानुअल तरिकाले डिभाइस कनेक्ट गरी हेर्नुहोस्"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"डिभाइस कनेक्ट गर्ने मोडमा राखी हेर्नुहोस्"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"कनेक्ट गर्न सकिने नजिकैका डिभाइसहरू"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"तपाईंको खातामा कनेक्ट गरिएका डिभाइस"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"तपाईंले सेभ गरेको डिभाइस उपलब्ध छ"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"नजिकै"</string>
-    <string name="common_devices" msgid="2635603125608104442">"डिभाइसहरू"</string>
-    <string name="common_connecting" msgid="160531481424245303">"कनेक्ट गरिँदै छ…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ब्याट्री: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"सम्पन्न भयो"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"सेभ गर्नुहोस्"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"कनेक्ट गर्नुहोस्"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"सेटअप गर्नुहोस्"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"सेटिङ"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-night/colors.xml b/nearby/halfsheet/res/values-night/colors.xml
deleted file mode 100644
index 69b832a..0000000
--- a/nearby/halfsheet/res/values-night/colors.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools">
-    <!-- Use original background color -->
-    <color name="fast_pair_notification_background">#00000000</color>
-    <!-- Ignores NewApi as below system colors are available since API 31, and HalfSheet is always
-         running on T+ even though it has min_sdk 30 to match its containing APEX -->
-    <color name="fast_pair_half_sheet_button_color" tools:ignore="NewApi">
-        @android:color/system_accent1_100</color>
-    <color name="fast_pair_half_sheet_button_text" tools:ignore="NewApi">
-        @android:color/system_neutral1_50</color>
-    <color name="fast_pair_half_sheet_button_accent_text" tools:ignore="NewApi">
-        @android:color/system_neutral1_900</color>
-    <color name="fast_pair_progress_color" tools:ignore="NewApi">
-        @android:color/system_accent1_600</color>
-    <color name="fast_pair_half_sheet_subtitle_color" tools:ignore="NewApi">
-        @android:color/system_neutral2_200</color>
-    <color name="fast_pair_half_sheet_text_color" tools:ignore="NewApi">
-        @android:color/system_neutral1_50</color>
-    <color name="fast_pair_half_sheet_background" tools:ignore="NewApi">
-        @android:color/system_neutral1_800</color>
-
-    <!-- Fast Pair -->
-    <color name="fast_pair_primary_text">#FFFFFF</color>
-    <color name="fast_pair_notification_image_outline">#24FFFFFF</color>
-    <color name="fast_pair_battery_level_low">#F6AEA9</color>
-
-</resources>
-
diff --git a/nearby/halfsheet/res/values-nl/strings.xml b/nearby/halfsheet/res/values-nl/strings.xml
deleted file mode 100644
index e956116..0000000
--- a/nearby/halfsheet/res/values-nl/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Instellen starten…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Apparaat instellen"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Apparaat verbonden"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Verbonden met %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Kan geen verbinding maken"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Kan geen verbinding maken"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Probeer handmatig met het apparaat te koppelen"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Activeer de koppelingsstand van het apparaat"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Apparaten binnen bereik"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Apparaten met je account"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Opgeslagen apparaat beschikbaar"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"In de buurt"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Apparaten"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Verbinden…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batterij: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Klaar"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Opslaan"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Verbinden"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Instellen"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Instellingen"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-or/strings.xml b/nearby/halfsheet/res/values-or/strings.xml
deleted file mode 100644
index 0ec472c..0000000
--- a/nearby/halfsheet/res/values-or/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ସେଟଅପ ଆରମ୍ଭ କରାଯାଉଛି…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"ଡିଭାଇସ ସେଟ ଅପ କରନ୍ତୁ"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"ଡିଭାଇସ ସଂଯୁକ୍ତ ହୋଇଛି"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” ସହ କନେକ୍ଟ କରାଯାଇଛି"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"ସଂଯୋଗ କରାଯାଇପାରିଲା ନାହିଁ"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"କନେକ୍ଟ କରିବାରେ ଅସମର୍ଥ"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ଡିଭାଇସ ସହ ମାନୁଆଲୀ ପେୟାର କରିବା ପାଇଁ ଚେଷ୍ଟା କରନ୍ତୁ"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ଡିଭାଇସକୁ ପେୟାରିଂ ମୋଡରେ ରଖିବାକୁ ଚେଷ୍ଟା କରନ୍ତୁ"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ପହଞ୍ଚ ଭିତରେ ଥିବା ଡିଭାଇସଗୁଡ଼ିକ"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ଆପଣଙ୍କ ଆକାଉଣ୍ଟ ସହ ଥିବା ଡିଭାଇସଗୁଡ଼ିକ"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"ସେଭ ଥିବା ଆପଣଙ୍କ ଡିଭାଇସ ଉପଲବ୍ଧ"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"ଆଖପାଖର"</string>
-    <string name="common_devices" msgid="2635603125608104442">"ଡିଭାଇସଗୁଡ଼ିକ"</string>
-    <string name="common_connecting" msgid="160531481424245303">"କନେକ୍ଟ କରାଯାଉଛି…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ବେଟେରୀ: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"ହୋଇଗଲା"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"ସେଭ କରନ୍ତୁ"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"ସଂଯୋଗ କରନ୍ତୁ"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"ସେଟ ଅପ କରନ୍ତୁ"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"ସେଟିଂସ"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-pa/strings.xml b/nearby/halfsheet/res/values-pa/strings.xml
deleted file mode 100644
index 4eb0553..0000000
--- a/nearby/halfsheet/res/values-pa/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ਸੈੱਟਅੱਪ ਸ਼ੁਰੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"ਡੀਵਾਈਸ ਸੈੱਟਅੱਪ ਕਰੋ"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"ਡੀਵਾਈਸ ਕਨੈਕਟ ਕੀਤਾ ਗਿਆ"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” ਨਾਲ ਕਨੈਕਟ ਹੈ"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"ਕਨੈਕਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"ਕਨੈਕਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ਡੀਵਾਈਸ ਨਾਲ ਹੱਥੀਂ ਜੋੜਾਬੱਧ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ਡੀਵਾਈਸ ਨੂੰ ਜੋੜਾਬੱਧਕਰਨ ਮੋਡ ਵਿੱਚ ਰੱਖਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ਡੀਵਾਈਸ ਜੋ ਪਹੁੰਚ ਅੰਦਰ ਹਨ"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ਤੁਹਾਡੇ ਖਾਤੇ ਵਾਲੇ ਡੀਵਾਈਸ"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"ਰੱਖਿਅਤ ਕੀਤਾ ਡੀਵਾਈਸ ਉਪਲਬਧ ਹੈ"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"ਨਜ਼ਦੀਕੀ"</string>
-    <string name="common_devices" msgid="2635603125608104442">"ਡੀਵਾਈਸ"</string>
-    <string name="common_connecting" msgid="160531481424245303">"ਕਨੈਕਟ ਹੋ ਰਿਹਾ ਹੈ…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"ਬੈਟਰੀ: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"ਹੋ ਗਿਆ"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"ਰੱਖਿਅਤ ਕਰੋ"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"ਕਨੈਕਟ ਕਰੋ"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"ਸੈੱਟਅੱਪ ਕਰੋ"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"ਸੈਟਿੰਗਾਂ"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-pl/strings.xml b/nearby/halfsheet/res/values-pl/strings.xml
deleted file mode 100644
index 5082e18..0000000
--- a/nearby/halfsheet/res/values-pl/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Rozpoczynam konfigurowanie…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Skonfiguruj urządzenie"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Urządzenie połączone"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Połączono z: „%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nie udało się połączyć"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nie udało się połączyć"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Spróbuj ręcznie sparować urządzenie"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Włącz na urządzeniu tryb parowania"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Urządzenia w zasięgu"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Urządzenia z Twoim kontem"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Zapisane urządzenie dostępne"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"W pobliżu"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Urządzenia"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Łączę…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Gotowe"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Zapisz"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Połącz"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Skonfiguruj"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Ustawienia"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-pt-rBR/strings.xml b/nearby/halfsheet/res/values-pt-rBR/strings.xml
deleted file mode 100644
index 15d29d2..0000000
--- a/nearby/halfsheet/res/values-pt-rBR/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando a configuração…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurar dispositivo"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo conectado"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Conectado ao dispositivo \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Erro ao conectar"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Não foi possível conectar"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Tente parear o dispositivo manualmente"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Coloque o dispositivo no modo de pareamento"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos ao alcance"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos conectados à sua conta"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispositivo salvo disponível"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Por perto"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Conectando…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Concluído"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Salvar"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Configurações"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-pt-rPT/strings.xml b/nearby/halfsheet/res/values-pt-rPT/strings.xml
deleted file mode 100644
index ab8decf..0000000
--- a/nearby/halfsheet/res/values-pt-rPT/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"A iniciar a configuração…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configure o dispositivo"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo ligado"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Ligado a \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Não foi possível ligar"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Impossível ligar"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Experimente sincronizar manualmente com o dispositivo"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Experimente ativar o modo de sincronização no dispositivo"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos ao alcance"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos com a sua conta"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Disposit. guardado disponível"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Partilha"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
-    <string name="common_connecting" msgid="160531481424245303">"A ligar…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Concluir"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Guardar"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Ligar"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Definições"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-pt/strings.xml b/nearby/halfsheet/res/values-pt/strings.xml
deleted file mode 100644
index 15d29d2..0000000
--- a/nearby/halfsheet/res/values-pt/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando a configuração…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurar dispositivo"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo conectado"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Conectado ao dispositivo \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Erro ao conectar"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Não foi possível conectar"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Tente parear o dispositivo manualmente"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Coloque o dispositivo no modo de pareamento"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos ao alcance"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos conectados à sua conta"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispositivo salvo disponível"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Por perto"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Conectando…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Concluído"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Salvar"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Configurações"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ro/strings.xml b/nearby/halfsheet/res/values-ro/strings.xml
deleted file mode 100644
index 0335d01..0000000
--- a/nearby/halfsheet/res/values-ro/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Începe configurarea…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurează dispozitivul"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispozitivul s-a conectat"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Conectat la %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nu s-a putut conecta"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nu se poate conecta"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Încearcă asocierea manuală cu dispozitivul"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Încearcă să pui dispozitivul în modul de asociere"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispozitive în vecinătate"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispozitive conectate cu contul"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispozitivul este disponibil"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"În apropiere"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Dispozitive"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Se conectează…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Baterie: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Gata"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Salvează"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Conectează"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Configurează"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Setări"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ru/strings.xml b/nearby/halfsheet/res/values-ru/strings.xml
deleted file mode 100644
index d90b644..0000000
--- a/nearby/halfsheet/res/values-ru/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Начинаем настройку…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Настройка устройства"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Устройство подключено"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Подключено к устройству \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ошибка подключения"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Не удалось подключиться"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Попробуйте подключиться к устройству вручную."</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Переведите устройство в режим подключения."</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Устройства в зоне охвата"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Устройства с вашим аккаунтом"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Доступно сохранен. устройство"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Мое окружение"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Устройства"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Подключение…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Батарея: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Сохранить"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Подключить"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Настроить"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Открыть настройки"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-si/strings.xml b/nearby/halfsheet/res/values-si/strings.xml
deleted file mode 100644
index c9b96bb..0000000
--- a/nearby/halfsheet/res/values-si/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"පිහිටුවීම ආරම්භ කරමින්…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"උපාංගය පිහිටුවන්න"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"උපාංගය සම්බන්ධිතයි"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” වෙත සම්බන්ධ විය"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"සම්බන්ධ කළ නොහැකි විය"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"සම්බන්ධ වීමට නොහැකි වේ"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"උපාංගය වෙත හස්තීයව යුගල කිරීමට උත්සාහ කරන්න"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"උපාංගය යුගල කිරීමේ ප්‍රකාරයට දැමීමට උත්සාහ කරන්න"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ළඟා විය හැකි උපාංග"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ඔබේ ගිණුම සමග උපාංග"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"ඔබේ සුරැකි උපාංගය ලබා ගත හැක"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"අවට"</string>
-    <string name="common_devices" msgid="2635603125608104442">"උපාංග"</string>
-    <string name="common_connecting" msgid="160531481424245303">"සම්බන්ධ වෙමින්…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"බැටරිය: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"නිමයි"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"සුරකින්න"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"සම්බන්ධ කරන්න"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"පිහිටුවන්න"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"සැකසීම්"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-sk/strings.xml b/nearby/halfsheet/res/values-sk/strings.xml
deleted file mode 100644
index 7938211..0000000
--- a/nearby/halfsheet/res/values-sk/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Spúšťa sa nastavenie…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Nastavte zariadenie"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Zariadenie je pripojené"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Pripojené k zariadeniu %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nepodarilo sa pripojiť"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nepodarilo sa pripojiť"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Skúste zariadenie spárovať ručne"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prepnite zariadenie do párovacieho režimu"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Zariadenia v dosahu"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Zariadenia s vaším účtom"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Uložené zariadenie je dostupné"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Nablízku"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Zariadenia"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Pripája sa…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batéria: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Hotovo"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Uložiť"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Pripojiť"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Nastaviť"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Nastavenia"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-sl/strings.xml b/nearby/halfsheet/res/values-sl/strings.xml
deleted file mode 100644
index 9e9357c..0000000
--- a/nearby/halfsheet/res/values-sl/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Začetek nastavitve …"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Nastavitev naprave"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Naprava je povezana"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Povezano z napravo »%s«"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezava ni mogoča"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Povezave ni mogoče vzpostaviti"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Poskusite ročno seznaniti napravo."</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Poskusite napravo preklopiti v način za seznanjanje."</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Naprave znotraj dosega"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Naprave z vašim računom"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Shranjena naprava je na voljo"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Bližina"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Naprave"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Povezovanje …"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Baterija: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Končano"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Shrani"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Nastavi"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Nastavitve"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-sq/strings.xml b/nearby/halfsheet/res/values-sq/strings.xml
deleted file mode 100644
index 538e9d6..0000000
--- a/nearby/halfsheet/res/values-sq/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Po nis konfigurimin…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfiguro pajisjen"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Pajisja u lidh"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"U lidh me “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nuk mund të lidhej"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nuk mund të lidhet"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Provo të çiftosh me pajisjen manualisht"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Provo ta vendosësh pajisjen në modalitetin e çiftimit"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Pajisjet që mund të arrish"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Pajisjet me llogarinë tënde"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Pajisja jote e ruajtur ofrohet"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Në afërsi"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Pajisjet"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Po lidhet…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"U krye"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Ruaj"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Lidh"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Konfiguro"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Cilësimet"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-sr/strings.xml b/nearby/halfsheet/res/values-sr/strings.xml
deleted file mode 100644
index c4bcd19..0000000
--- a/nearby/halfsheet/res/values-sr/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Подешавање се покреће…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Подесите уређај"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Уређај је повезан"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Повезани сте са уређајем %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Повезивање није успело"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Повезивање није успело"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Пробајте да упарите ручно са уређајем"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Пробајте да пребаците уређај у режим упаривања"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Уређаји у домету"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Уређаји повезани са налогом"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Сачувани уређај је доступан"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"У близини"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Уређаји"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Повезује се…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Батерија: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Сачувај"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Повежи"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Подеси"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Подешавања"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-sv/strings.xml b/nearby/halfsheet/res/values-sv/strings.xml
deleted file mode 100644
index b00091c..0000000
--- a/nearby/halfsheet/res/values-sv/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Konfigureringen startas …"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfigurera enheten"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Enheten är ansluten"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Ansluten till %s"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Det gick inte att ansluta"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Det går inte att ansluta"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Testa att parkoppla enheten manuellt"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Testa att aktivera parkopplingsläget på enheten"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Enheter inom räckvidd"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Enheter med ditt konto"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"En sparad enhet är tillgänglig"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Närdelning"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Enheter"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Ansluter …"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batteri: %d %%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Klar"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Spara"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Anslut"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Konfigurera"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Inställningar"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-sw/strings.xml b/nearby/halfsheet/res/values-sw/strings.xml
deleted file mode 100644
index 238a288..0000000
--- a/nearby/halfsheet/res/values-sw/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Inaanza Kuweka Mipangilio…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Weka mipangilio ya kifaa"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Kifaa kimeunganishwa"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Imeunganishwa kwenye “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Imeshindwa kuunganisha"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Imeshindwa kuunganisha"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Jaribu kuoanisha mwenyewe kwenye kifaa"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Jaribu kuweka kifaa katika hali ya kuoanisha"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Vifaa vilivyo karibu nawe"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Vifaa vilivyounganishwa kwenye akaunti yako"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Kifaa ulichohifadhi kinapatikana"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Uhamishaji wa Karibu"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Vifaa"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Inaunganisha…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Betri: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Imemaliza"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Hifadhi"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Unganisha"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Weka mipangilio"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Mipangilio"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ta/strings.xml b/nearby/halfsheet/res/values-ta/strings.xml
deleted file mode 100644
index baadcc2..0000000
--- a/nearby/halfsheet/res/values-ta/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"அமைவைத் தொடங்குகிறது…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"சாதனத்தை அமையுங்கள்"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"சாதனம் இணைக்கப்பட்டது"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” உடன் இணைக்கப்பட்டது"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"இணைக்க முடியவில்லை"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"இணைக்க முடியவில்லை"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"சாதனத்துடன் நீங்களாகவே இணைக்க முயலவும்"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"சாதனத்தை \'இணைத்தல் பயன்முறையில்\' வைக்கவும்"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"தொடர்பு வரம்பிலுள்ள சாதனங்கள்"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"உங்கள் கணக்குடன் இணைந்துள்ள சாதனங்கள்"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"நீங்கள் சேமித்த சாதனம் உள்ளது"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"அருகில் பகிர்தல்"</string>
-    <string name="common_devices" msgid="2635603125608104442">"சாதனங்கள்"</string>
-    <string name="common_connecting" msgid="160531481424245303">"இணைக்கிறது…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"பேட்டரி: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"முடிந்தது"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"சேமி"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"இணை"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"அமை"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"அமைப்புகள்"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-te/strings.xml b/nearby/halfsheet/res/values-te/strings.xml
deleted file mode 100644
index cb8f91b..0000000
--- a/nearby/halfsheet/res/values-te/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"సెటప్ ప్రారంభమవుతోంది…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"పరికరాన్ని సెటప్ చేయండి"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"పరికరం కనెక్ట్ చేయబడింది"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s”కు కనెక్ట్ చేయబడింది"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"కనెక్ట్ చేయడం సాధ్యపడలేదు"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"కనెక్ట్ చేయలేకపోయింది"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"పరికరానికి మాన్యువల్‌గా పెయిరింగ్ చేయడానికి ట్రై చేయండి"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"పరికరాన్ని పెయిరింగ్ మోడ్‌లో ఉంచడానికి ట్రై చేయండి"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"అందుబాటులో ఉన్న పరికరాలు"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"మీ ఖాతా ఉన్న పరికరాలు"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"మీ సేవ్ చేసిన పరికరం అందుబాటులో ఉంది"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"సమీపం"</string>
-    <string name="common_devices" msgid="2635603125608104442">"పరికరాలు"</string>
-    <string name="common_connecting" msgid="160531481424245303">"కనెక్ట్ అవుతోంది…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"బ్యాటరీ: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"పూర్తయింది"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"సేవ్ చేయండి"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"కనెక్ట్ చేయండి"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"సెటప్ చేయండి"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"సెట్టింగ్‌లు"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-th/strings.xml b/nearby/halfsheet/res/values-th/strings.xml
deleted file mode 100644
index f5c5c2e..0000000
--- a/nearby/halfsheet/res/values-th/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"กำลังเริ่มการตั้งค่า…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"ตั้งค่าอุปกรณ์"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"เชื่อมต่ออุปกรณ์แล้ว"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"เชื่อมต่อกับ \"%s\" แล้ว"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"เชื่อมต่อไม่ได้"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"เชื่อมต่อไม่ได้"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ลองจับคู่อุปกรณ์ด้วยตนเอง"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"พยายามนำอุปกรณ์เข้าสู่โหมดการจับคู่"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"อุปกรณ์ที่อยู่ติดกัน"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"อุปกรณ์ที่มีบัญชีของคุณ"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"อุปกรณ์ที่บันทึกพร้อมใช้แล้ว"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"ใกล้เคียง"</string>
-    <string name="common_devices" msgid="2635603125608104442">"อุปกรณ์"</string>
-    <string name="common_connecting" msgid="160531481424245303">"กำลังเชื่อมต่อ…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"แบตเตอรี่: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"เสร็จสิ้น"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"บันทึก"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"เชื่อมต่อ"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"ตั้งค่า"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"การตั้งค่า"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-tl/strings.xml b/nearby/halfsheet/res/values-tl/strings.xml
deleted file mode 100644
index a546da6..0000000
--- a/nearby/halfsheet/res/values-tl/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Sinisimulan ang Pag-set Up…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"I-set up ang device"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Naikonekta na ang device"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Nakakonekta sa “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Hindi makakonekta"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Hindi makakonekta"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Subukang manual na magpares sa device"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Subukang ilagay sa pairing mode ang device"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Mga naaabot na device"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Mga device sa iyong account"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Available ang iyong na-save na device"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Kalapit"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Mga Device"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Kumokonekta…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Baterya: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Tapos na"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"I-save"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Kumonekta"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"I-set up"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Mga Setting"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-tr/strings.xml b/nearby/halfsheet/res/values-tr/strings.xml
deleted file mode 100644
index a54c5e7..0000000
--- a/nearby/halfsheet/res/values-tr/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Kurulum Başlatılıyor…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Cihazı kur"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Cihaz bağlandı"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\"%s\" cihazına bağlanıldı"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Bağlanamadı"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Bağlantı kurulamadı"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Cihazla manuel olarak eşlemeyi deneyin"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Cihazı eşleme moduna geçirmeyi deneyin"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Erişilebilecek cihazlar"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Hesabınıza bağlı cihazlar"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Kayıtlı cihazınız kullanılabilir"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Yakındaki"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Cihazlar"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Bağlanıyor…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Pil seviyesi: %%%d"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Bitti"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Kaydet"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Bağlan"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Kur"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Ayarlar"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-uk/strings.xml b/nearby/halfsheet/res/values-uk/strings.xml
deleted file mode 100644
index ab73c1f..0000000
--- a/nearby/halfsheet/res/values-uk/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Запуск налаштування…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Налаштуйте пристрій"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Пристрій підключено"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Підключено до пристрою \"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Не вдалося підключити"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Не вдалося підключитися"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Спробуйте підключитися до пристрою вручну"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Спробуйте ввімкнути на пристрої режим підключення"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Пристрої в радіусі дії"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Пристрої з вашим обліковим записом"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Збережений пристрій доступний"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Поблизу"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Пристрої"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Підключення…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Акумулятор: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Зберегти"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Підключити"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Налаштувати"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Налаштування"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-ur/strings.xml b/nearby/halfsheet/res/values-ur/strings.xml
deleted file mode 100644
index a2b2038..0000000
--- a/nearby/halfsheet/res/values-ur/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"سیٹ اپ شروع ہو رہا ہے…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"آلہ سیٹ اپ کریں"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"آلہ منسلک ہے"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"‏\"‎\"%s سے منسلک ہے"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"منسلک نہیں ہو سکا"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"منسلک ہونے سے قاصر"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"دستی طور پر آلے کے ساتھ جوڑا بنانا آزمائیں"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"آلے کو جوڑا بنانے والے موڈ میں رکھ کر آزمائیں"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"رسائی کے اندر آلات"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"آپ کے اکاؤنٹ سے منسلک آلات"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"آپ کا محفوظ کردہ آلہ دستیاب ہے"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"قریبی"</string>
-    <string name="common_devices" msgid="2635603125608104442">"آلات"</string>
-    <string name="common_connecting" msgid="160531481424245303">"منسلک ہو رہا ہے…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"‏بیٹری: ‎‎%d%%‎"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"ہو گیا"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"محفوظ کریں"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"منسلک کریں"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"سیٹ اپ کریں"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"ترتیبات"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-uz/strings.xml b/nearby/halfsheet/res/values-uz/strings.xml
deleted file mode 100644
index 70c190a..0000000
--- a/nearby/halfsheet/res/values-uz/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Sozlash boshlandi…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Qurilmani sozlash"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Qurilma ulandi"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Bunga ulandi: “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ulanmadi"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Ulanmadi"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Qurilmangizga odatiy usulda ulaning"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Qurilmada ulanish rejimini yoqing"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Atrofdagi qurilmalar"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Hisobingizga ulangan qurilmalar"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Saqlangan qurilmangiz mavjud"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Yaqin-atrofda"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Qurilmalar"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Ulanmoqda…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Batareya: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Tayyor"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Saqlash"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Ulanish"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Sozlash"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Sozlamalar"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-vi/strings.xml b/nearby/halfsheet/res/values-vi/strings.xml
deleted file mode 100644
index e2ea467..0000000
--- a/nearby/halfsheet/res/values-vi/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Đang bắt đầu thiết lập…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Thiết lập thiết bị"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Đã kết nối thiết bị"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Đã kết nối với “%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Không kết nối được"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Không thể kết nối"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Hãy thử ghép nối thủ công với thiết bị"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Hãy thử đưa thiết bị này vào chế độ ghép nối"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Thiết bị trong tầm tay"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Các thiết bị có tài khoản của bạn"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Thiết bị đã lưu của bạn có sẵn"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Lân cận"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Thiết bị"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Đang kết nối…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Pin: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Xong"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Lưu"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Kết nối"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Thiết lập"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Cài đặt"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-zh-rCN/strings.xml b/nearby/halfsheet/res/values-zh-rCN/strings.xml
deleted file mode 100644
index 8117bac..0000000
--- a/nearby/halfsheet/res/values-zh-rCN/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"正在启动设置…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"设置设备"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"设备已连接"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"已连接到“%s”"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"无法连接"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"无法连接"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"请尝试手动与该设备配对"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"请尝试让设备进入配对模式"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"附近的设备"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"与您的帐号相关联的设备"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"您保存的设备已可供使用"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"附近"</string>
-    <string name="common_devices" msgid="2635603125608104442">"设备"</string>
-    <string name="common_connecting" msgid="160531481424245303">"正在连接…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"电量:%d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"完成"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"保存"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"连接"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"设置"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"设置"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-zh-rHK/strings.xml b/nearby/halfsheet/res/values-zh-rHK/strings.xml
deleted file mode 100644
index 4dac931..0000000
--- a/nearby/halfsheet/res/values-zh-rHK/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"開始設定…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"設定裝置"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"已連接裝置"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"已連線至「%s」"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"無法連接"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"無法連線"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"嘗試手動配對裝置"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"嘗試讓裝置進入配對模式"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"附近的裝置"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"已連結你帳戶的裝置"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"你儲存的裝置已可使用"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"咫尺共享"</string>
-    <string name="common_devices" msgid="2635603125608104442">"裝置"</string>
-    <string name="common_connecting" msgid="160531481424245303">"正在連接…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"電量:%d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"完成"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"儲存"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"連接"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"設定"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"設定"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-zh-rTW/strings.xml b/nearby/halfsheet/res/values-zh-rTW/strings.xml
deleted file mode 100644
index 0c90ebb..0000000
--- a/nearby/halfsheet/res/values-zh-rTW/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"正在啟動設定程序…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"設定裝置"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"裝置已連線"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"已連線到「%s」"</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"無法連線"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"無法連線"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"嘗試手動配對裝置"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"嘗試讓裝置進入配對模式"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"鄰近裝置"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"與你帳戶連結的裝置"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"你儲存的裝置已可使用"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"鄰近分享"</string>
-    <string name="common_devices" msgid="2635603125608104442">"裝置"</string>
-    <string name="common_connecting" msgid="160531481424245303">"連線中…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"電池電量:%d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"完成"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"儲存"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"連線"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"設定"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"設定"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values-zu/strings.xml b/nearby/halfsheet/res/values-zu/strings.xml
deleted file mode 100644
index 3f26469..0000000
--- a/nearby/halfsheet/res/values-zu/strings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iqalisa Ukusetha…"</string>
-    <string name="fast_pair_title_setup" msgid="2894360355540593246">"Setha idivayisi"</string>
-    <string name="fast_pair_device_ready" msgid="2903490346082833101">"Idivayisi ixhunyiwe"</string>
-    <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Ixhunywe ku-\"%s\""</string>
-    <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ayikwazanga ukuxhuma"</string>
-    <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Ayikwazanga ukuxhuma"</string>
-    <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Zama ukubhangqa kule divayisi"</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Zama ukubeka le divayisi kumodi yokubhangqa"</string>
-    <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Amadivayisi afinyelelekayo"</string>
-    <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Amadivayisi ane-akhawunti yakho"</string>
-    <string name="fast_pair_your_device" msgid="3662423897069320840">"Idivayisi yakho elondoloziwe ikhona"</string>
-    <string name="common_nearby_title" msgid="5480324514713607015">"Eduze"</string>
-    <string name="common_devices" msgid="2635603125608104442">"Amadivayisi"</string>
-    <string name="common_connecting" msgid="160531481424245303">"Iyaxhuma…"</string>
-    <string name="common_battery_level" msgid="8748495823047456645">"Ibhethri: %d%%"</string>
-    <string name="paring_action_done" msgid="6888875159174470731">"Kwenziwe"</string>
-    <string name="paring_action_save" msgid="6259357442067880136">"Londoloza"</string>
-    <string name="paring_action_connect" msgid="4801102939608129181">"Xhuma"</string>
-    <string name="paring_action_launch" msgid="8940808384126591230">"Setha"</string>
-    <string name="paring_action_settings" msgid="424875657242864302">"Amasethingi"</string>
-</resources>
diff --git a/nearby/halfsheet/res/values/colors.xml b/nearby/halfsheet/res/values/colors.xml
deleted file mode 100644
index b066665..0000000
--- a/nearby/halfsheet/res/values/colors.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools">
-  <!-- Use original background color -->
-  <color name="fast_pair_notification_background">#00000000</color>
-  <!-- Ignores NewApi as below system colors are available since API 31, and HalfSheet is always
-       running on T+ even though it has min_sdk 30 to match its containing APEX -->
-  <color name="fast_pair_half_sheet_button_color" tools:ignore="NewApi">@android:color/system_accent1_100</color>
-  <color name="fast_pair_half_sheet_button_text" tools:ignore="NewApi">@android:color/system_neutral1_900</color>
-  <color name="fast_pair_half_sheet_button_accent_text" tools:ignore="NewApi">@android:color/system_neutral1_900</color>
-  <color name="fast_pair_progress_color" tools:ignore="NewApi">@android:color/system_accent1_600</color>
-  <color name="fast_pair_half_sheet_subtitle_color" tools:ignore="NewApi">@android:color/system_neutral2_700</color>
-  <color name="fast_pair_half_sheet_text_color" tools:ignore="NewApi">@android:color/system_neutral1_900</color>
-
-  <!-- Nearby Discoverer -->
-  <color name="discovery_activity_accent">#4285F4</color>
-
-  <!-- Fast Pair -->
-  <color name="fast_pair_primary_text">#DE000000</color>
-  <color name="fast_pair_notification_image_outline">#24000000</color>
-  <color name="fast_pair_battery_level_low">#D93025</color>
-  <color name="fast_pair_battery_level_normal">#80868B</color>
-  <color name="fast_pair_half_sheet_background">#FFFFFF</color>
-  <color name="fast_pair_half_sheet_color_accent">#1A73E8</color>
-  <color name="fast_pair_fail_progress_color">#F44336</color>
-  <color name="fast_pair_progress_back_ground">#24000000</color>
-</resources>
diff --git a/nearby/halfsheet/res/values/dimens.xml b/nearby/halfsheet/res/values/dimens.xml
deleted file mode 100644
index f843042..0000000
--- a/nearby/halfsheet/res/values/dimens.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-  <!-- Fast Pair notification values -->
-  <dimen name="fast_pair_halfsheet_mid_image_size">160dp</dimen>
-  <dimen name="fast_pair_notification_text_size">14sp</dimen>
-  <dimen name="fast_pair_notification_text_size_small">11sp</dimen>
-  <dimen name="fast_pair_battery_notification_empty_view_height">4dp</dimen>
-  <dimen name="fast_pair_battery_notification_margin_top">8dp</dimen>
-  <dimen name="fast_pair_battery_notification_margin_bottom">8dp</dimen>
-  <dimen name="fast_pair_battery_notification_content_height">40dp</dimen>
-  <dimen name="fast_pair_battery_notification_content_height_v2">64dp</dimen>
-  <dimen name="fast_pair_battery_notification_image_size">32dp</dimen>
-  <dimen name="fast_pair_battery_notification_image_padding">3dp</dimen>
-  <dimen name="fast_pair_half_sheet_min_height">350dp</dimen>
-  <dimen name="fast_pair_half_sheet_image_size">215dp</dimen>
-  <dimen name="fast_pair_half_sheet_land_image_size">136dp</dimen>
-  <dimen name="fast_pair_connect_button_height">36dp</dimen>
-  <dimen name="accessibility_required_min_touch_target_size">48dp</dimen>
-  <dimen name="fast_pair_half_sheet_battery_case_image_size">152dp</dimen>
-  <dimen name="fast_pair_half_sheet_battery_bud_image_size">100dp</dimen>
-  <integer name="half_sheet_battery_case_width_dp">156</integer>
-  <integer name="half_sheet_battery_case_height_dp">182</integer>
-
-  <!-- Maximum height for SliceView, override on slices/view/src/main/res/values/dimens.xml -->
-  <dimen name="abc_slice_large_height">360dp</dimen>
-
-  <dimen name="action_dialog_content_margin_left">16dp</dimen>
-  <dimen name="action_dialog_content_margin_top">70dp</dimen>
-  <dimen name="action_button_focused_elevation">4dp</dimen>
-  <!-- Subsequent Notification -->
-  <dimen name="fast_pair_notification_padding">4dp</dimen>
-  <dimen name="fast_pair_notification_large_image_size">32dp</dimen>
-  <dimen name="fast_pair_notification_small_image_size">32dp</dimen>
-  <!-- Battery Notification -->
-  <dimen name="fast_pair_battery_notification_main_view_padding">0dp</dimen>
-  <dimen name="fast_pair_battery_notification_title_image_margin_start">0dp</dimen>
-  <dimen name="fast_pair_battery_notification_title_text_margin_start">0dp</dimen>
-  <dimen name="fast_pair_battery_notification_title_text_margin_start_v2">0dp</dimen>
-  <dimen name="fast_pair_battery_notification_image_margin_start">0dp</dimen>
-
-  <dimen name="fast_pair_half_sheet_bottom_button_height">48dp</dimen>
-</resources>
diff --git a/nearby/halfsheet/res/values/ints.xml b/nearby/halfsheet/res/values/ints.xml
deleted file mode 100644
index 07bf9d2..0000000
--- a/nearby/halfsheet/res/values/ints.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-  <integer name="half_sheet_slide_in_duration">250</integer>
-  <integer name="half_sheet_fade_out_duration">250</integer>
-</resources>
diff --git a/nearby/halfsheet/res/values/overlayable.xml b/nearby/halfsheet/res/values/overlayable.xml
deleted file mode 100644
index fffa2e3..0000000
--- a/nearby/halfsheet/res/values/overlayable.xml
+++ /dev/null
@@ -1,24 +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.
-  -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-    <overlayable name="NearbyHalfSheetResourcesConfig">
-        <policy type="product|system|vendor">
-            <item type="color" name="fast_pair_half_sheet_background"/>
-            <item type="color" name="fast_pair_half_sheet_button_color"/>
-        </policy>
-    </overlayable>
-</resources>
\ No newline at end of file
diff --git a/nearby/halfsheet/res/values/strings.xml b/nearby/halfsheet/res/values/strings.xml
deleted file mode 100644
index c1f53d4..0000000
--- a/nearby/halfsheet/res/values/strings.xml
+++ /dev/null
@@ -1,94 +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.
-  -->
-
-<resources>
-
-    <!--
-      ============================================================
-      PAIRING FRAGMENT
-      ============================================================
-    -->
-
-    <!--
-      A button shown to remind user setup is in progress. [CHAR LIMIT=30]
-    -->
-    <string name="fast_pair_setup_in_progress">Starting Setup&#x2026;</string>
-    <!--
-      Title text shown to remind user to setup a device through companion app. [CHAR LIMIT=40]
-    -->
-    <string name="fast_pair_title_setup">Set up device</string>
-    <!--
-      Title after we successfully pair with the audio device
-      [CHAR LIMIT=30]
-    -->
-    <string name="fast_pair_device_ready">Device connected</string>
-    <string name="fast_pair_device_ready_with_device_name" description="Notification title combined with device name after we successfully pair with the audio device. For example: Connected to &quot;Tommy's Bose QC35.&quot; [BACKUP_MESSAGE_ID: 6018442069058338390]">Connected to \u201c%s\u201d</string>
-    <!-- Title text shown when peripheral device fail to connect to phone. [CHAR_LIMIT=30] -->
-    <string name="fast_pair_title_fail">Couldn\'t connect</string>
-    <string name="fast_pair_unable_to_connect" description="Notification title after a pairing has failed. [CHAR LIMIT=30]">Unable to connect</string>
-    <string name="fast_pair_unable_to_connect_description" description="Notification body after a pairing has failed. [CHAR LIMIT=120]">Try manually pairing to the device</string>
-    <string name="fast_pair_turn_on_bt_device_pairing_mode" description="Notification body after a pairing has failed. [CHAR LIMIT=120]">Try putting the device into pairing mode</string>
-
-    <!--
-      ============================================================
-      PAIRING NOTIFICATION
-      ============================================================
-    -->
-
-    <string name="devices_within_reach_channel_name" description="Notification channel for devices within reach. [CHAR LIMIT=37]">Devices within reach</string>
-    <string name="devices_with_your_account_channel_name" description="Notification channel for devices that are connected to the user's account. [CHAR LIMIT=37]">Devices with your account</string>
-    <string name="fast_pair_your_device" description="Notification title for devices that are recognized as being owned by you. [CHAR LIMIT=30]">Your saved device is available</string>
-
-    <!--
-      ============================================================
-      MISCELLANEOUS
-      ============================================================
-    -->
-
-    <!-- Title for Nearby component [CHAR LIMIT=40] -->
-    <string name="common_nearby_title">Nearby</string>
-    <!-- The product name for devices notification and list view. [CHAR LIMIT=37]-->
-    <string name="common_devices">Devices</string>
-    <!-- Text used to indicate that a connection attempt is ongoing [CHAR LIMIT=20] -->
-    <string name="common_connecting">Connecting&#8230;</string>
-    <!-- Label describing the battery level, for example "Battery: 72%". [CHAR LIMIT=60] -->
-    <string name="common_battery_level">Battery: %d%%</string>
-    <!--
-      A button shown after paring process to dismiss the current activity.
-      [CHAR LIMIT=30]
-    -->
-    <string name="paring_action_done">Done</string>
-    <!--
-      A button shown for retroactive paring.
-      [CHAR LIMIT=30]
-     -->
-    <string name="paring_action_save">Save</string>
-    <!--
-      A button to start connecting process.
-      [CHAR LIMIT=30]
-     -->
-    <string name="paring_action_connect">Connect</string>
-    <!--
-      A button to launch a companion app.
-      [CHAR LIMIT=30]
-    -->
-    <string name="paring_action_launch">Set up</string>
-    <!--
-      A button to launch a bluetooth Settings page.
-      [CHAR LIMIT=20]
-    -->
-    <string name="paring_action_settings">Settings</string>
-</resources>
\ No newline at end of file
diff --git a/nearby/halfsheet/res/values/styles.xml b/nearby/halfsheet/res/values/styles.xml
deleted file mode 100644
index 917bb63..0000000
--- a/nearby/halfsheet/res/values/styles.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-  <style name="HalfSheetStyle" parent="Theme.Material3.DayNight.NoActionBar">
-    <item name="android:windowFrame">@null</item>
-    <item name="android:windowBackground">@android:color/transparent</item>
-    <item name="android:windowEnterAnimation">@anim/fast_pair_half_sheet_slide_in</item>
-    <item name="android:windowExitAnimation">@anim/fast_pair_half_sheet_slide_out</item>
-    <item name="android:windowIsTranslucent">true</item>
-    <item name="android:windowContentOverlay">@null</item>
-    <item name="android:windowNoTitle">true</item>
-    <item name="android:backgroundDimEnabled">true</item>
-    <item name="android:statusBarColor">@android:color/transparent</item>
-    <item name="android:fitsSystemWindows">true</item>
-    <item name="android:windowTranslucentNavigation">true</item>
-  </style>
-
-  <style name="HalfSheetButton" parent="@style/Widget.Material3.Button.TonalButton">
-    <item name="android:textColor">@color/fast_pair_half_sheet_button_accent_text</item>
-    <item name="android:backgroundTint">@color/fast_pair_half_sheet_button_color</item>
-    <item name="android:textSize">@dimen/fast_pair_notification_text_size</item>
-    <item name="android:fontFamily">google-sans-medium</item>
-    <item name="android:textAlignment">center</item>
-    <item name="android:textAllCaps">false</item>
-  </style>
-
-  <style name="HalfSheetButtonBorderless" parent="@style/Widget.Material3.Button.OutlinedButton">
-    <item name="android:textColor">@color/fast_pair_half_sheet_button_text</item>
-    <item name="android:strokeColor">@color/fast_pair_half_sheet_button_color</item>
-    <item name="android:textAllCaps">false</item>
-    <item name="android:textSize">@dimen/fast_pair_notification_text_size</item>
-    <item name="android:fontFamily">google-sans-medium</item>
-    <item name="android:layout_width">wrap_content</item>
-    <item name="android:layout_height">wrap_content</item>
-    <item name="android:textAlignment">center</item>
-    <item name="android:minHeight">@dimen/accessibility_required_min_touch_target_size</item>
-  </style>
-
-</resources>
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/FastPairUiServiceClient.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/FastPairUiServiceClient.java
deleted file mode 100644
index bec0c0a..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/FastPairUiServiceClient.java
+++ /dev/null
@@ -1,117 +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.nearby.halfsheet;
-
-import android.content.Context;
-import android.nearby.FastPairDevice;
-import android.nearby.FastPairStatusCallback;
-import android.nearby.PairStatusMetadata;
-import android.nearby.aidl.IFastPairStatusCallback;
-import android.nearby.aidl.IFastPairUiService;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import androidx.annotation.BinderThread;
-import androidx.annotation.UiThread;
-
-import java.lang.ref.WeakReference;
-
-/**
- *  A utility class for connecting to the {@link IFastPairUiService} and receive callbacks.
- *
- * @hide
- */
-@UiThread
-public class FastPairUiServiceClient {
-
-    private static final String TAG = "FastPairHalfSheet";
-
-    private final IBinder mBinder;
-    private final WeakReference<Context> mWeakContext;
-    IFastPairUiService mFastPairUiService;
-    PairStatusCallbackIBinder mPairStatusCallbackIBinder;
-
-    /**
-     * The Ibinder instance should be from
-     * {@link com.android.server.nearby.fastpair.halfsheet.FastPairUiServiceImpl} so that the client can
-     * talk with the service.
-     */
-    public FastPairUiServiceClient(Context context, IBinder binder) {
-        mBinder = binder;
-        mFastPairUiService = IFastPairUiService.Stub.asInterface(mBinder);
-        mWeakContext = new WeakReference<>(context);
-    }
-
-    /**
-     * Registers a callback at service to get UI updates.
-     */
-    public void registerHalfSheetStateCallBack(FastPairStatusCallback fastPairStatusCallback) {
-        if (mPairStatusCallbackIBinder != null) {
-            return;
-        }
-        mPairStatusCallbackIBinder = new PairStatusCallbackIBinder(fastPairStatusCallback);
-        try {
-            mFastPairUiService.registerCallback(mPairStatusCallbackIBinder);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed to register fastPairStatusCallback", e);
-        }
-    }
-
-    /**
-     * Pairs the device at service.
-     */
-    public void connect(FastPairDevice fastPairDevice) {
-        try {
-            mFastPairUiService.connect(fastPairDevice);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed to connect Fast Pair device" + fastPairDevice, e);
-        }
-    }
-
-    /**
-     * Cancels Fast Pair connection and dismisses half sheet.
-     */
-    public void cancel(FastPairDevice fastPairDevice) {
-        try {
-            mFastPairUiService.cancel(fastPairDevice);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed to connect Fast Pair device" + fastPairDevice, e);
-        }
-    }
-
-    private class PairStatusCallbackIBinder extends IFastPairStatusCallback.Stub {
-        private final FastPairStatusCallback mStatusCallback;
-
-        private PairStatusCallbackIBinder(FastPairStatusCallback fastPairStatusCallback) {
-            mStatusCallback = fastPairStatusCallback;
-        }
-
-        @BinderThread
-        @Override
-        public synchronized void onPairUpdate(FastPairDevice fastPairDevice,
-                PairStatusMetadata pairStatusMetadata) {
-            Context context = mWeakContext.get();
-            if (context != null) {
-                Handler handler = new Handler(context.getMainLooper());
-                handler.post(() ->
-                        mStatusCallback.onPairUpdate(fastPairDevice, pairStatusMetadata));
-            }
-        }
-    }
-}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
deleted file mode 100644
index 94f4ef4..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
+++ /dev/null
@@ -1,245 +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.nearby.halfsheet;
-
-import static android.Manifest.permission.ACCESS_FINE_LOCATION;
-
-import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET;
-import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_CANCEL;
-import static com.android.nearby.halfsheet.constants.Constant.ACTION_HALF_SHEET_FOREGROUND_STATE;
-import static com.android.nearby.halfsheet.constants.Constant.DEVICE_PAIRING_FRAGMENT_TYPE;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_FOREGROUND;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_INFO;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_IS_RETROACTIVE;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_TYPE;
-import static com.android.nearby.halfsheet.constants.Constant.TAG;
-import static com.android.nearby.halfsheet.constants.FastPairConstants.EXTRA_MODEL_ID;
-import static com.android.nearby.halfsheet.constants.UserActionHandlerBase.EXTRA_MAC_ADDRESS;
-import static com.android.nearby.halfsheet.fragment.DevicePairingFragment.APP_LAUNCH_FRAGMENT_TYPE;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.FragmentActivity;
-
-import com.android.nearby.halfsheet.fragment.DevicePairingFragment;
-import com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment;
-import com.android.nearby.halfsheet.utils.BroadcastUtils;
-
-import com.google.protobuf.InvalidProtocolBufferException;
-
-import java.util.Locale;
-
-import service.proto.Cache;
-
-/**
- * A class show Fast Pair related information in Half sheet format.
- */
-public class HalfSheetActivity extends FragmentActivity {
-    @Nullable
-    private HalfSheetModuleFragment mHalfSheetModuleFragment;
-    @Nullable
-    private Cache.ScanFastPairStoreItem mScanFastPairStoreItem;
-
-    @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        byte[] infoArray = getIntent().getByteArrayExtra(EXTRA_HALF_SHEET_INFO);
-        String fragmentType = getIntent().getStringExtra(EXTRA_HALF_SHEET_TYPE);
-        if (infoArray == null || fragmentType == null) {
-            Log.d(
-                    "HalfSheetActivity",
-                    "exit flag off or do not have enough half sheet information.");
-            finish();
-            return;
-        }
-
-        switch (fragmentType) {
-            case DEVICE_PAIRING_FRAGMENT_TYPE:
-                mHalfSheetModuleFragment = DevicePairingFragment.newInstance(getIntent(),
-                        savedInstanceState);
-                if (mHalfSheetModuleFragment == null) {
-                    Log.d(TAG, "device pairing fragment has error.");
-                    finish();
-                    return;
-                }
-                break;
-            case APP_LAUNCH_FRAGMENT_TYPE:
-                // currentFragment = AppLaunchFragment.newInstance(getIntent());
-                if (mHalfSheetModuleFragment == null) {
-                    Log.v(TAG, "app launch fragment has error.");
-                    finish();
-                    return;
-                }
-                break;
-            default:
-                Log.w(TAG, "there is no valid type for half sheet");
-                finish();
-                return;
-        }
-        if (mHalfSheetModuleFragment != null) {
-            getSupportFragmentManager()
-                    .beginTransaction()
-                    .replace(R.id.fragment_container, mHalfSheetModuleFragment)
-                    .commit();
-        }
-        setContentView(R.layout.fast_pair_half_sheet);
-
-        // If the user taps on the background, then close the activity.
-        // Unless they tap on the card itself, then ignore the tap.
-        findViewById(R.id.background).setOnClickListener(v -> onCancelClicked());
-        findViewById(R.id.card)
-                .setOnClickListener(
-                        v -> Log.v(TAG, "card view is clicked noop"));
-        try {
-            mScanFastPairStoreItem =
-                    Cache.ScanFastPairStoreItem.parseFrom(infoArray);
-        } catch (InvalidProtocolBufferException e) {
-            Log.w(
-                    TAG, "error happens when pass info to half sheet");
-        }
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        BroadcastUtils.sendBroadcast(
-                this,
-                new Intent(ACTION_HALF_SHEET_FOREGROUND_STATE)
-                        .putExtra(EXTRA_HALF_SHEET_FOREGROUND, true));
-    }
-
-    @Override
-    protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
-        super.onSaveInstanceState(savedInstanceState);
-        if (mHalfSheetModuleFragment != null) {
-            mHalfSheetModuleFragment.onSaveInstanceState(savedInstanceState);
-        }
-    }
-
-    @Override
-    public void onBackPressed() {
-        super.onBackPressed();
-        sendHalfSheetCancelBroadcast();
-    }
-
-    @Override
-    protected void onUserLeaveHint() {
-        super.onUserLeaveHint();
-        sendHalfSheetCancelBroadcast();
-    }
-
-    @Override
-    protected void onNewIntent(Intent intent) {
-        super.onNewIntent(intent);
-        String fragmentType = getIntent().getStringExtra(EXTRA_HALF_SHEET_TYPE);
-        if (fragmentType == null) {
-            return;
-        }
-        if (fragmentType.equals(DEVICE_PAIRING_FRAGMENT_TYPE)
-                && intent.getExtras() != null
-                && intent.getByteArrayExtra(EXTRA_HALF_SHEET_INFO) != null) {
-            try {
-                Cache.ScanFastPairStoreItem testScanFastPairStoreItem =
-                        Cache.ScanFastPairStoreItem.parseFrom(
-                                intent.getByteArrayExtra(EXTRA_HALF_SHEET_INFO));
-                if (mScanFastPairStoreItem != null
-                        && !testScanFastPairStoreItem.getAddress().equals(
-                        mScanFastPairStoreItem.getAddress())
-                        && testScanFastPairStoreItem.getModelId().equals(
-                        mScanFastPairStoreItem.getModelId())) {
-                    Log.d(TAG, "possible factory reset happens");
-                    halfSheetStateChange();
-                }
-            } catch (InvalidProtocolBufferException | NullPointerException e) {
-                Log.w(TAG, "error happens when pass info to half sheet");
-            }
-        }
-    }
-
-    /** This function should be called when user click empty area and cancel button. */
-    public void onCancelClicked() {
-        Log.d(TAG, "Cancels the half sheet and paring.");
-        sendHalfSheetCancelBroadcast();
-        finish();
-    }
-
-    /** Changes the half sheet foreground state to false. */
-    public void halfSheetStateChange() {
-        BroadcastUtils.sendBroadcast(
-                this,
-                new Intent(ACTION_HALF_SHEET_FOREGROUND_STATE)
-                        .putExtra(EXTRA_HALF_SHEET_FOREGROUND, false));
-        finish();
-    }
-
-
-    /**
-     * Changes the half sheet ban state to active.
-     * Sometimes users leave half sheet to go to fast pair info page,
-     * we do not want the behavior to be counted as dismiss.
-     */
-    public void sendBanStateResetBroadcast() {
-        if (mScanFastPairStoreItem == null) {
-            return;
-        }
-        BroadcastUtils.sendBroadcast(
-                this,
-                new Intent(ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET)
-                        .putExtra(EXTRA_MODEL_ID, mScanFastPairStoreItem.getModelId()
-                                .toLowerCase(Locale.ROOT)));
-    }
-
-    private void sendHalfSheetCancelBroadcast() {
-        BroadcastUtils.sendBroadcast(
-                this,
-                new Intent(ACTION_HALF_SHEET_FOREGROUND_STATE)
-                        .putExtra(EXTRA_HALF_SHEET_FOREGROUND, false));
-        if (mScanFastPairStoreItem == null) {
-            return;
-        }
-        BroadcastUtils.sendBroadcast(
-                this,
-                new Intent(ACTION_FAST_PAIR_HALF_SHEET_CANCEL)
-                        .putExtra(EXTRA_MODEL_ID,
-                                mScanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT))
-                        .putExtra(EXTRA_HALF_SHEET_TYPE,
-                                getIntent().getStringExtra(EXTRA_HALF_SHEET_TYPE))
-                        .putExtra(
-                                EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR,
-                                getIntent().getBooleanExtra(EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR,
-                                        false))
-                        .putExtra(
-                                EXTRA_HALF_SHEET_IS_RETROACTIVE,
-                                getIntent().getBooleanExtra(EXTRA_HALF_SHEET_IS_RETROACTIVE,
-                                        false))
-                        .putExtra(EXTRA_MAC_ADDRESS, mScanFastPairStoreItem.getAddress()),
-                ACCESS_FINE_LOCATION);
-    }
-
-    @Override
-    public void setTitle(CharSequence title) {
-        super.setTitle(title);
-        TextView toolbarTitle = findViewById(R.id.toolbar_title);
-        toolbarTitle.setText(title);
-    }
-}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/Constant.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/Constant.java
deleted file mode 100644
index 65c76d1..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/Constant.java
+++ /dev/null
@@ -1,65 +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.nearby.halfsheet.constants;
-
-/**
- * String constant for half sheet.
- */
-public class Constant {
-    public static final String TAG = "FastPairHalfSheet";
-    private static final String PREFIX = "com.android.nearby.halfsheet.";
-
-    // Intent extra
-    public static final String EXTRA_BINDER = "com.android.server.nearby.fastpair.BINDER";
-    public static final String EXTRA_BUNDLE = "com.android.server.nearby.fastpair.BUNDLE_EXTRA";
-
-    public static final String EXTRA_TITLE = PREFIX + "HALF_SHEET_TITLE";
-    public static final String EXTRA_DESCRIPTION = PREFIX + "HALF_SHEET_DESCRIPTION";
-    public static final String EXTRA_HALF_SHEET_ID = PREFIX + "HALF_SHEET_ID";
-    public static final String EXTRA_HALF_SHEET_INFO = PREFIX + "HALF_SHEET";
-    public static final String EXTRA_HALF_SHEET_TYPE = PREFIX + "HALF_SHEET_TYPE";
-    public static final String EXTRA_HALF_SHEET_ACCOUNT_NAME = PREFIX + "HALF_SHEET_ACCOUNT_NAME";
-    public static final String EXTRA_HALF_SHEET_CONTENT = PREFIX + "HALF_SHEET_CONTENT";
-    public static final String EXTRA_HALF_SHEET_FOREGROUND =
-            PREFIX + "EXTRA_HALF_SHEET_FOREGROUND";
-    public static final String EXTRA_HALF_SHEET_IS_RETROACTIVE =
-            PREFIX + "HALF_SHEET_IS_RETROACTIVE";
-    public static final String EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR =
-            PREFIX + "HALF_SHEET_IS_SUBSEQUENT_PAIR";
-    public static final String EXTRA_HALF_SHEET_PAIRING_RESURFACE =
-            PREFIX + "EXTRA_HALF_SHEET_PAIRING_RESURFACE";
-
-    // Intent Actions
-    public static final String ACTION_HALF_SHEET_FOREGROUND_STATE =
-            PREFIX + "ACTION_HALF_SHEET_FOREGROUND_STATE";
-    public static final String ACTION_FAST_PAIR_HALF_SHEET_CANCEL =
-            "com.android.nearby.ACTION_FAST_PAIR_HALF_SHEET_CANCEL";
-    public static final String ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET =
-            "com.android.nearby.ACTION_FAST_PAIR_BAN_STATE_RESET";
-    public static final String ACTION_RESOURCES_APK =
-            "android.nearby.SHOW_HALFSHEET";
-    public static final String ACTION_FAST_PAIR = PREFIX + "ACTION_MAGIC_PAIR";
-
-    public static final String RESULT_FAIL = "RESULT_FAIL";
-    public static final String ARG_FRAGMENT_STATE = "ARG_FRAGMENT_STATE";
-    public static final String DEVICE_PAIRING_FRAGMENT_TYPE = "DEVICE_PAIRING";
-
-    // Content url for help page about Fast Pair in half sheet.
-    // Todo(b/246007000): Add a flag to set up content url of the help page.
-    public static final String FAST_PAIR_HALF_SHEET_HELP_URL =
-            "https://support.google.com/android/answer/9075925";
-}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/FastPairConstants.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/FastPairConstants.java
deleted file mode 100644
index 7cfd33a..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/FastPairConstants.java
+++ /dev/null
@@ -1,26 +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.nearby.halfsheet.constants;
-
-/** Constants to share with other team. */
-public class FastPairConstants {
-    private static final String PACKAGE_NAME = "com.android.server.nearby";
-    public static final String PREFIX = PACKAGE_NAME + ".common.bluetooth.fastpair.";
-
-    /** MODEL_ID item name for extended intent field. */
-    public static final String EXTRA_MODEL_ID = PREFIX + "MODEL_ID";
-}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/UserActionHandlerBase.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/UserActionHandlerBase.java
deleted file mode 100644
index 767c6d6..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/UserActionHandlerBase.java
+++ /dev/null
@@ -1,29 +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.nearby.halfsheet.constants;
-
-/** Handles intents to {@link com.android.server.nearby.fastpair.FastPairManager}. */
-public class UserActionHandlerBase {
-    public static final String PREFIX = "com.android.server.nearby.fastpair.";
-    public static final String ACTION_PREFIX = "com.android.server.nearby:";
-
-    public static final String EXTRA_ITEM_ID = PREFIX + "EXTRA_ITEM_ID";
-    public static final String EXTRA_COMPANION_APP = ACTION_PREFIX + "EXTRA_COMPANION_APP";
-    public static final String EXTRA_MAC_ADDRESS = PREFIX + "EXTRA_MAC_ADDRESS";
-
-    public static final String ACTION_FAST_PAIR = ACTION_PREFIX + "ACTION_FAST_PAIR";
-}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java
deleted file mode 100644
index 9f5c915..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java
+++ /dev/null
@@ -1,507 +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.nearby.halfsheet.fragment;
-
-import static android.text.TextUtils.isEmpty;
-
-import static com.android.nearby.halfsheet.constants.Constant.ARG_FRAGMENT_STATE;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_BINDER;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_BUNDLE;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_DESCRIPTION;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_ACCOUNT_NAME;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_CONTENT;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_ID;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_INFO;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_TITLE;
-import static com.android.nearby.halfsheet.constants.Constant.FAST_PAIR_HALF_SHEET_HELP_URL;
-import static com.android.nearby.halfsheet.constants.Constant.RESULT_FAIL;
-import static com.android.nearby.halfsheet.constants.Constant.TAG;
-import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.FAILED;
-import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.FOUND_DEVICE;
-import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.NOT_STARTED;
-import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRED_LAUNCHABLE;
-import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRED_UNLAUNCHABLE;
-import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRING;
-
-import android.bluetooth.BluetoothDevice;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.nearby.FastPairDevice;
-import android.nearby.FastPairStatusCallback;
-import android.nearby.NearbyDevice;
-import android.nearby.PairStatusMetadata;
-import android.os.Bundle;
-import android.provider.Settings;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.nearby.halfsheet.FastPairUiServiceClient;
-import com.android.nearby.halfsheet.HalfSheetActivity;
-import com.android.nearby.halfsheet.R;
-import com.android.nearby.halfsheet.utils.FastPairUtils;
-import com.android.nearby.halfsheet.utils.HelpUtils;
-import com.android.nearby.halfsheet.utils.IconUtils;
-
-import com.google.protobuf.InvalidProtocolBufferException;
-
-import java.util.Objects;
-
-import service.proto.Cache.ScanFastPairStoreItem;
-
-/**
- * Modularize half sheet for fast pair this fragment will show when half sheet does device pairing.
- *
- * <p>This fragment will handle initial pairing subsequent pairing and retroactive pairing.
- */
-@SuppressWarnings("nullness")
-public class DevicePairingFragment extends HalfSheetModuleFragment implements
-        FastPairStatusCallback {
-    private TextView mTitleView;
-    private TextView mSubTitleView;
-    private ImageView mImage;
-
-    private Button mConnectButton;
-    private Button mSetupButton;
-    private Button mCancelButton;
-    // Opens Bluetooth Settings.
-    private Button mSettingsButton;
-    private ImageView mInfoIconButton;
-    private ProgressBar mConnectProgressBar;
-
-    private Bundle mBundle;
-
-    private ScanFastPairStoreItem mScanFastPairStoreItem;
-    private FastPairUiServiceClient mFastPairUiServiceClient;
-
-    private @PairStatusMetadata.Status int mPairStatus = PairStatusMetadata.Status.UNKNOWN;
-    // True when there is a companion app to open.
-    private boolean mIsLaunchable;
-    private boolean mIsConnecting;
-    // Indicates that the setup button is clicked before.
-    private boolean mSetupButtonClicked = false;
-
-    // Holds the new text while we transition between the two.
-    private static final int TAG_PENDING_TEXT = R.id.toolbar_title;
-    public static final String APP_LAUNCH_FRAGMENT_TYPE = "APP_LAUNCH";
-
-    private static final String ARG_SETUP_BUTTON_CLICKED = "SETUP_BUTTON_CLICKED";
-    private static final String ARG_PAIRING_RESULT = "PAIRING_RESULT";
-
-    /**
-     * Create certain fragment according to the intent.
-     */
-    @Nullable
-    public static HalfSheetModuleFragment newInstance(
-            Intent intent, @Nullable Bundle saveInstanceStates) {
-        Bundle args = new Bundle();
-        byte[] infoArray = intent.getByteArrayExtra(EXTRA_HALF_SHEET_INFO);
-
-        Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE);
-        String title = intent.getStringExtra(EXTRA_TITLE);
-        String description = intent.getStringExtra(EXTRA_DESCRIPTION);
-        String accountName = intent.getStringExtra(EXTRA_HALF_SHEET_ACCOUNT_NAME);
-        String result = intent.getStringExtra(EXTRA_HALF_SHEET_CONTENT);
-        int halfSheetId = intent.getIntExtra(EXTRA_HALF_SHEET_ID, 0);
-
-        args.putByteArray(EXTRA_HALF_SHEET_INFO, infoArray);
-        args.putString(EXTRA_HALF_SHEET_ACCOUNT_NAME, accountName);
-        args.putString(EXTRA_TITLE, title);
-        args.putString(EXTRA_DESCRIPTION, description);
-        args.putInt(EXTRA_HALF_SHEET_ID, halfSheetId);
-        args.putString(EXTRA_HALF_SHEET_CONTENT, result == null ? "" : result);
-        args.putBundle(EXTRA_BUNDLE, bundle);
-        if (saveInstanceStates != null) {
-            if (saveInstanceStates.containsKey(ARG_FRAGMENT_STATE)) {
-                args.putSerializable(
-                        ARG_FRAGMENT_STATE, saveInstanceStates.getSerializable(ARG_FRAGMENT_STATE));
-            }
-            if (saveInstanceStates.containsKey(BluetoothDevice.EXTRA_DEVICE)) {
-                args.putParcelable(
-                        BluetoothDevice.EXTRA_DEVICE,
-                        saveInstanceStates.getParcelable(BluetoothDevice.EXTRA_DEVICE));
-            }
-            if (saveInstanceStates.containsKey(BluetoothDevice.EXTRA_PAIRING_KEY)) {
-                args.putInt(
-                        BluetoothDevice.EXTRA_PAIRING_KEY,
-                        saveInstanceStates.getInt(BluetoothDevice.EXTRA_PAIRING_KEY));
-            }
-            if (saveInstanceStates.containsKey(ARG_SETUP_BUTTON_CLICKED)) {
-                args.putBoolean(
-                        ARG_SETUP_BUTTON_CLICKED,
-                        saveInstanceStates.getBoolean(ARG_SETUP_BUTTON_CLICKED));
-            }
-            if (saveInstanceStates.containsKey(ARG_PAIRING_RESULT)) {
-                args.putBoolean(ARG_PAIRING_RESULT,
-                        saveInstanceStates.getBoolean(ARG_PAIRING_RESULT));
-            }
-        }
-        DevicePairingFragment fragment = new DevicePairingFragment();
-        fragment.setArguments(args);
-        return fragment;
-    }
-
-    @Nullable
-    @Override
-    public View onCreateView(
-            LayoutInflater inflater, @Nullable ViewGroup container,
-            @Nullable Bundle savedInstanceState) {
-        /* attachToRoot= */
-        View rootView = inflater.inflate(
-                R.layout.fast_pair_device_pairing_fragment, container, /* attachToRoot= */
-                false);
-        if (getContext() == null) {
-            Log.d(TAG, "can't find the attached activity");
-            return rootView;
-        }
-
-        Bundle args = getArguments();
-        byte[] storeFastPairItemBytesArray = args.getByteArray(EXTRA_HALF_SHEET_INFO);
-        mBundle = args.getBundle(EXTRA_BUNDLE);
-        if (mBundle != null) {
-            mFastPairUiServiceClient =
-                    new FastPairUiServiceClient(getContext(), mBundle.getBinder(EXTRA_BINDER));
-            mFastPairUiServiceClient.registerHalfSheetStateCallBack(this);
-        }
-        if (args.containsKey(EXTRA_HALF_SHEET_CONTENT)) {
-            if (RESULT_FAIL.equals(args.getString(EXTRA_HALF_SHEET_CONTENT))) {
-                mPairStatus = PairStatusMetadata.Status.FAIL;
-            }
-        }
-        if (args.containsKey(ARG_FRAGMENT_STATE)) {
-            mFragmentState = (HalfSheetFragmentState) args.getSerializable(ARG_FRAGMENT_STATE);
-        }
-        if (args.containsKey(ARG_SETUP_BUTTON_CLICKED)) {
-            mSetupButtonClicked = args.getBoolean(ARG_SETUP_BUTTON_CLICKED);
-        }
-        if (args.containsKey(ARG_PAIRING_RESULT)) {
-            mPairStatus = args.getInt(ARG_PAIRING_RESULT);
-        }
-
-        // Initiate views.
-        mTitleView = Objects.requireNonNull(getActivity()).findViewById(R.id.toolbar_title);
-        mSubTitleView = rootView.findViewById(R.id.header_subtitle);
-        mImage = rootView.findViewById(R.id.pairing_pic);
-        mConnectProgressBar = rootView.findViewById(R.id.connect_progressbar);
-        mConnectButton = rootView.findViewById(R.id.connect_btn);
-        mCancelButton = rootView.findViewById(R.id.cancel_btn);
-        mSettingsButton = rootView.findViewById(R.id.settings_btn);
-        mSetupButton = rootView.findViewById(R.id.setup_btn);
-        mInfoIconButton = rootView.findViewById(R.id.info_icon);
-        mInfoIconButton.setImageResource(R.drawable.fast_pair_ic_info);
-
-        try {
-            setScanFastPairStoreItem(ScanFastPairStoreItem.parseFrom(storeFastPairItemBytesArray));
-        } catch (InvalidProtocolBufferException e) {
-            Log.w(TAG,
-                    "DevicePairingFragment: error happens when pass info to half sheet");
-            return rootView;
-        }
-
-        // Config for landscape mode
-        DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
-        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
-            rootView.getLayoutParams().height = displayMetrics.heightPixels * 4 / 5;
-            rootView.getLayoutParams().width = displayMetrics.heightPixels * 4 / 5;
-            mImage.getLayoutParams().height = displayMetrics.heightPixels / 2;
-            mImage.getLayoutParams().width = displayMetrics.heightPixels / 2;
-            mConnectProgressBar.getLayoutParams().width = displayMetrics.heightPixels / 2;
-            mConnectButton.getLayoutParams().width = displayMetrics.heightPixels / 2;
-            //TODO(b/213373051): Add cancel button
-        }
-
-        Bitmap icon = IconUtils.getIcon(mScanFastPairStoreItem.getIconPng().toByteArray(),
-                mScanFastPairStoreItem.getIconPng().size());
-        if (icon != null) {
-            mImage.setImageBitmap(icon);
-        }
-        mConnectButton.setOnClickListener(v -> onConnectClicked());
-        mCancelButton.setOnClickListener(v ->
-                ((HalfSheetActivity) getActivity()).onCancelClicked());
-        mSettingsButton.setOnClickListener(v -> onSettingsClicked());
-        mSetupButton.setOnClickListener(v -> onSetupClicked());
-        mInfoIconButton.setOnClickListener(v -> onHelpClicked());
-        return rootView;
-    }
-
-    @Override
-    public void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // Get access to the activity's menu
-        setHasOptionsMenu(true);
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        Log.v(TAG, "onStart: invalidate states");
-        // If the fragmentState is not NOT_STARTED, it is because the fragment was just resumed from
-        // configuration change (e.g. rotating the screen or half-sheet resurface). Let's recover
-        // the UI directly.
-        if (mFragmentState != NOT_STARTED) {
-            setState(mFragmentState);
-        } else {
-            invalidateState();
-        }
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle savedInstanceState) {
-        super.onSaveInstanceState(savedInstanceState);
-
-        savedInstanceState.putSerializable(ARG_FRAGMENT_STATE, mFragmentState);
-        savedInstanceState.putBoolean(ARG_SETUP_BUTTON_CLICKED, mSetupButtonClicked);
-        savedInstanceState.putInt(ARG_PAIRING_RESULT, mPairStatus);
-    }
-
-    private void onSettingsClicked() {
-        startActivity(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS));
-    }
-
-    private void onSetupClicked() {
-        String companionApp =
-                FastPairUtils.getCompanionAppFromActionUrl(mScanFastPairStoreItem.getActionUrl());
-        Intent intent =
-                FastPairUtils.createCompanionAppIntent(
-                        Objects.requireNonNull(getContext()),
-                        companionApp,
-                        mScanFastPairStoreItem.getAddress());
-        mSetupButtonClicked = true;
-        if (mFragmentState == PAIRED_LAUNCHABLE) {
-            if (intent != null) {
-                startActivity(intent);
-            }
-        } else {
-            Log.d(TAG, "onSetupClick: State is " + mFragmentState);
-        }
-    }
-
-    private void onConnectClicked() {
-        if (mScanFastPairStoreItem == null) {
-            Log.w(TAG, "No pairing related information in half sheet");
-            return;
-        }
-        if (getFragmentState() == PAIRING) {
-            return;
-        }
-        mIsConnecting = true;
-        invalidateState();
-        mFastPairUiServiceClient.connect(
-                new FastPairDevice.Builder()
-                        .addMedium(NearbyDevice.Medium.BLE)
-                        .setBluetoothAddress(mScanFastPairStoreItem.getAddress())
-                        .setData(FastPairUtils.convertFrom(mScanFastPairStoreItem)
-                                .toByteArray())
-                        .build());
-    }
-
-    private void onHelpClicked() {
-        HelpUtils.showHelpPage(getContext(), FAST_PAIR_HALF_SHEET_HELP_URL);
-        ((HalfSheetActivity) getActivity()).sendBanStateResetBroadcast();
-        getActivity().finish();
-    }
-
-    // Receives callback from service.
-    @Override
-    public void onPairUpdate(FastPairDevice fastPairDevice, PairStatusMetadata pairStatusMetadata) {
-        @PairStatusMetadata.Status int status = pairStatusMetadata.getStatus();
-        if (status == PairStatusMetadata.Status.DISMISS && getActivity() != null) {
-            getActivity().finish();
-        }
-        mIsConnecting = false;
-        mPairStatus = status;
-        invalidateState();
-    }
-
-    @Override
-    public void invalidateState() {
-        HalfSheetFragmentState newState = NOT_STARTED;
-        if (mIsConnecting) {
-            newState = PAIRING;
-        } else {
-            switch (mPairStatus) {
-                case PairStatusMetadata.Status.SUCCESS:
-                    newState = mIsLaunchable ? PAIRED_LAUNCHABLE : PAIRED_UNLAUNCHABLE;
-                    break;
-                case PairStatusMetadata.Status.FAIL:
-                    newState = FAILED;
-                    break;
-                default:
-                    if (mScanFastPairStoreItem != null) {
-                        newState = FOUND_DEVICE;
-                    }
-            }
-        }
-        if (newState == mFragmentState) {
-            return;
-        }
-        setState(newState);
-    }
-
-    @Override
-    public void setState(HalfSheetFragmentState state) {
-        super.setState(state);
-        invalidateTitles();
-        invalidateButtons();
-    }
-
-    private void setScanFastPairStoreItem(ScanFastPairStoreItem item) {
-        mScanFastPairStoreItem = item;
-        invalidateLaunchable();
-    }
-
-    private void invalidateLaunchable() {
-        String companionApp =
-                FastPairUtils.getCompanionAppFromActionUrl(mScanFastPairStoreItem.getActionUrl());
-        if (isEmpty(companionApp)) {
-            mIsLaunchable = false;
-            return;
-        }
-        mIsLaunchable =
-                FastPairUtils.isLaunchable(Objects.requireNonNull(getContext()), companionApp);
-    }
-
-    private void invalidateButtons() {
-        mConnectProgressBar.setVisibility(View.INVISIBLE);
-        mConnectButton.setVisibility(View.INVISIBLE);
-        mCancelButton.setVisibility(View.INVISIBLE);
-        mSetupButton.setVisibility(View.INVISIBLE);
-        mSettingsButton.setVisibility(View.INVISIBLE);
-        mInfoIconButton.setVisibility(View.INVISIBLE);
-
-        switch (mFragmentState) {
-            case FOUND_DEVICE:
-                mInfoIconButton.setVisibility(View.VISIBLE);
-                mConnectButton.setVisibility(View.VISIBLE);
-                break;
-            case PAIRING:
-                mConnectProgressBar.setVisibility(View.VISIBLE);
-                mCancelButton.setVisibility(View.VISIBLE);
-                setBackgroundClickable(false);
-                break;
-            case PAIRED_LAUNCHABLE:
-                mCancelButton.setVisibility(View.VISIBLE);
-                mSetupButton.setVisibility(View.VISIBLE);
-                setBackgroundClickable(true);
-                break;
-            case FAILED:
-                mSettingsButton.setVisibility(View.VISIBLE);
-                setBackgroundClickable(true);
-                break;
-            case NOT_STARTED:
-            case PAIRED_UNLAUNCHABLE:
-            default:
-                mCancelButton.setVisibility(View.VISIBLE);
-                setBackgroundClickable(true);
-        }
-    }
-
-    private void setBackgroundClickable(boolean isClickable) {
-        HalfSheetActivity activity = (HalfSheetActivity) getActivity();
-        if (activity == null) {
-            Log.w(TAG, "setBackgroundClickable: failed to set clickable to " + isClickable
-                    + " because cannot get HalfSheetActivity.");
-            return;
-        }
-        View background = activity.findViewById(R.id.background);
-        if (background == null) {
-            Log.w(TAG, "setBackgroundClickable: failed to set clickable to " + isClickable
-                    + " cannot find background at HalfSheetActivity.");
-            return;
-        }
-        Log.d(TAG, "setBackgroundClickable to " + isClickable);
-        background.setClickable(isClickable);
-    }
-
-    private void invalidateTitles() {
-        String newTitle = getTitle();
-        invalidateTextView(mTitleView, newTitle);
-        String newSubTitle = getSubTitle();
-        invalidateTextView(mSubTitleView, newSubTitle);
-    }
-
-    private void invalidateTextView(TextView textView, String newText) {
-        CharSequence oldText =
-                textView.getTag(TAG_PENDING_TEXT) != null
-                        ? (CharSequence) textView.getTag(TAG_PENDING_TEXT)
-                        : textView.getText();
-        if (TextUtils.equals(oldText, newText)) {
-            return;
-        }
-        if (TextUtils.isEmpty(oldText)) {
-            // First time run. Don't animate since there's nothing to animate from.
-            textView.setText(newText);
-        } else {
-            textView.setTag(TAG_PENDING_TEXT, newText);
-            textView
-                    .animate()
-                    .alpha(0f)
-                    .setDuration(TEXT_ANIMATION_DURATION_MILLISECONDS)
-                    .withEndAction(
-                            () -> {
-                                textView.setText(newText);
-                                textView
-                                        .animate()
-                                        .alpha(1f)
-                                        .setDuration(TEXT_ANIMATION_DURATION_MILLISECONDS);
-                            });
-        }
-    }
-
-    private String getTitle() {
-        switch (mFragmentState) {
-            case PAIRED_LAUNCHABLE:
-                return getString(R.string.fast_pair_title_setup);
-            case FAILED:
-                return getString(R.string.fast_pair_title_fail);
-            case FOUND_DEVICE:
-            case NOT_STARTED:
-            case PAIRED_UNLAUNCHABLE:
-            default:
-                return mScanFastPairStoreItem.getDeviceName();
-        }
-    }
-
-    private String getSubTitle() {
-        switch (mFragmentState) {
-            case PAIRED_LAUNCHABLE:
-                return String.format(
-                        mScanFastPairStoreItem
-                                .getFastPairStrings()
-                                .getPairingFinishedCompanionAppInstalled(),
-                        mScanFastPairStoreItem.getDeviceName());
-            case FAILED:
-                return mScanFastPairStoreItem.getFastPairStrings().getPairingFailDescription();
-            case PAIRED_UNLAUNCHABLE:
-                return getString(R.string.fast_pair_device_ready);
-            case FOUND_DEVICE:
-            case NOT_STARTED:
-                return mScanFastPairStoreItem.getFastPairStrings().getInitialPairingDescription();
-            default:
-                return "";
-        }
-    }
-}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java
deleted file mode 100644
index d87c015..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java
+++ /dev/null
@@ -1,77 +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.nearby.halfsheet.fragment;
-
-import static com.android.nearby.halfsheet.constants.Constant.TAG;
-import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.NOT_STARTED;
-
-import android.os.Bundle;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-
-
-/** Base class for all of the half sheet fragment. */
-public abstract class HalfSheetModuleFragment extends Fragment {
-
-    static final int TEXT_ANIMATION_DURATION_MILLISECONDS = 200;
-
-    HalfSheetFragmentState mFragmentState = NOT_STARTED;
-
-    @Override
-    public void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-    }
-
-    /** UI states of the half-sheet fragment. */
-    public enum HalfSheetFragmentState {
-        NOT_STARTED, // Initial status
-        FOUND_DEVICE, // When a device is found found from Nearby scan service
-        PAIRING, // When user taps 'Connect' and Fast Pair stars pairing process
-        PAIRED_LAUNCHABLE, // When pair successfully
-        // and we found a launchable companion app installed
-        PAIRED_UNLAUNCHABLE, // When pair successfully
-        // but we cannot find a companion app to launch it
-        FAILED, // When paring was failed
-        FINISHED // When the activity is about to end finished.
-    }
-
-    /**
-     * Returns the {@link HalfSheetFragmentState} to the parent activity.
-     *
-     * <p>Overrides this method if the fragment's state needs to be preserved in the parent
-     * activity.
-     */
-    public HalfSheetFragmentState getFragmentState() {
-        return mFragmentState;
-    }
-
-    void setState(HalfSheetFragmentState state) {
-        Log.v(TAG, "Settings state from " + mFragmentState + " to " + state);
-        mFragmentState = state;
-    }
-
-    /**
-     * Populate data to UI widgets according to the latest {@link HalfSheetFragmentState}.
-     */
-    abstract void invalidateState();
-}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java
deleted file mode 100644
index 2f1e90a..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java
+++ /dev/null
@@ -1,43 +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.nearby.halfsheet.utils;
-
-import android.content.Context;
-import android.content.Intent;
-
-/**
- * Broadcast util class
- */
-public class BroadcastUtils {
-
-    /**
-     * Helps send broadcast.
-     */
-    public static void sendBroadcast(Context context, Intent intent) {
-        context.sendBroadcast(intent);
-    }
-
-    /**
-     * Helps send a broadcast with specified receiver permission.
-     */
-    public static void sendBroadcast(Context context, Intent intent, String receiverPermission) {
-        context.sendBroadcast(intent, receiverPermission);
-    }
-
-    private BroadcastUtils() {
-    }
-}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java
deleted file mode 100644
index a1588a9..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java
+++ /dev/null
@@ -1,150 +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.nearby.halfsheet.utils;
-
-import static com.android.nearby.halfsheet.constants.UserActionHandlerBase.ACTION_FAST_PAIR;
-import static com.android.nearby.halfsheet.constants.UserActionHandlerBase.EXTRA_COMPANION_APP;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.net.URISyntaxException;
-
-import service.proto.Cache;
-
-/**
- * Util class in half sheet apk
- */
-public class FastPairUtils {
-
-    /** FastPair util method check certain app is install on the device or not. */
-    public static boolean isAppInstalled(Context context, String packageName) {
-        try {
-            context.getPackageManager().getPackageInfo(packageName, 0);
-            return true;
-        } catch (PackageManager.NameNotFoundException e) {
-            return false;
-        }
-    }
-
-    /** FastPair util method to properly format the action url extra. */
-    @Nullable
-    public static String getCompanionAppFromActionUrl(String actionUrl) {
-        try {
-            Intent intent = Intent.parseUri(actionUrl, Intent.URI_INTENT_SCHEME);
-            if (!intent.getAction().equals(ACTION_FAST_PAIR)) {
-                Log.e("FastPairUtils", "Companion app launch attempted from malformed action url");
-                return null;
-            }
-            return intent.getStringExtra(EXTRA_COMPANION_APP);
-        } catch (URISyntaxException e) {
-            Log.e("FastPairUtils", "FastPair: fail to get companion app info from discovery item");
-            return null;
-        }
-    }
-
-    /**
-     * Converts {@link service.proto.Cache.StoredDiscoveryItem} from
-     * {@link service.proto.Cache.ScanFastPairStoreItem}
-     */
-    public static Cache.StoredDiscoveryItem convertFrom(Cache.ScanFastPairStoreItem item) {
-        return convertFrom(item, /* isSubsequentPair= */ false);
-    }
-
-    /**
-     * Converts a {@link service.proto.Cache.ScanFastPairStoreItem}
-     * to a {@link service.proto.Cache.StoredDiscoveryItem}.
-     *
-     * <p>This is needed to make the new Fast Pair scanning stack compatible with the rest of the
-     * legacy Fast Pair code.
-     */
-    public static Cache.StoredDiscoveryItem convertFrom(
-            Cache.ScanFastPairStoreItem item, boolean isSubsequentPair) {
-        return Cache.StoredDiscoveryItem.newBuilder()
-                .setId(item.getModelId())
-                .setFirstObservationTimestampMillis(item.getFirstObservationTimestampMillis())
-                .setLastObservationTimestampMillis(item.getLastObservationTimestampMillis())
-                .setActionUrl(item.getActionUrl())
-                .setActionUrlType(Cache.ResolvedUrlType.APP)
-                .setTitle(
-                        isSubsequentPair
-                                ? item.getFastPairStrings().getTapToPairWithoutAccount()
-                                : item.getDeviceName())
-                .setMacAddress(item.getAddress())
-                .setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED)
-                .setTriggerId(item.getModelId())
-                .setIconPng(item.getIconPng())
-                .setIconFifeUrl(item.getIconFifeUrl())
-                .setDescription(
-                        isSubsequentPair
-                                ? item.getDeviceName()
-                                : item.getFastPairStrings().getTapToPairWithoutAccount())
-                .setAuthenticationPublicKeySecp256R1(item.getAntiSpoofingPublicKey())
-                .setCompanionDetail(item.getCompanionDetail())
-                .setFastPairStrings(item.getFastPairStrings())
-                .setFastPairInformation(
-                        Cache.FastPairInformation.newBuilder()
-                                .setDataOnlyConnection(item.getDataOnlyConnection())
-                                .setTrueWirelessImages(item.getTrueWirelessImages())
-                                .setAssistantSupported(item.getAssistantSupported())
-                                .setCompanyName(item.getCompanyName()))
-                .build();
-    }
-
-    /**
-     * Returns true the application is installed and can be opened on device.
-     */
-    public static boolean isLaunchable(@NonNull Context context, String companionApp) {
-        return isAppInstalled(context, companionApp)
-                && createCompanionAppIntent(context, companionApp, null) != null;
-    }
-
-    /**
-     * Returns an intent to launch given the package name and bluetooth address (if provided).
-     * Returns null if no such an intent can be found.
-     */
-    @Nullable
-    public static Intent createCompanionAppIntent(@NonNull Context context, String packageName,
-            @Nullable String address) {
-        Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
-        if (intent == null) {
-            return null;
-        }
-        if (address != null) {
-            BluetoothAdapter adapter = getBluetoothAdapter(context);
-            if (adapter != null) {
-                intent.putExtra(BluetoothDevice.EXTRA_DEVICE, adapter.getRemoteDevice(address));
-            }
-        }
-        return intent;
-    }
-
-    @Nullable
-    private static BluetoothAdapter getBluetoothAdapter(@NonNull Context context) {
-        BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
-        return bluetoothManager == null ? null : bluetoothManager.getAdapter();
-    }
-
-    private FastPairUtils() {}
-}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/HelpUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/HelpUtils.java
deleted file mode 100644
index 98f2242..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/HelpUtils.java
+++ /dev/null
@@ -1,54 +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.nearby.halfsheet.utils;
-
-import static com.android.nearby.halfsheet.constants.Constant.TAG;
-
-import static java.util.Objects.requireNonNull;
-
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.util.Log;
-
-/**
- * Util class for launching help page in Fast Pair.
- */
-public class HelpUtils {
-    /**
-     * Sets up the info button to launch a help page
-     */
-    public static void showHelpPage(Context context, String url)  {
-        requireNonNull(context, "context cannot be null");
-        requireNonNull(url, "url cannot be null");
-
-        try {
-            context.startActivity(createHelpPageIntent(url));
-        } catch (ActivityNotFoundException e) {
-            Log.w(TAG, "Failed to find activity for url " + url, e);
-        }
-    }
-
-    /**
-     * Creates the intent for help page
-     */
-    private static Intent createHelpPageIntent(String url) {
-        return new Intent(Intent.ACTION_VIEW, Uri.parse(url)).setFlags(
-                Intent.FLAG_ACTIVITY_NEW_TASK);
-    }
-}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java
deleted file mode 100644
index e547369..0000000
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java
+++ /dev/null
@@ -1,92 +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.nearby.halfsheet.utils;
-
-import static com.android.nearby.halfsheet.constants.Constant.TAG;
-
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-/**
- * Utility class for icon size verification.
- */
-public class IconUtils {
-
-    private static final float NOTIFICATION_BACKGROUND_PADDING_PERCENT = 0.125f;
-    private static final float NOTIFICATION_BACKGROUND_ALPHA = 0.7f;
-    private static final int MIN_ICON_SIZE = 16;
-    private static final int DESIRED_ICON_SIZE = 32;
-
-    /**
-     * Verify that the icon is non null and falls in the small bucket. Just because an icon isn't
-     * small doesn't guarantee it is large or exists.
-     */
-    public static boolean isIconSizedSmall(@Nullable Bitmap bitmap) {
-        if (bitmap == null) {
-            return false;
-        }
-        return bitmap.getWidth() >= MIN_ICON_SIZE
-                && bitmap.getWidth() < DESIRED_ICON_SIZE
-                && bitmap.getHeight() >= MIN_ICON_SIZE
-                && bitmap.getHeight() < DESIRED_ICON_SIZE;
-    }
-
-    /**
-     * Verify that the icon is non null and falls in the regular / default size bucket. Doesn't
-     * guarantee if not regular then it is small.
-     */
-    static boolean isIconSizedRegular(@Nullable Bitmap bitmap) {
-        if (bitmap == null) {
-            return false;
-        }
-        return bitmap.getWidth() >= DESIRED_ICON_SIZE && bitmap.getHeight() >= DESIRED_ICON_SIZE;
-    }
-
-    /**
-     * All icons that are sized correctly (larger than the MIN_ICON_SIZE icon size)
-     * are resize on the server to the DESIRED_ICON_SIZE icon size so that
-     * they appear correct.
-     */
-    public static boolean isIconSizeCorrect(@Nullable Bitmap bitmap) {
-        if (bitmap == null) {
-            return false;
-        }
-        return isIconSizedSmall(bitmap) || isIconSizedRegular(bitmap);
-    }
-
-    /**
-     * Returns the bitmap from the byte array. Returns null if cannot decode or not in correct size.
-     */
-    @Nullable
-    public static Bitmap getIcon(byte[] imageData, int size) {
-        try {
-            Bitmap icon =
-                    BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, size);
-            if (IconUtils.isIconSizeCorrect(icon)) {
-                // Do not add background for Half Sheet.
-                return icon;
-            }
-        } catch (OutOfMemoryError e) {
-            Log.w(TAG, "getIcon: Failed to decode icon, returning null.", e);
-        }
-        return null;
-    }
-}
-
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index d860048..749113d 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -24,47 +25,13 @@
     ],
 }
 
-// Common lib for nearby end-to-end testing.
-java_library {
-    name: "nearby-common-lib",
-    srcs: [
-        "java/com/android/server/nearby/common/bloomfilter/*.java",
-        "java/com/android/server/nearby/common/bluetooth/*.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryption.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddress.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuids.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/Constants.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchange.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/Ltv.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java",
-        "java/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoder.java",
-        "java/com/android/server/nearby/common/bluetooth/testability/**/*.java",
-        "java/com/android/server/nearby/common/bluetooth/gatt/*.java",
-        "java/com/android/server/nearby/common/bluetooth/util/*.java",
-    ],
-    libs: [
-        "androidx.annotation_annotation",
-        "androidx.core_core",
-        "error_prone_annotations",
-        "framework-bluetooth",
-        "guava",
-    ],
-    sdk_version: "module_current",
-    visibility: [
-        "//packages/modules/Connectivity/nearby/tests/multidevices/clients/test_support/fastpair_provider",
-    ],
-}
-
 // Main lib for nearby services.
 java_library {
     name: "service-nearby-pre-jarjar",
     srcs: [":nearby-service-srcs"],
 
     defaults: [
-        "framework-system-server-module-defaults"
+        "framework-system-server-module-defaults",
     ],
     libs: [
         "androidx.annotation_annotation",
@@ -73,13 +40,12 @@
         "framework-configinfrastructure",
         "framework-connectivity-t.impl",
         "framework-statsd",
-        "HalfSheetUX",
     ],
     static_libs: [
         "androidx.core_core",
+        "android.hardware.bluetooth.finder-V1-java",
         "guava",
         "libprotobuf-java-lite",
-        "fast-pair-lite-protos",
         "modules-utils-build",
         "modules-utils-handlerexecutor",
         "modules-utils-preconditions",
@@ -88,7 +54,7 @@
     ],
     sdk_version: "system_server_current",
     // This is included in service-connectivity which is 30+
-    // TODO: allow APEXes to have service jars with higher min_sdk than the APEX
+    // 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",
 
@@ -102,13 +68,16 @@
     apex_available: [
         "com.android.tethering",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 genrule {
     name: "statslog-nearby-java-gen",
     tools: ["stats-log-api-gen"],
     cmd: "$(location stats-log-api-gen) --java $(out) --module nearby " +
-         " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
-         " --minApiLevel 33",
+        " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
+        " --minApiLevel 33",
     out: ["com/android/server/nearby/proto/NearbyStatsLog.java"],
 }
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 9b32d69..9ef905d 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -54,6 +54,11 @@
     public static final String NEARBY_REFACTOR_DISCOVERY_MANAGER =
             "nearby_refactor_discovery_manager";
 
+    /**
+     * Flag to guard enable BLE during Nearby Service init time.
+     */
+    public static final String NEARBY_ENABLE_BLE_IN_INIT = "nearby_enable_ble_in_init";
+
     private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
 
     private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
@@ -67,6 +72,8 @@
     private boolean mSupportTestApp;
     @GuardedBy("mDeviceConfigLock")
     private boolean mRefactorDiscoveryManager;
+    @GuardedBy("mDeviceConfigLock")
+    private boolean mEnableBleInInit;
 
     public NearbyConfiguration() {
         mDeviceConfigListener.start();
@@ -131,6 +138,15 @@
         }
     }
 
+    /**
+     * @return {@code true} if enableBLE() is called during NearbyService init time.
+     */
+    public boolean enableBleInInit() {
+        synchronized (mDeviceConfigLock) {
+            return mEnableBleInInit;
+        }
+    }
+
     private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
         public void start() {
             DeviceConfig.addOnPropertiesChangedListener(getNamespace(),
@@ -149,6 +165,8 @@
                         NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
                 mRefactorDiscoveryManager = getDeviceConfigBoolean(
                         NEARBY_REFACTOR_DISCOVERY_MANAGER, false /* defaultValue */);
+                mEnableBleInInit = getDeviceConfigBoolean(
+                        NEARBY_ENABLE_BLE_IN_INIT, true /* defaultValue */);
             }
         }
     }
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 2ac2b00..1575f07 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -18,7 +18,6 @@
 
 import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
-import static com.android.server.SystemService.PHASE_THIRD_PARTY_APPS_CAN_START;
 
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -36,24 +35,25 @@
 import android.nearby.INearbyManager;
 import android.nearby.IScanListener;
 import android.nearby.NearbyManager;
+import android.nearby.PoweredOffFindingEphemeralId;
 import android.nearby.ScanRequest;
 import android.nearby.aidl.IOffloadCallback;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.FastPairManager;
 import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.managers.BluetoothFinderManager;
 import com.android.server.nearby.managers.BroadcastProviderManager;
 import com.android.server.nearby.managers.DiscoveryManager;
 import com.android.server.nearby.managers.DiscoveryProviderManager;
 import com.android.server.nearby.managers.DiscoveryProviderManagerLegacy;
 import com.android.server.nearby.presence.PresenceManager;
-import com.android.server.nearby.provider.FastPairDataProvider;
 import com.android.server.nearby.util.identity.CallerIdentity;
 import com.android.server.nearby.util.permissions.BroadcastPermissions;
 import com.android.server.nearby.util.permissions.DiscoveryPermissions;
 
+import java.util.List;
+
 /** Service implementing nearby functionality. */
 public class NearbyService extends INearbyManager.Stub {
     public static final String TAG = "NearbyService";
@@ -61,7 +61,6 @@
     public static final Boolean MANUAL_TEST = false;
 
     private final Context mContext;
-    private final FastPairManager mFastPairManager;
     private final PresenceManager mPresenceManager;
     private final NearbyConfiguration mNearbyConfiguration;
     private Injector mInjector;
@@ -84,19 +83,19 @@
             };
     private final DiscoveryManager mDiscoveryProviderManager;
     private final BroadcastProviderManager mBroadcastProviderManager;
+    private final BluetoothFinderManager mBluetoothFinderManager;
 
     public NearbyService(Context context) {
         mContext = context;
         mInjector = new SystemInjector(context);
         mBroadcastProviderManager = new BroadcastProviderManager(context, mInjector);
-        final LocatorContextWrapper lcw = new LocatorContextWrapper(context, null);
-        mFastPairManager = new FastPairManager(lcw);
-        mPresenceManager = new PresenceManager(lcw);
+        mPresenceManager = new PresenceManager(context);
         mNearbyConfiguration = new NearbyConfiguration();
         mDiscoveryProviderManager =
                 mNearbyConfiguration.refactorDiscoveryManager()
                         ? new DiscoveryProviderManager(context, mInjector)
                         : new DiscoveryProviderManagerLegacy(context, mInjector);
+        mBluetoothFinderManager = new BluetoothFinderManager();
     }
 
     @VisibleForTesting
@@ -155,6 +154,30 @@
         mDiscoveryProviderManager.queryOffloadCapability(callback);
     }
 
+    @Override
+    public void setPoweredOffFindingEphemeralIds(List<PoweredOffFindingEphemeralId> eids) {
+        // Permissions check
+        enforceBluetoothPrivilegedPermission(mContext);
+
+        mBluetoothFinderManager.sendEids(eids);
+    }
+
+    @Override
+    public void setPoweredOffModeEnabled(boolean enabled) {
+        // Permissions check
+        enforceBluetoothPrivilegedPermission(mContext);
+
+        mBluetoothFinderManager.setPoweredOffFinderMode(enabled);
+    }
+
+    @Override
+    public boolean getPoweredOffModeEnabled() {
+        // Permissions check
+        enforceBluetoothPrivilegedPermission(mContext);
+
+        return mBluetoothFinderManager.getPoweredOffFinderMode();
+    }
+
     /**
      * Called by the service initializer.
      *
@@ -167,10 +190,6 @@
                     ((SystemInjector) mInjector).initializeAppOpsManager();
                 }
                 break;
-            case PHASE_THIRD_PARTY_APPS_CAN_START:
-                // Ensures that a fast pair data provider exists which will work in direct boot.
-                FastPairDataProvider.init(mContext);
-                break;
             case PHASE_BOOT_COMPLETED:
                 // mInjector needs to be initialized before mProviderManager.
                 if (mInjector instanceof SystemInjector) {
@@ -183,7 +202,6 @@
                 mContext.registerReceiver(
                         mBluetoothReceiver,
                         new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
-                mFastPairManager.initiate();
                 // Only enable for manual Presence test on device.
                 if (MANUAL_TEST) {
                     mPresenceManager.initiate();
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java b/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
deleted file mode 100644
index dc4e11e..0000000
--- a/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
+++ /dev/null
@@ -1,753 +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.server.nearby.common.ble;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.le.ScanFilter;
-import android.os.Parcel;
-import android.os.ParcelUuid;
-import android.os.Parcelable;
-import android.text.TextUtils;
-
-import androidx.annotation.Nullable;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.UUID;
-
-/**
- * Criteria for filtering BLE devices. A {@link BleFilter} allows clients to restrict BLE devices to
- * only those that are of interest to them.
- *
- *
- * <p>Current filtering on the following fields are supported:
- * <li>Service UUIDs which identify the bluetooth gatt services running on the device.
- * <li>Name of remote Bluetooth LE device.
- * <li>Mac address of the remote device.
- * <li>Service data which is the data associated with a service.
- * <li>Manufacturer specific data which is the data associated with a particular manufacturer.
- *
- * @see BleSighting
- */
-public final class BleFilter implements Parcelable {
-
-    @Nullable
-    private String mDeviceName;
-
-    @Nullable
-    private String mDeviceAddress;
-
-    @Nullable
-    private ParcelUuid mServiceUuid;
-
-    @Nullable
-    private ParcelUuid mServiceUuidMask;
-
-    @Nullable
-    private ParcelUuid mServiceDataUuid;
-
-    @Nullable
-    private byte[] mServiceData;
-
-    @Nullable
-    private byte[] mServiceDataMask;
-
-    private int mManufacturerId;
-
-    @Nullable
-    private byte[] mManufacturerData;
-
-    @Nullable
-    private byte[] mManufacturerDataMask;
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    BleFilter() {
-    }
-
-    BleFilter(
-            @Nullable String deviceName,
-            @Nullable String deviceAddress,
-            @Nullable ParcelUuid serviceUuid,
-            @Nullable ParcelUuid serviceUuidMask,
-            @Nullable ParcelUuid serviceDataUuid,
-            @Nullable byte[] serviceData,
-            @Nullable byte[] serviceDataMask,
-            int manufacturerId,
-            @Nullable byte[] manufacturerData,
-            @Nullable byte[] manufacturerDataMask) {
-        this.mDeviceName = deviceName;
-        this.mDeviceAddress = deviceAddress;
-        this.mServiceUuid = serviceUuid;
-        this.mServiceUuidMask = serviceUuidMask;
-        this.mServiceDataUuid = serviceDataUuid;
-        this.mServiceData = serviceData;
-        this.mServiceDataMask = serviceDataMask;
-        this.mManufacturerId = manufacturerId;
-        this.mManufacturerData = manufacturerData;
-        this.mManufacturerDataMask = manufacturerDataMask;
-    }
-
-    public static final Parcelable.Creator<BleFilter> CREATOR = new Creator<BleFilter>() {
-        @Override
-        public BleFilter createFromParcel(Parcel source) {
-            BleFilter nBleFilter = new BleFilter();
-            nBleFilter.mDeviceName = source.readString();
-            nBleFilter.mDeviceAddress = source.readString();
-            nBleFilter.mManufacturerId = source.readInt();
-            nBleFilter.mManufacturerData = source.marshall();
-            nBleFilter.mManufacturerDataMask = source.marshall();
-            nBleFilter.mServiceDataUuid = source.readParcelable(null);
-            nBleFilter.mServiceData = source.marshall();
-            nBleFilter.mServiceDataMask = source.marshall();
-            nBleFilter.mServiceUuid = source.readParcelable(null);
-            nBleFilter.mServiceUuidMask = source.readParcelable(null);
-            return nBleFilter;
-        }
-
-        @Override
-        public BleFilter[] newArray(int size) {
-            return new BleFilter[size];
-        }
-    };
-
-
-    /** Returns the filter set on the device name field of Bluetooth advertisement data. */
-    @Nullable
-    public String getDeviceName() {
-        return mDeviceName;
-    }
-
-    /** Returns the filter set on the service uuid. */
-    @Nullable
-    public ParcelUuid getServiceUuid() {
-        return mServiceUuid;
-    }
-
-    /** Returns the mask for the service uuid. */
-    @Nullable
-    public ParcelUuid getServiceUuidMask() {
-        return mServiceUuidMask;
-    }
-
-    /** Returns the filter set on the device address. */
-    @Nullable
-    public String getDeviceAddress() {
-        return mDeviceAddress;
-    }
-
-    /** Returns the filter set on the service data. */
-    @Nullable
-    public byte[] getServiceData() {
-        return mServiceData;
-    }
-
-    /** Returns the mask for the service data. */
-    @Nullable
-    public byte[] getServiceDataMask() {
-        return mServiceDataMask;
-    }
-
-    /** Returns the filter set on the service data uuid. */
-    @Nullable
-    public ParcelUuid getServiceDataUuid() {
-        return mServiceDataUuid;
-    }
-
-    /** Returns the manufacturer id. -1 if the manufacturer filter is not set. */
-    public int getManufacturerId() {
-        return mManufacturerId;
-    }
-
-    /** Returns the filter set on the manufacturer data. */
-    @Nullable
-    public byte[] getManufacturerData() {
-        return mManufacturerData;
-    }
-
-    /** Returns the mask for the manufacturer data. */
-    @Nullable
-    public byte[] getManufacturerDataMask() {
-        return mManufacturerDataMask;
-    }
-
-    /**
-     * Check if the filter matches a {@code BleSighting}. A BLE sighting is considered as a match if
-     * it matches all the field filters.
-     */
-    public boolean matches(@Nullable BleSighting bleSighting) {
-        if (bleSighting == null) {
-            return false;
-        }
-        BluetoothDevice device = bleSighting.getDevice();
-        // Device match.
-        if (mDeviceAddress != null && (device == null || !mDeviceAddress.equals(
-                device.getAddress()))) {
-            return false;
-        }
-
-        BleRecord bleRecord = bleSighting.getBleRecord();
-
-        // Scan record is null but there exist filters on it.
-        if (bleRecord == null
-                && (mDeviceName != null
-                || mServiceUuid != null
-                || mManufacturerData != null
-                || mServiceData != null)) {
-            return false;
-        }
-
-        // Local name match.
-        if (mDeviceName != null && !mDeviceName.equals(bleRecord.getDeviceName())) {
-            return false;
-        }
-
-        // UUID match.
-        if (mServiceUuid != null
-                && !matchesServiceUuids(mServiceUuid, mServiceUuidMask,
-                bleRecord.getServiceUuids())) {
-            return false;
-        }
-
-        // Service data match
-        if (mServiceDataUuid != null
-                && !matchesPartialData(
-                mServiceData, mServiceDataMask, bleRecord.getServiceData(mServiceDataUuid))) {
-            return false;
-        }
-
-        // Manufacturer data match.
-        if (mManufacturerId >= 0
-                && !matchesPartialData(
-                mManufacturerData,
-                mManufacturerDataMask,
-                bleRecord.getManufacturerSpecificData(mManufacturerId))) {
-            return false;
-        }
-
-        // All filters match.
-        return true;
-    }
-
-    /**
-     * Determines if the characteristics of this filter are a superset of the characteristics of the
-     * given filter.
-     */
-    public boolean isSuperset(@Nullable BleFilter bleFilter) {
-        if (bleFilter == null) {
-            return false;
-        }
-
-        if (equals(bleFilter)) {
-            return true;
-        }
-
-        // Verify device address matches.
-        if (mDeviceAddress != null && !mDeviceAddress.equals(bleFilter.getDeviceAddress())) {
-            return false;
-        }
-
-        // Verify device name matches.
-        if (mDeviceName != null && !mDeviceName.equals(bleFilter.getDeviceName())) {
-            return false;
-        }
-
-        // Verify UUID is a superset.
-        if (mServiceUuid != null
-                && !serviceUuidIsSuperset(
-                mServiceUuid,
-                mServiceUuidMask,
-                bleFilter.getServiceUuid(),
-                bleFilter.getServiceUuidMask())) {
-            return false;
-        }
-
-        // Verify service data is a superset.
-        if (mServiceDataUuid != null
-                && (!mServiceDataUuid.equals(bleFilter.getServiceDataUuid())
-                || !partialDataIsSuperset(
-                mServiceData,
-                mServiceDataMask,
-                bleFilter.getServiceData(),
-                bleFilter.getServiceDataMask()))) {
-            return false;
-        }
-
-        // Verify manufacturer data is a superset.
-        if (mManufacturerId >= 0
-                && (mManufacturerId != bleFilter.getManufacturerId()
-                || !partialDataIsSuperset(
-                mManufacturerData,
-                mManufacturerDataMask,
-                bleFilter.getManufacturerData(),
-                bleFilter.getManufacturerDataMask()))) {
-            return false;
-        }
-
-        return true;
-    }
-
-    /** Determines if the first uuid and mask are a superset of the second uuid and mask. */
-    private static boolean serviceUuidIsSuperset(
-            @Nullable ParcelUuid uuid1,
-            @Nullable ParcelUuid uuidMask1,
-            @Nullable ParcelUuid uuid2,
-            @Nullable ParcelUuid uuidMask2) {
-        // First uuid1 is null so it can match any service UUID.
-        if (uuid1 == null) {
-            return true;
-        }
-
-        // uuid2 is a superset of uuid1, but not the other way around.
-        if (uuid2 == null) {
-            return false;
-        }
-
-        // Without a mask, the uuids must match.
-        if (uuidMask1 == null) {
-            return uuid1.equals(uuid2);
-        }
-
-        // Mask2 should be at least as specific as mask1.
-        if (uuidMask2 != null) {
-            long uuid1MostSig = uuidMask1.getUuid().getMostSignificantBits();
-            long uuid1LeastSig = uuidMask1.getUuid().getLeastSignificantBits();
-            long uuid2MostSig = uuidMask2.getUuid().getMostSignificantBits();
-            long uuid2LeastSig = uuidMask2.getUuid().getLeastSignificantBits();
-            if (((uuid1MostSig & uuid2MostSig) != uuid1MostSig)
-                    || ((uuid1LeastSig & uuid2LeastSig) != uuid1LeastSig)) {
-                return false;
-            }
-        }
-
-        if (!matchesServiceUuids(uuid1, uuidMask1, Arrays.asList(uuid2))) {
-            return false;
-        }
-
-        return true;
-    }
-
-    /** Determines if the first data and mask are the superset of the second data and mask. */
-    private static boolean partialDataIsSuperset(
-            @Nullable byte[] data1,
-            @Nullable byte[] dataMask1,
-            @Nullable byte[] data2,
-            @Nullable byte[] dataMask2) {
-        if (Arrays.equals(data1, data2) && Arrays.equals(dataMask1, dataMask2)) {
-            return true;
-        }
-
-        if (data1 == null) {
-            return true;
-        }
-
-        if (data2 == null) {
-            return false;
-        }
-
-        // Mask2 should be at least as specific as mask1.
-        if (dataMask1 != null && dataMask2 != null) {
-            for (int i = 0, j = 0; i < dataMask1.length && j < dataMask2.length; i++, j++) {
-                if ((dataMask1[i] & dataMask2[j]) != dataMask1[i]) {
-                    return false;
-                }
-            }
-        }
-
-        if (!matchesPartialData(data1, dataMask1, data2)) {
-            return false;
-        }
-
-        return true;
-    }
-
-    /** Check if the uuid pattern is contained in a list of parcel uuids. */
-    private static boolean matchesServiceUuids(
-            @Nullable ParcelUuid uuid, @Nullable ParcelUuid parcelUuidMask,
-            List<ParcelUuid> uuids) {
-        if (uuid == null) {
-            // No service uuid filter has been set, so there's a match.
-            return true;
-        }
-
-        UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
-        for (ParcelUuid parcelUuid : uuids) {
-            if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /** Check if the uuid pattern matches the particular service uuid. */
-    private static boolean matchesServiceUuid(UUID uuid, @Nullable UUID mask, UUID data) {
-        if (mask == null) {
-            return uuid.equals(data);
-        }
-        if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits())
-                != (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) {
-            return false;
-        }
-        return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits())
-                == (data.getMostSignificantBits() & mask.getMostSignificantBits()));
-    }
-
-    /**
-     * Check whether the data pattern matches the parsed data. Assumes that {@code data} and {@code
-     * dataMask} have the same length.
-     */
-    /* package */
-    static boolean matchesPartialData(
-            @Nullable byte[] data, @Nullable byte[] dataMask, @Nullable byte[] parsedData) {
-        if (data == null || parsedData == null || parsedData.length < data.length) {
-            return false;
-        }
-        if (dataMask == null) {
-            for (int i = 0; i < data.length; ++i) {
-                if (parsedData[i] != data[i]) {
-                    return false;
-                }
-            }
-            return true;
-        }
-        for (int i = 0; i < data.length; ++i) {
-            if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    @Override
-    public String toString() {
-        return "BleFilter [deviceName="
-                + mDeviceName
-                + ", deviceAddress="
-                + mDeviceAddress
-                + ", uuid="
-                + mServiceUuid
-                + ", uuidMask="
-                + mServiceUuidMask
-                + ", serviceDataUuid="
-                + mServiceDataUuid
-                + ", serviceData="
-                + Arrays.toString(mServiceData)
-                + ", serviceDataMask="
-                + Arrays.toString(mServiceDataMask)
-                + ", manufacturerId="
-                + mManufacturerId
-                + ", manufacturerData="
-                + Arrays.toString(mManufacturerData)
-                + ", manufacturerDataMask="
-                + Arrays.toString(mManufacturerDataMask)
-                + "]";
-    }
-
-    @Override
-    public void writeToParcel(Parcel out, int flags) {
-        out.writeString(mDeviceName);
-        out.writeString(mDeviceAddress);
-        out.writeInt(mManufacturerId);
-        out.writeByteArray(mManufacturerData);
-        out.writeByteArray(mManufacturerDataMask);
-        out.writeParcelable(mServiceDataUuid, flags);
-        out.writeByteArray(mServiceData);
-        out.writeByteArray(mServiceDataMask);
-        out.writeParcelable(mServiceUuid, flags);
-        out.writeParcelable(mServiceUuidMask, flags);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(
-                mDeviceName,
-                mDeviceAddress,
-                mManufacturerId,
-                Arrays.hashCode(mManufacturerData),
-                Arrays.hashCode(mManufacturerDataMask),
-                mServiceDataUuid,
-                Arrays.hashCode(mServiceData),
-                Arrays.hashCode(mServiceDataMask),
-                mServiceUuid,
-                mServiceUuidMask);
-    }
-
-    @Override
-    public boolean equals(@Nullable Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null || getClass() != obj.getClass()) {
-            return false;
-        }
-        BleFilter other = (BleFilter) obj;
-        return equal(mDeviceName, other.mDeviceName)
-                && equal(mDeviceAddress, other.mDeviceAddress)
-                && mManufacturerId == other.mManufacturerId
-                && Arrays.equals(mManufacturerData, other.mManufacturerData)
-                && Arrays.equals(mManufacturerDataMask, other.mManufacturerDataMask)
-                && equal(mServiceDataUuid, other.mServiceDataUuid)
-                && Arrays.equals(mServiceData, other.mServiceData)
-                && Arrays.equals(mServiceDataMask, other.mServiceDataMask)
-                && equal(mServiceUuid, other.mServiceUuid)
-                && equal(mServiceUuidMask, other.mServiceUuidMask);
-    }
-
-    /** Builder class for {@link BleFilter}. */
-    public static final class Builder {
-
-        private String mDeviceName;
-        private String mDeviceAddress;
-
-        @Nullable
-        private ParcelUuid mServiceUuid;
-        @Nullable
-        private ParcelUuid mUuidMask;
-
-        private ParcelUuid mServiceDataUuid;
-        @Nullable
-        private byte[] mServiceData;
-        @Nullable
-        private byte[] mServiceDataMask;
-
-        private int mManufacturerId = -1;
-        private byte[] mManufacturerData;
-        @Nullable
-        private byte[] mManufacturerDataMask;
-
-        /** Set filter on device name. */
-        public Builder setDeviceName(String deviceName) {
-            this.mDeviceName = deviceName;
-            return this;
-        }
-
-        /**
-         * Set filter on device address.
-         *
-         * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
-         *                      format of "01:02:03:AB:CD:EF". The device address can be validated
-         *                      using {@link
-         *                      BluetoothAdapter#checkBluetoothAddress}.
-         * @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
-         */
-        public Builder setDeviceAddress(String deviceAddress) {
-            if (!BluetoothAdapter.checkBluetoothAddress(deviceAddress)) {
-                throw new IllegalArgumentException("invalid device address " + deviceAddress);
-            }
-            this.mDeviceAddress = deviceAddress;
-            return this;
-        }
-
-        /** Set filter on service uuid. */
-        public Builder setServiceUuid(@Nullable ParcelUuid serviceUuid) {
-            this.mServiceUuid = serviceUuid;
-            mUuidMask = null; // clear uuid mask
-            return this;
-        }
-
-        /**
-         * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the {@code
-         * serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the bit in
-         * {@code serviceUuid}, and 0 to ignore that bit.
-         *
-         * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but {@code
-         *                                  uuidMask}
-         *                                  is not {@code null}.
-         */
-        public Builder setServiceUuid(@Nullable ParcelUuid serviceUuid,
-                @Nullable ParcelUuid uuidMask) {
-            if (uuidMask != null && serviceUuid == null) {
-                throw new IllegalArgumentException("uuid is null while uuidMask is not null!");
-            }
-            this.mServiceUuid = serviceUuid;
-            this.mUuidMask = uuidMask;
-            return this;
-        }
-
-        /**
-         * Set filtering on service data.
-         */
-        public Builder setServiceData(ParcelUuid serviceDataUuid, @Nullable byte[] serviceData) {
-            this.mServiceDataUuid = serviceDataUuid;
-            this.mServiceData = serviceData;
-            mServiceDataMask = null; // clear service data mask
-            return this;
-        }
-
-        /**
-         * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to
-         * match
-         * the one in service data, otherwise set it to 0 to ignore that bit.
-         *
-         * <p>The {@code serviceDataMask} must have the same length of the {@code serviceData}.
-         *
-         * @throws IllegalArgumentException If {@code serviceDataMask} is {@code null} while {@code
-         *                                  serviceData} is not or {@code serviceDataMask} and
-         *                                  {@code serviceData} has different
-         *                                  length.
-         */
-        public Builder setServiceData(
-                ParcelUuid serviceDataUuid,
-                @Nullable byte[] serviceData,
-                @Nullable byte[] serviceDataMask) {
-            if (serviceDataMask != null) {
-                if (serviceData == null) {
-                    throw new IllegalArgumentException(
-                            "serviceData is null while serviceDataMask is not null");
-                }
-                // Since the serviceDataMask is a bit mask for serviceData, the lengths of the two
-                // byte array need to be the same.
-                if (serviceData.length != serviceDataMask.length) {
-                    throw new IllegalArgumentException(
-                            "size mismatch for service data and service data mask");
-                }
-            }
-            this.mServiceDataUuid = serviceDataUuid;
-            this.mServiceData = serviceData;
-            this.mServiceDataMask = serviceDataMask;
-            return this;
-        }
-
-        /**
-         * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id.
-         *
-         * <p>Note the first two bytes of the {@code manufacturerData} is the manufacturerId.
-         *
-         * @throws IllegalArgumentException If the {@code manufacturerId} is invalid.
-         */
-        public Builder setManufacturerData(int manufacturerId, @Nullable byte[] manufacturerData) {
-            return setManufacturerData(manufacturerId, manufacturerData, null /* mask */);
-        }
-
-        /**
-         * Set filter on partial manufacture data. For any bit in the mask, set it to 1 if it needs
-         * to
-         * match the one in manufacturer data, otherwise set it to 0.
-         *
-         * <p>The {@code manufacturerDataMask} must have the same length of {@code
-         * manufacturerData}.
-         *
-         * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or {@code
-         *                                  manufacturerData} is null while {@code
-         *                                  manufacturerDataMask} is not, or {@code
-         *                                  manufacturerData} and {@code manufacturerDataMask} have
-         *                                  different length.
-         */
-        public Builder setManufacturerData(
-                int manufacturerId,
-                @Nullable byte[] manufacturerData,
-                @Nullable byte[] manufacturerDataMask) {
-            if (manufacturerData != null && manufacturerId < 0) {
-                throw new IllegalArgumentException("invalid manufacture id");
-            }
-            if (manufacturerDataMask != null) {
-                if (manufacturerData == null) {
-                    throw new IllegalArgumentException(
-                            "manufacturerData is null while manufacturerDataMask is not null");
-                }
-                // Since the manufacturerDataMask is a bit mask for manufacturerData, the lengths
-                // of the two byte array need to be the same.
-                if (manufacturerData.length != manufacturerDataMask.length) {
-                    throw new IllegalArgumentException(
-                            "size mismatch for manufacturerData and manufacturerDataMask");
-                }
-            }
-            this.mManufacturerId = manufacturerId;
-            this.mManufacturerData = manufacturerData == null ? new byte[0] : manufacturerData;
-            this.mManufacturerDataMask = manufacturerDataMask;
-            return this;
-        }
-
-
-        /**
-         * Builds the filter.
-         *
-         * @throws IllegalArgumentException If the filter cannot be built.
-         */
-        public BleFilter build() {
-            return new BleFilter(
-                    mDeviceName,
-                    mDeviceAddress,
-                    mServiceUuid,
-                    mUuidMask,
-                    mServiceDataUuid,
-                    mServiceData,
-                    mServiceDataMask,
-                    mManufacturerId,
-                    mManufacturerData,
-                    mManufacturerDataMask);
-        }
-    }
-
-    /**
-     * Changes ble filter to os filter
-     */
-    public ScanFilter toOsFilter() {
-        ScanFilter.Builder osFilterBuilder = new ScanFilter.Builder();
-        if (!TextUtils.isEmpty(getDeviceAddress())) {
-            osFilterBuilder.setDeviceAddress(getDeviceAddress());
-        }
-        if (!TextUtils.isEmpty(getDeviceName())) {
-            osFilterBuilder.setDeviceName(getDeviceName());
-        }
-
-        byte[] manufacturerData = getManufacturerData();
-        if (getManufacturerId() != -1 && manufacturerData != null) {
-            byte[] manufacturerDataMask = getManufacturerDataMask();
-            if (manufacturerDataMask != null) {
-                osFilterBuilder.setManufacturerData(
-                        getManufacturerId(), manufacturerData, manufacturerDataMask);
-            } else {
-                osFilterBuilder.setManufacturerData(getManufacturerId(), manufacturerData);
-            }
-        }
-
-        ParcelUuid serviceDataUuid = getServiceDataUuid();
-        byte[] serviceData = getServiceData();
-        if (serviceDataUuid != null && serviceData != null) {
-            byte[] serviceDataMask = getServiceDataMask();
-            if (serviceDataMask != null) {
-                osFilterBuilder.setServiceData(serviceDataUuid, serviceData, serviceDataMask);
-            } else {
-                osFilterBuilder.setServiceData(serviceDataUuid, serviceData);
-            }
-        }
-
-        ParcelUuid serviceUuid = getServiceUuid();
-        if (serviceUuid != null) {
-            ParcelUuid serviceUuidMask = getServiceUuidMask();
-            if (serviceUuidMask != null) {
-                osFilterBuilder.setServiceUuid(serviceUuid, serviceUuidMask);
-            } else {
-                osFilterBuilder.setServiceUuid(serviceUuid);
-            }
-        }
-        return osFilterBuilder.build();
-    }
-
-    /**
-     * equal() method for two possibly-null objects
-     */
-    private static boolean equal(@Nullable Object obj1, @Nullable Object obj2) {
-        return obj1 == obj2 || (obj1 != null && obj1.equals(obj2));
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/BleRecord.java b/nearby/service/java/com/android/server/nearby/common/ble/BleRecord.java
deleted file mode 100644
index 103a27f..0000000
--- a/nearby/service/java/com/android/server/nearby/common/ble/BleRecord.java
+++ /dev/null
@@ -1,395 +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.server.nearby.common.ble;
-
-import android.os.ParcelUuid;
-import android.util.Log;
-import android.util.SparseArray;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.nearby.common.ble.util.StringUtils;
-
-import com.google.common.collect.ImmutableList;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-/**
- * Represents a BLE record from Bluetooth LE scan.
- */
-public final class BleRecord {
-
-    // The following data type values are assigned by Bluetooth SIG.
-    // For more details refer to Bluetooth 4.1 specification, Volume 3, Part C, Section 18.
-    private static final int DATA_TYPE_FLAGS = 0x01;
-    private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL = 0x02;
-    private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE = 0x03;
-    private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL = 0x04;
-    private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE = 0x05;
-    private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL = 0x06;
-    private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07;
-    private static final int DATA_TYPE_LOCAL_NAME_SHORT = 0x08;
-    private static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09;
-    private static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A;
-    private static final int DATA_TYPE_SERVICE_DATA = 0x16;
-    private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
-
-    /** The base 128-bit UUID representation of a 16-bit UUID. */
-    private static final ParcelUuid BASE_UUID =
-            ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB");
-    /** Length of bytes for 16 bit UUID. */
-    private static final int UUID_BYTES_16_BIT = 2;
-    /** Length of bytes for 32 bit UUID. */
-    private static final int UUID_BYTES_32_BIT = 4;
-    /** Length of bytes for 128 bit UUID. */
-    private static final int UUID_BYTES_128_BIT = 16;
-
-    // Flags of the advertising data.
-    // -1 when the scan record is not valid.
-    private final int mAdvertiseFlags;
-
-    private final ImmutableList<ParcelUuid> mServiceUuids;
-
-    // null when the scan record is not valid.
-    @Nullable
-    private final SparseArray<byte[]> mManufacturerSpecificData;
-
-    // null when the scan record is not valid.
-    @Nullable
-    private final Map<ParcelUuid, byte[]> mServiceData;
-
-    // Transmission power level(in dB).
-    // Integer.MIN_VALUE when the scan record is not valid.
-    private final int mTxPowerLevel;
-
-    // Local name of the Bluetooth LE device.
-    // null when the scan record is not valid.
-    @Nullable
-    private final String mDeviceName;
-
-    // Raw bytes of scan record.
-    // Never null, whether valid or not.
-    private final byte[] mBytes;
-
-    // If the raw scan record byte[] cannot be parsed, all non-primitive args here other than the
-    // raw scan record byte[] and serviceUudis will be null. See parsefromBytes().
-    private BleRecord(
-            List<ParcelUuid> serviceUuids,
-            @Nullable SparseArray<byte[]> manufacturerData,
-            @Nullable Map<ParcelUuid, byte[]> serviceData,
-            int advertiseFlags,
-            int txPowerLevel,
-            @Nullable String deviceName,
-            byte[] bytes) {
-        this.mServiceUuids = ImmutableList.copyOf(serviceUuids);
-        mManufacturerSpecificData = manufacturerData;
-        this.mServiceData = serviceData;
-        this.mDeviceName = deviceName;
-        this.mAdvertiseFlags = advertiseFlags;
-        this.mTxPowerLevel = txPowerLevel;
-        this.mBytes = bytes;
-    }
-
-    /**
-     * Returns a list of service UUIDs within the advertisement that are used to identify the
-     * bluetooth GATT services.
-     */
-    public ImmutableList<ParcelUuid> getServiceUuids() {
-        return mServiceUuids;
-    }
-
-    /**
-     * Returns a sparse array of manufacturer identifier and its corresponding manufacturer specific
-     * data.
-     */
-    @Nullable
-    public SparseArray<byte[]> getManufacturerSpecificData() {
-        return mManufacturerSpecificData;
-    }
-
-    /**
-     * Returns the manufacturer specific data associated with the manufacturer id. Returns {@code
-     * null} if the {@code manufacturerId} is not found.
-     */
-    @Nullable
-    public byte[] getManufacturerSpecificData(int manufacturerId) {
-        if (mManufacturerSpecificData == null) {
-            return null;
-        }
-        return mManufacturerSpecificData.get(manufacturerId);
-    }
-
-    /** Returns a map of service UUID and its corresponding service data. */
-    @Nullable
-    public Map<ParcelUuid, byte[]> getServiceData() {
-        return mServiceData;
-    }
-
-    /**
-     * Returns the service data byte array associated with the {@code serviceUuid}. Returns {@code
-     * null} if the {@code serviceDataUuid} is not found.
-     */
-    @Nullable
-    public byte[] getServiceData(ParcelUuid serviceDataUuid) {
-        if (serviceDataUuid == null || mServiceData == null) {
-            return null;
-        }
-        return mServiceData.get(serviceDataUuid);
-    }
-
-    /**
-     * Returns the transmission power level of the packet in dBm. Returns {@link Integer#MIN_VALUE}
-     * if
-     * the field is not set. This value can be used to calculate the path loss of a received packet
-     * using the following equation:
-     *
-     * <p><code>pathloss = txPowerLevel - rssi</code>
-     */
-    public int getTxPowerLevel() {
-        return mTxPowerLevel;
-    }
-
-    /** Returns the local name of the BLE device. The is a UTF-8 encoded string. */
-    @Nullable
-    public String getDeviceName() {
-        return mDeviceName;
-    }
-
-    /** Returns raw bytes of scan record. */
-    public byte[] getBytes() {
-        return mBytes;
-    }
-
-    /**
-     * Parse scan record bytes to {@link BleRecord}.
-     *
-     * <p>The format is defined in Bluetooth 4.1 specification, Volume 3, Part C, Section 11 and 18.
-     *
-     * <p>All numerical multi-byte entities and values shall use little-endian <strong>byte</strong>
-     * order.
-     *
-     * @param scanRecord The scan record of Bluetooth LE advertisement and/or scan response.
-     */
-    public static BleRecord parseFromBytes(byte[] scanRecord) {
-        int currentPos = 0;
-        int advertiseFlag = -1;
-        List<ParcelUuid> serviceUuids = new ArrayList<>();
-        String localName = null;
-        int txPowerLevel = Integer.MIN_VALUE;
-
-        SparseArray<byte[]> manufacturerData = new SparseArray<>();
-        Map<ParcelUuid, byte[]> serviceData = new HashMap<>();
-
-        try {
-            while (currentPos < scanRecord.length) {
-                // length is unsigned int.
-                int length = scanRecord[currentPos++] & 0xFF;
-                if (length == 0) {
-                    break;
-                }
-                // Note the length includes the length of the field type itself.
-                int dataLength = length - 1;
-                // fieldType is unsigned int.
-                int fieldType = scanRecord[currentPos++] & 0xFF;
-                switch (fieldType) {
-                    case DATA_TYPE_FLAGS:
-                        advertiseFlag = scanRecord[currentPos] & 0xFF;
-                        break;
-                    case DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL:
-                    case DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE:
-                        parseServiceUuid(scanRecord, currentPos, dataLength, UUID_BYTES_16_BIT,
-                                serviceUuids);
-                        break;
-                    case DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL:
-                    case DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE:
-                        parseServiceUuid(scanRecord, currentPos, dataLength, UUID_BYTES_32_BIT,
-                                serviceUuids);
-                        break;
-                    case DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL:
-                    case DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE:
-                        parseServiceUuid(scanRecord, currentPos, dataLength, UUID_BYTES_128_BIT,
-                                serviceUuids);
-                        break;
-                    case DATA_TYPE_LOCAL_NAME_SHORT:
-                    case DATA_TYPE_LOCAL_NAME_COMPLETE:
-                        localName = new String(extractBytes(scanRecord, currentPos, dataLength));
-                        break;
-                    case DATA_TYPE_TX_POWER_LEVEL:
-                        txPowerLevel = scanRecord[currentPos];
-                        break;
-                    case DATA_TYPE_SERVICE_DATA:
-                        // The first two bytes of the service data are service data UUID in little
-                        // endian. The rest bytes are service data.
-                        int serviceUuidLength = UUID_BYTES_16_BIT;
-                        byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos,
-                                serviceUuidLength);
-                        ParcelUuid serviceDataUuid = parseUuidFrom(serviceDataUuidBytes);
-                        byte[] serviceDataArray =
-                                extractBytes(
-                                        scanRecord, currentPos + serviceUuidLength,
-                                        dataLength - serviceUuidLength);
-                        serviceData.put(serviceDataUuid, serviceDataArray);
-                        break;
-                    case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA:
-                        // The first two bytes of the manufacturer specific data are
-                        // manufacturer ids in little endian.
-                        int manufacturerId =
-                                ((scanRecord[currentPos + 1] & 0xFF) << 8) + (scanRecord[currentPos]
-                                        & 0xFF);
-                        byte[] manufacturerDataBytes = extractBytes(scanRecord, currentPos + 2,
-                                dataLength - 2);
-                        manufacturerData.put(manufacturerId, manufacturerDataBytes);
-                        break;
-                    default:
-                        // Just ignore, we don't handle such data type.
-                        break;
-                }
-                currentPos += dataLength;
-            }
-
-            return new BleRecord(
-                    serviceUuids,
-                    manufacturerData,
-                    serviceData,
-                    advertiseFlag,
-                    txPowerLevel,
-                    localName,
-                    scanRecord);
-        } catch (Exception e) {
-            Log.w("BleRecord", "Unable to parse scan record: " + Arrays.toString(scanRecord), e);
-            // As the record is invalid, ignore all the parsed results for this packet
-            // and return an empty record with raw scanRecord bytes in results
-            // check at the top of this method does? Maybe we expect callers to use the
-            // scanRecord part in
-            // some fallback. But if that's the reason, it would seem we still can return null.
-            // They still
-            // have the raw scanRecord in hand, 'cause they passed it to us. It seems too easy for a
-            // caller to misuse this "empty" BleRecord (as in b/22693067).
-            return new BleRecord(ImmutableList.of(), null, null, -1, Integer.MIN_VALUE, null,
-                    scanRecord);
-        }
-    }
-
-    // Parse service UUIDs.
-    private static int parseServiceUuid(
-            byte[] scanRecord,
-            int currentPos,
-            int dataLength,
-            int uuidLength,
-            List<ParcelUuid> serviceUuids) {
-        while (dataLength > 0) {
-            byte[] uuidBytes = extractBytes(scanRecord, currentPos, uuidLength);
-            serviceUuids.add(parseUuidFrom(uuidBytes));
-            dataLength -= uuidLength;
-            currentPos += uuidLength;
-        }
-        return currentPos;
-    }
-
-    // Helper method to extract bytes from byte array.
-    private static byte[] extractBytes(byte[] scanRecord, int start, int length) {
-        byte[] bytes = new byte[length];
-        System.arraycopy(scanRecord, start, bytes, 0, length);
-        return bytes;
-    }
-
-    @Override
-    public String toString() {
-        return "BleRecord [advertiseFlags="
-                + mAdvertiseFlags
-                + ", serviceUuids="
-                + mServiceUuids
-                + ", manufacturerSpecificData="
-                + StringUtils.toString(mManufacturerSpecificData)
-                + ", serviceData="
-                + StringUtils.toString(mServiceData)
-                + ", txPowerLevel="
-                + mTxPowerLevel
-                + ", deviceName="
-                + mDeviceName
-                + "]";
-    }
-
-    @Override
-    public boolean equals(@Nullable Object obj) {
-        if (obj == this) {
-            return true;
-        }
-        if (!(obj instanceof BleRecord)) {
-            return false;
-        }
-        BleRecord record = (BleRecord) obj;
-        // BleRecord objects are built from bytes, so we only need that field.
-        return Arrays.equals(mBytes, record.mBytes);
-    }
-
-    @Override
-    public int hashCode() {
-        // BleRecord objects are built from bytes, so we only need that field.
-        return Arrays.hashCode(mBytes);
-    }
-
-    /**
-     * Parse UUID from bytes. The {@code uuidBytes} can represent a 16-bit, 32-bit or 128-bit UUID,
-     * but the returned UUID is always in 128-bit format. Note UUID is little endian in Bluetooth.
-     *
-     * @param uuidBytes Byte representation of uuid.
-     * @return {@link ParcelUuid} parsed from bytes.
-     * @throws IllegalArgumentException If the {@code uuidBytes} cannot be parsed.
-     */
-    private static ParcelUuid parseUuidFrom(byte[] uuidBytes) {
-        if (uuidBytes == null) {
-            throw new IllegalArgumentException("uuidBytes cannot be null");
-        }
-        int length = uuidBytes.length;
-        if (length != UUID_BYTES_16_BIT
-                && length != UUID_BYTES_32_BIT
-                && length != UUID_BYTES_128_BIT) {
-            throw new IllegalArgumentException("uuidBytes length invalid - " + length);
-        }
-        // Construct a 128 bit UUID.
-        if (length == UUID_BYTES_128_BIT) {
-            ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN);
-            long msb = buf.getLong(8);
-            long lsb = buf.getLong(0);
-            return new ParcelUuid(new UUID(msb, lsb));
-        }
-        // For 16 bit and 32 bit UUID we need to convert them to 128 bit value.
-        // 128_bit_value = uuid * 2^96 + BASE_UUID
-        long shortUuid;
-        if (length == UUID_BYTES_16_BIT) {
-            shortUuid = uuidBytes[0] & 0xFF;
-            shortUuid += (uuidBytes[1] & 0xFF) << 8;
-        } else {
-            shortUuid = uuidBytes[0] & 0xFF;
-            shortUuid += (uuidBytes[1] & 0xFF) << 8;
-            shortUuid += (uuidBytes[2] & 0xFF) << 16;
-            shortUuid += (uuidBytes[3] & 0xFF) << 24;
-        }
-        long msb = BASE_UUID.getUuid().getMostSignificantBits() + (shortUuid << 32);
-        long lsb = BASE_UUID.getUuid().getLeastSignificantBits();
-        return new ParcelUuid(new UUID(msb, lsb));
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/BleSighting.java b/nearby/service/java/com/android/server/nearby/common/ble/BleSighting.java
deleted file mode 100644
index 71ec10c..0000000
--- a/nearby/service/java/com/android/server/nearby/common/ble/BleSighting.java
+++ /dev/null
@@ -1,215 +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.server.nearby.common.ble;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.le.ScanRecord;
-import android.bluetooth.le.ScanResult;
-import android.os.Build.VERSION_CODES;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.VisibleForTesting;
-
-import java.util.Arrays;
-import java.util.Objects;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A sighting of a BLE device found in a Bluetooth LE scan.
- */
-
-public class BleSighting implements Parcelable {
-
-    public static final Parcelable.Creator<BleSighting> CREATOR = new Creator<BleSighting>() {
-        @Override
-        public BleSighting createFromParcel(Parcel source) {
-            BleSighting nBleSighting = new BleSighting(source.readParcelable(null),
-                    source.marshall(), source.readInt(), source.readLong());
-            return null;
-        }
-
-        @Override
-        public BleSighting[] newArray(int size) {
-            return new BleSighting[size];
-        }
-    };
-
-    // Max and min rssi value which is from {@link android.bluetooth.le.ScanResult#getRssi()}.
-    @VisibleForTesting
-    public static final int MAX_RSSI_VALUE = 126;
-    @VisibleForTesting
-    public static final int MIN_RSSI_VALUE = -127;
-
-    /** Remote bluetooth device. */
-    private final BluetoothDevice mDevice;
-
-    /**
-     * BLE record, including advertising data and response data. BleRecord is not parcelable, so
-     * this
-     * is created from bleRecordBytes.
-     */
-    private final BleRecord mBleRecord;
-
-    /** The bytes of a BLE record. */
-    private final byte[] mBleRecordBytes;
-
-    /** Received signal strength. */
-    private final int mRssi;
-
-    /** Nanos timestamp when the ble device was observed (epoch time). */
-    private final long mTimestampEpochNanos;
-
-    /**
-     * Constructor of a BLE sighting.
-     *
-     * @param device              Remote bluetooth device that is found.
-     * @param bleRecordBytes      The bytes that will create a BleRecord.
-     * @param rssi                Received signal strength.
-     * @param timestampEpochNanos Nanos timestamp when the BLE device was observed (epoch time).
-     */
-    public BleSighting(BluetoothDevice device, byte[] bleRecordBytes, int rssi,
-            long timestampEpochNanos) {
-        this.mDevice = device;
-        this.mBleRecordBytes = bleRecordBytes;
-        this.mRssi = rssi;
-        this.mTimestampEpochNanos = timestampEpochNanos;
-        mBleRecord = BleRecord.parseFromBytes(bleRecordBytes);
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    /** Returns the remote bluetooth device identified by the bluetooth device address. */
-    public BluetoothDevice getDevice() {
-        return mDevice;
-    }
-
-    /** Returns the BLE record, which is a combination of advertisement and scan response. */
-    public BleRecord getBleRecord() {
-        return mBleRecord;
-    }
-
-    /** Returns the bytes of the BLE record. */
-    public byte[] getBleRecordBytes() {
-        return mBleRecordBytes;
-    }
-
-    /** Returns the received signal strength in dBm. The valid range is [-127, 127]. */
-    public int getRssi() {
-        return mRssi;
-    }
-
-    /**
-     * Returns the received signal strength normalized with the offset specific to the given device.
-     * 3 is the rssi offset to calculate fast init distance.
-     * <p>This method utilized the rssi offset maintained by Nearby Sharing.
-     *
-     * @return normalized rssi which is between [-127, 126] according to {@link
-     * android.bluetooth.le.ScanResult#getRssi()}.
-     */
-    public int getNormalizedRSSI() {
-        int adjustedRssi = mRssi + 3;
-        if (adjustedRssi < MIN_RSSI_VALUE) {
-            return MIN_RSSI_VALUE;
-        } else if (adjustedRssi > MAX_RSSI_VALUE) {
-            return MAX_RSSI_VALUE;
-        } else {
-            return adjustedRssi;
-        }
-    }
-
-    /** Returns timestamp in epoch time when the scan record was observed. */
-    public long getTimestampNanos() {
-        return mTimestampEpochNanos;
-    }
-
-    /** Returns timestamp in epoch time when the scan record was observed, in millis. */
-    public long getTimestampMillis() {
-        return TimeUnit.NANOSECONDS.toMillis(mTimestampEpochNanos);
-    }
-
-    @Override
-    public void writeToParcel(Parcel dest, int flags) {
-        dest.writeParcelable(mDevice, flags);
-        dest.writeByteArray(mBleRecordBytes);
-        dest.writeInt(mRssi);
-        dest.writeLong(mTimestampEpochNanos);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(mDevice, mRssi, mTimestampEpochNanos, Arrays.hashCode(mBleRecordBytes));
-    }
-
-    @Override
-    public boolean equals(@Nullable Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (!(obj instanceof BleSighting)) {
-            return false;
-        }
-        BleSighting other = (BleSighting) obj;
-        return Objects.equals(mDevice, other.mDevice)
-                && mRssi == other.mRssi
-                && Arrays.equals(mBleRecordBytes, other.mBleRecordBytes)
-                && mTimestampEpochNanos == other.mTimestampEpochNanos;
-    }
-
-    @Override
-    public String toString() {
-        return "BleSighting{"
-                + "device="
-                + mDevice
-                + ", bleRecord="
-                + mBleRecord
-                + ", rssi="
-                + mRssi
-                + ", timestampNanos="
-                + mTimestampEpochNanos
-                + "}";
-    }
-
-    /** Creates {@link BleSighting} using the {@link ScanResult}. */
-    @RequiresApi(api = VERSION_CODES.LOLLIPOP)
-    @Nullable
-    public static BleSighting createFromOsScanResult(ScanResult osResult) {
-        ScanRecord osScanRecord = osResult.getScanRecord();
-        if (osScanRecord == null) {
-            return null;
-        }
-
-        return new BleSighting(
-                osResult.getDevice(),
-                osScanRecord.getBytes(),
-                osResult.getRssi(),
-                // The timestamp from ScanResult is 'nanos since boot', Beacon lib will change it
-                // as 'nanos
-                // since epoch', but Nearby never reference this field, just pass it as 'nanos
-                // since boot'.
-                // ref to beacon/scan/impl/LBluetoothLeScannerCompat.fromOs for beacon design
-                // about how to
-                // convert nanos since boot to epoch.
-                osResult.getTimestampNanos());
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/decode/BeaconDecoder.java b/nearby/service/java/com/android/server/nearby/common/ble/decode/BeaconDecoder.java
deleted file mode 100644
index 9e795ac..0000000
--- a/nearby/service/java/com/android/server/nearby/common/ble/decode/BeaconDecoder.java
+++ /dev/null
@@ -1,106 +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.server.nearby.common.ble.decode;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.nearby.common.ble.BleRecord;
-
-/**
- * This class encapsulates the logic specific to each manufacturer for parsing formats for beacons,
- * and presents a common API to access important ADV/EIR packet fields such as:
- *
- * <ul>
- *   <li><b>UUID (universally unique identifier)</b>, a value uniquely identifying a group of one or
- *       more beacons as belonging to an organization or of a certain type, up to 128 bits.
- *   <li><b>Instance</b> a 32-bit unsigned integer that can be used to group related beacons that
- *       have the same UUID.
- *   <li>the mathematics of <b>TX signal strength</b>, used for proximity calculations.
- * </ul>
- *
- * ...and others.
- *
- * @see <a href="http://go/ble-glossary">BLE Glossary</a>
- * @see <a href="https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=245130">Bluetooth
- * Data Types Specification</a>
- */
-public abstract class BeaconDecoder {
-    /**
-     * Returns true if the bleRecord corresponds to a beacon format that contains sufficient
-     * information to construct a BeaconId and contains the Tx power.
-     */
-    public boolean supportsBeaconIdAndTxPower(@SuppressWarnings("unused") BleRecord bleRecord) {
-        return true;
-    }
-
-    /**
-     * Returns true if this decoder supports returning TxPower via {@link
-     * #getCalibratedBeaconTxPower(BleRecord)}.
-     */
-    public boolean supportsTxPower() {
-        return true;
-    }
-
-    /**
-     * Reads the calibrated transmitted power at 1 meter of the beacon in dBm. This value is
-     * contained
-     * in the scan record, as set by the transmitting beacon. Suitable for use in computing path
-     * loss,
-     * distance, and related derived values.
-     *
-     * @param bleRecord the parsed payload contained in the beacon packet
-     * @return integer value of the calibrated Tx power in dBm or null if the bleRecord doesn't
-     * contain sufficient information to calculate the Tx power.
-     */
-    @Nullable
-    public abstract Integer getCalibratedBeaconTxPower(BleRecord bleRecord);
-
-    /**
-     * Extract telemetry information from the beacon. Byte 0 of the returned telemetry block should
-     * encode the telemetry format.
-     *
-     * @return telemetry block for this beacon, or null if no telemetry data is found in the scan
-     * record.
-     */
-    @Nullable
-    public byte[] getTelemetry(@SuppressWarnings("unused") BleRecord bleRecord) {
-        return null;
-    }
-
-    /** Returns the appropriate type for this scan record. */
-    public abstract int getBeaconIdType();
-
-    /**
-     * Returns an array of bytes which uniquely identify this beacon, for beacons from any of the
-     * supported beacon types. This unique identifier is the indexing key for various internal
-     * services. Returns null if the bleRecord doesn't contain sufficient information to construct
-     * the
-     * ID.
-     */
-    @Nullable
-    public abstract byte[] getBeaconIdBytes(BleRecord bleRecord);
-
-    /**
-     * Returns the URL of the beacon. Returns null if the bleRecord doesn't contain a URL or
-     * contains
-     * a malformed URL.
-     */
-    @Nullable
-    public String getUrl(BleRecord bleRecord) {
-        return null;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/decode/FastPairDecoder.java b/nearby/service/java/com/android/server/nearby/common/ble/decode/FastPairDecoder.java
deleted file mode 100644
index c1ff9fd..0000000
--- a/nearby/service/java/com/android/server/nearby/common/ble/decode/FastPairDecoder.java
+++ /dev/null
@@ -1,297 +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.server.nearby.common.ble.decode;
-
-import android.bluetooth.le.ScanRecord;
-import android.os.ParcelUuid;
-import android.util.Log;
-import android.util.SparseArray;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.nearby.common.ble.BleFilter;
-import com.android.server.nearby.common.ble.BleRecord;
-
-import java.util.Arrays;
-
-/**
- * Parses Fast Pair information out of {@link BleRecord}s.
- *
- * <p>There are 2 different packet formats that are supported, which is used can be determined by
- * packet length:
- *
- * <p>For 3-byte packets, the full packet is the model ID.
- *
- * <p>For all other packets, the first byte is the header, followed by the model ID, followed by
- * zero or more extra fields. Each field has its own header byte followed by the field value. The
- * packet header is formatted as 0bVVVLLLLR (V = version, L = model ID length, R = reserved) and
- * each extra field header is 0bLLLLTTTT (L = field length, T = field type).
- *
- * @see <a href="http://go/fast-pair-2-service-data">go/fast-pair-2-service-data</a>
- */
-public class FastPairDecoder extends BeaconDecoder {
-
-    private static final int FIELD_TYPE_BLOOM_FILTER = 0;
-    private static final int FIELD_TYPE_BLOOM_FILTER_SALT = 1;
-    private static final int FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION = 2;
-    private static final int FIELD_TYPE_BATTERY = 3;
-    private static final int FIELD_TYPE_BATTERY_NO_NOTIFICATION = 4;
-    public static final int FIELD_TYPE_CONNECTION_STATE = 5;
-    private static final int FIELD_TYPE_RANDOM_RESOLVABLE_DATA = 6;
-
-    /** FE2C is the 16-bit Service UUID. The rest is the base UUID. See BluetoothUuid (hidden). */
-    private static final ParcelUuid FAST_PAIR_SERVICE_PARCEL_UUID =
-            ParcelUuid.fromString("0000FE2C-0000-1000-8000-00805F9B34FB");
-
-    /** The filter you use to scan for Fast Pair BLE advertisements. */
-    public static final BleFilter FILTER =
-            new BleFilter.Builder().setServiceData(FAST_PAIR_SERVICE_PARCEL_UUID,
-                    new byte[0]).build();
-
-    // NOTE: Ensure that all bitmasks are always ints, not bytes so that bitshifting works correctly
-    // without needing worry about signing errors.
-    private static final int HEADER_VERSION_BITMASK = 0b11100000;
-    private static final int HEADER_LENGTH_BITMASK = 0b00011110;
-    private static final int HEADER_VERSION_OFFSET = 5;
-    private static final int HEADER_LENGTH_OFFSET = 1;
-
-    private static final int EXTRA_FIELD_LENGTH_BITMASK = 0b11110000;
-    private static final int EXTRA_FIELD_TYPE_BITMASK = 0b00001111;
-    private static final int EXTRA_FIELD_LENGTH_OFFSET = 4;
-    private static final int EXTRA_FIELD_TYPE_OFFSET = 0;
-
-    private static final int MIN_ID_LENGTH = 3;
-    private static final int MAX_ID_LENGTH = 14;
-    private static final int HEADER_INDEX = 0;
-    private static final int HEADER_LENGTH = 1;
-    private static final int FIELD_HEADER_LENGTH = 1;
-
-    // Not using java.util.IllegalFormatException because it is unchecked.
-    private static class IllegalFormatException extends Exception {
-        private IllegalFormatException(String message) {
-            super(message);
-        }
-    }
-
-    @Nullable
-    @Override
-    public Integer getCalibratedBeaconTxPower(BleRecord bleRecord) {
-        return null;
-    }
-
-    // TODO(b/205320613) create beacon type
-    @Override
-    public int getBeaconIdType() {
-        return 1;
-    }
-
-    /** Returns the Model ID from our service data, if present. */
-    @Nullable
-    @Override
-    public byte[] getBeaconIdBytes(BleRecord bleRecord) {
-        return getModelId(bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID));
-    }
-
-    /** Returns the Model ID from our service data, if present. */
-    @Nullable
-    public static byte[] getModelId(@Nullable byte[] serviceData) {
-        if (serviceData == null) {
-            return null;
-        }
-
-        if (serviceData.length >= MIN_ID_LENGTH) {
-            if (serviceData.length == MIN_ID_LENGTH) {
-                // If the length == 3, all bytes are the ID. See flag docs for more about
-                // endianness.
-                return serviceData;
-            } else {
-                // Otherwise, the first byte is a header which contains the length of the
-                // big-endian model
-                // ID that follows. The model ID will be trimmed if it contains leading zeros.
-                int idIndex = 1;
-                int end = idIndex + getIdLength(serviceData);
-                while (serviceData[idIndex] == 0 && end - idIndex > MIN_ID_LENGTH) {
-                    idIndex++;
-                }
-                return Arrays.copyOfRange(serviceData, idIndex, end);
-            }
-        }
-        return null;
-    }
-
-    /** Gets the FastPair service data array if available, otherwise returns null. */
-    @Nullable
-    public static byte[] getServiceDataArray(BleRecord bleRecord) {
-        return bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
-    }
-
-    /** Gets the FastPair service data array if available, otherwise returns null. */
-    @Nullable
-    public static byte[] getServiceDataArray(ScanRecord scanRecord) {
-        return scanRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
-    }
-
-    /** Gets the bloom filter from the extra fields if available, otherwise returns null. */
-    @Nullable
-    public static byte[] getBloomFilter(@Nullable byte[] serviceData) {
-        return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER);
-    }
-
-    /** Gets the bloom filter salt from the extra fields if available, otherwise returns null. */
-    @Nullable
-    public static byte[] getBloomFilterSalt(byte[] serviceData) {
-        return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_SALT);
-    }
-
-    /**
-     * Gets the suppress notification with bloom filter from the extra fields if available,
-     * otherwise
-     * returns null.
-     */
-    @Nullable
-    public static byte[] getBloomFilterNoNotification(@Nullable byte[] serviceData) {
-        return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION);
-    }
-
-    /** Gets the battery level from extra fields if available, otherwise return null. */
-    @Nullable
-    public static byte[] getBatteryLevel(byte[] serviceData) {
-        return getExtraField(serviceData, FIELD_TYPE_BATTERY);
-    }
-
-    /**
-     * Gets the suppress notification with battery level from extra fields if available, otherwise
-     * return null.
-     */
-    @Nullable
-    public static byte[] getBatteryLevelNoNotification(byte[] serviceData) {
-        return getExtraField(serviceData, FIELD_TYPE_BATTERY_NO_NOTIFICATION);
-    }
-
-    /**
-     * Gets the random resolvable data from extra fields if available, otherwise
-     * return null.
-     */
-    @Nullable
-    public static byte[] getRandomResolvableData(byte[] serviceData) {
-        return getExtraField(serviceData, FIELD_TYPE_RANDOM_RESOLVABLE_DATA);
-    }
-
-    @Nullable
-    private static byte[] getExtraField(@Nullable byte[] serviceData, int fieldId) {
-        if (serviceData == null || serviceData.length < HEADER_INDEX + HEADER_LENGTH) {
-            return null;
-        }
-        try {
-            return getExtraFields(serviceData).get(fieldId);
-        } catch (IllegalFormatException e) {
-            Log.v("FastPairDecode", "Extra fields incorrectly formatted.");
-            return null;
-        }
-    }
-
-    /** Gets extra field data at the end of the packet, defined by the extra field header. */
-    private static SparseArray<byte[]> getExtraFields(byte[] serviceData)
-            throws IllegalFormatException {
-        SparseArray<byte[]> extraFields = new SparseArray<>();
-        if (getVersion(serviceData) != 0) {
-            return extraFields;
-        }
-        int headerIndex = getFirstExtraFieldHeaderIndex(serviceData);
-        while (headerIndex < serviceData.length) {
-            int length = getExtraFieldLength(serviceData, headerIndex);
-            int index = headerIndex + FIELD_HEADER_LENGTH;
-            int type = getExtraFieldType(serviceData, headerIndex);
-            int end = index + length;
-            if (extraFields.get(type) == null) {
-                if (end <= serviceData.length) {
-                    extraFields.put(type, Arrays.copyOfRange(serviceData, index, end));
-                } else {
-                    throw new IllegalFormatException(
-                            "Invalid length, " + end + " is longer than service data size "
-                                    + serviceData.length);
-                }
-            }
-            headerIndex = end;
-        }
-        return extraFields;
-    }
-
-    /** Checks whether or not a valid ID is included in the service data packet. */
-    public static boolean hasBeaconIdBytes(BleRecord bleRecord) {
-        byte[] serviceData = bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
-        return checkModelId(serviceData);
-    }
-
-    /** Check whether byte array is FastPair model id or not. */
-    public static boolean checkModelId(@Nullable byte[] scanResult) {
-        return scanResult != null
-                // The 3-byte format has no header byte (all bytes are the ID).
-                && (scanResult.length == MIN_ID_LENGTH
-                // Header byte exists. We support only format version 0. (A different version
-                // indicates
-                // a breaking change in the format.)
-                || (scanResult.length > MIN_ID_LENGTH
-                && getVersion(scanResult) == 0
-                && isIdLengthValid(scanResult)));
-    }
-
-    /** Checks whether or not bloom filter is included in the service data packet. */
-    public static boolean hasBloomFilter(BleRecord bleRecord) {
-        return (getBloomFilter(getServiceDataArray(bleRecord)) != null
-                || getBloomFilterNoNotification(getServiceDataArray(bleRecord)) != null);
-    }
-
-    /** Checks whether or not bloom filter is included in the service data packet. */
-    public static boolean hasBloomFilter(ScanRecord scanRecord) {
-        return (getBloomFilter(getServiceDataArray(scanRecord)) != null
-                || getBloomFilterNoNotification(getServiceDataArray(scanRecord)) != null);
-    }
-
-    private static int getVersion(byte[] serviceData) {
-        return serviceData.length == MIN_ID_LENGTH
-                ? 0
-                : (serviceData[HEADER_INDEX] & HEADER_VERSION_BITMASK) >> HEADER_VERSION_OFFSET;
-    }
-
-    private static int getIdLength(byte[] serviceData) {
-        return serviceData.length == MIN_ID_LENGTH
-                ? MIN_ID_LENGTH
-                : (serviceData[HEADER_INDEX] & HEADER_LENGTH_BITMASK) >> HEADER_LENGTH_OFFSET;
-    }
-
-    private static int getFirstExtraFieldHeaderIndex(byte[] serviceData) {
-        return HEADER_INDEX + HEADER_LENGTH + getIdLength(serviceData);
-    }
-
-    private static int getExtraFieldLength(byte[] serviceData, int extraFieldIndex) {
-        return (serviceData[extraFieldIndex] & EXTRA_FIELD_LENGTH_BITMASK)
-                >> EXTRA_FIELD_LENGTH_OFFSET;
-    }
-
-    private static int getExtraFieldType(byte[] serviceData, int extraFieldIndex) {
-        return (serviceData[extraFieldIndex] & EXTRA_FIELD_TYPE_BITMASK) >> EXTRA_FIELD_TYPE_OFFSET;
-    }
-
-    private static boolean isIdLengthValid(byte[] serviceData) {
-        int idLength = getIdLength(serviceData);
-        return MIN_ID_LENGTH <= idLength
-                && idLength <= MAX_ID_LENGTH
-                && idLength + HEADER_LENGTH <= serviceData.length;
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java b/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
deleted file mode 100644
index b4f46f8..0000000
--- a/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
+++ /dev/null
@@ -1,228 +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.server.nearby.common.ble.testing;
-
-import com.android.server.nearby.util.ArrayUtils;
-import com.android.server.nearby.util.Hex;
-
-/**
- * Test class to provide example to unit test.
- */
-public class FastPairTestData {
-    private static final byte[] FAST_PAIR_RECORD_BIG_ENDIAN =
-            Hex.stringToBytes("02011E020AF006162CFEAABBCC");
-
-    /**
-     * A Fast Pair frame, Note: The service UUID is FE2C, but in the
-     * packet it's 2CFE, since the core Bluetooth data types are little-endian.
-     *
-     * <p>However, the model ID is big-endian (multi-byte values in our spec are now big-endian, aka
-     * network byte order).
-     *
-     * @see {http://go/fast-pair-service-data}
-     */
-    public static byte[] getFastPairRecord() {
-        return FAST_PAIR_RECORD_BIG_ENDIAN;
-    }
-
-    /** A Fast Pair frame, with a shared account key. */
-    public static final byte[] FAST_PAIR_SHARED_ACCOUNT_KEY_RECORD =
-            Hex.stringToBytes("02011E020AF00C162CFE007011223344556677");
-
-    /** Model ID in {@link #getFastPairRecord()}. */
-    public static final byte[] FAST_PAIR_MODEL_ID = Hex.stringToBytes("AABBCC");
-
-    /** An arbitrary BLE device address. */
-    public static final String DEVICE_ADDRESS = "00:00:00:00:00:01";
-
-    /** Arbitrary RSSI (Received Signal Strength Indicator). */
-    public static final int RSSI = -72;
-
-    /** @see #getFastPairRecord() */
-    public static byte[] newFastPairRecord(byte header, byte[] modelId) {
-        return newFastPairRecord(
-                modelId.length == 3 ? modelId : ArrayUtils.concatByteArrays(new byte[] {header},
-                        modelId));
-    }
-
-    /** @see #getFastPairRecord() */
-    public static byte[] newFastPairRecord(byte[] serviceData) {
-        int length = /* length of type and service UUID = */ 3 + serviceData.length;
-        return Hex.stringToBytes(
-                String.format("02011E020AF0%02X162CFE%s", length,
-                        Hex.bytesToStringUppercase(serviceData)));
-    }
-
-    // This is an example extended inquiry response for a phone with PANU
-    // and Hands-free Audio Gateway
-    public static byte[] eir_1 = {
-            0x06, // Length of this Data
-            0x09, // <<Complete Local Name>>
-            'P',
-            'h',
-            'o',
-            'n',
-            'e',
-            0x05, // Length of this Data
-            0x03, // <<Complete list of 16-bit Service UUIDs>>
-            0x15,
-            0x11, // PANU service class UUID
-            0x1F,
-            0x11, // Hands-free Audio Gateway service class UUID
-            0x01, // Length of this data
-            0x05, // <<Complete list of 32-bit Service UUIDs>>
-            0x11, // Length of this data
-            0x07, // <<Complete list of 128-bit Service UUIDs>>
-            0x01,
-            0x02,
-            0x03,
-            0x04,
-            0x05,
-            0x06,
-            0x07,
-            0x08, // Made up UUID
-            0x11,
-            0x12,
-            0x13,
-            0x14,
-            0x15,
-            0x16,
-            0x17,
-            0x18, //
-            0x00 // End of Data (Not transmitted over the air
-    };
-
-    // This is an example of advertising data with AD types
-    public static byte[] adv_1 = {
-            0x02, // Length of this Data
-            0x01, // <<Flags>>
-            0x01, // LE Limited Discoverable Mode
-            0x0A, // Length of this Data
-            0x09, // <<Complete local name>>
-            'P', 'e', 'd', 'o', 'm', 'e', 't', 'e', 'r'
-    };
-
-    // This is an example of advertising data with positive TX Power
-    // Level.
-    public static byte[] adv_2 = {
-            0x02, // Length of this Data
-            0x0a, // <<TX Power Level>>
-            127 // Level = 127
-    };
-
-    // Example data including a service data block
-    public static byte[] sd1 = {
-            0x02, // Length of this Data
-            0x01, // <<Flags>>
-            0x04, // BR/EDR Not Supported.
-            0x03, // Length of this Data
-            0x02, // <<Incomplete List of 16-bit Service UUIDs>>
-            0x04,
-            0x18, // TX Power Service UUID
-            0x1e, // Length of this Data
-            (byte) 0x16, // <<Service Specific Data>>
-            // Service UUID
-            (byte) 0xe0,
-            0x00,
-            // gBeacon Header
-            0x15,
-            // Running time ENCRYPT
-            (byte) 0xd2,
-            0x77,
-            0x01,
-            0x00,
-            // Scan Freq ENCRYPT
-            0x32,
-            0x05,
-            // Time in slow mode
-            0x00,
-            0x00,
-            // Time in fast mode
-            0x7f,
-            0x17,
-            // Subset of UID
-            0x56,
-            0x00,
-            // ID Mask
-            (byte) 0xd4,
-            0x7c,
-            0x18,
-            // RFU (reserved)
-            0x00,
-            // GUID = decimal 1297482358
-            0x76,
-            0x02,
-            0x56,
-            0x4d,
-            0x00,
-            // Ranging Payload Header
-            0x24,
-            // MAC of scanning address
-            (byte) 0xa4,
-            (byte) 0xbb,
-            // NORM RX RSSI -67dBm
-            (byte) 0xb0,
-            // NORM TX POWER -77dBm, so actual TX POWER = -36dBm
-            (byte) 0xb3,
-            // Note based on the values aboves PATH LOSS = (-36) - (-67) = 31dBm
-            // Below zero padding added to test it is handled correctly
-            0x00
-    };
-
-    // An Eddystone UID frame. go/eddystone for more info
-    public static byte[] eddystone_header_and_uuid = {
-            // BLE Flags
-            (byte) 0x02,
-            (byte) 0x01,
-            (byte) 0x06,
-            // Service UUID
-            (byte) 0x03,
-            (byte) 0x03,
-            (byte) 0xaa,
-            (byte) 0xfe,
-            // Service data header
-            (byte) 0x17,
-            (byte) 0x16,
-            (byte) 0xaa,
-            (byte) 0xfe,
-            // Eddystone frame type
-            (byte) 0x00,
-            // Ranging data
-            (byte) 0xb3,
-            // Eddystone ID namespace
-            (byte) 0x0a,
-            (byte) 0x09,
-            (byte) 0x08,
-            (byte) 0x07,
-            (byte) 0x06,
-            (byte) 0x05,
-            (byte) 0x04,
-            (byte) 0x03,
-            (byte) 0x02,
-            (byte) 0x01,
-            // Eddystone ID instance
-            (byte) 0x16,
-            (byte) 0x15,
-            (byte) 0x14,
-            (byte) 0x13,
-            (byte) 0x12,
-            (byte) 0x11,
-            // RFU
-            (byte) 0x00,
-            (byte) 0x00
-    };
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/util/RangingUtils.java b/nearby/service/java/com/android/server/nearby/common/ble/util/RangingUtils.java
deleted file mode 100644
index eec52ad..0000000
--- a/nearby/service/java/com/android/server/nearby/common/ble/util/RangingUtils.java
+++ /dev/null
@@ -1,161 +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.server.nearby.common.ble.util;
-
-
-/**
- * Ranging utilities embody the physics of converting RF path loss to distance. The free space path
- * loss is proportional to the square of the distance from transmitter to receiver, and to the
- * square of the frequency of the propagation signal.
- */
-public final class RangingUtils {
-    private static final int MAX_RSSI_VALUE = 126;
-    private static final int MIN_RSSI_VALUE = -127;
-
-    private RangingUtils() {
-    }
-
-    /* This was original derived in {@link com.google.android.gms.beacon.util.RangingUtils} from
-     * <a href="http://en.wikipedia.org/wiki/Free-space_path_loss">Free-space_path_loss</a>.
-     * Duplicated here for easy reference.
-     *
-     * c   = speed of light (2.9979 x 10^8 m/s);
-     * f   = frequency (Bluetooth center frequency is 2.44175GHz = 2.44175x10^9 Hz);
-     * l   = wavelength (in meters);
-     * d   = distance (from transmitter to receiver in meters);
-     * dB  = decibels
-     * dBm = decibel milliwatts
-     *
-     *
-     * Free-space path loss (FSPL) is proportional to the square of the distance between the
-     * transmitter and the receiver, and also proportional to the square of the frequency of the
-     * radio signal.
-     *
-     * FSPL      = (4 * pi * d / l)^2 = (4 * pi * d * f / c)^2
-     *
-     * FSPL (dB) = 10 * log10((4 * pi * d  * f / c)^2)
-     *           = 20 * log10(4 * pi * d * f / c)
-     *           = (20 * log10(d)) + (20 * log10(f)) + (20 * log10(4 * pi/c))
-     *
-     * Calculating constants:
-     *
-     * FSPL_FREQ        = 20 * log10(f)
-     *                  = 20 * log10(2.44175 * 10^9)
-     *                  = 187.75
-     *
-     * FSPL_LIGHT       = 20 * log10(4 * pi/c)
-     *                  = 20 * log10(4 * pi/(2.9979 * 10^8))
-     *                  = 20 * log10(4 * pi/(2.9979 * 10^8))
-     *                  = 20 * log10(41.9172441s * 10^-9)
-     *                  = -147.55
-     *
-     * FSPL_DISTANCE_1M = 20 * log10(1)
-     *                  = 0
-     *
-     * PATH_LOSS_AT_1M  = FSPL_DISTANCE_1M + FSPL_FREQ + FSPL_LIGHT
-     *                  =       0          + 187.75    + (-147.55)
-     *                  = 40.20db [round to 41db]
-     *
-     * Note: Rounding up makes us "closer" and makes us more aggressive at showing notifications.
-     */
-    private static final int RSSI_DROP_OFF_AT_1_M = 41;
-
-    /**
-     * Convert target distance and txPower to a RSSI value using the Log-distance path loss model
-     * with Path Loss at 1m of 41db.
-     *
-     * @return RSSI expected at distanceInMeters with device broadcasting at txPower.
-     */
-    public static int rssiFromTargetDistance(double distanceInMeters, int txPower) {
-        /*
-         * See <a href="https://en.wikipedia.org/wiki/Log-distance_path_loss_model">
-         * Log-distance path loss model</a>.
-         *
-         * PL      = total path loss in db
-         * txPower = TxPower in dbm
-         * rssi    = Received signal strength in dbm
-         * PL_0    = Path loss at reference distance d_0 {@link RSSI_DROP_OFF_AT_1_M} dbm
-         * d       = length of path
-         * d_0     = reference distance  (1 m)
-         * gamma   = path loss exponent (2 in free space)
-         *
-         * Log-distance path loss (LDPL) formula:
-         *
-         * PL = txPower - rssi =                   PL_0          + 10 * gamma  * log_10(d / d_0)
-         *      txPower - rssi =            RSSI_DROP_OFF_AT_1_M + 10 * 2 * log_10
-         * (distanceInMeters / 1)
-         *              - rssi = -txPower + RSSI_DROP_OFF_AT_1_M + 20 * log_10(distanceInMeters)
-         *                rssi =  txPower - RSSI_DROP_OFF_AT_1_M - 20 * log_10(distanceInMeters)
-         */
-        txPower = adjustPower(txPower);
-        return distanceInMeters == 0
-                ? txPower
-                : (int) Math.floor((txPower - RSSI_DROP_OFF_AT_1_M)
-                        - 20 * Math.log10(distanceInMeters));
-    }
-
-    /**
-     * Convert RSSI and txPower to a distance value using the Log-distance path loss model with Path
-     * Loss at 1m of 41db.
-     *
-     * @return distance in meters with device broadcasting at txPower and given RSSI.
-     */
-    public static double distanceFromRssiAndTxPower(int rssi, int txPower) {
-        /*
-         * See <a href="https://en.wikipedia.org/wiki/Log-distance_path_loss_model">Log-distance
-         * path
-         * loss model</a>.
-         *
-         * PL      = total path loss in db
-         * txPower = TxPower in dbm
-         * rssi    = Received signal strength in dbm
-         * PL_0    = Path loss at reference distance d_0 {@link RSSI_DROP_OFF_AT_1_M} dbm
-         * d       = length of path
-         * d_0     = reference distance  (1 m)
-         * gamma   = path loss exponent (2 in free space)
-         *
-         * Log-distance path loss (LDPL) formula:
-         *
-         * PL =    txPower - rssi                               = PL_0 + 10 * gamma  * log_10(d /
-         *  d_0)
-         *         txPower - rssi               = RSSI_DROP_OFF_AT_1_M + 10 * gamma  * log_10(d /
-         *  d_0)
-         *         txPower - rssi - RSSI_DROP_OFF_AT_1_M        = 10 * 2 * log_10
-         * (distanceInMeters / 1)
-         *         txPower - rssi - RSSI_DROP_OFF_AT_1_M        = 20 * log_10(distanceInMeters / 1)
-         *        (txPower - rssi - RSSI_DROP_OFF_AT_1_M) / 20  = log_10(distanceInMeters)
-         *  10 ^ ((txPower - rssi - RSSI_DROP_OFF_AT_1_M) / 20) = distanceInMeters
-         */
-        txPower = adjustPower(txPower);
-        rssi = adjustPower(rssi);
-        return Math.pow(10, (txPower - rssi - RSSI_DROP_OFF_AT_1_M) / 20.0);
-    }
-
-    /**
-     * Prevents the power from becoming too large or too small.
-     */
-    private static int adjustPower(int power) {
-        if (power > MAX_RSSI_VALUE) {
-            return MAX_RSSI_VALUE;
-        }
-        if (power < MIN_RSSI_VALUE) {
-            return MIN_RSSI_VALUE;
-        }
-        return power;
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/util/StringUtils.java b/nearby/service/java/com/android/server/nearby/common/ble/util/StringUtils.java
deleted file mode 100644
index 4d90b6d..0000000
--- a/nearby/service/java/com/android/server/nearby/common/ble/util/StringUtils.java
+++ /dev/null
@@ -1,70 +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.server.nearby.common.ble.util;
-
-import android.annotation.Nullable;
-import android.util.SparseArray;
-
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.Map;
-
-/** Helper class for Bluetooth LE utils. */
-public final class StringUtils {
-    private StringUtils() {
-    }
-
-    /** Returns a string composed from a {@link SparseArray}. */
-    public static String toString(@Nullable SparseArray<byte[]> array) {
-        if (array == null) {
-            return "null";
-        }
-        if (array.size() == 0) {
-            return "{}";
-        }
-        StringBuilder buffer = new StringBuilder();
-        buffer.append('{');
-        for (int i = 0; i < array.size(); ++i) {
-            buffer.append(array.keyAt(i)).append("=").append(Arrays.toString(array.valueAt(i)));
-        }
-        buffer.append('}');
-        return buffer.toString();
-    }
-
-    /** Returns a string composed from a {@link Map}. */
-    public static <T> String toString(@Nullable Map<T, byte[]> map) {
-        if (map == null) {
-            return "null";
-        }
-        if (map.isEmpty()) {
-            return "{}";
-        }
-        StringBuilder buffer = new StringBuilder();
-        buffer.append('{');
-        Iterator<Map.Entry<T, byte[]>> it = map.entrySet().iterator();
-        while (it.hasNext()) {
-            Map.Entry<T, byte[]> entry = it.next();
-            Object key = entry.getKey();
-            buffer.append(key).append("=").append(Arrays.toString(map.get(key)));
-            if (it.hasNext()) {
-                buffer.append(", ");
-            }
-        }
-        buffer.append('}');
-        return buffer.toString();
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bloomfilter/BloomFilter.java b/nearby/service/java/com/android/server/nearby/common/bloomfilter/BloomFilter.java
deleted file mode 100644
index 6d4275f..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bloomfilter/BloomFilter.java
+++ /dev/null
@@ -1,108 +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.server.nearby.common.bloomfilter;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.primitives.UnsignedInts;
-
-import java.nio.charset.Charset;
-import java.util.Arrays;
-import java.util.BitSet;
-
-/**
- * A bloom filter that gives access to the underlying BitSet.
- */
-public class BloomFilter {
-    private static final Charset CHARSET = UTF_8;
-
-    /**
-     * Receives a value and converts it into an array of ints that will be converted to indexes for
-     * the filter.
-     */
-    public interface Hasher {
-        /**
-         * Generate hash value.
-         */
-        int[] getHashes(byte[] value);
-    }
-
-    // The backing data for this bloom filter. As additions are made, they're OR'd until it
-    // eventually reaches 0xFF.
-    private final BitSet mBits;
-    // The max length of bits.
-    private final int mBitLength;
-    // The hasher to use for converting a value into an array of hashes.
-    private final Hasher mHasher;
-
-    public BloomFilter(byte[] bytes, Hasher hasher) {
-        this.mBits = BitSet.valueOf(bytes);
-        this.mBitLength = bytes.length * 8;
-        this.mHasher = hasher;
-    }
-
-    /**
-     * Return the bloom filter check bit set as byte array.
-     */
-    public byte[] asBytes() {
-        // BitSet.toByteArray() truncates all the unset bits after the last set bit (eg. [0,0,1,0]
-        // becomes [0,0,1]) so we re-add those bytes if needed with Arrays.copy().
-        byte[] b = mBits.toByteArray();
-        if (b.length == mBitLength / 8) {
-            return b;
-        }
-        return Arrays.copyOf(b, mBitLength / 8);
-    }
-
-    /**
-     * Add string value to bloom filter hash.
-     */
-    public void add(String s) {
-        add(s.getBytes(CHARSET));
-    }
-
-    /**
-     * Adds value to bloom filter hash.
-     */
-    public void add(byte[] value) {
-        int[] hashes = mHasher.getHashes(value);
-        for (int hash : hashes) {
-            mBits.set(UnsignedInts.remainder(hash, mBitLength));
-        }
-    }
-
-    /**
-     * Check if the string format has collision.
-     */
-    public boolean possiblyContains(String s) {
-        return possiblyContains(s.getBytes(CHARSET));
-    }
-
-    /**
-     * Checks if value after hash will have collision.
-     */
-    public boolean possiblyContains(byte[] value) {
-        int[] hashes = mHasher.getHashes(value);
-        for (int hash : hashes) {
-            if (!mBits.get(UnsignedInts.remainder(hash, mBitLength))) {
-                return false;
-            }
-        }
-        return true;
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasher.java b/nearby/service/java/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasher.java
deleted file mode 100644
index 0ccee97..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasher.java
+++ /dev/null
@@ -1,41 +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.server.nearby.common.bloomfilter;
-
-import com.google.common.hash.Hashing;
-
-import java.nio.ByteBuffer;
-
-/**
- * Hasher which hashes a value using SHA-256 and splits it into parts, each of which can be
- * converted to an index.
- */
-public class FastPairBloomFilterHasher implements BloomFilter.Hasher {
-
-    private static final int NUM_INDEXES = 8;
-
-    @Override
-    public int[] getHashes(byte[] value) {
-        byte[] hash = Hashing.sha256().hashBytes(value).asBytes();
-        ByteBuffer buffer = ByteBuffer.wrap(hash);
-        int[] hashes = new int[NUM_INDEXES];
-        for (int i = 0; i < NUM_INDEXES; i++) {
-            hashes[i] = buffer.getInt();
-        }
-        return hashes;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothConsts.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothConsts.java
deleted file mode 100644
index 3a02b18..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothConsts.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth;
-
-import java.util.UUID;
-
-/**
- * Bluetooth constants.
- */
-public class BluetoothConsts {
-
-    /**
-     * Default MTU when value is unknown.
-     */
-    public static final int DEFAULT_MTU = 23;
-
-    // The following random uuids are used to indicate that the device has dynamic services.
-    /**
-     * UUID of dynamic service.
-     */
-    public static final UUID SERVICE_DYNAMIC_SERVICE =
-            UUID.fromString("00000100-0af3-11e5-a6c0-1697f925ec7b");
-
-    /**
-     * UUID of dynamic characteristic.
-     */
-    public static final UUID SERVICE_DYNAMIC_CHARACTERISTIC =
-            UUID.fromString("00002A05-0af3-11e5-a6c0-1697f925ec7b");
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothException.java
deleted file mode 100644
index db2e1cc..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth;
-
-/**
- * {@link Exception} thrown during a Bluetooth operation.
- */
-public class BluetoothException extends Exception {
-    /** Constructor. */
-    public BluetoothException(String message) {
-        super(message);
-    }
-
-    /** Constructor. */
-    public BluetoothException(String message, Throwable throwable) {
-        super(message, throwable);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothGattException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothGattException.java
deleted file mode 100644
index 5ac4882..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothGattException.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth;
-
-/**
- * Exception for Bluetooth GATT operations.
- */
-public class BluetoothGattException extends BluetoothException {
-    private final int mErrorCode;
-
-    /** Constructor. */
-    public BluetoothGattException(String message, int errorCode) {
-        super(message);
-        mErrorCode = errorCode;
-    }
-
-    /** Constructor. */
-    public BluetoothGattException(String message, int errorCode, Throwable cause) {
-        super(message, cause);
-        mErrorCode = errorCode;
-    }
-
-    /** Returns Gatt error code. */
-    public int getGattErrorCode() {
-        return mErrorCode;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothTimeoutException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothTimeoutException.java
deleted file mode 100644
index 30fd188..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothTimeoutException.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth;
-
-/**
- * {@link Exception} thrown during a Bluetooth operation when a timeout occurs.
- */
-public class BluetoothTimeoutException extends BluetoothException {
-
-    /** Constructor. */
-    public BluetoothTimeoutException(String message) {
-        super(message);
-    }
-
-    /** Constructor. */
-    public BluetoothTimeoutException(String message, Throwable throwable) {
-        super(message, throwable);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/ReservedUuids.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/ReservedUuids.java
deleted file mode 100644
index 249011a..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/ReservedUuids.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth;
-
-import java.util.UUID;
-
-/**
- * Reserved UUIDS by BT SIG.
- * <p>
- * See https://developer.bluetooth.org for more details.
- */
-public class ReservedUuids {
-    /** UUIDs reserved for services. */
-    public static class Services {
-        /**
-         * The Device Information Service exposes manufacturer and/or vendor info about a device.
-         * <p>
-         * See reserved UUID org.bluetooth.service.device_information.
-         */
-        public static final UUID DEVICE_INFORMATION = fromShortUuid((short) 0x180A);
-
-        /**
-         * Generic attribute service.
-         * <p>
-         * See reserved UUID org.bluetooth.service.generic_attribute.
-         */
-        public static final UUID GENERIC_ATTRIBUTE = fromShortUuid((short) 0x1801);
-    }
-
-    /** UUIDs reserved for characteristics. */
-    public static class Characteristics {
-        /**
-         * The value of this characteristic is a UTF-8 string representing the firmware revision for
-         * the firmware within the device.
-         * <p>
-         * See reserved UUID org.bluetooth.characteristic.firmware_revision_string.
-         */
-        public static final UUID FIRMWARE_REVISION_STRING = fromShortUuid((short) 0x2A26);
-
-        /**
-         * Service change characteristic.
-         * <p>
-         * See reserved UUID org.bluetooth.characteristic.gatt.service_changed.
-         */
-        public static final UUID SERVICE_CHANGE = fromShortUuid((short) 0x2A05);
-    }
-
-    /** UUIDs reserved for descriptors. */
-    public static class Descriptors {
-        /**
-         * This descriptor shall be persistent across connections for bonded devices. The Client
-         * Characteristic Configuration descriptor is unique for each client. A client may read and
-         * write this descriptor to determine and set the configuration for that client.
-         * Authentication and authorization may be required by the server to write this descriptor.
-         * The default value for the Client Characteristic Configuration descriptor is 0x00. Upon
-         * connection of non-binded clients, this descriptor is set to the default value.
-         * <p>
-         * See reserved UUID org.bluetooth.descriptor.gatt.client_characteristic_configuration.
-         */
-        public static final UUID CLIENT_CHARACTERISTIC_CONFIGURATION =
-                fromShortUuid((short) 0x2902);
-    }
-
-    /** The base 128-bit UUID representation of a 16-bit UUID */
-    public static final UUID BASE_16_BIT_UUID =
-            UUID.fromString("00000000-0000-1000-8000-00805F9B34FB");
-
-    /** Converts from short UUId to UUID. */
-    public static UUID fromShortUuid(short shortUuid) {
-        return new UUID(((((long) shortUuid) << 32) & 0x0000FFFF00000000L)
-                | ReservedUuids.BASE_16_BIT_UUID.getMostSignificantBits(),
-                ReservedUuids.BASE_16_BIT_UUID.getLeastSignificantBits());
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGenerator.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGenerator.java
deleted file mode 100644
index 28a9c33..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGenerator.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.generateKey;
-
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic;
-
-import java.security.NoSuchAlgorithmException;
-
-/**
- * This is to generate account key with fast-pair style.
- */
-public final class AccountKeyGenerator {
-
-    // Generate a key where the first byte is always defined as the type, 0x04. This maintains 15
-    // bytes of entropy in the key while also allowing providers to verify that they have received
-    // a properly formatted key and decrypted it correctly, minimizing the risk of replay attacks.
-
-    /**
-     * Creates account key.
-     */
-    public static byte[] createAccountKey() throws NoSuchAlgorithmException {
-        byte[] accountKey = generateKey();
-        accountKey[0] = AccountKeyCharacteristic.TYPE;
-        return accountKey;
-    }
-
-    private AccountKeyGenerator() {
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoder.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoder.java
deleted file mode 100644
index c9ccfd5..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoder.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE;
-
-import static com.google.common.primitives.Bytes.concat;
-
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-
-/**
- * Utilities for encoding/decoding the additional data packet and verifying both the data integrity
- * and the authentication.
- *
- * <p>Additional Data packet is:
- *
- * <ol>
- *   <li>AdditionalData_Packet[0 - 7]: the first 8-byte of HMAC.
- *   <li>AdditionalData_Packet[8 - var]: the encrypted message by AES-CTR, with 8-byte nonce
- *       appended to the front.
- * </ol>
- *
- * See https://developers.google.com/nearby/fast-pair/spec#AdditionalData.
- */
-public final class AdditionalDataEncoder {
-
-    static final int EXTRACT_HMAC_SIZE = 8;
-    static final int MAX_LENGTH_OF_DATA = 64;
-
-    /**
-     * Encodes the given data to additional data packet by the given secret.
-     */
-    static byte[] encodeAdditionalDataPacket(byte[] secret, byte[] additionalData)
-            throws GeneralSecurityException {
-        if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) {
-            throw new GeneralSecurityException(
-                    "Incorrect secret for encoding additional data packet, secret.length = "
-                            + (secret == null ? "NULL" : secret.length));
-        }
-
-        if ((additionalData == null)
-                || (additionalData.length == 0)
-                || (additionalData.length > MAX_LENGTH_OF_DATA)) {
-            throw new GeneralSecurityException(
-                    "Invalid data for encoding additional data packet, data = "
-                            + (additionalData == null ? "NULL" : additionalData.length));
-        }
-
-        byte[] encryptedData = AesCtrMultipleBlockEncryption.encrypt(secret, additionalData);
-        byte[] extractedHmac =
-                Arrays.copyOf(HmacSha256.build(secret, encryptedData), EXTRACT_HMAC_SIZE);
-
-        return concat(extractedHmac, encryptedData);
-    }
-
-    /**
-     * Decodes additional data packet by the given secret.
-     *
-     * @param secret AES-128 key used in the encryption to decrypt data
-     * @param additionalDataPacket additional data packet which is encoded by the given secret
-     * @return the data byte array decoded from the given packet
-     * @throws GeneralSecurityException if the given key or additional data packet is invalid for
-     * decoding
-     */
-    static byte[] decodeAdditionalDataPacket(byte[] secret, byte[] additionalDataPacket)
-            throws GeneralSecurityException {
-        if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) {
-            throw new GeneralSecurityException(
-                    "Incorrect secret for decoding additional data packet, secret.length = "
-                            + (secret == null ? "NULL" : secret.length));
-        }
-        if (additionalDataPacket == null
-                || additionalDataPacket.length <= EXTRACT_HMAC_SIZE
-                || additionalDataPacket.length
-                > (MAX_LENGTH_OF_DATA + EXTRACT_HMAC_SIZE + NONCE_SIZE)) {
-            throw new GeneralSecurityException(
-                    "Additional data packet size is incorrect, additionalDataPacket.length is "
-                            + (additionalDataPacket == null ? "NULL"
-                            : additionalDataPacket.length));
-        }
-
-        if (!verifyHmac(secret, additionalDataPacket)) {
-            throw new GeneralSecurityException(
-                    "Verify HMAC failed, could be incorrect key or packet.");
-        }
-        byte[] encryptedData =
-                Arrays.copyOfRange(
-                        additionalDataPacket, EXTRACT_HMAC_SIZE, additionalDataPacket.length);
-        return AesCtrMultipleBlockEncryption.decrypt(secret, encryptedData);
-    }
-
-    // Computes the HMAC of the given key and additional data, and compares the first 8-byte of the
-    // HMAC result with the one from additional data packet.
-    // Must call constant-time comparison to prevent a possible timing attack, e.g. time the same
-    // MAC with all different first byte for a given ciphertext, the right one will take longer as
-    // it will fail on the second byte's verification.
-    private static boolean verifyHmac(byte[] key, byte[] additionalDataPacket)
-            throws GeneralSecurityException {
-        byte[] packetHmac =
-                Arrays.copyOfRange(additionalDataPacket, /* from= */ 0, EXTRACT_HMAC_SIZE);
-        byte[] encryptedData =
-                Arrays.copyOfRange(
-                        additionalDataPacket, EXTRACT_HMAC_SIZE, additionalDataPacket.length);
-        byte[] computedHmac = Arrays.copyOf(
-                HmacSha256.build(key, encryptedData), EXTRACT_HMAC_SIZE);
-
-        return HmacSha256.compareTwoHMACs(packetHmac, computedHmac);
-    }
-
-    private AdditionalDataEncoder() {
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java
deleted file mode 100644
index 50a818b..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.primitives.Bytes.concat;
-
-import androidx.annotation.VisibleForTesting;
-
-import java.security.GeneralSecurityException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-
-/**
- * AES-CTR utilities used for encrypting and decrypting Fast Pair packets that contain multiple
- * blocks. Encrypts input data by:
- *
- * <ol>
- *   <li>encryptedBlock[i] = clearBlock[i] ^ AES(counter), and
- *   <li>concat(encryptedBlock[0], encryptedBlock[1],...) to create the encrypted result, where
- *   <li>counter: the 16-byte input of AES. counter = iv + block_index.
- *   <li>iv: extend 8-byte nonce to 16 bytes with zero padding. i.e. concat(0x0000000000000000,
- *       nonce).
- *   <li>nonce: the cryptographically random 8 bytes, must never be reused with the same key.
- * </ol>
- */
-final class AesCtrMultipleBlockEncryption {
-
-    /** Length for AES-128 key. */
-    static final int KEY_LENGTH = AesEcbSingleBlockEncryption.KEY_LENGTH;
-
-    @VisibleForTesting
-    static final int AES_BLOCK_LENGTH = AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
-
-    /** Length of the nonce, a byte array of cryptographically random bytes. */
-    static final int NONCE_SIZE = 8;
-
-    private static final int IV_SIZE = AES_BLOCK_LENGTH;
-    private static final int MAX_NUMBER_OF_BLOCKS = 4;
-
-    private AesCtrMultipleBlockEncryption() {}
-
-    /** Generates a 16-byte AES key. */
-    static byte[] generateKey() throws NoSuchAlgorithmException {
-        return AesEcbSingleBlockEncryption.generateKey();
-    }
-
-    /**
-     * Encrypts data using AES-CTR by the given secret.
-     *
-     * @param secret AES-128 key.
-     * @param data the plaintext to be encrypted.
-     * @return the encrypted data with the 8-byte nonce appended to the front.
-     */
-    static byte[] encrypt(byte[] secret, byte[] data) throws GeneralSecurityException {
-        byte[] nonce = generateNonce();
-        return concat(nonce, doAesCtr(secret, data, nonce));
-    }
-
-    /**
-     * Decrypts data using AES-CTR by the given secret and nonce.
-     *
-     * @param secret AES-128 key.
-     * @param data the first 8 bytes is the nonce, and the remaining is the encrypted data to be
-     *     decrypted.
-     * @return the decrypted data.
-     */
-    static byte[] decrypt(byte[] secret, byte[] data) throws GeneralSecurityException {
-        if (data == null || data.length <= NONCE_SIZE) {
-            throw new GeneralSecurityException(
-                    "Incorrect data length "
-                            + (data == null ? "NULL" : data.length)
-                            + " to decrypt, the data should contain nonce.");
-        }
-        byte[] nonce = Arrays.copyOf(data, NONCE_SIZE);
-        byte[] encryptedData = Arrays.copyOfRange(data, NONCE_SIZE, data.length);
-        return doAesCtr(secret, encryptedData, nonce);
-    }
-
-    /**
-     * Generates cryptographically random NONCE_SIZE bytes nonce. This nonce can be used only once.
-     * Always call this function to generate a new nonce before a new encryption.
-     */
-    // Suppression for a warning for potentially insecure random numbers on Android 4.3 and older.
-    // Fast Pair service is only for Android 6.0+ devices.
-    static byte[] generateNonce() {
-        SecureRandom random = new SecureRandom();
-        byte[] nonce = new byte[NONCE_SIZE];
-        random.nextBytes(nonce);
-
-        return nonce;
-    }
-
-    // AES-CTR implementation.
-    @VisibleForTesting
-    static byte[] doAesCtr(byte[] secret, byte[] data, byte[] nonce)
-            throws GeneralSecurityException {
-        if (secret.length != KEY_LENGTH) {
-            throw new IllegalArgumentException(
-                    "Incorrect key length for encryption, only supports 16-byte AES Key.");
-        }
-        if (nonce.length != NONCE_SIZE) {
-            throw new IllegalArgumentException(
-                    "Incorrect nonce length for encryption, "
-                            + "Fast Pair naming scheme only supports 8-byte nonce.");
-        }
-
-        // Keeps the following operations on this byte[], returns it as the final AES-CTR result.
-        byte[] aesCtrResult = new byte[data.length];
-        System.arraycopy(data, /*srcPos=*/ 0, aesCtrResult, /*destPos=*/ 0, data.length);
-
-        // Initializes counter as IV.
-        byte[] counter = createIv(nonce);
-        // The length of the given data is permitted to non-align block size.
-        int numberOfBlocks =
-                (data.length / AES_BLOCK_LENGTH) + ((data.length % AES_BLOCK_LENGTH == 0) ? 0 : 1);
-
-        if (numberOfBlocks > MAX_NUMBER_OF_BLOCKS) {
-            throw new IllegalArgumentException(
-                    "Incorrect data size, Fast Pair naming scheme only supports 4 blocks.");
-        }
-
-        for (int i = 0; i < numberOfBlocks; i++) {
-            // Performs the operation: encryptedBlock[i] = clearBlock[i] ^ AES(counter).
-            counter[0] = (byte) (i & 0xFF);
-            byte[] aesOfCounter = doAesSingleBlock(secret, counter);
-            int start = i * AES_BLOCK_LENGTH;
-            // The size of the last block of data may not be 16 bytes. If not, still do xor to the
-            // last byte of data.
-            int end = Math.min(start + AES_BLOCK_LENGTH, data.length);
-            for (int j = 0; start < end; j++, start++) {
-                aesCtrResult[start] ^= aesOfCounter[j];
-            }
-        }
-        return aesCtrResult;
-    }
-
-    private static byte[] doAesSingleBlock(byte[] secret, byte[] counter)
-            throws GeneralSecurityException {
-        return AesEcbSingleBlockEncryption.encrypt(secret, counter);
-    }
-
-    /** Extends 8-byte nonce to 16 bytes with zero padding to create IV. */
-    private static byte[] createIv(byte[] nonce) {
-        return concat(new byte[IV_SIZE - NONCE_SIZE], nonce);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryption.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryption.java
deleted file mode 100644
index 547931e..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryption.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import android.annotation.SuppressLint;
-
-import java.security.GeneralSecurityException;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.Cipher;
-import javax.crypto.KeyGenerator;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * Utilities used for encrypting and decrypting Fast Pair packets.
- */
-// SuppressLint for ""ecb encryption mode should not be used".
-// Reasons:
-//    1. FastPair data is guaranteed to be only 1 AES block in size, ECB is secure.
-//    2. In each case, the encrypted data is less than 16-bytes and is
-//       padded up to 16-bytes using random data to fill the rest of the byte array,
-//       so the plaintext will never be the same.
-@SuppressLint("GetInstance")
-public final class AesEcbSingleBlockEncryption {
-
-    public static final int AES_BLOCK_LENGTH = 16;
-    public static final int KEY_LENGTH = 16;
-
-    private AesEcbSingleBlockEncryption() {
-    }
-
-    /**
-     * Generates a 16-byte AES key.
-     */
-    public static byte[] generateKey() throws NoSuchAlgorithmException {
-        KeyGenerator generator = KeyGenerator.getInstance("AES");
-        generator.init(KEY_LENGTH * 8); // Ensure a 16-byte key is always used.
-        return generator.generateKey().getEncoded();
-    }
-
-    /**
-     * Encrypts data with the provided secret.
-     */
-    public static byte[] encrypt(byte[] secret, byte[] data) throws GeneralSecurityException {
-        return doEncryption(Cipher.ENCRYPT_MODE, secret, data);
-    }
-
-    /**
-     * Decrypts data with the provided secret.
-     */
-    public static byte[] decrypt(byte[] secret, byte[] data) throws GeneralSecurityException {
-        return doEncryption(Cipher.DECRYPT_MODE, secret, data);
-    }
-
-    private static byte[] doEncryption(int mode, byte[] secret, byte[] data)
-            throws GeneralSecurityException {
-        if (data.length != AES_BLOCK_LENGTH) {
-            throw new IllegalArgumentException("This encrypter only supports 16-byte inputs.");
-        }
-        Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
-        cipher.init(mode, new SecretKeySpec(secret, "AES"));
-        return cipher.doFinal(data);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddress.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddress.java
deleted file mode 100644
index 9bb5a86..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddress.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.io.BaseEncoding.base16;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.content.Context;
-import android.provider.Settings;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.google.common.base.Ascii;
-import com.google.common.io.BaseEncoding;
-
-import java.util.Locale;
-
-/** Utils for dealing with Bluetooth addresses. */
-public final class BluetoothAddress {
-
-    private static final BaseEncoding ENCODING = base16().upperCase().withSeparator(":", 2);
-
-    @VisibleForTesting
-    static final String SECURE_SETTINGS_KEY_BLUETOOTH_ADDRESS = "bluetooth_address";
-
-    /**
-     * @return The string format used by e.g. {@link android.bluetooth.BluetoothDevice}. Upper case.
-     *     Example: "AA:BB:CC:11:22:33"
-     */
-    public static String encode(byte[] address) {
-        return ENCODING.encode(address);
-    }
-
-    /**
-     * @param address The string format used by e.g. {@link android.bluetooth.BluetoothDevice}.
-     *     Case-insensitive. Example: "AA:BB:CC:11:22:33"
-     */
-    public static byte[] decode(String address) {
-        return ENCODING.decode(address.toUpperCase(Locale.US));
-    }
-
-    /**
-     * Get public bluetooth address.
-     *
-     * @param context a valid {@link Context} instance.
-     */
-    public static @Nullable byte[] getPublicAddress(Context context) {
-        String publicAddress =
-                Settings.Secure.getString(
-                        context.getContentResolver(), SECURE_SETTINGS_KEY_BLUETOOTH_ADDRESS);
-        return publicAddress != null && BluetoothAdapter.checkBluetoothAddress(publicAddress)
-                ? decode(publicAddress)
-                : null;
-    }
-
-    /**
-     * Hides partial information of Bluetooth address.
-     * ex1: input is null, output should be empty string
-     * ex2: input is String(AA:BB:CC), output should be AA:BB:CC
-     * ex3: input is String(AA:BB:CC:DD:EE:FF), output should be XX:XX:XX:XX:EE:FF
-     * ex4: input is String(Aa:Bb:Cc:Dd:Ee:Ff), output should be XX:XX:XX:XX:EE:FF
-     * ex5: input is BluetoothDevice(AA:BB:CC:DD:EE:FF), output should be XX:XX:XX:XX:EE:FF
-     */
-    public static String maskBluetoothAddress(@Nullable Object address) {
-        if (address == null) {
-            return "";
-        }
-
-        if (address instanceof String) {
-            String originalAddress = (String) address;
-            String upperCasedAddress = Ascii.toUpperCase(originalAddress);
-            if (!BluetoothAdapter.checkBluetoothAddress(upperCasedAddress)) {
-                return originalAddress;
-            }
-            return convert(upperCasedAddress);
-        } else if (address instanceof BluetoothDevice) {
-            return convert(((BluetoothDevice) address).getAddress());
-        }
-
-        // For others, returns toString().
-        return address.toString();
-    }
-
-    private static String convert(String address) {
-        return "XX:XX:XX:XX:" + address.substring(12);
-    }
-
-    private BluetoothAddress() {}
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairer.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairer.java
deleted file mode 100644
index 07306c1..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairer.java
+++ /dev/null
@@ -1,774 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static android.bluetooth.BluetoothDevice.BOND_BONDED;
-import static android.bluetooth.BluetoothDevice.BOND_BONDING;
-import static android.bluetooth.BluetoothDevice.BOND_NONE;
-import static android.bluetooth.BluetoothDevice.ERROR;
-import static android.bluetooth.BluetoothProfile.A2DP;
-import static android.bluetooth.BluetoothProfile.HEADSET;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
-
-import static java.util.concurrent.Executors.newSingleThreadExecutor;
-
-import android.Manifest.permission;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Parcelable;
-import android.os.SystemClock;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-import androidx.core.content.ContextCompat;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.PasskeyCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.Profile;
-import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver;
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.ConnectErrorCode;
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.CreateBondErrorCode;
-import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
-
-import com.google.common.base.Preconditions;
-import com.google.common.util.concurrent.SettableFuture;
-
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import java.util.UUID;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Pairs to Bluetooth audio devices.
- */
-public class BluetoothAudioPairer {
-
-    private static final String TAG = BluetoothAudioPairer.class.getSimpleName();
-
-    /**
-     * Hidden, see {@link BluetoothDevice}.
-     */
-    // TODO(b/202549655): remove Hidden usage.
-    private static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON";
-
-    /**
-     * Hidden, see {@link BluetoothDevice}.
-     */
-    // TODO(b/202549655): remove Hidden usage.
-    private static final int PAIRING_VARIANT_CONSENT = 3;
-
-    /**
-     * Hidden, see {@link BluetoothDevice}.
-     */
-    // TODO(b/202549655): remove Hidden usage.
-    public static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4;
-
-    private static final int DISCOVERY_STATE_CHANGE_TIMEOUT_MS = 3000;
-
-    private final Context mContext;
-    private final Preferences mPreferences;
-    private final EventLoggerWrapper mEventLogger;
-    private final BluetoothDevice mDevice;
-    @Nullable
-    private final KeyBasedPairingInfo mKeyBasedPairingInfo;
-    @Nullable
-    private final PasskeyConfirmationHandler mPasskeyConfirmationHandler;
-    private final TimingLogger mTimingLogger;
-
-    private static boolean sTestMode = false;
-
-    static void enableTestMode() {
-        sTestMode = true;
-    }
-
-    static class KeyBasedPairingInfo {
-
-        private final byte[] mSecret;
-        private final GattConnectionManager mGattConnectionManager;
-        private final boolean mProviderInitiatesBonding;
-
-        /**
-         * @param secret The secret negotiated during the initial BLE handshake for Key-based
-         * Pairing. See {@link FastPairConnection#handshake}.
-         * @param gattConnectionManager A manager that knows how to get and create Gatt connections
-         * to the remote device.
-         */
-        KeyBasedPairingInfo(
-                byte[] secret,
-                GattConnectionManager gattConnectionManager,
-                boolean providerInitiatesBonding) {
-            this.mSecret = secret;
-            this.mGattConnectionManager = gattConnectionManager;
-            this.mProviderInitiatesBonding = providerInitiatesBonding;
-        }
-    }
-
-    public BluetoothAudioPairer(
-            Context context,
-            BluetoothDevice device,
-            Preferences preferences,
-            EventLoggerWrapper eventLogger,
-            @Nullable KeyBasedPairingInfo keyBasedPairingInfo,
-            @Nullable PasskeyConfirmationHandler passkeyConfirmationHandler,
-            TimingLogger timingLogger)
-            throws PairingException {
-        this.mContext = context;
-        this.mDevice = device;
-        this.mPreferences = preferences;
-        this.mEventLogger = eventLogger;
-        this.mKeyBasedPairingInfo = keyBasedPairingInfo;
-        this.mPasskeyConfirmationHandler = passkeyConfirmationHandler;
-        this.mTimingLogger = timingLogger;
-
-        // TODO(b/203455314): follow up with the following comments.
-        // The OS should give the user some UI to choose if they want to allow access, but there
-        // seems to be a bug where if we don't reject access, it's auto-granted in some cases
-        // (Plantronics headset gets contacts access when pairing with my Taimen via Bluetooth
-        // Settings, without me seeing any UI about it). b/64066631
-        //
-        // If that OS bug doesn't get fixed, we can flip these flags to force-reject the
-        // permissions.
-        if (preferences.getRejectPhonebookAccess() && (sTestMode ? false :
-                !device.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED))) {
-            throw new PairingException("Failed to deny contacts (phonebook) access.");
-        }
-        if (preferences.getRejectMessageAccess()
-                && (sTestMode ? false :
-                !device.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED))) {
-            throw new PairingException("Failed to deny message access.");
-        }
-        if (preferences.getRejectSimAccess()
-                && (sTestMode ? false :
-                !device.setSimAccessPermission(BluetoothDevice.ACCESS_REJECTED))) {
-            throw new PairingException("Failed to deny SIM access.");
-        }
-    }
-
-    boolean isPaired() {
-        return (sTestMode ? false : mDevice.getBondState() == BOND_BONDED);
-    }
-
-    /**
-     * Unpairs from the device. Throws an exception if any error occurs.
-     */
-    @WorkerThread
-    void unpair()
-            throws InterruptedException, ExecutionException, TimeoutException, PairingException {
-        int bondState =  sTestMode ? BOND_NONE : mDevice.getBondState();
-        try (UnbondedReceiver unbondedReceiver = new UnbondedReceiver();
-                ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
-                        "Unpair for state: " + bondState)) {
-            // We'll only get a state change broadcast if we're actually unbonding (method returns
-            // true).
-            if (bondState == BluetoothDevice.BOND_BONDED) {
-                mEventLogger.setCurrentEvent(EventCode.REMOVE_BOND);
-                Log.i(TAG,  "removeBond with " + maskBluetoothAddress(mDevice));
-                mDevice.removeBond();
-                unbondedReceiver.await(
-                        mPreferences.getRemoveBondTimeoutSeconds(), TimeUnit.SECONDS);
-            } else if (bondState == BluetoothDevice.BOND_BONDING) {
-                mEventLogger.setCurrentEvent(EventCode.CANCEL_BOND);
-                Log.i(TAG,  "cancelBondProcess with " + maskBluetoothAddress(mDevice));
-                mDevice.cancelBondProcess();
-                unbondedReceiver.await(
-                        mPreferences.getRemoveBondTimeoutSeconds(), TimeUnit.SECONDS);
-            } else {
-                // The OS may have beaten us in a race, unbonding before we called the method. So if
-                // we're (somehow) in the desired state then we're happy, if not then bail.
-                if (bondState != BluetoothDevice.BOND_NONE) {
-                    throw new PairingException("returned false, state=%s", bondState);
-                }
-            }
-        }
-
-        // This seems to improve the probability that createBond will succeed after removeBond.
-        SystemClock.sleep(mPreferences.getRemoveBondSleepMillis());
-        mEventLogger.logCurrentEventSucceeded();
-    }
-
-    /**
-     * Pairs with the device. Throws an exception if any error occurs.
-     */
-    @WorkerThread
-    void pair()
-            throws InterruptedException, ExecutionException, TimeoutException, PairingException {
-        // Unpair first, because if we have a bond, but the other device has forgotten its bond,
-        // it can send us a pairing request that we're not ready for (which can pop up a dialog).
-        // Or, if we're in the middle of a (too-long) bonding attempt, we want to cancel.
-        unpair();
-
-        mEventLogger.setCurrentEvent(EventCode.CREATE_BOND);
-        try (BondedReceiver bondedReceiver = new BondedReceiver();
-                ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Create bond")) {
-            // If the provider's initiating the bond, we do nothing but wait for broadcasts.
-            if (mKeyBasedPairingInfo == null || !mKeyBasedPairingInfo.mProviderInitiatesBonding) {
-                if (!sTestMode) {
-                    Log.i(TAG, "createBond with " + maskBluetoothAddress(mDevice) + ", type="
-                        + mDevice.getType());
-                    if (mPreferences.getSpecifyCreateBondTransportType()) {
-                        mDevice.createBond(mPreferences.getCreateBondTransportType());
-                    } else {
-                        mDevice.createBond();
-                    }
-                }
-            }
-            try {
-                bondedReceiver.await(mPreferences.getCreateBondTimeoutSeconds(), TimeUnit.SECONDS);
-            } catch (TimeoutException e) {
-                Log.w(TAG, "bondedReceiver time out after " + mPreferences
-                        .getCreateBondTimeoutSeconds() + " seconds");
-                if (mPreferences.getIgnoreUuidTimeoutAfterBonded() && isPaired()) {
-                    Log.w(TAG, "Created bond but never received UUIDs, attempting to continue.");
-                } else {
-                    // Rethrow e to cause the pairing to fail and be retried if necessary.
-                    throw e;
-                }
-            }
-        }
-        mEventLogger.logCurrentEventSucceeded();
-    }
-
-    /**
-     * Connects to the given profile. Throws an exception if any error occurs.
-     *
-     * <p>If remote device clears the link key, the BOND_BONDED state would transit to BOND_BONDING
-     * (and go through the pairing process again) when directly connecting the profile. By enabling
-     * enablePairingBehavior, we provide both pairing and connecting behaviors at the same time. See
-     * b/145699390 for more details.
-     */
-    // Suppression for possible null from ImmutableMap#get. See go/lsc-get-nullable
-    @SuppressWarnings("nullness:argument")
-    @WorkerThread
-    public void connect(short profileUuid, boolean enablePairingBehavior)
-            throws InterruptedException, ReflectionException, TimeoutException, ExecutionException,
-            ConnectException {
-        if (!mPreferences.isSupportedProfile(profileUuid)) {
-            throw new ConnectException(
-                    ConnectErrorCode.UNSUPPORTED_PROFILE, "Unsupported profile=%s", profileUuid);
-        }
-        Profile profile = Constants.PROFILES.get(profileUuid);
-        Log.i(TAG,
-                "Connecting to profile=" + profile + " on device=" + maskBluetoothAddress(mDevice));
-        try (BondedReceiver bondedReceiver = enablePairingBehavior ? new BondedReceiver() : null;
-                ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
-                        "Connect: " + profile)) {
-            connectByProfileProxy(profile);
-        }
-    }
-
-    private void connectByProfileProxy(Profile profile)
-            throws ReflectionException, InterruptedException, ExecutionException, TimeoutException,
-            ConnectException {
-        try (BluetoothProfileWrapper autoClosingProxy = new BluetoothProfileWrapper(profile);
-                ConnectedReceiver connectedReceiver = new ConnectedReceiver(profile)) {
-            BluetoothProfile proxy = autoClosingProxy.mProxy;
-
-            // Try to connect via reflection
-            Log.v(TAG, "Connect to proxy=" + proxy);
-
-            if (!sTestMode) {
-                if (!(Boolean) Reflect.on(proxy).withMethod("connect", BluetoothDevice.class)
-                        .get(mDevice)) {
-                    // If we're already connecting, connect() may return false. :/
-                    Log.w(TAG, "connect returned false, expected if connecting, state="
-                            + proxy.getConnectionState(mDevice));
-                }
-            }
-
-            // If we're already connected, the OS may not send the connection state broadcast, so
-            // return immediately for that case.
-            if (!sTestMode) {
-                if (proxy.getConnectionState(mDevice) == BluetoothProfile.STATE_CONNECTED) {
-                    Log.v(TAG, "connectByProfileProxy: already connected to device="
-                            + maskBluetoothAddress(mDevice));
-                    return;
-                }
-            }
-
-            try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Wait connection")) {
-                // Wait for connecting to succeed or fail (via event or timeout).
-                connectedReceiver
-                        .await(mPreferences.getCreateBondTimeoutSeconds(), TimeUnit.SECONDS);
-            }
-        }
-    }
-
-    private class BluetoothProfileWrapper implements AutoCloseable {
-
-        // incompatible types in assignment.
-        @SuppressWarnings("nullness:assignment")
-        private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
-
-        private final Profile mProfile;
-        private final BluetoothProfile mProxy;
-
-        /**
-         * Blocks until we get the proxy. Throws on error.
-         */
-        private BluetoothProfileWrapper(Profile profile)
-                throws InterruptedException, ExecutionException, TimeoutException,
-                ConnectException {
-            this.mProfile = profile;
-            mProxy = getProfileProxy(profile);
-        }
-
-        @Override
-        public void close() {
-            try (ScopedTiming scopedTiming =
-                    new ScopedTiming(mTimingLogger, "Close profile: " + mProfile)) {
-                if (!sTestMode) {
-                    mBluetoothAdapter.closeProfileProxy(mProfile.type, mProxy);
-                }
-            }
-        }
-
-        private BluetoothProfile getProfileProxy(BluetoothProfileWrapper this, Profile profile)
-                throws InterruptedException, ExecutionException, TimeoutException,
-                ConnectException {
-            if (profile.type != A2DP && profile.type != HEADSET) {
-                throw new IllegalArgumentException("Unsupported profile type=" + profile.type);
-            }
-
-            SettableFuture<BluetoothProfile> proxyFuture = SettableFuture.create();
-            BluetoothProfile.ServiceListener listener =
-                    new BluetoothProfile.ServiceListener() {
-                        @UiThread
-                        @Override
-                        public void onServiceConnected(int profileType, BluetoothProfile proxy) {
-                            proxyFuture.set(proxy);
-                        }
-
-                        @Override
-                        public void onServiceDisconnected(int profileType) {
-                            Log.v(TAG, "proxy disconnected for profile=" + profile);
-                        }
-                    };
-
-            if (!mBluetoothAdapter.getProfileProxy(mContext, listener, profile.type)) {
-                throw new ConnectException(
-                        ConnectErrorCode.GET_PROFILE_PROXY_FAILED,
-                        "getProfileProxy failed immediately");
-            }
-
-            return proxyFuture.get(mPreferences.getProxyTimeoutSeconds(), TimeUnit.SECONDS);
-        }
-    }
-
-    private class UnbondedReceiver extends DeviceIntentReceiver {
-
-        private UnbondedReceiver() {
-            super(mContext, mPreferences, mDevice, BluetoothDevice.ACTION_BOND_STATE_CHANGED);
-        }
-
-        @Override
-        protected void onReceiveDeviceIntent(Intent intent) throws Exception {
-            if (mDevice.getBondState() == BOND_NONE) {
-                try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
-                        "Close UnbondedReceiver")) {
-                    close();
-                }
-            }
-        }
-    }
-
-    /**
-     * Receiver that closes after bonding has completed.
-     */
-    class BondedReceiver extends DeviceIntentReceiver {
-
-        private boolean mReceivedUuids = false;
-        private boolean mReceivedPasskey = false;
-
-        private BondedReceiver() {
-            super(
-                    mContext,
-                    mPreferences,
-                    mDevice,
-                    BluetoothDevice.ACTION_PAIRING_REQUEST,
-                    BluetoothDevice.ACTION_BOND_STATE_CHANGED,
-                    BluetoothDevice.ACTION_UUID);
-        }
-
-        // switching on a possibly-null value (intent.getAction())
-        // incompatible types in argument.
-        @SuppressWarnings({"nullness:switching.nullable", "nullness:argument"})
-        @Override
-        protected void onReceiveDeviceIntent(Intent intent)
-                throws PairingException, InterruptedException, ExecutionException, TimeoutException,
-                BluetoothException, GeneralSecurityException {
-            switch (intent.getAction()) {
-                case BluetoothDevice.ACTION_PAIRING_REQUEST:
-                    int variant = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, ERROR);
-                    int passkey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, ERROR);
-                    handlePairingRequest(variant, passkey);
-                    break;
-                case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
-                    // Use the state in the intent, not device.getBondState(), to avoid a race where
-                    // we log the wrong failure reason during a rapid transition.
-                    int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, ERROR);
-                    int reason = intent.getIntExtra(EXTRA_REASON, ERROR);
-                    handleBondStateChanged(bondState, reason);
-                    break;
-                case BluetoothDevice.ACTION_UUID:
-                    // According to eisenbach@ and pavlin@, there's always a UUID broadcast when
-                    // pairing (it can happen either before or after the transition to BONDED).
-                    if (mPreferences.getWaitForUuidsAfterBonding()) {
-                        Parcelable[] uuids = intent
-                                .getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
-                        handleUuids(uuids);
-                    }
-                    break;
-                default:
-                    break;
-            }
-        }
-
-        private void handlePairingRequest(int variant, int passkey) {
-            Log.i(TAG, "Pairing request, variant=" + variant + ", passkey=" + (passkey == ERROR
-                    ? "(none)" : String.valueOf(passkey)));
-            if (mPreferences.getMoreEventLogForQuality()) {
-                mEventLogger.setCurrentEvent(EventCode.HANDLE_PAIRING_REQUEST);
-            }
-
-            if (mPreferences.getSupportHidDevice() && variant == PAIRING_VARIANT_DISPLAY_PASSKEY) {
-                mReceivedPasskey = true;
-                extendAwaitSecond(
-                        mPreferences.getHidCreateBondTimeoutSeconds()
-                                - mPreferences.getCreateBondTimeoutSeconds());
-                triggerDiscoverStateChange();
-                if (mPreferences.getMoreEventLogForQuality()) {
-                    mEventLogger.logCurrentEventSucceeded();
-                }
-                return;
-
-            } else {
-                // Prevent Bluetooth Settings from getting the pairing request and showing its own
-                // UI.
-                abortBroadcast();
-
-                if (variant == PAIRING_VARIANT_CONSENT
-                        && mKeyBasedPairingInfo == null // Fast Pair 1.0 device
-                        && mPreferences.getAcceptConsentForFastPairOne()) {
-                    // Previously, if Bluetooth decided to use the Just Works variant (e.g. Fast
-                    // Pair 1.0), we don't get a pairing request broadcast at all.
-                    // However, after CVE-2019-2225, Bluetooth will decide to ask consent from
-                    // users. Details:
-                    // https://source.android.com/security/bulletin/2019-12-01#system
-                    // Since we've certified the Fast Pair 1.0 devices, and user taps to pair it
-                    // (with the device's image), we could help user to accept the consent.
-                    if (!sTestMode) {
-                        mDevice.setPairingConfirmation(true);
-                    }
-                    if (mPreferences.getMoreEventLogForQuality()) {
-                        mEventLogger.logCurrentEventSucceeded();
-                    }
-                    return;
-                } else if (variant != BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION) {
-                    if (!sTestMode) {
-                        mDevice.setPairingConfirmation(false);
-                    }
-                    if (mPreferences.getMoreEventLogForQuality()) {
-                        mEventLogger.logCurrentEventFailed(
-                                new CreateBondException(
-                                        CreateBondErrorCode.INCORRECT_VARIANT, 0,
-                                        "Incorrect variant for FastPair"));
-                    }
-                    return;
-                }
-                mReceivedPasskey = true;
-
-                if (mKeyBasedPairingInfo == null) {
-                    if (mPreferences.getAcceptPasskey()) {
-                        // Must be the simulator using FP 1.0 (no Key-based Pairing). Real
-                        // headphones using FP 1.0 use Just Works instead (and maybe we should
-                        // disable this flag for them).
-                        if (!sTestMode) {
-                            mDevice.setPairingConfirmation(true);
-                        }
-                    }
-                    if (mPreferences.getMoreEventLogForQuality()) {
-                        if (!sTestMode) {
-                            mEventLogger.logCurrentEventSucceeded();
-                        }
-                    }
-                    return;
-                }
-            }
-
-            if (mPreferences.getMoreEventLogForQuality()) {
-                mEventLogger.logCurrentEventSucceeded();
-            }
-
-            newSingleThreadExecutor()
-                    .execute(
-                            () -> {
-                                try (ScopedTiming scopedTiming1 =
-                                        new ScopedTiming(mTimingLogger, "Exchange passkey")) {
-                                    mEventLogger.setCurrentEvent(EventCode.PASSKEY_EXCHANGE);
-
-                                    // We already check above, but the static analyzer's not
-                                    // convinced without this.
-                                    Preconditions.checkNotNull(mKeyBasedPairingInfo);
-                                    BluetoothGattConnection connection =
-                                            mKeyBasedPairingInfo.mGattConnectionManager
-                                                    .getConnection();
-                                    UUID characteristicUuid =
-                                            PasskeyCharacteristic.getId(connection);
-                                    ChangeObserver remotePasskeyObserver =
-                                            connection.enableNotification(FastPairService.ID,
-                                                    characteristicUuid);
-                                    Log.i(TAG, "Sending local passkey.");
-                                    byte[] encryptedData;
-                                    try (ScopedTiming scopedTiming2 =
-                                            new ScopedTiming(mTimingLogger, "Encrypt passkey")) {
-                                        encryptedData =
-                                                PasskeyCharacteristic.encrypt(
-                                                        PasskeyCharacteristic.Type.SEEKER,
-                                                        mKeyBasedPairingInfo.mSecret, passkey);
-                                    }
-                                    try (ScopedTiming scopedTiming3 =
-                                            new ScopedTiming(mTimingLogger,
-                                                    "Send passkey to remote")) {
-                                        connection.writeCharacteristic(
-                                                FastPairService.ID, characteristicUuid,
-                                                encryptedData);
-                                    }
-                                    Log.i(TAG, "Waiting for remote passkey.");
-                                    byte[] encryptedRemotePasskey;
-                                    try (ScopedTiming scopedTiming4 =
-                                            new ScopedTiming(mTimingLogger,
-                                                    "Wait for remote passkey")) {
-                                        encryptedRemotePasskey =
-                                                remotePasskeyObserver.waitForUpdate(
-                                                        TimeUnit.SECONDS.toMillis(mPreferences
-                                                                .getGattOperationTimeoutSeconds()));
-                                    }
-                                    int remotePasskey;
-                                    try (ScopedTiming scopedTiming5 =
-                                            new ScopedTiming(mTimingLogger, "Decrypt passkey")) {
-                                        remotePasskey =
-                                                PasskeyCharacteristic.decrypt(
-                                                        PasskeyCharacteristic.Type.PROVIDER,
-                                                        mKeyBasedPairingInfo.mSecret,
-                                                        encryptedRemotePasskey);
-                                    }
-
-                                    // We log success if we made it through with no exceptions.
-                                    // If the passkey was wrong, pairing will fail and we'll log
-                                    // BOND_BROKEN with reason = AUTH_FAILED.
-                                    mEventLogger.logCurrentEventSucceeded();
-
-                                    boolean isPasskeyCorrect = passkey == remotePasskey;
-                                    if (isPasskeyCorrect) {
-                                        Log.i(TAG, "Passkey correct.");
-                                    } else {
-                                        Log.e(TAG, "Passkey incorrect, local= " + passkey
-                                                + ", remote=" + remotePasskey);
-                                    }
-
-                                    // Don't estimate the {@code ScopedTiming} because the
-                                    // passkey confirmation is done by UI.
-                                    if (isPasskeyCorrect
-                                            && mPreferences.getHandlePasskeyConfirmationByUi()
-                                            && mPasskeyConfirmationHandler != null) {
-                                        Log.i(TAG, "Callback the passkey to UI for confirmation.");
-                                        mPasskeyConfirmationHandler
-                                                .onPasskeyConfirmation(mDevice, passkey);
-                                    } else {
-                                        try (ScopedTiming scopedTiming6 =
-                                                new ScopedTiming(
-                                                        mTimingLogger, "Confirm the pairing: "
-                                                        + isPasskeyCorrect)) {
-                                            mDevice.setPairingConfirmation(isPasskeyCorrect);
-                                        }
-                                    }
-                                } catch (BluetoothException
-                                        | GeneralSecurityException
-                                        | InterruptedException
-                                        | ExecutionException
-                                        | TimeoutException e) {
-                                    mEventLogger.logCurrentEventFailed(e);
-                                    closeWithError(e);
-                                }
-                            });
-        }
-
-        /**
-         * Workaround to let Settings popup a pairing dialog instead of notification. When pairing
-         * request intent passed to Settings, it'll check several conditions to decide that it
-         * should show a dialog or a notification. One of those conditions is to check if the device
-         * is in discovery mode recently, which can be fulfilled by calling {@link
-         * BluetoothAdapter#startDiscovery()}. This method aims to fulfill the condition, and block
-         * the pairing broadcast for at most
-         * {@link BluetoothAudioPairer#DISCOVERY_STATE_CHANGE_TIMEOUT_MS}
-         * to make sure that we fulfill the condition first and successful.
-         */
-        // dereference of possibly-null reference bluetoothAdapter
-        @SuppressWarnings("nullness:dereference.of.nullable")
-        private void triggerDiscoverStateChange() {
-            BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
-
-            if (bluetoothAdapter.isDiscovering()) {
-                return;
-            }
-
-            HandlerThread backgroundThread = new HandlerThread("TriggerDiscoverStateChangeThread");
-            backgroundThread.start();
-
-            AtomicBoolean result = new AtomicBoolean(false);
-            SimpleBroadcastReceiver receiver =
-                    new SimpleBroadcastReceiver(
-                            mContext,
-                            mPreferences,
-                            new Handler(backgroundThread.getLooper()),
-                            BluetoothAdapter.ACTION_DISCOVERY_STARTED,
-                            BluetoothAdapter.ACTION_DISCOVERY_FINISHED) {
-
-                        @Override
-                        protected void onReceive(Intent intent) throws Exception {
-                            result.set(true);
-                            close();
-                        }
-                    };
-
-            Log.i(TAG, "triggerDiscoverStateChange call startDiscovery.");
-            // Uses startDiscovery to trigger Settings show pairing dialog instead of notification.
-            if (!sTestMode) {
-                bluetoothAdapter.startDiscovery();
-                bluetoothAdapter.cancelDiscovery();
-            }
-            try {
-                receiver.await(DISCOVERY_STATE_CHANGE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-            } catch (InterruptedException | ExecutionException | TimeoutException e) {
-                Log.w(TAG, "triggerDiscoverStateChange failed!");
-            }
-
-            backgroundThread.quitSafely();
-            try {
-                backgroundThread.join();
-            } catch (InterruptedException e) {
-                Log.i(TAG, "triggerDiscoverStateChange backgroundThread.join meet exception!", e);
-            }
-
-            if (result.get()) {
-                Log.i(TAG, "triggerDiscoverStateChange successful.");
-            }
-        }
-
-        private void handleBondStateChanged(int bondState, int reason)
-                throws PairingException, InterruptedException, ExecutionException,
-                TimeoutException {
-            Log.i(TAG, "Bond state changed to " + bondState + ", reason=" + reason);
-            switch (bondState) {
-                case BOND_BONDED:
-                    if (mKeyBasedPairingInfo != null && !mReceivedPasskey) {
-                        // The device bonded with Just Works, although we did the Key-based Pairing
-                        // GATT handshake and agreed on a pairing secret. It might be a Person In
-                        // The Middle Attack!
-                        try (ScopedTiming scopedTiming =
-                                new ScopedTiming(mTimingLogger,
-                                        "Close BondedReceiver: POSSIBLE_MITM")) {
-                            closeWithError(
-                                    new CreateBondException(
-                                            CreateBondErrorCode.POSSIBLE_MITM,
-                                            reason,
-                                            "Unexpectedly bonded without a passkey. It might be a "
-                                                    + "Person In The Middle Attack! Unbonding!"));
-                        }
-                        unpair();
-                    } else if (!mPreferences.getWaitForUuidsAfterBonding()
-                            || (mPreferences.getReceiveUuidsAndBondedEventBeforeClose()
-                            && mReceivedUuids)) {
-                        try (ScopedTiming scopedTiming =
-                                new ScopedTiming(mTimingLogger, "Close BondedReceiver")) {
-                            close();
-                        }
-                    }
-                    break;
-                case BOND_NONE:
-                    throw new CreateBondException(
-                            CreateBondErrorCode.BOND_BROKEN, reason, "Bond broken, reason=%d",
-                            reason);
-                case BOND_BONDING:
-                default:
-                    break;
-            }
-        }
-
-        private void handleUuids(Parcelable[] uuids) {
-            Log.i(TAG, "Got UUIDs for " + maskBluetoothAddress(mDevice) + ": "
-                    + Arrays.toString(uuids));
-            mReceivedUuids = true;
-            if (!mPreferences.getReceiveUuidsAndBondedEventBeforeClose() || isPaired()) {
-                try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
-                        "Close BondedReceiver")) {
-                    close();
-                }
-            }
-        }
-    }
-
-    private class ConnectedReceiver extends DeviceIntentReceiver {
-
-        private ConnectedReceiver(Profile profile) throws ConnectException {
-            super(mContext, mPreferences, mDevice, profile.connectionStateAction);
-        }
-
-        @Override
-        public void onReceiveDeviceIntent(Intent intent) throws PairingException {
-            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, ERROR);
-            Log.i(TAG, "Connection state changed to " + state);
-            switch (state) {
-                case BluetoothAdapter.STATE_CONNECTED:
-                    try (ScopedTiming scopedTiming =
-                            new ScopedTiming(mTimingLogger, "Close ConnectedReceiver")) {
-                        close();
-                    }
-                    break;
-                case BluetoothAdapter.STATE_DISCONNECTED:
-                    throw new ConnectException(ConnectErrorCode.DISCONNECTED, "Disconnected");
-                case BluetoothAdapter.STATE_CONNECTING:
-                case BluetoothAdapter.STATE_DISCONNECTING:
-                default:
-                    break;
-            }
-        }
-    }
-
-    private boolean hasPermission(String permission) {
-        return ContextCompat.checkSelfPermission(mContext, permission) == PERMISSION_GRANTED;
-    }
-
-    public BluetoothDevice getDevice() {
-        return mDevice;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairer.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairer.java
deleted file mode 100644
index 6c467d3..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairer.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static android.bluetooth.BluetoothDevice.BOND_BONDED;
-import static android.bluetooth.BluetoothDevice.BOND_BONDING;
-import static android.bluetooth.BluetoothDevice.BOND_NONE;
-import static android.bluetooth.BluetoothDevice.ERROR;
-import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
-
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import android.annotation.SuppressLint;
-import android.bluetooth.BluetoothDevice;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-import androidx.annotation.WorkerThread;
-
-import com.google.common.base.Strings;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Pairs to Bluetooth classic devices with passkey confirmation.
- */
-// TODO(b/202524672): Add class unit test.
-public class BluetoothClassicPairer {
-
-    private static final String TAG = BluetoothClassicPairer.class.getSimpleName();
-    /**
-     * Hidden, see {@link BluetoothDevice}.
-     */
-    private static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON";
-
-    private final Context mContext;
-    private final BluetoothDevice mDevice;
-    private final Preferences mPreferences;
-    private final PasskeyConfirmationHandler mPasskeyConfirmationHandler;
-
-    public BluetoothClassicPairer(
-            Context context,
-            BluetoothDevice device,
-            Preferences preferences,
-            PasskeyConfirmationHandler passkeyConfirmationHandler) {
-        this.mContext = context;
-        this.mDevice = device;
-        this.mPreferences = preferences;
-        this.mPasskeyConfirmationHandler = passkeyConfirmationHandler;
-    }
-
-    /**
-     * Pairs with the device. Throws a {@link PairingException} if any error occurs.
-     */
-    @WorkerThread
-    public void pair() throws PairingException {
-        Log.i(TAG, "BluetoothClassicPairer, createBond with " + maskBluetoothAddress(mDevice)
-                + ", type=" + mDevice.getType());
-        try (BondedReceiver bondedReceiver = new BondedReceiver()) {
-            if (mDevice.createBond()) {
-                bondedReceiver.await(mPreferences.getCreateBondTimeoutSeconds(), SECONDS);
-            } else {
-                throw new PairingException(
-                        "BluetoothClassicPairer, createBond got immediate error");
-            }
-        } catch (TimeoutException | InterruptedException | ExecutionException e) {
-            throw new PairingException("BluetoothClassicPairer, createBond failed", e);
-        }
-    }
-
-    protected boolean isPaired() {
-        return mDevice.getBondState() == BOND_BONDED;
-    }
-
-    /**
-     * Receiver that closes after bonding has completed.
-     */
-    private class BondedReceiver extends DeviceIntentReceiver {
-
-        private BondedReceiver() {
-            super(
-                    mContext,
-                    mPreferences,
-                    mDevice,
-                    BluetoothDevice.ACTION_PAIRING_REQUEST,
-                    BluetoothDevice.ACTION_BOND_STATE_CHANGED);
-        }
-
-        /**
-         * Called with ACTION_PAIRING_REQUEST and ACTION_BOND_STATE_CHANGED about the interesting
-         * device (see {@link DeviceIntentReceiver}).
-         *
-         * <p>The ACTION_PAIRING_REQUEST intent provides the passkey which will be sent to the
-         * {@link PasskeyConfirmationHandler} for showing the UI, and the ACTION_BOND_STATE_CHANGED
-         * will provide the result of the bonding.
-         */
-        @Override
-        protected void onReceiveDeviceIntent(Intent intent) {
-            String intentAction = intent.getAction();
-            BluetoothDevice remoteDevice = intent.getParcelableExtra(EXTRA_DEVICE);
-            if (Strings.isNullOrEmpty(intentAction)
-                    || remoteDevice == null
-                    || !remoteDevice.getAddress().equals(mDevice.getAddress())) {
-                Log.w(TAG,
-                        "BluetoothClassicPairer, receives " + intentAction
-                                + " from unexpected device " + maskBluetoothAddress(remoteDevice));
-                return;
-            }
-            switch (intentAction) {
-                case BluetoothDevice.ACTION_PAIRING_REQUEST:
-                    handlePairingRequest(
-                            remoteDevice,
-                            intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, ERROR),
-                            intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, ERROR));
-                    break;
-                case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
-                    handleBondStateChanged(
-                            intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, ERROR),
-                            intent.getIntExtra(EXTRA_REASON, ERROR));
-                    break;
-                default:
-                    break;
-            }
-        }
-
-        private void handlePairingRequest(BluetoothDevice device, int variant, int passkey) {
-            Log.i(TAG,
-                    "BluetoothClassicPairer, pairing request, " + device + ", " + variant + ", "
-                            + passkey);
-            // Prevent Bluetooth Settings from getting the pairing request and showing its own UI.
-            abortBroadcast();
-            mPasskeyConfirmationHandler.onPasskeyConfirmation(device, passkey);
-        }
-
-        private void handleBondStateChanged(int bondState, int reason) {
-            Log.i(TAG,
-                    "BluetoothClassicPairer, bond state changed to " + bondState + ", reason="
-                            + reason);
-            switch (bondState) {
-                case BOND_BONDING:
-                    // Don't close!
-                    return;
-                case BOND_BONDED:
-                    close();
-                    return;
-                case BOND_NONE:
-                default:
-                    closeWithError(
-                            new PairingException(
-                                    "BluetoothClassicPairer, createBond failed, reason:" + reason));
-            }
-        }
-    }
-
-    // Applies UsesPermission annotation will create circular dependency.
-    @SuppressLint("MissingPermission")
-    static void setPairingConfirmation(BluetoothDevice device, boolean confirm) {
-        Log.i(TAG, "BluetoothClassicPairer: setPairingConfirmation " + maskBluetoothAddress(device)
-                + ", confirm: " + confirm);
-        device.setPairingConfirmation(confirm);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuids.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuids.java
deleted file mode 100644
index c5475a6..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuids.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import java.util.UUID;
-
-/**
- * Utilities for dealing with UUIDs assigned by the Bluetooth SIG. Has a lot in common with
- * com.android.BluetoothUuid, but that class is hidden.
- */
-public class BluetoothUuids {
-
-    /**
-     * The Base UUID is used for calculating 128-bit UUIDs from "short UUIDs" (16- and 32-bit).
-     *
-     * @see {https://www.bluetooth.com/specifications/assigned-numbers/service-discovery}
-     */
-    private static final UUID BASE_UUID = UUID.fromString("00000000-0000-1000-8000-00805F9B34FB");
-
-    /**
-     * Fast Pair custom GATT characteristics 128-bit UUIDs base.
-     *
-     * <p>Notes: The 16-bit value locates at the 3rd and 4th bytes.
-     *
-     * @see {go/fastpair-128bit-gatt}
-     */
-    private static final UUID FAST_PAIR_BASE_UUID =
-            UUID.fromString("FE2C0000-8366-4814-8EB0-01DE32100BEA");
-
-    private static final int BIT_INDEX_OF_16_BIT_UUID = 32;
-
-    private BluetoothUuids() {}
-
-    /**
-     * Returns the 16-bit version of the UUID. If this is not a 16-bit UUID, throws
-     * IllegalArgumentException.
-     */
-    public static short get16BitUuid(UUID uuid) {
-        if (!is16BitUuid(uuid)) {
-            throw new IllegalArgumentException("Not a 16-bit Bluetooth UUID: " + uuid);
-        }
-        return (short) (uuid.getMostSignificantBits() >> BIT_INDEX_OF_16_BIT_UUID);
-    }
-
-    /** Checks whether the UUID is 16 bit */
-    public static boolean is16BitUuid(UUID uuid) {
-        // See Service Discovery Protocol in the Bluetooth Core Specification. Bits at index 32-48
-        // are the 16-bit UUID, and the rest must match the Base UUID.
-        return uuid.getLeastSignificantBits() == BASE_UUID.getLeastSignificantBits()
-                && (uuid.getMostSignificantBits() & 0xFFFF0000FFFFFFFFL)
-                == BASE_UUID.getMostSignificantBits();
-    }
-
-    /** Converts short UUID to 128 bit UUID */
-    public static UUID to128BitUuid(short shortUuid) {
-        return new UUID(
-                ((shortUuid & 0xFFFFL) << BIT_INDEX_OF_16_BIT_UUID)
-                        | BASE_UUID.getMostSignificantBits(), BASE_UUID.getLeastSignificantBits());
-    }
-
-    /** Transfers the 16-bit Fast Pair custom GATT characteristics to 128-bit. */
-    public static UUID toFastPair128BitUuid(short shortUuid) {
-        return new UUID(
-                ((shortUuid & 0xFFFFL) << BIT_INDEX_OF_16_BIT_UUID)
-                        | FAST_PAIR_BASE_UUID.getMostSignificantBits(),
-                FAST_PAIR_BASE_UUID.getLeastSignificantBits());
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BroadcastConstants.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BroadcastConstants.java
deleted file mode 100644
index c26c6ad..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BroadcastConstants.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-/**
- * Constants to share with the cloud syncing process.
- */
-public class BroadcastConstants {
-
-    // TODO: Set right value for AOSP.
-    /** Package name of the cloud syncing logic. */
-    public static final String PACKAGE_NAME = "PACKAGE_NAME";
-    /** Service name of the cloud syncing instance. */
-    public static final String SERVICE_NAME = PACKAGE_NAME + ".SERVICE_NAME";
-    private static final String PREFIX = PACKAGE_NAME + ".PREFIX_NAME.";
-
-    /** Action when a fast pair device is added. */
-    public static final String ACTION_FAST_PAIR_DEVICE_ADDED =
-            PREFIX + "ACTION_FAST_PAIR_DEVICE_ADDED";
-    /**
-     * The BLE address of a device. BLE is used here instead of public because the caller of the
-     * library never knows what the device's public address is.
-     */
-    public static final String EXTRA_ADDRESS = PREFIX + "BLE_ADDRESS";
-    /** The public address of a device. */
-    public static final String EXTRA_PUBLIC_ADDRESS = PREFIX + "PUBLIC_ADDRESS";
-    /** Account key. */
-    public static final String EXTRA_ACCOUNT_KEY = PREFIX + "ACCOUNT_KEY";
-    /** Whether a paring is retroactive. */
-    public static final String EXTRA_RETROACTIVE_PAIR = PREFIX + "EXTRA_RETROACTIVE_PAIR";
-
-    private BroadcastConstants() {
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java
deleted file mode 100644
index f6e77e6..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.ShortBuffer;
-import java.util.Arrays;
-
-/** Represents a block of bytes, with hashCode and equals. */
-public abstract class Bytes {
-    private static final char[] sHexDigits = "0123456789abcdef".toCharArray();
-    private final byte[] mBytes;
-
-    /**
-     * A logical value consisting of one or more bytes in the given order (little-endian, i.e.
-     * LSO...MSO, or big-endian, i.e. MSO...LSO). E.g. the Fast Pair Model ID is a 3-byte value,
-     * and a Bluetooth device address is a 6-byte value.
-     */
-    public static class Value extends Bytes {
-        private final ByteOrder mByteOrder;
-
-        /**
-         * Constructor.
-         */
-        public Value(byte[] bytes, ByteOrder byteOrder) {
-            super(bytes);
-            this.mByteOrder = byteOrder;
-        }
-
-        /**
-         * Gets bytes.
-         */
-        public byte[] getBytes(ByteOrder byteOrder) {
-            return this.mByteOrder.equals(byteOrder) ? getBytes() : reverse(getBytes());
-        }
-
-        @VisibleForTesting
-        static byte[] reverse(byte[] bytes) {
-            byte[] reversedBytes = new byte[bytes.length];
-            for (int i = 0; i < bytes.length; i++) {
-                reversedBytes[i] = bytes[bytes.length - i - 1];
-            }
-            return reversedBytes;
-        }
-    }
-
-    Bytes(byte[] bytes) {
-        mBytes = bytes;
-    }
-
-    private static String toHexString(byte[] bytes) {
-        StringBuilder sb = new StringBuilder(2 * bytes.length);
-        for (byte b : bytes) {
-            sb.append(sHexDigits[(b >> 4) & 0xf]).append(sHexDigits[b & 0xf]);
-        }
-        return sb.toString();
-    }
-
-    /** Returns 2-byte values in the same order, each using the given byte order. */
-    public static byte[] toBytes(ByteOrder byteOrder, short... shorts) {
-        ByteBuffer byteBuffer = ByteBuffer.allocate(shorts.length * 2).order(byteOrder);
-        for (short s : shorts) {
-            byteBuffer.putShort(s);
-        }
-        return byteBuffer.array();
-    }
-
-    /** Returns the shorts in the same order, each converted using the given byte order. */
-    static short[] toShorts(ByteOrder byteOrder, byte[] bytes) {
-        ShortBuffer shortBuffer = ByteBuffer.wrap(bytes).order(byteOrder).asShortBuffer();
-        short[] shorts = new short[shortBuffer.remaining()];
-        shortBuffer.get(shorts);
-        return shorts;
-    }
-
-    /** @return The bytes. */
-    public byte[] getBytes() {
-        return mBytes;
-    }
-
-    @Override
-    public boolean equals(@Nullable Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (!(o instanceof Bytes)) {
-            return false;
-        }
-        Bytes that = (Bytes) o;
-        return Arrays.equals(mBytes, that.mBytes);
-    }
-
-    @Override
-    public int hashCode() {
-        return Arrays.hashCode(mBytes);
-    }
-
-    @Override
-    public String toString() {
-        return toHexString(mBytes);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ConnectException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ConnectException.java
deleted file mode 100644
index 9c8d292..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ConnectException.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.ConnectErrorCode;
-
-
-/** Thrown when connecting to a bluetooth device fails. */
-public class ConnectException extends PairingException {
-    final @ConnectErrorCode int mErrorCode;
-
-    ConnectException(@ConnectErrorCode int errorCode, String format, Object... objects) {
-        super(format, objects);
-        this.mErrorCode = errorCode;
-    }
-
-    /** Returns error code. */
-    public @ConnectErrorCode int getErrorCode() {
-        return mErrorCode;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Constants.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Constants.java
deleted file mode 100644
index cfecd2f..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Constants.java
+++ /dev/null
@@ -1,703 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static android.bluetooth.BluetoothProfile.A2DP;
-import static android.bluetooth.BluetoothProfile.HEADSET;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid;
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.toFastPair128BitUuid;
-
-import static com.google.common.primitives.Bytes.concat;
-
-import android.bluetooth.BluetoothA2dp;
-import android.bluetooth.BluetoothHeadset;
-import android.util.Log;
-
-import androidx.annotation.IntDef;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
-
-import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.primitives.Shorts;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.nio.ByteBuffer;
-import java.security.GeneralSecurityException;
-import java.util.Random;
-import java.util.UUID;
-
-/**
- * Fast Pair and Transport Discovery Service constants.
- *
- * <p>Unless otherwise specified, these numbers come from
- * {https://www.bluetooth.com/specifications/gatt}.
- */
-public final class Constants {
-
-    /** A2DP sink service uuid. */
-    public static final short A2DP_SINK_SERVICE_UUID = 0x110B;
-
-    /** Headset service uuid. */
-    public static final short HEADSET_SERVICE_UUID = 0x1108;
-
-    /** Hands free sink service uuid. */
-    public static final short HANDS_FREE_SERVICE_UUID = 0x111E;
-
-    /** Bluetooth address length. */
-    public static final int BLUETOOTH_ADDRESS_LENGTH = 6;
-
-    private static final String TAG = Constants.class.getSimpleName();
-
-    /**
-     * Defined by https://developers.google.com/nearby/fast-pair/spec.
-     */
-    public static final class FastPairService {
-
-        /** Fast Pair service UUID. */
-        public static final UUID ID = to128BitUuid((short) 0xFE2C);
-
-        /**
-         * Characteristic to write verification bytes to during the key handshake.
-         */
-        public static final class KeyBasedPairingCharacteristic {
-
-            private static final short SHORT_UUID = 0x1234;
-
-            /**
-             * Gets the new 128-bit UUID of this characteristic.
-             *
-             * <p>Note: For GATT server only. GATT client should use {@link
-             * KeyBasedPairingCharacteristic#getId(BluetoothGattConnection)}.
-             */
-            public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
-
-            /**
-             * Gets the {@link UUID} of this characteristic.
-             *
-             * <p>This method is designed for being backward compatible with old version of UUID
-             * therefore needs the {@link BluetoothGattConnection} parameter to check the supported
-             * status of the Fast Pair provider.
-             */
-            public static UUID getId(BluetoothGattConnection gattConnection) {
-                return getSupportedUuid(gattConnection, SHORT_UUID);
-            }
-
-            /**
-             * Constants related to the decrypted request written to this characteristic.
-             */
-            public static final class Request {
-
-                /**
-                 * The size of this message.
-                 */
-                public static final int SIZE = 16;
-
-                /**
-                 * The index of this message for indicating the type byte.
-                 */
-                public static final int TYPE_INDEX = 0;
-
-                /**
-                 * The index of this message for indicating the flags byte.
-                 */
-                public static final int FLAGS_INDEX = 1;
-
-                /**
-                 * The index of this message for indicating the verification data start from.
-                 */
-                public static final int VERIFICATION_DATA_INDEX = 2;
-
-                /**
-                 * The length of verification data, it is Provider’s current BLE address or public
-                 * address.
-                 */
-                public static final int VERIFICATION_DATA_LENGTH = BLUETOOTH_ADDRESS_LENGTH;
-
-                /**
-                 * The index of this message for indicating the seeker's public address start from.
-                 */
-                public static final int SEEKER_PUBLIC_ADDRESS_INDEX = 8;
-
-                /**
-                 * The index of this message for indicating event group.
-                 */
-                public static final int EVENT_GROUP_INDEX = 8;
-
-                /**
-                 * The index of this message for indicating event code.
-                 */
-                public static final int EVENT_CODE_INDEX = 9;
-
-                /**
-                 * The index of this message for indicating the length of additional data of the
-                 * event.
-                 */
-                public static final int EVENT_ADDITIONAL_DATA_LENGTH_INDEX = 10;
-
-                /**
-                 * The index of this message for indicating the event additional data start from.
-                 */
-                public static final int EVENT_ADDITIONAL_DATA_INDEX = 11;
-
-                /**
-                 * The index of this message for indicating the additional data type used in the
-                 * following Additional Data characteristic.
-                 */
-                public static final int ADDITIONAL_DATA_TYPE_INDEX = 10;
-
-                /**
-                 * The type of this message for Key-based Pairing Request.
-                 */
-                public static final byte TYPE_KEY_BASED_PAIRING_REQUEST = 0x00;
-
-                /**
-                 * The bit indicating that the Fast Pair device should temporarily become
-                 * discoverable.
-                 */
-                public static final byte REQUEST_DISCOVERABLE = (byte) (1 << 7);
-
-                /**
-                 * The bit indicating that the requester (Seeker) has included their public address
-                 * in bytes [7,12] of the request, and the Provider should initiate bonding to that
-                 * address.
-                 */
-                public static final byte PROVIDER_INITIATES_BONDING = (byte) (1 << 6);
-
-                /**
-                 * The bit indicating that Seeker requests Provider shall return the existing name.
-                 */
-                public static final byte REQUEST_DEVICE_NAME = (byte) (1 << 5);
-
-                /**
-                 * The bit to request retroactive pairing.
-                 */
-                public static final byte REQUEST_RETROACTIVE_PAIR = (byte) (1 << 4);
-
-                /**
-                 * The type of this message for action over BLE.
-                 */
-                public static final byte TYPE_ACTION_OVER_BLE = 0x10;
-
-                private Request() {
-                }
-            }
-
-            /**
-             * Enumerates all flags of key-based pairing request.
-             */
-            @Retention(RetentionPolicy.SOURCE)
-            @IntDef(
-                    value = {
-                            KeyBasedPairingRequestFlag.REQUEST_DISCOVERABLE,
-                            KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING,
-                            KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME,
-                            KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR,
-                    })
-            public @interface KeyBasedPairingRequestFlag {
-                /**
-                 * The bit indicating that the Fast Pair device should temporarily become
-                 * discoverable.
-                 */
-                int REQUEST_DISCOVERABLE = (byte) (1 << 7);
-                /**
-                 * The bit indicating that the requester (Seeker) has included their public address
-                 * in bytes [7,12] of the request, and the Provider should initiate bonding to that
-                 * address.
-                 */
-                int PROVIDER_INITIATES_BONDING = (byte) (1 << 6);
-                /**
-                 * The bit indicating that Seeker requests Provider shall return the existing name.
-                 */
-                int REQUEST_DEVICE_NAME = (byte) (1 << 5);
-                /**
-                 * The bit indicating that the Seeker request retroactive pairing.
-                 */
-                int REQUEST_RETROACTIVE_PAIR = (byte) (1 << 4);
-            }
-
-            /**
-             * Enumerates all flags of action over BLE request, see Fast Pair spec for details.
-             */
-            @IntDef(
-                    value = {
-                            ActionOverBleFlag.DEVICE_ACTION,
-                            ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC,
-                    })
-            public @interface ActionOverBleFlag {
-                /**
-                 * The bit indicating that the handshaking is for Device Action.
-                 */
-                int DEVICE_ACTION = (byte) (1 << 7);
-                /**
-                 * The bit indicating that this handshake will be followed by Additional Data
-                 * characteristic.
-                 */
-                int ADDITIONAL_DATA_CHARACTERISTIC = (byte) (1 << 6);
-            }
-
-
-            /**
-             * Constants related to the decrypted response sent back in a notify.
-             */
-            public static final class Response {
-
-                /**
-                 * The type of this message = Key-based Pairing Response.
-                 */
-                public static final byte TYPE = 0x01;
-
-                private Response() {
-                }
-            }
-
-            private KeyBasedPairingCharacteristic() {
-            }
-        }
-
-        /**
-         * Characteristic used during Key-based Pairing, to exchange the encrypted passkey.
-         */
-        public static final class PasskeyCharacteristic {
-
-            private static final short SHORT_UUID = 0x1235;
-
-            /**
-             * Gets the new 128-bit UUID of this characteristic.
-             *
-             * <p>Note: For GATT server only. GATT client should use {@link
-             * PasskeyCharacteristic#getId(BluetoothGattConnection)}.
-             */
-            public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
-
-            /**
-             * Gets the {@link UUID} of this characteristic.
-             *
-             * <p>This method is designed for being backward compatible with old version of UUID
-             * therefore
-             * needs the {@link BluetoothGattConnection} parameter to check the supported status of
-             * the Fast Pair provider.
-             */
-            public static UUID getId(BluetoothGattConnection gattConnection) {
-                return getSupportedUuid(gattConnection, SHORT_UUID);
-            }
-
-            /**
-             * The type of the Passkey Block message.
-             */
-            @IntDef(
-                    value = {
-                            Type.SEEKER,
-                            Type.PROVIDER,
-                    })
-            public @interface Type {
-                /**
-                 * Seeker's Passkey.
-                 */
-                int SEEKER = (byte) 0x02;
-                /**
-                 * Provider's Passkey.
-                 */
-                int PROVIDER = (byte) 0x03;
-            }
-
-            /**
-             * Constructs the encrypted value to write to the characteristic.
-             */
-            public static byte[] encrypt(@Type int type, byte[] secret, int passkey)
-                    throws GeneralSecurityException {
-                Preconditions.checkArgument(
-                        0 < passkey && passkey < /*2^24=*/ 16777216,
-                        "Passkey %s must be positive and fit in 3 bytes",
-                        passkey);
-                byte[] passkeyBytes =
-                        new byte[]{(byte) (passkey >>> 16), (byte) (passkey >>> 8), (byte) passkey};
-                byte[] salt =
-                        new byte[AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH - 1
-                                - passkeyBytes.length];
-                new Random().nextBytes(salt);
-                return AesEcbSingleBlockEncryption.encrypt(
-                        secret, concat(new byte[]{(byte) type}, passkeyBytes, salt));
-            }
-
-            /**
-             * Extracts the passkey from the encrypted characteristic value.
-             */
-            public static int decrypt(@Type int type, byte[] secret,
-                    byte[] passkeyCharacteristicValue)
-                    throws GeneralSecurityException {
-                byte[] decrypted = AesEcbSingleBlockEncryption
-                        .decrypt(secret, passkeyCharacteristicValue);
-                if (decrypted[0] != (byte) type) {
-                    throw new GeneralSecurityException(
-                            "Wrong Passkey Block type (expected " + type + ", got "
-                                    + decrypted[0] + ")");
-                }
-                return ByteBuffer.allocate(4)
-                        .put((byte) 0)
-                        .put(decrypted, /*offset=*/ 1, /*length=*/ 3)
-                        .getInt(0);
-            }
-
-            private PasskeyCharacteristic() {
-            }
-        }
-
-        /**
-         * Characteristic to write to during the key exchange.
-         */
-        public static final class AccountKeyCharacteristic {
-
-            private static final short SHORT_UUID = 0x1236;
-
-            /**
-             * Gets the new 128-bit UUID of this characteristic.
-             *
-             * <p>Note: For GATT server only. GATT client should use {@link
-             * AccountKeyCharacteristic#getId(BluetoothGattConnection)}.
-             */
-            public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
-
-            /**
-             * Gets the {@link UUID} of this characteristic.
-             *
-             * <p>This method is designed for being backward compatible with old version of UUID
-             * therefore
-             * needs the {@link BluetoothGattConnection} parameter to check the supported status of
-             * the Fast Pair provider.
-             */
-            public static UUID getId(BluetoothGattConnection gattConnection) {
-                return getSupportedUuid(gattConnection, SHORT_UUID);
-            }
-
-            /**
-             * The type for this message, account key request.
-             */
-            public static final byte TYPE = 0x04;
-
-            private AccountKeyCharacteristic() {
-            }
-        }
-
-        /**
-         * Characteristic to write to and notify on for handling personalized name, see {@link
-         * NamingEncoder}.
-         */
-        public static final class NameCharacteristic {
-
-            private static final short SHORT_UUID = 0x1237;
-
-            /**
-             * Gets the new 128-bit UUID of this characteristic.
-             *
-             * <p>Note: For GATT server only. GATT client should use {@link
-             * NameCharacteristic#getId(BluetoothGattConnection)}.
-             */
-            public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
-
-            /**
-             * Gets the {@link UUID} of this characteristic.
-             *
-             * <p>This method is designed for being backward compatible with old version of UUID
-             * therefore
-             * needs the {@link BluetoothGattConnection} parameter to check the supported status of
-             * the Fast Pair provider.
-             */
-            public static UUID getId(BluetoothGattConnection gattConnection) {
-                return getSupportedUuid(gattConnection, SHORT_UUID);
-            }
-
-            private NameCharacteristic() {
-            }
-        }
-
-        /**
-         * Characteristic to write to and notify on for handling additional data, see
-         * https://developers.google.com/nearby/fast-pair/early-access/spec#AdditionalData
-         */
-        public static final class AdditionalDataCharacteristic {
-
-            private static final short SHORT_UUID = 0x1237;
-
-            public static final int DATA_ID_INDEX = 0;
-            public static final int DATA_LENGTH_INDEX = 1;
-            public static final int DATA_START_INDEX = 2;
-
-            /**
-             * Gets the new 128-bit UUID of this characteristic.
-             *
-             * <p>Note: For GATT server only. GATT client should use {@link
-             * AdditionalDataCharacteristic#getId(BluetoothGattConnection)}.
-             */
-            public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
-
-            /**
-             * Gets the {@link UUID} of this characteristic.
-             *
-             * <p>This method is designed for being backward compatible with old version of UUID
-             * therefore
-             * needs the {@link BluetoothGattConnection} parameter to check the supported status of
-             * the Fast Pair provider.
-             */
-            public static UUID getId(BluetoothGattConnection gattConnection) {
-                return getSupportedUuid(gattConnection, SHORT_UUID);
-            }
-
-            /**
-             * Enumerates all types of additional data.
-             */
-            @Retention(RetentionPolicy.SOURCE)
-            @IntDef(
-                    value = {
-                            AdditionalDataType.PERSONALIZED_NAME,
-                            AdditionalDataType.UNKNOWN,
-                    })
-            public @interface AdditionalDataType {
-                /**
-                 * The value indicating that the type is for personalized name.
-                 */
-                int PERSONALIZED_NAME = (byte) 0x01;
-                int UNKNOWN = (byte) 0x00; // and all others.
-            }
-        }
-
-        /**
-         * Characteristic to control the beaconing feature (FastPair+Eddystone).
-         */
-        public static final class BeaconActionsCharacteristic {
-
-            private static final short SHORT_UUID = 0x1238;
-
-            /**
-             * Gets the new 128-bit UUID of this characteristic.
-             *
-             * <p>Note: For GATT server only. GATT client should use {@link
-             * BeaconActionsCharacteristic#getId(BluetoothGattConnection)}.
-             */
-            public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
-
-            /**
-             * Gets the {@link UUID} of this characteristic.
-             *
-             * <p>This method is designed for being backward compatible with old version of UUID
-             * therefore
-             * needs the {@link BluetoothGattConnection} parameter to check the supported status of
-             * the Fast Pair provider.
-             */
-            public static UUID getId(BluetoothGattConnection gattConnection) {
-                return getSupportedUuid(gattConnection, SHORT_UUID);
-            }
-
-            /**
-             * Enumerates all types of beacon actions.
-             */
-            /** Fast Pair Bond State. */
-            @Retention(RetentionPolicy.SOURCE)
-            @IntDef(
-                    value = {
-                            BeaconActionType.READ_BEACON_PARAMETERS,
-                            BeaconActionType.READ_PROVISIONING_STATE,
-                            BeaconActionType.SET_EPHEMERAL_IDENTITY_KEY,
-                            BeaconActionType.CLEAR_EPHEMERAL_IDENTITY_KEY,
-                            BeaconActionType.READ_EPHEMERAL_IDENTITY_KEY,
-                            BeaconActionType.RING,
-                            BeaconActionType.READ_RINGING_STATE,
-                            BeaconActionType.UNKNOWN,
-                    })
-            public @interface BeaconActionType {
-                int READ_BEACON_PARAMETERS = (byte) 0x00;
-                int READ_PROVISIONING_STATE = (byte) 0x01;
-                int SET_EPHEMERAL_IDENTITY_KEY = (byte) 0x02;
-                int CLEAR_EPHEMERAL_IDENTITY_KEY = (byte) 0x03;
-                int READ_EPHEMERAL_IDENTITY_KEY = (byte) 0x04;
-                int RING = (byte) 0x05;
-                int READ_RINGING_STATE = (byte) 0x06;
-                int UNKNOWN = (byte) 0xFF; // and all others
-            }
-
-            /** Converts value to enum. */
-            public static @BeaconActionType int valueOf(byte value) {
-                switch(value) {
-                    case BeaconActionType.READ_BEACON_PARAMETERS:
-                    case BeaconActionType.READ_PROVISIONING_STATE:
-                    case BeaconActionType.SET_EPHEMERAL_IDENTITY_KEY:
-                    case BeaconActionType.CLEAR_EPHEMERAL_IDENTITY_KEY:
-                    case BeaconActionType.READ_EPHEMERAL_IDENTITY_KEY:
-                    case BeaconActionType.RING:
-                    case BeaconActionType.READ_RINGING_STATE:
-                    case BeaconActionType.UNKNOWN:
-                        return value;
-                    default:
-                        return BeaconActionType.UNKNOWN;
-                }
-            }
-        }
-
-
-        /**
-         * Characteristic to read for checking firmware version. 0X2A26 is assigned number from
-         * bluetooth SIG website.
-         */
-        public static final class FirmwareVersionCharacteristic {
-
-            /** UUID for firmware version. */
-            public static final UUID ID = to128BitUuid((short) 0x2A26);
-
-            private FirmwareVersionCharacteristic() {
-            }
-        }
-
-        private FastPairService() {
-        }
-    }
-
-    /**
-     * Defined by the BR/EDR Handover Profile. Pre-release version here:
-     * {https://jfarfel.users.x20web.corp.google.com/Bluetooth%20Handover%20d09.pdf}
-     */
-    public interface TransportDiscoveryService {
-
-        UUID ID = to128BitUuid((short) 0x1824);
-
-        byte BLUETOOTH_SIG_ORGANIZATION_ID = 0x01;
-        byte SERVICE_UUIDS_16_BIT_LIST_TYPE = 0x01;
-        byte SERVICE_UUIDS_32_BIT_LIST_TYPE = 0x02;
-        byte SERVICE_UUIDS_128_BIT_LIST_TYPE = 0x03;
-
-        /**
-         * Writing to this allows you to activate the BR/EDR transport.
-         */
-        interface ControlPointCharacteristic {
-
-            UUID ID = to128BitUuid((short) 0x2ABC);
-            byte ACTIVATE_TRANSPORT_OP_CODE = 0x01;
-        }
-
-        /**
-         * Info necessary to pair (mostly the Bluetooth Address).
-         */
-        interface BrHandoverDataCharacteristic {
-
-            UUID ID = to128BitUuid((short) 0x2C01);
-
-            /**
-             * All bits are reserved for future use.
-             */
-            byte BR_EDR_FEATURES = 0x00;
-        }
-
-        /**
-         * This characteristic exists only to wrap the descriptor.
-         */
-        interface BluetoothSigDataCharacteristic {
-
-            UUID ID = to128BitUuid((short) 0x2C02);
-
-            /**
-             * The entire Transport Block data (e.g. supported Bluetooth services).
-             */
-            interface BrTransportBlockDataDescriptor {
-
-                UUID ID = to128BitUuid((short) 0x2C03);
-            }
-        }
-    }
-
-    public static final UUID CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID =
-            to128BitUuid((short) 0x2902);
-
-    /**
-     * Wrapper for Bluetooth profile
-     */
-    public static class Profile {
-
-        public final int type;
-        public final String name;
-        public final String connectionStateAction;
-
-        private Profile(int type, String name, String connectionStateAction) {
-            this.type = type;
-            this.name = name;
-            this.connectionStateAction = connectionStateAction;
-        }
-
-        @Override
-        public String toString() {
-            return name;
-        }
-    }
-
-    /**
-     * {@link BluetoothHeadset} is used for both Headset and HandsFree (HFP).
-     */
-    private static final Profile HEADSET_AND_HANDS_FREE_PROFILE =
-            new Profile(
-                    HEADSET, "HEADSET_AND_HANDS_FREE",
-                    BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
-
-    /** Fast Pair supported profiles. */
-    public static final ImmutableMap<Short, Profile> PROFILES =
-            ImmutableMap.<Short, Profile>builder()
-                    .put(
-                            Constants.A2DP_SINK_SERVICE_UUID,
-                            new Profile(A2DP, "A2DP",
-                                    BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED))
-                    .put(Constants.HEADSET_SERVICE_UUID, HEADSET_AND_HANDS_FREE_PROFILE)
-                    .put(Constants.HANDS_FREE_SERVICE_UUID, HEADSET_AND_HANDS_FREE_PROFILE)
-                    .build();
-
-    static short[] getSupportedProfiles() {
-        return Shorts.toArray(PROFILES.keySet());
-    }
-
-    /**
-     * Helper method of getting 128-bit UUID for Fast Pair custom GATT characteristics.
-     *
-     * <p>This method is designed for being backward compatible with old version of UUID therefore
-     * needs the {@link BluetoothGattConnection} parameter to check the supported status of the Fast
-     * Pair provider.
-     *
-     * <p>Note: For new custom GATT characteristics, don't need to use this helper and please just
-     * call {@code toFastPair128BitUuid(shortUuid)} to get the UUID. Which also implies that callers
-     * don't need to provide {@link BluetoothGattConnection} to get the UUID anymore.
-     */
-    private static UUID getSupportedUuid(BluetoothGattConnection gattConnection, short shortUuid) {
-        // In worst case (new characteristic not found), this method's performance impact is about
-        // 6ms
-        // by using Pixel2 + JBL LIVE220. And the impact should be less and less along with more and
-        // more devices adopt the new characteristics.
-        try {
-            // Checks the new UUID first.
-            if (gattConnection
-                    .getCharacteristic(FastPairService.ID, toFastPair128BitUuid(shortUuid))
-                    != null) {
-                Log.d(TAG, "Uses new KeyBasedPairingCharacteristic.ID");
-                return toFastPair128BitUuid(shortUuid);
-            }
-        } catch (BluetoothException e) {
-            Log.d(TAG, "Uses old KeyBasedPairingCharacteristic.ID");
-        }
-        // Returns the old UUID for default.
-        return to128BitUuid(shortUuid);
-    }
-
-    private Constants() {
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/CreateBondException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/CreateBondException.java
deleted file mode 100644
index d6aa3b2..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/CreateBondException.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.CreateBondErrorCode;
-
-/** Thrown when binding (pairing) with a bluetooth device fails. */
-public class CreateBondException extends PairingException {
-    final @CreateBondErrorCode int mErrorCode;
-    int mReason;
-
-    CreateBondException(@CreateBondErrorCode int errorCode, int reason, String format,
-            Object... objects) {
-        super(format, objects);
-        this.mErrorCode = errorCode;
-        this.mReason = reason;
-    }
-
-    /** Returns error code. */
-    public @CreateBondErrorCode int getErrorCode() {
-        return mErrorCode;
-    }
-
-    /** Returns reason. */
-    public int getReason() {
-        return mReason;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiver.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiver.java
deleted file mode 100644
index 5bcf10a..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiver.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
-
-import android.bluetooth.BluetoothDevice;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-/**
- * Like {@link SimpleBroadcastReceiver}, but for intents about a certain {@link BluetoothDevice}.
- */
-abstract class DeviceIntentReceiver extends SimpleBroadcastReceiver {
-
-    private static final String TAG = DeviceIntentReceiver.class.getSimpleName();
-
-    private final BluetoothDevice mDevice;
-
-    static DeviceIntentReceiver oneShotReceiver(
-            Context context, Preferences preferences, BluetoothDevice device, String... actions) {
-        return new DeviceIntentReceiver(context, preferences, device, actions) {
-            @Override
-            protected void onReceiveDeviceIntent(Intent intent) throws Exception {
-                close();
-            }
-        };
-    }
-
-    /**
-     * @param context The context to use to register / unregister the receiver.
-     * @param device The interesting device. We ignore intents about other devices.
-     * @param actions The actions to include in our intent filter.
-     */
-    protected DeviceIntentReceiver(
-            Context context, Preferences preferences, BluetoothDevice device, String... actions) {
-        super(context, preferences, actions);
-        this.mDevice = device;
-    }
-
-    /**
-     * Called with intents about the interesting device (see {@link #DeviceIntentReceiver}). Any
-     * exception thrown by this method will be delivered via {@link #await}.
-     */
-    protected abstract void onReceiveDeviceIntent(Intent intent) throws Exception;
-
-    // incompatible types in argument.
-    @Override
-    protected void onReceive(Intent intent) throws Exception {
-        BluetoothDevice intentDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-        if (mDevice == null || mDevice.equals(intentDevice)) {
-            onReceiveDeviceIntent(intent);
-        } else {
-            Log.v(TAG,
-                    "Ignoring intent for device=" + maskBluetoothAddress(intentDevice)
-                            + "(expected "
-                            + maskBluetoothAddress(mDevice) + ")");
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchange.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchange.java
deleted file mode 100644
index dbcdf07..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchange.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.primitives.Bytes.concat;
-
-import androidx.annotation.Nullable;
-
-import java.math.BigInteger;
-import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECGenParameterSpec;
-import java.security.spec.ECParameterSpec;
-import java.security.spec.ECPoint;
-import java.security.spec.ECPrivateKeySpec;
-import java.security.spec.ECPublicKeySpec;
-import java.util.Arrays;
-
-import javax.crypto.KeyAgreement;
-
-/**
- * Helper for generating keys based off of the Elliptic-Curve Diffie-Hellman algorithm (ECDH).
- */
-public final class EllipticCurveDiffieHellmanExchange {
-
-    public static final int PUBLIC_KEY_LENGTH = 64;
-    static final int PRIVATE_KEY_LENGTH = 32;
-
-    private static final String[] PROVIDERS = {"GmsCore_OpenSSL", "AndroidOpenSSL", "SC", "BC"};
-
-    private static final String EC_ALGORITHM = "EC";
-
-    /**
-     * Also known as prime256v1 or NIST P-256.
-     */
-    private static final ECGenParameterSpec EC_GEN_PARAMS = new ECGenParameterSpec("secp256r1");
-
-    @Nullable
-    private final ECPublicKey mPublicKey;
-    private final ECPrivateKey mPrivateKey;
-
-    /**
-     * Creates a new EllipticCurveDiffieHellmanExchange object.
-     */
-    public static EllipticCurveDiffieHellmanExchange create() throws GeneralSecurityException {
-        KeyPair keyPair = generateKeyPair();
-        return new EllipticCurveDiffieHellmanExchange(
-                (ECPublicKey) keyPair.getPublic(), (ECPrivateKey) keyPair.getPrivate());
-    }
-
-    /**
-     * Creates a new EllipticCurveDiffieHellmanExchange object.
-     */
-    public static EllipticCurveDiffieHellmanExchange create(byte[] privateKey)
-            throws GeneralSecurityException {
-        ECPrivateKey ecPrivateKey = (ECPrivateKey) generatePrivateKey(privateKey);
-        return new EllipticCurveDiffieHellmanExchange(/*publicKey=*/ null, ecPrivateKey);
-    }
-
-    private EllipticCurveDiffieHellmanExchange(
-            @Nullable ECPublicKey publicKey, ECPrivateKey privateKey) {
-        this.mPublicKey = publicKey;
-        this.mPrivateKey = privateKey;
-    }
-
-    /**
-     * @param otherPublicKey Another party's public key. See {@link #getPublicKey()} for format.
-     * @return The shared secret. Given our public key (and its private key), the other party can
-     * generate the same secret. This is a key meant for symmetric encryption.
-     */
-    public byte[] generateSecret(byte[] otherPublicKey) throws GeneralSecurityException {
-        KeyAgreement agreement = keyAgreement();
-        agreement.init(mPrivateKey);
-        agreement.doPhase(generatePublicKey(otherPublicKey), /*lastPhase=*/ true);
-        byte[] secret = agreement.generateSecret();
-        // Headsets only support AES with 128-bit keys. So, hash the secret so that the entropy is
-        // high and then take only the first 128-bits.
-        secret = MessageDigest.getInstance("SHA-256").digest(secret);
-        return Arrays.copyOf(secret, 16);
-    }
-
-    /**
-     * Returns a public point W on the NIST P-256 elliptic curve. First 32 bytes are the X
-     * coordinate, next 32 bytes are the Y coordinate. Each coordinate is an unsigned big-endian
-     * integer.
-     */
-    public @Nullable byte[] getPublicKey() {
-        if (mPublicKey == null) {
-            return null;
-        }
-        ECPoint w = mPublicKey.getW();
-        // See getPrivateKey for why we're resizing.
-        byte[] x = resizeWithLeadingZeros(w.getAffineX().toByteArray(), 32);
-        byte[] y = resizeWithLeadingZeros(w.getAffineY().toByteArray(), 32);
-        return concat(x, y);
-    }
-
-    /**
-     * Returns a private value S, an unsigned big-endian integer.
-     */
-    public byte[] getPrivateKey() {
-        // Note that BigInteger.toByteArray() returns a signed representation, so it will add an
-        // extra zero byte to the front if the first bit is 1.
-        // We must remove that leading zero (we know the number is unsigned). We must also add
-        // leading zeros if the number is too small.
-        return resizeWithLeadingZeros(mPrivateKey.getS().toByteArray(), 32);
-    }
-
-    /**
-     * Removes or adds leading zeros until we have an array of size {@code n}.
-     */
-    private static byte[] resizeWithLeadingZeros(byte[] x, int n) {
-        if (n < x.length) {
-            int start = x.length - n;
-            for (int i = 0; i < start; i++) {
-                if (x[i] != 0) {
-                    throw new IllegalArgumentException(
-                            "More than " + n + " non-zero bytes in " + Arrays.toString(x));
-                }
-            }
-            return Arrays.copyOfRange(x, start, x.length);
-        }
-        return concat(new byte[n - x.length], x);
-    }
-
-    /**
-     * @param publicKey See {@link #getPublicKey()} for format.
-     */
-    private static PublicKey generatePublicKey(byte[] publicKey) throws GeneralSecurityException {
-        if (publicKey.length != PUBLIC_KEY_LENGTH) {
-            throw new GeneralSecurityException("Public key length incorrect: " + publicKey.length);
-        }
-        byte[] x = Arrays.copyOf(publicKey, publicKey.length / 2);
-        byte[] y = Arrays.copyOfRange(publicKey, publicKey.length / 2, publicKey.length);
-        return keyFactory()
-                .generatePublic(
-                        new ECPublicKeySpec(
-                                new ECPoint(new BigInteger(/*signum=*/ 1, x),
-                                        new BigInteger(/*signum=*/ 1, y)),
-                                ecParameterSpec()));
-    }
-
-    /**
-     * @param privateKey See {@link #getPrivateKey()} for format.
-     */
-    private static PrivateKey generatePrivateKey(byte[] privateKey)
-            throws GeneralSecurityException {
-        if (privateKey.length != PRIVATE_KEY_LENGTH) {
-            throw new GeneralSecurityException("Private key length incorrect: "
-                    + privateKey.length);
-        }
-        return keyFactory()
-                .generatePrivate(
-                        new ECPrivateKeySpec(new BigInteger(/*signum=*/ 1, privateKey),
-                                ecParameterSpec()));
-    }
-
-    private static ECParameterSpec ecParameterSpec() throws GeneralSecurityException {
-        // This seems to be the simplest way to get the curve's ECParameterSpec. Verified that it's
-        // the same whether you get it from the public or private key, and that it's the same as the
-        // raw params in SecAggEcUtil.getNistP256Params().
-        return ((ECPublicKey) generateKeyPair().getPublic()).getParams();
-    }
-
-    private static KeyPair generateKeyPair() throws GeneralSecurityException {
-        KeyPairGenerator generator = findProvider(p -> KeyPairGenerator.getInstance(EC_ALGORITHM,
-                p));
-        generator.initialize(EC_GEN_PARAMS);
-        return generator.generateKeyPair();
-    }
-
-    private static KeyAgreement keyAgreement() throws NoSuchProviderException {
-        return findProvider(p -> KeyAgreement.getInstance("ECDH", p));
-    }
-
-    private static KeyFactory keyFactory() throws NoSuchProviderException {
-        return findProvider(p -> KeyFactory.getInstance(EC_ALGORITHM, p));
-    }
-
-    private interface ProviderConsumer<T> {
-
-        T tryProvider(String provider) throws NoSuchAlgorithmException, NoSuchProviderException;
-    }
-
-    private static <T> T findProvider(ProviderConsumer<T> providerConsumer)
-            throws NoSuchProviderException {
-        for (String provider : PROVIDERS) {
-            try {
-                return providerConsumer.tryProvider(provider);
-            } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
-                // No-op
-            }
-        }
-        throw new NoSuchProviderException();
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java
deleted file mode 100644
index 7a0548b..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import android.bluetooth.BluetoothDevice;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
-
-import java.util.Objects;
-
-import javax.annotation.Nullable;
-
-/**
- * Describes events that are happening during fast pairing. EventCode is required, everything else
- * is optional.
- */
-public class Event implements Parcelable {
-
-    private final @EventCode int mEventCode;
-    private final long mTimestamp;
-    private final Short mProfile;
-    private final BluetoothDevice mBluetoothDevice;
-    private final Exception mException;
-
-    private Event(@EventCode int eventCode, long timestamp, @Nullable Short profile,
-            @Nullable BluetoothDevice bluetoothDevice, @Nullable Exception exception) {
-        mEventCode = eventCode;
-        mTimestamp = timestamp;
-        mProfile = profile;
-        mBluetoothDevice = bluetoothDevice;
-        mException = exception;
-    }
-
-    /**
-     * Returns event code.
-     */
-    public @EventCode int getEventCode() {
-        return mEventCode;
-    }
-
-    /**
-     * Returns timestamp.
-     */
-    public long getTimestamp() {
-        return mTimestamp;
-    }
-
-    /**
-     * Returns profile.
-     */
-    @Nullable
-    public Short getProfile() {
-        return mProfile;
-    }
-
-    /**
-     * Returns Bluetooth device.
-     */
-    @Nullable
-    public BluetoothDevice getBluetoothDevice() {
-        return mBluetoothDevice;
-    }
-
-    /**
-     * Returns exception.
-     */
-    @Nullable
-    public Exception getException() {
-        return mException;
-    }
-
-    /**
-     * Returns whether profile is not null.
-     */
-    public boolean hasProfile() {
-        return getProfile() != null;
-    }
-
-    /**
-     * Returns whether Bluetooth device is not null.
-     */
-    public boolean hasBluetoothDevice() {
-        return getBluetoothDevice() != null;
-    }
-
-    /**
-     * Returns a builder.
-     */
-    public static Builder builder() {
-        return new Event.Builder();
-    }
-
-    /**
-     * Returns whether it fails.
-     */
-    public boolean isFailure() {
-        return getException() != null;
-    }
-
-    @Override
-    public String toString() {
-        return "Event{"
-                + "eventCode=" + mEventCode + ", "
-                + "timestamp=" + mTimestamp + ", "
-                + "profile=" + mProfile + ", "
-                + "bluetoothDevice=" + mBluetoothDevice + ", "
-                + "exception=" + mException
-                + "}";
-    }
-
-    @Override
-    public boolean equals(@Nullable Object o) {
-        if (o == this) {
-            return true;
-        }
-        if (o instanceof Event) {
-            Event that = (Event) o;
-            return this.mEventCode == that.getEventCode()
-                    && this.mTimestamp == that.getTimestamp()
-                    && (this.mBluetoothDevice == null
-                    ? that.getBluetoothDevice() == null :
-                    this.mBluetoothDevice.equals(that.getBluetoothDevice()))
-                    && (this.mProfile == null
-                    ? that.getProfile() == null : this.mProfile.equals(that.getProfile()));
-        }
-        return false;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(mEventCode, mTimestamp, mProfile, mBluetoothDevice, mException);
-    }
-
-    /**
-     * Builder
-     */
-    public static class Builder {
-        private @EventCode int mEventCode;
-        private long mTimestamp;
-        private Short mProfile;
-        private BluetoothDevice mBluetoothDevice;
-        private Exception mException;
-
-        /**
-         * Set event code.
-         */
-        public Builder setEventCode(@EventCode int eventCode) {
-            this.mEventCode = eventCode;
-            return this;
-        }
-
-        /**
-         * Set timestamp.
-         */
-        public Builder setTimestamp(long timestamp) {
-            this.mTimestamp = timestamp;
-            return this;
-        }
-
-        /**
-         * Set profile.
-         */
-        public Builder setProfile(@Nullable Short profile) {
-            this.mProfile = profile;
-            return this;
-        }
-
-        /**
-         * Set Bluetooth device.
-         */
-        public Builder setBluetoothDevice(@Nullable BluetoothDevice device) {
-            this.mBluetoothDevice = device;
-            return this;
-        }
-
-        /**
-         * Set exception.
-         */
-        public Builder setException(@Nullable Exception exception) {
-            this.mException = exception;
-            return this;
-        }
-
-        /**
-         * Builds event.
-         */
-        public Event build() {
-            return new Event(mEventCode, mTimestamp, mProfile, mBluetoothDevice, mException);
-        }
-    }
-
-    @Override
-    public final void writeToParcel(Parcel dest, int flags) {
-        dest.writeInt(getEventCode());
-        dest.writeLong(getTimestamp());
-        dest.writeValue(getProfile());
-        dest.writeParcelable(getBluetoothDevice(), 0);
-        dest.writeSerializable(getException());
-    }
-
-    @Override
-    public final int describeContents() {
-        return 0;
-    }
-
-    /**
-     * Event Creator instance.
-     */
-    public static final Creator<Event> CREATOR =
-            new Creator<Event>() {
-                @Override
-                /** Creates Event from Parcel. */
-                public Event createFromParcel(Parcel in) {
-                    return Event.builder()
-                            .setEventCode(in.readInt())
-                            .setTimestamp(in.readLong())
-                            .setProfile((Short) in.readValue(Short.class.getClassLoader()))
-                            .setBluetoothDevice(
-                                    in.readParcelable(BluetoothDevice.class.getClassLoader()))
-                            .setException((Exception) in.readSerializable())
-                            .build();
-                }
-
-                @Override
-                /** Returns Event array. */
-                public Event[] newArray(int size) {
-                    return new Event[size];
-                }
-            };
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLogger.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLogger.java
deleted file mode 100644
index 4fc1917..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLogger.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-/** Logs events triggered during Fast Pairing. */
-public interface EventLogger {
-
-    /** Log successful event. */
-    void logEventSucceeded(Event event);
-
-    /** Log failed event. */
-    void logEventFailed(Event event, Exception e);
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLoggerWrapper.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLoggerWrapper.java
deleted file mode 100644
index 024bfde..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLoggerWrapper.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import android.bluetooth.BluetoothDevice;
-import android.content.Context;
-
-import com.android.server.nearby.common.bluetooth.fastpair.Preferences.ExtraLoggingInformation;
-import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
-
-import javax.annotation.Nullable;
-
-/**
- * Convenience wrapper around EventLogger.
- */
-// TODO(b/202559985): cleanup EventLoggerWrapper.
-class EventLoggerWrapper {
-
-    EventLoggerWrapper(@Nullable EventLogger eventLogger) {
-    }
-
-    /**
-     * Binds to the logging service. This operation blocks until binding has completed or timed
-     * out.
-     */
-    void bind(
-            Context context, String address,
-            @Nullable ExtraLoggingInformation extraLoggingInformation) {
-    }
-
-    boolean isBound() {
-        return false;
-    }
-
-    void unbind(Context context) {
-    }
-
-    void setCurrentEvent(@EventCode int code) {
-    }
-
-    void setCurrentProfile(short profile) {
-    }
-
-    void logCurrentEventFailed(Exception e) {
-    }
-
-    void logCurrentEventSucceeded() {
-    }
-
-    void setDevice(@Nullable BluetoothDevice device) {
-    }
-
-    boolean isCurrentEvent() {
-        return false;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConnection.java
deleted file mode 100644
index c963aa6..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConnection.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import android.annotation.WorkerThread;
-import android.bluetooth.BluetoothDevice;
-
-import androidx.annotation.Nullable;
-import androidx.core.util.Consumer;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-
-/** Abstract class for pairing or connecting via FastPair. */
-public abstract class FastPairConnection {
-    @Nullable protected OnPairedCallback mPairedCallback;
-    @Nullable protected OnGetBluetoothAddressCallback mOnGetBluetoothAddressCallback;
-    @Nullable protected PasskeyConfirmationHandler mPasskeyConfirmationHandler;
-    @Nullable protected FastPairSignalChecker mFastPairSignalChecker;
-    @Nullable protected Consumer<Integer> mRescueFromError;
-    @Nullable protected Runnable mPrepareCreateBondCallback;
-    protected boolean mPasskeyIsGotten;
-
-    /** Sets a callback to be invoked once the device is paired. */
-    public void setOnPairedCallback(OnPairedCallback callback) {
-        this.mPairedCallback = callback;
-    }
-
-    /** Sets a callback to be invoked while the target bluetooth address is decided. */
-    public void setOnGetBluetoothAddressCallback(OnGetBluetoothAddressCallback callback) {
-        this.mOnGetBluetoothAddressCallback = callback;
-    }
-
-    /** Sets a callback to be invoked while handling the passkey confirmation. */
-    public void setPasskeyConfirmationHandler(
-            PasskeyConfirmationHandler passkeyConfirmationHandler) {
-        this.mPasskeyConfirmationHandler = passkeyConfirmationHandler;
-    }
-
-    public void setFastPairSignalChecker(FastPairSignalChecker fastPairSignalChecker) {
-        this.mFastPairSignalChecker = fastPairSignalChecker;
-    }
-
-    public void setRescueFromError(Consumer<Integer> rescueFromError) {
-        this.mRescueFromError = rescueFromError;
-    }
-
-    public void setPrepareCreateBondCallback(Runnable runnable) {
-        this.mPrepareCreateBondCallback = runnable;
-    }
-
-    @VisibleForTesting
-    @Nullable
-    public Runnable getPrepareCreateBondCallback() {
-        return mPrepareCreateBondCallback;
-    }
-
-    /**
-     * Sets the fast pair history for identifying whether or not the provider has paired with the
-     * primary account on other phones before.
-     */
-    @WorkerThread
-    public abstract void setFastPairHistory(List<FastPairHistoryItem> fastPairHistoryItem);
-
-    /** Sets the device name to the Provider. */
-    public abstract void setProviderDeviceName(String deviceName);
-
-    /** Gets the device name from the Provider. */
-    @Nullable
-    public abstract String getProviderDeviceName();
-
-    /**
-     * Gets the existing account key of the Provider.
-     *
-     * @return the existing account key if the Provider has paired with the account, null otherwise
-     */
-    @WorkerThread
-    @Nullable
-    public abstract byte[] getExistingAccountKey();
-
-    /**
-     * Pairs with Provider. Synchronous: Blocks until paired and connected. Throws on any error.
-     *
-     * @return the secret key for the user's account, if written
-     */
-    @WorkerThread
-    @Nullable
-    public abstract SharedSecret pair()
-            throws BluetoothException, InterruptedException, TimeoutException, ExecutionException,
-            PairingException, ReflectionException;
-
-    /**
-     * Pairs with Provider. Synchronous: Blocks until paired and connected. Throws on any error.
-     *
-     * @param key can be in two different formats. If it is 16 bytes long, then it is an AES account
-     *    key. Otherwise, it's a public key generated by {@link EllipticCurveDiffieHellmanExchange}.
-     *    See go/fast-pair-2-spec for how each of these keys are used.
-     * @return the secret key for the user's account, if written
-     */
-    @WorkerThread
-    @Nullable
-    public abstract SharedSecret pair(@Nullable byte[] key)
-            throws BluetoothException, InterruptedException, TimeoutException, ExecutionException,
-            PairingException, GeneralSecurityException, ReflectionException;
-
-    /** Unpairs with Provider. Synchronous: Blocks until unpaired. Throws on any error. */
-    @WorkerThread
-    public abstract void unpair(BluetoothDevice device)
-            throws InterruptedException, TimeoutException, ExecutionException, PairingException,
-            ReflectionException;
-
-    /** Gets the public address of the Provider. */
-    @Nullable
-    public abstract String getPublicAddress();
-
-
-    /** Callback for getting notifications when pairing has completed. */
-    public interface OnPairedCallback {
-        /** Called when the device at address has finished pairing. */
-        void onPaired(String address);
-    }
-
-    /** Callback for getting bluetooth address Bisto oobe need this information */
-    public interface OnGetBluetoothAddressCallback {
-        /** Called when the device has received bluetooth address. */
-        void onGetBluetoothAddress(String address);
-    }
-
-    /** Holds the exchanged secret key and the public mac address of the device. */
-    public static class SharedSecret {
-        private final byte[] mKey;
-        private final String mAddress;
-        private SharedSecret(byte[] key, String address) {
-            mKey = key;
-            mAddress = address;
-        }
-
-        /** Creates Shared Secret. */
-        public static SharedSecret create(byte[] key, String address) {
-            return new SharedSecret(key, address);
-        }
-
-        /** Gets Shared Secret Key. */
-        public byte[] getKey() {
-            return mKey;
-        }
-
-        /** Gets Shared Secret Address. */
-        public String getAddress() {
-            return mAddress;
-        }
-
-        @Override
-        public String toString() {
-            return "SharedSecret{"
-                    + "key=" + Arrays.toString(mKey) + ", "
-                    + "address=" + mAddress
-                    + "}";
-        }
-
-        @Override
-        public boolean equals(@Nullable Object o) {
-            if (o == this) {
-                return true;
-            }
-            if (o instanceof SharedSecret) {
-                SharedSecret that = (SharedSecret) o;
-                return Arrays.equals(this.mKey, that.getKey())
-                        && this.mAddress.equals(that.getAddress());
-            }
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(Arrays.hashCode(mKey), mAddress);
-        }
-    }
-
-    /** Invokes if gotten the passkey. */
-    public void setPasskeyIsGotten() {
-        mPasskeyIsGotten = true;
-    }
-
-    /** Returns the value of passkeyIsGotten. */
-    public boolean getPasskeyIsGotten() {
-        return mPasskeyIsGotten;
-    }
-
-    /** Interface to get latest address of ModelId. */
-    public interface FastPairSignalChecker {
-        /** Gets address of ModelId. */
-        String getValidAddressForModelId(String currentDevice);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConstants.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConstants.java
deleted file mode 100644
index 008891f..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConstants.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.nearby.halfsheet.constants.FastPairConstants.PREFIX;
-
-import android.bluetooth.BluetoothDevice;
-
-/** Constants to share with other team. */
-public class FastPairConstants {
-    /** CONNECTION_ID item name for extended intent field. */
-    public static final String EXTRA_CONNECTION_ID = PREFIX + "CONNECTION_ID";
-    /** BLUETOOTH_MAC_ADDRESS item name for extended intent field. */
-    public static final String EXTRA_BLUETOOTH_MAC_ADDRESS = PREFIX + "BLUETOOTH_MAC_ADDRESS";
-    /** COMPANION_SCAN_ITEM item name for extended intent field. */
-    public static final String EXTRA_SCAN_ITEM = PREFIX + "COMPANION_SCAN_ITEM";
-    /** BOND_RESULT item name for extended intent field. */
-    public static final String EXTRA_BOND_RESULT = PREFIX + "EXTRA_BOND_RESULT";
-
-    /**
-     * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
-     * means device is BONDED but the pairing process is not triggered by FastPair.
-     */
-    public static final int BOND_RESULT_SUCCESS_WITHOUT_FP = 0;
-
-    /**
-     * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
-     * means device is BONDED and the pairing process is triggered by FastPair.
-     */
-    public static final int BOND_RESULT_SUCCESS_WITH_FP = 1;
-
-    /**
-     * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
-     * means the pairing process triggered by FastPair is failed due to the lack of PIN code.
-     */
-    public static final int BOND_RESULT_FAIL_WITH_FP_WITHOUT_PIN = 2;
-
-    /**
-     * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
-     * means the pairing process triggered by FastPair is failed due to the PIN code is not
-     * confirmed by the user.
-     */
-    public static final int BOND_RESULT_FAIL_WITH_FP_WITH_PIN_NOT_CONFIRMED = 3;
-
-    /**
-     * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
-     * means the pairing process triggered by FastPair is failed due to the user thinks the PIN is
-     * wrong.
-     */
-    public static final int BOND_RESULT_FAIL_WITH_FP_WITH_PIN_WRONG = 4;
-
-    /**
-     * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
-     * means the pairing process triggered by FastPair is failed even after the user confirmed the
-     * PIN code is correct.
-     */
-    public static final int BOND_RESULT_FAIL_WITH_FP_WITH_PIN_CORRECT = 5;
-
-    private FastPairConstants() {}
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
deleted file mode 100644
index 60afa31..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
+++ /dev/null
@@ -1,2128 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static android.bluetooth.BluetoothDevice.BOND_BONDED;
-import static android.bluetooth.BluetoothDevice.BOND_BONDING;
-import static android.bluetooth.BluetoothDevice.BOND_NONE;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.get16BitUuid;
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid;
-import static com.android.server.nearby.common.bluetooth.fastpair.Bytes.toBytes;
-import static com.android.server.nearby.common.bluetooth.fastpair.Bytes.toShorts;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Verify.verifyNotNull;
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.primitives.Bytes.concat;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.ParcelUuid;
-import android.os.SystemClock;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.annotation.WorkerThread;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
-import com.android.server.nearby.common.bluetooth.fastpair.BluetoothAudioPairer.KeyBasedPairingInfo;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.FirmwareVersionCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.NameCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService;
-import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.ActionOverBle;
-import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.HandshakeException;
-import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.HandshakeMessage;
-import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.KeyBasedPairingRequest;
-import com.android.server.nearby.common.bluetooth.fastpair.Ltv.ParseException;
-import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.fastpair.FastPairController;
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.BrEdrHandoverErrorCode;
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.ConnectErrorCode;
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.CreateBondErrorCode;
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.ErrorCode;
-import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
-
-import com.google.common.base.Ascii;
-import com.google.common.base.Preconditions;
-import com.google.common.primitives.Shorts;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.nio.ByteOrder;
-import java.security.GeneralSecurityException;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Supports Fast Pair pairing with certain Bluetooth headphones, Auto, etc.
- *
- * <p>Based on https://developers.google.com/nearby/fast-pair/spec, the pairing is constructed by
- * both BLE and BREDR connections. Example state transitions for Fast Pair 2, ie a pairing key is
- * included in the request (note: timeouts and retries are governed by flags, may change):
- *
- * <pre>
- * {@code
- *   Connect GATT
- *     A) Success -> Handshake
- *     B) Failure (3s timeout) -> Retry 2x -> end
- *
- *   Handshake
- *     A) Generate a shared secret with the headset (either using anti-spoofing key or account key)
- *       1) Account key is used directly as the key
- *       2) Anti-spoofing key is used by combining out private key with the headset's public and
- *          sending our public to the headset to combine with their private to generate a shared
- *          key. Sending our public key to headset takes ~3s.
- *     B) Write an encrypted packet to the headset containing their BLE address for verification
- *        that both sides have the same key (headset decodes this packet and checks it against their
- *        own address) (~250ms).
- *     C) Receive a response from the headset containing their public address (~250ms).
- *
- *   Discovery (for devices < Oreo)
- *     A) Success -> Create Bond
- *     B) Failure (10s timeout) -> Sleep 1s, Retry 3x -> end
- *
- *   Connect to device
- *     A) If already bonded
- *       1) Attempt directly connecting to supported profiles (A2DP, etc)
- *         a) Success -> Write Account Key
- *         b) Failure (15s timeout, usually fails within a ~2s) -> Remove bond (~1s) -> Create bond
- *     B) If not already bonded
- *       1) Create bond
- *         a) Success -> Connect profile
- *         b) Failure (15s timeout) -> Retry 2x -> end
- *       2) Connect profile
- *         a) Success -> Write account key
- *         b) Failure -> Retry -> end
- *
- *   Write account key
- *     A) Callback that pairing succeeded
- *     B) Disconnect GATT
- *     C) Reconnect GATT for secure connection
- *     D) Write account key (~3s)
- * }
- * </pre>
- *
- * The performance profiling result by {@link TimingLogger}:
- *
- * <pre>
- *   FastPairDualConnection [Exclusive time] / [Total time] ([Timestamp])
- *     Connect GATT #1 3054ms (0)
- *     Handshake 32ms / 740ms (3054)
- *       Generate key via ECDH 10ms (3054)
- *       Add salt 1ms (3067)
- *       Encrypt request 3ms (3068)
- *       Write data to GATT 692ms (3097)
- *       Wait response from GATT 0ms (3789)
- *       Decrypt response 2ms (3789)
- *     Get BR/EDR handover information via SDP 1ms (3795)
- *     Pair device #1 6ms / 4887ms (3805)
- *       Create bond 3965ms / 4881ms (3809)
- *         Exchange passkey 587ms / 915ms (7124)
- *           Encrypt passkey 6ms (7694)
- *           Send passkey to remote 290ms (7700)
- *           Wait for remote passkey 0ms (7993)
- *           Decrypt passkey 18ms (7994)
- *           Confirm the pairing: true 14ms (8025)
- *         Close BondedReceiver 1ms (8688)
- *     Connect: A2DP 19ms / 370ms (8701)
- *       Wait connection 348ms / 349ms (8720)
- *         Close ConnectedReceiver 1ms (9068)
- *       Close profile: A2DP 2ms (9069)
- *     Write account key 2ms / 789ms (9163)
- *       Encrypt key 0ms (9164)
- *       Write key via GATT #1 777ms / 783ms (9164)
- *         Close GATT 6ms (9941)
- *       Start CloudSyncing 2ms (9947)
- *       Broadcast Validator 2ms (9949)
- *   FastPairDualConnection end, 9952ms
- * </pre>
- */
-// TODO(b/203441105): break down FastPairDualConnection into smaller classes.
-public class FastPairDualConnection extends FastPairConnection {
-
-    private static final String TAG = FastPairDualConnection.class.getSimpleName();
-
-    @VisibleForTesting
-    static final int GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST = 10000;
-    @VisibleForTesting
-    static final int GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED = 20000;
-    @VisibleForTesting
-    static final int GATT_ERROR_CODE_USER_RETRY = 30000;
-    @VisibleForTesting
-    static final int GATT_ERROR_CODE_PAIR_WITH_SAME_MODEL_ID_COUNT = 40000;
-    @VisibleForTesting
-    static final int GATT_ERROR_CODE_TIMEOUT = 1000;
-
-    @Nullable
-    private static String sInitialConnectionFirmwareVersion;
-    private static final byte[] REQUESTED_SERVICES_LTV =
-            new Ltv(
-                    TransportDiscoveryService.SERVICE_UUIDS_16_BIT_LIST_TYPE,
-                    toBytes(
-                            ByteOrder.LITTLE_ENDIAN,
-                            Constants.A2DP_SINK_SERVICE_UUID,
-                            Constants.HANDS_FREE_SERVICE_UUID,
-                            Constants.HEADSET_SERVICE_UUID))
-                    .getBytes();
-    private static final byte[] TDS_CONTROL_POINT_REQUEST =
-            concat(
-                    new byte[]{
-                            TransportDiscoveryService.ControlPointCharacteristic
-                                    .ACTIVATE_TRANSPORT_OP_CODE,
-                            TransportDiscoveryService.BLUETOOTH_SIG_ORGANIZATION_ID
-                    },
-                    REQUESTED_SERVICES_LTV);
-
-    private static boolean sTestMode = false;
-
-    static void enableTestMode() {
-        sTestMode = true;
-    }
-
-    /**
-     * Operation Result Code.
-     */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(
-            value = {
-                    ResultCode.UNKNOWN,
-                    ResultCode.SUCCESS,
-                    ResultCode.OP_CODE_NOT_SUPPORTED,
-                    ResultCode.INVALID_PARAMETER,
-                    ResultCode.UNSUPPORTED_ORGANIZATION_ID,
-                    ResultCode.OPERATION_FAILED,
-            })
-
-    public @interface ResultCode {
-
-        int UNKNOWN = (byte) 0xFF;
-        int SUCCESS = (byte) 0x00;
-        int OP_CODE_NOT_SUPPORTED = (byte) 0x01;
-        int INVALID_PARAMETER = (byte) 0x02;
-        int UNSUPPORTED_ORGANIZATION_ID = (byte) 0x03;
-        int OPERATION_FAILED = (byte) 0x04;
-    }
-
-
-    private static @ResultCode int fromTdsControlPointIndication(byte[] response) {
-        return response == null || response.length < 2 ? ResultCode.UNKNOWN : from(response[1]);
-    }
-
-    private static @ResultCode int from(byte byteValue) {
-        switch (byteValue) {
-            case ResultCode.UNKNOWN:
-            case ResultCode.SUCCESS:
-            case ResultCode.OP_CODE_NOT_SUPPORTED:
-            case ResultCode.INVALID_PARAMETER:
-            case ResultCode.UNSUPPORTED_ORGANIZATION_ID:
-            case ResultCode.OPERATION_FAILED:
-                return byteValue;
-            default:
-                return ResultCode.UNKNOWN;
-        }
-    }
-
-    private static class BrEdrHandoverInformation {
-
-        private final byte[] mBluetoothAddress;
-        private final short[] mProfiles;
-
-        private BrEdrHandoverInformation(byte[] bluetoothAddress, short[] profiles) {
-            this.mBluetoothAddress = bluetoothAddress;
-
-            // For now, since we only connect to one profile, prefer A2DP Sink over headset/HFP.
-            // TODO(b/37167120): Connect to more than one profile.
-            Set<Short> profileSet = new HashSet<>(Shorts.asList(profiles));
-            if (profileSet.contains(Constants.A2DP_SINK_SERVICE_UUID)) {
-                profileSet.remove(Constants.HEADSET_SERVICE_UUID);
-                profileSet.remove(Constants.HANDS_FREE_SERVICE_UUID);
-            }
-            this.mProfiles = Shorts.toArray(profileSet);
-        }
-
-        @Override
-        public String toString() {
-            return "BrEdrHandoverInformation{"
-                    + maskBluetoothAddress(BluetoothAddress.encode(mBluetoothAddress))
-                    + ", profiles="
-                    + (mProfiles.length > 0 ? Shorts.join(",", mProfiles) : "(none)")
-                    + "}";
-        }
-    }
-
-    private final Context mContext;
-    private final Preferences mPreferences;
-    private final EventLoggerWrapper mEventLogger;
-    private final BluetoothAdapter mBluetoothAdapter =
-            checkNotNull(BluetoothAdapter.getDefaultAdapter());
-    private String mBleAddress;
-
-    private final TimingLogger mTimingLogger;
-    private GattConnectionManager mGattConnectionManager;
-    private boolean mProviderInitiatesBonding;
-    private @Nullable
-    byte[] mPairingSecret;
-    private @Nullable
-    byte[] mPairingKey;
-    @Nullable
-    private String mPublicAddress;
-    @VisibleForTesting
-    @Nullable
-    FastPairHistoryFinder mPairedHistoryFinder;
-    @Nullable
-    private String mProviderDeviceName = null;
-    private boolean mNeedUpdateProviderName = false;
-    @Nullable
-    DeviceNameReceiver mDeviceNameReceiver;
-    @Nullable
-    private HandshakeHandler mHandshakeHandlerForTest;
-    @Nullable
-    private Runnable mBeforeDirectlyConnectProfileFromCacheForTest;
-
-    public FastPairDualConnection(
-            Context context,
-            String bleAddress,
-            Preferences preferences,
-            @Nullable EventLogger eventLogger) {
-        this(context, bleAddress, preferences, eventLogger,
-                new TimingLogger("FastPairDualConnection", preferences));
-    }
-
-    @VisibleForTesting
-    FastPairDualConnection(
-            Context context,
-            String bleAddress,
-            Preferences preferences,
-            @Nullable EventLogger eventLogger,
-            TimingLogger timingLogger) {
-        this.mContext = context;
-        this.mPreferences = preferences;
-        this.mEventLogger = new EventLoggerWrapper(eventLogger);
-        this.mBleAddress = bleAddress;
-        this.mTimingLogger = timingLogger;
-    }
-
-    /**
-     * Unpairs with headphones. Synchronous: Blocks until unpaired. Throws on any error.
-     */
-    @WorkerThread
-    public void unpair(BluetoothDevice device)
-            throws ReflectionException, InterruptedException, ExecutionException, TimeoutException,
-            PairingException {
-        if (mPreferences.getExtraLoggingInformation() != null) {
-            mEventLogger
-                    .bind(mContext, device.getAddress(), mPreferences.getExtraLoggingInformation());
-        }
-        new BluetoothAudioPairer(
-                mContext,
-                device,
-                mPreferences,
-                mEventLogger,
-                /* keyBasedPairingInfo= */ null,
-                /* passkeyConfirmationHandler= */ null,
-                mTimingLogger)
-                .unpair();
-        if (mEventLogger.isBound()) {
-            mEventLogger.unbind(mContext);
-        }
-    }
-
-    /**
-     * Sets the fast pair history for identifying the provider which has paired (without being
-     * forgotten) with the primary account on the device, i.e. the history is not limited on this
-     * phone, can be on other phones with the same account. If they have already paired, Fast Pair
-     * should not generate new account key and default personalized name for it after initial pair.
-     */
-    @WorkerThread
-    public void setFastPairHistory(List<FastPairHistoryItem> fastPairHistoryItem) {
-        Log.i(TAG, "Paired history has been set.");
-        this.mPairedHistoryFinder = new FastPairHistoryFinder(fastPairHistoryItem);
-    }
-
-    /**
-     * Update the provider device name when we take provider default name and account based name
-     * into consideration.
-     */
-    public void setProviderDeviceName(String deviceName) {
-        Log.i(TAG, "Update provider device name = " + deviceName);
-        mProviderDeviceName = deviceName;
-        mNeedUpdateProviderName = true;
-    }
-
-    /**
-     * Gets the device name from the Provider (via GATT notify).
-     */
-    @Nullable
-    public String getProviderDeviceName() {
-        if (mDeviceNameReceiver == null) {
-            Log.i(TAG, "getProviderDeviceName failed, deviceNameReceiver == null.");
-            return null;
-        }
-        if (mPairingSecret == null) {
-            Log.i(TAG, "getProviderDeviceName failed, pairingSecret == null.");
-            return null;
-        }
-        String deviceName = mDeviceNameReceiver.getParsedResult(mPairingSecret);
-        Log.i(TAG, "getProviderDeviceName = " + deviceName);
-
-        return deviceName;
-    }
-
-    /**
-     * Get the existing account key of the provider, this API can be called after handshake.
-     *
-     * @return the existing account key if the provider has paired with the account before.
-     * Otherwise, return null, i.e. it is a real initial pairing.
-     */
-    @WorkerThread
-    @Nullable
-    public byte[] getExistingAccountKey() {
-        return mPairedHistoryFinder == null ? null : mPairedHistoryFinder.getExistingAccountKey();
-    }
-
-    /**
-     * Pairs with headphones. Synchronous: Blocks until paired and connected. Throws on any error.
-     *
-     * @return the secret key for the user's account, if written.
-     */
-    @WorkerThread
-    @Nullable
-    public SharedSecret pair()
-            throws BluetoothException, InterruptedException, ReflectionException, TimeoutException,
-            ExecutionException, PairingException {
-        try {
-            return pair(/*key=*/ null);
-        } catch (GeneralSecurityException e) {
-            throw new RuntimeException("Should never happen, no security key!", e);
-        }
-    }
-
-    /**
-     * Pairs with headphones. Synchronous: Blocks until paired and connected. Throws on any error.
-     *
-     * @param key can be in two different formats. If it is 16 bytes long, then it is an AES account
-     * key. Otherwise, it's a public key generated by {@link EllipticCurveDiffieHellmanExchange}.
-     * See go/fast-pair-2-spec for how each of these keys are used.
-     * @return the secret key for the user's account, if written
-     */
-    @WorkerThread
-    @Nullable
-    public SharedSecret pair(@Nullable byte[] key)
-            throws BluetoothException, InterruptedException, ReflectionException, TimeoutException,
-            ExecutionException, PairingException, GeneralSecurityException {
-        mPairingKey = key;
-        if (key != null) {
-            Log.i(TAG, "Starting to pair " + maskBluetoothAddress(mBleAddress) + ": key["
-                    + key.length + "], " + mPreferences);
-        } else {
-            Log.i(TAG, "Pairing " + maskBluetoothAddress(mBleAddress) + ": " + mPreferences);
-        }
-        if (mPreferences.getExtraLoggingInformation() != null) {
-            this.mEventLogger.bind(
-                    mContext, mBleAddress, mPreferences.getExtraLoggingInformation());
-        }
-        // Provider never initiates if key is null (Fast Pair 1.0).
-        if (key != null && mPreferences.getProviderInitiatesBondingIfSupported()) {
-            // Provider can't initiate if we can't get our own public address, so check.
-            this.mEventLogger.setCurrentEvent(EventCode.GET_LOCAL_PUBLIC_ADDRESS);
-            if (BluetoothAddress.getPublicAddress(mContext) != null) {
-                this.mEventLogger.logCurrentEventSucceeded();
-                mProviderInitiatesBonding = true;
-            } else {
-                this.mEventLogger
-                        .logCurrentEventFailed(new IllegalStateException("null bluetooth_address"));
-                Log.e(TAG,
-                        "Want provider to initiate bonding, but cannot access Bluetooth public "
-                                + "address. Falling back to initiating bonding ourselves.");
-            }
-        }
-
-        // User might be pairing with a bonded device. In this case, we just connect profile
-        // directly and finish pairing.
-        if (directConnectProfileWithCachedAddress()) {
-            callbackOnPaired();
-            mTimingLogger.dump();
-            if (mEventLogger.isBound()) {
-                mEventLogger.unbind(mContext);
-            }
-            return null;
-        }
-
-        // Lazily initialize a new connection manager for each pairing request.
-        initGattConnectionManager();
-        boolean isSecretHandshakeCompleted = true;
-
-        try {
-            if (key != null && key.length > 0) {
-                // GATT_CONNECTION_AND_SECRET_HANDSHAKE start.
-                mEventLogger.setCurrentEvent(EventCode.GATT_CONNECTION_AND_SECRET_HANDSHAKE);
-                isSecretHandshakeCompleted = false;
-                Exception lastException = null;
-                boolean lastExceptionFromHandshake = false;
-                long startTime = SystemClock.elapsedRealtime();
-                // We communicate over this connection twice for Key-based Pairing: once before
-                // bonding begins, and once during (to transfer the passkey). Empirically, keeping
-                // it alive throughout is far more reliable than disconnecting and reconnecting for
-                // each step. The while loop is for retry of GATT connection and handshake only.
-                do {
-                    boolean isHandshaking = false;
-                    try (BluetoothGattConnection connection =
-                            mGattConnectionManager
-                                    .getConnectionWithSignalLostCheck(mRescueFromError)) {
-                        mEventLogger.setCurrentEvent(EventCode.SECRET_HANDSHAKE);
-                        if (lastException != null && !lastExceptionFromHandshake) {
-                            logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_GATT, lastException,
-                                    mEventLogger);
-                            lastException = null;
-                        }
-                        try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
-                                "Handshake")) {
-                            isHandshaking = true;
-                            handshakeForKeyBasedPairing(key);
-                            // After handshake, Fast Pair has the public address of the provider, so
-                            // we can check if it has paired with the account.
-                            if (mPublicAddress != null && mPairedHistoryFinder != null) {
-                                if (mPairedHistoryFinder.isInPairedHistory(mPublicAddress)) {
-                                    Log.i(TAG, "The provider is found in paired history.");
-                                } else {
-                                    Log.i(TAG, "The provider is not found in paired history.");
-                                }
-                            }
-                        }
-                        isHandshaking = false;
-                        // SECRET_HANDSHAKE end.
-                        mEventLogger.logCurrentEventSucceeded();
-                        isSecretHandshakeCompleted = true;
-                        if (mPrepareCreateBondCallback != null) {
-                            mPrepareCreateBondCallback.run();
-                        }
-                        if (lastException != null && lastExceptionFromHandshake) {
-                            logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_HANDSHAKE_RECONNECT,
-                                    lastException, mEventLogger);
-                        }
-                        logManualRetryCounts(/* success= */ true);
-                        // GATT_CONNECTION_AND_SECRET_HANDSHAKE end.
-                        mEventLogger.logCurrentEventSucceeded();
-                        return pair(mPreferences.getEnableBrEdrHandover());
-                    } catch (SignalLostException e) {
-                        long spentTime = SystemClock.elapsedRealtime() - startTime;
-                        if (spentTime > mPreferences.getAddressRotateRetryMaxSpentTimeMs()) {
-                            Log.w(TAG, "Signal lost but already spend too much time " + spentTime
-                                    + "ms");
-                            throw e;
-                        }
-
-                        logCurrentEventFailedBySignalLost(e);
-                        lastException = (Exception) e.getCause();
-                        lastExceptionFromHandshake = isHandshaking;
-                        if (mRescueFromError != null && isHandshaking) {
-                            mRescueFromError.accept(ErrorCode.SUCCESS_SECRET_HANDSHAKE_RECONNECT);
-                        }
-                        Log.i(TAG, "Signal lost, retry");
-                        // In case we meet some GATT error which is not recoverable and fail very
-                        // quick.
-                        SystemClock.sleep(mPreferences.getPairingRetryDelayMs());
-                    } catch (SignalRotatedException e) {
-                        long spentTime = SystemClock.elapsedRealtime() - startTime;
-                        if (spentTime > mPreferences.getAddressRotateRetryMaxSpentTimeMs()) {
-                            Log.w(TAG, "Address rotated but already spend too much time "
-                                    + spentTime + "ms");
-                            throw e;
-                        }
-
-                        logCurrentEventFailedBySignalRotated(e);
-                        setBleAddress(e.getNewAddress());
-                        lastException = (Exception) e.getCause();
-                        lastExceptionFromHandshake = isHandshaking;
-                        if (mRescueFromError != null) {
-                            mRescueFromError.accept(ErrorCode.SUCCESS_ADDRESS_ROTATE);
-                        }
-                        Log.i(TAG, "Address rotated, retry");
-                    } catch (HandshakeException e) {
-                        long spentTime = SystemClock.elapsedRealtime() - startTime;
-                        if (spentTime > mPreferences
-                                .getSecretHandshakeRetryGattConnectionMaxSpentTimeMs()) {
-                            Log.w(TAG, "Secret handshake failed but already spend too much time "
-                                    + spentTime + "ms");
-                            throw e.getOriginalException();
-                        }
-                        if (mEventLogger.isCurrentEvent()) {
-                            mEventLogger.logCurrentEventFailed(e.getOriginalException());
-                        }
-                        initGattConnectionManager();
-                        lastException = e.getOriginalException();
-                        lastExceptionFromHandshake = true;
-                        if (mRescueFromError != null) {
-                            mRescueFromError.accept(ErrorCode.SUCCESS_SECRET_HANDSHAKE_RECONNECT);
-                        }
-                        Log.i(TAG, "Handshake failed, retry GATT connection");
-                    }
-                } while (mPreferences.getRetryGattConnectionAndSecretHandshake());
-            }
-            if (mPrepareCreateBondCallback != null) {
-                mPrepareCreateBondCallback.run();
-            }
-            return pair(mPreferences.getEnableBrEdrHandover());
-        } catch (SignalLostException e) {
-            logCurrentEventFailedBySignalLost(e);
-            // GATT_CONNECTION_AND_SECRET_HANDSHAKE end.
-            if (!isSecretHandshakeCompleted) {
-                logManualRetryCounts(/* success= */ false);
-                logCurrentEventFailedBySignalLost(e);
-            }
-            throw e;
-        } catch (SignalRotatedException e) {
-            logCurrentEventFailedBySignalRotated(e);
-            // GATT_CONNECTION_AND_SECRET_HANDSHAKE end.
-            if (!isSecretHandshakeCompleted) {
-                logManualRetryCounts(/* success= */ false);
-                logCurrentEventFailedBySignalRotated(e);
-            }
-            throw e;
-        } catch (BluetoothException
-                | InterruptedException
-                | ReflectionException
-                | TimeoutException
-                | ExecutionException
-                | PairingException
-                | GeneralSecurityException e) {
-            if (mEventLogger.isCurrentEvent()) {
-                mEventLogger.logCurrentEventFailed(e);
-            }
-            // GATT_CONNECTION_AND_SECRET_HANDSHAKE end.
-            if (!isSecretHandshakeCompleted) {
-                logManualRetryCounts(/* success= */ false);
-                if (mEventLogger.isCurrentEvent()) {
-                    mEventLogger.logCurrentEventFailed(e);
-                }
-            }
-            throw e;
-        } finally {
-            mTimingLogger.dump();
-            if (mEventLogger.isBound()) {
-                mEventLogger.unbind(mContext);
-            }
-        }
-    }
-
-    private boolean directConnectProfileWithCachedAddress() throws ReflectionException {
-        if (TextUtils.isEmpty(mPreferences.getCachedDeviceAddress())
-                || !mPreferences.getDirectConnectProfileIfModelIdInCache()
-                || mPreferences.getSkipConnectingProfiles()) {
-            return false;
-        }
-        Log.i(TAG, "Try to direct connect profile with cached address "
-                + maskBluetoothAddress(mPreferences.getCachedDeviceAddress()));
-        mEventLogger.setCurrentEvent(EventCode.DIRECTLY_CONNECT_PROFILE_WITH_CACHED_ADDRESS);
-        BluetoothDevice device =
-                mBluetoothAdapter.getRemoteDevice(mPreferences.getCachedDeviceAddress()).unwrap();
-        AtomicBoolean interruptConnection = new AtomicBoolean(false);
-        BroadcastReceiver receiver =
-                new BroadcastReceiver() {
-                    @Override
-                    public void onReceive(Context context, Intent intent) {
-                        if (intent == null
-                                || !BluetoothDevice.ACTION_PAIRING_REQUEST
-                                .equals(intent.getAction())) {
-                            return;
-                        }
-                        BluetoothDevice pairingDevice = intent
-                                .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-                        if (pairingDevice == null || !device.getAddress()
-                                .equals(pairingDevice.getAddress())) {
-                            return;
-                        }
-                        abortBroadcast();
-                        // Should be the clear link key case, make it fail directly to go back to
-                        // initial pairing process.
-                        pairingDevice.setPairingConfirmation(/* confirm= */ false);
-                        Log.w(TAG, "Get pairing request broadcast for device "
-                                + maskBluetoothAddress(device.getAddress())
-                                + " while try to direct connect profile with cached address, reject"
-                                + " and to go back to initial pairing process");
-                        interruptConnection.set(true);
-                    }
-                };
-        mContext.registerReceiver(receiver,
-                new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST));
-        try (ScopedTiming scopedTiming =
-                new ScopedTiming(mTimingLogger,
-                        "Connect to profile with cached address directly")) {
-            if (mBeforeDirectlyConnectProfileFromCacheForTest != null) {
-                mBeforeDirectlyConnectProfileFromCacheForTest.run();
-            }
-            attemptConnectProfiles(
-                    new BluetoothAudioPairer(
-                            mContext,
-                            device,
-                            mPreferences,
-                            mEventLogger,
-                            /* keyBasedPairingInfo= */ null,
-                            /* passkeyConfirmationHandler= */ null,
-                            mTimingLogger),
-                    maskBluetoothAddress(device),
-                    getSupportedProfiles(device),
-                    /* numConnectionAttempts= */ 1,
-                    /* enablePairingBehavior= */ false,
-                    interruptConnection);
-            Log.i(TAG,
-                    "Directly connected to " + maskBluetoothAddress(device)
-                            + "with cached address.");
-            mEventLogger.logCurrentEventSucceeded();
-            mEventLogger.setDevice(device);
-            logPairWithPossibleCachedAddress(device.getAddress());
-            return true;
-        } catch (PairingException e) {
-            if (interruptConnection.get()) {
-                Log.w(TAG, "Fail to connected to " + maskBluetoothAddress(device)
-                        + " with cached address due to link key is cleared.", e);
-                mEventLogger.logCurrentEventFailed(
-                        new ConnectException(ConnectErrorCode.LINK_KEY_CLEARED,
-                                "Link key is cleared"));
-            } else {
-                Log.w(TAG, "Fail to connected to " + maskBluetoothAddress(device)
-                        + " with cached address.", e);
-                mEventLogger.logCurrentEventFailed(e);
-            }
-            return false;
-        } finally {
-            mContext.unregisterReceiver(receiver);
-        }
-    }
-
-    /**
-     * Logs for user retry, check go/fastpairquality21q3 for more details.
-     */
-    private void logManualRetryCounts(boolean success) {
-        if (!mPreferences.getLogUserManualRetry()) {
-            return;
-        }
-
-        // We don't want to be the final event on analytics.
-        if (!mEventLogger.isCurrentEvent()) {
-            return;
-        }
-
-        mEventLogger.setCurrentEvent(EventCode.GATT_HANDSHAKE_MANUAL_RETRY_ATTEMPTS);
-        if (mPreferences.getPairFailureCounts() <= 0 && success) {
-            mEventLogger.logCurrentEventSucceeded();
-        } else {
-            int errorCode = mPreferences.getPairFailureCounts();
-            if (errorCode > 99) {
-                errorCode = 99;
-            }
-            errorCode += success ? 0 : 100;
-            // To not conflict with current error codes.
-            errorCode += GATT_ERROR_CODE_USER_RETRY;
-            mEventLogger.logCurrentEventFailed(
-                    new BluetoothGattException("Error for manual retry", errorCode));
-        }
-    }
-
-    static void logRetrySuccessEvent(
-            @EventCode int eventCode,
-            @Nullable Exception recoverFromException,
-            EventLoggerWrapper eventLogger) {
-        if (recoverFromException == null) {
-            return;
-        }
-        eventLogger.setCurrentEvent(eventCode);
-        eventLogger.logCurrentEventFailed(recoverFromException);
-    }
-
-    private void initGattConnectionManager() {
-        mGattConnectionManager =
-                new GattConnectionManager(
-                        mContext,
-                        mPreferences,
-                        mEventLogger,
-                        mBluetoothAdapter,
-                        this::toggleBluetooth,
-                        mBleAddress,
-                        mTimingLogger,
-                        mFastPairSignalChecker,
-                        isPairingWithAntiSpoofingPublicKey());
-    }
-
-    private void logCurrentEventFailedBySignalRotated(SignalRotatedException e) {
-        if (!mEventLogger.isCurrentEvent()) {
-            return;
-        }
-
-        Log.w(TAG, "BLE Address for pairing device might rotated!");
-        mEventLogger.logCurrentEventFailed(
-                new BluetoothGattException(
-                        "BLE Address for pairing device might rotated",
-                        appendMoreErrorCode(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED,
-                                e.getCause()),
-                        e));
-    }
-
-    private void logCurrentEventFailedBySignalLost(SignalLostException e) {
-        if (!mEventLogger.isCurrentEvent()) {
-            return;
-        }
-
-        Log.w(TAG, "BLE signal for pairing device might lost!");
-        mEventLogger.logCurrentEventFailed(
-                new BluetoothGattException(
-                        "BLE signal for pairing device might lost",
-                        appendMoreErrorCode(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST, e.getCause()),
-                        e));
-    }
-
-    @VisibleForTesting
-    static int appendMoreErrorCode(int masterErrorCode, @Nullable Throwable cause) {
-        if (cause instanceof BluetoothGattException) {
-            return masterErrorCode + ((BluetoothGattException) cause).getGattErrorCode();
-        } else if (cause instanceof TimeoutException
-                || cause instanceof BluetoothTimeoutException
-                || cause instanceof BluetoothOperationTimeoutException) {
-            return masterErrorCode + GATT_ERROR_CODE_TIMEOUT;
-        } else {
-            return masterErrorCode;
-        }
-    }
-
-    private void setBleAddress(String newAddress) {
-        if (TextUtils.isEmpty(newAddress) || Ascii.equalsIgnoreCase(newAddress, mBleAddress)) {
-            return;
-        }
-
-        mBleAddress = newAddress;
-
-        // Recreates a GattConnectionManager with the new address for establishing a new GATT
-        // connection later.
-        initGattConnectionManager();
-
-        mEventLogger.setDevice(mBluetoothAdapter.getRemoteDevice(mBleAddress).unwrap());
-    }
-
-    /**
-     * Gets the public address of the headset used in the connection. Before the handshake, this
-     * could be null.
-     */
-    @Nullable
-    public String getPublicAddress() {
-        return mPublicAddress;
-    }
-
-    /**
-     * Pairs with a Bluetooth device. In general, this process goes through the following steps:
-     *
-     * <ol>
-     *   <li>Get BrEdr handover information if requested
-     *   <li>Discover the device (on Android N and lower to work around a bug)
-     *   <li>Connect to the device
-     *       <ul>
-     *         <li>Attempt a direct connection to a supported profile if we're already bonded
-     *         <li>Create a new bond with the not bonded device and then connect to a supported
-     *             profile
-     *       </ul>
-     *   <li>Write the account secret
-     * </ol>
-     *
-     * <p>Blocks until paired. May take 10+ seconds, so run on a background thread.
-     */
-    @Nullable
-    private SharedSecret pair(boolean enableBrEdrHandover)
-            throws BluetoothException, InterruptedException, ReflectionException, TimeoutException,
-            ExecutionException, PairingException, GeneralSecurityException {
-        BrEdrHandoverInformation brEdrHandoverInformation = null;
-        if (enableBrEdrHandover) {
-            try (ScopedTiming scopedTiming =
-                    new ScopedTiming(mTimingLogger, "Get BR/EDR handover information via GATT")) {
-                brEdrHandoverInformation =
-                        getBrEdrHandoverInformation(mGattConnectionManager.getConnection());
-            } catch (BluetoothException | TdsException e) {
-                Log.w(TAG,
-                        "Couldn't get BR/EDR Handover info via TDS. Trying direct connect.", e);
-                mEventLogger.logCurrentEventFailed(e);
-            }
-        }
-
-        if (brEdrHandoverInformation == null) {
-            // Pair directly to the BLE address. Works if the BLE and Bluetooth Classic addresses
-            // are the same, or if we can do BLE cross-key transport.
-            brEdrHandoverInformation =
-                    new BrEdrHandoverInformation(
-                            BluetoothAddress
-                                    .decode(mPublicAddress != null ? mPublicAddress : mBleAddress),
-                            attemptGetBluetoothClassicProfiles(
-                                    mBluetoothAdapter.getRemoteDevice(mBleAddress).unwrap(),
-                                    mPreferences.getNumSdpAttempts()));
-        }
-
-        BluetoothDevice device =
-                mBluetoothAdapter.getRemoteDevice(brEdrHandoverInformation.mBluetoothAddress)
-                        .unwrap();
-        callbackOnGetAddress(device.getAddress());
-        mEventLogger.setDevice(device);
-
-        Log.i(TAG, "Pairing with " + brEdrHandoverInformation);
-        KeyBasedPairingInfo keyBasedPairingInfo =
-                mPairingSecret == null
-                        ? null
-                        : new KeyBasedPairingInfo(
-                                mPairingSecret, mGattConnectionManager, mProviderInitiatesBonding);
-
-        BluetoothAudioPairer pairer =
-                new BluetoothAudioPairer(
-                        mContext,
-                        device,
-                        mPreferences,
-                        mEventLogger,
-                        keyBasedPairingInfo,
-                        mPasskeyConfirmationHandler,
-                        mTimingLogger);
-
-        logPairWithPossibleCachedAddress(device.getAddress());
-        logPairWithModelIdInCacheAndDiscoveryFailForCachedAddress(device);
-
-        // In the case where we are already bonded, we should first just try connecting to supported
-        // profiles. If successful, then this will be much faster than recreating the bond like we
-        // normally do and we can finish early. It is also more reliable than tearing down the bond
-        // and recreating it.
-        try {
-            if (!sTestMode) {
-                attemptDirectConnectionIfBonded(device, pairer);
-            }
-            callbackOnPaired();
-            return maybeWriteAccountKey(device);
-        } catch (PairingException e) {
-            Log.i(TAG, "Failed to directly connect to supported profiles: " + e.getMessage());
-            // Catches exception when we fail to connect support profile. And makes the flow to go
-            // through step to write account key when device is bonded.
-            if (mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()
-                    && device.getBondState() == BluetoothDevice.BOND_BONDED) {
-                if (mPreferences.getSkipConnectingProfiles()
-                        && !mPreferences.getCheckBondStateWhenSkipConnectingProfiles()) {
-                    Log.i(TAG, "For notCheckBondStateWhenSkipConnectingProfiles case should do "
-                            + "re-bond");
-                } else {
-                    Log.i(TAG, "Fail to connect profile when device is bonded, still call back on"
-                            + "pair callback to show ui");
-                    callbackOnPaired();
-                    return maybeWriteAccountKey(device);
-                }
-            }
-        }
-
-        if (mPreferences.getMoreEventLogForQuality()) {
-            switch (device.getBondState()) {
-                case BOND_BONDED:
-                    mEventLogger.setCurrentEvent(EventCode.BEFORE_CREATE_BOND_BONDED);
-                    break;
-                case BOND_BONDING:
-                    mEventLogger.setCurrentEvent(EventCode.BEFORE_CREATE_BOND_BONDING);
-                    break;
-                case BOND_NONE:
-                default:
-                    mEventLogger.setCurrentEvent(EventCode.BEFORE_CREATE_BOND);
-            }
-        }
-
-        for (int i = 1; i <= mPreferences.getNumCreateBondAttempts(); i++) {
-            try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Pair device #" + i)) {
-                pairer.pair();
-                if (mPreferences.getMoreEventLogForQuality()) {
-                    // For EventCode.BEFORE_CREATE_BOND
-                    mEventLogger.logCurrentEventSucceeded();
-                }
-                break;
-            } catch (Exception e) {
-                mEventLogger.logCurrentEventFailed(e);
-                if (mPasskeyIsGotten) {
-                    Log.w(TAG,
-                            "createBond() failed because of " + e.getMessage()
-                                    + " after getting the passkey. Skip retry.");
-                    if (mPreferences.getMoreEventLogForQuality()) {
-                        // For EventCode.BEFORE_CREATE_BOND
-                        mEventLogger.logCurrentEventFailed(
-                                new CreateBondException(
-                                        CreateBondErrorCode.FAILED_BUT_ALREADY_RECEIVE_PASS_KEY,
-                                        0,
-                                        "Already get the passkey"));
-                    }
-                    break;
-                }
-                Log.e(TAG,
-                        "removeBond() or createBond() failed, attempt " + i + " of " + mPreferences
-                                .getNumCreateBondAttempts() + ". Bond state "
-                                + device.getBondState(), e);
-                if (i < mPreferences.getNumCreateBondAttempts()) {
-                    toggleBluetooth();
-
-                    // We've seen 3 createBond() failures within 100ms (!). And then success again
-                    // later (even without turning on/off bluetooth). So create some minimum break
-                    // time.
-                    Log.i(TAG, "Sleeping 1 sec after createBond() failure.");
-                    SystemClock.sleep(1000);
-                } else if (mPreferences.getMoreEventLogForQuality()) {
-                    // For EventCode.BEFORE_CREATE_BOND
-                    mEventLogger.logCurrentEventFailed(e);
-                }
-            }
-        }
-        boolean deviceCreateBondFailWithNullSecret = false;
-        if (!pairer.isPaired()) {
-            if (mPairingSecret != null) {
-                // Bonding could fail for a few different reasons here. It could be an error, an
-                // attacker may have tried to bond, or the device may not be up to spec.
-                throw new PairingException("createBond() failed, exiting connection process.");
-            } else if (mPreferences.getSkipConnectingProfiles()) {
-                throw new PairingException(
-                        "createBond() failed and skipping connecting to a profile.");
-            } else {
-                // When bond creation has failed, connecting a profile will still work most of the
-                // time for Fast Pair 1.0 devices (ie, pairing secret is null), so continue on with
-                // the spec anyways and attempt to connect supported profiles.
-                Log.w(TAG, "createBond() failed, will try connecting profiles anyway.");
-                deviceCreateBondFailWithNullSecret = true;
-            }
-        } else if (mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()) {
-            Log.i(TAG, "new flow to call on paired callback for ui when pairing step is finished");
-            callbackOnPaired();
-        }
-
-        if (!mPreferences.getSkipConnectingProfiles()) {
-            if (mPreferences.getWaitForUuidsAfterBonding()
-                    && brEdrHandoverInformation.mProfiles.length == 0) {
-                short[] supportedProfiles = getCachedUuids(device);
-                if (supportedProfiles.length == 0
-                        && mPreferences.getNumSdpAttemptsAfterBonded() > 0) {
-                    Log.i(TAG, "Found no supported profiles in UUID cache, manually trigger SDP.");
-                    attemptGetBluetoothClassicProfiles(device,
-                            mPreferences.getNumSdpAttemptsAfterBonded());
-                }
-                brEdrHandoverInformation =
-                        new BrEdrHandoverInformation(
-                                brEdrHandoverInformation.mBluetoothAddress, supportedProfiles);
-            }
-            short[] profiles = brEdrHandoverInformation.mProfiles;
-            if (profiles.length == 0) {
-                profiles = Constants.getSupportedProfiles();
-                Log.w(TAG,
-                        "Attempting to connect constants profiles, " + Arrays.toString(profiles));
-            } else {
-                Log.i(TAG, "Attempting to connect device profiles, " + Arrays.toString(profiles));
-            }
-
-            try {
-                attemptConnectProfiles(
-                        pairer,
-                        maskBluetoothAddress(device),
-                        profiles,
-                        mPreferences.getNumConnectAttempts(),
-                        /* enablePairingBehavior= */ false);
-            } catch (PairingException e) {
-                // For new pair flow to show ui, we already show success ui when finishing the
-                // createBond step. So we should catch the exception from connecting profile to
-                // avoid showing fail ui for user.
-                if (mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()
-                        && !deviceCreateBondFailWithNullSecret) {
-                    Log.i(TAG, "Fail to connect profile when device is bonded");
-                } else {
-                    throw e;
-                }
-            }
-        }
-        if (!mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()) {
-            Log.i(TAG, "original flow to call on paired callback for ui");
-            callbackOnPaired();
-        } else if (deviceCreateBondFailWithNullSecret) {
-            // This paired callback is called for device which create bond fail with null secret
-            // such as FastPair 1.0 device when directly connecting to any supported profile.
-            Log.i(TAG, "call on paired callback for ui for device with null secret without bonded "
-                    + "state");
-            callbackOnPaired();
-        }
-        if (mPreferences.getEnableFirmwareVersionCharacteristic()
-                && validateBluetoothGattCharacteristic(
-                mGattConnectionManager.getConnection(), FirmwareVersionCharacteristic.ID)) {
-            try {
-                sInitialConnectionFirmwareVersion = readFirmwareVersion();
-            } catch (BluetoothException e) {
-                Log.i(TAG, "Fast Pair: head phone does not support firmware read", e);
-            }
-        }
-
-        // Catch exception when writing account key or name fail to avoid showing pairing failure
-        // notice for user. Because device is already paired successfully based on paring step.
-        SharedSecret secret = null;
-        try {
-            secret = maybeWriteAccountKey(device);
-        } catch (InterruptedException
-                | ExecutionException
-                | TimeoutException
-                | NoSuchAlgorithmException
-                | BluetoothException e) {
-            Log.w(TAG, "Fast Pair: Got exception when writing account key or name to provider", e);
-        }
-
-        return secret;
-    }
-
-    private void logPairWithPossibleCachedAddress(String brEdrAddressForBonding) {
-        if (TextUtils.isEmpty(mPreferences.getPossibleCachedDeviceAddress())
-                || !mPreferences.getLogPairWithCachedModelId()) {
-            return;
-        }
-        mEventLogger.setCurrentEvent(EventCode.PAIR_WITH_CACHED_MODEL_ID);
-        if (Ascii.equalsIgnoreCase(
-                mPreferences.getPossibleCachedDeviceAddress(), brEdrAddressForBonding)) {
-            mEventLogger.logCurrentEventSucceeded();
-            Log.i(TAG, "Repair with possible cached device "
-                    + maskBluetoothAddress(mPreferences.getPossibleCachedDeviceAddress()));
-        } else {
-            mEventLogger.logCurrentEventFailed(
-                    new PairingException("Pairing with 2nd device with same model ID"));
-            Log.i(TAG, "Pair with a new device " + maskBluetoothAddress(brEdrAddressForBonding)
-                    + " with model ID in cache "
-                    + maskBluetoothAddress(mPreferences.getPossibleCachedDeviceAddress()));
-        }
-    }
-
-    /**
-     * Logs two type of events. First, why cachedAddress mechanism doesn't work if it's repair with
-     * bonded device case. Second, if it's not the case, log how many devices with the same model Id
-     * is already paired.
-     */
-    private void logPairWithModelIdInCacheAndDiscoveryFailForCachedAddress(BluetoothDevice device) {
-        if (!mPreferences.getLogPairWithCachedModelId()) {
-            return;
-        }
-
-        if (device.getBondState() == BOND_BONDED) {
-            if (mPreferences.getSameModelIdPairedDeviceCount() <= 0) {
-                Log.i(TAG, "Device is bonded but we don't have this model Id in cache.");
-            } else if (TextUtils.isEmpty(mPreferences.getCachedDeviceAddress())
-                    && mPreferences.getDirectConnectProfileIfModelIdInCache()
-                    && !mPreferences.getSkipConnectingProfiles()) {
-                // Pair with bonded device case. Log why the cached address is not found.
-                mEventLogger.setCurrentEvent(
-                        EventCode.DIRECTLY_CONNECT_PROFILE_WITH_CACHED_ADDRESS);
-                mEventLogger.logCurrentEventFailed(
-                        mPreferences.getIsDeviceFinishCheckAddressFromCache()
-                                ? new ConnectException(ConnectErrorCode.FAIL_TO_DISCOVERY,
-                                "Failed to discovery")
-                                : new ConnectException(
-                                        ConnectErrorCode.DISCOVERY_NOT_FINISHED,
-                                        "Discovery not finished"));
-                Log.i(TAG, "Failed to get cached address due to "
-                        + (mPreferences.getIsDeviceFinishCheckAddressFromCache()
-                        ? "Failed to discovery"
-                        : "Discovery not finished"));
-            }
-        } else if (device.getBondState() == BOND_NONE) {
-            // Pair with new device case, log how many devices with the same model id is in FastPair
-            // cache already.
-            mEventLogger.setCurrentEvent(EventCode.PAIR_WITH_NEW_MODEL);
-            if (mPreferences.getSameModelIdPairedDeviceCount() <= 0) {
-                mEventLogger.logCurrentEventSucceeded();
-            } else {
-                mEventLogger.logCurrentEventFailed(
-                        new BluetoothGattException(
-                                "Already have this model ID in cache",
-                                GATT_ERROR_CODE_PAIR_WITH_SAME_MODEL_ID_COUNT
-                                        + mPreferences.getSameModelIdPairedDeviceCount()));
-            }
-            Log.i(TAG, "This device already has " + mPreferences.getSameModelIdPairedDeviceCount()
-                    + " peripheral with the same model Id");
-        }
-    }
-
-    /**
-     * Attempts to directly connect to any supported profile if we're already bonded, this will save
-     * time over tearing down the bond and recreating it.
-     */
-    private void attemptDirectConnectionIfBonded(BluetoothDevice device,
-            BluetoothAudioPairer pairer)
-            throws PairingException {
-        if (mPreferences.getSkipConnectingProfiles()) {
-            if (mPreferences.getCheckBondStateWhenSkipConnectingProfiles()
-                    && device.getBondState() == BluetoothDevice.BOND_BONDED) {
-                Log.i(TAG, "Skipping connecting to profiles by preferences.");
-                return;
-            }
-            throw new PairingException(
-                    "Skipping connecting to profiles, no direct connection possible.");
-        } else if (!mPreferences.getAttemptDirectConnectionWhenPreviouslyBonded()
-                || device.getBondState() != BluetoothDevice.BOND_BONDED) {
-            throw new PairingException(
-                    "Not previously bonded skipping direct connection, %s", device.getBondState());
-        }
-        short[] supportedProfiles = getSupportedProfiles(device);
-        mEventLogger.setCurrentEvent(EventCode.DIRECTLY_CONNECTED_TO_PROFILE);
-        try (ScopedTiming scopedTiming =
-                new ScopedTiming(mTimingLogger, "Connect to profile directly")) {
-            attemptConnectProfiles(
-                    pairer,
-                    maskBluetoothAddress(device),
-                    supportedProfiles,
-                    mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()
-                            ? mPreferences.getNumConnectAttempts()
-                            : 1,
-                    mPreferences.getEnablePairingWhileDirectlyConnecting());
-            Log.i(TAG, "Directly connected to " + maskBluetoothAddress(device));
-            mEventLogger.logCurrentEventSucceeded();
-        } catch (PairingException e) {
-            mEventLogger.logCurrentEventFailed(e);
-            // Rethrow e so that the exception bubbles up and we continue the normal pairing
-            // process.
-            throw e;
-        }
-    }
-
-    @VisibleForTesting
-    void attemptConnectProfiles(
-            BluetoothAudioPairer pairer,
-            String deviceMaskedBluetoothAddress,
-            short[] profiles,
-            int numConnectionAttempts,
-            boolean enablePairingBehavior)
-            throws PairingException {
-        attemptConnectProfiles(
-                pairer,
-                deviceMaskedBluetoothAddress,
-                profiles,
-                numConnectionAttempts,
-                enablePairingBehavior,
-                new AtomicBoolean(false));
-    }
-
-    private void attemptConnectProfiles(
-            BluetoothAudioPairer pairer,
-            String deviceMaskedBluetoothAddress,
-            short[] profiles,
-            int numConnectionAttempts,
-            boolean enablePairingBehavior,
-            AtomicBoolean interruptConnection)
-            throws PairingException {
-        if (mPreferences.getMoreEventLogForQuality()) {
-            mEventLogger.setCurrentEvent(EventCode.BEFORE_CONNECT_PROFILE);
-        }
-        Exception lastException = null;
-        for (short profile : profiles) {
-            if (interruptConnection.get()) {
-                Log.w(TAG, "attemptConnectProfiles interrupted");
-                break;
-            }
-            if (!mPreferences.isSupportedProfile(profile)) {
-                Log.w(TAG, "Ignoring unsupported profile=" + profile);
-                continue;
-            }
-            for (int i = 1; i <= numConnectionAttempts; i++) {
-                if (interruptConnection.get()) {
-                    Log.w(TAG, "attemptConnectProfiles interrupted");
-                    break;
-                }
-                mEventLogger.setCurrentEvent(EventCode.CONNECT_PROFILE);
-                mEventLogger.setCurrentProfile(profile);
-                try {
-                    pairer.connect(profile, enablePairingBehavior);
-                    mEventLogger.logCurrentEventSucceeded();
-                    if (mPreferences.getMoreEventLogForQuality()) {
-                        // For EventCode.BEFORE_CONNECT_PROFILE
-                        mEventLogger.logCurrentEventSucceeded();
-                    }
-                    // If successful, we're done.
-                    // TODO(b/37167120): Connect to more than one profile.
-                    return;
-                } catch (InterruptedException
-                        | ReflectionException
-                        | TimeoutException
-                        | ExecutionException
-                        | ConnectException e) {
-                    Log.w(TAG,
-                            "Error connecting to profile=" + profile
-                                    + " for device=" + deviceMaskedBluetoothAddress
-                                    + " (attempt " + i + " of " + mPreferences
-                                    .getNumConnectAttempts(), e);
-                    mEventLogger.logCurrentEventFailed(e);
-                    lastException = e;
-                }
-            }
-        }
-        if (mPreferences.getMoreEventLogForQuality()) {
-            // For EventCode.BEFORE_CONNECT_PROFILE
-            if (lastException != null) {
-                mEventLogger.logCurrentEventFailed(lastException);
-            } else {
-                mEventLogger.logCurrentEventSucceeded();
-            }
-        }
-        throw new PairingException(
-                "Unable to connect to any profiles in: %s", Arrays.toString(profiles));
-    }
-
-    /**
-     * Checks whether or not an account key should be written to the device and writes it if so.
-     * This is called after handle notifying the pairedCallback that we've finished pairing, because
-     * at this point the headset is ready to use.
-     */
-    @Nullable
-    private SharedSecret maybeWriteAccountKey(BluetoothDevice device)
-            throws InterruptedException, ExecutionException, TimeoutException,
-            NoSuchAlgorithmException,
-            BluetoothException {
-        if (!sTestMode) {
-            Locator.get(mContext, FastPairController.class).setShouldUpload(false);
-        }
-        if (!shouldWriteAccountKey()) {
-            // For FastPair 2.0, here should be a subsequent pairing case.
-            return null;
-        }
-
-        // Check if it should be a subsequent pairing but go through initial pairing. If there is an
-        // existed paired history found, use the same account key instead of creating a new one.
-        byte[] accountKey =
-                mPairedHistoryFinder == null ? null : mPairedHistoryFinder.getExistingAccountKey();
-        if (accountKey == null) {
-            // It is a real initial pairing, generate a new account key for the headset.
-            try (ScopedTiming scopedTiming1 =
-                    new ScopedTiming(mTimingLogger, "Write account key")) {
-                accountKey = doWriteAccountKey(createAccountKey(), device.getAddress());
-                if (accountKey == null) {
-                    // Without writing account key back to provider, close the connection.
-                    mGattConnectionManager.closeConnection();
-                    return null;
-                }
-                if (!mPreferences.getIsRetroactivePairing()) {
-                    try (ScopedTiming scopedTiming2 = new ScopedTiming(mTimingLogger,
-                            "Start CloudSyncing")) {
-                        // Start to sync to the footprint
-                        Locator.get(mContext, FastPairController.class).setShouldUpload(true);
-                        //mContext.startService(createCloudSyncingIntent(accountKey));
-                    } catch (SecurityException e) {
-                        Log.w(TAG, "Error adding device.", e);
-                    }
-                }
-            }
-        } else if (shouldWriteAccountKeyForExistingCase(accountKey)) {
-            // There is an existing account key, but go through initial pairing, and still write the
-            // existing account key.
-            doWriteAccountKey(accountKey, device.getAddress());
-        }
-
-        // When finish writing account key in initial pairing, write new device name back to
-        // provider.
-        UUID characteristicUuid = NameCharacteristic.getId(mGattConnectionManager.getConnection());
-        if (mPreferences.getEnableNamingCharacteristic()
-                && mNeedUpdateProviderName
-                && validateBluetoothGattCharacteristic(
-                mGattConnectionManager.getConnection(), characteristicUuid)) {
-            try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
-                    "WriteNameToProvider")) {
-                writeNameToProvider(this.mProviderDeviceName, device.getAddress());
-            }
-        }
-
-        // When finish writing account key and name back to provider, close the connection.
-        mGattConnectionManager.closeConnection();
-        return SharedSecret.create(accountKey, device.getAddress());
-    }
-
-    private boolean shouldWriteAccountKey() {
-        return isWritingAccountKeyEnabled() && isPairingWithAntiSpoofingPublicKey();
-    }
-
-    private boolean isWritingAccountKeyEnabled() {
-        return mPreferences.getNumWriteAccountKeyAttempts() > 0;
-    }
-
-    private boolean isPairingWithAntiSpoofingPublicKey() {
-        return isPairingWithAntiSpoofingPublicKey(mPairingKey);
-    }
-
-    private boolean isPairingWithAntiSpoofingPublicKey(@Nullable byte[] key) {
-        return key != null && key.length == EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH;
-    }
-
-    /**
-     * Creates and writes an account key to the provided mac address.
-     */
-    @Nullable
-    private byte[] doWriteAccountKey(byte[] accountKey, String macAddress)
-            throws InterruptedException, ExecutionException, TimeoutException, BluetoothException {
-        byte[] localPairingSecret = mPairingSecret;
-        if (localPairingSecret == null) {
-            Log.w(TAG, "Pairing secret was null, account key couldn't be encrypted or written.");
-            return null;
-        }
-        if (!mPreferences.getSkipDisconnectingGattBeforeWritingAccountKey()) {
-            try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
-                    "Close GATT and sleep")) {
-                // Make a new connection instead of reusing gattConnection, because this is
-                // post-pairing and we need an encrypted connection.
-                mGattConnectionManager.closeConnection();
-                // Sleep before re-connecting to gatt, for writing account key, could increase
-                // stability.
-                Thread.sleep(mPreferences.getWriteAccountKeySleepMillis());
-            }
-        }
-
-        byte[] encryptedKey;
-        try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Encrypt key")) {
-            encryptedKey = AesEcbSingleBlockEncryption.encrypt(localPairingSecret, accountKey);
-        } catch (GeneralSecurityException e) {
-            Log.w("Failed to encrypt key.", e);
-            return null;
-        }
-
-        for (int i = 1; i <= mPreferences.getNumWriteAccountKeyAttempts(); i++) {
-            mEventLogger.setCurrentEvent(EventCode.WRITE_ACCOUNT_KEY);
-            try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
-                    "Write key via GATT #" + i)) {
-                writeAccountKey(encryptedKey, macAddress);
-                mEventLogger.logCurrentEventSucceeded();
-                return accountKey;
-            } catch (BluetoothException e) {
-                Log.w("Error writing account key attempt " + i + " of " + mPreferences
-                        .getNumWriteAccountKeyAttempts(), e);
-                mEventLogger.logCurrentEventFailed(e);
-                // Retry with a while for stability.
-                Thread.sleep(mPreferences.getWriteAccountKeySleepMillis());
-            }
-        }
-        return null;
-    }
-
-    private byte[] createAccountKey() throws NoSuchAlgorithmException {
-        return AccountKeyGenerator.createAccountKey();
-    }
-
-    @VisibleForTesting
-    boolean shouldWriteAccountKeyForExistingCase(byte[] existingAccountKey) {
-        if (!mPreferences.getKeepSameAccountKeyWrite()) {
-            Log.i(TAG,
-                    "The provider has already paired with the account, skip writing account key.");
-            return false;
-        }
-        if (existingAccountKey[0] != AccountKeyCharacteristic.TYPE) {
-            Log.i(TAG,
-                    "The provider has already paired with the account, but accountKey[0] != 0x04."
-                            + " Forget the device from the account and re-try");
-
-            return false;
-        }
-        Log.i(TAG, "The provider has already paired with the account, still write the same account "
-                + "key.");
-        return true;
-    }
-
-    /**
-     * Performs a key-based pairing request handshake to authenticate and get the remote device's
-     * public address.
-     *
-     * @param key is described in {@link #pair(byte[])}
-     */
-    @VisibleForTesting
-    SharedSecret handshakeForKeyBasedPairing(byte[] key)
-            throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
-            GeneralSecurityException, PairingException {
-        // We may also initialize gattConnectionManager of prepareForHandshake() that will be used
-        // in registerNotificationForNamePacket(), so we need to call it here.
-        HandshakeHandler handshakeHandler = prepareForHandshake();
-        KeyBasedPairingRequest.Builder keyBasedPairingRequestBuilder =
-                new KeyBasedPairingRequest.Builder()
-                        .setVerificationData(BluetoothAddress.decode(mBleAddress));
-        if (mProviderInitiatesBonding) {
-            keyBasedPairingRequestBuilder
-                    .addFlag(KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING);
-        }
-        // Seeker only request provider device name in initial pairing.
-        if (mPreferences.getEnableNamingCharacteristic() && isPairingWithAntiSpoofingPublicKey(
-                key)) {
-            keyBasedPairingRequestBuilder.addFlag(KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME);
-            // Register listener to receive name characteristic response from provider.
-            registerNotificationForNamePacket();
-        }
-        if (mPreferences.getIsRetroactivePairing()) {
-            keyBasedPairingRequestBuilder
-                    .addFlag(KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR);
-            keyBasedPairingRequestBuilder.setSeekerPublicAddress(
-                    Preconditions.checkNotNull(BluetoothAddress.getPublicAddress(mContext)));
-        }
-
-        return performHandshakeWithRetryAndSignalLostCheck(
-                handshakeHandler, key, keyBasedPairingRequestBuilder.build(), /* withRetry= */
-                true);
-    }
-
-    /**
-     * Performs an action-over-BLE request handshake for authentication, i.e. to identify the shared
-     * secret. The given key should be the account key.
-     */
-    private SharedSecret handshakeForActionOverBle(byte[] key,
-            @AdditionalDataType int additionalDataType)
-            throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
-            GeneralSecurityException, PairingException {
-        HandshakeHandler handshakeHandler = prepareForHandshake();
-        return performHandshakeWithRetryAndSignalLostCheck(
-                handshakeHandler,
-                key,
-                new ActionOverBle.Builder()
-                        .setVerificationData(BluetoothAddress.decode(mBleAddress))
-                        .setAdditionalDataType(additionalDataType)
-                        .build(),
-                /* withRetry= */ false);
-    }
-
-    private HandshakeHandler prepareForHandshake() {
-        if (mGattConnectionManager == null) {
-            mGattConnectionManager =
-                    new GattConnectionManager(
-                            mContext,
-                            mPreferences,
-                            mEventLogger,
-                            mBluetoothAdapter,
-                            this::toggleBluetooth,
-                            mBleAddress,
-                            mTimingLogger,
-                            mFastPairSignalChecker,
-                            isPairingWithAntiSpoofingPublicKey());
-        }
-        if (mHandshakeHandlerForTest != null) {
-            Log.w(TAG, "Use handshakeHandlerForTest!");
-            return verifyNotNull(mHandshakeHandlerForTest);
-        }
-        return new HandshakeHandler(
-                mGattConnectionManager, mBleAddress, mPreferences, mEventLogger,
-                mFastPairSignalChecker);
-    }
-
-    @VisibleForTesting
-    void setHandshakeHandlerForTest(@Nullable HandshakeHandler handshakeHandlerForTest) {
-        this.mHandshakeHandlerForTest = handshakeHandlerForTest;
-    }
-
-    private SharedSecret performHandshakeWithRetryAndSignalLostCheck(
-            HandshakeHandler handshakeHandler,
-            byte[] key,
-            HandshakeMessage handshakeMessage,
-            boolean withRetry)
-            throws GeneralSecurityException, ExecutionException, BluetoothException,
-            InterruptedException, TimeoutException, PairingException {
-        SharedSecret handshakeResult =
-                withRetry
-                        ? handshakeHandler.doHandshakeWithRetryAndSignalLostCheck(
-                        key, handshakeMessage, mRescueFromError)
-                        : handshakeHandler.doHandshake(key, handshakeMessage);
-        // TODO: Try to remove these two global variables, publicAddress and pairingSecret.
-        mPublicAddress = handshakeResult.getAddress();
-        mPairingSecret = handshakeResult.getKey();
-        return handshakeResult;
-    }
-
-    private void toggleBluetooth()
-            throws InterruptedException, ExecutionException, TimeoutException {
-        if (!mPreferences.getToggleBluetoothOnFailure()) {
-            return;
-        }
-
-        Log.i(TAG, "Turning Bluetooth off.");
-        mEventLogger.setCurrentEvent(EventCode.DISABLE_BLUETOOTH);
-        mBluetoothAdapter.unwrap().disable();
-        disableBle(mBluetoothAdapter.unwrap());
-        try {
-            waitForBluetoothState(android.bluetooth.BluetoothAdapter.STATE_OFF);
-            mEventLogger.logCurrentEventSucceeded();
-        } catch (TimeoutException e) {
-            mEventLogger.logCurrentEventFailed(e);
-            // Soldier on despite failing to turn off Bluetooth. We can't control whether other
-            // clients (even inside GCore) kept it enabled in BLE-only mode.
-            Log.w(TAG, "Bluetooth still on. BluetoothAdapter state="
-                    + getBleState(mBluetoothAdapter.unwrap()), e);
-        }
-
-        // Note: Intentionally don't re-enable BLE-only mode, because we don't know which app
-        // enabled it. The client app should listen to Bluetooth events and enable as necessary
-        // (because the user can toggle at any time; e.g. via Airplane mode).
-        Log.i(TAG, "Turning Bluetooth on.");
-        mEventLogger.setCurrentEvent(EventCode.ENABLE_BLUETOOTH);
-        mBluetoothAdapter.unwrap().enable();
-        waitForBluetoothState(android.bluetooth.BluetoothAdapter.STATE_ON);
-        mEventLogger.logCurrentEventSucceeded();
-    }
-
-    private void waitForBluetoothState(int state)
-            throws TimeoutException, ExecutionException, InterruptedException {
-        waitForBluetoothStateUsingPolling(state);
-    }
-
-    private void waitForBluetoothStateUsingPolling(int state) throws TimeoutException {
-        // There's a bug where we (pretty often!) never get the broadcast for STATE_ON or STATE_OFF.
-        // So poll instead.
-        long start = SystemClock.elapsedRealtime();
-        long timeoutMillis = mPreferences.getBluetoothToggleTimeoutSeconds() * 1000L;
-        while (SystemClock.elapsedRealtime() - start < timeoutMillis) {
-            if (state == getBleState(mBluetoothAdapter.unwrap())) {
-                break;
-            }
-            SystemClock.sleep(mPreferences.getBluetoothStatePollingMillis());
-        }
-
-        if (state != getBleState(mBluetoothAdapter.unwrap())) {
-            throw new TimeoutException(
-                    String.format(
-                            Locale.getDefault(),
-                            "Timed out waiting for state %d, current state is %d",
-                            state,
-                            getBleState(mBluetoothAdapter.unwrap())));
-        }
-    }
-
-    private BrEdrHandoverInformation getBrEdrHandoverInformation(BluetoothGattConnection connection)
-            throws BluetoothException, TdsException, InterruptedException, ExecutionException,
-            TimeoutException {
-        Log.i(TAG, "Connecting GATT server to BLE address=" + maskBluetoothAddress(mBleAddress));
-        Log.i(TAG, "Telling device to become discoverable");
-        mEventLogger.setCurrentEvent(EventCode.BR_EDR_HANDOVER_WRITE_CONTROL_POINT_REQUEST);
-        ChangeObserver changeObserver =
-                connection.enableNotification(
-                        TransportDiscoveryService.ID,
-                        TransportDiscoveryService.ControlPointCharacteristic.ID);
-        connection.writeCharacteristic(
-                TransportDiscoveryService.ID,
-                TransportDiscoveryService.ControlPointCharacteristic.ID,
-                TDS_CONTROL_POINT_REQUEST);
-
-        byte[] response =
-                changeObserver.waitForUpdate(
-                        TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
-        @ResultCode int resultCode = fromTdsControlPointIndication(response);
-        if (resultCode != ResultCode.SUCCESS) {
-            throw new TdsException(
-                    BrEdrHandoverErrorCode.CONTROL_POINT_RESULT_CODE_NOT_SUCCESS,
-                    "TDS Control Point result code (%s) was not success in response %s",
-                    resultCode,
-                    base16().lowerCase().encode(response));
-        }
-        mEventLogger.logCurrentEventSucceeded();
-        return new BrEdrHandoverInformation(
-                getAddressFromBrEdrConnection(connection),
-                getProfilesFromBrEdrConnection(connection));
-    }
-
-    private byte[] getAddressFromBrEdrConnection(BluetoothGattConnection connection)
-            throws BluetoothException, TdsException {
-        Log.i(TAG, "Getting Bluetooth MAC");
-        mEventLogger.setCurrentEvent(EventCode.BR_EDR_HANDOVER_READ_BLUETOOTH_MAC);
-        byte[] brHandoverData =
-                connection.readCharacteristic(
-                        TransportDiscoveryService.ID,
-                        to128BitUuid(mPreferences.getBrHandoverDataCharacteristicId()));
-        if (brHandoverData == null || brHandoverData.length < 7) {
-            throw new TdsException(
-                    BrEdrHandoverErrorCode.BLUETOOTH_MAC_INVALID,
-                    "Bluetooth MAC not contained in BR handover data: %s",
-                    brHandoverData != null ? base16().lowerCase().encode(brHandoverData)
-                            : "(none)");
-        }
-        byte[] bluetoothAddress =
-                new Bytes.Value(Arrays.copyOfRange(brHandoverData, 1, 7), ByteOrder.LITTLE_ENDIAN)
-                        .getBytes(ByteOrder.BIG_ENDIAN);
-        mEventLogger.logCurrentEventSucceeded();
-        return bluetoothAddress;
-    }
-
-    private short[] getProfilesFromBrEdrConnection(BluetoothGattConnection connection) {
-        mEventLogger.setCurrentEvent(EventCode.BR_EDR_HANDOVER_READ_TRANSPORT_BLOCK);
-        try {
-            byte[] transportBlock =
-                    connection.readDescriptor(
-                            TransportDiscoveryService.ID,
-                            to128BitUuid(mPreferences.getBluetoothSigDataCharacteristicId()),
-                            to128BitUuid(mPreferences.getBrTransportBlockDataDescriptorId()));
-            Log.i(TAG, "Got transport block: " + base16().lowerCase().encode(transportBlock));
-            short[] profiles = getSupportedProfiles(transportBlock);
-            mEventLogger.logCurrentEventSucceeded();
-            return profiles;
-        } catch (BluetoothException | TdsException | ParseException e) {
-            Log.w(TAG, "Failed to get supported profiles from transport block.", e);
-            mEventLogger.logCurrentEventFailed(e);
-        }
-        return new short[0];
-    }
-
-    @VisibleForTesting
-    boolean writeNameToProvider(@Nullable String deviceName, @Nullable String address)
-            throws InterruptedException, TimeoutException, ExecutionException {
-        if (deviceName == null || address == null) {
-            Log.i(TAG, "writeNameToProvider fail because provider name or address is null.");
-            return false;
-        }
-        if (mPairingSecret == null) {
-            Log.i(TAG, "writeNameToProvider fail because no pairingSecret.");
-            return false;
-        }
-        byte[] encryptedDeviceNamePacket;
-        try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Encode device name")) {
-            encryptedDeviceNamePacket =
-                    NamingEncoder.encodeNamingPacket(mPairingSecret, deviceName);
-        } catch (GeneralSecurityException e) {
-            Log.w(TAG, "Failed to encrypt device name.", e);
-            return false;
-        }
-
-        for (int i = 1; i <= mPreferences.getNumWriteAccountKeyAttempts(); i++) {
-            mEventLogger.setCurrentEvent(EventCode.WRITE_DEVICE_NAME);
-            try {
-                writeDeviceName(encryptedDeviceNamePacket, address);
-                mEventLogger.logCurrentEventSucceeded();
-                return true;
-            } catch (BluetoothException e) {
-                Log.w(TAG, "Error writing name attempt " + i + " of "
-                        + mPreferences.getNumWriteAccountKeyAttempts());
-                mEventLogger.logCurrentEventFailed(e);
-                // Reuses the existing preference because the same usage.
-                Thread.sleep(mPreferences.getWriteAccountKeySleepMillis());
-            }
-        }
-        return false;
-    }
-
-    private void writeAccountKey(byte[] encryptedAccountKey, String address)
-            throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
-        Log.i(TAG, "Writing account key to address=" + maskBluetoothAddress(address));
-        BluetoothGattConnection connection = mGattConnectionManager.getConnection();
-        connection.setOperationTimeout(
-                TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
-        UUID characteristicUuid = AccountKeyCharacteristic.getId(connection);
-        connection.writeCharacteristic(FastPairService.ID, characteristicUuid, encryptedAccountKey);
-        Log.i(TAG,
-                "Finished writing encrypted account key=" + base16().encode(encryptedAccountKey));
-    }
-
-    private void writeDeviceName(byte[] naming, String address)
-            throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
-        Log.i(TAG, "Writing new device name to address=" + maskBluetoothAddress(address));
-        BluetoothGattConnection connection = mGattConnectionManager.getConnection();
-        connection.setOperationTimeout(
-                TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
-        UUID characteristicUuid = NameCharacteristic.getId(connection);
-        connection.writeCharacteristic(FastPairService.ID, characteristicUuid, naming);
-        Log.i(TAG, "Finished writing new device name=" + base16().encode(naming));
-    }
-
-    /**
-     * Reads firmware version after write account key to provider since simulator is more stable to
-     * read firmware version in initial gatt connection. This function will also read firmware when
-     * detect bloomfilter. Need to verify this after real device come out. TODO(b/130592473)
-     */
-    @Nullable
-    public String readFirmwareVersion()
-            throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
-        if (!TextUtils.isEmpty(sInitialConnectionFirmwareVersion)) {
-            String result = sInitialConnectionFirmwareVersion;
-            sInitialConnectionFirmwareVersion = null;
-            return result;
-        }
-        if (mGattConnectionManager == null) {
-            mGattConnectionManager =
-                    new GattConnectionManager(
-                            mContext,
-                            mPreferences,
-                            mEventLogger,
-                            mBluetoothAdapter,
-                            this::toggleBluetooth,
-                            mBleAddress,
-                            mTimingLogger,
-                            mFastPairSignalChecker,
-                            /* setMtu= */ true);
-            mGattConnectionManager.closeConnection();
-        }
-        if (sTestMode) {
-            return null;
-        }
-        BluetoothGattConnection connection = mGattConnectionManager.getConnection();
-        connection.setOperationTimeout(
-                TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
-
-        try {
-            String firmwareVersion =
-                    new String(
-                            connection.readCharacteristic(
-                                    FastPairService.ID,
-                                    to128BitUuid(
-                                            mPreferences.getFirmwareVersionCharacteristicId())));
-            Log.i(TAG, "FastPair: Got the firmware info version number = " + firmwareVersion);
-            mGattConnectionManager.closeConnection();
-            return firmwareVersion;
-        } catch (BluetoothException e) {
-            Log.i(TAG, "FastPair: can't read firmware characteristic.", e);
-            mGattConnectionManager.closeConnection();
-            return null;
-        }
-    }
-
-    @VisibleForTesting
-    @Nullable
-    String getInitialConnectionFirmware() {
-        return sInitialConnectionFirmwareVersion;
-    }
-
-    private void registerNotificationForNamePacket()
-            throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
-        Log.i(TAG,
-                "register for the device name response from address=" + maskBluetoothAddress(
-                        mBleAddress));
-
-        BluetoothGattConnection gattConnection = mGattConnectionManager.getConnection();
-        gattConnection.setOperationTimeout(
-                TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
-        try {
-            mDeviceNameReceiver = new DeviceNameReceiver(gattConnection);
-        } catch (BluetoothException e) {
-            Log.i(TAG, "Can't register for device name response, no naming characteristic.");
-            return;
-        }
-    }
-
-    private short[] getSupportedProfiles(BluetoothDevice device) {
-        short[] supportedProfiles = getCachedUuids(device);
-        if (supportedProfiles.length == 0 && mPreferences.getNumSdpAttemptsAfterBonded() > 0) {
-            supportedProfiles =
-                    attemptGetBluetoothClassicProfiles(device,
-                            mPreferences.getNumSdpAttemptsAfterBonded());
-        }
-        if (supportedProfiles.length == 0) {
-            supportedProfiles = Constants.getSupportedProfiles();
-            Log.w(TAG, "Attempting to connect constants profiles, "
-                    + Arrays.toString(supportedProfiles));
-        } else {
-            Log.i(TAG,
-                    "Attempting to connect device profiles, " + Arrays.toString(supportedProfiles));
-        }
-        return supportedProfiles;
-    }
-
-    private static short[] getSupportedProfiles(byte[] transportBlock)
-            throws TdsException, ParseException {
-        if (transportBlock == null || transportBlock.length < 4) {
-            throw new TdsException(
-                    BrEdrHandoverErrorCode.TRANSPORT_BLOCK_INVALID,
-                    "Transport Block null or too short: %s",
-                    base16().lowerCase().encode(transportBlock));
-        }
-        int transportDataLength = transportBlock[2];
-        if (transportBlock.length < 3 + transportDataLength) {
-            throw new TdsException(
-                    BrEdrHandoverErrorCode.TRANSPORT_BLOCK_INVALID,
-                    "Transport Block has wrong length byte: %s",
-                    base16().lowerCase().encode(transportBlock));
-        }
-        byte[] transportData = Arrays.copyOfRange(transportBlock, 3, 3 + transportDataLength);
-        for (Ltv ltv : Ltv.parse(transportData)) {
-            int uuidLength = uuidLength(ltv.mType);
-            // We currently only support a single list of 2-byte UUIDs.
-            // TODO(b/37539535): Support multiple lists, and longer (32-bit, 128-bit) IDs?
-            if (uuidLength == 2) {
-                return toShorts(ByteOrder.LITTLE_ENDIAN, ltv.mValue);
-            }
-        }
-        return new short[0];
-    }
-
-    /**
-     * Returns 0 if the type is not one of the UUID list types; otherwise returns length in bytes.
-     */
-    private static int uuidLength(byte dataType) {
-        switch (dataType) {
-            case TransportDiscoveryService.SERVICE_UUIDS_16_BIT_LIST_TYPE:
-                return 2;
-            case TransportDiscoveryService.SERVICE_UUIDS_32_BIT_LIST_TYPE:
-                return 4;
-            case TransportDiscoveryService.SERVICE_UUIDS_128_BIT_LIST_TYPE:
-                return 16;
-            default:
-                return 0;
-        }
-    }
-
-    private short[] attemptGetBluetoothClassicProfiles(BluetoothDevice device, int numSdpAttempts) {
-        // The docs say that if fetchUuidsWithSdp() has an error or "takes a long time", we get an
-        // intent containing only the stuff in the cache (i.e. nothing). Retry a few times.
-        short[] supportedProfiles = null;
-        for (int i = 1; i <= numSdpAttempts; i++) {
-            mEventLogger.setCurrentEvent(EventCode.GET_PROFILES_VIA_SDP);
-            try (ScopedTiming scopedTiming =
-                    new ScopedTiming(mTimingLogger,
-                            "Get BR/EDR handover information via SDP #" + i)) {
-                supportedProfiles = getSupportedProfilesViaBluetoothClassic(device);
-            } catch (ExecutionException | InterruptedException | TimeoutException e) {
-                // Ignores and retries if needed.
-            }
-            if (supportedProfiles != null && supportedProfiles.length != 0) {
-                mEventLogger.logCurrentEventSucceeded();
-                break;
-            } else {
-                mEventLogger.logCurrentEventFailed(new TimeoutException());
-                Log.w(TAG, "SDP returned no UUIDs from " + maskBluetoothAddress(device.getAddress())
-                        + ", assuming timeout (attempt " + i + " of " + numSdpAttempts + ").");
-            }
-        }
-        return (supportedProfiles == null) ? new short[0] : supportedProfiles;
-    }
-
-    private short[] getSupportedProfilesViaBluetoothClassic(BluetoothDevice device)
-            throws ExecutionException, InterruptedException, TimeoutException {
-        Log.i(TAG, "Getting supported profiles via SDP (Bluetooth Classic) for "
-                + maskBluetoothAddress(device.getAddress()));
-        try (DeviceIntentReceiver supportedProfilesReceiver =
-                DeviceIntentReceiver.oneShotReceiver(
-                        mContext, mPreferences, device, BluetoothDevice.ACTION_UUID)) {
-            device.fetchUuidsWithSdp();
-            supportedProfilesReceiver.await(mPreferences.getSdpTimeoutSeconds(), TimeUnit.SECONDS);
-        }
-        return getCachedUuids(device);
-    }
-
-    private static short[] getCachedUuids(BluetoothDevice device) {
-        ParcelUuid[] parcelUuids = device.getUuids();
-        Log.i(TAG, "Got supported UUIDs: " + Arrays.toString(parcelUuids));
-        if (parcelUuids == null) {
-            // The OS can return null.
-            parcelUuids = new ParcelUuid[0];
-        }
-
-        List<Short> shortUuids = new ArrayList<>(parcelUuids.length);
-        for (ParcelUuid parcelUuid : parcelUuids) {
-            UUID uuid = parcelUuid.getUuid();
-            if (BluetoothUuids.is16BitUuid(uuid)) {
-                shortUuids.add(get16BitUuid(uuid));
-            }
-        }
-        return Shorts.toArray(shortUuids);
-    }
-
-    private void callbackOnPaired() {
-        if (mPairedCallback != null) {
-            mPairedCallback.onPaired(mPublicAddress != null ? mPublicAddress : mBleAddress);
-        }
-    }
-
-    private void callbackOnGetAddress(String address) {
-        if (mOnGetBluetoothAddressCallback != null) {
-            mOnGetBluetoothAddressCallback.onGetBluetoothAddress(address);
-        }
-    }
-
-    private boolean validateBluetoothGattCharacteristic(
-            BluetoothGattConnection connection, UUID characteristicUUID) {
-        try (ScopedTiming scopedTiming =
-                new ScopedTiming(mTimingLogger, "Get service characteristic list")) {
-            List<BluetoothGattCharacteristic> serviceCharacteristicList =
-                    connection.getService(FastPairService.ID).getCharacteristics();
-            for (BluetoothGattCharacteristic characteristic : serviceCharacteristicList) {
-                if (characteristicUUID.equals(characteristic.getUuid())) {
-                    Log.i(TAG, "characteristic is exists, uuid = " + characteristicUUID);
-                    return true;
-                }
-            }
-        } catch (BluetoothException e) {
-            Log.w(TAG, "Can't get service characteristic list.", e);
-        }
-        Log.i(TAG, "can't find characteristic, uuid = " + characteristicUUID);
-        return false;
-    }
-
-    // This method is only for testing to make test method block until get name response or time
-    // out.
-    /**
-     * Set name response countdown latch.
-     */
-    public void setNameResponseCountDownLatch(CountDownLatch countDownLatch) {
-        if (mDeviceNameReceiver != null) {
-            mDeviceNameReceiver.setCountDown(countDownLatch);
-            Log.v(TAG, "set up nameResponseCountDown");
-        }
-    }
-
-    private static int getBleState(android.bluetooth.BluetoothAdapter bluetoothAdapter) {
-        // Can't use the public isLeEnabled() API, because it returns false for
-        // STATE_BLE_TURNING_(ON|OFF). So if we assume false == STATE_OFF, that can be
-        // very wrong.
-        return getLeState(bluetoothAdapter);
-    }
-
-    @VisibleForTesting
-    static int getLeState(android.bluetooth.BluetoothAdapter adapter) {
-        try {
-            return (Integer) Reflect.on(adapter).withMethod("getLeState").get();
-        } catch (ReflectionException e) {
-            Log.i(TAG, "Can't call getLeState", e);
-        }
-        return adapter.getState();
-    }
-
-    private static void disableBle(android.bluetooth.BluetoothAdapter adapter) {
-        adapter.disableBLE();
-    }
-
-    /**
-     * Handle the searching of Fast Pair history. Since there is only one public address using
-     * during Fast Pair connection, {@link #isInPairedHistory(String)} only needs to be called once,
-     * then the result is kept, and call {@link #getExistingAccountKey()} to get the result.
-     */
-    @VisibleForTesting
-    static final class FastPairHistoryFinder {
-
-        private @Nullable
-        byte[] mExistingAccountKey;
-        @Nullable
-        private final List<FastPairHistoryItem> mHistoryItems;
-
-        FastPairHistoryFinder(List<FastPairHistoryItem> historyItems) {
-            this.mHistoryItems = historyItems;
-        }
-
-        @WorkerThread
-        @VisibleForTesting
-        boolean isInPairedHistory(String publicAddress) {
-            if (mHistoryItems == null || mHistoryItems.isEmpty()) {
-                return false;
-            }
-            for (FastPairHistoryItem item : mHistoryItems) {
-                if (item.isMatched(BluetoothAddress.decode(publicAddress))) {
-                    mExistingAccountKey = item.accountKey().toByteArray();
-                    return true;
-                }
-            }
-            return false;
-        }
-
-        // This function should be called after isInPairedHistory(). Or it will just return null.
-        @WorkerThread
-        @VisibleForTesting
-        @Nullable
-        byte[] getExistingAccountKey() {
-            return mExistingAccountKey;
-        }
-    }
-
-    private static final class DeviceNameReceiver {
-
-        @GuardedBy("this")
-        private @Nullable
-        byte[] mEncryptedResponse;
-
-        @GuardedBy("this")
-        @Nullable
-        private String mDecryptedDeviceName;
-
-        @Nullable
-        private CountDownLatch mResponseCountDown;
-
-        DeviceNameReceiver(BluetoothGattConnection gattConnection) throws BluetoothException {
-            UUID characteristicUuid = NameCharacteristic.getId(gattConnection);
-            ChangeObserver observer =
-                    gattConnection.enableNotification(FastPairService.ID, characteristicUuid);
-            observer.setListener(
-                    (byte[] value) -> {
-                        synchronized (DeviceNameReceiver.this) {
-                            Log.i(TAG, "DeviceNameReceiver: device name response size = "
-                                    + value.length);
-                            // We don't decrypt it here because we may not finish handshaking and
-                            // the pairing
-                            // secret is not available.
-                            mEncryptedResponse = value;
-                        }
-                        // For testing to know we get the device name from provider.
-                        if (mResponseCountDown != null) {
-                            mResponseCountDown.countDown();
-                            Log.v(TAG, "Finish nameResponseCountDown.");
-                        }
-                    });
-        }
-
-        void setCountDown(CountDownLatch countDownLatch) {
-            this.mResponseCountDown = countDownLatch;
-        }
-
-        synchronized @Nullable String getParsedResult(byte[] secret) {
-            if (mDecryptedDeviceName != null) {
-                return mDecryptedDeviceName;
-            }
-            if (mEncryptedResponse == null) {
-                Log.i(TAG, "DeviceNameReceiver: no device name sent from the Provider.");
-                return null;
-            }
-            try {
-                mDecryptedDeviceName = NamingEncoder.decodeNamingPacket(secret, mEncryptedResponse);
-                Log.i(TAG, "DeviceNameReceiver: decrypted provider's name from naming response, "
-                        + "name = " + mDecryptedDeviceName);
-            } catch (GeneralSecurityException e) {
-                Log.w(TAG, "DeviceNameReceiver: fail to parse the NameCharacteristic from provider"
-                        + ".", e);
-                return null;
-            }
-            return mDecryptedDeviceName;
-        }
-    }
-
-    static void checkFastPairSignal(
-            FastPairSignalChecker fastPairSignalChecker,
-            String currentAddress,
-            Exception originalException)
-            throws SignalLostException, SignalRotatedException {
-        String newAddress = fastPairSignalChecker.getValidAddressForModelId(currentAddress);
-        if (TextUtils.isEmpty(newAddress)) {
-            throw new SignalLostException("Signal lost", originalException);
-        } else if (!Ascii.equalsIgnoreCase(currentAddress, newAddress)) {
-            throw new SignalRotatedException("Address rotated", newAddress, originalException);
-        }
-    }
-
-    @VisibleForTesting
-    public Preferences getPreferences() {
-        return mPreferences;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItem.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItem.java
deleted file mode 100644
index e774886..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItem.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.primitives.Bytes.concat;
-
-import com.google.common.hash.Hashing;
-import com.google.protobuf.ByteString;
-
-import java.util.Arrays;
-
-/**
- * It contains the sha256 of "account key + headset's public address" to identify the headset which
- * has paired with the account. Previously, account key is the only information for Fast Pair to
- * identify the headset, but Fast Pair can't identify the headset in initial pairing, there is no
- * account key data advertising from headset.
- */
-public class FastPairHistoryItem {
-
-    private final ByteString mAccountKey;
-    private final ByteString mSha256AccountKeyPublicAddress;
-
-    FastPairHistoryItem(ByteString accountkey, ByteString sha256AccountKeyPublicAddress) {
-        mAccountKey = accountkey;
-        mSha256AccountKeyPublicAddress = sha256AccountKeyPublicAddress;
-    }
-
-    /**
-     * Creates an instance of {@link FastPairHistoryItem}.
-     *
-     * @param accountKey key of an account that has paired with the headset.
-     * @param sha256AccountKeyPublicAddress hash value of account key and headset's public address.
-     */
-    public static FastPairHistoryItem create(
-            ByteString accountKey, ByteString sha256AccountKeyPublicAddress) {
-        return new FastPairHistoryItem(accountKey, sha256AccountKeyPublicAddress);
-    }
-
-    ByteString accountKey() {
-        return mAccountKey;
-    }
-
-    ByteString sha256AccountKeyPublicAddress() {
-        return mSha256AccountKeyPublicAddress;
-    }
-
-    // Return true if the input public address is considered the same as this history item. Because
-    // of privacy concern, Fast Pair does not really store the public address, it is identified by
-    // the SHA256 of the account key and the public key.
-    final boolean isMatched(byte[] publicAddress) {
-        return Arrays.equals(
-                sha256AccountKeyPublicAddress().toByteArray(),
-                Hashing.sha256().hashBytes(concat(accountKey().toByteArray(), publicAddress))
-                        .asBytes());
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java
deleted file mode 100644
index e7ce4bf..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
-import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.logRetrySuccessEvent;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import android.content.Context;
-import android.os.SystemClock;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.core.util.Consumer;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
-import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.ErrorCode;
-import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Manager for working with Gatt connections.
- *
- * <p>This helper class allows for opening and closing GATT connections to a provided address.
- * Optionally, it can also support automatically reopening a connection in the case that it has been
- * closed when it's next needed through {@link Preferences#getAutomaticallyReconnectGattWhenNeeded}.
- */
-// TODO(b/202524672): Add class unit test.
-final class GattConnectionManager {
-
-    private static final String TAG = GattConnectionManager.class.getSimpleName();
-
-    private final Context mContext;
-    private final Preferences mPreferences;
-    private final EventLoggerWrapper mEventLogger;
-    private final BluetoothAdapter mBluetoothAdapter;
-    private final ToggleBluetoothTask mToggleBluetooth;
-    private final String mAddress;
-    private final TimingLogger mTimingLogger;
-    private final boolean mSetMtu;
-    @Nullable
-    private final FastPairConnection.FastPairSignalChecker mFastPairSignalChecker;
-    @Nullable
-    private BluetoothGattConnection mGattConnection;
-    private static boolean sTestMode = false;
-
-    static void enableTestMode() {
-        sTestMode = true;
-    }
-
-    GattConnectionManager(
-            Context context,
-            Preferences preferences,
-            EventLoggerWrapper eventLogger,
-            BluetoothAdapter bluetoothAdapter,
-            ToggleBluetoothTask toggleBluetooth,
-            String address,
-            TimingLogger timingLogger,
-            @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker,
-            boolean setMtu) {
-        this.mContext = context;
-        this.mPreferences = preferences;
-        this.mEventLogger = eventLogger;
-        this.mBluetoothAdapter = bluetoothAdapter;
-        this.mToggleBluetooth = toggleBluetooth;
-        this.mAddress = address;
-        this.mTimingLogger = timingLogger;
-        this.mFastPairSignalChecker = fastPairSignalChecker;
-        this.mSetMtu = setMtu;
-    }
-
-    /**
-     * Gets a gatt connection to address. If this connection does not exist, it creates one.
-     */
-    BluetoothGattConnection getConnection()
-            throws InterruptedException, ExecutionException, TimeoutException, BluetoothException {
-        if (mGattConnection == null) {
-            try {
-                mGattConnection =
-                        connect(mAddress, /* checkSignalWhenFail= */ false,
-                                /* rescueFromError= */ null);
-            } catch (SignalLostException | SignalRotatedException e) {
-                // Impossible to happen here because we didn't do signal check.
-                throw new ExecutionException("getConnection throws SignalLostException", e);
-            }
-        }
-        return mGattConnection;
-    }
-
-    BluetoothGattConnection getConnectionWithSignalLostCheck(
-            @Nullable Consumer<Integer> rescueFromError)
-            throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
-            SignalLostException, SignalRotatedException {
-        if (mGattConnection == null) {
-            mGattConnection = connect(mAddress, /* checkSignalWhenFail= */ true,
-                    rescueFromError);
-        }
-        return mGattConnection;
-    }
-
-    /**
-     * Closes the gatt connection when it is open.
-     */
-    void closeConnection() throws BluetoothException {
-        if (mGattConnection != null) {
-            try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Close GATT")) {
-                mGattConnection.close();
-                mGattConnection = null;
-            }
-        }
-    }
-
-    private BluetoothGattConnection connect(
-            String address, boolean checkSignalWhenFail,
-            @Nullable Consumer<Integer> rescueFromError)
-            throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
-            SignalLostException, SignalRotatedException {
-        int i = 1;
-        boolean isRecoverable = true;
-        long startElapsedRealtime = SystemClock.elapsedRealtime();
-        BluetoothException lastException = null;
-        mEventLogger.setCurrentEvent(EventCode.GATT_CONNECT);
-        while (isRecoverable) {
-            try (ScopedTiming scopedTiming =
-                    new ScopedTiming(mTimingLogger, "Connect GATT #" + i)) {
-                Log.i(TAG, "Connecting to GATT server at " + maskBluetoothAddress(address));
-                if (sTestMode) {
-                    return null;
-                }
-                BluetoothGattConnection connection =
-                        new BluetoothGattHelper(mContext, mBluetoothAdapter)
-                                .connect(
-                                        mBluetoothAdapter.getRemoteDevice(address),
-                                        getConnectionOptions(startElapsedRealtime));
-                connection.setOperationTimeout(
-                        TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
-                if (mPreferences.getAutomaticallyReconnectGattWhenNeeded()) {
-                    connection.addCloseListener(
-                            () -> {
-                                Log.i(TAG, "Gatt connection with " + maskBluetoothAddress(address)
-                                        + " closed.");
-                                mGattConnection = null;
-                            });
-                }
-                mEventLogger.logCurrentEventSucceeded();
-                if (lastException != null) {
-                    logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_GATT, lastException,
-                            mEventLogger);
-                }
-                return connection;
-            } catch (BluetoothException e) {
-                lastException = e;
-
-                boolean ableToRetry;
-                if (mPreferences.getGattConnectRetryTimeoutMillis() > 0) {
-                    ableToRetry =
-                            (SystemClock.elapsedRealtime() - startElapsedRealtime)
-                                    < mPreferences.getGattConnectRetryTimeoutMillis();
-                    Log.i(TAG, "Retry connecting GATT by timeout: " + ableToRetry);
-                } else {
-                    ableToRetry = i < mPreferences.getNumAttempts();
-                }
-
-                if (mPreferences.getRetryGattConnectionAndSecretHandshake()) {
-                    if (isNoRetryError(mPreferences, e)) {
-                        ableToRetry = false;
-                    }
-
-                    if (ableToRetry) {
-                        if (rescueFromError != null) {
-                            rescueFromError.accept(
-                                    e instanceof BluetoothOperationTimeoutException
-                                            ? ErrorCode.SUCCESS_RETRY_GATT_TIMEOUT
-                                            : ErrorCode.SUCCESS_RETRY_GATT_ERROR);
-                        }
-                        if (mFastPairSignalChecker != null && checkSignalWhenFail) {
-                            FastPairDualConnection
-                                    .checkFastPairSignal(mFastPairSignalChecker, address, e);
-                        }
-                    }
-                    isRecoverable = ableToRetry;
-                    if (ableToRetry && mPreferences.getPairingRetryDelayMs() > 0) {
-                        SystemClock.sleep(mPreferences.getPairingRetryDelayMs());
-                    }
-                } else {
-                    isRecoverable =
-                            ableToRetry
-                                    && (e instanceof BluetoothOperationTimeoutException
-                                    || e instanceof BluetoothTimeoutException
-                                    || (e instanceof BluetoothGattException
-                                    && ((BluetoothGattException) e).getGattErrorCode() == 133));
-                }
-                Log.w(TAG, "GATT connect attempt " + i + "of " + mPreferences.getNumAttempts()
-                        + " failed, " + (isRecoverable ? "recovering" : "permanently"), e);
-                if (isRecoverable) {
-                    // If we're going to retry, log failure here. If we throw, an upper level will
-                    // log it.
-                    mToggleBluetooth.toggleBluetooth();
-                    i++;
-                    mEventLogger.logCurrentEventFailed(e);
-                    mEventLogger.setCurrentEvent(EventCode.GATT_CONNECT);
-                }
-            }
-        }
-        throw checkNotNull(lastException);
-    }
-
-    static boolean isNoRetryError(Preferences preferences, BluetoothException e) {
-        return e instanceof BluetoothGattException
-                && preferences
-                .getGattConnectionAndSecretHandshakeNoRetryGattError()
-                .contains(((BluetoothGattException) e).getGattErrorCode());
-    }
-
-    @VisibleForTesting
-    long getTimeoutMs(long spentTime) {
-        long timeoutInMs;
-        if (mPreferences.getRetryGattConnectionAndSecretHandshake()) {
-            timeoutInMs =
-                    spentTime < mPreferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs()
-                            ? mPreferences.getGattConnectShortTimeoutMs()
-                            : mPreferences.getGattConnectLongTimeoutMs();
-        } else {
-            timeoutInMs = TimeUnit.SECONDS.toMillis(mPreferences.getGattConnectionTimeoutSeconds());
-        }
-        return timeoutInMs;
-    }
-
-    private ConnectionOptions getConnectionOptions(long startElapsedRealtime) {
-        return createConnectionOptions(
-                mSetMtu,
-                getTimeoutMs(SystemClock.elapsedRealtime() - startElapsedRealtime));
-    }
-
-    public static ConnectionOptions createConnectionOptions(boolean setMtu, long timeoutInMs) {
-        ConnectionOptions.Builder builder = ConnectionOptions.builder();
-        if (setMtu) {
-            // There are 3 overhead bytes added to BLE packets.
-            builder.setMtu(
-                    AES_BLOCK_LENGTH + EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH + 3);
-        }
-        builder.setConnectionTimeoutMillis(timeoutInMs);
-        return builder.build();
-    }
-
-    @VisibleForTesting
-    void setGattConnection(BluetoothGattConnection gattConnection) {
-        this.mGattConnection = gattConnection;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandler.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandler.java
deleted file mode 100644
index 984133b..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandler.java
+++ /dev/null
@@ -1,560 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.decrypt;
-import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.encrypt;
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.BLUETOOTH_ADDRESS_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.DEVICE_ACTION;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.ADDITIONAL_DATA_TYPE_INDEX;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_ADDITIONAL_DATA_INDEX;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_ADDITIONAL_DATA_LENGTH_INDEX;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_CODE_INDEX;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_GROUP_INDEX;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.FLAGS_INDEX;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.SEEKER_PUBLIC_ADDRESS_INDEX;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.TYPE_ACTION_OVER_BLE;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.TYPE_INDEX;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.TYPE_KEY_BASED_PAIRING_REQUEST;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.VERIFICATION_DATA_INDEX;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.VERIFICATION_DATA_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.logRetrySuccessEvent;
-import static com.android.server.nearby.common.bluetooth.fastpair.GattConnectionManager.isNoRetryError;
-
-import static com.google.common.base.Verify.verifyNotNull;
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.primitives.Bytes.concat;
-
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.core.util.Consumer;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request;
-import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection.SharedSecret;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.ErrorCode;
-import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
-
-import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.UUID;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Handles the handshake step of Fast Pair, the Provider's public address and the shared secret will
- * be disclosed during this step. It is the first step of all key-based operations, e.g. key-based
- * pairing and action over BLE.
- *
- * @see <a href="https://developers.google.com/nearby/fast-pair/spec#procedure">
- *     Fastpair Spec Procedure</a>
- */
-public class HandshakeHandler {
-
-    private static final String TAG = HandshakeHandler.class.getSimpleName();
-    private final GattConnectionManager mGattConnectionManager;
-    private final String mProviderBleAddress;
-    private final Preferences mPreferences;
-    private final EventLoggerWrapper mEventLogger;
-    @Nullable
-    private final FastPairConnection.FastPairSignalChecker mFastPairSignalChecker;
-
-    /**
-     * Keeps the keys used during handshaking, generated by {@link #createKey(byte[])}.
-     */
-    private static final class Keys {
-
-        private final byte[] mSharedSecret;
-        private final byte[] mPublicKey;
-
-        private Keys(byte[] sharedSecret, byte[] publicKey) {
-            this.mSharedSecret = sharedSecret;
-            this.mPublicKey = publicKey;
-        }
-    }
-
-    public HandshakeHandler(
-            GattConnectionManager gattConnectionManager,
-            String bleAddress,
-            Preferences preferences,
-            EventLoggerWrapper eventLogger,
-            @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker) {
-        this.mGattConnectionManager = gattConnectionManager;
-        this.mProviderBleAddress = bleAddress;
-        this.mPreferences = preferences;
-        this.mEventLogger = eventLogger;
-        this.mFastPairSignalChecker = fastPairSignalChecker;
-    }
-
-    /**
-     * Performs a handshake to authenticate and get the remote device's public address. Returns the
-     * AES-128 key as the shared secret for this pairing session.
-     */
-    public SharedSecret doHandshake(byte[] key, HandshakeMessage message)
-            throws GeneralSecurityException, InterruptedException, ExecutionException,
-            TimeoutException, BluetoothException, PairingException {
-        Keys keys = createKey(key);
-        Log.i(TAG,
-                "Handshake " + maskBluetoothAddress(mProviderBleAddress) + ", flags "
-                        + message.mFlags);
-        byte[] handshakeResponse =
-                processGattCommunication(
-                        createPacket(keys, message),
-                        SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
-        String providerPublicAddress = decodeResponse(keys.mSharedSecret, handshakeResponse);
-
-        return SharedSecret.create(keys.mSharedSecret, providerPublicAddress);
-    }
-
-    /**
-     * Performs a handshake to authenticate and get the remote device's public address. Returns the
-     * AES-128 key as the shared secret for this pairing session. Will retry and also performs
-     * FastPair signal check if fails.
-     */
-    public SharedSecret doHandshakeWithRetryAndSignalLostCheck(
-            byte[] key, HandshakeMessage message, @Nullable Consumer<Integer> rescueFromError)
-            throws GeneralSecurityException, InterruptedException, ExecutionException,
-            TimeoutException, BluetoothException, PairingException {
-        Keys keys = createKey(key);
-        Log.i(TAG,
-                "Handshake " + maskBluetoothAddress(mProviderBleAddress) + ", flags "
-                        + message.mFlags);
-        int retryCount = 0;
-        byte[] handshakeResponse = null;
-        long startTime = SystemClock.elapsedRealtime();
-        BluetoothException lastException = null;
-        do {
-            try {
-                mEventLogger.setCurrentEvent(EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
-                handshakeResponse =
-                        processGattCommunication(
-                                createPacket(keys, message),
-                                getTimeoutMs(SystemClock.elapsedRealtime() - startTime));
-                mEventLogger.logCurrentEventSucceeded();
-                if (lastException != null) {
-                    logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_HANDSHAKE, lastException,
-                            mEventLogger);
-                }
-            } catch (BluetoothException e) {
-                lastException = e;
-                long spentTime = SystemClock.elapsedRealtime() - startTime;
-                Log.w(TAG, "Secret handshake failed, address="
-                        + maskBluetoothAddress(mProviderBleAddress)
-                        + ", spent time=" + spentTime + "ms, retryCount=" + retryCount);
-                mEventLogger.logCurrentEventFailed(e);
-
-                if (!mPreferences.getRetryGattConnectionAndSecretHandshake()) {
-                    throw e;
-                }
-
-                if (spentTime > mPreferences.getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs()) {
-                    Log.w(TAG, "Spent too long time for handshake, timeInMs=" + spentTime);
-                    throw e;
-                }
-                if (isNoRetryError(mPreferences, e)) {
-                    throw e;
-                }
-
-                if (mFastPairSignalChecker != null) {
-                    FastPairDualConnection
-                            .checkFastPairSignal(mFastPairSignalChecker, mProviderBleAddress, e);
-                }
-                retryCount++;
-                if (retryCount > mPreferences.getSecretHandshakeRetryAttempts()
-                        || ((e instanceof BluetoothOperationTimeoutException)
-                        && !mPreferences.getRetrySecretHandshakeTimeout())) {
-                    throw new HandshakeException("Fail on handshake!", e);
-                }
-                if (rescueFromError != null) {
-                    rescueFromError.accept(
-                            (e instanceof BluetoothTimeoutException
-                                    || e instanceof BluetoothOperationTimeoutException)
-                                    ? ErrorCode.SUCCESS_RETRY_SECRET_HANDSHAKE_TIMEOUT
-                                    : ErrorCode.SUCCESS_RETRY_SECRET_HANDSHAKE_ERROR);
-                }
-            }
-        } while (mPreferences.getRetryGattConnectionAndSecretHandshake()
-                && handshakeResponse == null);
-        if (retryCount > 0) {
-            Log.i(TAG, "Secret handshake failed but restored by retry, retry count=" + retryCount);
-        }
-        String providerPublicAddress =
-                decodeResponse(keys.mSharedSecret, verifyNotNull(handshakeResponse));
-
-        return SharedSecret.create(keys.mSharedSecret, providerPublicAddress);
-    }
-
-    @VisibleForTesting
-    long getTimeoutMs(long spentTime) {
-        if (!mPreferences.getRetryGattConnectionAndSecretHandshake()) {
-            return SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds());
-        } else {
-            return spentTime < mPreferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
-                    ? mPreferences.getSecretHandshakeShortTimeoutMs()
-                    : mPreferences.getSecretHandshakeLongTimeoutMs();
-        }
-    }
-
-    /**
-     * If the given key is an ecc-256 public key (currently, we are using secp256r1), the shared
-     * secret is generated by ECDH; if the input key is AES-128 key (should be the account key),
-     * then it is the shared secret.
-     */
-    private Keys createKey(byte[] key) throws GeneralSecurityException {
-        if (key.length == EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH) {
-            EllipticCurveDiffieHellmanExchange exchange = EllipticCurveDiffieHellmanExchange
-                    .create();
-            byte[] publicKey = exchange.getPublicKey();
-            if (publicKey != null) {
-                Log.i(TAG, "Handshake " + maskBluetoothAddress(mProviderBleAddress)
-                        + ", generates key by ECDH.");
-            } else {
-                throw new GeneralSecurityException("Failed to do ECDH.");
-            }
-            return new Keys(exchange.generateSecret(key), publicKey);
-        } else if (key.length == AesEcbSingleBlockEncryption.KEY_LENGTH) {
-            Log.i(TAG, "Handshake " + maskBluetoothAddress(mProviderBleAddress)
-                    + ", using the given secret.");
-            return new Keys(key, new byte[0]);
-        } else {
-            throw new GeneralSecurityException("Key length is not correct: " + key.length);
-        }
-    }
-
-    private static byte[] createPacket(Keys keys, HandshakeMessage message)
-            throws GeneralSecurityException {
-        byte[] encryptedMessage = encrypt(keys.mSharedSecret, message.getBytes());
-        return concat(encryptedMessage, keys.mPublicKey);
-    }
-
-    private byte[] processGattCommunication(byte[] packet, long gattOperationTimeoutMS)
-            throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
-        BluetoothGattConnection gattConnection = mGattConnectionManager.getConnection();
-        gattConnection.setOperationTimeout(gattOperationTimeoutMS);
-        UUID characteristicUuid = KeyBasedPairingCharacteristic.getId(gattConnection);
-        ChangeObserver changeObserver =
-                gattConnection.enableNotification(FastPairService.ID, characteristicUuid);
-
-        Log.i(TAG,
-                "Writing handshake packet to address=" + maskBluetoothAddress(mProviderBleAddress));
-        gattConnection.writeCharacteristic(FastPairService.ID, characteristicUuid, packet);
-        Log.i(TAG, "Waiting handshake packet from address=" + maskBluetoothAddress(
-                mProviderBleAddress));
-        return changeObserver.waitForUpdate(gattOperationTimeoutMS);
-    }
-
-    private String decodeResponse(byte[] sharedSecret, byte[] response)
-            throws PairingException, GeneralSecurityException {
-        if (response.length != AES_BLOCK_LENGTH) {
-            throw new PairingException(
-                    "Handshake failed because of incorrect response: " + base16().encode(response));
-        }
-        // 1 byte type, 6 bytes public address, remainder random salt.
-        byte[] decryptedResponse = decrypt(sharedSecret, response);
-        if (decryptedResponse[0] != KeyBasedPairingCharacteristic.Response.TYPE) {
-            throw new PairingException(
-                    "Handshake response type incorrect: " + decryptedResponse[0]);
-        }
-        String address = BluetoothAddress.encode(Arrays.copyOfRange(decryptedResponse, 1, 7));
-        Log.i(TAG, "Handshake success with public " + maskBluetoothAddress(address) + ", ble "
-                + maskBluetoothAddress(mProviderBleAddress));
-        return address;
-    }
-
-    /**
-     * The base class for handshake message that contains the common data: message type, flags and
-     * verification data.
-     */
-    abstract static class HandshakeMessage {
-
-        final byte mType;
-        final byte mFlags;
-        private final byte[] mVerificationData;
-
-        HandshakeMessage(Builder<?> builder) {
-            this.mType = builder.mType;
-            this.mVerificationData = builder.mVerificationData;
-            this.mFlags = builder.mFlags;
-        }
-
-        abstract static class Builder<T extends Builder<T>> {
-
-            byte mType;
-            byte mFlags;
-            private byte[] mVerificationData;
-
-            abstract T getThis();
-
-            T setVerificationData(byte[] verificationData) {
-                if (verificationData.length != BLUETOOTH_ADDRESS_LENGTH) {
-                    throw new IllegalArgumentException(
-                            "Incorrect verification data length: " + verificationData.length + ".");
-                }
-                this.mVerificationData = verificationData;
-                return getThis();
-            }
-        }
-
-        /**
-         * Constructs the base handshake message according to the format of Fast Pair spec.
-         */
-        byte[] constructBaseBytes() {
-            byte[] rawMessage = new byte[Request.SIZE];
-            new SecureRandom().nextBytes(rawMessage);
-            rawMessage[TYPE_INDEX] = mType;
-            rawMessage[FLAGS_INDEX] = mFlags;
-
-            System.arraycopy(
-                    mVerificationData,
-                    /* srcPos= */ 0,
-                    rawMessage,
-                    VERIFICATION_DATA_INDEX,
-                    VERIFICATION_DATA_LENGTH);
-            return rawMessage;
-        }
-
-        /**
-         * Returns the raw handshake message.
-         */
-        abstract byte[] getBytes();
-    }
-
-    /**
-     * Extends {@link HandshakeMessage} and contains the required data for key-based pairing
-     * request.
-     */
-    public static class KeyBasedPairingRequest extends HandshakeMessage {
-
-        @Nullable
-        private final byte[] mSeekerPublicAddress;
-
-        private KeyBasedPairingRequest(Builder builder) {
-            super(builder);
-            this.mSeekerPublicAddress = builder.mSeekerPublicAddress;
-        }
-
-        @Override
-        byte[] getBytes() {
-            byte[] rawMessage = constructBaseBytes();
-            if (mSeekerPublicAddress != null) {
-                System.arraycopy(
-                        mSeekerPublicAddress,
-                        /* srcPos= */ 0,
-                        rawMessage,
-                        SEEKER_PUBLIC_ADDRESS_INDEX,
-                        BLUETOOTH_ADDRESS_LENGTH);
-            }
-            Log.i(TAG,
-                    "Handshake Message: type (" + rawMessage[TYPE_INDEX] + "), flag ("
-                            + rawMessage[FLAGS_INDEX] + ").");
-            return rawMessage;
-        }
-
-        /**
-         * Builder class for key-based pairing request.
-         */
-        public static class Builder extends HandshakeMessage.Builder<Builder> {
-
-            @Nullable
-            private byte[] mSeekerPublicAddress;
-
-            /**
-             * Adds flags without changing other flags.
-             */
-            public Builder addFlag(@KeyBasedPairingRequestFlag int flag) {
-                this.mFlags |= (byte) flag;
-                return this;
-            }
-
-            /**
-             * Set seeker's public address.
-             */
-            public Builder setSeekerPublicAddress(byte[] seekerPublicAddress) {
-                this.mSeekerPublicAddress = seekerPublicAddress;
-                return this;
-            }
-
-            /**
-             * Buulds KeyBasedPairigRequest.
-             */
-            public KeyBasedPairingRequest build() {
-                mType = TYPE_KEY_BASED_PAIRING_REQUEST;
-                return new KeyBasedPairingRequest(this);
-            }
-
-            @Override
-            Builder getThis() {
-                return this;
-            }
-        }
-    }
-
-    /**
-     * Extends {@link HandshakeMessage} and contains the required data for action over BLE request.
-     */
-    public static class ActionOverBle extends HandshakeMessage {
-
-        private final byte mEventGroup;
-        private final byte mEventCode;
-        @Nullable
-        private final byte[] mEventData;
-        private final byte mAdditionalDataType;
-
-        private ActionOverBle(Builder builder) {
-            super(builder);
-            this.mEventGroup = builder.mEventGroup;
-            this.mEventCode = builder.mEventCode;
-            this.mEventData = builder.mEventData;
-            this.mAdditionalDataType = builder.mAdditionalDataType;
-        }
-
-        @Override
-        byte[] getBytes() {
-            byte[] rawMessage = constructBaseBytes();
-            StringBuilder stringBuilder =
-                    new StringBuilder(
-                            String.format(
-                                    "type (%02X), flag (%02X)", rawMessage[TYPE_INDEX],
-                                    rawMessage[FLAGS_INDEX]));
-            if ((mFlags & (byte) DEVICE_ACTION) != 0) {
-                rawMessage[EVENT_GROUP_INDEX] = mEventGroup;
-                rawMessage[EVENT_CODE_INDEX] = mEventCode;
-
-                if (mEventData != null) {
-                    rawMessage[EVENT_ADDITIONAL_DATA_LENGTH_INDEX] = (byte) mEventData.length;
-                    System.arraycopy(
-                            mEventData,
-                            /* srcPos= */ 0,
-                            rawMessage,
-                            EVENT_ADDITIONAL_DATA_INDEX,
-                            mEventData.length);
-                } else {
-                    rawMessage[EVENT_ADDITIONAL_DATA_LENGTH_INDEX] = (byte) 0;
-                }
-                stringBuilder.append(
-                        String.format(
-                                ", group(%02X), code(%02X), length(%02X)",
-                                rawMessage[EVENT_GROUP_INDEX],
-                                rawMessage[EVENT_CODE_INDEX],
-                                rawMessage[EVENT_ADDITIONAL_DATA_LENGTH_INDEX]));
-            }
-            if ((mFlags & (byte) ADDITIONAL_DATA_CHARACTERISTIC) != 0) {
-                rawMessage[ADDITIONAL_DATA_TYPE_INDEX] = mAdditionalDataType;
-                stringBuilder.append(
-                        String.format(", data id(%02X)", rawMessage[ADDITIONAL_DATA_TYPE_INDEX]));
-            }
-            Log.i(TAG, "Handshake Message: " + stringBuilder);
-            return rawMessage;
-        }
-
-        /**
-         * Builder class for action over BLE request.
-         */
-        public static class Builder extends HandshakeMessage.Builder<Builder> {
-
-            private byte mEventGroup;
-            private byte mEventCode;
-            @Nullable
-            private byte[] mEventData;
-            private byte mAdditionalDataType;
-
-            // Adds a flag to this handshake message. This can be called repeatedly for adding
-            // different preference.
-
-            /**
-             * Adds flag without changing other flags.
-             */
-            public Builder addFlag(@ActionOverBleFlag int flag) {
-                this.mFlags |= (byte) flag;
-                return this;
-            }
-
-            /**
-             * Set event group and event code.
-             */
-            public Builder setEvent(int eventGroup, int eventCode) {
-                this.mFlags |= (byte) DEVICE_ACTION;
-                this.mEventGroup = (byte) (eventGroup & 0xFF);
-                this.mEventCode = (byte) (eventCode & 0xFF);
-                return this;
-            }
-
-            /**
-             * Set event additional data.
-             */
-            public Builder setEventAdditionalData(byte[] data) {
-                this.mEventData = data;
-                return this;
-            }
-
-            /**
-             * Set event additional data type.
-             */
-            public Builder setAdditionalDataType(@AdditionalDataType int additionalDataType) {
-                this.mFlags |= (byte) ADDITIONAL_DATA_CHARACTERISTIC;
-                this.mAdditionalDataType = (byte) additionalDataType;
-                return this;
-            }
-
-            @Override
-            Builder getThis() {
-                return this;
-            }
-
-            ActionOverBle build() {
-                mType = TYPE_ACTION_OVER_BLE;
-                return new ActionOverBle(this);
-            }
-        }
-    }
-
-    /**
-     * Exception for handshake failure.
-     */
-    public static class HandshakeException extends PairingException {
-
-        private final BluetoothException mOriginalException;
-
-        @VisibleForTesting
-        HandshakeException(String format, BluetoothException e) {
-            super(format);
-            mOriginalException = e;
-        }
-
-        public BluetoothException getOriginalException() {
-            return mOriginalException;
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPiece.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPiece.java
deleted file mode 100644
index 26ff79f..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPiece.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import androidx.annotation.Nullable;
-import androidx.core.content.FileProvider;
-
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * This class is subclass of real headset. It contains image url, battery value and charging
- * status.
- */
-public class HeadsetPiece implements Parcelable {
-    private int mLowLevelThreshold;
-    private int mBatteryLevel;
-    private String mImageUrl;
-    private boolean mCharging;
-    private Uri mImageContentUri;
-
-    private HeadsetPiece(
-            int lowLevelThreshold,
-            int batteryLevel,
-            String imageUrl,
-            boolean charging,
-            @Nullable Uri imageContentUri) {
-        this.mLowLevelThreshold = lowLevelThreshold;
-        this.mBatteryLevel = batteryLevel;
-        this.mImageUrl = imageUrl;
-        this.mCharging = charging;
-        this.mImageContentUri = imageContentUri;
-    }
-
-    /**
-     * Returns a builder of HeadsetPiece.
-     */
-    public static HeadsetPiece.Builder builder() {
-        return new HeadsetPiece.Builder();
-    }
-
-    /**
-     * The low level threshold.
-     */
-    public int lowLevelThreshold() {
-        return mLowLevelThreshold;
-    }
-
-    /**
-     * The battery level.
-     */
-    public int batteryLevel() {
-        return mBatteryLevel;
-    }
-
-    /**
-     * The web URL of the image.
-     */
-    public String imageUrl() {
-        return mImageUrl;
-    }
-
-    /**
-     * Whether the headset is charging.
-     */
-    public boolean charging() {
-        return mCharging;
-    }
-
-    /**
-     * The content Uri of the image if it could be downloaded from the web URL and generated through
-     * {@link FileProvider#getUriForFile} successfully, otherwise null.
-     */
-    @Nullable
-    public Uri imageContentUri() {
-        return mImageContentUri;
-    }
-
-    /**
-     * @return whether battery is low or not.
-     */
-    public boolean isBatteryLow() {
-        return batteryLevel() <= lowLevelThreshold() && batteryLevel() >= 0 && !charging();
-    }
-
-    @Override
-    public String toString() {
-        return "HeadsetPiece{"
-                + "lowLevelThreshold=" + mLowLevelThreshold + ", "
-                + "batteryLevel=" + mBatteryLevel + ", "
-                + "imageUrl=" + mImageUrl + ", "
-                + "charging=" + mCharging + ", "
-                + "imageContentUri=" + mImageContentUri
-                + "}";
-    }
-
-    /**
-     * Builder function for headset piece.
-     */
-    public static class Builder {
-        private int mLowLevelThreshold;
-        private int mBatteryLevel;
-        private String mImageUrl;
-        private boolean mCharging;
-        private Uri mImageContentUri;
-
-        /**
-         * Set low level threshold.
-         */
-        public HeadsetPiece.Builder setLowLevelThreshold(int lowLevelThreshold) {
-            this.mLowLevelThreshold = lowLevelThreshold;
-            return this;
-        }
-
-        /**
-         * Set battery level.
-         */
-        public HeadsetPiece.Builder setBatteryLevel(int level) {
-            this.mBatteryLevel = level;
-            return this;
-        }
-
-        /**
-         * Set image url.
-         */
-        public HeadsetPiece.Builder setImageUrl(String url) {
-            this.mImageUrl = url;
-            return this;
-        }
-
-        /**
-         * Set charging.
-         */
-        public HeadsetPiece.Builder setCharging(boolean charging) {
-            this.mCharging = charging;
-            return this;
-        }
-
-        /**
-         * Set image content Uri.
-         */
-        public HeadsetPiece.Builder setImageContentUri(Uri uri) {
-            this.mImageContentUri = uri;
-            return this;
-        }
-
-        /**
-         * Builds HeadSetPiece.
-         */
-        public HeadsetPiece build() {
-            return new HeadsetPiece(mLowLevelThreshold, mBatteryLevel, mImageUrl, mCharging,
-                    mImageContentUri);
-        }
-    }
-
-    @Override
-    public final void writeToParcel(Parcel dest, int flags) {
-        dest.writeString(imageUrl());
-        dest.writeInt(lowLevelThreshold());
-        dest.writeInt(batteryLevel());
-        // Writes 1 if charging, otherwise 0.
-        dest.writeByte((byte) (charging() ? 1 : 0));
-        dest.writeParcelable(imageContentUri(), flags);
-    }
-
-    @Override
-    public final int describeContents() {
-        return 0;
-    }
-
-    public static final Creator<HeadsetPiece> CREATOR =
-            new Creator<HeadsetPiece>() {
-                @Override
-                public HeadsetPiece createFromParcel(Parcel in) {
-                    String imageUrl = in.readString();
-                    return HeadsetPiece.builder()
-                            .setImageUrl(imageUrl != null ? imageUrl : "")
-                            .setLowLevelThreshold(in.readInt())
-                            .setBatteryLevel(in.readInt())
-                            .setCharging(in.readByte() != 0)
-                            .setImageContentUri(in.readParcelable(Uri.class.getClassLoader()))
-                            .build();
-                }
-
-                @Override
-                public HeadsetPiece[] newArray(int size) {
-                    return new HeadsetPiece[size];
-                }
-            };
-
-    @Override
-    public final int hashCode() {
-        return Arrays.hashCode(
-                new Object[]{
-                        lowLevelThreshold(), batteryLevel(), imageUrl(), charging(),
-                        imageContentUri()
-                });
-    }
-
-    @Override
-    public final boolean equals(@Nullable Object other) {
-        if (other == null) {
-            return false;
-        }
-
-        if (this == other) {
-            return true;
-        }
-
-        if (!(other instanceof HeadsetPiece)) {
-            return false;
-        }
-
-        HeadsetPiece that = (HeadsetPiece) other;
-        return lowLevelThreshold() == that.lowLevelThreshold()
-                && batteryLevel() == that.batteryLevel()
-                && Objects.equals(imageUrl(), that.imageUrl())
-                && charging() == that.charging()
-                && Objects.equals(imageContentUri(), that.imageContentUri());
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256.java
deleted file mode 100644
index cc7a300..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.KEY_LENGTH;
-
-import androidx.annotation.VisibleForTesting;
-
-import java.security.GeneralSecurityException;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * HMAC-SHA256 utility used to generate key-SHA256 based message authentication code. This is
- * specific for Fast Pair GATT connection exchanging data to verify both the data integrity and the
- * authentication of a message. It is defined as:
- *
- * <ol>
- *   <li>SHA256(concat((key ^ opad),SHA256(concat((key ^ ipad), data)))), where
- *   <li>key is the given secret extended to 64 bytes by concat(secret, ZEROS).
- *   <li>opad is 64 bytes outer padding, consisting of repeated bytes valued 0x5c.
- *   <li>ipad is 64 bytes inner padding, consisting of repeated bytes valued 0x36.
- * </ol>
- *
- */
-final class HmacSha256 {
-    @VisibleForTesting static final int HMAC_SHA256_BLOCK_SIZE = 64;
-
-    private HmacSha256() {}
-
-    /**
-     * Generates the HMAC for given parameters, this is specific for Fast Pair GATT connection
-     * exchanging data which is encrypted using AES-CTR.
-     *
-     * @param secret 16 bytes shared secret.
-     * @param data the data encrypted using AES-CTR and the given nonce.
-     * @return HMAC-SHA256 result.
-     */
-    static byte[] build(byte[] secret, byte[] data) throws GeneralSecurityException {
-        // Currently we only accept AES-128 key here, the second check is to secure we won't
-        // modify KEY_LENGTH to > HMAC_SHA256_BLOCK_SIZE by mistake.
-        if (secret.length != KEY_LENGTH) {
-            throw new GeneralSecurityException("Incorrect key length, should be the AES-128 key.");
-        }
-        if (KEY_LENGTH > HMAC_SHA256_BLOCK_SIZE) {
-            throw new GeneralSecurityException("KEY_LENGTH > HMAC_SHA256_BLOCK_SIZE!");
-        }
-
-        return buildWith64BytesKey(secret, data);
-    }
-
-    /**
-     * Generates the HMAC for given parameters, this is specific for Fast Pair GATT connection
-     * exchanging data which is encrypted using AES-CTR.
-     *
-     * @param secret 16 bytes shared secret.
-     * @param data the data encrypted using AES-CTR and the given nonce.
-     * @return HMAC-SHA256 result.
-     */
-    static byte[] buildWith64BytesKey(byte[] secret, byte[] data) throws GeneralSecurityException {
-        if (secret.length > HMAC_SHA256_BLOCK_SIZE) {
-            throw new GeneralSecurityException("KEY_LENGTH > HMAC_SHA256_BLOCK_SIZE!");
-        }
-
-        Mac mac = Mac.getInstance("HmacSHA256");
-        SecretKeySpec keySpec = new SecretKeySpec(secret, "HmacSHA256");
-        mac.init(keySpec);
-
-        return mac.doFinal(data);
-    }
-
-    /**
-     * Constant-time HMAC comparison to prevent a possible timing attack, e.g. time the same MAC
-     * with all different first byte for a given ciphertext, the right one will take longer as it
-     * will fail on the second byte's verification.
-     *
-     * @param hmac1 HMAC want to be compared with.
-     * @param hmac2 HMAC want to be compared with.
-     * @return true if and ony if the give 2 HMACs are identical and non-null.
-     */
-    static boolean compareTwoHMACs(byte[] hmac1, byte[] hmac2) {
-        if (hmac1 == null || hmac2 == null) {
-            return false;
-        }
-
-        if (hmac1.length != hmac2.length) {
-            return false;
-        }
-        // This is for constant-time comparison, don't optimize it.
-        int res = 0;
-        for (int i = 0; i < hmac1.length; i++) {
-            res |= hmac1[i] ^ hmac2[i];
-        }
-        return res == 0;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Ltv.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Ltv.java
deleted file mode 100644
index 88c9484..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Ltv.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.io.BaseEncoding.base16;
-
-import com.google.common.primitives.Bytes;
-import com.google.errorprone.annotations.FormatMethod;
-import com.google.errorprone.annotations.FormatString;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * A length, type, value (LTV) data block.
- */
-public class Ltv {
-
-    private static final int SIZE_OF_LEN_TYPE = 2;
-
-    final byte mType;
-    final byte[] mValue;
-
-    /**
-     * Thrown if there's an error during {@link #parse}.
-     */
-    public static class ParseException extends Exception {
-
-        @FormatMethod
-        private ParseException(@FormatString String format, Object... objects) {
-            super(String.format(format, objects));
-        }
-    }
-
-    /**
-     * Constructor.
-     */
-    public Ltv(byte type, byte... value) {
-        this.mType = type;
-        this.mValue = value;
-    }
-
-    /**
-     * Parses a list of LTV blocks out of the input byte block.
-     */
-    static List<Ltv> parse(byte[] bytes) throws ParseException {
-        List<Ltv> ltvs = new ArrayList<>();
-        // The "+ 2" is for the length and type bytes.
-        for (int valueLength, i = 0; i < bytes.length; i += SIZE_OF_LEN_TYPE + valueLength) {
-            // - 1 since the length in the packet includes the type byte.
-            valueLength = bytes[i] - 1;
-            if (valueLength < 0 || bytes.length < i + SIZE_OF_LEN_TYPE + valueLength) {
-                throw new ParseException(
-                        "Wrong length=%d at index=%d in LTVs=%s", bytes[i], i,
-                        base16().encode(bytes));
-            }
-            ltvs.add(new Ltv(bytes[i + 1], Arrays.copyOfRange(bytes, i + SIZE_OF_LEN_TYPE,
-                    i + SIZE_OF_LEN_TYPE + valueLength)));
-        }
-        return ltvs;
-    }
-
-    /**
-     * Returns an LTV block, where length is mValue.length + 1 (for the type byte).
-     */
-    public byte[] getBytes() {
-        return Bytes.concat(new byte[]{(byte) (mValue.length + 1), mType}, mValue);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java
deleted file mode 100644
index b04cf73..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.generateNonce;
-
-import static com.google.common.primitives.Bytes.concat;
-
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-
-/**
- * Message stream utilities for encoding raw packet with HMAC.
- *
- * <p>Encoded packet is:
- *
- * <ol>
- *   <li>Packet[0 - (data length - 1)]: the raw data.
- *   <li>Packet[data length - (data length + 7)]: the 8-byte message nonce.
- *   <li>Packet[(data length + 8) - (data length + 15)]: the 8-byte of HMAC.
- * </ol>
- */
-public class MessageStreamHmacEncoder {
-    public static final int EXTRACT_HMAC_SIZE = 8;
-    public static final int SECTION_NONCE_LENGTH = 8;
-
-    private MessageStreamHmacEncoder() {}
-
-    /** Encodes Message Packet. */
-    public static byte[] encodeMessagePacket(byte[] accountKey, byte[] sectionNonce, byte[] data)
-            throws GeneralSecurityException {
-        checkAccountKeyAndSectionNonce(accountKey, sectionNonce);
-
-        if (data == null || data.length == 0) {
-            throw new GeneralSecurityException("No input data for encodeMessagePacket");
-        }
-
-        byte[] messageNonce = generateNonce();
-        byte[] extractedHmac =
-                Arrays.copyOf(
-                        HmacSha256.buildWith64BytesKey(
-                                accountKey, concat(sectionNonce, messageNonce, data)),
-                        EXTRACT_HMAC_SIZE);
-
-        return concat(data, messageNonce, extractedHmac);
-    }
-
-    /** Verifies Hmac. */
-    public static boolean verifyHmac(byte[] accountKey, byte[] sectionNonce, byte[] data)
-            throws GeneralSecurityException {
-        checkAccountKeyAndSectionNonce(accountKey, sectionNonce);
-        if (data == null) {
-            throw new GeneralSecurityException("data is null");
-        }
-        if (data.length <= EXTRACT_HMAC_SIZE + SECTION_NONCE_LENGTH) {
-            throw new GeneralSecurityException("data.length too short");
-        }
-
-        byte[] hmac = Arrays.copyOfRange(data, data.length - EXTRACT_HMAC_SIZE, data.length);
-        byte[] messageNonce =
-                Arrays.copyOfRange(
-                        data,
-                        data.length - EXTRACT_HMAC_SIZE - SECTION_NONCE_LENGTH,
-                        data.length - EXTRACT_HMAC_SIZE);
-        byte[] rawData = Arrays.copyOf(
-                data, data.length - EXTRACT_HMAC_SIZE - SECTION_NONCE_LENGTH);
-        return Arrays.equals(
-                Arrays.copyOf(
-                        HmacSha256.buildWith64BytesKey(
-                                accountKey, concat(sectionNonce, messageNonce, rawData)),
-                        EXTRACT_HMAC_SIZE),
-                hmac);
-    }
-
-    private static void checkAccountKeyAndSectionNonce(byte[] accountKey, byte[] sectionNonce)
-            throws GeneralSecurityException {
-        if (accountKey == null || accountKey.length == 0) {
-            throw new GeneralSecurityException(
-                    "Incorrect accountKey for encoding message packet, accountKey.length = "
-                            + (accountKey == null ? "NULL" : accountKey.length));
-        }
-
-        if (sectionNonce == null || sectionNonce.length != SECTION_NONCE_LENGTH) {
-            throw new GeneralSecurityException(
-                    "Incorrect sectionNonce for encoding message packet, sectionNonce.length = "
-                            + (sectionNonce == null ? "NULL" : sectionNonce.length));
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoder.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoder.java
deleted file mode 100644
index 1521be6..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoder.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE;
-
-import static com.google.common.primitives.Bytes.concat;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import android.annotation.TargetApi;
-import android.os.Build.VERSION_CODES;
-
-import com.google.common.base.Utf8;
-
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-
-/**
- * Naming utilities for encoding naming packet, decoding naming packet and verifying both the data
- * integrity and the authentication of a message by checking HMAC.
- *
- * <p>Naming packet is:
- *
- * <ol>
- *   <li>Naming_Packet[0 - 7]: the first 8-byte of HMAC.
- *   <li>Naming_Packet[8 - var]: the encrypted name (with 8-byte nonce appended to the front).
- * </ol>
- */
-@TargetApi(VERSION_CODES.M)
-public final class NamingEncoder {
-
-    static final int EXTRACT_HMAC_SIZE = 8;
-    static final int MAX_LENGTH_OF_NAME = 48;
-
-    private NamingEncoder() {
-    }
-
-    /**
-     * Encodes the name to naming packet by the given secret.
-     *
-     * @param secret AES-128 key for encryption.
-     * @param name the given name to be encoded.
-     * @return the encrypted data with the 8-byte extracted HMAC appended to the front.
-     * @throws GeneralSecurityException if the given key or name is invalid for encoding.
-     */
-    public static byte[] encodeNamingPacket(byte[] secret, String name)
-            throws GeneralSecurityException {
-        if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) {
-            throw new GeneralSecurityException(
-                    "Incorrect secret for encoding name packet, secret.length = "
-                            + (secret == null ? "NULL" : secret.length));
-        }
-
-        if ((name == null) || (name.length() == 0) || (Utf8.encodedLength(name)
-                > MAX_LENGTH_OF_NAME)) {
-            throw new GeneralSecurityException(
-                    "Invalid name for encoding name packet, Utf8.encodedLength(name) = "
-                            + (name == null ? "NULL" : Utf8.encodedLength(name)));
-        }
-
-        byte[] encryptedData = AesCtrMultipleBlockEncryption.encrypt(secret, name.getBytes(UTF_8));
-        byte[] extractedHmac =
-                Arrays.copyOf(HmacSha256.build(secret, encryptedData), EXTRACT_HMAC_SIZE);
-
-        return concat(extractedHmac, encryptedData);
-    }
-
-    /**
-     * Decodes the name from naming packet by the given secret.
-     *
-     * @param secret AES-128 key used in the encryption to decrypt data.
-     * @param namingPacket naming packet which is encoded by the given secret..
-     * @return the name decoded from the given packet.
-     * @throws GeneralSecurityException if the given key or naming packet is invalid for decoding.
-     */
-    public static String decodeNamingPacket(byte[] secret, byte[] namingPacket)
-            throws GeneralSecurityException {
-        if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) {
-            throw new GeneralSecurityException(
-                    "Incorrect secret for decoding name packet, secret.length = "
-                            + (secret == null ? "NULL" : secret.length));
-        }
-        if (namingPacket == null
-                || namingPacket.length <= EXTRACT_HMAC_SIZE
-                || namingPacket.length > (MAX_LENGTH_OF_NAME + EXTRACT_HMAC_SIZE + NONCE_SIZE)) {
-            throw new GeneralSecurityException(
-                    "Naming packet size is incorrect, namingPacket.length is "
-                            + (namingPacket == null ? "NULL" : namingPacket.length));
-        }
-
-        if (!verifyHmac(secret, namingPacket)) {
-            throw new GeneralSecurityException(
-                    "Verify HMAC failed, could be incorrect key or naming packet.");
-        }
-        byte[] encryptedData = Arrays
-                .copyOfRange(namingPacket, EXTRACT_HMAC_SIZE, namingPacket.length);
-        return new String(AesCtrMultipleBlockEncryption.decrypt(secret, encryptedData), UTF_8);
-    }
-
-    // Computes the HMAC of the given key and name, and compares the first 8-byte of the HMAC result
-    // with the one from name packet. Must call constant-time comparison to prevent a possible
-    // timing attack, e.g. time the same MAC with all different first byte for a given ciphertext,
-    // the right one will take longer as it will fail on the second byte's verification.
-    private static boolean verifyHmac(byte[] key, byte[] namingPacket)
-            throws GeneralSecurityException {
-        byte[] packetHmac = Arrays.copyOfRange(namingPacket, /* from= */ 0, EXTRACT_HMAC_SIZE);
-        byte[] encryptedData = Arrays
-                .copyOfRange(namingPacket, EXTRACT_HMAC_SIZE, namingPacket.length);
-        byte[] computedHmac = Arrays
-                .copyOf(HmacSha256.build(key, encryptedData), EXTRACT_HMAC_SIZE);
-
-        return HmacSha256.compareTwoHMACs(packetHmac, computedHmac);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingException.java
deleted file mode 100644
index 722dc85..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-/** Base class for pairing exceptions. */
-// TODO(b/200594968): convert exceptions into error codes to save memory.
-public class PairingException extends Exception {
-    PairingException(String format, Object... objects) {
-        super(String.format(format, objects));
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListener.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListener.java
deleted file mode 100644
index 270cb42..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListener.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import androidx.annotation.IntDef;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/** Callback interface for pairing progress. */
-public interface PairingProgressListener {
-
-    /** Fast Pair Bond State. */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(
-            value = {
-                    PairingEvent.START,
-                    PairingEvent.SUCCESS,
-                    PairingEvent.FAILED,
-                    PairingEvent.UNKNOWN,
-            })
-    public @interface PairingEvent {
-        int START = 0;
-        int SUCCESS = 1;
-        int FAILED = 2;
-        int UNKNOWN = 3;
-    }
-
-    /** Returns enum based on the ordinal index. */
-    static @PairingEvent int fromOrdinal(int ordinal) {
-        switch (ordinal) {
-            case 0:
-                return PairingEvent.START;
-            case 1:
-                return PairingEvent.SUCCESS;
-            case 2:
-                return PairingEvent.FAILED;
-            case 3:
-                return PairingEvent.UNKNOWN;
-            default:
-                return PairingEvent.UNKNOWN;
-        }
-    }
-
-    /** Callback function upon pairing progress update. */
-    void onPairingProgressUpdating(@PairingEvent int event, String message);
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PasskeyConfirmationHandler.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PasskeyConfirmationHandler.java
deleted file mode 100644
index f5807a3..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PasskeyConfirmationHandler.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import android.bluetooth.BluetoothDevice;
-
-/** Interface for getting the passkey confirmation request. */
-public interface PasskeyConfirmationHandler {
-    /** Called when getting the passkey confirmation request while pairing. */
-    void onPasskeyConfirmation(BluetoothDevice device, int passkey);
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Preferences.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Preferences.java
deleted file mode 100644
index eb5bad5..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Preferences.java
+++ /dev/null
@@ -1,2216 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.get16BitUuid;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.FirmwareVersionCharacteristic;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.common.primitives.Shorts;
-
-import java.nio.ByteOrder;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * Preferences that tweak the Fast Pairing process: timeouts, number of retries... All preferences
- * have default values which should be reasonable for all clients.
- */
-public class Preferences {
-
-    private final int mGattOperationTimeoutSeconds;
-    private final int mGattConnectionTimeoutSeconds;
-    private final int mBluetoothToggleTimeoutSeconds;
-    private final int mBluetoothToggleSleepSeconds;
-    private final int mClassicDiscoveryTimeoutSeconds;
-    private final int mNumDiscoverAttempts;
-    private final int mDiscoveryRetrySleepSeconds;
-    private final boolean mIgnoreDiscoveryError;
-    private final int mSdpTimeoutSeconds;
-    private final int mNumSdpAttempts;
-    private final int mNumCreateBondAttempts;
-    private final int mNumConnectAttempts;
-    private final int mNumWriteAccountKeyAttempts;
-    private final boolean mToggleBluetoothOnFailure;
-    private final boolean mBluetoothStateUsesPolling;
-    private final int mBluetoothStatePollingMillis;
-    private final int mNumAttempts;
-    private final boolean mEnableBrEdrHandover;
-    private final short mBrHandoverDataCharacteristicId;
-    private final short mBluetoothSigDataCharacteristicId;
-    private final short mFirmwareVersionCharacteristicId;
-    private final short mBrTransportBlockDataDescriptorId;
-    private final boolean mWaitForUuidsAfterBonding;
-    private final boolean mReceiveUuidsAndBondedEventBeforeClose;
-    private final int mRemoveBondTimeoutSeconds;
-    private final int mRemoveBondSleepMillis;
-    private final int mCreateBondTimeoutSeconds;
-    private final int mHidCreateBondTimeoutSeconds;
-    private final int mProxyTimeoutSeconds;
-    private final boolean mRejectPhonebookAccess;
-    private final boolean mRejectMessageAccess;
-    private final boolean mRejectSimAccess;
-    private final int mWriteAccountKeySleepMillis;
-    private final boolean mSkipDisconnectingGattBeforeWritingAccountKey;
-    private final boolean mMoreEventLogForQuality;
-    private final boolean mRetryGattConnectionAndSecretHandshake;
-    private final long mGattConnectShortTimeoutMs;
-    private final long mGattConnectLongTimeoutMs;
-    private final long mGattConnectShortTimeoutRetryMaxSpentTimeMs;
-    private final long mAddressRotateRetryMaxSpentTimeMs;
-    private final long mPairingRetryDelayMs;
-    private final long mSecretHandshakeShortTimeoutMs;
-    private final long mSecretHandshakeLongTimeoutMs;
-    private final long mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs;
-    private final long mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs;
-    private final long mSecretHandshakeRetryAttempts;
-    private final long mSecretHandshakeRetryGattConnectionMaxSpentTimeMs;
-    private final long mSignalLostRetryMaxSpentTimeMs;
-    private final ImmutableSet<Integer> mGattConnectionAndSecretHandshakeNoRetryGattError;
-    private final boolean mRetrySecretHandshakeTimeout;
-    private final boolean mLogUserManualRetry;
-    private final int mPairFailureCounts;
-    private final String mCachedDeviceAddress;
-    private final String mPossibleCachedDeviceAddress;
-    private final int mSameModelIdPairedDeviceCount;
-    private final boolean mIsDeviceFinishCheckAddressFromCache;
-    private final boolean mLogPairWithCachedModelId;
-    private final boolean mDirectConnectProfileIfModelIdInCache;
-    private final boolean mAcceptPasskey;
-    private final byte[] mSupportedProfileUuids;
-    private final boolean mProviderInitiatesBondingIfSupported;
-    private final boolean mAttemptDirectConnectionWhenPreviouslyBonded;
-    private final boolean mAutomaticallyReconnectGattWhenNeeded;
-    private final boolean mSkipConnectingProfiles;
-    private final boolean mIgnoreUuidTimeoutAfterBonded;
-    private final boolean mSpecifyCreateBondTransportType;
-    private final int mCreateBondTransportType;
-    private final boolean mIncreaseIntentFilterPriority;
-    private final boolean mEvaluatePerformance;
-    private final Preferences.ExtraLoggingInformation mExtraLoggingInformation;
-    private final boolean mEnableNamingCharacteristic;
-    private final boolean mEnableFirmwareVersionCharacteristic;
-    private final boolean mKeepSameAccountKeyWrite;
-    private final boolean mIsRetroactivePairing;
-    private final int mNumSdpAttemptsAfterBonded;
-    private final boolean mSupportHidDevice;
-    private final boolean mEnablePairingWhileDirectlyConnecting;
-    private final boolean mAcceptConsentForFastPairOne;
-    private final int mGattConnectRetryTimeoutMillis;
-    private final boolean mEnable128BitCustomGattCharacteristicsId;
-    private final boolean mEnableSendExceptionStepToValidator;
-    private final boolean mEnableAdditionalDataTypeWhenActionOverBle;
-    private final boolean mCheckBondStateWhenSkipConnectingProfiles;
-    private final boolean mHandlePasskeyConfirmationByUi;
-    private final boolean mEnablePairFlowShowUiWithoutProfileConnection;
-
-    private Preferences(
-            int gattOperationTimeoutSeconds,
-            int gattConnectionTimeoutSeconds,
-            int bluetoothToggleTimeoutSeconds,
-            int bluetoothToggleSleepSeconds,
-            int classicDiscoveryTimeoutSeconds,
-            int numDiscoverAttempts,
-            int discoveryRetrySleepSeconds,
-            boolean ignoreDiscoveryError,
-            int sdpTimeoutSeconds,
-            int numSdpAttempts,
-            int numCreateBondAttempts,
-            int numConnectAttempts,
-            int numWriteAccountKeyAttempts,
-            boolean toggleBluetoothOnFailure,
-            boolean bluetoothStateUsesPolling,
-            int bluetoothStatePollingMillis,
-            int numAttempts,
-            boolean enableBrEdrHandover,
-            short brHandoverDataCharacteristicId,
-            short bluetoothSigDataCharacteristicId,
-            short firmwareVersionCharacteristicId,
-            short brTransportBlockDataDescriptorId,
-            boolean waitForUuidsAfterBonding,
-            boolean receiveUuidsAndBondedEventBeforeClose,
-            int removeBondTimeoutSeconds,
-            int removeBondSleepMillis,
-            int createBondTimeoutSeconds,
-            int hidCreateBondTimeoutSeconds,
-            int proxyTimeoutSeconds,
-            boolean rejectPhonebookAccess,
-            boolean rejectMessageAccess,
-            boolean rejectSimAccess,
-            int writeAccountKeySleepMillis,
-            boolean skipDisconnectingGattBeforeWritingAccountKey,
-            boolean moreEventLogForQuality,
-            boolean retryGattConnectionAndSecretHandshake,
-            long gattConnectShortTimeoutMs,
-            long gattConnectLongTimeoutMs,
-            long gattConnectShortTimeoutRetryMaxSpentTimeMs,
-            long addressRotateRetryMaxSpentTimeMs,
-            long pairingRetryDelayMs,
-            long secretHandshakeShortTimeoutMs,
-            long secretHandshakeLongTimeoutMs,
-            long secretHandshakeShortTimeoutRetryMaxSpentTimeMs,
-            long secretHandshakeLongTimeoutRetryMaxSpentTimeMs,
-            long secretHandshakeRetryAttempts,
-            long secretHandshakeRetryGattConnectionMaxSpentTimeMs,
-            long signalLostRetryMaxSpentTimeMs,
-            ImmutableSet<Integer> gattConnectionAndSecretHandshakeNoRetryGattError,
-            boolean retrySecretHandshakeTimeout,
-            boolean logUserManualRetry,
-            int pairFailureCounts,
-            String cachedDeviceAddress,
-            String possibleCachedDeviceAddress,
-            int sameModelIdPairedDeviceCount,
-            boolean isDeviceFinishCheckAddressFromCache,
-            boolean logPairWithCachedModelId,
-            boolean directConnectProfileIfModelIdInCache,
-            boolean acceptPasskey,
-            byte[] supportedProfileUuids,
-            boolean providerInitiatesBondingIfSupported,
-            boolean attemptDirectConnectionWhenPreviouslyBonded,
-            boolean automaticallyReconnectGattWhenNeeded,
-            boolean skipConnectingProfiles,
-            boolean ignoreUuidTimeoutAfterBonded,
-            boolean specifyCreateBondTransportType,
-            int createBondTransportType,
-            boolean increaseIntentFilterPriority,
-            boolean evaluatePerformance,
-            @Nullable Preferences.ExtraLoggingInformation extraLoggingInformation,
-            boolean enableNamingCharacteristic,
-            boolean enableFirmwareVersionCharacteristic,
-            boolean keepSameAccountKeyWrite,
-            boolean isRetroactivePairing,
-            int numSdpAttemptsAfterBonded,
-            boolean supportHidDevice,
-            boolean enablePairingWhileDirectlyConnecting,
-            boolean acceptConsentForFastPairOne,
-            int gattConnectRetryTimeoutMillis,
-            boolean enable128BitCustomGattCharacteristicsId,
-            boolean enableSendExceptionStepToValidator,
-            boolean enableAdditionalDataTypeWhenActionOverBle,
-            boolean checkBondStateWhenSkipConnectingProfiles,
-            boolean handlePasskeyConfirmationByUi,
-            boolean enablePairFlowShowUiWithoutProfileConnection) {
-        this.mGattOperationTimeoutSeconds = gattOperationTimeoutSeconds;
-        this.mGattConnectionTimeoutSeconds = gattConnectionTimeoutSeconds;
-        this.mBluetoothToggleTimeoutSeconds = bluetoothToggleTimeoutSeconds;
-        this.mBluetoothToggleSleepSeconds = bluetoothToggleSleepSeconds;
-        this.mClassicDiscoveryTimeoutSeconds = classicDiscoveryTimeoutSeconds;
-        this.mNumDiscoverAttempts = numDiscoverAttempts;
-        this.mDiscoveryRetrySleepSeconds = discoveryRetrySleepSeconds;
-        this.mIgnoreDiscoveryError = ignoreDiscoveryError;
-        this.mSdpTimeoutSeconds = sdpTimeoutSeconds;
-        this.mNumSdpAttempts = numSdpAttempts;
-        this.mNumCreateBondAttempts = numCreateBondAttempts;
-        this.mNumConnectAttempts = numConnectAttempts;
-        this.mNumWriteAccountKeyAttempts = numWriteAccountKeyAttempts;
-        this.mToggleBluetoothOnFailure = toggleBluetoothOnFailure;
-        this.mBluetoothStateUsesPolling = bluetoothStateUsesPolling;
-        this.mBluetoothStatePollingMillis = bluetoothStatePollingMillis;
-        this.mNumAttempts = numAttempts;
-        this.mEnableBrEdrHandover = enableBrEdrHandover;
-        this.mBrHandoverDataCharacteristicId = brHandoverDataCharacteristicId;
-        this.mBluetoothSigDataCharacteristicId = bluetoothSigDataCharacteristicId;
-        this.mFirmwareVersionCharacteristicId = firmwareVersionCharacteristicId;
-        this.mBrTransportBlockDataDescriptorId = brTransportBlockDataDescriptorId;
-        this.mWaitForUuidsAfterBonding = waitForUuidsAfterBonding;
-        this.mReceiveUuidsAndBondedEventBeforeClose = receiveUuidsAndBondedEventBeforeClose;
-        this.mRemoveBondTimeoutSeconds = removeBondTimeoutSeconds;
-        this.mRemoveBondSleepMillis = removeBondSleepMillis;
-        this.mCreateBondTimeoutSeconds = createBondTimeoutSeconds;
-        this.mHidCreateBondTimeoutSeconds = hidCreateBondTimeoutSeconds;
-        this.mProxyTimeoutSeconds = proxyTimeoutSeconds;
-        this.mRejectPhonebookAccess = rejectPhonebookAccess;
-        this.mRejectMessageAccess = rejectMessageAccess;
-        this.mRejectSimAccess = rejectSimAccess;
-        this.mWriteAccountKeySleepMillis = writeAccountKeySleepMillis;
-        this.mSkipDisconnectingGattBeforeWritingAccountKey =
-                skipDisconnectingGattBeforeWritingAccountKey;
-        this.mMoreEventLogForQuality = moreEventLogForQuality;
-        this.mRetryGattConnectionAndSecretHandshake = retryGattConnectionAndSecretHandshake;
-        this.mGattConnectShortTimeoutMs = gattConnectShortTimeoutMs;
-        this.mGattConnectLongTimeoutMs = gattConnectLongTimeoutMs;
-        this.mGattConnectShortTimeoutRetryMaxSpentTimeMs =
-                gattConnectShortTimeoutRetryMaxSpentTimeMs;
-        this.mAddressRotateRetryMaxSpentTimeMs = addressRotateRetryMaxSpentTimeMs;
-        this.mPairingRetryDelayMs = pairingRetryDelayMs;
-        this.mSecretHandshakeShortTimeoutMs = secretHandshakeShortTimeoutMs;
-        this.mSecretHandshakeLongTimeoutMs = secretHandshakeLongTimeoutMs;
-        this.mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs =
-                secretHandshakeShortTimeoutRetryMaxSpentTimeMs;
-        this.mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs =
-                secretHandshakeLongTimeoutRetryMaxSpentTimeMs;
-        this.mSecretHandshakeRetryAttempts = secretHandshakeRetryAttempts;
-        this.mSecretHandshakeRetryGattConnectionMaxSpentTimeMs =
-                secretHandshakeRetryGattConnectionMaxSpentTimeMs;
-        this.mSignalLostRetryMaxSpentTimeMs = signalLostRetryMaxSpentTimeMs;
-        this.mGattConnectionAndSecretHandshakeNoRetryGattError =
-                gattConnectionAndSecretHandshakeNoRetryGattError;
-        this.mRetrySecretHandshakeTimeout = retrySecretHandshakeTimeout;
-        this.mLogUserManualRetry = logUserManualRetry;
-        this.mPairFailureCounts = pairFailureCounts;
-        this.mCachedDeviceAddress = cachedDeviceAddress;
-        this.mPossibleCachedDeviceAddress = possibleCachedDeviceAddress;
-        this.mSameModelIdPairedDeviceCount = sameModelIdPairedDeviceCount;
-        this.mIsDeviceFinishCheckAddressFromCache = isDeviceFinishCheckAddressFromCache;
-        this.mLogPairWithCachedModelId = logPairWithCachedModelId;
-        this.mDirectConnectProfileIfModelIdInCache = directConnectProfileIfModelIdInCache;
-        this.mAcceptPasskey = acceptPasskey;
-        this.mSupportedProfileUuids = supportedProfileUuids;
-        this.mProviderInitiatesBondingIfSupported = providerInitiatesBondingIfSupported;
-        this.mAttemptDirectConnectionWhenPreviouslyBonded =
-                attemptDirectConnectionWhenPreviouslyBonded;
-        this.mAutomaticallyReconnectGattWhenNeeded = automaticallyReconnectGattWhenNeeded;
-        this.mSkipConnectingProfiles = skipConnectingProfiles;
-        this.mIgnoreUuidTimeoutAfterBonded = ignoreUuidTimeoutAfterBonded;
-        this.mSpecifyCreateBondTransportType = specifyCreateBondTransportType;
-        this.mCreateBondTransportType = createBondTransportType;
-        this.mIncreaseIntentFilterPriority = increaseIntentFilterPriority;
-        this.mEvaluatePerformance = evaluatePerformance;
-        this.mExtraLoggingInformation = extraLoggingInformation;
-        this.mEnableNamingCharacteristic = enableNamingCharacteristic;
-        this.mEnableFirmwareVersionCharacteristic = enableFirmwareVersionCharacteristic;
-        this.mKeepSameAccountKeyWrite = keepSameAccountKeyWrite;
-        this.mIsRetroactivePairing = isRetroactivePairing;
-        this.mNumSdpAttemptsAfterBonded = numSdpAttemptsAfterBonded;
-        this.mSupportHidDevice = supportHidDevice;
-        this.mEnablePairingWhileDirectlyConnecting = enablePairingWhileDirectlyConnecting;
-        this.mAcceptConsentForFastPairOne = acceptConsentForFastPairOne;
-        this.mGattConnectRetryTimeoutMillis = gattConnectRetryTimeoutMillis;
-        this.mEnable128BitCustomGattCharacteristicsId = enable128BitCustomGattCharacteristicsId;
-        this.mEnableSendExceptionStepToValidator = enableSendExceptionStepToValidator;
-        this.mEnableAdditionalDataTypeWhenActionOverBle = enableAdditionalDataTypeWhenActionOverBle;
-        this.mCheckBondStateWhenSkipConnectingProfiles = checkBondStateWhenSkipConnectingProfiles;
-        this.mHandlePasskeyConfirmationByUi = handlePasskeyConfirmationByUi;
-        this.mEnablePairFlowShowUiWithoutProfileConnection =
-                enablePairFlowShowUiWithoutProfileConnection;
-    }
-
-    /**
-     * Timeout for each GATT operation (not for the whole pairing process).
-     */
-    public int getGattOperationTimeoutSeconds() {
-        return mGattOperationTimeoutSeconds;
-    }
-
-    /**
-     * Timeout for Gatt connection operation.
-     */
-    public int getGattConnectionTimeoutSeconds() {
-        return mGattConnectionTimeoutSeconds;
-    }
-
-    /**
-     * Timeout for Bluetooth toggle.
-     */
-    public int getBluetoothToggleTimeoutSeconds() {
-        return mBluetoothToggleTimeoutSeconds;
-    }
-
-    /**
-     * Sleep time for Bluetooth toggle.
-     */
-    public int getBluetoothToggleSleepSeconds() {
-        return mBluetoothToggleSleepSeconds;
-    }
-
-    /**
-     * Timeout for classic discovery.
-     */
-    public int getClassicDiscoveryTimeoutSeconds() {
-        return mClassicDiscoveryTimeoutSeconds;
-    }
-
-    /**
-     * Number of discovery attempts allowed.
-     */
-    public int getNumDiscoverAttempts() {
-        return mNumDiscoverAttempts;
-    }
-
-    /**
-     * Sleep time between discovery retry.
-     */
-    public int getDiscoveryRetrySleepSeconds() {
-        return mDiscoveryRetrySleepSeconds;
-    }
-
-    /**
-     * Whether to ignore error incurred during discovery.
-     */
-    public boolean getIgnoreDiscoveryError() {
-        return mIgnoreDiscoveryError;
-    }
-
-    /**
-     * Timeout for Sdp.
-     */
-    public int getSdpTimeoutSeconds() {
-        return mSdpTimeoutSeconds;
-    }
-
-    /**
-     * Number of Sdp attempts allowed.
-     */
-    public int getNumSdpAttempts() {
-        return mNumSdpAttempts;
-    }
-
-    /**
-     * Number of create bond attempts allowed.
-     */
-    public int getNumCreateBondAttempts() {
-        return mNumCreateBondAttempts;
-    }
-
-    /**
-     * Number of connect attempts allowed.
-     */
-    public int getNumConnectAttempts() {
-        return mNumConnectAttempts;
-    }
-
-    /**
-     * Number of write account key attempts allowed.
-     */
-    public int getNumWriteAccountKeyAttempts() {
-        return mNumWriteAccountKeyAttempts;
-    }
-
-    /**
-     * Returns whether it is OK toggle bluetooth to retry upon failure.
-     */
-    public boolean getToggleBluetoothOnFailure() {
-        return mToggleBluetoothOnFailure;
-    }
-
-    /**
-     * Whether to get Bluetooth state using polling.
-     */
-    public boolean getBluetoothStateUsesPolling() {
-        return mBluetoothStateUsesPolling;
-    }
-
-    /**
-     * Polling time when retrieving Bluetooth state.
-     */
-    public int getBluetoothStatePollingMillis() {
-        return mBluetoothStatePollingMillis;
-    }
-
-    /**
-     * The number of times to attempt a generic operation, before giving up.
-     */
-    public int getNumAttempts() {
-        return mNumAttempts;
-    }
-
-    /**
-     * Returns whether BrEdr handover is enabled.
-     */
-    public boolean getEnableBrEdrHandover() {
-        return mEnableBrEdrHandover;
-    }
-
-    /**
-     * Returns characteristic Id for Br Handover data.
-     */
-    public short getBrHandoverDataCharacteristicId() {
-        return mBrHandoverDataCharacteristicId;
-    }
-
-    /**
-     * Returns characteristic Id for Bluethoth Sig data.
-     */
-    public short getBluetoothSigDataCharacteristicId() {
-        return mBluetoothSigDataCharacteristicId;
-    }
-
-    /**
-     * Returns characteristic Id for Firmware version.
-     */
-    public short getFirmwareVersionCharacteristicId() {
-        return mFirmwareVersionCharacteristicId;
-    }
-
-    /**
-     * Returns descripter Id for Br transport block data.
-     */
-    public short getBrTransportBlockDataDescriptorId() {
-        return mBrTransportBlockDataDescriptorId;
-    }
-
-    /**
-     * Whether to wait for Uuids after bonding.
-     */
-    public boolean getWaitForUuidsAfterBonding() {
-        return mWaitForUuidsAfterBonding;
-    }
-
-    /**
-     * Whether to get received Uuids and bonded events before close.
-     */
-    public boolean getReceiveUuidsAndBondedEventBeforeClose() {
-        return mReceiveUuidsAndBondedEventBeforeClose;
-    }
-
-    /**
-     * Timeout for remove bond operation.
-     */
-    public int getRemoveBondTimeoutSeconds() {
-        return mRemoveBondTimeoutSeconds;
-    }
-
-    /**
-     * Sleep time for remove bond operation.
-     */
-    public int getRemoveBondSleepMillis() {
-        return mRemoveBondSleepMillis;
-    }
-
-    /**
-     * This almost always succeeds (or fails) in 2-10 seconds (Taimen running O -> Nexus 6P sim).
-     */
-    public int getCreateBondTimeoutSeconds() {
-        return mCreateBondTimeoutSeconds;
-    }
-
-    /**
-     * Timeout for creating bond with Hid devices.
-     */
-    public int getHidCreateBondTimeoutSeconds() {
-        return mHidCreateBondTimeoutSeconds;
-    }
-
-    /**
-     * Timeout for get proxy operation.
-     */
-    public int getProxyTimeoutSeconds() {
-        return mProxyTimeoutSeconds;
-    }
-
-    /**
-     * Whether to reject phone book access.
-     */
-    public boolean getRejectPhonebookAccess() {
-        return mRejectPhonebookAccess;
-    }
-
-    /**
-     * Whether to reject message access.
-     */
-    public boolean getRejectMessageAccess() {
-        return mRejectMessageAccess;
-    }
-
-    /**
-     * Whether to reject sim access.
-     */
-    public boolean getRejectSimAccess() {
-        return mRejectSimAccess;
-    }
-
-    /**
-     * Sleep time for write account key operation.
-     */
-    public int getWriteAccountKeySleepMillis() {
-        return mWriteAccountKeySleepMillis;
-    }
-
-    /**
-     * Whether to skip disconneting gatt before writing account key.
-     */
-    public boolean getSkipDisconnectingGattBeforeWritingAccountKey() {
-        return mSkipDisconnectingGattBeforeWritingAccountKey;
-    }
-
-    /**
-     * Whether to get more event log for quality improvement.
-     */
-    public boolean getMoreEventLogForQuality() {
-        return mMoreEventLogForQuality;
-    }
-
-    /**
-     * Whether to retry gatt connection and secrete handshake.
-     */
-    public boolean getRetryGattConnectionAndSecretHandshake() {
-        return mRetryGattConnectionAndSecretHandshake;
-    }
-
-    /**
-     * Short Gatt connection timeoout.
-     */
-    public long getGattConnectShortTimeoutMs() {
-        return mGattConnectShortTimeoutMs;
-    }
-
-    /**
-     * Long Gatt connection timeout.
-     */
-    public long getGattConnectLongTimeoutMs() {
-        return mGattConnectLongTimeoutMs;
-    }
-
-    /**
-     * Short Timeout for Gatt connection, including retry.
-     */
-    public long getGattConnectShortTimeoutRetryMaxSpentTimeMs() {
-        return mGattConnectShortTimeoutRetryMaxSpentTimeMs;
-    }
-
-    /**
-     * Timeout for address rotation, including retry.
-     */
-    public long getAddressRotateRetryMaxSpentTimeMs() {
-        return mAddressRotateRetryMaxSpentTimeMs;
-    }
-
-    /**
-     * Returns pairing retry delay time.
-     */
-    public long getPairingRetryDelayMs() {
-        return mPairingRetryDelayMs;
-    }
-
-    /**
-     * Short timeout for secrete handshake.
-     */
-    public long getSecretHandshakeShortTimeoutMs() {
-        return mSecretHandshakeShortTimeoutMs;
-    }
-
-    /**
-     * Long timeout for secret handshake.
-     */
-    public long getSecretHandshakeLongTimeoutMs() {
-        return mSecretHandshakeLongTimeoutMs;
-    }
-
-    /**
-     * Short timeout for secret handshake, including retry.
-     */
-    public long getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs() {
-        return mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs;
-    }
-
-    /**
-     * Long timeout for secret handshake, including retry.
-     */
-    public long getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs() {
-        return mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs;
-    }
-
-    /**
-     * Number of secrete handshake retry allowed.
-     */
-    public long getSecretHandshakeRetryAttempts() {
-        return mSecretHandshakeRetryAttempts;
-    }
-
-    /**
-     * Timeout for secrete handshake and gatt connection, including retry.
-     */
-    public long getSecretHandshakeRetryGattConnectionMaxSpentTimeMs() {
-        return mSecretHandshakeRetryGattConnectionMaxSpentTimeMs;
-    }
-
-    /**
-     * Timeout for signal lost handling, including retry.
-     */
-    public long getSignalLostRetryMaxSpentTimeMs() {
-        return mSignalLostRetryMaxSpentTimeMs;
-    }
-
-    /**
-     * Returns error for gatt connection and secrete handshake, without retry.
-     */
-    public ImmutableSet<Integer> getGattConnectionAndSecretHandshakeNoRetryGattError() {
-        return mGattConnectionAndSecretHandshakeNoRetryGattError;
-    }
-
-    /**
-     * Whether to retry upon secrete handshake timeout.
-     */
-    public boolean getRetrySecretHandshakeTimeout() {
-        return mRetrySecretHandshakeTimeout;
-    }
-
-    /**
-     * Wehther to log user manual retry.
-     */
-    public boolean getLogUserManualRetry() {
-        return mLogUserManualRetry;
-    }
-
-    /**
-     * Returns number of pairing failure counts.
-     */
-    public int getPairFailureCounts() {
-        return mPairFailureCounts;
-    }
-
-    /**
-     * Returns cached device address.
-     */
-    public String getCachedDeviceAddress() {
-        return mCachedDeviceAddress;
-    }
-
-    /**
-     * Returns possible cached device address.
-     */
-    public String getPossibleCachedDeviceAddress() {
-        return mPossibleCachedDeviceAddress;
-    }
-
-    /**
-     * Returns count of paired devices from the same model Id.
-     */
-    public int getSameModelIdPairedDeviceCount() {
-        return mSameModelIdPairedDeviceCount;
-    }
-
-    /**
-     * Whether the bonded device address is in the Cache .
-     */
-    public boolean getIsDeviceFinishCheckAddressFromCache() {
-        return mIsDeviceFinishCheckAddressFromCache;
-    }
-
-    /**
-     * Whether to log pairing info when cached model Id is hit.
-     */
-    public boolean getLogPairWithCachedModelId() {
-        return mLogPairWithCachedModelId;
-    }
-
-    /**
-     * Whether to directly connnect to a profile of a device, whose model Id is in cache.
-     */
-    public boolean getDirectConnectProfileIfModelIdInCache() {
-        return mDirectConnectProfileIfModelIdInCache;
-    }
-
-    /**
-     * Whether to auto-accept
-     * {@link android.bluetooth.BluetoothDevice#PAIRING_VARIANT_PASSKEY_CONFIRMATION}.
-     * Only the Fast Pair Simulator (which runs on an Android device) sends this. Since real
-     * Bluetooth headphones don't have displays, they use secure simple pairing (no pin code
-     * confirmation; we get no pairing request broadcast at all). So we may want to turn this off in
-     * prod.
-     */
-    public boolean getAcceptPasskey() {
-        return mAcceptPasskey;
-    }
-
-    /**
-     * Returns Uuids for supported profiles.
-     */
-    @SuppressWarnings("mutable")
-    public byte[] getSupportedProfileUuids() {
-        return mSupportedProfileUuids;
-    }
-
-    /**
-     * If true, after the Key-based Pairing BLE handshake, we wait for the headphones to send a
-     * pairing request to us; if false, we send the request to them.
-     */
-    public boolean getProviderInitiatesBondingIfSupported() {
-        return mProviderInitiatesBondingIfSupported;
-    }
-
-    /**
-     * If true, the first step will be attempting to connect directly to our supported profiles when
-     * a device has previously been bonded. This will help with performance on subsequent bondings
-     * and help to increase reliability in some cases.
-     */
-    public boolean getAttemptDirectConnectionWhenPreviouslyBonded() {
-        return mAttemptDirectConnectionWhenPreviouslyBonded;
-    }
-
-    /**
-     * If true, closed Gatt connections will be reopened when they are needed again. Otherwise, they
-     * will remain closed until they are explicitly reopened.
-     */
-    public boolean getAutomaticallyReconnectGattWhenNeeded() {
-        return mAutomaticallyReconnectGattWhenNeeded;
-    }
-
-    /**
-     * If true, we'll finish the pairing process after we've created a bond instead of after
-     * connecting a profile.
-     */
-    public boolean getSkipConnectingProfiles() {
-        return mSkipConnectingProfiles;
-    }
-
-    /**
-     * If true, continues the pairing process if we've timed out due to not receiving UUIDs from the
-     * headset. We can still attempt to connect to A2DP afterwards. If false, Fast Pair will fail
-     * after this step since we're expecting to receive the UUIDs.
-     */
-    public boolean getIgnoreUuidTimeoutAfterBonded() {
-        return mIgnoreUuidTimeoutAfterBonded;
-    }
-
-    /**
-     * If true, a specific transport type will be included in the create bond request, which will be
-     * used for dual mode devices. Otherwise, we'll use the platform defined default which is
-     * BluetoothDevice.TRANSPORT_AUTO. See {@link #getCreateBondTransportType()}.
-     */
-    public boolean getSpecifyCreateBondTransportType() {
-        return mSpecifyCreateBondTransportType;
-    }
-
-    /**
-     * The transport type to use when creating a bond when
-     * {@link #getSpecifyCreateBondTransportType() is true. This should be one of
-     * BluetoothDevice.TRANSPORT_AUTO, BluetoothDevice.TRANSPORT_BREDR,
-     * or BluetoothDevice.TRANSPORT_LE.
-     */
-    public int getCreateBondTransportType() {
-        return mCreateBondTransportType;
-    }
-
-    /**
-     * Whether to increase intent filter priority.
-     */
-    public boolean getIncreaseIntentFilterPriority() {
-        return mIncreaseIntentFilterPriority;
-    }
-
-    /**
-     * Whether to evaluate performance.
-     */
-    public boolean getEvaluatePerformance() {
-        return mEvaluatePerformance;
-    }
-
-    /**
-     * Returns extra logging information.
-     */
-    @Nullable
-    public ExtraLoggingInformation getExtraLoggingInformation() {
-        return mExtraLoggingInformation;
-    }
-
-    /**
-     * Whether to enable naming characteristic.
-     */
-    public boolean getEnableNamingCharacteristic() {
-        return mEnableNamingCharacteristic;
-    }
-
-    /**
-     * Whether to enable firmware version characteristic.
-     */
-    public boolean getEnableFirmwareVersionCharacteristic() {
-        return mEnableFirmwareVersionCharacteristic;
-    }
-
-    /**
-     * If true, even Fast Pair identifies a provider have paired with the account, still writes the
-     * identified account key to the provider.
-     */
-    public boolean getKeepSameAccountKeyWrite() {
-        return mKeepSameAccountKeyWrite;
-    }
-
-    /**
-     * If true, run retroactive pairing.
-     */
-    public boolean getIsRetroactivePairing() {
-        return mIsRetroactivePairing;
-    }
-
-    /**
-     * If it's larger than 0, {@link android.bluetooth.BluetoothDevice#fetchUuidsWithSdp} would be
-     * triggered with number of attempts after device is bonded and no profiles were automatically
-     * discovered".
-     */
-    public int getNumSdpAttemptsAfterBonded() {
-        return mNumSdpAttemptsAfterBonded;
-    }
-
-    /**
-     * If true, supports HID device for fastpair.
-     */
-    public boolean getSupportHidDevice() {
-        return mSupportHidDevice;
-    }
-
-    /**
-     * If true, we'll enable the pairing behavior to handle the state transition from BOND_BONDED to
-     * BOND_BONDING when directly connecting profiles.
-     */
-    public boolean getEnablePairingWhileDirectlyConnecting() {
-        return mEnablePairingWhileDirectlyConnecting;
-    }
-
-    /**
-     * If true, we will accept the user confirmation when bonding with FastPair 1.0 devices.
-     */
-    public boolean getAcceptConsentForFastPairOne() {
-        return mAcceptConsentForFastPairOne;
-    }
-
-    /**
-     * If it's larger than 0, we will retry connecting GATT within the timeout.
-     */
-    public int getGattConnectRetryTimeoutMillis() {
-        return mGattConnectRetryTimeoutMillis;
-    }
-
-    /**
-     * If true, then uses the new custom GATT characteristics {go/fastpair-128bit-gatt}.
-     */
-    public boolean getEnable128BitCustomGattCharacteristicsId() {
-        return mEnable128BitCustomGattCharacteristicsId;
-    }
-
-    /**
-     * If true, then sends the internal pair step or Exception to Validator by Intent.
-     */
-    public boolean getEnableSendExceptionStepToValidator() {
-        return mEnableSendExceptionStepToValidator;
-    }
-
-    /**
-     * If true, then adds the additional data type in the handshake packet when action over BLE.
-     */
-    public boolean getEnableAdditionalDataTypeWhenActionOverBle() {
-        return mEnableAdditionalDataTypeWhenActionOverBle;
-    }
-
-    /**
-     * If true, then checks the bond state when skips connecting profiles in the pairing shortcut.
-     */
-    public boolean getCheckBondStateWhenSkipConnectingProfiles() {
-        return mCheckBondStateWhenSkipConnectingProfiles;
-    }
-
-    /**
-     * If true, the passkey confirmation will be handled by the half-sheet UI.
-     */
-    public boolean getHandlePasskeyConfirmationByUi() {
-        return mHandlePasskeyConfirmationByUi;
-    }
-
-    /**
-     * If true, then use pair flow to show ui when pairing is finished without connecting profile.
-     */
-    public boolean getEnablePairFlowShowUiWithoutProfileConnection() {
-        return mEnablePairFlowShowUiWithoutProfileConnection;
-    }
-
-    @Override
-    public String toString() {
-        return "Preferences{"
-                + "gattOperationTimeoutSeconds=" + mGattOperationTimeoutSeconds + ", "
-                + "gattConnectionTimeoutSeconds=" + mGattConnectionTimeoutSeconds + ", "
-                + "bluetoothToggleTimeoutSeconds=" + mBluetoothToggleTimeoutSeconds + ", "
-                + "bluetoothToggleSleepSeconds=" + mBluetoothToggleSleepSeconds + ", "
-                + "classicDiscoveryTimeoutSeconds=" + mClassicDiscoveryTimeoutSeconds + ", "
-                + "numDiscoverAttempts=" + mNumDiscoverAttempts + ", "
-                + "discoveryRetrySleepSeconds=" + mDiscoveryRetrySleepSeconds + ", "
-                + "ignoreDiscoveryError=" + mIgnoreDiscoveryError + ", "
-                + "sdpTimeoutSeconds=" + mSdpTimeoutSeconds + ", "
-                + "numSdpAttempts=" + mNumSdpAttempts + ", "
-                + "numCreateBondAttempts=" + mNumCreateBondAttempts + ", "
-                + "numConnectAttempts=" + mNumConnectAttempts + ", "
-                + "numWriteAccountKeyAttempts=" + mNumWriteAccountKeyAttempts + ", "
-                + "toggleBluetoothOnFailure=" + mToggleBluetoothOnFailure + ", "
-                + "bluetoothStateUsesPolling=" + mBluetoothStateUsesPolling + ", "
-                + "bluetoothStatePollingMillis=" + mBluetoothStatePollingMillis + ", "
-                + "numAttempts=" + mNumAttempts + ", "
-                + "enableBrEdrHandover=" + mEnableBrEdrHandover + ", "
-                + "brHandoverDataCharacteristicId=" + mBrHandoverDataCharacteristicId + ", "
-                + "bluetoothSigDataCharacteristicId=" + mBluetoothSigDataCharacteristicId + ", "
-                + "firmwareVersionCharacteristicId=" + mFirmwareVersionCharacteristicId + ", "
-                + "brTransportBlockDataDescriptorId=" + mBrTransportBlockDataDescriptorId + ", "
-                + "waitForUuidsAfterBonding=" + mWaitForUuidsAfterBonding + ", "
-                + "receiveUuidsAndBondedEventBeforeClose=" + mReceiveUuidsAndBondedEventBeforeClose
-                + ", "
-                + "removeBondTimeoutSeconds=" + mRemoveBondTimeoutSeconds + ", "
-                + "removeBondSleepMillis=" + mRemoveBondSleepMillis + ", "
-                + "createBondTimeoutSeconds=" + mCreateBondTimeoutSeconds + ", "
-                + "hidCreateBondTimeoutSeconds=" + mHidCreateBondTimeoutSeconds + ", "
-                + "proxyTimeoutSeconds=" + mProxyTimeoutSeconds + ", "
-                + "rejectPhonebookAccess=" + mRejectPhonebookAccess + ", "
-                + "rejectMessageAccess=" + mRejectMessageAccess + ", "
-                + "rejectSimAccess=" + mRejectSimAccess + ", "
-                + "writeAccountKeySleepMillis=" + mWriteAccountKeySleepMillis + ", "
-                + "skipDisconnectingGattBeforeWritingAccountKey="
-                + mSkipDisconnectingGattBeforeWritingAccountKey + ", "
-                + "moreEventLogForQuality=" + mMoreEventLogForQuality + ", "
-                + "retryGattConnectionAndSecretHandshake=" + mRetryGattConnectionAndSecretHandshake
-                + ", "
-                + "gattConnectShortTimeoutMs=" + mGattConnectShortTimeoutMs + ", "
-                + "gattConnectLongTimeoutMs=" + mGattConnectLongTimeoutMs + ", "
-                + "gattConnectShortTimeoutRetryMaxSpentTimeMs="
-                + mGattConnectShortTimeoutRetryMaxSpentTimeMs + ", "
-                + "addressRotateRetryMaxSpentTimeMs=" + mAddressRotateRetryMaxSpentTimeMs + ", "
-                + "pairingRetryDelayMs=" + mPairingRetryDelayMs + ", "
-                + "secretHandshakeShortTimeoutMs=" + mSecretHandshakeShortTimeoutMs + ", "
-                + "secretHandshakeLongTimeoutMs=" + mSecretHandshakeLongTimeoutMs + ", "
-                + "secretHandshakeShortTimeoutRetryMaxSpentTimeMs="
-                + mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs + ", "
-                + "secretHandshakeLongTimeoutRetryMaxSpentTimeMs="
-                + mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs + ", "
-                + "secretHandshakeRetryAttempts=" + mSecretHandshakeRetryAttempts + ", "
-                + "secretHandshakeRetryGattConnectionMaxSpentTimeMs="
-                + mSecretHandshakeRetryGattConnectionMaxSpentTimeMs + ", "
-                + "signalLostRetryMaxSpentTimeMs=" + mSignalLostRetryMaxSpentTimeMs + ", "
-                + "gattConnectionAndSecretHandshakeNoRetryGattError="
-                + mGattConnectionAndSecretHandshakeNoRetryGattError + ", "
-                + "retrySecretHandshakeTimeout=" + mRetrySecretHandshakeTimeout + ", "
-                + "logUserManualRetry=" + mLogUserManualRetry + ", "
-                + "pairFailureCounts=" + mPairFailureCounts + ", "
-                + "cachedDeviceAddress=" + mCachedDeviceAddress + ", "
-                + "possibleCachedDeviceAddress=" + mPossibleCachedDeviceAddress + ", "
-                + "sameModelIdPairedDeviceCount=" + mSameModelIdPairedDeviceCount + ", "
-                + "isDeviceFinishCheckAddressFromCache=" + mIsDeviceFinishCheckAddressFromCache
-                + ", "
-                + "logPairWithCachedModelId=" + mLogPairWithCachedModelId + ", "
-                + "directConnectProfileIfModelIdInCache=" + mDirectConnectProfileIfModelIdInCache
-                + ", "
-                + "acceptPasskey=" + mAcceptPasskey + ", "
-                + "supportedProfileUuids=" + Arrays.toString(mSupportedProfileUuids) + ", "
-                + "providerInitiatesBondingIfSupported=" + mProviderInitiatesBondingIfSupported
-                + ", "
-                + "attemptDirectConnectionWhenPreviouslyBonded="
-                + mAttemptDirectConnectionWhenPreviouslyBonded + ", "
-                + "automaticallyReconnectGattWhenNeeded=" + mAutomaticallyReconnectGattWhenNeeded
-                + ", "
-                + "skipConnectingProfiles=" + mSkipConnectingProfiles + ", "
-                + "ignoreUuidTimeoutAfterBonded=" + mIgnoreUuidTimeoutAfterBonded + ", "
-                + "specifyCreateBondTransportType=" + mSpecifyCreateBondTransportType + ", "
-                + "createBondTransportType=" + mCreateBondTransportType + ", "
-                + "increaseIntentFilterPriority=" + mIncreaseIntentFilterPriority + ", "
-                + "evaluatePerformance=" + mEvaluatePerformance + ", "
-                + "extraLoggingInformation=" + mExtraLoggingInformation + ", "
-                + "enableNamingCharacteristic=" + mEnableNamingCharacteristic + ", "
-                + "enableFirmwareVersionCharacteristic=" + mEnableFirmwareVersionCharacteristic
-                + ", "
-                + "keepSameAccountKeyWrite=" + mKeepSameAccountKeyWrite + ", "
-                + "isRetroactivePairing=" + mIsRetroactivePairing + ", "
-                + "numSdpAttemptsAfterBonded=" + mNumSdpAttemptsAfterBonded + ", "
-                + "supportHidDevice=" + mSupportHidDevice + ", "
-                + "enablePairingWhileDirectlyConnecting=" + mEnablePairingWhileDirectlyConnecting
-                + ", "
-                + "acceptConsentForFastPairOne=" + mAcceptConsentForFastPairOne + ", "
-                + "gattConnectRetryTimeoutMillis=" + mGattConnectRetryTimeoutMillis + ", "
-                + "enable128BitCustomGattCharacteristicsId="
-                + mEnable128BitCustomGattCharacteristicsId + ", "
-                + "enableSendExceptionStepToValidator=" + mEnableSendExceptionStepToValidator + ", "
-                + "enableAdditionalDataTypeWhenActionOverBle="
-                + mEnableAdditionalDataTypeWhenActionOverBle + ", "
-                + "checkBondStateWhenSkipConnectingProfiles="
-                + mCheckBondStateWhenSkipConnectingProfiles + ", "
-                + "handlePasskeyConfirmationByUi=" + mHandlePasskeyConfirmationByUi + ", "
-                + "enablePairFlowShowUiWithoutProfileConnection="
-                + mEnablePairFlowShowUiWithoutProfileConnection
-                + "}";
-    }
-
-    /**
-     * Converts an instance to a builder.
-     */
-    public Builder toBuilder() {
-        return new Preferences.Builder(this);
-    }
-
-    /**
-     * Constructs a builder.
-     */
-    public static Builder builder() {
-        return new Preferences.Builder()
-                .setGattOperationTimeoutSeconds(10)
-                .setGattConnectionTimeoutSeconds(15)
-                .setBluetoothToggleTimeoutSeconds(10)
-                .setBluetoothToggleSleepSeconds(2)
-                .setClassicDiscoveryTimeoutSeconds(13)
-                .setNumDiscoverAttempts(3)
-                .setDiscoveryRetrySleepSeconds(1)
-                .setIgnoreDiscoveryError(true)
-                .setSdpTimeoutSeconds(10)
-                .setNumSdpAttempts(0)
-                .setNumCreateBondAttempts(3)
-                .setNumConnectAttempts(2)
-                .setNumWriteAccountKeyAttempts(3)
-                .setToggleBluetoothOnFailure(false)
-                .setBluetoothStateUsesPolling(true)
-                .setBluetoothStatePollingMillis(1000)
-                .setNumAttempts(2)
-                .setEnableBrEdrHandover(false)
-                .setBrHandoverDataCharacteristicId(get16BitUuid(
-                        Constants.TransportDiscoveryService.BrHandoverDataCharacteristic.ID))
-                .setBluetoothSigDataCharacteristicId(get16BitUuid(
-                        Constants.TransportDiscoveryService.BluetoothSigDataCharacteristic.ID))
-                .setFirmwareVersionCharacteristicId(get16BitUuid(FirmwareVersionCharacteristic.ID))
-                .setBrTransportBlockDataDescriptorId(
-                        get16BitUuid(
-                                Constants.TransportDiscoveryService.BluetoothSigDataCharacteristic
-                                        .BrTransportBlockDataDescriptor.ID))
-                .setWaitForUuidsAfterBonding(true)
-                .setReceiveUuidsAndBondedEventBeforeClose(true)
-                .setRemoveBondTimeoutSeconds(5)
-                .setRemoveBondSleepMillis(1000)
-                .setCreateBondTimeoutSeconds(15)
-                .setHidCreateBondTimeoutSeconds(40)
-                .setProxyTimeoutSeconds(2)
-                .setRejectPhonebookAccess(false)
-                .setRejectMessageAccess(false)
-                .setRejectSimAccess(false)
-                .setAcceptPasskey(true)
-                .setSupportedProfileUuids(Constants.getSupportedProfiles())
-                .setWriteAccountKeySleepMillis(2000)
-                .setProviderInitiatesBondingIfSupported(false)
-                .setAttemptDirectConnectionWhenPreviouslyBonded(true)
-                .setAutomaticallyReconnectGattWhenNeeded(true)
-                .setSkipDisconnectingGattBeforeWritingAccountKey(true)
-                .setSkipConnectingProfiles(false)
-                .setIgnoreUuidTimeoutAfterBonded(true)
-                .setSpecifyCreateBondTransportType(false)
-                .setCreateBondTransportType(0 /*BluetoothDevice.TRANSPORT_AUTO*/)
-                .setIncreaseIntentFilterPriority(true)
-                .setEvaluatePerformance(true)
-                .setKeepSameAccountKeyWrite(true)
-                .setEnableNamingCharacteristic(true)
-                .setEnableFirmwareVersionCharacteristic(true)
-                .setIsRetroactivePairing(false)
-                .setNumSdpAttemptsAfterBonded(1)
-                .setSupportHidDevice(false)
-                .setEnablePairingWhileDirectlyConnecting(true)
-                .setAcceptConsentForFastPairOne(true)
-                .setGattConnectRetryTimeoutMillis(18000)
-                .setEnable128BitCustomGattCharacteristicsId(true)
-                .setEnableSendExceptionStepToValidator(true)
-                .setEnableAdditionalDataTypeWhenActionOverBle(true)
-                .setCheckBondStateWhenSkipConnectingProfiles(true)
-                .setHandlePasskeyConfirmationByUi(false)
-                .setMoreEventLogForQuality(true)
-                .setRetryGattConnectionAndSecretHandshake(true)
-                .setGattConnectShortTimeoutMs(7000)
-                .setGattConnectLongTimeoutMs(15000)
-                .setGattConnectShortTimeoutRetryMaxSpentTimeMs(10000)
-                .setAddressRotateRetryMaxSpentTimeMs(15000)
-                .setPairingRetryDelayMs(100)
-                .setSecretHandshakeShortTimeoutMs(3000)
-                .setSecretHandshakeLongTimeoutMs(10000)
-                .setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(5000)
-                .setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(7000)
-                .setSecretHandshakeRetryAttempts(3)
-                .setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(15000)
-                .setSignalLostRetryMaxSpentTimeMs(15000)
-                .setGattConnectionAndSecretHandshakeNoRetryGattError(ImmutableSet.of(257))
-                .setRetrySecretHandshakeTimeout(false)
-                .setLogUserManualRetry(true)
-                .setPairFailureCounts(0)
-                .setEnablePairFlowShowUiWithoutProfileConnection(true)
-                .setPairFailureCounts(0)
-                .setLogPairWithCachedModelId(true)
-                .setDirectConnectProfileIfModelIdInCache(true)
-                .setCachedDeviceAddress("")
-                .setPossibleCachedDeviceAddress("")
-                .setSameModelIdPairedDeviceCount(0)
-                .setIsDeviceFinishCheckAddressFromCache(true);
-    }
-
-    /**
-     * Preferences builder.
-     */
-    public static class Builder {
-
-        private int mGattOperationTimeoutSeconds;
-        private int mGattConnectionTimeoutSeconds;
-        private int mBluetoothToggleTimeoutSeconds;
-        private int mBluetoothToggleSleepSeconds;
-        private int mClassicDiscoveryTimeoutSeconds;
-        private int mNumDiscoverAttempts;
-        private int mDiscoveryRetrySleepSeconds;
-        private boolean mIgnoreDiscoveryError;
-        private int mSdpTimeoutSeconds;
-        private int mNumSdpAttempts;
-        private int mNumCreateBondAttempts;
-        private int mNumConnectAttempts;
-        private int mNumWriteAccountKeyAttempts;
-        private boolean mToggleBluetoothOnFailure;
-        private boolean mBluetoothStateUsesPolling;
-        private int mBluetoothStatePollingMillis;
-        private int mNumAttempts;
-        private boolean mEnableBrEdrHandover;
-        private short mBrHandoverDataCharacteristicId;
-        private short mBluetoothSigDataCharacteristicId;
-        private short mFirmwareVersionCharacteristicId;
-        private short mBrTransportBlockDataDescriptorId;
-        private boolean mWaitForUuidsAfterBonding;
-        private boolean mReceiveUuidsAndBondedEventBeforeClose;
-        private int mRemoveBondTimeoutSeconds;
-        private int mRemoveBondSleepMillis;
-        private int mCreateBondTimeoutSeconds;
-        private int mHidCreateBondTimeoutSeconds;
-        private int mProxyTimeoutSeconds;
-        private boolean mRejectPhonebookAccess;
-        private boolean mRejectMessageAccess;
-        private boolean mRejectSimAccess;
-        private int mWriteAccountKeySleepMillis;
-        private boolean mSkipDisconnectingGattBeforeWritingAccountKey;
-        private boolean mMoreEventLogForQuality;
-        private boolean mRetryGattConnectionAndSecretHandshake;
-        private long mGattConnectShortTimeoutMs;
-        private long mGattConnectLongTimeoutMs;
-        private long mGattConnectShortTimeoutRetryMaxSpentTimeMs;
-        private long mAddressRotateRetryMaxSpentTimeMs;
-        private long mPairingRetryDelayMs;
-        private long mSecretHandshakeShortTimeoutMs;
-        private long mSecretHandshakeLongTimeoutMs;
-        private long mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs;
-        private long mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs;
-        private long mSecretHandshakeRetryAttempts;
-        private long mSecretHandshakeRetryGattConnectionMaxSpentTimeMs;
-        private long mSignalLostRetryMaxSpentTimeMs;
-        private ImmutableSet<Integer> mGattConnectionAndSecretHandshakeNoRetryGattError;
-        private boolean mRetrySecretHandshakeTimeout;
-        private boolean mLogUserManualRetry;
-        private int mPairFailureCounts;
-        private String mCachedDeviceAddress;
-        private String mPossibleCachedDeviceAddress;
-        private int mSameModelIdPairedDeviceCount;
-        private boolean mIsDeviceFinishCheckAddressFromCache;
-        private boolean mLogPairWithCachedModelId;
-        private boolean mDirectConnectProfileIfModelIdInCache;
-        private boolean mAcceptPasskey;
-        private byte[] mSupportedProfileUuids;
-        private boolean mProviderInitiatesBondingIfSupported;
-        private boolean mAttemptDirectConnectionWhenPreviouslyBonded;
-        private boolean mAutomaticallyReconnectGattWhenNeeded;
-        private boolean mSkipConnectingProfiles;
-        private boolean mIgnoreUuidTimeoutAfterBonded;
-        private boolean mSpecifyCreateBondTransportType;
-        private int mCreateBondTransportType;
-        private boolean mIncreaseIntentFilterPriority;
-        private boolean mEvaluatePerformance;
-        private Preferences.ExtraLoggingInformation mExtraLoggingInformation;
-        private boolean mEnableNamingCharacteristic;
-        private boolean mEnableFirmwareVersionCharacteristic;
-        private boolean mKeepSameAccountKeyWrite;
-        private boolean mIsRetroactivePairing;
-        private int mNumSdpAttemptsAfterBonded;
-        private boolean mSupportHidDevice;
-        private boolean mEnablePairingWhileDirectlyConnecting;
-        private boolean mAcceptConsentForFastPairOne;
-        private int mGattConnectRetryTimeoutMillis;
-        private boolean mEnable128BitCustomGattCharacteristicsId;
-        private boolean mEnableSendExceptionStepToValidator;
-        private boolean mEnableAdditionalDataTypeWhenActionOverBle;
-        private boolean mCheckBondStateWhenSkipConnectingProfiles;
-        private boolean mHandlePasskeyConfirmationByUi;
-        private boolean mEnablePairFlowShowUiWithoutProfileConnection;
-
-        private Builder() {
-        }
-
-        private Builder(Preferences source) {
-            this.mGattOperationTimeoutSeconds = source.getGattOperationTimeoutSeconds();
-            this.mGattConnectionTimeoutSeconds = source.getGattConnectionTimeoutSeconds();
-            this.mBluetoothToggleTimeoutSeconds = source.getBluetoothToggleTimeoutSeconds();
-            this.mBluetoothToggleSleepSeconds = source.getBluetoothToggleSleepSeconds();
-            this.mClassicDiscoveryTimeoutSeconds = source.getClassicDiscoveryTimeoutSeconds();
-            this.mNumDiscoverAttempts = source.getNumDiscoverAttempts();
-            this.mDiscoveryRetrySleepSeconds = source.getDiscoveryRetrySleepSeconds();
-            this.mIgnoreDiscoveryError = source.getIgnoreDiscoveryError();
-            this.mSdpTimeoutSeconds = source.getSdpTimeoutSeconds();
-            this.mNumSdpAttempts = source.getNumSdpAttempts();
-            this.mNumCreateBondAttempts = source.getNumCreateBondAttempts();
-            this.mNumConnectAttempts = source.getNumConnectAttempts();
-            this.mNumWriteAccountKeyAttempts = source.getNumWriteAccountKeyAttempts();
-            this.mToggleBluetoothOnFailure = source.getToggleBluetoothOnFailure();
-            this.mBluetoothStateUsesPolling = source.getBluetoothStateUsesPolling();
-            this.mBluetoothStatePollingMillis = source.getBluetoothStatePollingMillis();
-            this.mNumAttempts = source.getNumAttempts();
-            this.mEnableBrEdrHandover = source.getEnableBrEdrHandover();
-            this.mBrHandoverDataCharacteristicId = source.getBrHandoverDataCharacteristicId();
-            this.mBluetoothSigDataCharacteristicId = source.getBluetoothSigDataCharacteristicId();
-            this.mFirmwareVersionCharacteristicId = source.getFirmwareVersionCharacteristicId();
-            this.mBrTransportBlockDataDescriptorId = source.getBrTransportBlockDataDescriptorId();
-            this.mWaitForUuidsAfterBonding = source.getWaitForUuidsAfterBonding();
-            this.mReceiveUuidsAndBondedEventBeforeClose = source
-                    .getReceiveUuidsAndBondedEventBeforeClose();
-            this.mRemoveBondTimeoutSeconds = source.getRemoveBondTimeoutSeconds();
-            this.mRemoveBondSleepMillis = source.getRemoveBondSleepMillis();
-            this.mCreateBondTimeoutSeconds = source.getCreateBondTimeoutSeconds();
-            this.mHidCreateBondTimeoutSeconds = source.getHidCreateBondTimeoutSeconds();
-            this.mProxyTimeoutSeconds = source.getProxyTimeoutSeconds();
-            this.mRejectPhonebookAccess = source.getRejectPhonebookAccess();
-            this.mRejectMessageAccess = source.getRejectMessageAccess();
-            this.mRejectSimAccess = source.getRejectSimAccess();
-            this.mWriteAccountKeySleepMillis = source.getWriteAccountKeySleepMillis();
-            this.mSkipDisconnectingGattBeforeWritingAccountKey = source
-                    .getSkipDisconnectingGattBeforeWritingAccountKey();
-            this.mMoreEventLogForQuality = source.getMoreEventLogForQuality();
-            this.mRetryGattConnectionAndSecretHandshake = source
-                    .getRetryGattConnectionAndSecretHandshake();
-            this.mGattConnectShortTimeoutMs = source.getGattConnectShortTimeoutMs();
-            this.mGattConnectLongTimeoutMs = source.getGattConnectLongTimeoutMs();
-            this.mGattConnectShortTimeoutRetryMaxSpentTimeMs = source
-                    .getGattConnectShortTimeoutRetryMaxSpentTimeMs();
-            this.mAddressRotateRetryMaxSpentTimeMs = source.getAddressRotateRetryMaxSpentTimeMs();
-            this.mPairingRetryDelayMs = source.getPairingRetryDelayMs();
-            this.mSecretHandshakeShortTimeoutMs = source.getSecretHandshakeShortTimeoutMs();
-            this.mSecretHandshakeLongTimeoutMs = source.getSecretHandshakeLongTimeoutMs();
-            this.mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs = source
-                    .getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs();
-            this.mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs = source
-                    .getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs();
-            this.mSecretHandshakeRetryAttempts = source.getSecretHandshakeRetryAttempts();
-            this.mSecretHandshakeRetryGattConnectionMaxSpentTimeMs = source
-                    .getSecretHandshakeRetryGattConnectionMaxSpentTimeMs();
-            this.mSignalLostRetryMaxSpentTimeMs = source.getSignalLostRetryMaxSpentTimeMs();
-            this.mGattConnectionAndSecretHandshakeNoRetryGattError = source
-                    .getGattConnectionAndSecretHandshakeNoRetryGattError();
-            this.mRetrySecretHandshakeTimeout = source.getRetrySecretHandshakeTimeout();
-            this.mLogUserManualRetry = source.getLogUserManualRetry();
-            this.mPairFailureCounts = source.getPairFailureCounts();
-            this.mCachedDeviceAddress = source.getCachedDeviceAddress();
-            this.mPossibleCachedDeviceAddress = source.getPossibleCachedDeviceAddress();
-            this.mSameModelIdPairedDeviceCount = source.getSameModelIdPairedDeviceCount();
-            this.mIsDeviceFinishCheckAddressFromCache = source
-                    .getIsDeviceFinishCheckAddressFromCache();
-            this.mLogPairWithCachedModelId = source.getLogPairWithCachedModelId();
-            this.mDirectConnectProfileIfModelIdInCache = source
-                    .getDirectConnectProfileIfModelIdInCache();
-            this.mAcceptPasskey = source.getAcceptPasskey();
-            this.mSupportedProfileUuids = source.getSupportedProfileUuids();
-            this.mProviderInitiatesBondingIfSupported = source
-                    .getProviderInitiatesBondingIfSupported();
-            this.mAttemptDirectConnectionWhenPreviouslyBonded = source
-                    .getAttemptDirectConnectionWhenPreviouslyBonded();
-            this.mAutomaticallyReconnectGattWhenNeeded = source
-                    .getAutomaticallyReconnectGattWhenNeeded();
-            this.mSkipConnectingProfiles = source.getSkipConnectingProfiles();
-            this.mIgnoreUuidTimeoutAfterBonded = source.getIgnoreUuidTimeoutAfterBonded();
-            this.mSpecifyCreateBondTransportType = source.getSpecifyCreateBondTransportType();
-            this.mCreateBondTransportType = source.getCreateBondTransportType();
-            this.mIncreaseIntentFilterPriority = source.getIncreaseIntentFilterPriority();
-            this.mEvaluatePerformance = source.getEvaluatePerformance();
-            this.mExtraLoggingInformation = source.getExtraLoggingInformation();
-            this.mEnableNamingCharacteristic = source.getEnableNamingCharacteristic();
-            this.mEnableFirmwareVersionCharacteristic = source
-                    .getEnableFirmwareVersionCharacteristic();
-            this.mKeepSameAccountKeyWrite = source.getKeepSameAccountKeyWrite();
-            this.mIsRetroactivePairing = source.getIsRetroactivePairing();
-            this.mNumSdpAttemptsAfterBonded = source.getNumSdpAttemptsAfterBonded();
-            this.mSupportHidDevice = source.getSupportHidDevice();
-            this.mEnablePairingWhileDirectlyConnecting = source
-                    .getEnablePairingWhileDirectlyConnecting();
-            this.mAcceptConsentForFastPairOne = source.getAcceptConsentForFastPairOne();
-            this.mGattConnectRetryTimeoutMillis = source.getGattConnectRetryTimeoutMillis();
-            this.mEnable128BitCustomGattCharacteristicsId = source
-                    .getEnable128BitCustomGattCharacteristicsId();
-            this.mEnableSendExceptionStepToValidator = source
-                    .getEnableSendExceptionStepToValidator();
-            this.mEnableAdditionalDataTypeWhenActionOverBle = source
-                    .getEnableAdditionalDataTypeWhenActionOverBle();
-            this.mCheckBondStateWhenSkipConnectingProfiles = source
-                    .getCheckBondStateWhenSkipConnectingProfiles();
-            this.mHandlePasskeyConfirmationByUi = source.getHandlePasskeyConfirmationByUi();
-            this.mEnablePairFlowShowUiWithoutProfileConnection = source
-                    .getEnablePairFlowShowUiWithoutProfileConnection();
-        }
-
-        /**
-         * Set gatt operation timeout.
-         */
-        public Builder setGattOperationTimeoutSeconds(int value) {
-            this.mGattOperationTimeoutSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set gatt connection timeout.
-         */
-        public Builder setGattConnectionTimeoutSeconds(int value) {
-            this.mGattConnectionTimeoutSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set bluetooth toggle timeout.
-         */
-        public Builder setBluetoothToggleTimeoutSeconds(int value) {
-            this.mBluetoothToggleTimeoutSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set bluetooth toggle sleep time.
-         */
-        public Builder setBluetoothToggleSleepSeconds(int value) {
-            this.mBluetoothToggleSleepSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set classic discovery timeout.
-         */
-        public Builder setClassicDiscoveryTimeoutSeconds(int value) {
-            this.mClassicDiscoveryTimeoutSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set number of discover attempts allowed.
-         */
-        public Builder setNumDiscoverAttempts(int value) {
-            this.mNumDiscoverAttempts = value;
-            return this;
-        }
-
-        /**
-         * Set discovery retry sleep time.
-         */
-        public Builder setDiscoveryRetrySleepSeconds(int value) {
-            this.mDiscoveryRetrySleepSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set whether to ignore discovery error.
-         */
-        public Builder setIgnoreDiscoveryError(boolean value) {
-            this.mIgnoreDiscoveryError = value;
-            return this;
-        }
-
-        /**
-         * Set sdp timeout.
-         */
-        public Builder setSdpTimeoutSeconds(int value) {
-            this.mSdpTimeoutSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set number of sdp attempts allowed.
-         */
-        public Builder setNumSdpAttempts(int value) {
-            this.mNumSdpAttempts = value;
-            return this;
-        }
-
-        /**
-         * Set number of allowed attempts to create bond.
-         */
-        public Builder setNumCreateBondAttempts(int value) {
-            this.mNumCreateBondAttempts = value;
-            return this;
-        }
-
-        /**
-         * Set number of connect attempts allowed.
-         */
-        public Builder setNumConnectAttempts(int value) {
-            this.mNumConnectAttempts = value;
-            return this;
-        }
-
-        /**
-         * Set number of write account key attempts allowed.
-         */
-        public Builder setNumWriteAccountKeyAttempts(int value) {
-            this.mNumWriteAccountKeyAttempts = value;
-            return this;
-        }
-
-        /**
-         * Set whether to retry by bluetooth toggle on failure.
-         */
-        public Builder setToggleBluetoothOnFailure(boolean value) {
-            this.mToggleBluetoothOnFailure = value;
-            return this;
-        }
-
-        /**
-         * Set whether to use polling to set bluetooth status.
-         */
-        public Builder setBluetoothStateUsesPolling(boolean value) {
-            this.mBluetoothStateUsesPolling = value;
-            return this;
-        }
-
-        /**
-         * Set Bluetooth state polling timeout.
-         */
-        public Builder setBluetoothStatePollingMillis(int value) {
-            this.mBluetoothStatePollingMillis = value;
-            return this;
-        }
-
-        /**
-         * Set number of attempts.
-         */
-        public Builder setNumAttempts(int value) {
-            this.mNumAttempts = value;
-            return this;
-        }
-
-        /**
-         * Set whether to enable BrEdr handover.
-         */
-        public Builder setEnableBrEdrHandover(boolean value) {
-            this.mEnableBrEdrHandover = value;
-            return this;
-        }
-
-        /**
-         * Set Br handover data characteristic Id.
-         */
-        public Builder setBrHandoverDataCharacteristicId(short value) {
-            this.mBrHandoverDataCharacteristicId = value;
-            return this;
-        }
-
-        /**
-         * Set Bluetooth Sig data characteristic Id.
-         */
-        public Builder setBluetoothSigDataCharacteristicId(short value) {
-            this.mBluetoothSigDataCharacteristicId = value;
-            return this;
-        }
-
-        /**
-         * Set Firmware version characteristic id.
-         */
-        public Builder setFirmwareVersionCharacteristicId(short value) {
-            this.mFirmwareVersionCharacteristicId = value;
-            return this;
-        }
-
-        /**
-         * Set Br transport block data descriptor id.
-         */
-        public Builder setBrTransportBlockDataDescriptorId(short value) {
-            this.mBrTransportBlockDataDescriptorId = value;
-            return this;
-        }
-
-        /**
-         * Set whether to wait for Uuids after bonding.
-         */
-        public Builder setWaitForUuidsAfterBonding(boolean value) {
-            this.mWaitForUuidsAfterBonding = value;
-            return this;
-        }
-
-        /**
-         * Set whether to receive Uuids and bonded event before close.
-         */
-        public Builder setReceiveUuidsAndBondedEventBeforeClose(boolean value) {
-            this.mReceiveUuidsAndBondedEventBeforeClose = value;
-            return this;
-        }
-
-        /**
-         * Set remove bond timeout.
-         */
-        public Builder setRemoveBondTimeoutSeconds(int value) {
-            this.mRemoveBondTimeoutSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set remove bound sleep time.
-         */
-        public Builder setRemoveBondSleepMillis(int value) {
-            this.mRemoveBondSleepMillis = value;
-            return this;
-        }
-
-        /**
-         * Set create bond timeout.
-         */
-        public Builder setCreateBondTimeoutSeconds(int value) {
-            this.mCreateBondTimeoutSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set Hid create bond timeout.
-         */
-        public Builder setHidCreateBondTimeoutSeconds(int value) {
-            this.mHidCreateBondTimeoutSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set proxy timeout.
-         */
-        public Builder setProxyTimeoutSeconds(int value) {
-            this.mProxyTimeoutSeconds = value;
-            return this;
-        }
-
-        /**
-         * Set whether to reject phone book access.
-         */
-        public Builder setRejectPhonebookAccess(boolean value) {
-            this.mRejectPhonebookAccess = value;
-            return this;
-        }
-
-        /**
-         * Set whether to reject message access.
-         */
-        public Builder setRejectMessageAccess(boolean value) {
-            this.mRejectMessageAccess = value;
-            return this;
-        }
-
-        /**
-         * Set whether to reject slim access.
-         */
-        public Builder setRejectSimAccess(boolean value) {
-            this.mRejectSimAccess = value;
-            return this;
-        }
-
-        /**
-         * Set whether to accept passkey.
-         */
-        public Builder setAcceptPasskey(boolean value) {
-            this.mAcceptPasskey = value;
-            return this;
-        }
-
-        /**
-         * Set supported profile Uuids.
-         */
-        public Builder setSupportedProfileUuids(byte[] value) {
-            this.mSupportedProfileUuids = value;
-            return this;
-        }
-
-        /**
-         * Set whether to collect more event log for quality.
-         */
-        public Builder setMoreEventLogForQuality(boolean value) {
-            this.mMoreEventLogForQuality = value;
-            return this;
-        }
-
-        /**
-         * Set supported profile Uuids.
-         */
-        public Builder setSupportedProfileUuids(short... uuids) {
-            return setSupportedProfileUuids(Bytes.toBytes(ByteOrder.BIG_ENDIAN, uuids));
-        }
-
-        /**
-         * Set write account key sleep time.
-         */
-        public Builder setWriteAccountKeySleepMillis(int value) {
-            this.mWriteAccountKeySleepMillis = value;
-            return this;
-        }
-
-        /**
-         * Set whether to do provider initialized bonding if supported.
-         */
-        public Builder setProviderInitiatesBondingIfSupported(boolean value) {
-            this.mProviderInitiatesBondingIfSupported = value;
-            return this;
-        }
-
-        /**
-         * Set whether to try direct connection when the device is previously bonded.
-         */
-        public Builder setAttemptDirectConnectionWhenPreviouslyBonded(boolean value) {
-            this.mAttemptDirectConnectionWhenPreviouslyBonded = value;
-            return this;
-        }
-
-        /**
-         * Set whether to automatically reconnect gatt when needed.
-         */
-        public Builder setAutomaticallyReconnectGattWhenNeeded(boolean value) {
-            this.mAutomaticallyReconnectGattWhenNeeded = value;
-            return this;
-        }
-
-        /**
-         * Set whether to skip disconnecting gatt before writing account key.
-         */
-        public Builder setSkipDisconnectingGattBeforeWritingAccountKey(boolean value) {
-            this.mSkipDisconnectingGattBeforeWritingAccountKey = value;
-            return this;
-        }
-
-        /**
-         * Set whether to skip connecting profiles.
-         */
-        public Builder setSkipConnectingProfiles(boolean value) {
-            this.mSkipConnectingProfiles = value;
-            return this;
-        }
-
-        /**
-         * Set whether to ignore Uuid timeout after bonded.
-         */
-        public Builder setIgnoreUuidTimeoutAfterBonded(boolean value) {
-            this.mIgnoreUuidTimeoutAfterBonded = value;
-            return this;
-        }
-
-        /**
-         * Set whether to include transport type in create bound request.
-         */
-        public Builder setSpecifyCreateBondTransportType(boolean value) {
-            this.mSpecifyCreateBondTransportType = value;
-            return this;
-        }
-
-        /**
-         * Set transport type used in create bond request.
-         */
-        public Builder setCreateBondTransportType(int value) {
-            this.mCreateBondTransportType = value;
-            return this;
-        }
-
-        /**
-         * Set whether to increase intent filter priority.
-         */
-        public Builder setIncreaseIntentFilterPriority(boolean value) {
-            this.mIncreaseIntentFilterPriority = value;
-            return this;
-        }
-
-        /**
-         * Set whether to evaluate performance.
-         */
-        public Builder setEvaluatePerformance(boolean value) {
-            this.mEvaluatePerformance = value;
-            return this;
-        }
-
-        /**
-         * Set extra logging info.
-         */
-        public Builder setExtraLoggingInformation(ExtraLoggingInformation value) {
-            this.mExtraLoggingInformation = value;
-            return this;
-        }
-
-        /**
-         * Set whether to enable naming characteristic.
-         */
-        public Builder setEnableNamingCharacteristic(boolean value) {
-            this.mEnableNamingCharacteristic = value;
-            return this;
-        }
-
-        /**
-         * Set whether to keep writing the account key to the provider, that has already paired with
-         * the account.
-         */
-        public Builder setKeepSameAccountKeyWrite(boolean value) {
-            this.mKeepSameAccountKeyWrite = value;
-            return this;
-        }
-
-        /**
-         * Set whether to enable firmware version characteristic.
-         */
-        public Builder setEnableFirmwareVersionCharacteristic(boolean value) {
-            this.mEnableFirmwareVersionCharacteristic = value;
-            return this;
-        }
-
-        /**
-         * Set whether it is retroactive pairing.
-         */
-        public Builder setIsRetroactivePairing(boolean value) {
-            this.mIsRetroactivePairing = value;
-            return this;
-        }
-
-        /**
-         * Set number of allowed sdp attempts after bonded.
-         */
-        public Builder setNumSdpAttemptsAfterBonded(int value) {
-            this.mNumSdpAttemptsAfterBonded = value;
-            return this;
-        }
-
-        /**
-         * Set whether to support Hid device.
-         */
-        public Builder setSupportHidDevice(boolean value) {
-            this.mSupportHidDevice = value;
-            return this;
-        }
-
-        /**
-         * Set wehther to enable the pairing behavior to handle the state transition from
-         * BOND_BONDED to BOND_BONDING when directly connecting profiles.
-         */
-        public Builder setEnablePairingWhileDirectlyConnecting(boolean value) {
-            this.mEnablePairingWhileDirectlyConnecting = value;
-            return this;
-        }
-
-        /**
-         * Set whether to accept consent for fast pair one.
-         */
-        public Builder setAcceptConsentForFastPairOne(boolean value) {
-            this.mAcceptConsentForFastPairOne = value;
-            return this;
-        }
-
-        /**
-         * Set Gatt connect retry timeout.
-         */
-        public Builder setGattConnectRetryTimeoutMillis(int value) {
-            this.mGattConnectRetryTimeoutMillis = value;
-            return this;
-        }
-
-        /**
-         * Set whether to enable 128 bit custom gatt characteristic Id.
-         */
-        public Builder setEnable128BitCustomGattCharacteristicsId(boolean value) {
-            this.mEnable128BitCustomGattCharacteristicsId = value;
-            return this;
-        }
-
-        /**
-         * Set whether to send exception step to validator.
-         */
-        public Builder setEnableSendExceptionStepToValidator(boolean value) {
-            this.mEnableSendExceptionStepToValidator = value;
-            return this;
-        }
-
-        /**
-         * Set wehther to add the additional data type in the handshake when action over BLE.
-         */
-        public Builder setEnableAdditionalDataTypeWhenActionOverBle(boolean value) {
-            this.mEnableAdditionalDataTypeWhenActionOverBle = value;
-            return this;
-        }
-
-        /**
-         * Set whether to check bond state when skip connecting profiles.
-         */
-        public Builder setCheckBondStateWhenSkipConnectingProfiles(boolean value) {
-            this.mCheckBondStateWhenSkipConnectingProfiles = value;
-            return this;
-        }
-
-        /**
-         * Set whether to handle passkey confirmation by UI.
-         */
-        public Builder setHandlePasskeyConfirmationByUi(boolean value) {
-            this.mHandlePasskeyConfirmationByUi = value;
-            return this;
-        }
-
-        /**
-         * Set wehther to retry gatt connection and secret handshake.
-         */
-        public Builder setRetryGattConnectionAndSecretHandshake(boolean value) {
-            this.mRetryGattConnectionAndSecretHandshake = value;
-            return this;
-        }
-
-        /**
-         * Set gatt connect short timeout.
-         */
-        public Builder setGattConnectShortTimeoutMs(long value) {
-            this.mGattConnectShortTimeoutMs = value;
-            return this;
-        }
-
-        /**
-         * Set gatt connect long timeout.
-         */
-        public Builder setGattConnectLongTimeoutMs(long value) {
-            this.mGattConnectLongTimeoutMs = value;
-            return this;
-        }
-
-        /**
-         * Set gatt connection short timoutout, including retry.
-         */
-        public Builder setGattConnectShortTimeoutRetryMaxSpentTimeMs(long value) {
-            this.mGattConnectShortTimeoutRetryMaxSpentTimeMs = value;
-            return this;
-        }
-
-        /**
-         * Set address rotate timeout, including retry.
-         */
-        public Builder setAddressRotateRetryMaxSpentTimeMs(long value) {
-            this.mAddressRotateRetryMaxSpentTimeMs = value;
-            return this;
-        }
-
-        /**
-         * Set pairing retry delay time.
-         */
-        public Builder setPairingRetryDelayMs(long value) {
-            this.mPairingRetryDelayMs = value;
-            return this;
-        }
-
-        /**
-         * Set secret handshake short timeout.
-         */
-        public Builder setSecretHandshakeShortTimeoutMs(long value) {
-            this.mSecretHandshakeShortTimeoutMs = value;
-            return this;
-        }
-
-        /**
-         * Set secret handshake long timeout.
-         */
-        public Builder setSecretHandshakeLongTimeoutMs(long value) {
-            this.mSecretHandshakeLongTimeoutMs = value;
-            return this;
-        }
-
-        /**
-         * Set secret handshake short timeout retry max spent time.
-         */
-        public Builder setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(long value) {
-            this.mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs = value;
-            return this;
-        }
-
-        /**
-         * Set secret handshake long timeout retry max spent time.
-         */
-        public Builder setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(long value) {
-            this.mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs = value;
-            return this;
-        }
-
-        /**
-         * Set secret handshake retry attempts allowed.
-         */
-        public Builder setSecretHandshakeRetryAttempts(long value) {
-            this.mSecretHandshakeRetryAttempts = value;
-            return this;
-        }
-
-        /**
-         * Set secret handshake retry gatt connection max spent time.
-         */
-        public Builder setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(long value) {
-            this.mSecretHandshakeRetryGattConnectionMaxSpentTimeMs = value;
-            return this;
-        }
-
-        /**
-         * Set signal loss retry max spent time.
-         */
-        public Builder setSignalLostRetryMaxSpentTimeMs(long value) {
-            this.mSignalLostRetryMaxSpentTimeMs = value;
-            return this;
-        }
-
-        /**
-         * Set gatt connection and secret handshake no retry gatt error.
-         */
-        public Builder setGattConnectionAndSecretHandshakeNoRetryGattError(
-                ImmutableSet<Integer> value) {
-            this.mGattConnectionAndSecretHandshakeNoRetryGattError = value;
-            return this;
-        }
-
-        /**
-         * Set retry secret handshake timeout.
-         */
-        public Builder setRetrySecretHandshakeTimeout(boolean value) {
-            this.mRetrySecretHandshakeTimeout = value;
-            return this;
-        }
-
-        /**
-         * Set whether to log user manual retry.
-         */
-        public Builder setLogUserManualRetry(boolean value) {
-            this.mLogUserManualRetry = value;
-            return this;
-        }
-
-        /**
-         * Set pair falure counts.
-         */
-        public Builder setPairFailureCounts(int counts) {
-            this.mPairFailureCounts = counts;
-            return this;
-        }
-
-        /**
-         * Set whether to use pair flow to show ui when pairing is finished without connecting
-         * profile..
-         */
-        public Builder setEnablePairFlowShowUiWithoutProfileConnection(boolean value) {
-            this.mEnablePairFlowShowUiWithoutProfileConnection = value;
-            return this;
-        }
-
-        /**
-         * Set whether to log pairing with cached module Id.
-         */
-        public Builder setLogPairWithCachedModelId(boolean value) {
-            this.mLogPairWithCachedModelId = value;
-            return this;
-        }
-
-        /**
-         * Set possible cached device address.
-         */
-        public Builder setPossibleCachedDeviceAddress(String value) {
-            this.mPossibleCachedDeviceAddress = value;
-            return this;
-        }
-
-        /**
-         * Set paired device count from the same module Id.
-         */
-        public Builder setSameModelIdPairedDeviceCount(int value) {
-            this.mSameModelIdPairedDeviceCount = value;
-            return this;
-        }
-
-        /**
-         * Set whether the bonded device address is from cache.
-         */
-        public Builder setIsDeviceFinishCheckAddressFromCache(boolean value) {
-            this.mIsDeviceFinishCheckAddressFromCache = value;
-            return this;
-        }
-
-        /**
-         * Set whether to directly connect profile if modelId is in cache.
-         */
-        public Builder setDirectConnectProfileIfModelIdInCache(boolean value) {
-            this.mDirectConnectProfileIfModelIdInCache = value;
-            return this;
-        }
-
-        /**
-         * Set cached device address.
-         */
-        public Builder setCachedDeviceAddress(String value) {
-            this.mCachedDeviceAddress = value;
-            return this;
-        }
-
-        /**
-         * Builds a Preferences instance.
-         */
-        public Preferences build() {
-            return new Preferences(
-                    this.mGattOperationTimeoutSeconds,
-                    this.mGattConnectionTimeoutSeconds,
-                    this.mBluetoothToggleTimeoutSeconds,
-                    this.mBluetoothToggleSleepSeconds,
-                    this.mClassicDiscoveryTimeoutSeconds,
-                    this.mNumDiscoverAttempts,
-                    this.mDiscoveryRetrySleepSeconds,
-                    this.mIgnoreDiscoveryError,
-                    this.mSdpTimeoutSeconds,
-                    this.mNumSdpAttempts,
-                    this.mNumCreateBondAttempts,
-                    this.mNumConnectAttempts,
-                    this.mNumWriteAccountKeyAttempts,
-                    this.mToggleBluetoothOnFailure,
-                    this.mBluetoothStateUsesPolling,
-                    this.mBluetoothStatePollingMillis,
-                    this.mNumAttempts,
-                    this.mEnableBrEdrHandover,
-                    this.mBrHandoverDataCharacteristicId,
-                    this.mBluetoothSigDataCharacteristicId,
-                    this.mFirmwareVersionCharacteristicId,
-                    this.mBrTransportBlockDataDescriptorId,
-                    this.mWaitForUuidsAfterBonding,
-                    this.mReceiveUuidsAndBondedEventBeforeClose,
-                    this.mRemoveBondTimeoutSeconds,
-                    this.mRemoveBondSleepMillis,
-                    this.mCreateBondTimeoutSeconds,
-                    this.mHidCreateBondTimeoutSeconds,
-                    this.mProxyTimeoutSeconds,
-                    this.mRejectPhonebookAccess,
-                    this.mRejectMessageAccess,
-                    this.mRejectSimAccess,
-                    this.mWriteAccountKeySleepMillis,
-                    this.mSkipDisconnectingGattBeforeWritingAccountKey,
-                    this.mMoreEventLogForQuality,
-                    this.mRetryGattConnectionAndSecretHandshake,
-                    this.mGattConnectShortTimeoutMs,
-                    this.mGattConnectLongTimeoutMs,
-                    this.mGattConnectShortTimeoutRetryMaxSpentTimeMs,
-                    this.mAddressRotateRetryMaxSpentTimeMs,
-                    this.mPairingRetryDelayMs,
-                    this.mSecretHandshakeShortTimeoutMs,
-                    this.mSecretHandshakeLongTimeoutMs,
-                    this.mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs,
-                    this.mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs,
-                    this.mSecretHandshakeRetryAttempts,
-                    this.mSecretHandshakeRetryGattConnectionMaxSpentTimeMs,
-                    this.mSignalLostRetryMaxSpentTimeMs,
-                    this.mGattConnectionAndSecretHandshakeNoRetryGattError,
-                    this.mRetrySecretHandshakeTimeout,
-                    this.mLogUserManualRetry,
-                    this.mPairFailureCounts,
-                    this.mCachedDeviceAddress,
-                    this.mPossibleCachedDeviceAddress,
-                    this.mSameModelIdPairedDeviceCount,
-                    this.mIsDeviceFinishCheckAddressFromCache,
-                    this.mLogPairWithCachedModelId,
-                    this.mDirectConnectProfileIfModelIdInCache,
-                    this.mAcceptPasskey,
-                    this.mSupportedProfileUuids,
-                    this.mProviderInitiatesBondingIfSupported,
-                    this.mAttemptDirectConnectionWhenPreviouslyBonded,
-                    this.mAutomaticallyReconnectGattWhenNeeded,
-                    this.mSkipConnectingProfiles,
-                    this.mIgnoreUuidTimeoutAfterBonded,
-                    this.mSpecifyCreateBondTransportType,
-                    this.mCreateBondTransportType,
-                    this.mIncreaseIntentFilterPriority,
-                    this.mEvaluatePerformance,
-                    this.mExtraLoggingInformation,
-                    this.mEnableNamingCharacteristic,
-                    this.mEnableFirmwareVersionCharacteristic,
-                    this.mKeepSameAccountKeyWrite,
-                    this.mIsRetroactivePairing,
-                    this.mNumSdpAttemptsAfterBonded,
-                    this.mSupportHidDevice,
-                    this.mEnablePairingWhileDirectlyConnecting,
-                    this.mAcceptConsentForFastPairOne,
-                    this.mGattConnectRetryTimeoutMillis,
-                    this.mEnable128BitCustomGattCharacteristicsId,
-                    this.mEnableSendExceptionStepToValidator,
-                    this.mEnableAdditionalDataTypeWhenActionOverBle,
-                    this.mCheckBondStateWhenSkipConnectingProfiles,
-                    this.mHandlePasskeyConfirmationByUi,
-                    this.mEnablePairFlowShowUiWithoutProfileConnection);
-        }
-    }
-
-    /**
-     * Whether a given Uuid is supported.
-     */
-    public boolean isSupportedProfile(short profileUuid) {
-        return Constants.PROFILES.containsKey(profileUuid)
-                && Shorts.contains(
-                Bytes.toShorts(ByteOrder.BIG_ENDIAN, getSupportedProfileUuids()), profileUuid);
-    }
-
-    /**
-     * Information that will be used for logging.
-     */
-    public static class ExtraLoggingInformation {
-
-        private final String mModelId;
-
-        private ExtraLoggingInformation(String modelId) {
-            this.mModelId = modelId;
-        }
-
-        /**
-         * Returns model Id.
-         */
-        public String getModelId() {
-            return mModelId;
-        }
-
-        /**
-         * Converts an instance to a builder.
-         */
-        public Builder toBuilder() {
-            return new Builder(this);
-        }
-
-        /**
-         * Creates a builder for ExtraLoggingInformation.
-         */
-        public static Builder builder() {
-            return new ExtraLoggingInformation.Builder();
-        }
-
-        @Override
-        public String toString() {
-            return "ExtraLoggingInformation{" + "modelId=" + mModelId + "}";
-        }
-
-        @Override
-        public boolean equals(@Nullable Object o) {
-            if (o == this) {
-                return true;
-            }
-            if (o instanceof ExtraLoggingInformation) {
-                Preferences.ExtraLoggingInformation that = (Preferences.ExtraLoggingInformation) o;
-                return this.mModelId.equals(that.getModelId());
-            }
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(mModelId);
-        }
-
-        /**
-         * Extra logging information builder.
-         */
-        public static class Builder {
-
-            private String mModelId;
-
-            private Builder() {
-            }
-
-            private Builder(ExtraLoggingInformation source) {
-                this.mModelId = source.getModelId();
-            }
-
-            /**
-             * Set model ID.
-             */
-            public Builder setModelId(String modelId) {
-                this.mModelId = modelId;
-                return this;
-            }
-
-            /**
-             * Builds extra logging information.
-             */
-            public ExtraLoggingInformation build() {
-                return new ExtraLoggingInformation(mModelId);
-            }
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Reflect.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Reflect.java
deleted file mode 100644
index a2603b5..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Reflect.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-/**
- * Utilities for calling methods using reflection. The main benefit of using this helper is to avoid
- * complications around exception handling when calling methods reflectively. It's not safe to use
- * Java 8's multicatch on such exceptions, because the java compiler converts multicatch into
- * ReflectiveOperationException in some instances, which doesn't work on older sdk versions.
- * Instead, use these utilities and catch ReflectionException.
- *
- * <p>Example usage:
- *
- * <pre>{@code
- * try {
- *   Reflect.on(btAdapter)
- *       .withMethod("setScanMode", int.class)
- *       .invoke(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
- * } catch (ReflectionException e) { }
- * }</pre>
- */
-// TODO(b/202549655): remove existing Reflect usage. New usage is not allowed! No exception!
-public final class Reflect {
-    private final Object mTargetObject;
-
-    private Reflect(Object targetObject) {
-        this.mTargetObject = targetObject;
-    }
-
-    /** Creates an instance of this helper to invoke methods on the given target object. */
-    public static Reflect on(Object targetObject) {
-        return new Reflect(targetObject);
-    }
-
-    /** Finds a method with the given name and parameter types. */
-    public ReflectionMethod withMethod(String methodName, Class<?>... paramTypes)
-            throws ReflectionException {
-        try {
-            return new ReflectionMethod(mTargetObject.getClass().getMethod(methodName, paramTypes));
-        } catch (NoSuchMethodException e) {
-            throw new ReflectionException(e);
-        }
-    }
-
-    /** Represents an invokable method found reflectively. */
-    public final class ReflectionMethod {
-        private final Method mMethod;
-
-        private ReflectionMethod(Method method) {
-            this.mMethod = method;
-        }
-
-        /**
-         * Invokes this instance method with the given parameters. The called method does not return
-         * a value.
-         */
-        public void invoke(Object... parameters) throws ReflectionException {
-            try {
-                mMethod.invoke(mTargetObject, parameters);
-            } catch (IllegalAccessException e) {
-                throw new ReflectionException(e);
-            } catch (InvocationTargetException e) {
-                throw new ReflectionException(e);
-            }
-        }
-
-        /**
-         * Invokes this instance method with the given parameters. The called method returns a non
-         * null value.
-         */
-        public Object get(Object... parameters) throws ReflectionException {
-            Object value;
-            try {
-                value = mMethod.invoke(mTargetObject, parameters);
-            } catch (IllegalAccessException e) {
-                throw new ReflectionException(e);
-            } catch (InvocationTargetException e) {
-                throw new ReflectionException(e);
-            }
-            if (value == null) {
-                throw new ReflectionException(new NullPointerException());
-            }
-            return value;
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ReflectionException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ReflectionException.java
deleted file mode 100644
index 1c20c55..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ReflectionException.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-/**
- * An exception thrown during a reflection operation. Like ReflectiveOperationException, except
- * compatible on older API versions.
- */
-public final class ReflectionException extends Exception {
-    ReflectionException(Throwable cause) {
-        super(cause.getMessage(), cause);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalLostException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalLostException.java
deleted file mode 100644
index 244ee66..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalLostException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-/** Base class for fast pair signal lost exceptions. */
-public class SignalLostException extends PairingException {
-    SignalLostException(String message, Exception e) {
-        super(message);
-        initCause(e);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalRotatedException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalRotatedException.java
deleted file mode 100644
index d0d2a5d..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalRotatedException.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-/** Base class for fast pair signal rotated exceptions. */
-public class SignalRotatedException extends PairingException {
-    private final String mNewAddress;
-
-    SignalRotatedException(String message, String newAddress, Exception e) {
-        super(message);
-        this.mNewAddress = newAddress;
-        initCause(e);
-    }
-
-    /** Returns the new BLE address for the model ID. */
-    public String getNewAddress() {
-        return mNewAddress;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SimpleBroadcastReceiver.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SimpleBroadcastReceiver.java
deleted file mode 100644
index 7f525a7..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SimpleBroadcastReceiver.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import android.annotation.TargetApi;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Build.VERSION_CODES;
-import android.os.Handler;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.google.common.util.concurrent.SettableFuture;
-
-import java.util.Arrays;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Like {@link BroadcastReceiver}, but:
- *
- * <ul>
- *   <li>Simpler to create and register, with a list of actions.
- *   <li>Implements AutoCloseable. If used as a resource in try-with-resources (available on
- *       KitKat+), unregisters itself automatically.
- *   <li>Lets you block waiting for your state transition with {@link #await}.
- * </ul>
- */
-// AutoCloseable only available on KitKat+.
-@TargetApi(VERSION_CODES.KITKAT)
-public abstract class SimpleBroadcastReceiver extends BroadcastReceiver implements AutoCloseable {
-
-    private static final String TAG = SimpleBroadcastReceiver.class.getSimpleName();
-
-    /**
-     * Creates a one shot receiver.
-     */
-    public static SimpleBroadcastReceiver oneShotReceiver(
-            Context context, Preferences preferences, String... actions) {
-        return new SimpleBroadcastReceiver(context, preferences, actions) {
-            @Override
-            protected void onReceive(Intent intent) {
-                close();
-            }
-        };
-    }
-
-    private final Context mContext;
-    private final SettableFuture<Void> mIsClosedFuture = SettableFuture.create();
-    private long mAwaitExtendSecond;
-
-    // Nullness checker complains about 'this' being @UnderInitialization
-    @SuppressWarnings("nullness")
-    public SimpleBroadcastReceiver(
-            Context context, Preferences preferences, @Nullable Handler handler,
-            String... actions) {
-        Log.v(TAG, this + " listening for actions " + Arrays.toString(actions));
-        this.mContext = context;
-        IntentFilter intentFilter = new IntentFilter();
-        if (preferences.getIncreaseIntentFilterPriority()) {
-            intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
-        }
-        for (String action : actions) {
-            intentFilter.addAction(action);
-        }
-        context.registerReceiver(this, intentFilter, /* broadcastPermission= */ null, handler);
-    }
-
-    public SimpleBroadcastReceiver(Context context, Preferences preferences, String... actions) {
-        this(context, preferences, /* handler= */ null, actions);
-    }
-
-    /**
-     * Any exception thrown by this method will be delivered via {@link #await}.
-     */
-    protected abstract void onReceive(Intent intent) throws Exception;
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Log.v(TAG, "Got intent with action= " + intent.getAction());
-        try {
-            onReceive(intent);
-        } catch (Exception e) {
-            closeWithError(e);
-        }
-    }
-
-    @Override
-    public void close() {
-        closeWithError(null);
-    }
-
-    void closeWithError(@Nullable Exception e) {
-        try {
-            mContext.unregisterReceiver(this);
-        } catch (IllegalArgumentException ignored) {
-            // Ignore. Happens if you unregister twice.
-        }
-        if (e == null) {
-            mIsClosedFuture.set(null);
-        } else {
-            mIsClosedFuture.setException(e);
-        }
-    }
-
-    /**
-     * Extends the awaiting time.
-     */
-    public void extendAwaitSecond(int awaitExtendSecond) {
-        this.mAwaitExtendSecond = awaitExtendSecond;
-    }
-
-    /**
-     * Blocks until this receiver has closed (i.e. the state transition that this receiver is
-     * interested in has completed). Throws an exception on any error.
-     */
-    public void await(long timeout, TimeUnit timeUnit)
-            throws InterruptedException, ExecutionException, TimeoutException {
-        Log.v(TAG, this + " waiting on future for " + timeout + " " + timeUnit);
-        try {
-            mIsClosedFuture.get(timeout, timeUnit);
-        } catch (TimeoutException e) {
-            if (mAwaitExtendSecond <= 0) {
-                throw e;
-            }
-            Log.i(TAG, "Extend timeout for " + mAwaitExtendSecond + " seconds");
-            mIsClosedFuture.get(mAwaitExtendSecond, TimeUnit.SECONDS);
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TdsException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TdsException.java
deleted file mode 100644
index 7382ff3..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TdsException.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import com.android.server.nearby.intdefs.FastPairEventIntDefs.BrEdrHandoverErrorCode;
-
-import com.google.errorprone.annotations.FormatMethod;
-
-/**
- * Thrown when BR/EDR Handover fails.
- */
-public class TdsException extends Exception {
-
-    final @BrEdrHandoverErrorCode int mErrorCode;
-
-    @FormatMethod
-    TdsException(@BrEdrHandoverErrorCode int errorCode, String format, Object... objects) {
-        super(String.format(format, objects));
-        this.mErrorCode = errorCode;
-    }
-
-    /** Returns error code. */
-    public @BrEdrHandoverErrorCode int getErrorCode() {
-        return mErrorCode;
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TimingLogger.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TimingLogger.java
deleted file mode 100644
index 83ee309..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TimingLogger.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import androidx.annotation.VisibleForTesting;
-
-import java.util.ArrayDeque;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-/**
- * A profiler for performance metrics.
- *
- * <p>This class aim to break down the execution time for each steps of process to figure out the
- * bottleneck.
- */
-public class TimingLogger {
-
-    private static final String TAG = TimingLogger.class.getSimpleName();
-
-    /**
-     * The name of this session.
-     */
-    private final String mName;
-
-    private final Preferences mPreference;
-
-    /**
-     * The ordered timing sequence data. It's composed by a paired {@link Timing} generated from
-     * {@link #start} and {@link #end}.
-     */
-    private final List<Timing> mTimings;
-
-    private final long mStartTimestampMs;
-
-    /** Constructor. */
-    public TimingLogger(String name, Preferences mPreference) {
-        this.mName = name;
-        this.mPreference = mPreference;
-        mTimings = new CopyOnWriteArrayList<>();
-        mStartTimestampMs = SystemClock.elapsedRealtime();
-    }
-
-    @VisibleForTesting
-    List<Timing> getTimings() {
-        return mTimings;
-    }
-
-    /**
-     * Start a new paired timing.
-     *
-     * @param label The split name of paired timing.
-     */
-    public void start(String label) {
-        if (mPreference.getEvaluatePerformance()) {
-            mTimings.add(new Timing(label));
-        }
-    }
-
-    /**
-     * End a paired timing.
-     */
-    public void end() {
-        if (mPreference.getEvaluatePerformance()) {
-            mTimings.add(new Timing(Timing.END_LABEL));
-        }
-    }
-
-    /**
-     * Print out the timing data.
-     */
-    public void dump() {
-        if (!mPreference.getEvaluatePerformance()) {
-            return;
-        }
-
-        calculateTiming();
-        Log.i(TAG, mName + "[Exclusive time] / [Total time] ([Timestamp])");
-        int indentCount = 0;
-        for (Timing timing : mTimings) {
-            if (timing.isEndTiming()) {
-                indentCount--;
-                continue;
-            }
-            indentCount++;
-            if (timing.mExclusiveTime == timing.mTotalTime) {
-                Log.i(TAG, getIndentString(indentCount) + timing.mName + " " + timing.mExclusiveTime
-                        + "ms (" + getRelativeTimestamp(timing.getTimestamp()) + ")");
-            } else {
-                Log.i(TAG, getIndentString(indentCount) + timing.mName + " " + timing.mExclusiveTime
-                        + "ms / " + timing.mTotalTime + "ms (" + getRelativeTimestamp(
-                        timing.getTimestamp()) + ")");
-            }
-        }
-        Log.i(TAG, mName + "end, " + getTotalTime() + "ms");
-    }
-
-    private void calculateTiming() {
-        ArrayDeque<Timing> arrayDeque = new ArrayDeque<>();
-        for (Timing timing : mTimings) {
-            if (timing.isStartTiming()) {
-                arrayDeque.addFirst(timing);
-                continue;
-            }
-
-            Timing timingStart = arrayDeque.removeFirst();
-            final long time = timing.mTimestamp - timingStart.mTimestamp;
-            timingStart.mExclusiveTime += time;
-            timingStart.mTotalTime += time;
-            if (!arrayDeque.isEmpty()) {
-                arrayDeque.peekFirst().mExclusiveTime -= time;
-            }
-        }
-    }
-
-    private String getIndentString(int indentCount) {
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < indentCount; i++) {
-            sb.append("  ");
-        }
-        return sb.toString();
-    }
-
-    private long getRelativeTimestamp(long timestamp) {
-        return timestamp - mTimings.get(0).mTimestamp;
-    }
-
-    @VisibleForTesting
-    long getTotalTime() {
-        return mTimings.get(mTimings.size() - 1).mTimestamp - mTimings.get(0).mTimestamp;
-    }
-
-    /**
-     * Gets the current latency since this object was created.
-     */
-    public long getLatencyMs() {
-        return SystemClock.elapsedRealtime() - mStartTimestampMs;
-    }
-
-    @VisibleForTesting
-    static class Timing {
-
-        private static final String END_LABEL = "END_LABEL";
-
-        /**
-         * The name of this paired timing.
-         */
-        private final String mName;
-
-        /**
-         * System uptime in millisecond.
-         */
-        private final long mTimestamp;
-
-        /**
-         * The execution time exclude inner split timings.
-         */
-        private long mExclusiveTime;
-
-        /**
-         * The execution time within a start and an end timing.
-         */
-        private long mTotalTime;
-
-        private Timing(String name) {
-            this.mName = name;
-            mTimestamp = SystemClock.elapsedRealtime();
-            mExclusiveTime = 0;
-            mTotalTime = 0;
-        }
-
-        @VisibleForTesting
-        String getName() {
-            return mName;
-        }
-
-        @VisibleForTesting
-        long getTimestamp() {
-            return mTimestamp;
-        }
-
-        @VisibleForTesting
-        long getExclusiveTime() {
-            return mExclusiveTime;
-        }
-
-        @VisibleForTesting
-        long getTotalTime() {
-            return mTotalTime;
-        }
-
-        @VisibleForTesting
-        boolean isStartTiming() {
-            return !isEndTiming();
-        }
-
-        @VisibleForTesting
-        boolean isEndTiming() {
-            return END_LABEL.equals(mName);
-        }
-    }
-
-    /**
-     * This class ensures each split timing is paired with a start and an end timing.
-     */
-    public static class ScopedTiming implements AutoCloseable {
-
-        private final TimingLogger mTimingLogger;
-
-        public ScopedTiming(TimingLogger logger, String label) {
-            mTimingLogger = logger;
-            mTimingLogger.start(label);
-        }
-
-        @Override
-        public void close() {
-            mTimingLogger.end();
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ToggleBluetoothTask.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ToggleBluetoothTask.java
deleted file mode 100644
index 41ac9f5..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ToggleBluetoothTask.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-
-/** Task for toggling Bluetooth on and back off again. */
-interface ToggleBluetoothTask {
-
-    /**
-     * Toggles the bluetooth adapter off and back on again to help improve connection reliability.
-     *
-     * @throws InterruptedException when waiting for the bluetooth adapter's state to be set has
-     *     been interrupted.
-     * @throws ExecutionException when waiting for the bluetooth adapter's state to be set has
-     *     failed.
-     * @throws TimeoutException when the bluetooth adapter's state fails to be set on or off.
-     */
-    void toggleBluetooth() throws InterruptedException, ExecutionException, TimeoutException;
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnection.java
deleted file mode 100644
index de131e4..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnection.java
+++ /dev/null
@@ -1,781 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.gatt;
-
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothStatusCodes;
-import android.util.Log;
-
-import androidx.annotation.VisibleForTesting;
-
-import com.android.server.nearby.common.bluetooth.BluetoothConsts;
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
-import com.android.server.nearby.common.bluetooth.ReservedUuids;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.OperationType;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattWrapper;
-import com.android.server.nearby.common.bluetooth.util.BluetoothGattUtils;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.SynchronousOperation;
-
-import com.google.common.base.Preconditions;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.BlockingDeque;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.GuardedBy;
-
-/**
- * Gatt connection to a Bluetooth device.
- */
-public class BluetoothGattConnection implements AutoCloseable {
-
-    private static final String TAG = BluetoothGattConnection.class.getSimpleName();
-
-    @VisibleForTesting
-    static final long OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(1);
-    @VisibleForTesting
-    static final long SLOW_OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
-
-    @VisibleForTesting
-    static final int GATT_INTERNAL_ERROR = 129;
-    @VisibleForTesting
-    static final int GATT_ERROR = 133;
-
-    private final BluetoothGattWrapper mGatt;
-    private final BluetoothOperationExecutor mBluetoothOperationExecutor;
-    private final ConnectionOptions mConnectionOptions;
-
-    private volatile boolean mServicesDiscovered = false;
-
-    private volatile boolean mIsConnected = false;
-
-    private volatile int mMtu = BluetoothConsts.DEFAULT_MTU;
-
-    private final ConcurrentMap<BluetoothGattCharacteristic, ChangeObserver> mChangeObservers =
-            new ConcurrentHashMap<>();
-
-    private final List<ConnectionCloseListener> mCloseListeners = new ArrayList<>();
-
-    private long mOperationTimeoutMillis = OPERATION_TIMEOUT_MILLIS;
-
-    BluetoothGattConnection(
-            BluetoothGattWrapper gatt,
-            BluetoothOperationExecutor bluetoothOperationExecutor,
-            ConnectionOptions connectionOptions) {
-        mGatt = gatt;
-        mBluetoothOperationExecutor = bluetoothOperationExecutor;
-        mConnectionOptions = connectionOptions;
-    }
-
-    /**
-     * Set operation timeout.
-     */
-    public void setOperationTimeout(long timeoutMillis) {
-        Preconditions.checkArgument(timeoutMillis > 0, "invalid time out value");
-        mOperationTimeoutMillis = timeoutMillis;
-    }
-
-    /**
-     * Returns connected device.
-     */
-    public BluetoothDevice getDevice() {
-        return mGatt.getDevice();
-    }
-
-    public ConnectionOptions getConnectionOptions() {
-        return mConnectionOptions;
-    }
-
-    public boolean isConnected() {
-        return mIsConnected;
-    }
-
-    /**
-     * Get service.
-     */
-    public BluetoothGattService getService(UUID uuid) throws BluetoothException {
-        Log.d(TAG, String.format("Getting service %s.", uuid));
-        if (!mServicesDiscovered) {
-            discoverServices();
-        }
-        BluetoothGattService match = null;
-        for (BluetoothGattService service : mGatt.getServices()) {
-            if (service.getUuid().equals(uuid)) {
-                if (match != null) {
-                    throw new BluetoothException(
-                            String.format("More than one service %s found on device %s.",
-                                    uuid,
-                                    mGatt.getDevice()));
-                }
-                match = service;
-            }
-        }
-        if (match == null) {
-            throw new BluetoothException(String.format("Service %s not found on device %s.",
-                    uuid,
-                    mGatt.getDevice()));
-        }
-        Log.d(TAG, "Service found.");
-        return match;
-    }
-
-    /**
-     * Returns a list of all characteristics under a given service UUID.
-     */
-    private List<BluetoothGattCharacteristic> getCharacteristics(UUID serviceUuid)
-            throws BluetoothException {
-        if (!mServicesDiscovered) {
-            discoverServices();
-        }
-        ArrayList<BluetoothGattCharacteristic> characteristics = new ArrayList<>();
-        for (BluetoothGattService service : mGatt.getServices()) {
-            // Add all characteristics under this service if its service UUID matches.
-            if (service.getUuid().equals(serviceUuid)) {
-                characteristics.addAll(service.getCharacteristics());
-            }
-        }
-        return characteristics;
-    }
-
-    /**
-     * Get characteristic.
-     */
-    public BluetoothGattCharacteristic getCharacteristic(UUID serviceUuid,
-            UUID characteristicUuid) throws BluetoothException {
-        Log.d(TAG, String.format("Getting characteristic %s on service %s.", characteristicUuid,
-                serviceUuid));
-        BluetoothGattCharacteristic match = null;
-        for (BluetoothGattCharacteristic characteristic : getCharacteristics(serviceUuid)) {
-            if (characteristic.getUuid().equals(characteristicUuid)) {
-                if (match != null) {
-                    throw new BluetoothException(String.format(
-                            "More than one characteristic %s found on service %s on device %s.",
-                            characteristicUuid,
-                            serviceUuid,
-                            mGatt.getDevice()));
-                }
-                match = characteristic;
-            }
-        }
-        if (match == null) {
-            throw new BluetoothException(String.format(
-                    "Characteristic %s not found on service %s of device %s.",
-                    characteristicUuid,
-                    serviceUuid,
-                    mGatt.getDevice()));
-        }
-        Log.d(TAG, "Characteristic found.");
-        return match;
-    }
-
-    /**
-     * Get descriptor.
-     */
-    public BluetoothGattDescriptor getDescriptor(UUID serviceUuid,
-            UUID characteristicUuid, UUID descriptorUuid) throws BluetoothException {
-        Log.d(TAG, String.format("Getting descriptor %s on characteristic %s on service %s.",
-                descriptorUuid, characteristicUuid, serviceUuid));
-        BluetoothGattDescriptor match = null;
-        for (BluetoothGattDescriptor descriptor :
-                getCharacteristic(serviceUuid, characteristicUuid).getDescriptors()) {
-            if (descriptor.getUuid().equals(descriptorUuid)) {
-                if (match != null) {
-                    throw new BluetoothException(String.format("More than one descriptor %s found "
-                                    + "on characteristic %s service %s on device %s.",
-                            descriptorUuid,
-                            characteristicUuid,
-                            serviceUuid,
-                            mGatt.getDevice()));
-                }
-                match = descriptor;
-            }
-        }
-        if (match == null) {
-            throw new BluetoothException(String.format(
-                    "Descriptor %s not found on characteristic %s on service %s of device %s.",
-                    descriptorUuid,
-                    characteristicUuid,
-                    serviceUuid,
-                    mGatt.getDevice()));
-        }
-        Log.d(TAG, "Descriptor found.");
-        return match;
-    }
-
-    /**
-     * Discover services.
-     */
-    public void discoverServices() throws BluetoothException {
-        mBluetoothOperationExecutor.execute(
-                new SynchronousOperation<Void>(OperationType.DISCOVER_SERVICES) {
-                    @Nullable
-                    @Override
-                    public Void call() throws BluetoothException {
-                        if (mServicesDiscovered) {
-                            return null;
-                        }
-                        boolean forceRefresh = false;
-                        try {
-                            discoverServicesInternal();
-                        } catch (BluetoothException e) {
-                            if (!(e instanceof BluetoothGattException)) {
-                                throw e;
-                            }
-                            int errorCode = ((BluetoothGattException) e).getGattErrorCode();
-                            if (errorCode != GATT_ERROR && errorCode != GATT_INTERNAL_ERROR) {
-                                throw e;
-                            }
-                            Log.e(TAG, e.getMessage()
-                                    + "\n Ignore the gatt error for post MNC apis and force "
-                                    + "a refresh");
-                            forceRefresh = true;
-                        }
-
-                        forceRefreshServiceCacheIfNeeded(forceRefresh);
-
-                        mServicesDiscovered = true;
-
-                        return null;
-                    }
-                });
-    }
-
-    private void discoverServicesInternal() throws BluetoothException {
-        Log.i(TAG, "Starting services discovery.");
-        long startTimeMillis = System.currentTimeMillis();
-        try {
-            mBluetoothOperationExecutor.execute(
-                    new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL, mGatt) {
-                        @Override
-                        public void run() throws BluetoothException {
-                            boolean success = mGatt.discoverServices();
-                            if (!success) {
-                                throw new BluetoothException(
-                                        "gatt.discoverServices returned false.");
-                            }
-                        }
-                    },
-                    SLOW_OPERATION_TIMEOUT_MILLIS);
-            Log.i(TAG, String.format("Services discovered successfully in %s ms.",
-                    System.currentTimeMillis() - startTimeMillis));
-        } catch (BluetoothException e) {
-            if (e instanceof BluetoothGattException) {
-                throw new BluetoothGattException(String.format(
-                        "Failed to discover services on device: %s.",
-                        mGatt.getDevice()), ((BluetoothGattException) e).getGattErrorCode(), e);
-            } else {
-                throw new BluetoothException(String.format(
-                        "Failed to discover services on device: %s.",
-                        mGatt.getDevice()), e);
-            }
-        }
-    }
-
-    private boolean hasDynamicServices() {
-        BluetoothGattService gattService =
-                mGatt.getService(ReservedUuids.Services.GENERIC_ATTRIBUTE);
-        if (gattService != null) {
-            BluetoothGattCharacteristic serviceChange =
-                    gattService.getCharacteristic(ReservedUuids.Characteristics.SERVICE_CHANGE);
-            if (serviceChange != null) {
-                return true;
-            }
-        }
-
-        // Check whether the server contains a self defined service dynamic characteristic.
-        gattService = mGatt.getService(BluetoothConsts.SERVICE_DYNAMIC_SERVICE);
-        if (gattService != null) {
-            BluetoothGattCharacteristic serviceChange =
-                    gattService.getCharacteristic(BluetoothConsts.SERVICE_DYNAMIC_CHARACTERISTIC);
-            if (serviceChange != null) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    private void forceRefreshServiceCacheIfNeeded(boolean forceRefresh) throws BluetoothException {
-        if (mGatt.getDevice().getBondState() != BluetoothDevice.BOND_BONDED) {
-            // Device is not bonded, so services should not have been cached.
-            return;
-        }
-
-        if (!forceRefresh && !hasDynamicServices()) {
-            return;
-        }
-        Log.i(TAG, "Forcing a refresh of local cache of GATT services");
-        boolean success = mGatt.refresh();
-        if (!success) {
-            throw new BluetoothException("gatt.refresh returned false.");
-        }
-        discoverServicesInternal();
-    }
-
-    /**
-     * Read characteristic.
-     */
-    public byte[] readCharacteristic(UUID serviceUuid, UUID characteristicUuid)
-            throws BluetoothException {
-        return readCharacteristic(getCharacteristic(serviceUuid, characteristicUuid));
-    }
-
-    /**
-     * Read characteristic.
-     */
-    public byte[] readCharacteristic(final BluetoothGattCharacteristic characteristic)
-            throws BluetoothException {
-        try {
-            return mBluetoothOperationExecutor.executeNonnull(
-                    new Operation<byte[]>(OperationType.READ_CHARACTERISTIC, mGatt,
-                            characteristic) {
-                        @Override
-                        public void run() throws BluetoothException {
-                            boolean success = mGatt.readCharacteristic(characteristic);
-                            if (!success) {
-                                throw new BluetoothException(
-                                        "gatt.readCharacteristic returned false.");
-                            }
-                        }
-                    },
-                    mOperationTimeoutMillis);
-        } catch (BluetoothException e) {
-            throw new BluetoothException(String.format(
-                    "Failed to read %s on device %s.",
-                    BluetoothGattUtils.toString(characteristic),
-                    mGatt.getDevice()), e);
-        }
-    }
-
-    /**
-     * Writes Characteristic.
-     */
-    public void writeCharacteristic(UUID serviceUuid, UUID characteristicUuid, byte[] value)
-            throws BluetoothException {
-        writeCharacteristic(getCharacteristic(serviceUuid, characteristicUuid), value);
-    }
-
-    /**
-     * Writes Characteristic.
-     */
-    public void writeCharacteristic(final BluetoothGattCharacteristic characteristic,
-            final byte[] value) throws BluetoothException {
-        Log.d(TAG, String.format("Writing %d bytes on %s on device %s.",
-                value.length,
-                BluetoothGattUtils.toString(characteristic),
-                mGatt.getDevice()));
-        if ((characteristic.getProperties() & (BluetoothGattCharacteristic.PROPERTY_WRITE
-                | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) == 0) {
-            throw new BluetoothException(String.format("%s is not writable!", characteristic));
-        }
-        try {
-            mBluetoothOperationExecutor.execute(
-                    new Operation<Void>(OperationType.WRITE_CHARACTERISTIC, mGatt, characteristic) {
-                        @Override
-                        public void run() throws BluetoothException {
-                            int writeCharacteristicResponseCode = mGatt.writeCharacteristic(
-                                    characteristic, value, characteristic.getWriteType());
-                            if (writeCharacteristicResponseCode != BluetoothStatusCodes.SUCCESS) {
-                                throw new BluetoothException(
-                                        "gatt.writeCharacteristic returned "
-                                        + writeCharacteristicResponseCode);
-                            }
-                        }
-                    },
-                    mOperationTimeoutMillis);
-        } catch (BluetoothException e) {
-            throw new BluetoothException(String.format(
-                    "Failed to write %s on device %s.",
-                    BluetoothGattUtils.toString(characteristic),
-                    mGatt.getDevice()), e);
-        }
-        Log.d(TAG, "Writing characteristic done.");
-    }
-
-    /**
-     * Reads descriptor.
-     */
-    public byte[] readDescriptor(UUID serviceUuid, UUID characteristicUuid, UUID descriptorUuid)
-            throws BluetoothException {
-        return readDescriptor(getDescriptor(serviceUuid, characteristicUuid, descriptorUuid));
-    }
-
-    /**
-     * Reads descriptor.
-     */
-    public byte[] readDescriptor(final BluetoothGattDescriptor descriptor)
-            throws BluetoothException {
-        try {
-            return mBluetoothOperationExecutor.executeNonnull(
-                    new Operation<byte[]>(OperationType.READ_DESCRIPTOR, mGatt, descriptor) {
-                        @Override
-                        public void run() throws BluetoothException {
-                            boolean success = mGatt.readDescriptor(descriptor);
-                            if (!success) {
-                                throw new BluetoothException("gatt.readDescriptor returned false.");
-                            }
-                        }
-                    },
-                    mOperationTimeoutMillis);
-        } catch (BluetoothException e) {
-            throw new BluetoothException(String.format(
-                    "Failed to read %s on %s on device %s.",
-                    descriptor.getUuid(),
-                    BluetoothGattUtils.toString(descriptor),
-                    mGatt.getDevice()), e);
-        }
-    }
-
-    /**
-     * Writes descriptor.
-     */
-    public void writeDescriptor(UUID serviceUuid, UUID characteristicUuid, UUID descriptorUuid,
-            byte[] value) throws BluetoothException {
-        writeDescriptor(getDescriptor(serviceUuid, characteristicUuid, descriptorUuid), value);
-    }
-
-    /**
-     * Writes descriptor.
-     */
-    public void writeDescriptor(final BluetoothGattDescriptor descriptor, final byte[] value)
-            throws BluetoothException {
-        Log.d(TAG, String.format(
-                "Writing %d bytes on %s on device %s.",
-                value.length,
-                BluetoothGattUtils.toString(descriptor),
-                mGatt.getDevice()));
-        long startTimeMillis = System.currentTimeMillis();
-        try {
-            mBluetoothOperationExecutor.execute(
-                    new Operation<Void>(OperationType.WRITE_DESCRIPTOR, mGatt, descriptor) {
-                        @Override
-                        public void run() throws BluetoothException {
-                            int writeDescriptorResponseCode = mGatt.writeDescriptor(descriptor,
-                                    value);
-                            if (writeDescriptorResponseCode != BluetoothStatusCodes.SUCCESS) {
-                                throw new BluetoothException(
-                                        "gatt.writeDescriptor returned "
-                                        + writeDescriptorResponseCode);
-                            }
-                        }
-                    },
-                    mOperationTimeoutMillis);
-            Log.d(TAG, String.format("Writing descriptor done in %s ms.",
-                    System.currentTimeMillis() - startTimeMillis));
-        } catch (BluetoothException e) {
-            throw new BluetoothException(String.format(
-                    "Failed to write %s on device %s.",
-                    BluetoothGattUtils.toString(descriptor),
-                    mGatt.getDevice()), e);
-        }
-    }
-
-    /**
-     * Reads remote Rssi.
-     */
-    public int readRemoteRssi() throws BluetoothException {
-        try {
-            return mBluetoothOperationExecutor.executeNonnull(
-                    new Operation<Integer>(OperationType.READ_RSSI, mGatt) {
-                        @Override
-                        public void run() throws BluetoothException {
-                            boolean success = mGatt.readRemoteRssi();
-                            if (!success) {
-                                throw new BluetoothException("gatt.readRemoteRssi returned false.");
-                            }
-                        }
-                    },
-                    mOperationTimeoutMillis);
-        } catch (BluetoothException e) {
-            throw new BluetoothException(
-                    String.format("Failed to read rssi on device %s.", mGatt.getDevice()), e);
-        }
-    }
-
-    public int getMtu() {
-        return mMtu;
-    }
-
-    /**
-     * Get max data packet size.
-     */
-    public int getMaxDataPacketSize() {
-        // Per BT specs (3.2.9), only MTU - 3 bytes can be used to transmit data
-        return mMtu - 3;
-    }
-
-    /** Set notification enabled or disabled. */
-    @VisibleForTesting
-    public void setNotificationEnabled(BluetoothGattCharacteristic characteristic, boolean enabled)
-            throws BluetoothException {
-        boolean isIndication;
-        int properties = characteristic.getProperties();
-        if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
-            isIndication = false;
-        } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
-            isIndication = true;
-        } else {
-            throw new BluetoothException(String.format(
-                    "%s on device %s supports neither notifications nor indications.",
-                    BluetoothGattUtils.toString(characteristic),
-                    mGatt.getDevice()));
-        }
-        BluetoothGattDescriptor clientConfigDescriptor =
-                characteristic.getDescriptor(
-                        ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
-        if (clientConfigDescriptor == null) {
-            throw new BluetoothException(String.format(
-                    "%s on device %s is missing client config descriptor.",
-                    BluetoothGattUtils.toString(characteristic),
-                    mGatt.getDevice()));
-        }
-        long startTime = System.currentTimeMillis();
-        Log.d(TAG, String.format("%s %s on characteristic %s.", enabled ? "Enabling" : "Disabling",
-                isIndication ? "indication" : "notification", characteristic.getUuid()));
-
-        if (enabled) {
-            mGatt.setCharacteristicNotification(characteristic, enabled);
-        }
-        writeDescriptor(clientConfigDescriptor,
-                enabled
-                        ? (isIndication
-                        ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE :
-                        BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
-                        : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
-        if (!enabled) {
-            mGatt.setCharacteristicNotification(characteristic, enabled);
-        }
-
-        Log.d(TAG, String.format("Done in %d ms.", System.currentTimeMillis() - startTime));
-    }
-
-    /**
-     * Enables notification.
-     */
-    public ChangeObserver enableNotification(UUID serviceUuid, UUID characteristicUuid)
-            throws BluetoothException {
-        return enableNotification(getCharacteristic(serviceUuid, characteristicUuid));
-    }
-
-    /**
-     * Enables notification.
-     */
-    public ChangeObserver enableNotification(final BluetoothGattCharacteristic characteristic)
-            throws BluetoothException {
-        return mBluetoothOperationExecutor.executeNonnull(
-                new SynchronousOperation<ChangeObserver>(
-                        OperationType.NOTIFICATION_CHANGE,
-                        characteristic) {
-                    @Override
-                    public ChangeObserver call() throws BluetoothException {
-                        ChangeObserver changeObserver = new ChangeObserver();
-                        mChangeObservers.put(characteristic, changeObserver);
-                        setNotificationEnabled(characteristic, true);
-                        return changeObserver;
-                    }
-                });
-    }
-
-    /**
-     * Disables notification.
-     */
-    public void disableNotification(UUID serviceUuid, UUID characteristicUuid)
-            throws BluetoothException {
-        disableNotification(getCharacteristic(serviceUuid, characteristicUuid));
-    }
-
-    /**
-     * Disables notification.
-     */
-    public void disableNotification(final BluetoothGattCharacteristic characteristic)
-            throws BluetoothException {
-        mBluetoothOperationExecutor.execute(
-                new SynchronousOperation<Void>(
-                        OperationType.NOTIFICATION_CHANGE,
-                        characteristic) {
-                    @Nullable
-                    @Override
-                    public Void call() throws BluetoothException {
-                        setNotificationEnabled(characteristic, false);
-                        mChangeObservers.remove(characteristic);
-                        return null;
-                    }
-                });
-    }
-
-    /**
-     * Adds a close listener.
-     */
-    public void addCloseListener(ConnectionCloseListener listener) {
-        mCloseListeners.add(listener);
-        if (!mIsConnected) {
-            listener.onClose();
-            return;
-        }
-    }
-
-    /**
-     * Removes a close listener.
-     */
-    public void removeCloseListener(ConnectionCloseListener listener) {
-        mCloseListeners.remove(listener);
-    }
-
-    /** onCharacteristicChanged callback. */
-    public void onCharacteristicChanged(BluetoothGattCharacteristic characteristic, byte[] value) {
-        ChangeObserver observer = mChangeObservers.get(characteristic);
-        if (observer == null) {
-            return;
-        }
-        observer.onValueChange(value);
-    }
-
-    @Override
-    public void close() throws BluetoothException {
-        Log.d(TAG, "close");
-        try {
-            if (!mIsConnected) {
-                // Don't call disconnect on a closed connection, since Android framework won't
-                // provide any feedback.
-                return;
-            }
-            mBluetoothOperationExecutor.execute(
-                    new Operation<Void>(OperationType.DISCONNECT, mGatt.getDevice()) {
-                        @Override
-                        public void run() throws BluetoothException {
-                            mGatt.disconnect();
-                        }
-                    }, mOperationTimeoutMillis);
-        } finally {
-            mGatt.close();
-        }
-    }
-
-    /** onConnected callback. */
-    public void onConnected() {
-        Log.d(TAG, "onConnected");
-        mIsConnected = true;
-    }
-
-    /** onClosed callback. */
-    public void onClosed() {
-        Log.d(TAG, "onClosed");
-        if (!mIsConnected) {
-            return;
-        }
-        mIsConnected = false;
-        for (ConnectionCloseListener listener : mCloseListeners) {
-            listener.onClose();
-        }
-        mGatt.close();
-    }
-
-    /**
-     * Observer to wait or be called back when value change.
-     */
-    public static class ChangeObserver {
-
-        private final BlockingDeque<byte[]> mValues = new LinkedBlockingDeque<byte[]>();
-
-        @GuardedBy("mValues")
-        @Nullable
-        private volatile CharacteristicChangeListener mListener;
-
-        /**
-         * Set listener.
-         */
-        public void setListener(@Nullable CharacteristicChangeListener listener) {
-            synchronized (mValues) {
-                mListener = listener;
-                if (listener != null) {
-                    byte[] value;
-                    while ((value = mValues.poll()) != null) {
-                        listener.onValueChange(value);
-                    }
-                }
-            }
-        }
-
-        /**
-         * onValueChange callback.
-         */
-        public void onValueChange(byte[] newValue) {
-            synchronized (mValues) {
-                CharacteristicChangeListener listener = mListener;
-                if (listener == null) {
-                    mValues.add(newValue);
-                } else {
-                    listener.onValueChange(newValue);
-                }
-            }
-        }
-
-        /**
-         * Waits for update for a given time.
-         */
-        public byte[] waitForUpdate(long timeoutMillis) throws BluetoothException {
-            byte[] result;
-            try {
-                result = mValues.poll(timeoutMillis, TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                throw new BluetoothException("Operation interrupted.");
-            }
-            if (result == null) {
-                throw new BluetoothTimeoutException(
-                        String.format("Operation timed out after %dms", timeoutMillis));
-            }
-            return result;
-        }
-    }
-
-    /**
-     * Listener for characteristic data changes over notifications or indications.
-     */
-    public interface CharacteristicChangeListener {
-
-        /**
-         * onValueChange callback.
-         */
-        void onValueChange(byte[] newValue);
-    }
-
-    /**
-     * Listener for connection close events.
-     */
-    public interface ConnectionCloseListener {
-
-        /**
-         * onClose callback.
-         */
-        void onClose();
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelper.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelper.java
deleted file mode 100644
index 18a9f5f..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelper.java
+++ /dev/null
@@ -1,690 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.gatt;
-
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.os.ParcelUuid;
-import android.util.Log;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattCallback;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattWrapper;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeScanner;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanCallback;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanResult;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
-
-import com.google.common.base.Preconditions;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.GuardedBy;
-
-/**
- * Wrapper of {@link BluetoothGattWrapper} that provides blocking methods, errors and timeout
- * handling.
- */
-@SuppressWarnings("Guava") // java.util.Optional is not available until API 24
-public class BluetoothGattHelper {
-
-    private static final String TAG = BluetoothGattHelper.class.getSimpleName();
-
-    @VisibleForTesting
-    static final long LOW_LATENCY_SCAN_MILLIS = TimeUnit.SECONDS.toMillis(5);
-    private static final long POLL_INTERVAL_MILLIS = 5L /* milliseconds */;
-
-    /**
-     * BT operation types that can be in flight.
-     */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(
-            value = {
-                    OperationType.SCAN,
-                    OperationType.CONNECT,
-                    OperationType.DISCOVER_SERVICES,
-                    OperationType.DISCOVER_SERVICES_INTERNAL,
-                    OperationType.NOTIFICATION_CHANGE,
-                    OperationType.READ_CHARACTERISTIC,
-                    OperationType.WRITE_CHARACTERISTIC,
-                    OperationType.READ_DESCRIPTOR,
-                    OperationType.WRITE_DESCRIPTOR,
-                    OperationType.READ_RSSI,
-                    OperationType.WRITE_RELIABLE,
-                    OperationType.CHANGE_MTU,
-                    OperationType.DISCONNECT,
-            })
-    public @interface OperationType {
-        int SCAN = 0;
-        int CONNECT = 1;
-        int DISCOVER_SERVICES = 2;
-        int DISCOVER_SERVICES_INTERNAL = 3;
-        int NOTIFICATION_CHANGE = 4;
-        int READ_CHARACTERISTIC = 5;
-        int WRITE_CHARACTERISTIC = 6;
-        int READ_DESCRIPTOR = 7;
-        int WRITE_DESCRIPTOR = 8;
-        int READ_RSSI = 9;
-        int WRITE_RELIABLE = 10;
-        int CHANGE_MTU = 11;
-        int DISCONNECT = 12;
-    }
-
-    @VisibleForTesting
-    final ScanCallback mScanCallback = new InternalScanCallback();
-    @VisibleForTesting
-    final BluetoothGattCallback mBluetoothGattCallback =
-            new InternalBluetoothGattCallback();
-    @VisibleForTesting
-    final ConcurrentMap<BluetoothGattWrapper, BluetoothGattConnection> mConnections =
-            new ConcurrentHashMap<>();
-
-    private final Context mApplicationContext;
-    private final BluetoothAdapter mBluetoothAdapter;
-    private final BluetoothOperationExecutor mBluetoothOperationExecutor;
-
-    @VisibleForTesting
-    BluetoothGattHelper(
-            Context applicationContext,
-            BluetoothAdapter bluetoothAdapter,
-            BluetoothOperationExecutor bluetoothOperationExecutor) {
-        mApplicationContext = applicationContext;
-        mBluetoothAdapter = bluetoothAdapter;
-        mBluetoothOperationExecutor = bluetoothOperationExecutor;
-    }
-
-    public BluetoothGattHelper(Context applicationContext, BluetoothAdapter bluetoothAdapter) {
-        this(
-                Preconditions.checkNotNull(applicationContext),
-                Preconditions.checkNotNull(bluetoothAdapter),
-                new BluetoothOperationExecutor(5));
-    }
-
-    /**
-     * Auto-connects a serice Uuid.
-     */
-    public BluetoothGattConnection autoConnect(final UUID serviceUuid) throws BluetoothException {
-        Log.d(TAG, String.format("Starting autoconnection to a device advertising service %s.",
-                serviceUuid));
-        BluetoothDevice device = null;
-        int retries = 3;
-        final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
-        if (scanner == null) {
-            throw new BluetoothException("Bluetooth is disabled or LE is not supported.");
-        }
-        final ScanFilter serviceFilter = new ScanFilter.Builder()
-                .setServiceUuid(new ParcelUuid(serviceUuid))
-                .build();
-        ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder()
-                .setReportDelay(0);
-        final ScanSettings scanSettingsLowLatency = scanSettingsBuilder
-                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
-                .build();
-        final ScanSettings scanSettingsLowPower = scanSettingsBuilder
-                .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
-                .build();
-        while (true) {
-            long startTimeMillis = System.currentTimeMillis();
-            try {
-                Log.d(TAG, "Starting low latency scanning.");
-                device =
-                        mBluetoothOperationExecutor.executeNonnull(
-                                new Operation<BluetoothDevice>(OperationType.SCAN) {
-                                    @Override
-                                    public void run() throws BluetoothException {
-                                        scanner.startScan(Arrays.asList(serviceFilter),
-                                                scanSettingsLowLatency, mScanCallback);
-                                    }
-                                }, LOW_LATENCY_SCAN_MILLIS);
-            } catch (BluetoothOperationTimeoutException e) {
-                Log.d(TAG, String.format(
-                        "Cannot find a nearby device in low latency scanning after %s ms.",
-                        LOW_LATENCY_SCAN_MILLIS));
-            } finally {
-                scanner.stopScan(mScanCallback);
-            }
-            if (device == null) {
-                Log.d(TAG, "Starting low power scanning.");
-                try {
-                    device = mBluetoothOperationExecutor.executeNonnull(
-                            new Operation<BluetoothDevice>(OperationType.SCAN) {
-                                @Override
-                                public void run() throws BluetoothException {
-                                    scanner.startScan(Arrays.asList(serviceFilter),
-                                            scanSettingsLowPower, mScanCallback);
-                                }
-                            });
-                } finally {
-                    scanner.stopScan(mScanCallback);
-                }
-            }
-            Log.d(TAG, String.format("Scanning done in %d ms. Found device %s.",
-                    System.currentTimeMillis() - startTimeMillis, device));
-
-            try {
-                return connect(device);
-            } catch (BluetoothException e) {
-                retries--;
-                if (retries == 0) {
-                    throw e;
-                } else {
-                    Log.d(TAG, String.format(
-                            "Connection failed: %s. Retrying %d more times.", e, retries));
-                }
-            }
-        }
-    }
-
-    /**
-     * Connects to a device using default connection options.
-     */
-    public BluetoothGattConnection connect(BluetoothDevice bluetoothDevice)
-            throws BluetoothException {
-        return connect(bluetoothDevice, ConnectionOptions.builder().build());
-    }
-
-    /**
-     * Connects to a device using specifies connection options.
-     */
-    public BluetoothGattConnection connect(
-            BluetoothDevice bluetoothDevice, ConnectionOptions options) throws BluetoothException {
-        Log.d(TAG, String.format("Connecting to device %s.", bluetoothDevice));
-        long startTimeMillis = System.currentTimeMillis();
-
-        Operation<BluetoothGattConnection> connectOperation =
-                new Operation<BluetoothGattConnection>(OperationType.CONNECT, bluetoothDevice) {
-                    private final Object mLock = new Object();
-
-                    @GuardedBy("mLock")
-                    private boolean mIsCanceled = false;
-
-                    @GuardedBy("mLock")
-                    @Nullable(/* null before operation is executed */)
-                    private BluetoothGattWrapper mBluetoothGatt;
-
-                    @Override
-                    public void run() throws BluetoothException {
-                        synchronized (mLock) {
-                            if (mIsCanceled) {
-                                return;
-                            }
-                            BluetoothGattWrapper bluetoothGattWrapper;
-                            Log.d(TAG, "Use LE transport");
-                            bluetoothGattWrapper =
-                                    bluetoothDevice.connectGatt(
-                                            mApplicationContext,
-                                            options.autoConnect(),
-                                            mBluetoothGattCallback,
-                                            android.bluetooth.BluetoothDevice.TRANSPORT_LE);
-                            if (bluetoothGattWrapper == null) {
-                                throw new BluetoothException("connectGatt() returned null.");
-                            }
-
-                            try {
-                                // Set connection priority without waiting for connection callback.
-                                // Per code, btif_gatt_client.c, when priority is set before
-                                // connection, this sets preferred connection parameters that will
-                                // be used during the connection establishment.
-                                Optional<Integer> connectionPriorityOption =
-                                        options.connectionPriority();
-                                if (connectionPriorityOption.isPresent()) {
-                                    // requestConnectionPriority can only be called when
-                                    // BluetoothGatt is connected to the system BluetoothGatt
-                                    // service (see android/bluetooth/BluetoothGatt.java code).
-                                    // However, there is no callback to the app to inform when this
-                                    // is done. requestConnectionPriority will returns false with no
-                                    // side-effect before the service is connected, so we just poll
-                                    // here until true is returned.
-                                    int connectionPriority = connectionPriorityOption.get();
-                                    long startTimeMillis = System.currentTimeMillis();
-                                    while (!bluetoothGattWrapper.requestConnectionPriority(
-                                            connectionPriority)) {
-                                        if (System.currentTimeMillis() - startTimeMillis
-                                                > options.connectionTimeoutMillis()) {
-                                            throw new BluetoothException(
-                                                    String.format(
-                                                            Locale.US,
-                                                            "Failed to set connectionPriority "
-                                                                    + "after %dms.",
-                                                            options.connectionTimeoutMillis()));
-                                        }
-                                        try {
-                                            Thread.sleep(POLL_INTERVAL_MILLIS);
-                                        } catch (InterruptedException e) {
-                                            Thread.currentThread().interrupt();
-                                            throw new BluetoothException(
-                                                    "connect() operation interrupted.");
-                                        }
-                                    }
-                                }
-                            } catch (Exception e) {
-                                // Make sure to clean connection.
-                                bluetoothGattWrapper.disconnect();
-                                bluetoothGattWrapper.close();
-                                throw e;
-                            }
-
-                            BluetoothGattConnection connection = new BluetoothGattConnection(
-                                    bluetoothGattWrapper, mBluetoothOperationExecutor, options);
-                            mConnections.put(bluetoothGattWrapper, connection);
-                            mBluetoothGatt = bluetoothGattWrapper;
-                        }
-                    }
-
-                    @Override
-                    public void cancel() {
-                        // Clean connection if connection times out.
-                        synchronized (mLock) {
-                            if (mIsCanceled) {
-                                return;
-                            }
-                            mIsCanceled = true;
-                            BluetoothGattWrapper bluetoothGattWrapper = mBluetoothGatt;
-                            if (bluetoothGattWrapper == null) {
-                                return;
-                            }
-                            mConnections.remove(bluetoothGattWrapper);
-                            bluetoothGattWrapper.disconnect();
-                            bluetoothGattWrapper.close();
-                        }
-                    }
-                };
-        BluetoothGattConnection result;
-        if (options.autoConnect()) {
-            result = mBluetoothOperationExecutor.executeNonnull(connectOperation);
-        } else {
-            result =
-                    mBluetoothOperationExecutor.executeNonnull(
-                            connectOperation, options.connectionTimeoutMillis());
-        }
-        Log.d(TAG, String.format("Connection success in %d ms.",
-                System.currentTimeMillis() - startTimeMillis));
-        return result;
-    }
-
-    private BluetoothGattConnection getConnectionByGatt(BluetoothGattWrapper gatt)
-            throws BluetoothException {
-        BluetoothGattConnection connection = mConnections.get(gatt);
-        if (connection == null) {
-            throw new BluetoothException("Receive callback on unexpected device: " + gatt);
-        }
-        return connection;
-    }
-
-    private class InternalBluetoothGattCallback extends BluetoothGattCallback {
-
-        @Override
-        public void onConnectionStateChange(BluetoothGattWrapper gatt, int status, int newState) {
-            BluetoothGattConnection connection;
-            BluetoothDevice device = gatt.getDevice();
-            switch (newState) {
-                case BluetoothGatt.STATE_CONNECTED: {
-                    connection = mConnections.get(gatt);
-                    if (connection == null) {
-                        Log.w(TAG, String.format(
-                                "Received unexpected successful connection for dev %s! Ignoring.",
-                                device));
-                        break;
-                    }
-
-                    Operation<BluetoothGattConnection> operation =
-                            new Operation<>(OperationType.CONNECT, device);
-                    if (status != BluetoothGatt.GATT_SUCCESS) {
-                        mConnections.remove(gatt);
-                        gatt.disconnect();
-                        gatt.close();
-                        mBluetoothOperationExecutor.notifyCompletion(operation, status, null);
-                        break;
-                    }
-
-                    // Process connection options
-                    ConnectionOptions options = connection.getConnectionOptions();
-                    Optional<Integer> mtuOption = options.mtu();
-                    if (mtuOption.isPresent()) {
-                        // Requesting MTU and waiting for MTU callback.
-                        boolean success = gatt.requestMtu(mtuOption.get());
-                        if (!success) {
-                            mBluetoothOperationExecutor.notifyFailure(operation,
-                                    new BluetoothException(String.format(Locale.US,
-                                            "Failed to request MTU of %d for dev %s: "
-                                                    + "returned false.",
-                                            mtuOption.get(), device)));
-                            // Make sure to clean connection.
-                            mConnections.remove(gatt);
-                            gatt.disconnect();
-                            gatt.close();
-                        }
-                        break;
-                    }
-
-                    // Connection successful
-                    connection.onConnected();
-                    mBluetoothOperationExecutor.notifyCompletion(operation, status, connection);
-                    break;
-                }
-                case BluetoothGatt.STATE_DISCONNECTED: {
-                    connection = mConnections.remove(gatt);
-                    if (connection == null) {
-                        Log.w(TAG, String.format("Received unexpected disconnection"
-                                + " for device %s! Ignoring.", device));
-                        break;
-                    }
-                    if (!connection.isConnected()) {
-                        // This is a failed connection attempt
-                        if (status == BluetoothGatt.GATT_SUCCESS) {
-                            // This is weird... considering this as a failure
-                            Log.w(TAG, String.format(
-                                    "Received a success for a failed connection "
-                                            + "attempt for device %s! Ignoring.", device));
-                            status = BluetoothGatt.GATT_FAILURE;
-                        }
-                        mBluetoothOperationExecutor
-                                .notifyCompletion(new Operation<BluetoothGattConnection>(
-                                        OperationType.CONNECT, device), status, null);
-                        // Clean Gatt object in every case.
-                        gatt.disconnect();
-                        gatt.close();
-                        break;
-                    }
-                    connection.onClosed();
-                    mBluetoothOperationExecutor.notifyCompletion(
-                            new Operation<>(OperationType.DISCONNECT, device), status);
-                    break;
-                }
-                default:
-                    Log.e(TAG, "Unexpected connection state: " + newState);
-            }
-        }
-
-        @Override
-        public void onMtuChanged(BluetoothGattWrapper gatt, int mtu, int status) {
-            BluetoothGattConnection connection = mConnections.get(gatt);
-            BluetoothDevice device = gatt.getDevice();
-            if (connection == null) {
-                Log.w(TAG, String.format(
-                        "Received unexpected MTU change for device %s! Ignoring.", device));
-                return;
-            }
-            if (connection.isConnected()) {
-                // This is the callback for the deprecated BluetoothGattConnection.requestMtu.
-                mBluetoothOperationExecutor.notifyCompletion(
-                        new Operation<>(OperationType.CHANGE_MTU, gatt), status, mtu);
-            } else {
-                // This is the callback when requesting MTU right after connecting.
-                connection.onConnected();
-                mBluetoothOperationExecutor.notifyCompletion(
-                        new Operation<>(OperationType.CONNECT, device), status, connection);
-                if (status != BluetoothGatt.GATT_SUCCESS) {
-                    Log.w(TAG, String.format(
-                            "%s responds MTU change failed, status %s.", device, status));
-                    // Clean connection if it's failed.
-                    mConnections.remove(gatt);
-                    gatt.disconnect();
-                    gatt.close();
-                    return;
-                }
-            }
-        }
-
-        @Override
-        public void onServicesDiscovered(BluetoothGattWrapper gatt, int status) {
-            mBluetoothOperationExecutor.notifyCompletion(
-                    new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL, gatt), status);
-        }
-
-        @Override
-        public void onCharacteristicRead(BluetoothGattWrapper gatt,
-                BluetoothGattCharacteristic characteristic, int status) {
-            mBluetoothOperationExecutor.notifyCompletion(
-                    new Operation<byte[]>(OperationType.READ_CHARACTERISTIC, gatt, characteristic),
-                    status, characteristic.getValue());
-        }
-
-        @Override
-        public void onCharacteristicWrite(BluetoothGattWrapper gatt,
-                BluetoothGattCharacteristic characteristic, int status) {
-            mBluetoothOperationExecutor.notifyCompletion(new Operation<Void>(
-                    OperationType.WRITE_CHARACTERISTIC, gatt, characteristic), status);
-        }
-
-        @Override
-        public void onDescriptorRead(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor,
-                int status) {
-            mBluetoothOperationExecutor.notifyCompletion(
-                    new Operation<byte[]>(OperationType.READ_DESCRIPTOR, gatt, descriptor), status,
-                    descriptor.getValue());
-        }
-
-        @Override
-        public void onDescriptorWrite(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor,
-                int status) {
-            Log.d(TAG, String.format("onDescriptorWrite %s, %s, %d",
-                    gatt.getDevice(), descriptor.getUuid(), status));
-            mBluetoothOperationExecutor.notifyCompletion(
-                    new Operation<Void>(OperationType.WRITE_DESCRIPTOR, gatt, descriptor), status);
-        }
-
-        @Override
-        public void onReadRemoteRssi(BluetoothGattWrapper gatt, int rssi, int status) {
-            mBluetoothOperationExecutor.notifyCompletion(
-                    new Operation<Integer>(OperationType.READ_RSSI, gatt), status, rssi);
-        }
-
-        @Override
-        public void onReliableWriteCompleted(BluetoothGattWrapper gatt, int status) {
-            mBluetoothOperationExecutor.notifyCompletion(
-                    new Operation<Void>(OperationType.WRITE_RELIABLE, gatt), status);
-        }
-
-        @Override
-        public void onCharacteristicChanged(BluetoothGattWrapper gatt,
-                BluetoothGattCharacteristic characteristic) {
-            byte[] value = characteristic.getValue();
-            if (value == null) {
-                // Value is not supposed to be null, but just to be safe...
-                value = new byte[0];
-            }
-            Log.d(TAG, String.format("Characteristic %s changed, Gatt device: %s",
-                    characteristic.getUuid(), gatt.getDevice()));
-            try {
-                getConnectionByGatt(gatt).onCharacteristicChanged(characteristic, value);
-            } catch (BluetoothException e) {
-                Log.e(TAG, "Error in onCharacteristicChanged", e);
-            }
-        }
-    }
-
-    private class InternalScanCallback extends ScanCallback {
-
-        @Override
-        public void onScanFailed(int errorCode) {
-            String errorMessage;
-            switch (errorCode) {
-                case ScanCallback.SCAN_FAILED_ALREADY_STARTED:
-                    errorMessage = "SCAN_FAILED_ALREADY_STARTED";
-                    break;
-                case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
-                    errorMessage = "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED";
-                    break;
-                case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED:
-                    errorMessage = "SCAN_FAILED_FEATURE_UNSUPPORTED";
-                    break;
-                case ScanCallback.SCAN_FAILED_INTERNAL_ERROR:
-                    errorMessage = "SCAN_FAILED_INTERNAL_ERROR";
-                    break;
-                default:
-                    errorMessage = "Unknown error code - " + errorCode;
-            }
-            mBluetoothOperationExecutor.notifyFailure(
-                    new Operation<BluetoothDevice>(OperationType.SCAN),
-                    new BluetoothException("Scan failed: " + errorMessage));
-        }
-
-        @Override
-        public void onScanResult(int callbackType, ScanResult result) {
-            mBluetoothOperationExecutor.notifySuccess(
-                    new Operation<BluetoothDevice>(OperationType.SCAN), result.getDevice());
-        }
-    }
-
-    /**
-     * Options for {@link #connect}.
-     */
-    public static class ConnectionOptions {
-
-        private boolean mAutoConnect;
-        private long mConnectionTimeoutMillis;
-        private Optional<Integer> mConnectionPriority;
-        private Optional<Integer> mMtu;
-
-        private ConnectionOptions(boolean autoConnect, long connectionTimeoutMillis,
-                Optional<Integer> connectionPriority,
-                Optional<Integer> mtu) {
-            this.mAutoConnect = autoConnect;
-            this.mConnectionTimeoutMillis = connectionTimeoutMillis;
-            this.mConnectionPriority = connectionPriority;
-            this.mMtu = mtu;
-        }
-
-        boolean autoConnect() {
-            return mAutoConnect;
-        }
-
-        long connectionTimeoutMillis() {
-            return mConnectionTimeoutMillis;
-        }
-
-        Optional<Integer> connectionPriority() {
-            return mConnectionPriority;
-        }
-
-        Optional<Integer> mtu() {
-            return mMtu;
-        }
-
-        @Override
-        public String toString() {
-            return "ConnectionOptions{"
-                    + "autoConnect=" + mAutoConnect + ", "
-                    + "connectionTimeoutMillis=" + mConnectionTimeoutMillis + ", "
-                    + "connectionPriority=" + mConnectionPriority + ", "
-                    + "mtu=" + mMtu
-                    + "}";
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) {
-                return true;
-            }
-            if (o instanceof ConnectionOptions) {
-                ConnectionOptions that = (ConnectionOptions) o;
-                return this.mAutoConnect == that.autoConnect()
-                        && this.mConnectionTimeoutMillis == that.connectionTimeoutMillis()
-                        && this.mConnectionPriority.equals(that.connectionPriority())
-                        && this.mMtu.equals(that.mtu());
-            }
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(mAutoConnect, mConnectionTimeoutMillis, mConnectionPriority, mMtu);
-        }
-
-        /**
-         * Creates a builder of ConnectionOptions.
-         */
-        public static Builder builder() {
-            return new ConnectionOptions.Builder()
-                    .setAutoConnect(false)
-                    .setConnectionTimeoutMillis(TimeUnit.SECONDS.toMillis(5));
-        }
-
-        /**
-         * Builder for {@link ConnectionOptions}.
-         */
-        public static class Builder {
-
-            private boolean mAutoConnect;
-            private long mConnectionTimeoutMillis;
-            private Optional<Integer> mConnectionPriority = Optional.empty();
-            private Optional<Integer> mMtu = Optional.empty();
-
-            /**
-             * See {@link android.bluetooth.BluetoothDevice#connectGatt}.
-             */
-            public Builder setAutoConnect(boolean autoConnect) {
-                this.mAutoConnect = autoConnect;
-                return this;
-            }
-
-            /**
-             * See {@link android.bluetooth.BluetoothGatt#requestConnectionPriority(int)}.
-             */
-            public Builder setConnectionPriority(int connectionPriority) {
-                this.mConnectionPriority = Optional.of(connectionPriority);
-                return this;
-            }
-
-            /**
-             * See {@link android.bluetooth.BluetoothGatt#requestMtu(int)}.
-             */
-            public Builder setMtu(int mtu) {
-                this.mMtu = Optional.of(mtu);
-                return this;
-            }
-
-            /**
-             * Sets the timeout for the GATT connection.
-             */
-            public Builder setConnectionTimeoutMillis(long connectionTimeoutMillis) {
-                this.mConnectionTimeoutMillis = connectionTimeoutMillis;
-                return this;
-            }
-
-            /**
-             * Builds ConnectionOptions.
-             */
-            public ConnectionOptions build() {
-                return new ConnectionOptions(mAutoConnect, mConnectionTimeoutMillis,
-                        mConnectionPriority, mMtu);
-            }
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/NonnullProvider.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/NonnullProvider.java
deleted file mode 100644
index 16abd99..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/NonnullProvider.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability;
-
-/**
- * Provider that returns non-null instances.
- *
- * @param <T> Type of provided instance.
- */
-public interface NonnullProvider<T> {
-    /** Get a non-null instance. */
-    T get();
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/Testability.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/Testability.java
deleted file mode 100644
index 6cfdd78..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/Testability.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability;
-
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
-
-import javax.annotation.Nullable;
-
-/** Util class to convert from or to testable classes. */
-public class Testability {
-    /** Wraps a Bluetooth device. */
-    public static BluetoothDevice wrap(android.bluetooth.BluetoothDevice bluetoothDevice) {
-        return BluetoothDevice.wrap(bluetoothDevice);
-    }
-
-    /** Wraps a Bluetooth adapter. */
-    @Nullable
-    public static BluetoothAdapter wrap(
-            @Nullable android.bluetooth.BluetoothAdapter bluetoothAdapter) {
-        return BluetoothAdapter.wrap(bluetoothAdapter);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/TimeProvider.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/TimeProvider.java
deleted file mode 100644
index a4de913..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/TimeProvider.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability;
-
-/** Provider of time for testability. */
-public class TimeProvider {
-    public long getTimeMillis() {
-        return System.currentTimeMillis();
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/VersionProvider.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/VersionProvider.java
deleted file mode 100644
index f46ea7a..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/VersionProvider.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability;
-
-import android.os.Build.VERSION;
-
-/**
- * Provider of android sdk version for testability
- */
-public class VersionProvider {
-    public int getSdkInt() {
-        return VERSION.SDK_INT;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapter.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapter.java
deleted file mode 100644
index afa2a1b..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapter.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import android.annotation.TargetApi;
-import android.os.Build;
-
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeAdvertiser;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeScanner;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-import javax.annotation.Nullable;
-
-/**
- * Mockable wrapper of {@link android.bluetooth.BluetoothAdapter}.
- */
-public class BluetoothAdapter {
-    /** See {@link android.bluetooth.BluetoothAdapter#ACTION_REQUEST_ENABLE}. */
-    public static final String ACTION_REQUEST_ENABLE =
-            android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE;
-
-    /** See {@link android.bluetooth.BluetoothAdapter#ACTION_STATE_CHANGED}. */
-    public static final String ACTION_STATE_CHANGED =
-            android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
-
-    /** See {@link android.bluetooth.BluetoothAdapter#EXTRA_STATE}. */
-    public static final String EXTRA_STATE =
-            android.bluetooth.BluetoothAdapter.EXTRA_STATE;
-
-    /** See {@link android.bluetooth.BluetoothAdapter#STATE_OFF}. */
-    public static final int STATE_OFF =
-            android.bluetooth.BluetoothAdapter.STATE_OFF;
-
-    /** See {@link android.bluetooth.BluetoothAdapter#STATE_ON}. */
-    public static final int STATE_ON =
-            android.bluetooth.BluetoothAdapter.STATE_ON;
-
-    /** See {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_OFF}. */
-    public static final int STATE_TURNING_OFF =
-            android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF;
-
-    /** See {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_ON}. */
-    public static final int STATE_TURNING_ON =
-            android.bluetooth.BluetoothAdapter.STATE_TURNING_ON;
-
-    private final android.bluetooth.BluetoothAdapter mWrappedBluetoothAdapter;
-
-    private BluetoothAdapter(android.bluetooth.BluetoothAdapter bluetoothAdapter) {
-        mWrappedBluetoothAdapter = bluetoothAdapter;
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#disable()}. */
-    public boolean disable() {
-        return mWrappedBluetoothAdapter.disable();
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#enable()}. */
-    public boolean enable() {
-        return mWrappedBluetoothAdapter.enable();
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#getBluetoothLeScanner}. */
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    @Nullable
-    public BluetoothLeScanner getBluetoothLeScanner() {
-        return BluetoothLeScanner.wrap(mWrappedBluetoothAdapter.getBluetoothLeScanner());
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#getBluetoothLeAdvertiser()}. */
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    @Nullable
-    public BluetoothLeAdvertiser getBluetoothLeAdvertiser() {
-        return BluetoothLeAdvertiser.wrap(mWrappedBluetoothAdapter.getBluetoothLeAdvertiser());
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#getBondedDevices()}. */
-    @Nullable
-    public Set<BluetoothDevice> getBondedDevices() {
-        Set<android.bluetooth.BluetoothDevice> bondedDevices =
-                mWrappedBluetoothAdapter.getBondedDevices();
-        if (bondedDevices == null) {
-            return null;
-        }
-        Set<BluetoothDevice> result = new HashSet<BluetoothDevice>();
-        for (android.bluetooth.BluetoothDevice device : bondedDevices) {
-            if (device == null) {
-                continue;
-            }
-            result.add(BluetoothDevice.wrap(device));
-        }
-        return Collections.unmodifiableSet(result);
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#getRemoteDevice(byte[])}. */
-    public BluetoothDevice getRemoteDevice(byte[] address) {
-        return BluetoothDevice.wrap(mWrappedBluetoothAdapter.getRemoteDevice(address));
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#getRemoteDevice(String)}. */
-    public BluetoothDevice getRemoteDevice(String address) {
-        return BluetoothDevice.wrap(mWrappedBluetoothAdapter.getRemoteDevice(address));
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#isEnabled()}. */
-    public boolean isEnabled() {
-        return mWrappedBluetoothAdapter.isEnabled();
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#isDiscovering()}. */
-    public boolean isDiscovering() {
-        return mWrappedBluetoothAdapter.isDiscovering();
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#startDiscovery()}. */
-    public boolean startDiscovery() {
-        return mWrappedBluetoothAdapter.startDiscovery();
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#cancelDiscovery()}. */
-    public boolean cancelDiscovery() {
-        return mWrappedBluetoothAdapter.cancelDiscovery();
-    }
-
-    /** See {@link android.bluetooth.BluetoothAdapter#getDefaultAdapter()}. */
-    @Nullable
-    public static BluetoothAdapter getDefaultAdapter() {
-        android.bluetooth.BluetoothAdapter adapter =
-                android.bluetooth.BluetoothAdapter.getDefaultAdapter();
-        if (adapter == null) {
-            return null;
-        }
-        return new BluetoothAdapter(adapter);
-    }
-
-    /** Wraps a Bluetooth adapter. */
-    @Nullable
-    public static BluetoothAdapter wrap(
-            @Nullable android.bluetooth.BluetoothAdapter bluetoothAdapter) {
-        if (bluetoothAdapter == null) {
-            return null;
-        }
-        return new BluetoothAdapter(bluetoothAdapter);
-    }
-
-    /** Unwraps a Bluetooth adapter. */
-    public android.bluetooth.BluetoothAdapter unwrap() {
-        return mWrappedBluetoothAdapter;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java
deleted file mode 100644
index b2002c5..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import android.annotation.TargetApi;
-import android.bluetooth.BluetoothClass;
-import android.bluetooth.BluetoothSocket;
-import android.content.Context;
-import android.os.ParcelUuid;
-
-import java.io.IOException;
-import java.util.UUID;
-
-import javax.annotation.Nullable;
-
-/**
- * Mockable wrapper of {@link android.bluetooth.BluetoothDevice}.
- */
-@TargetApi(18)
-public class BluetoothDevice {
-    /** See {@link android.bluetooth.BluetoothDevice#BOND_BONDED}. */
-    public static final int BOND_BONDED = android.bluetooth.BluetoothDevice.BOND_BONDED;
-
-    /** See {@link android.bluetooth.BluetoothDevice#BOND_BONDING}. */
-    public static final int BOND_BONDING = android.bluetooth.BluetoothDevice.BOND_BONDING;
-
-    /** See {@link android.bluetooth.BluetoothDevice#BOND_NONE}. */
-    public static final int BOND_NONE = android.bluetooth.BluetoothDevice.BOND_NONE;
-
-    /** See {@link android.bluetooth.BluetoothDevice#ACTION_ACL_CONNECTED}. */
-    public static final String ACTION_ACL_CONNECTED =
-            android.bluetooth.BluetoothDevice.ACTION_ACL_CONNECTED;
-
-    /** See {@link android.bluetooth.BluetoothDevice#ACTION_ACL_DISCONNECT_REQUESTED}. */
-    public static final String ACTION_ACL_DISCONNECT_REQUESTED =
-            android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED;
-
-    /** See {@link android.bluetooth.BluetoothDevice#ACTION_ACL_DISCONNECTED}. */
-    public static final String ACTION_ACL_DISCONNECTED =
-            android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECTED;
-
-    /** See {@link android.bluetooth.BluetoothDevice#ACTION_BOND_STATE_CHANGED}. */
-    public static final String ACTION_BOND_STATE_CHANGED =
-            android.bluetooth.BluetoothDevice.ACTION_BOND_STATE_CHANGED;
-
-    /** See {@link android.bluetooth.BluetoothDevice#ACTION_CLASS_CHANGED}. */
-    public static final String ACTION_CLASS_CHANGED =
-            android.bluetooth.BluetoothDevice.ACTION_CLASS_CHANGED;
-
-    /** See {@link android.bluetooth.BluetoothDevice#ACTION_FOUND}. */
-    public static final String ACTION_FOUND = android.bluetooth.BluetoothDevice.ACTION_FOUND;
-
-    /** See {@link android.bluetooth.BluetoothDevice#ACTION_NAME_CHANGED}. */
-    public static final String ACTION_NAME_CHANGED =
-            android.bluetooth.BluetoothDevice.ACTION_NAME_CHANGED;
-
-    /** See {@link android.bluetooth.BluetoothDevice#ACTION_PAIRING_REQUEST}. */
-    // API 19 only
-    public static final String ACTION_PAIRING_REQUEST =
-            "android.bluetooth.device.action.PAIRING_REQUEST";
-
-    /** See {@link android.bluetooth.BluetoothDevice#ACTION_UUID}. */
-    public static final String ACTION_UUID = android.bluetooth.BluetoothDevice.ACTION_UUID;
-
-    /** See {@link android.bluetooth.BluetoothDevice#DEVICE_TYPE_CLASSIC}. */
-    public static final int DEVICE_TYPE_CLASSIC =
-            android.bluetooth.BluetoothDevice.DEVICE_TYPE_CLASSIC;
-
-    /** See {@link android.bluetooth.BluetoothDevice#DEVICE_TYPE_DUAL}. */
-    public static final int DEVICE_TYPE_DUAL = android.bluetooth.BluetoothDevice.DEVICE_TYPE_DUAL;
-
-    /** See {@link android.bluetooth.BluetoothDevice#DEVICE_TYPE_LE}. */
-    public static final int DEVICE_TYPE_LE = android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE;
-
-    /** See {@link android.bluetooth.BluetoothDevice#DEVICE_TYPE_UNKNOWN}. */
-    public static final int DEVICE_TYPE_UNKNOWN =
-            android.bluetooth.BluetoothDevice.DEVICE_TYPE_UNKNOWN;
-
-    /** See {@link android.bluetooth.BluetoothDevice#ERROR}. */
-    public static final int ERROR = android.bluetooth.BluetoothDevice.ERROR;
-
-    /** See {@link android.bluetooth.BluetoothDevice#EXTRA_BOND_STATE}. */
-    public static final String EXTRA_BOND_STATE =
-            android.bluetooth.BluetoothDevice.EXTRA_BOND_STATE;
-
-    /** See {@link android.bluetooth.BluetoothDevice#EXTRA_CLASS}. */
-    public static final String EXTRA_CLASS = android.bluetooth.BluetoothDevice.EXTRA_CLASS;
-
-    /** See {@link android.bluetooth.BluetoothDevice#EXTRA_DEVICE}. */
-    public static final String EXTRA_DEVICE = android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
-
-    /** See {@link android.bluetooth.BluetoothDevice#EXTRA_NAME}. */
-    public static final String EXTRA_NAME = android.bluetooth.BluetoothDevice.EXTRA_NAME;
-
-    /** See {@link android.bluetooth.BluetoothDevice#EXTRA_PAIRING_KEY}. */
-    // API 19 only
-    public static final String EXTRA_PAIRING_KEY = "android.bluetooth.device.extra.PAIRING_KEY";
-
-    /** See {@link android.bluetooth.BluetoothDevice#EXTRA_PAIRING_VARIANT}. */
-    // API 19 only
-    public static final String EXTRA_PAIRING_VARIANT =
-            "android.bluetooth.device.extra.PAIRING_VARIANT";
-
-    /** See {@link android.bluetooth.BluetoothDevice#EXTRA_PREVIOUS_BOND_STATE}. */
-    public static final String EXTRA_PREVIOUS_BOND_STATE =
-            android.bluetooth.BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE;
-
-    /** See {@link android.bluetooth.BluetoothDevice#EXTRA_RSSI}. */
-    public static final String EXTRA_RSSI = android.bluetooth.BluetoothDevice.EXTRA_RSSI;
-
-    /** See {@link android.bluetooth.BluetoothDevice#EXTRA_UUID}. */
-    public static final String EXTRA_UUID = android.bluetooth.BluetoothDevice.EXTRA_UUID;
-
-    /** See {@link android.bluetooth.BluetoothDevice#PAIRING_VARIANT_PASSKEY_CONFIRMATION}. */
-    // API 19 only
-    public static final int PAIRING_VARIANT_PASSKEY_CONFIRMATION = 2;
-
-    /** See {@link android.bluetooth.BluetoothDevice#PAIRING_VARIANT_PIN}. */
-    // API 19 only
-    public static final int PAIRING_VARIANT_PIN = 0;
-
-    private final android.bluetooth.BluetoothDevice mWrappedBluetoothDevice;
-
-    private BluetoothDevice(android.bluetooth.BluetoothDevice bluetoothDevice) {
-        mWrappedBluetoothDevice = bluetoothDevice;
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothDevice#connectGatt(Context, boolean,
-     * android.bluetooth.BluetoothGattCallback)}.
-     */
-    @Nullable(/* when bt service is not available */)
-    public BluetoothGattWrapper connectGatt(Context context, boolean autoConnect,
-            BluetoothGattCallback callback) {
-        android.bluetooth.BluetoothGatt gatt =
-                mWrappedBluetoothDevice.connectGatt(context, autoConnect, callback.unwrap());
-        if (gatt == null) {
-            return null;
-        }
-        return BluetoothGattWrapper.wrap(gatt);
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothDevice#connectGatt(Context, boolean,
-     * android.bluetooth.BluetoothGattCallback, int)}.
-     */
-    @TargetApi(23)
-    @Nullable(/* when bt service is not available */)
-    public BluetoothGattWrapper connectGatt(Context context, boolean autoConnect,
-            BluetoothGattCallback callback, int transport) {
-        android.bluetooth.BluetoothGatt gatt =
-                mWrappedBluetoothDevice.connectGatt(
-                        context, autoConnect, callback.unwrap(), transport);
-        if (gatt == null) {
-            return null;
-        }
-        return BluetoothGattWrapper.wrap(gatt);
-    }
-
-
-    /**
-     * See {@link android.bluetooth.BluetoothDevice#createRfcommSocketToServiceRecord(UUID)}.
-     */
-    public BluetoothSocket createRfcommSocketToServiceRecord(UUID uuid) throws IOException {
-        return mWrappedBluetoothDevice.createRfcommSocketToServiceRecord(uuid);
-    }
-
-    /**
-     * See
-     * {@link android.bluetooth.BluetoothDevice#createInsecureRfcommSocketToServiceRecord(UUID)}.
-     */
-    public BluetoothSocket createInsecureRfcommSocketToServiceRecord(UUID uuid) throws IOException {
-        return mWrappedBluetoothDevice.createInsecureRfcommSocketToServiceRecord(uuid);
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#setPairingConfirmation(boolean)}. */
-    public boolean setPairingConfirmation(boolean confirm) {
-        return mWrappedBluetoothDevice.setPairingConfirmation(confirm);
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#fetchUuidsWithSdp()}. */
-    public boolean fetchUuidsWithSdp() {
-        return mWrappedBluetoothDevice.fetchUuidsWithSdp();
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#createBond()}. */
-    public boolean createBond() {
-        return mWrappedBluetoothDevice.createBond();
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#getUuids()}. */
-    @Nullable(/* on error */)
-    public ParcelUuid[] getUuids() {
-        return mWrappedBluetoothDevice.getUuids();
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#getBondState()}. */
-    public int getBondState() {
-        return mWrappedBluetoothDevice.getBondState();
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#getAddress()}. */
-    public String getAddress() {
-        return mWrappedBluetoothDevice.getAddress();
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#getBluetoothClass()}. */
-    @Nullable(/* on error */)
-    public BluetoothClass getBluetoothClass() {
-        return mWrappedBluetoothDevice.getBluetoothClass();
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#getType()}. */
-    public int getType() {
-        return mWrappedBluetoothDevice.getType();
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#getName()}. */
-    @Nullable(/* on error */)
-    public String getName() {
-        return mWrappedBluetoothDevice.getName();
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#toString()}. */
-    @Override
-    public String toString() {
-        return mWrappedBluetoothDevice.toString();
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#hashCode()}. */
-    @Override
-    public int hashCode() {
-        return mWrappedBluetoothDevice.hashCode();
-    }
-
-    /** See {@link android.bluetooth.BluetoothDevice#equals(Object)}. */
-    @Override
-    public boolean equals(@Nullable Object o) {
-        if (o ==  this) {
-            return true;
-        }
-        if (!(o instanceof BluetoothDevice)) {
-            return false;
-        }
-        return mWrappedBluetoothDevice.equals(((BluetoothDevice) o).unwrap());
-    }
-
-    /** Unwraps a Bluetooth device. */
-    public android.bluetooth.BluetoothDevice unwrap() {
-        return mWrappedBluetoothDevice;
-    }
-
-    /** Wraps a Bluetooth device. */
-    public static BluetoothDevice wrap(android.bluetooth.BluetoothDevice bluetoothDevice) {
-        return new BluetoothDevice(bluetoothDevice);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallback.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallback.java
deleted file mode 100644
index d36cfa2..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallback.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-
-/**
- * Wrapper of {@link android.bluetooth.BluetoothGattCallback} that uses mockable objects.
- */
-public abstract class BluetoothGattCallback {
-
-    private final android.bluetooth.BluetoothGattCallback mWrappedBluetoothGattCallback =
-            new InternalBluetoothGattCallback();
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattCallback#onConnectionStateChange(
-     * android.bluetooth.BluetoothGatt, int, int)}
-     */
-    public void onConnectionStateChange(BluetoothGattWrapper gatt, int status, int newState) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattCallback#onServicesDiscovered(
-     * android.bluetooth.BluetoothGatt,int)}
-     */
-    public void onServicesDiscovered(BluetoothGattWrapper gatt, int status) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattCallback#onCharacteristicRead(
-     * android.bluetooth.BluetoothGatt, BluetoothGattCharacteristic, int)}
-     */
-    public void onCharacteristicRead(BluetoothGattWrapper gatt, BluetoothGattCharacteristic
-            characteristic, int status) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattCallback#onCharacteristicWrite(
-     * android.bluetooth.BluetoothGatt, BluetoothGattCharacteristic, int)}
-     */
-    public void onCharacteristicWrite(BluetoothGattWrapper gatt,
-            BluetoothGattCharacteristic characteristic, int status) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattCallback#onDescriptorRead(
-     * android.bluetooth.BluetoothGatt, BluetoothGattDescriptor, int)}
-     */
-    public void onDescriptorRead(
-            BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor, int status) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattCallback#onDescriptorWrite(
-     * android.bluetooth.BluetoothGatt, BluetoothGattDescriptor, int)}
-     */
-    public void onDescriptorWrite(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor,
-            int status) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattCallback#onReadRemoteRssi(
-     * android.bluetooth.BluetoothGatt, int, int)}
-     */
-    public void onReadRemoteRssi(BluetoothGattWrapper gatt, int rssi, int status) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattCallback#onReliableWriteCompleted(
-     * android.bluetooth.BluetoothGatt, int)}
-     */
-    public void onReliableWriteCompleted(BluetoothGattWrapper gatt, int status) {}
-
-    /**
-     * See
-     * {@link android.bluetooth.BluetoothGattCallback#onMtuChanged(android.bluetooth.BluetoothGatt,
-     * int, int)}
-     */
-    public void onMtuChanged(BluetoothGattWrapper gatt, int mtu, int status) {}
-
-    /**
-     * See
-     * {@link android.bluetooth.BluetoothGattCallback#onCharacteristicChanged(
-     * android.bluetooth.BluetoothGatt, BluetoothGattCharacteristic)}
-     */
-    public void onCharacteristicChanged(BluetoothGattWrapper gatt,
-            BluetoothGattCharacteristic characteristic) {}
-
-    /** Unwraps a Bluetooth Gatt callback. */
-    public android.bluetooth.BluetoothGattCallback unwrap() {
-        return mWrappedBluetoothGattCallback;
-    }
-
-    /** Forward callback to testable instance. */
-    private class InternalBluetoothGattCallback extends android.bluetooth.BluetoothGattCallback {
-        @Override
-        public void onConnectionStateChange(android.bluetooth.BluetoothGatt gatt, int status,
-                int newState) {
-            BluetoothGattCallback.this.onConnectionStateChange(BluetoothGattWrapper.wrap(gatt),
-                    status, newState);
-        }
-
-        @Override
-        public void onServicesDiscovered(android.bluetooth.BluetoothGatt gatt, int status) {
-            BluetoothGattCallback.this.onServicesDiscovered(BluetoothGattWrapper.wrap(gatt),
-                    status);
-        }
-
-        @Override
-        public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt,
-                BluetoothGattCharacteristic characteristic, int status) {
-            BluetoothGattCallback.this.onCharacteristicRead(
-                    BluetoothGattWrapper.wrap(gatt), characteristic, status);
-        }
-
-        @Override
-        public void onCharacteristicWrite(android.bluetooth.BluetoothGatt gatt,
-                BluetoothGattCharacteristic characteristic, int status) {
-            BluetoothGattCallback.this.onCharacteristicWrite(
-                    BluetoothGattWrapper.wrap(gatt), characteristic, status);
-        }
-
-        @Override
-        public void onDescriptorRead(android.bluetooth.BluetoothGatt gatt,
-                BluetoothGattDescriptor descriptor, int status) {
-            BluetoothGattCallback.this.onDescriptorRead(
-                    BluetoothGattWrapper.wrap(gatt), descriptor, status);
-        }
-
-        @Override
-        public void onDescriptorWrite(android.bluetooth.BluetoothGatt gatt,
-                BluetoothGattDescriptor descriptor, int status) {
-            BluetoothGattCallback.this.onDescriptorWrite(
-                    BluetoothGattWrapper.wrap(gatt), descriptor, status);
-        }
-
-        @Override
-        public void onReadRemoteRssi(android.bluetooth.BluetoothGatt gatt, int rssi, int status) {
-            BluetoothGattCallback.this.onReadRemoteRssi(BluetoothGattWrapper.wrap(gatt), rssi,
-                    status);
-        }
-
-        @Override
-        public void onReliableWriteCompleted(android.bluetooth.BluetoothGatt gatt, int status) {
-            BluetoothGattCallback.this.onReliableWriteCompleted(BluetoothGattWrapper.wrap(gatt),
-                    status);
-        }
-
-        @Override
-        public void onMtuChanged(android.bluetooth.BluetoothGatt gatt, int mtu, int status) {
-            BluetoothGattCallback.this.onMtuChanged(BluetoothGattWrapper.wrap(gatt), mtu, status);
-        }
-
-        @Override
-        public void onCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
-                BluetoothGattCharacteristic characteristic) {
-            BluetoothGattCallback.this.onCharacteristicChanged(
-                    BluetoothGattWrapper.wrap(gatt), characteristic);
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java
deleted file mode 100644
index d4873fd..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-
-import java.util.UUID;
-
-import javax.annotation.Nullable;
-
-/**
- * Mockable wrapper of {@link android.bluetooth.BluetoothGattServer}.
- */
-public class BluetoothGattServer {
-
-    /** See {@link android.bluetooth.BluetoothGattServer#STATE_CONNECTED}. */
-    public static final int STATE_CONNECTED = android.bluetooth.BluetoothGattServer.STATE_CONNECTED;
-
-    /** See {@link android.bluetooth.BluetoothGattServer#STATE_DISCONNECTED}. */
-    public static final int STATE_DISCONNECTED =
-            android.bluetooth.BluetoothGattServer.STATE_DISCONNECTED;
-
-    private android.bluetooth.BluetoothGattServer mWrappedInstance;
-
-    private BluetoothGattServer(android.bluetooth.BluetoothGattServer instance) {
-        mWrappedInstance = instance;
-    }
-
-    /** Wraps a Bluetooth Gatt server. */
-    @Nullable
-    public static BluetoothGattServer wrap(
-            @Nullable android.bluetooth.BluetoothGattServer instance) {
-        if (instance == null) {
-            return null;
-        }
-        return new BluetoothGattServer(instance);
-    }
-
-    /** Unwraps a Bluetooth Gatt server. */
-    public android.bluetooth.BluetoothGattServer unwrap() {
-        return mWrappedInstance;
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServer#connect(
-     * android.bluetooth.BluetoothDevice, boolean)}
-     */
-    public boolean connect(BluetoothDevice device, boolean autoConnect) {
-        return mWrappedInstance.connect(device.unwrap(), autoConnect);
-    }
-
-    /** See {@link android.bluetooth.BluetoothGattServer#addService(BluetoothGattService)}. */
-    public boolean addService(BluetoothGattService service) {
-        return mWrappedInstance.addService(service);
-    }
-
-    /** See {@link android.bluetooth.BluetoothGattServer#clearServices()}. */
-    public void clearServices() {
-        mWrappedInstance.clearServices();
-    }
-
-    /** See {@link android.bluetooth.BluetoothGattServer#close()}. */
-    public void close() {
-        mWrappedInstance.close();
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServer#notifyCharacteristicChanged(
-     * android.bluetooth.BluetoothDevice, BluetoothGattCharacteristic, boolean)}.
-     */
-    public boolean notifyCharacteristicChanged(BluetoothDevice device,
-            BluetoothGattCharacteristic characteristic, boolean confirm) {
-        return mWrappedInstance.notifyCharacteristicChanged(
-                device.unwrap(), characteristic, confirm);
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServer#sendResponse(
-     * android.bluetooth.BluetoothDevice, int, int, int, byte[])}.
-     */
-    public void sendResponse(BluetoothDevice device, int requestId, int status, int offset,
-            @Nullable byte[] value) {
-        mWrappedInstance.sendResponse(device.unwrap(), requestId, status, offset, value);
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServer#cancelConnection(
-     * android.bluetooth.BluetoothDevice)}.
-     */
-    public void cancelConnection(BluetoothDevice device) {
-        mWrappedInstance.cancelConnection(device.unwrap());
-    }
-
-    /** See {@link android.bluetooth.BluetoothGattServer#getService(UUID uuid)}. */
-    public BluetoothGattService getService(UUID uuid) {
-        return mWrappedInstance.getService(uuid);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallback.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallback.java
deleted file mode 100644
index 875dad5..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallback.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-
-/**
- * Wrapper of {@link android.bluetooth.BluetoothGattServerCallback} that uses mockable objects.
- */
-public abstract class BluetoothGattServerCallback {
-
-    private final android.bluetooth.BluetoothGattServerCallback mWrappedInstance =
-            new InternalBluetoothGattServerCallback();
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServerCallback#onCharacteristicReadRequest(
-     * android.bluetooth.BluetoothDevice, int, int, BluetoothGattCharacteristic)}
-     */
-    public void onCharacteristicReadRequest(BluetoothDevice device, int requestId,
-            int offset, BluetoothGattCharacteristic characteristic) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServerCallback#onCharacteristicWriteRequest(
-     * android.bluetooth.BluetoothDevice, int, BluetoothGattCharacteristic, boolean, boolean, int,
-     * byte[])}
-     */
-    public void onCharacteristicWriteRequest(BluetoothDevice device,
-            int requestId,
-            BluetoothGattCharacteristic characteristic,
-            boolean preparedWrite,
-            boolean responseNeeded,
-            int offset,
-            byte[] value) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServerCallback#onConnectionStateChange(
-     * android.bluetooth.BluetoothDevice, int, int)}
-     */
-    public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServerCallback#onDescriptorReadRequest(
-     * android.bluetooth.BluetoothDevice, int, int, BluetoothGattDescriptor)}
-     */
-    public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
-            BluetoothGattDescriptor descriptor) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServerCallback#onDescriptorWriteRequest(
-     * android.bluetooth.BluetoothDevice, int, BluetoothGattDescriptor, boolean, boolean, int,
-     * byte[])}
-     */
-    public void onDescriptorWriteRequest(BluetoothDevice device,
-            int requestId,
-            BluetoothGattDescriptor descriptor,
-            boolean preparedWrite,
-            boolean responseNeeded,
-            int offset,
-            byte[] value) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServerCallback#onExecuteWrite(
-     * android.bluetooth.BluetoothDevice, int, boolean)}
-     */
-    public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServerCallback#onMtuChanged(
-     * android.bluetooth.BluetoothDevice, int)}
-     */
-    public void onMtuChanged(BluetoothDevice device, int mtu) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServerCallback#onNotificationSent(
-     * android.bluetooth.BluetoothDevice, int)}
-     */
-    public void onNotificationSent(BluetoothDevice device, int status) {}
-
-    /**
-     * See {@link android.bluetooth.BluetoothGattServerCallback#onServiceAdded(int,
-     * BluetoothGattService)}
-     */
-    public void onServiceAdded(int status, BluetoothGattService service) {}
-
-    /** Unwraps a Bluetooth Gatt server callback. */
-    public android.bluetooth.BluetoothGattServerCallback unwrap() {
-        return mWrappedInstance;
-    }
-
-    /** Forward callback to testable instance. */
-    private class InternalBluetoothGattServerCallback extends
-            android.bluetooth.BluetoothGattServerCallback {
-        @Override
-        public void onCharacteristicReadRequest(android.bluetooth.BluetoothDevice device,
-                int requestId, int offset, BluetoothGattCharacteristic characteristic) {
-            BluetoothGattServerCallback.this.onCharacteristicReadRequest(
-                    BluetoothDevice.wrap(device), requestId, offset, characteristic);
-        }
-
-        @Override
-        public void onCharacteristicWriteRequest(android.bluetooth.BluetoothDevice device,
-                int requestId,
-                BluetoothGattCharacteristic characteristic,
-                boolean preparedWrite,
-                boolean responseNeeded,
-                int offset,
-                byte[] value) {
-            BluetoothGattServerCallback.this.onCharacteristicWriteRequest(
-                    BluetoothDevice.wrap(device),
-                    requestId,
-                    characteristic,
-                    preparedWrite,
-                    responseNeeded,
-                    offset,
-                    value);
-        }
-
-        @Override
-        public void onConnectionStateChange(android.bluetooth.BluetoothDevice device, int status,
-                int newState) {
-            BluetoothGattServerCallback.this.onConnectionStateChange(
-                    BluetoothDevice.wrap(device), status, newState);
-        }
-
-        @Override
-        public void onDescriptorReadRequest(android.bluetooth.BluetoothDevice device, int requestId,
-                int offset, BluetoothGattDescriptor descriptor) {
-            BluetoothGattServerCallback.this.onDescriptorReadRequest(BluetoothDevice.wrap(device),
-                    requestId, offset, descriptor);
-        }
-
-        @Override
-        public void onDescriptorWriteRequest(android.bluetooth.BluetoothDevice device,
-                int requestId,
-                BluetoothGattDescriptor descriptor,
-                boolean preparedWrite,
-                boolean responseNeeded,
-                int offset,
-                byte[] value) {
-            BluetoothGattServerCallback.this.onDescriptorWriteRequest(BluetoothDevice.wrap(device),
-                    requestId,
-                    descriptor,
-                    preparedWrite,
-                    responseNeeded,
-                    offset,
-                    value);
-        }
-
-        @Override
-        public void onExecuteWrite(android.bluetooth.BluetoothDevice device, int requestId,
-                boolean execute) {
-            BluetoothGattServerCallback.this.onExecuteWrite(BluetoothDevice.wrap(device), requestId,
-                    execute);
-        }
-
-        @Override
-        public void onMtuChanged(android.bluetooth.BluetoothDevice device, int mtu) {
-            BluetoothGattServerCallback.this.onMtuChanged(BluetoothDevice.wrap(device), mtu);
-        }
-
-        @Override
-        public void onNotificationSent(android.bluetooth.BluetoothDevice device, int status) {
-            BluetoothGattServerCallback.this.onNotificationSent(
-                    BluetoothDevice.wrap(device), status);
-        }
-
-        @Override
-        public void onServiceAdded(int status, BluetoothGattService service) {
-            BluetoothGattServerCallback.this.onServiceAdded(status, service);
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapper.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapper.java
deleted file mode 100644
index 453ee5d..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapper.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import android.annotation.TargetApi;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.os.Build;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.List;
-import java.util.UUID;
-
-import javax.annotation.Nullable;
-
-/** Mockable wrapper of {@link android.bluetooth.BluetoothGatt}. */
-@TargetApi(Build.VERSION_CODES.TIRAMISU)
-public class BluetoothGattWrapper {
-    private final android.bluetooth.BluetoothGatt mWrappedBluetoothGatt;
-
-    private BluetoothGattWrapper(android.bluetooth.BluetoothGatt bluetoothGatt) {
-        mWrappedBluetoothGatt = bluetoothGatt;
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#getDevice()}. */
-    public BluetoothDevice getDevice() {
-        return BluetoothDevice.wrap(mWrappedBluetoothGatt.getDevice());
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#getServices()}. */
-    public List<BluetoothGattService> getServices() {
-        return mWrappedBluetoothGatt.getServices();
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#getService(UUID)}. */
-    @Nullable(/* null if service is not found */)
-    public BluetoothGattService getService(UUID uuid) {
-        return mWrappedBluetoothGatt.getService(uuid);
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#discoverServices()}. */
-    public boolean discoverServices() {
-        return mWrappedBluetoothGatt.discoverServices();
-    }
-
-    /**
-     * Hidden method. Clears the internal cache and forces a refresh of the services from the remote
-     * device.
-     */
-    // TODO(b/201300471): remove refresh call using reflection.
-    public boolean refresh() {
-        try {
-            Method refreshMethod = android.bluetooth.BluetoothGatt.class.getMethod("refresh");
-            return (Boolean) refreshMethod.invoke(mWrappedBluetoothGatt);
-        } catch (NoSuchMethodException
-            | IllegalAccessException
-            | IllegalArgumentException
-            | InvocationTargetException e) {
-            return false;
-        }
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)}.
-     */
-    public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) {
-        return mWrappedBluetoothGatt.readCharacteristic(characteristic);
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic,
-     * byte[], int)} .
-     */
-    public int writeCharacteristic(BluetoothGattCharacteristic characteristic, byte[] value,
-            int writeType) {
-        return mWrappedBluetoothGatt.writeCharacteristic(characteristic, value, writeType);
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#readDescriptor(BluetoothGattDescriptor)}. */
-    public boolean readDescriptor(BluetoothGattDescriptor descriptor) {
-        return mWrappedBluetoothGatt.readDescriptor(descriptor);
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothGatt#writeDescriptor(BluetoothGattDescriptor,
-     * byte[])}.
-     */
-    public int writeDescriptor(BluetoothGattDescriptor descriptor, byte[] value) {
-        return mWrappedBluetoothGatt.writeDescriptor(descriptor, value);
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#readRemoteRssi()}. */
-    public boolean readRemoteRssi() {
-        return mWrappedBluetoothGatt.readRemoteRssi();
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#requestConnectionPriority(int)}. */
-    public boolean requestConnectionPriority(int connectionPriority) {
-        return mWrappedBluetoothGatt.requestConnectionPriority(connectionPriority);
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#requestMtu(int)}. */
-    public boolean requestMtu(int mtu) {
-        return mWrappedBluetoothGatt.requestMtu(mtu);
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#setCharacteristicNotification}. */
-    public boolean setCharacteristicNotification(
-            BluetoothGattCharacteristic characteristic, boolean enable) {
-        return mWrappedBluetoothGatt.setCharacteristicNotification(characteristic, enable);
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#disconnect()}. */
-    public void disconnect() {
-        mWrappedBluetoothGatt.disconnect();
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#close()}. */
-    public void close() {
-        mWrappedBluetoothGatt.close();
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#hashCode()}. */
-    @Override
-    public int hashCode() {
-        return mWrappedBluetoothGatt.hashCode();
-    }
-
-    /** See {@link android.bluetooth.BluetoothGatt#equals(Object)}. */
-    @Override
-    public boolean equals(@Nullable Object o) {
-        if (o == this) {
-            return true;
-        }
-        if (!(o instanceof BluetoothGattWrapper)) {
-            return false;
-        }
-        return mWrappedBluetoothGatt.equals(((BluetoothGattWrapper) o).unwrap());
-    }
-
-    /** Unwraps a Bluetooth Gatt instance. */
-    public android.bluetooth.BluetoothGatt unwrap() {
-        return mWrappedBluetoothGatt;
-    }
-
-    /** Wraps a Bluetooth Gatt instance. */
-    public static BluetoothGattWrapper wrap(android.bluetooth.BluetoothGatt gatt) {
-        return new BluetoothGattWrapper(gatt);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java
deleted file mode 100644
index b2c61ab..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability.android.bluetooth.le;
-
-import android.annotation.TargetApi;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.os.Build;
-
-import javax.annotation.Nullable;
-
-/**
- * Mockable wrapper of {@link android.bluetooth.le.BluetoothLeAdvertiser}.
- */
-@TargetApi(Build.VERSION_CODES.LOLLIPOP)
-public class BluetoothLeAdvertiser {
-
-    private final android.bluetooth.le.BluetoothLeAdvertiser mWrappedInstance;
-
-    private BluetoothLeAdvertiser(
-            android.bluetooth.le.BluetoothLeAdvertiser bluetoothLeAdvertiser) {
-        mWrappedInstance = bluetoothLeAdvertiser;
-    }
-
-    /**
-     * See {@link android.bluetooth.le.BluetoothLeAdvertiser#startAdvertising(AdvertiseSettings,
-     * AdvertiseData, AdvertiseCallback)}.
-     */
-    public void startAdvertising(AdvertiseSettings settings, AdvertiseData advertiseData,
-            AdvertiseCallback callback) {
-        mWrappedInstance.startAdvertising(settings, advertiseData, callback);
-    }
-
-    /**
-     * See {@link android.bluetooth.le.BluetoothLeAdvertiser#startAdvertising(AdvertiseSettings,
-     * AdvertiseData, AdvertiseData, AdvertiseCallback)}.
-     */
-    public void startAdvertising(AdvertiseSettings settings, AdvertiseData advertiseData,
-            AdvertiseData scanResponse, AdvertiseCallback callback) {
-        mWrappedInstance.startAdvertising(settings, advertiseData, scanResponse, callback);
-    }
-
-    /**
-     * See {@link android.bluetooth.le.BluetoothLeAdvertiser#stopAdvertising(AdvertiseCallback)}.
-     */
-    public void stopAdvertising(AdvertiseCallback callback) {
-        mWrappedInstance.stopAdvertising(callback);
-    }
-
-    /** Wraps a Bluetooth LE advertiser. */
-    @Nullable
-    public static BluetoothLeAdvertiser wrap(
-            @Nullable android.bluetooth.le.BluetoothLeAdvertiser bluetoothLeAdvertiser) {
-        if (bluetoothLeAdvertiser == null) {
-            return null;
-        }
-        return new BluetoothLeAdvertiser(bluetoothLeAdvertiser);
-    }
-
-    /** Unwraps a Bluetooth LE advertiser. */
-    @Nullable
-    public android.bluetooth.le.BluetoothLeAdvertiser unwrap() {
-        return mWrappedInstance;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java
deleted file mode 100644
index 9b3447e..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability.android.bluetooth.le;
-
-import android.annotation.TargetApi;
-import android.app.PendingIntent;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanSettings;
-import android.os.Build;
-
-import java.util.List;
-
-import javax.annotation.Nullable;
-
-/**
- * Mockable wrapper of {@link android.bluetooth.le.BluetoothLeScanner}.
- */
-@TargetApi(Build.VERSION_CODES.LOLLIPOP)
-public class BluetoothLeScanner {
-
-    private final android.bluetooth.le.BluetoothLeScanner mWrappedBluetoothLeScanner;
-
-    private BluetoothLeScanner(android.bluetooth.le.BluetoothLeScanner bluetoothLeScanner) {
-        mWrappedBluetoothLeScanner = bluetoothLeScanner;
-    }
-
-    /**
-     * See {@link android.bluetooth.le.BluetoothLeScanner#startScan(List, ScanSettings,
-     * android.bluetooth.le.ScanCallback)}.
-     */
-    public void startScan(List<ScanFilter> filters, ScanSettings settings,
-            ScanCallback callback) {
-        mWrappedBluetoothLeScanner.startScan(filters, settings, callback.unwrap());
-    }
-
-    /**
-     * See {@link android.bluetooth.le.BluetoothLeScanner#startScan(List, ScanSettings,
-     * PendingIntent)}.
-     */
-    public void startScan(
-            List<ScanFilter> filters, ScanSettings settings, PendingIntent callbackIntent) {
-        mWrappedBluetoothLeScanner.startScan(filters, settings, callbackIntent);
-    }
-
-    /**
-     * See {@link
-     * android.bluetooth.le.BluetoothLeScanner#startScan(android.bluetooth.le.ScanCallback)}.
-     */
-    public void startScan(ScanCallback callback) {
-        mWrappedBluetoothLeScanner.startScan(callback.unwrap());
-    }
-
-    /**
-     * See
-     * {@link android.bluetooth.le.BluetoothLeScanner#stopScan(android.bluetooth.le.ScanCallback)}.
-     */
-    public void stopScan(ScanCallback callback) {
-        mWrappedBluetoothLeScanner.stopScan(callback.unwrap());
-    }
-
-    /** See {@link android.bluetooth.le.BluetoothLeScanner#stopScan(PendingIntent)}. */
-    public void stopScan(PendingIntent callbackIntent) {
-        mWrappedBluetoothLeScanner.stopScan(callbackIntent);
-    }
-
-    /** Unwraps a Bluetooth LE scanner. */
-    @Nullable
-    public android.bluetooth.le.BluetoothLeScanner unwrap() {
-        return mWrappedBluetoothLeScanner;
-    }
-
-    /** Wraps a Bluetooth LE scanner. */
-    @Nullable
-    public static BluetoothLeScanner wrap(
-            @Nullable android.bluetooth.le.BluetoothLeScanner bluetoothLeScanner) {
-        if (bluetoothLeScanner == null) {
-            return null;
-        }
-        return new BluetoothLeScanner(bluetoothLeScanner);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallback.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallback.java
deleted file mode 100644
index 70926a7..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallback.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability.android.bluetooth.le;
-
-import android.annotation.TargetApi;
-import android.os.Build;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Wrapper of {@link android.bluetooth.le.ScanCallback} that uses mockable objects.
- */
-@TargetApi(Build.VERSION_CODES.LOLLIPOP)
-public abstract class ScanCallback {
-
-    /** See {@link android.bluetooth.le.ScanCallback#SCAN_FAILED_ALREADY_STARTED} */
-    public static final int SCAN_FAILED_ALREADY_STARTED =
-            android.bluetooth.le.ScanCallback.SCAN_FAILED_ALREADY_STARTED;
-
-    /** See {@link android.bluetooth.le.ScanCallback#SCAN_FAILED_APPLICATION_REGISTRATION_FAILED} */
-    public static final int SCAN_FAILED_APPLICATION_REGISTRATION_FAILED =
-            android.bluetooth.le.ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED;
-
-    /** See {@link android.bluetooth.le.ScanCallback#SCAN_FAILED_FEATURE_UNSUPPORTED} */
-    public static final int SCAN_FAILED_FEATURE_UNSUPPORTED =
-            android.bluetooth.le.ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED;
-
-    /** See {@link android.bluetooth.le.ScanCallback#SCAN_FAILED_INTERNAL_ERROR} */
-    public static final int SCAN_FAILED_INTERNAL_ERROR =
-            android.bluetooth.le.ScanCallback.SCAN_FAILED_INTERNAL_ERROR;
-
-    private final android.bluetooth.le.ScanCallback mWrappedScanCallback =
-            new InternalScanCallback();
-
-    /**
-     * See {@link android.bluetooth.le.ScanCallback#onScanFailed(int)}
-     */
-    public void onScanFailed(int errorCode) {}
-
-    /**
-     * See
-     * {@link android.bluetooth.le.ScanCallback#onScanResult(int, android.bluetooth.le.ScanResult)}.
-     */
-    public void onScanResult(int callbackType, ScanResult result) {}
-
-    /**
-     * See {@link
-     * android.bluetooth.le.ScanCallback#onBatchScanResult(List<android.bluetooth.le.ScanResult>)}.
-     */
-    public void onBatchScanResults(List<ScanResult> results) {}
-
-    /** Unwraps scan callback. */
-    public android.bluetooth.le.ScanCallback unwrap() {
-        return mWrappedScanCallback;
-    }
-
-    /** Forward callback to testable instance. */
-    private class InternalScanCallback extends android.bluetooth.le.ScanCallback {
-        @Override
-        public void onScanFailed(int errorCode) {
-            ScanCallback.this.onScanFailed(errorCode);
-        }
-
-        @Override
-        public void onScanResult(int callbackType, android.bluetooth.le.ScanResult result) {
-            ScanCallback.this.onScanResult(callbackType, ScanResult.wrap(result));
-        }
-
-        @Override
-        public void onBatchScanResults(List<android.bluetooth.le.ScanResult> results) {
-            List<ScanResult> wrappedScanResults = new ArrayList<>();
-            for (android.bluetooth.le.ScanResult result : results) {
-                wrappedScanResults.add(ScanResult.wrap(result));
-            }
-            ScanCallback.this.onBatchScanResults(wrappedScanResults);
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResult.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResult.java
deleted file mode 100644
index 1a6b7b3..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResult.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.testability.android.bluetooth.le;
-
-import android.annotation.TargetApi;
-import android.bluetooth.le.ScanRecord;
-import android.os.Build;
-
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
-
-import javax.annotation.Nullable;
-
-/**
- * Mockable wrapper of {@link android.bluetooth.le.ScanResult}.
- */
-@TargetApi(Build.VERSION_CODES.LOLLIPOP)
-public class ScanResult {
-
-    private final android.bluetooth.le.ScanResult mWrappedScanResult;
-
-    private ScanResult(android.bluetooth.le.ScanResult scanResult) {
-        mWrappedScanResult = scanResult;
-    }
-
-    /** See {@link android.bluetooth.le.ScanResult#getScanRecord()}. */
-    @Nullable
-    public ScanRecord getScanRecord() {
-        return mWrappedScanResult.getScanRecord();
-    }
-
-    /** See {@link android.bluetooth.le.ScanResult#getRssi()}. */
-    public int getRssi() {
-        return mWrappedScanResult.getRssi();
-    }
-
-    /** See {@link android.bluetooth.le.ScanResult#getTimestampNanos()}. */
-    public long getTimestampNanos() {
-        return mWrappedScanResult.getTimestampNanos();
-    }
-
-    /** See {@link android.bluetooth.le.ScanResult#getDevice()}. */
-    public BluetoothDevice getDevice() {
-        return BluetoothDevice.wrap(mWrappedScanResult.getDevice());
-    }
-
-    /** Creates a wrapper of scan result. */
-    public static ScanResult wrap(android.bluetooth.le.ScanResult scanResult) {
-        return new ScanResult(scanResult);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java
deleted file mode 100644
index bb51920..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.util;
-
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-
-import javax.annotation.Nullable;
-
-/**
- * Utils for Gatt profile.
- */
-public class BluetoothGattUtils {
-
-    /**
-     * Returns a string message for a BluetoothGatt status codes.
-     */
-    public static String getMessageForStatusCode(int statusCode) {
-        switch (statusCode) {
-            case BluetoothGatt.GATT_SUCCESS:
-                return "GATT_SUCCESS";
-            case BluetoothGatt.GATT_FAILURE:
-                return "GATT_FAILURE";
-            case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
-                return "GATT_INSUFFICIENT_AUTHENTICATION";
-            case BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION:
-                return "GATT_INSUFFICIENT_AUTHORIZATION";
-            case BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION:
-                return "GATT_INSUFFICIENT_ENCRYPTION";
-            case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH:
-                return "GATT_INVALID_ATTRIBUTE_LENGTH";
-            case BluetoothGatt.GATT_INVALID_OFFSET:
-                return "GATT_INVALID_OFFSET";
-            case BluetoothGatt.GATT_READ_NOT_PERMITTED:
-                return "GATT_READ_NOT_PERMITTED";
-            case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
-                return "GATT_REQUEST_NOT_SUPPORTED";
-            case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
-                return "GATT_WRITE_NOT_PERMITTED";
-            case BluetoothGatt.GATT_CONNECTION_CONGESTED:
-                return "GATT_CONNECTION_CONGESTED";
-            default:
-                return "Unknown error code";
-        }
-    }
-
-    /** Creates a user-readable string from a {@link BluetoothGattDescriptor}. */
-    public static String toString(@Nullable BluetoothGattDescriptor descriptor) {
-        if (descriptor == null) {
-            return "null descriptor";
-        }
-        return String.format("descriptor %s on %s",
-                descriptor.getUuid(),
-                toString(descriptor.getCharacteristic()));
-    }
-
-    /** Creates a user-readable string from a {@link BluetoothGattCharacteristic}. */
-    public static String toString(@Nullable BluetoothGattCharacteristic characteristic) {
-        if (characteristic == null) {
-            return "null characteristic";
-        }
-        return String.format("characteristic %s on %s",
-                characteristic.getUuid(),
-                toString(characteristic.getService()));
-    }
-
-    /** Creates a user-readable string from a {@link BluetoothGattService}. */
-    public static String toString(@Nullable BluetoothGattService service) {
-        if (service == null) {
-            return "null service";
-        }
-        return String.format("service %s", service.getUuid());
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutor.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutor.java
deleted file mode 100644
index fecf483..0000000
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutor.java
+++ /dev/null
@@ -1,548 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.util;
-
-import android.bluetooth.BluetoothGatt;
-import android.util.Log;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.testability.NonnullProvider;
-import com.android.server.nearby.common.bluetooth.testability.TimeProvider;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Queue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import javax.annotation.Nullable;
-
-/**
- * Scheduler to coordinate parallel bluetooth operations.
- */
-public class BluetoothOperationExecutor {
-
-    private static final String TAG = BluetoothOperationExecutor.class.getSimpleName();
-
-    /**
-     * Special value to indicate that the result is null (since {@link BlockingQueue} doesn't allow
-     * null elements).
-     */
-    private static final Object NULL_RESULT = new Object();
-
-    /**
-     * Special value to indicate that there should be no timeout on the operation.
-     */
-    private static final long NO_TIMEOUT = -1;
-
-    private final NonnullProvider<BlockingQueue<Object>> mBlockingQueueProvider;
-    private final TimeProvider mTimeProvider;
-    @VisibleForTesting
-    final Map<Operation<?>, Queue<Object>> mOperationResultQueues = new HashMap<>();
-    private final Semaphore mOperationSemaphore;
-
-    /**
-     * New instance that limits concurrent operations to maxConcurrentOperations.
-     */
-    public BluetoothOperationExecutor(int maxConcurrentOperations) {
-        this(
-                new Semaphore(maxConcurrentOperations, true),
-                new TimeProvider(),
-                new NonnullProvider<BlockingQueue<Object>>() {
-                    @Override
-                    public BlockingQueue<Object> get() {
-                        return new LinkedBlockingDeque<Object>();
-                    }
-                });
-    }
-
-    /**
-     * Constructor for unit tests.
-     */
-    @VisibleForTesting
-    BluetoothOperationExecutor(Semaphore operationSemaphore,
-            TimeProvider timeProvider,
-            NonnullProvider<BlockingQueue<Object>> blockingQueueProvider) {
-        mOperationSemaphore = operationSemaphore;
-        mTimeProvider = timeProvider;
-        mBlockingQueueProvider = blockingQueueProvider;
-    }
-
-    /**
-     * Executes the operation and waits for its completion.
-     */
-    @Nullable
-    public <T> T execute(Operation<T> operation) throws BluetoothException {
-        return getResult(schedule(operation));
-    }
-
-    /**
-     * Executes the operation and waits for its completion and returns a non-null result.
-     */
-    public <T> T executeNonnull(Operation<T> operation) throws BluetoothException {
-        T result = getResult(schedule(operation));
-        if (result == null) {
-            throw new BluetoothException(
-                    String.format(Locale.US, "Operation %s returned a null result.", operation));
-        }
-        return result;
-    }
-
-    /**
-     * Executes the operation and waits for its completion with a timeout.
-     */
-    @Nullable
-    public <T> T execute(Operation<T> bluetoothOperation, long timeoutMillis)
-            throws BluetoothException, BluetoothOperationTimeoutException {
-        return getResult(schedule(bluetoothOperation), timeoutMillis);
-    }
-
-    /**
-     * Executes the operation and waits for its completion with a timeout and returns a non-null
-     * result.
-     */
-    public <T> T executeNonnull(Operation<T> bluetoothOperation, long timeoutMillis)
-            throws BluetoothException {
-        T result = getResult(schedule(bluetoothOperation), timeoutMillis);
-        if (result == null) {
-            throw new BluetoothException(
-                    String.format(Locale.US, "Operation %s returned a null result.",
-                            bluetoothOperation));
-        }
-        return result;
-    }
-
-    /**
-     * Schedules an operation and returns a {@link Future} that waits on operation completion and
-     * gets its result.
-     */
-    public <T> Future<T> schedule(Operation<T> bluetoothOperation) {
-        BlockingQueue<Object> resultQueue = mBlockingQueueProvider.get();
-        mOperationResultQueues.put(bluetoothOperation, resultQueue);
-
-        boolean semaphoreAcquired = mOperationSemaphore.tryAcquire();
-        Log.d(TAG, String.format(Locale.US,
-                "Scheduling operation %s; %d permits available; Semaphore acquired: %b",
-                bluetoothOperation,
-                mOperationSemaphore.availablePermits(),
-                semaphoreAcquired));
-
-        if (semaphoreAcquired) {
-            bluetoothOperation.execute(this);
-        }
-        return new BluetoothOperationFuture<T>(resultQueue, bluetoothOperation, semaphoreAcquired);
-    }
-
-    /**
-     * Notifies that this operation has completed with success.
-     */
-    public void notifySuccess(Operation<Void> bluetoothOperation) {
-        postResult(bluetoothOperation, null);
-    }
-
-    /**
-     * Notifies that this operation has completed with success and with a result.
-     */
-    public <T> void notifySuccess(Operation<T> bluetoothOperation, T result) {
-        postResult(bluetoothOperation, result);
-    }
-
-    /**
-     * Notifies that this operation has completed with the given BluetoothGatt status code (which
-     * may indicate success or failure).
-     */
-    public void notifyCompletion(Operation<Void> bluetoothOperation, int status) {
-        notifyCompletion(bluetoothOperation, status, null);
-    }
-
-    /**
-     * Notifies that this operation has completed with the given BluetoothGatt status code (which
-     * may indicate success or failure) and with a result.
-     */
-    public <T> void notifyCompletion(Operation<T> bluetoothOperation, int status,
-            @Nullable T result) {
-        if (status != BluetoothGatt.GATT_SUCCESS) {
-            notifyFailure(bluetoothOperation, new BluetoothGattException(
-                    String.format(Locale.US,
-                            "Operation %s failed: %d - %s.", bluetoothOperation, status,
-                            BluetoothGattUtils.getMessageForStatusCode(status)),
-                    status));
-            return;
-        }
-        postResult(bluetoothOperation, result);
-    }
-
-    /**
-     * Notifies that this operation has completed with failure.
-     */
-    public void notifyFailure(Operation<?> bluetoothOperation, BluetoothException exception) {
-        postResult(bluetoothOperation, exception);
-    }
-
-    private void postResult(Operation<?> bluetoothOperation, @Nullable Object result) {
-        Queue<Object> resultQueue = mOperationResultQueues.get(bluetoothOperation);
-        if (resultQueue == null) {
-            Log.e(TAG, String.format(Locale.US,
-                    "Receive completion for unexpected operation: %s.", bluetoothOperation));
-            return;
-        }
-        resultQueue.add(result == null ? NULL_RESULT : result);
-        mOperationResultQueues.remove(bluetoothOperation);
-        mOperationSemaphore.release();
-        Log.d(TAG, String.format(Locale.US,
-                "Released semaphore for operation %s. There are %d permits left",
-                bluetoothOperation, mOperationSemaphore.availablePermits()));
-    }
-
-    /**
-     * Waits for all future on the list to complete, ignoring the results.
-     */
-    public <T> void waitFor(List<Future<T>> futures) throws BluetoothException {
-        for (Future<T> future : futures) {
-            if (future == null) {
-                continue;
-            }
-            getResult(future);
-        }
-    }
-
-    /**
-     * Waits with timeout for all future on the list to complete, ignoring the results.
-     */
-    public <T> void waitFor(List<Future<T>> futures, long timeoutMillis)
-            throws BluetoothException {
-        long startTime = mTimeProvider.getTimeMillis();
-        for (Future<T> future : futures) {
-            if (future == null) {
-                continue;
-            }
-            getResult(future,
-                    timeoutMillis - (mTimeProvider.getTimeMillis() - startTime));
-        }
-    }
-
-    /**
-     * Waits for a future to complete and returns the result.
-     */
-    @Nullable
-    public static <T> T getResult(Future<T> future) throws BluetoothException {
-        return getResultInternal(future, NO_TIMEOUT);
-    }
-
-    /**
-     * Waits for a future to complete and returns the result with timeout.
-     */
-    @Nullable
-    public static <T> T getResult(Future<T> future, long timeoutMillis) throws BluetoothException {
-        return getResultInternal(future, Math.max(0, timeoutMillis));
-    }
-
-    @Nullable
-    private static <T> T getResultInternal(Future<T> future, long timeoutMillis)
-            throws BluetoothException {
-        try {
-            if (timeoutMillis == NO_TIMEOUT) {
-                return future.get();
-            } else {
-                return future.get(timeoutMillis, TimeUnit.MILLISECONDS);
-            }
-        } catch (InterruptedException e) {
-            try {
-                boolean cancelSuccess = future.cancel(true);
-                if (!cancelSuccess && future.isDone()) {
-                    // Operation has succeeded before we send cancel to it.
-                    return getResultInternal(future, NO_TIMEOUT);
-                }
-            } finally {
-                // Re-interrupt the thread last since we're recursively calling getResultInternal.
-                // We know the future is done, so there's no need to be interrupted while we call.
-                Thread.currentThread().interrupt();
-            }
-            throw new BluetoothException("Wait interrupted");
-        } catch (ExecutionException e) {
-            Throwable cause = e.getCause();
-            if (cause instanceof BluetoothException) {
-                throw (BluetoothException) cause;
-            }
-            throw new RuntimeException(e);
-        } catch (TimeoutException e) {
-            boolean cancelSuccess = future.cancel(true);
-            if (!cancelSuccess && future.isDone()) {
-                // Operation has succeeded before we send cancel to it.
-                return getResultInternal(future, NO_TIMEOUT);
-            }
-            throw new BluetoothOperationTimeoutException(
-                    String.format(Locale.US, "Wait timed out after %s ms.", timeoutMillis), e);
-        }
-    }
-
-    /**
-     * Asynchronous bluetooth operation to schedule.
-     *
-     * <p>An instance that doesn't implemented run() can be used to notify operation result.
-     *
-     * @param <T> Type of provided instance.
-     */
-    public static class Operation<T> {
-
-        private Object[] mElements;
-
-        public Operation(Object... elements) {
-            mElements = elements;
-        }
-
-        /**
-         * Executes operation using executor.
-         */
-        public void execute(BluetoothOperationExecutor executor) {
-            try {
-                run();
-            } catch (BluetoothException e) {
-                executor.postResult(this, e);
-            }
-        }
-
-        /**
-         * Run function. Not supported.
-         */
-        @SuppressWarnings("unused")
-        public void run() throws BluetoothException {
-            throw new RuntimeException("Not implemented");
-        }
-
-        /**
-         * Try to cancel operation when a timeout occurs.
-         */
-        public void cancel() {
-        }
-
-        @Override
-        public boolean equals(@Nullable Object o) {
-            if (o == null) {
-                return false;
-            }
-            if (!Operation.class.isInstance(o)) {
-                return false;
-            }
-            Operation<?> other = (Operation<?>) o;
-            return Arrays.equals(mElements, other.mElements);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hashCode(mElements);
-        }
-
-        @Override
-        public String toString() {
-            return Joiner.on('-').join(mElements);
-        }
-    }
-
-    /**
-     * Synchronous bluetooth operation to schedule.
-     *
-     * @param <T> Type of provided instance.
-     */
-    public static class SynchronousOperation<T> extends Operation<T> {
-
-        public SynchronousOperation(Object... elements) {
-            super(elements);
-        }
-
-        @Override
-        public void execute(BluetoothOperationExecutor executor) {
-            try {
-                Object result = call();
-                if (result == null) {
-                    result = NULL_RESULT;
-                }
-                executor.postResult(this, result);
-            } catch (BluetoothException e) {
-                executor.postResult(this, e);
-            }
-        }
-
-        /**
-         * Call function. Not supported.
-         */
-        @SuppressWarnings("unused")
-        @Nullable
-        public T call() throws BluetoothException {
-            throw new RuntimeException("Not implemented");
-        }
-    }
-
-    /**
-     * {@link Future} to wait / get result of an operation.
-     *
-     * <li>Waits for operation to complete
-     * <li>Handles timeouts if needed
-     * <li>Queues identical Bluetooth operations
-     * <li>Unwraps Exceptions and null values
-     */
-    private class BluetoothOperationFuture<T> implements Future<T> {
-
-        private final Object mLock = new Object();
-
-        /**
-         * Queue that will be used to store the result. It should normally contains one element
-         * maximum, but using a queue avoid some race conditions.
-         */
-        private final BlockingQueue<Object> mResultQueue;
-        private final Operation<T> mBluetoothOperation;
-        private final boolean mOperationExecuted;
-        private boolean mIsCancelled = false;
-        private boolean mIsDone = false;
-
-        BluetoothOperationFuture(BlockingQueue<Object> resultQueue,
-                Operation<T> bluetoothOperation, boolean operationExecuted) {
-            mResultQueue = resultQueue;
-            mBluetoothOperation = bluetoothOperation;
-            mOperationExecuted = operationExecuted;
-        }
-
-        @Override
-        public boolean cancel(boolean mayInterruptIfRunning) {
-            synchronized (mLock) {
-                if (mIsDone) {
-                    return false;
-                }
-                if (mIsCancelled) {
-                    return true;
-                }
-                mBluetoothOperation.cancel();
-                mIsCancelled = true;
-                notifyFailure(mBluetoothOperation, new BluetoothException("Operation cancelled."));
-                return true;
-            }
-        }
-
-        @Override
-        public boolean isCancelled() {
-            synchronized (mLock) {
-                return mIsCancelled;
-            }
-        }
-
-        @Override
-        public boolean isDone() {
-            synchronized (mLock) {
-                return mIsDone;
-            }
-        }
-
-        @Override
-        @Nullable
-        public T get() throws InterruptedException, ExecutionException {
-            try {
-                return getInternal(NO_TIMEOUT, TimeUnit.MILLISECONDS);
-            } catch (TimeoutException e) {
-                throw new RuntimeException(e); // This is not supposed to be thrown
-            }
-        }
-
-        @Override
-        @Nullable
-        public T get(long timeoutMillis, TimeUnit unit)
-                throws InterruptedException, ExecutionException, TimeoutException {
-            return getInternal(Math.max(0, timeoutMillis), unit);
-        }
-
-        @SuppressWarnings("unchecked")
-        @Nullable
-        private T getInternal(long timeoutMillis, TimeUnit unit)
-                throws ExecutionException, InterruptedException, TimeoutException {
-            // Prevent parallel executions of this method.
-            long startTime = mTimeProvider.getTimeMillis();
-            synchronized (this) {
-                synchronized (mLock) {
-                    if (mIsDone) {
-                        throw new ExecutionException(
-                                new BluetoothException("get() called twice..."));
-                    }
-                }
-                if (!mOperationExecuted) {
-                    if (timeoutMillis == NO_TIMEOUT) {
-                        mOperationSemaphore.acquire();
-                    } else {
-                        if (!mOperationSemaphore.tryAcquire(timeoutMillis
-                                - (mTimeProvider.getTimeMillis() - startTime), unit)) {
-                            throw new TimeoutException(String.format(Locale.US,
-                                    "A timeout occurred when processing %s after %s %s.",
-                                    mBluetoothOperation, timeoutMillis, unit));
-                        }
-                    }
-                    mBluetoothOperation.execute(BluetoothOperationExecutor.this);
-                }
-                Object result;
-
-                if (timeoutMillis == NO_TIMEOUT) {
-                    result = mResultQueue.take();
-                } else {
-                    result = mResultQueue.poll(
-                            timeoutMillis - (mTimeProvider.getTimeMillis() - startTime), unit);
-                }
-
-                if (result == null) {
-                    throw new TimeoutException(String.format(Locale.US,
-                            "A timeout occurred when processing %s after %s ms.",
-                            mBluetoothOperation, timeoutMillis));
-                }
-                synchronized (mLock) {
-                    mIsDone = true;
-                }
-                if (result instanceof BluetoothException) {
-                    throw new ExecutionException((BluetoothException) result);
-                }
-                if (result == NULL_RESULT) {
-                    result = null;
-                }
-                return (T) result;
-            }
-        }
-    }
-
-    /**
-     * Exception thrown when an operation execution times out. Since state of the system is unknown
-     * afterward (operation may still complete or not), it is recommended to disconnect and
-     * reconnect.
-     */
-    public static class BluetoothOperationTimeoutException extends BluetoothException {
-
-        public BluetoothOperationTimeoutException(String message) {
-            super(message);
-        }
-
-        public BluetoothOperationTimeoutException(String message, Throwable cause) {
-            super(message, cause);
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/Annotations.java b/nearby/service/java/com/android/server/nearby/common/eventloop/Annotations.java
deleted file mode 100644
index 44c9422..0000000
--- a/nearby/service/java/com/android/server/nearby/common/eventloop/Annotations.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.eventloop;
-
-import static java.lang.annotation.ElementType.CONSTRUCTOR;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.CLASS;
-
-import androidx.annotation.AnyThread;
-import androidx.annotation.BinderThread;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-/**
- * A collection of threading annotations relating to EventLoop. These should be used in conjunction
- * with {@link UiThread}, {@link BinderThread}, {@link WorkerThread}, and {@link AnyThread}.
- */
-public class Annotations {
-
-    /**
-     * Denotes that the annotated method or constructor should only be called on the EventLoop
-     * thread.
-     */
-    @Retention(CLASS)
-    @Target({METHOD, CONSTRUCTOR, TYPE})
-    public @interface EventThread {
-    }
-
-    /** Denotes that the annotated method or constructor should only be called on a Network
-     * thread. */
-    @Retention(CLASS)
-    @Target({METHOD, CONSTRUCTOR, TYPE})
-    public @interface NetworkThread {
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/EventLoop.java b/nearby/service/java/com/android/server/nearby/common/eventloop/EventLoop.java
deleted file mode 100644
index c89366f..0000000
--- a/nearby/service/java/com/android/server/nearby/common/eventloop/EventLoop.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.eventloop;
-
-import android.annotation.Nullable;
-import android.os.Handler;
-import android.os.Looper;
-
-/**
- * Handles executing runnables on a background thread.
- *
- * <p>Nearby services follow an event loop model where events can be queued and delivered in the
- * future. All code that is run in this EventLoop is guaranteed to be run on this thread. The main
- * advantage of this model is that all modules don't have to deal with synchronization and race
- * conditions, while making it easy to handle the several asynchronous tasks that are expected to be
- * needed for this type of provider (such as starting a WiFi scan and waiting for the result,
- * starting BLE scans, doing a server request and waiting for the response etc.).
- *
- * <p>Code that needs to wait for an event should not spawn a new thread nor sleep. It should simply
- * deliver a new message to the event queue when the reply of the event happens.
- */
-// TODO(b/177675274): Resolve nullness suppression.
-@SuppressWarnings("nullness")
-public class EventLoop {
-
-    private final Interface mImpl;
-
-    private EventLoop(Interface impl) {
-        this.mImpl = impl;
-    }
-
-    protected EventLoop(String name) {
-        this(new HandlerEventLoopImpl(name));
-    }
-
-    /** Creates an EventLoop. */
-    public static EventLoop newInstance(String name) {
-        return new EventLoop(name);
-    }
-
-    /** Creates an EventLoop. */
-    public static EventLoop newInstance(String name, Looper looper) {
-        return new EventLoop(new HandlerEventLoopImpl(name, looper));
-    }
-
-    /** Marks the EventLoop as destroyed. Any further messages received will be ignored. */
-    public void destroy() {
-        mImpl.destroy();
-    }
-
-    /**
-     * Posts a runnable to this event loop, blocking until the runnable has been executed. This
-     * should
-     * be used rarely. It could be useful, for example, for a runnable that initializes the system
-     * and
-     * must block the posting of all other runnables.
-     *
-     * @param runnable a Runnable to post. This method will not return until the run() method of the
-     *                 given runnable has executed on the background thread.
-     */
-    public void postAndWait(final NamedRunnable runnable) throws InterruptedException {
-        mImpl.postAndWait(runnable);
-    }
-
-    /**
-     * Posts a runnable to this to the front of the event loop, blocking until the runnable has been
-     * executed. This should be used rarely, as it can starve the event loop.
-     *
-     * @param runnable a Runnable to post. This method will not return until the run() method of the
-     *                 given runnable has executed on the background thread.
-     */
-    public void postToFrontAndWait(final NamedRunnable runnable) throws InterruptedException {
-        mImpl.postToFrontAndWait(runnable);
-    }
-
-    /** Checks if there are any pending posts of the Runnable in the queue. */
-    public boolean isPosted(NamedRunnable runnable) {
-        return mImpl.isPosted(runnable);
-    }
-
-    /**
-     * Run code on the event loop thread.
-     *
-     * @param runnable the runnable to execute.
-     */
-    public void postRunnable(NamedRunnable runnable) {
-        mImpl.postRunnable(runnable);
-    }
-
-    /**
-     * Run code to be executed when there is no runnable scheduled.
-     *
-     * @param runnable last runnable to execute.
-     */
-    public void postEmptyQueueRunnable(final NamedRunnable runnable) {
-        mImpl.postEmptyQueueRunnable(runnable);
-    }
-
-    /**
-     * Run code on the event loop thread after delayedMillis.
-     *
-     * @param runnable      the runnable to execute.
-     * @param delayedMillis the number of milliseconds before executing the runnable.
-     */
-    public void postRunnableDelayed(NamedRunnable runnable, long delayedMillis) {
-        mImpl.postRunnableDelayed(runnable, delayedMillis);
-    }
-
-    /**
-     * Removes and cancels the specified {@code runnable} if it had not posted/started yet. Calling
-     * with null does nothing.
-     */
-    public void removeRunnable(@Nullable NamedRunnable runnable) {
-        mImpl.removeRunnable(runnable);
-    }
-
-    /** Asserts that the current operation is being executed in the Event Loop's thread. */
-    public void checkThread() {
-        mImpl.checkThread();
-    }
-
-    public Handler getHandler() {
-        return mImpl.getHandler();
-    }
-
-    interface Interface {
-        void destroy();
-
-        void postAndWait(NamedRunnable runnable) throws InterruptedException;
-
-        void postToFrontAndWait(NamedRunnable runnable) throws InterruptedException;
-
-        boolean isPosted(NamedRunnable runnable);
-
-        void postRunnable(NamedRunnable runnable);
-
-        void postEmptyQueueRunnable(NamedRunnable runnable);
-
-        void postRunnableDelayed(NamedRunnable runnable, long delayedMillis);
-
-        void removeRunnable(NamedRunnable runnable);
-
-        void checkThread();
-
-        Handler getHandler();
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/HandlerEventLoopImpl.java b/nearby/service/java/com/android/server/nearby/common/eventloop/HandlerEventLoopImpl.java
deleted file mode 100644
index 018dcdb..0000000
--- a/nearby/service/java/com/android/server/nearby/common/eventloop/HandlerEventLoopImpl.java
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.eventloop;
-
-import android.annotation.Nullable;
-import android.annotation.SuppressLint;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.os.MessageQueue;
-import android.os.Process;
-import android.os.SystemClock;
-import android.util.Log;
-
-import java.text.SimpleDateFormat;
-import java.util.Locale;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-
-/**
- * Handles executing runnables on a background thread.
- *
- * <p>Nearby services follow an event loop model where events can be queued and delivered in the
- * future. All code that is run in this package is guaranteed to be run on this thread. The main
- * advantage of this model is that all modules don't have to deal with synchronization and race
- * conditions, while making it easy to handle the several asynchronous tasks that are expected to be
- * needed for this type of provider (such as starting a WiFi scan and waiting for the result,
- * starting BLE scans, doing a server request and waiting for the response etc.).
- *
- * <p>Code that needs to wait for an event should not spawn a new thread nor sleep. It should simply
- * deliver a new message to the event queue when the reply of the event happens.
- *
- * <p>
- */
-// TODO(b/203471261) use executor instead of handler
-// TODO(b/177675274): Resolve nullness suppression.
-@SuppressWarnings("nullness")
-final class HandlerEventLoopImpl implements EventLoop.Interface {
-    /** The {@link Message#what} code for all messages that we post to the EventLoop. */
-    private static final int WHAT = 0;
-
-    private static final long ELAPSED_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(5);
-    private static final long RUNNABLE_DELAY_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(2);
-    private static final String TAG = HandlerEventLoopImpl.class.getSimpleName();
-    private final MyHandler mHandler;
-
-    private volatile boolean mIsDestroyed = false;
-
-    /** Constructs an EventLoop. */
-    HandlerEventLoopImpl(String name) {
-        this(name, createHandlerThread(name));
-    }
-
-    HandlerEventLoopImpl(String name, Looper looper) {
-
-        mHandler = new MyHandler(looper);
-        Log.d(TAG,
-                "Created EventLoop for thread '" + looper.getThread().getName()
-                        + "(id: " + looper.getThread().getId() + ")'");
-    }
-
-    private static Looper createHandlerThread(String name) {
-        HandlerThread handlerThread = new HandlerThread(name, Process.THREAD_PRIORITY_BACKGROUND);
-        handlerThread.start();
-
-        return handlerThread.getLooper();
-    }
-
-    /**
-     * Wrapper to satisfy Android Lint. {@link Looper#getQueue()} is public and available since ICS,
-     * but was marked @hide until Marshmallow. Tested that this code doesn't crash pre-Marshmallow.
-     * /aosp-ics/frameworks/base/core/java/android/os/Looper.java?l=218
-     */
-    @SuppressLint("NewApi")
-    private static MessageQueue getQueue(Handler handler) {
-        return handler.getLooper().getQueue();
-    }
-
-    /** Marks the EventLoop as destroyed. Any further messages received will be ignored. */
-    @Override
-    public void destroy() {
-        Looper looper = mHandler.getLooper();
-        Log.d(TAG,
-                "Destroying EventLoop for thread " + looper.getThread().getName()
-                        + " (id: " + looper.getThread().getId() + ")");
-        looper.quit();
-        mIsDestroyed = true;
-    }
-
-    /**
-     * Posts a runnable to this event loop, blocking until the runnable has been executed. This
-     * should
-     * be used rarely. It could be useful, for example, for a runnable that initializes the system
-     * and
-     * must block the posting of all other runnables.
-     *
-     * @param runnable a Runnable to post. This method will not return until the run() method of the
-     *                 given runnable has executed on the background thread.
-     */
-    @Override
-    public void postAndWait(final NamedRunnable runnable) throws InterruptedException {
-        internalPostAndWait(runnable, false);
-    }
-
-    @Override
-    public void postToFrontAndWait(final NamedRunnable runnable) throws InterruptedException {
-        internalPostAndWait(runnable, true);
-    }
-
-    /** Checks if there are any pending posts of the Runnable in the queue. */
-    @Override
-    public boolean isPosted(NamedRunnable runnable) {
-        return mHandler.hasMessages(WHAT, runnable);
-    }
-
-    /**
-     * Run code on the event loop thread.
-     *
-     * @param runnable the runnable to execute.
-     */
-    @Override
-    public void postRunnable(NamedRunnable runnable) {
-        Log.d(TAG, "Posting " + runnable);
-        mHandler.post(runnable, 0L, false);
-    }
-
-    /**
-     * Run code to be executed when there is no runnable scheduled.
-     *
-     * @param runnable last runnable to execute.
-     */
-    @Override
-    public void postEmptyQueueRunnable(final NamedRunnable runnable) {
-        mHandler.post(
-                () ->
-                        getQueue(mHandler)
-                                .addIdleHandler(
-                                        () -> {
-                                            if (mHandler.hasMessages(WHAT)) {
-                                                return true;
-                                            } else {
-                                                // Only stop if start has not been called since
-                                                // this was queued
-                                                runnable.run();
-                                                return false;
-                                            }
-                                        }));
-    }
-
-    /**
-     * Run code on the event loop thread after delayedMillis.
-     *
-     * @param runnable      the runnable to execute.
-     * @param delayedMillis the number of milliseconds before executing the runnable.
-     */
-    @Override
-    public void postRunnableDelayed(NamedRunnable runnable, long delayedMillis) {
-        Log.d(TAG, "Posting " + runnable + " [delay " + delayedMillis + "]");
-        mHandler.post(runnable, delayedMillis, false);
-    }
-
-    /**
-     * Removes and cancels the specified {@code runnable} if it had not posted/started yet. Calling
-     * with null does nothing.
-     */
-    @Override
-    public void removeRunnable(@Nullable NamedRunnable runnable) {
-        if (runnable != null) {
-            // Removes any pending sent messages where what=WHAT and obj=runnable. We can't use
-            // removeCallbacks(runnable) because we're not posting the runnable directly, we're
-            // sending a Message with the runnable as its obj.
-            mHandler.removeMessages(WHAT, runnable);
-        }
-    }
-
-    /** Asserts that the current operation is being executed in the Event Loop's thread. */
-    @Override
-    public void checkThread() {
-
-        Thread currentThread = Looper.myLooper().getThread();
-        Thread expectedThread = mHandler.getLooper().getThread();
-        if (currentThread.getId() != expectedThread.getId()) {
-            throw new IllegalStateException(
-                    String.format(
-                            "This method must run in the EventLoop thread '%s (id: %s)'. "
-                                    + "Was called from thread '%s (id: %s)'.",
-                            expectedThread.getName(),
-                            expectedThread.getId(),
-                            currentThread.getName(),
-                            currentThread.getId()));
-        }
-
-    }
-
-    @Override
-    public Handler getHandler() {
-        return mHandler;
-    }
-
-    private void internalPostAndWait(final NamedRunnable runnable, boolean postToFront)
-            throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(1);
-        NamedRunnable delegate =
-                new NamedRunnable(runnable.name) {
-                    @Override
-                    public void run() {
-                        try {
-                            runnable.run();
-                        } finally {
-                            latch.countDown();
-                        }
-                    }
-                };
-
-        Log.d(TAG, "Posting " + delegate + " and wait");
-        if (!mHandler.post(delegate, 0L, postToFront)) {
-            // Do not wait if delegate is not posted.
-            Log.d(TAG, delegate + " not posted");
-            latch.countDown();
-        }
-        latch.await();
-    }
-
-    /** Handler that executes code on a private event loop thread. */
-    private class MyHandler extends Handler {
-
-        MyHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            NamedRunnable runnable = (NamedRunnable) msg.obj;
-
-            if (mIsDestroyed) {
-                Log.w(TAG, "Runnable " + runnable
-                        + " attempted to run after the EventLoop was destroyed. Ignoring");
-                return;
-            }
-            Log.i(TAG, "Executing " + runnable);
-
-            // Did this runnable start much later than we expected it to? If so, then log.
-            long expectedStartTime = (long) msg.arg1 << 32 | (msg.arg2 & 0xFFFFFFFFL);
-            logIfExceedsThreshold(
-                    RUNNABLE_DELAY_THRESHOLD_MS, expectedStartTime, runnable, "was delayed for");
-
-            long startTimeMillis = SystemClock.elapsedRealtime();
-            try {
-                runnable.run();
-            } catch (Exception t) {
-                Log.e(TAG, runnable + "crashed.");
-                throw t;
-            } finally {
-                logIfExceedsThreshold(ELAPSED_THRESHOLD_MS, startTimeMillis, runnable, "ran for");
-            }
-        }
-
-        private boolean post(NamedRunnable runnable, long delayedMillis, boolean postToFront) {
-            if (mIsDestroyed) {
-                Log.w(TAG, runnable + " not posted since EventLoop is destroyed");
-                return false;
-            }
-            long expectedStartTime = SystemClock.elapsedRealtime() + delayedMillis;
-            int arg1 = (int) (expectedStartTime >> 32);
-            int arg2 = (int) expectedStartTime;
-            Message message = obtainMessage(WHAT, arg1, arg2, runnable /* obj */);
-            boolean sent =
-                    postToFront
-                            ? sendMessageAtFrontOfQueue(message)
-                            : sendMessageDelayed(message, delayedMillis);
-            if (!sent) {
-                Log.w(TAG, runnable + "not posted since looper is exiting");
-            }
-            return sent;
-        }
-
-        private void logIfExceedsThreshold(
-                long thresholdMillis, long startTimeMillis, NamedRunnable runnable,
-                String message) {
-            long elapsedMillis = SystemClock.elapsedRealtime() - startTimeMillis;
-            if (elapsedMillis > thresholdMillis) {
-                String elapsedFormatted =
-                        new SimpleDateFormat("mm:ss.SSS", Locale.US).format(elapsedMillis);
-                Log.w(TAG, runnable + " " + message + " " + elapsedFormatted);
-            }
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/NamedRunnable.java b/nearby/service/java/com/android/server/nearby/common/eventloop/NamedRunnable.java
deleted file mode 100644
index 578e3f6..0000000
--- a/nearby/service/java/com/android/server/nearby/common/eventloop/NamedRunnable.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.eventloop;
-
-/** A Runnable with a name, for logging purposes. */
-public abstract class NamedRunnable implements Runnable {
-    public final String name;
-
-    public NamedRunnable(String name) {
-        this.name = name;
-    }
-
-    @Override
-    public String toString() {
-        return "Runnable[" + name + "]";
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/fastpair/IconUtils.java b/nearby/service/java/com/android/server/nearby/common/fastpair/IconUtils.java
deleted file mode 100644
index 35a1a9f..0000000
--- a/nearby/service/java/com/android/server/nearby/common/fastpair/IconUtils.java
+++ /dev/null
@@ -1,113 +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.server.nearby.common.fastpair;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.core.graphics.ColorUtils;
-
-/** Utility methods for icon size verification. */
-public class IconUtils {
-    private static final int MIN_ICON_SIZE = 16;
-    private static final int DESIRED_ICON_SIZE = 32;
-    private static final double NOTIFICATION_BACKGROUND_PADDING_PERCENTAGE = 0.125;
-    private static final double NOTIFICATION_BACKGROUND_ALPHA = 0.7;
-
-    /**
-     * Verify that the icon is non null and falls in the small bucket. Just because an icon isn't
-     * small doesn't guarantee it is large or exists.
-     */
-    @VisibleForTesting
-    static boolean isIconSizedSmall(@Nullable Bitmap bitmap) {
-        if (bitmap == null) {
-            return false;
-        }
-        int min = MIN_ICON_SIZE;
-        int desired = DESIRED_ICON_SIZE;
-        return bitmap.getWidth() >= min
-                && bitmap.getWidth() < desired
-                && bitmap.getHeight() >= min
-                && bitmap.getHeight() < desired;
-    }
-
-    /**
-     * Verify that the icon is non null and falls in the regular / default size bucket. Doesn't
-     * guarantee if not regular then it is small.
-     */
-    @VisibleForTesting
-    static boolean isIconSizedRegular(@Nullable Bitmap bitmap) {
-        if (bitmap == null) {
-            return false;
-        }
-        return bitmap.getWidth() >= DESIRED_ICON_SIZE
-                && bitmap.getHeight() >= DESIRED_ICON_SIZE;
-    }
-
-    // All icons that are sized correctly (larger than the min icon size) are resize on the server
-    // to the desired icon size so that they appear correct in notifications.
-
-    /**
-     * All icons that are sized correctly (larger than the min icon size) are resize on the server
-     * to the desired icon size so that they appear correct in notifications.
-     */
-    public static boolean isIconSizeCorrect(@Nullable Bitmap bitmap) {
-        if (bitmap == null) {
-            return false;
-        }
-        return isIconSizedSmall(bitmap) || isIconSizedRegular(bitmap);
-    }
-
-    /** Adds a circular, white background to the bitmap. */
-    @Nullable
-    public static Bitmap addWhiteCircleBackground(Context context, @Nullable Bitmap bitmap) {
-        if (bitmap == null) {
-            return null;
-        }
-
-        if (bitmap.getWidth() != bitmap.getHeight()) {
-            return bitmap;
-        }
-
-        int padding = (int) (bitmap.getWidth() * NOTIFICATION_BACKGROUND_PADDING_PERCENTAGE);
-        Bitmap bitmapWithBackground =
-                Bitmap.createBitmap(
-                        bitmap.getWidth() + (2 * padding),
-                        bitmap.getHeight() + (2 * padding),
-                        bitmap.getConfig());
-        Canvas canvas = new Canvas(bitmapWithBackground);
-        Paint paint = new Paint();
-        paint.setColor(
-                ColorUtils.setAlphaComponent(
-                        Color.WHITE, (int) (255 * NOTIFICATION_BACKGROUND_ALPHA)));
-        paint.setStyle(Paint.Style.FILL);
-        paint.setAntiAlias(true);
-        canvas.drawCircle(
-                bitmapWithBackground.getWidth() / 2f,
-                bitmapWithBackground.getHeight() / 2f,
-                bitmapWithBackground.getWidth() / 2f,
-                paint);
-        canvas.drawBitmap(bitmap, padding, padding, null);
-        return bitmapWithBackground;
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/common/locator/Locator.java b/nearby/service/java/com/android/server/nearby/common/locator/Locator.java
deleted file mode 100644
index 2003335..0000000
--- a/nearby/service/java/com/android/server/nearby/common/locator/Locator.java
+++ /dev/null
@@ -1,299 +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.server.nearby.common.locator;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.util.Log;
-
-import androidx.annotation.VisibleForTesting;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Collection of bindings that map service types to their respective implementation(s). */
-public class Locator {
-    private static final Object UNBOUND = new Object();
-    private final Context mContext;
-    @Nullable
-    private Locator mParent;
-    private final String mTag; // For debugging
-    private final Map<Class<?>, Object> mBindings = new HashMap<>();
-    private final ArrayList<Module> mModules = new ArrayList<>();
-
-    /** Thrown upon attempt to bind an interface twice. */
-    public static class DuplicateBindingException extends RuntimeException {
-        DuplicateBindingException(String msg) {
-            super(msg);
-        }
-    }
-
-    /** Constructor with a null parent. */
-    public Locator(Context context) {
-        this(context, null);
-    }
-
-    /**
-     * Constructor. Supply a valid context and the Locator's parent.
-     *
-     * <p>To find a suitable parent you may want to use findLocator.
-     */
-    public Locator(Context context, @Nullable Locator parent) {
-        this.mContext = context;
-        this.mParent = parent;
-        this.mTag = context.getClass().getName();
-    }
-
-    /** Attaches the parent to the locator. */
-    public void attachParent(Locator parent) {
-        this.mParent = parent;
-    }
-
-    /** Associates the specified type with the supplied instance. */
-    public <T extends Object> Locator bind(Class<T> type, T instance) {
-        bindKeyValue(type, instance);
-        return this;
-    }
-
-    /** For tests only. Disassociates the specified type from any instance. */
-    @VisibleForTesting
-    public <T extends Object> Locator overrideBindingForTest(Class<T> type, T instance) {
-        mBindings.remove(type);
-        return bind(type, instance);
-    }
-
-    /** For tests only. Force Locator to return null when try to get an instance. */
-    @VisibleForTesting
-    public <T> Locator removeBindingForTest(Class<T> type) {
-        Locator locator = this;
-        do {
-            locator.mBindings.put(type, UNBOUND);
-            locator = locator.mParent;
-        } while (locator != null);
-        return this;
-    }
-
-    /** Binds a module. */
-    public synchronized Locator bind(Module module) {
-        mModules.add(module);
-        return this;
-    }
-
-    /**
-     * Searches the chain of locators for a binding for the given type.
-     *
-     * @throws IllegalStateException if no binding is found.
-     */
-    public <T> T get(Class<T> type) {
-        T instance = getOptional(type);
-        if (instance != null) {
-            return instance;
-        }
-
-        String errorMessage = getUnboundErrorMessage(type);
-        throw new IllegalStateException(errorMessage);
-    }
-
-    @VisibleForTesting
-    String getUnboundErrorMessage(Class<?> type) {
-        StringBuilder sb = new StringBuilder();
-        sb.append("Unbound type: ").append(type.getName()).append("\n").append(
-                "Searched locators:\n");
-        Locator locator = this;
-        while (true) {
-            sb.append(locator.mTag);
-            locator = locator.mParent;
-            if (locator == null) {
-                break;
-            }
-            sb.append(" ->\n");
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Searches the chain of locators for a binding for the given type. Returns null if no locator
-     * was
-     * found.
-     */
-    @Nullable
-    public <T> T getOptional(Class<T> type) {
-        Locator locator = this;
-        do {
-            T instance = locator.getInstance(type);
-            if (instance != null) {
-                return instance;
-            }
-            locator = locator.mParent;
-        } while (locator != null);
-        return null;
-    }
-
-    private synchronized <T extends Object> void bindKeyValue(Class<T> key, T value) {
-        Object boundInstance = mBindings.get(key);
-        if (boundInstance != null) {
-            if (boundInstance == UNBOUND) {
-                Log.w(mTag, "Bind call too late - someone already tried to get: " + key);
-            } else {
-                throw new DuplicateBindingException("Duplicate binding: " + key);
-            }
-        }
-        mBindings.put(key, value);
-    }
-
-    // Suppress warning of cast from Object -> T
-    @SuppressWarnings("unchecked")
-    @Nullable
-    private synchronized <T> T getInstance(Class<T> type) {
-        if (mContext == null) {
-            throw new IllegalStateException("Locator not initialized yet.");
-        }
-
-        T instance = (T) mBindings.get(type);
-        if (instance != null) {
-            return instance != UNBOUND ? instance : null;
-        }
-
-        // Ask modules to supply a binding
-        int moduleCount = mModules.size();
-        for (int i = 0; i < moduleCount; i++) {
-            mModules.get(i).configure(mContext, type, this);
-        }
-
-        instance = (T) mBindings.get(type);
-        if (instance == null) {
-            mBindings.put(type, UNBOUND);
-        }
-        return instance;
-    }
-
-    /**
-     * Iterates over all bound objects and gives the modules a chance to clean up the objects they
-     * have created.
-     */
-    public synchronized void destroy() {
-        for (Class<?> type : mBindings.keySet()) {
-            Object instance = mBindings.get(type);
-            if (instance == UNBOUND) {
-                continue;
-            }
-
-            for (Module module : mModules) {
-                module.destroy(mContext, type, instance);
-            }
-        }
-        mBindings.clear();
-    }
-
-    /** Returns true if there are no bindings. */
-    public boolean isEmpty() {
-        return mBindings.isEmpty();
-    }
-
-    /** Returns the parent locator or null if no parent. */
-    @Nullable
-    public Locator getParent() {
-        return mParent;
-    }
-
-    /**
-     * Finds the first locator, then searches the chain of locators for a binding for the given
-     * type.
-     *
-     * @throws IllegalStateException if no binding is found.
-     */
-    public static <T> T get(Context context, Class<T> type) {
-        Locator locator = findLocator(context);
-        if (locator == null) {
-            throw new IllegalStateException("No locator found in context " + context);
-        }
-        return locator.get(type);
-    }
-
-    /**
-     * Find the first locator from the context wrapper.
-     */
-    public static <T> T getFromContextWrapper(LocatorContextWrapper wrapper, Class<T> type) {
-        Locator locator = wrapper.getLocator();
-        if (locator == null) {
-            throw new IllegalStateException("No locator found in context wrapper");
-        }
-        return locator.get(type);
-    }
-
-    /**
-     * Finds the first locator, then searches the chain of locators for a binding for the given
-     * type.
-     * Returns null if no binding was found.
-     */
-    @Nullable
-    public static <T> T getOptional(Context context, Class<T> type) {
-        Locator locator = findLocator(context);
-        if (locator == null) {
-            return null;
-        }
-        return locator.getOptional(type);
-    }
-
-    /** Finds the first locator in the context hierarchy. */
-    @Nullable
-    public static Locator findLocator(Context context) {
-        Context applicationContext = context.getApplicationContext();
-        boolean applicationContextVisited = false;
-
-        Context searchContext = context;
-        do {
-            Locator locator = tryGetLocator(searchContext);
-            if (locator != null) {
-                return locator;
-            }
-
-            applicationContextVisited |= (searchContext == applicationContext);
-
-            if (searchContext instanceof ContextWrapper) {
-                searchContext = ((ContextWrapper) context).getBaseContext();
-
-                if (searchContext == null) {
-                    throw new IllegalStateException(
-                            "Invalid ContextWrapper -- If this is a Robolectric test, "
-                                    + "have you called ActivityController.create()?");
-                }
-            } else if (!applicationContextVisited) {
-                searchContext = applicationContext;
-            } else {
-                searchContext = null;
-            }
-        } while (searchContext != null);
-
-        return null;
-    }
-
-    @Nullable
-    private static Locator tryGetLocator(Object object) {
-        if (object instanceof LocatorContext) {
-            Locator locator = ((LocatorContext) object).getLocator();
-            if (locator == null) {
-                throw new IllegalStateException(
-                        "LocatorContext must not return null Locator: " + object);
-            }
-            return locator;
-        }
-        return null;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/locator/LocatorContext.java b/nearby/service/java/com/android/server/nearby/common/locator/LocatorContext.java
deleted file mode 100644
index 06eef8a..0000000
--- a/nearby/service/java/com/android/server/nearby/common/locator/LocatorContext.java
+++ /dev/null
@@ -1,26 +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.server.nearby.common.locator;
-
-/**
- * An object that has a {@link Locator}. The locator can be used to resolve service types to their
- * respective implementation(s).
- */
-public interface LocatorContext {
-    /** Returns the locator. May not return null. */
-    Locator getLocator();
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/locator/LocatorContextWrapper.java b/nearby/service/java/com/android/server/nearby/common/locator/LocatorContextWrapper.java
deleted file mode 100644
index 03df33f..0000000
--- a/nearby/service/java/com/android/server/nearby/common/locator/LocatorContextWrapper.java
+++ /dev/null
@@ -1,57 +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.server.nearby.common.locator;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.ContextWrapper;
-
-/**
- * Wraps a Context and associates it with a Locator, optionally linking it with a parent locator.
- */
-public class LocatorContextWrapper extends ContextWrapper implements LocatorContext {
-    private final Locator mLocator;
-    private final Context mContext;
-    /** Constructs a context wrapper with a Locator linked to the passed locator. */
-    public LocatorContextWrapper(Context context, @Nullable Locator parentLocator) {
-        super(context);
-        mContext = context;
-        // Assigning under initialization object, but it's safe, since locator is used lazily.
-        this.mLocator = new Locator(this, parentLocator);
-    }
-
-    /**
-     * Constructs a context wrapper.
-     *
-     * <p>Uses the Locator associated with the passed context as the parent.
-     */
-    public LocatorContextWrapper(Context context) {
-        this(context, Locator.findLocator(context));
-    }
-
-    /**
-     * Get the context of the context wrapper.
-     */
-    public Context getContext() {
-        return mContext;
-    }
-
-    @Override
-    public Locator getLocator() {
-        return mLocator;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/locator/Module.java b/nearby/service/java/com/android/server/nearby/common/locator/Module.java
deleted file mode 100644
index 0131c44..0000000
--- a/nearby/service/java/com/android/server/nearby/common/locator/Module.java
+++ /dev/null
@@ -1,57 +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.server.nearby.common.locator;
-
-import android.content.Context;
-
-/** Configures late bindings of service types to their concrete implementations. */
-public abstract class Module {
-    /**
-     * Configures the binding between the {@code type} and its implementation by calling methods on
-     * the {@code locator}, for example:
-     *
-     * <pre>{@code
-     * void configure(Context context, Class<?> type, Locator locator) {
-     *   if (type == MyService.class) {
-     *     locator.bind(MyService.class, new MyImplementation(context));
-     *   }
-     * }
-     * }</pre>
-     *
-     * <p>If the module does not recognize the specified type, the method does not have to do
-     * anything.
-     */
-    public abstract void configure(Context context, Class<?> type, Locator locator);
-
-    /**
-     * Notifies you that a binding of class {@code type} is no longer needed and can now release
-     * everything it was holding on to, such as a database connection.
-     *
-     * <pre>{@code
-     * void destroy(Context context, Class<?> type, Object instance) {
-     *   if (type == MyService.class) {
-     *     ((MyService) instance).destroy();
-     *   }
-     * }
-     * }</pre>
-     *
-     * <p>If the module does not recognize the specified type, the method does not have to do
-     * anything.
-     */
-    public void destroy(Context context, Class<?> type, Object instance) {}
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/CurrentUserServiceProvider.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/CurrentUserServiceProvider.java
deleted file mode 100644
index 80248e8..0000000
--- a/nearby/service/java/com/android/server/nearby/common/servicemonitor/CurrentUserServiceProvider.java
+++ /dev/null
@@ -1,217 +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.server.nearby.common.servicemonitor;
-
-import static android.content.pm.PackageManager.GET_META_DATA;
-import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
-import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
-import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
-import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
-
-import android.app.ActivityManager;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ResolveInfo;
-import android.os.UserHandle;
-
-import com.android.internal.util.Preconditions;
-import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceChangedListener;
-import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceProvider;
-
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * This is mostly borrowed from frameworks CurrentUserServiceSupplier.
- * Provides services based on the current active user and version as defined in the service
- * manifest. This implementation uses {@link android.content.pm.PackageManager#MATCH_SYSTEM_ONLY} to
- * ensure only system (ie, privileged) services are matched. It also handles services that are not
- * direct boot aware, and will automatically pick the best service as the user's direct boot state
- * changes.
- */
-public final class CurrentUserServiceProvider extends BroadcastReceiver implements
-        ServiceProvider<CurrentUserServiceProvider.BoundServiceInfo> {
-
-    private static final String TAG = "CurrentUserServiceProvider";
-
-    private static final String EXTRA_SERVICE_VERSION = "serviceVersion";
-
-    // This is equal to the hidden Intent.ACTION_USER_SWITCHED.
-    private static final String ACTION_USER_SWITCHED = "android.intent.action.USER_SWITCHED";
-    // This is equal to the hidden Intent.EXTRA_USER_HANDLE.
-    private static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle";
-    // This is equal to the hidden UserHandle.USER_NULL.
-    private static final int USER_NULL = -10000;
-
-    private static final Comparator<BoundServiceInfo> sBoundServiceInfoComparator = (o1, o2) -> {
-        if (o1 == o2) {
-            return 0;
-        } else if (o1 == null) {
-            return -1;
-        } else if (o2 == null) {
-            return 1;
-        }
-
-        // ServiceInfos with higher version numbers always win.
-        return Integer.compare(o1.getVersion(), o2.getVersion());
-    };
-
-    /** Bound service information with version information. */
-    public static class BoundServiceInfo extends ServiceMonitor.BoundServiceInfo {
-
-        private static int parseUid(ResolveInfo resolveInfo) {
-            return resolveInfo.serviceInfo.applicationInfo.uid;
-        }
-
-        private static int parseVersion(ResolveInfo resolveInfo) {
-            int version = Integer.MIN_VALUE;
-            if (resolveInfo.serviceInfo.metaData != null) {
-                version = resolveInfo.serviceInfo.metaData.getInt(EXTRA_SERVICE_VERSION, version);
-            }
-            return version;
-        }
-
-        private final int mVersion;
-
-        protected BoundServiceInfo(String action, ResolveInfo resolveInfo) {
-            this(
-                    action,
-                    parseUid(resolveInfo),
-                    new ComponentName(
-                            resolveInfo.serviceInfo.packageName,
-                            resolveInfo.serviceInfo.name),
-                    parseVersion(resolveInfo));
-        }
-
-        protected BoundServiceInfo(String action, int uid, ComponentName componentName,
-                int version) {
-            super(action, uid, componentName);
-            mVersion = version;
-        }
-
-        public int getVersion() {
-            return mVersion;
-        }
-
-        @Override
-        public String toString() {
-            return super.toString() + "@" + mVersion;
-        }
-    }
-
-    /**
-     * Creates an instance with the specific service details.
-     *
-     * @param context the context the provider is to use
-     * @param action the action the service must declare in its intent-filter
-     */
-    public static CurrentUserServiceProvider create(Context context, String action) {
-        return new CurrentUserServiceProvider(context, action);
-    }
-
-    private final Context mContext;
-    private final Intent mIntent;
-    private volatile ServiceChangedListener mListener;
-
-    private CurrentUserServiceProvider(Context context, String action) {
-        mContext = context;
-        mIntent = new Intent(action);
-    }
-
-    @Override
-    public boolean hasMatchingService() {
-        int intentQueryFlags =
-                MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE | MATCH_SYSTEM_ONLY;
-        List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser(
-                mIntent, intentQueryFlags, UserHandle.SYSTEM);
-        return !resolveInfos.isEmpty();
-    }
-
-    @Override
-    public void register(ServiceChangedListener listener) {
-        Preconditions.checkState(mListener == null);
-
-        mListener = listener;
-
-        IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(ACTION_USER_SWITCHED);
-        intentFilter.addAction(Intent.ACTION_USER_UNLOCKED);
-        mContext.registerReceiverForAllUsers(this, intentFilter, null,
-                ForegroundThread.getHandler());
-    }
-
-    @Override
-    public void unregister() {
-        Preconditions.checkArgument(mListener != null);
-
-        mListener = null;
-        mContext.unregisterReceiver(this);
-    }
-
-    @Override
-    public BoundServiceInfo getServiceInfo() {
-        BoundServiceInfo bestServiceInfo = null;
-
-        // only allow services in the correct direct boot state to match
-        int intentQueryFlags = MATCH_DIRECT_BOOT_AUTO | GET_META_DATA | MATCH_SYSTEM_ONLY;
-        List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser(
-                mIntent, intentQueryFlags, UserHandle.of(ActivityManager.getCurrentUser()));
-        for (ResolveInfo resolveInfo : resolveInfos) {
-            BoundServiceInfo serviceInfo =
-                    new BoundServiceInfo(mIntent.getAction(), resolveInfo);
-
-            if (sBoundServiceInfoComparator.compare(serviceInfo, bestServiceInfo) > 0) {
-                bestServiceInfo = serviceInfo;
-            }
-        }
-
-        return bestServiceInfo;
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        String action = intent.getAction();
-        if (action == null) {
-            return;
-        }
-        int userId = intent.getIntExtra(EXTRA_USER_HANDLE, USER_NULL);
-        if (userId == USER_NULL) {
-            return;
-        }
-        ServiceChangedListener listener = mListener;
-        if (listener == null) {
-            return;
-        }
-
-        switch (action) {
-            case ACTION_USER_SWITCHED:
-                listener.onServiceChanged();
-                break;
-            case Intent.ACTION_USER_UNLOCKED:
-                // user unlocked implies direct boot mode may have changed
-                if (userId == ActivityManager.getCurrentUser()) {
-                    listener.onServiceChanged();
-                }
-                break;
-            default:
-                break;
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ForegroundThread.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ForegroundThread.java
deleted file mode 100644
index 2c363f8..0000000
--- a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ForegroundThread.java
+++ /dev/null
@@ -1,77 +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.server.nearby.common.servicemonitor;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-
-import com.android.modules.utils.HandlerExecutor;
-
-import java.util.concurrent.Executor;
-
-/**
- * Thread for asynchronous event processing. This thread is configured as
- * {@link android.os.Process#THREAD_PRIORITY_FOREGROUND}, which means more CPU
- * resources will be dedicated to it, and it will be treated like "a user
- * interface that the user is interacting with."
- * <p>
- * This thread is best suited for tasks that the user is actively waiting for,
- * or for tasks that the user expects to be executed immediately.
- *
- */
-public final class ForegroundThread extends HandlerThread {
-    private static ForegroundThread sInstance;
-    private static Handler sHandler;
-    private static HandlerExecutor sHandlerExecutor;
-
-    private ForegroundThread() {
-        super("nearbyfg", android.os.Process.THREAD_PRIORITY_FOREGROUND);
-    }
-
-    private static void ensureThreadLocked() {
-        if (sInstance == null) {
-            sInstance = new ForegroundThread();
-            sInstance.start();
-            sHandler = new Handler(sInstance.getLooper());
-            sHandlerExecutor = new HandlerExecutor(sHandler);
-        }
-    }
-
-    /** Get ForegroundThread singleton instance. */
-    public static ForegroundThread get() {
-        synchronized (ForegroundThread.class) {
-            ensureThreadLocked();
-            return sInstance;
-        }
-    }
-
-    /** Get ForegroundThread singleton handler. */
-    public static Handler getHandler() {
-        synchronized (ForegroundThread.class) {
-            ensureThreadLocked();
-            return sHandler;
-        }
-    }
-
-    /** Get ForegroundThread singleton executor. */
-    public static Executor getExecutor() {
-        synchronized (ForegroundThread.class) {
-            ensureThreadLocked();
-            return sHandlerExecutor;
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/PackageWatcher.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/PackageWatcher.java
deleted file mode 100644
index 7d1db57..0000000
--- a/nearby/service/java/com/android/server/nearby/common/servicemonitor/PackageWatcher.java
+++ /dev/null
@@ -1,130 +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.server.nearby.common.servicemonitor;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
-
-import com.android.modules.utils.BackgroundThread;
-
-import java.util.Objects;
-
-/**
- * This is mostly from frameworks PackageMonitor.
- * Helper class for watching somePackagesChanged.
- */
-public abstract class PackageWatcher extends BroadcastReceiver {
-    static final String TAG = "PackageWatcher";
-    static final IntentFilter sPackageFilt = new IntentFilter();
-    static final IntentFilter sNonDataFilt = new IntentFilter();
-    static final IntentFilter sExternalFilt = new IntentFilter();
-
-    static {
-        sPackageFilt.addAction(Intent.ACTION_PACKAGE_ADDED);
-        sPackageFilt.addAction(Intent.ACTION_PACKAGE_REMOVED);
-        sPackageFilt.addAction(Intent.ACTION_PACKAGE_CHANGED);
-        sPackageFilt.addDataScheme("package");
-        sNonDataFilt.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
-        sNonDataFilt.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
-        sExternalFilt.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
-        sExternalFilt.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
-    }
-
-    Context mRegisteredContext;
-    Handler mRegisteredHandler;
-    boolean mSomePackagesChanged;
-
-    public PackageWatcher() {
-    }
-
-    void register(Context context, Looper thread, boolean externalStorage) {
-        register(context, externalStorage,
-                (thread == null) ? BackgroundThread.getHandler() : new Handler(thread));
-    }
-
-    void register(Context context, boolean externalStorage, Handler handler) {
-        if (mRegisteredContext != null) {
-            throw new IllegalStateException("Already registered");
-        }
-        mRegisteredContext = context;
-        mRegisteredHandler = Objects.requireNonNull(handler);
-        context.registerReceiverForAllUsers(this, sPackageFilt, null, mRegisteredHandler);
-        context.registerReceiverForAllUsers(this, sNonDataFilt, null, mRegisteredHandler);
-        if (externalStorage) {
-            context.registerReceiverForAllUsers(this, sExternalFilt, null, mRegisteredHandler);
-        }
-    }
-
-    void unregister() {
-        if (mRegisteredContext == null) {
-            throw new IllegalStateException("Not registered");
-        }
-        mRegisteredContext.unregisterReceiver(this);
-        mRegisteredContext = null;
-    }
-
-    // Called when some package has been changed.
-    abstract void onSomePackagesChanged();
-
-    String getPackageName(Intent intent) {
-        Uri uri = intent.getData();
-        String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
-        return pkg;
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        mSomePackagesChanged = false;
-
-        String action = intent.getAction();
-        if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
-            // We consider something to have changed regardless of whether
-            // this is just an update, because the update is now finished
-            // and the contents of the package may have changed.
-            mSomePackagesChanged = true;
-        } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
-            String pkg = getPackageName(intent);
-            if (pkg != null) {
-                if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
-                    mSomePackagesChanged = true;
-                }
-            }
-        } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
-            String pkg = getPackageName(intent);
-            if (pkg != null) {
-                mSomePackagesChanged = true;
-            }
-        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
-            mSomePackagesChanged = true;
-        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
-            mSomePackagesChanged = true;
-        } else if (Intent.ACTION_PACKAGES_SUSPENDED.equals(action)) {
-            mSomePackagesChanged = true;
-        } else if (Intent.ACTION_PACKAGES_UNSUSPENDED.equals(action)) {
-            mSomePackagesChanged = true;
-        }
-
-        if (mSomePackagesChanged) {
-            onSomePackagesChanged();
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitor.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitor.java
deleted file mode 100644
index a86af85..0000000
--- a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitor.java
+++ /dev/null
@@ -1,249 +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.server.nearby.common.servicemonitor;
-
-import android.annotation.Nullable;
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.RemoteException;
-
-import java.io.PrintWriter;
-import java.util.Objects;
-import java.util.concurrent.Executor;
-
-/**
- * This is exported from frameworks ServiceWatcher.
- * A ServiceMonitor is responsible for continuously maintaining an active binding to a service
- * selected by it's {@link ServiceProvider}. The {@link ServiceProvider} may change the service it
- * selects over time, and the currently bound service may crash, restart, have a user change, have
- * changes made to its package, and so on and so forth. The ServiceMonitor is responsible for
- * maintaining the binding across all these changes.
- *
- * <p>Clients may invoke {@link BinderOperation}s on the ServiceMonitor, and it will make a best
- * effort to run these on the currently bound service, but individual operations may fail (if there
- * is no service currently bound for instance). In order to help clients maintain the correct state,
- * clients may supply a {@link ServiceListener}, which is informed when the ServiceMonitor connects
- * and disconnects from a service. This allows clients to bring a bound service back into a known
- * state on connection, and then run binder operations from there. In order to help clients
- * accomplish this, ServiceMonitor guarantees that {@link BinderOperation}s and the
- * {@link ServiceListener} will always be run on the same thread, so that strong ordering guarantees
- * can be established between them.
- *
- * There is never any guarantee of whether a ServiceMonitor is currently connected to a service, and
- * whether any particular {@link BinderOperation} will succeed. Clients must ensure they do not rely
- * on this, and instead use {@link ServiceListener} notifications as necessary to recover from
- * failures.
- */
-public interface ServiceMonitor {
-
-    /**
-     * Operation to run on a binder interface. All operations will be run on the thread used by the
-     * ServiceMonitor this is run with.
-     */
-    interface BinderOperation {
-        /** Invoked to run the operation. Run on the ServiceMonitor thread. */
-        void run(IBinder binder) throws RemoteException;
-
-        /**
-         * Invoked if {@link #run(IBinder)} could not be invoked because there was no current
-         * binding, or if {@link #run(IBinder)} threw an exception ({@link RemoteException} or
-         * {@link RuntimeException}). This callback is only intended for resource deallocation and
-         * cleanup in response to a single binder operation, it should not be used to propagate
-         * errors further. Run on the ServiceMonitor thread.
-         */
-        default void onError() {}
-    }
-
-    /**
-     * Listener for bind and unbind events. All operations will be run on the thread used by the
-     * ServiceMonitor this is run with.
-     *
-     * @param <TBoundServiceInfo> type of bound service
-     */
-    interface ServiceListener<TBoundServiceInfo extends BoundServiceInfo> {
-        /** Invoked when a service is bound. Run on the ServiceMonitor thread. */
-        void onBind(IBinder binder, TBoundServiceInfo service) throws RemoteException;
-
-        /** Invoked when a service is unbound. Run on the ServiceMonitor thread. */
-        void onUnbind();
-    }
-
-    /**
-     * A listener for when a {@link ServiceProvider} decides that the current service has changed.
-     */
-    interface ServiceChangedListener {
-        /**
-         * Should be invoked when the current service may have changed.
-         */
-        void onServiceChanged();
-    }
-
-    /**
-     * This provider encapsulates the logic of deciding what service a {@link ServiceMonitor} should
-     * be bound to at any given moment.
-     *
-     * @param <TBoundServiceInfo> type of bound service
-     */
-    interface ServiceProvider<TBoundServiceInfo extends BoundServiceInfo> {
-        /**
-         * Should return true if there exists at least one service capable of meeting the criteria
-         * of this provider. This does not imply that {@link #getServiceInfo()} will always return a
-         * non-null result, as any service may be disqualified for various reasons at any point in
-         * time. May be invoked at any time from any thread and thus should generally not have any
-         * dependency on the other methods in this interface.
-         */
-        boolean hasMatchingService();
-
-        /**
-         * Invoked when the provider should start monitoring for any changes that could result in a
-         * different service selection, and should invoke
-         * {@link ServiceChangedListener#onServiceChanged()} in that case. {@link #getServiceInfo()}
-         * may be invoked after this method is called.
-         */
-        void register(ServiceChangedListener listener);
-
-        /**
-         * Invoked when the provider should stop monitoring for any changes that could result in a
-         * different service selection, should no longer invoke
-         * {@link ServiceChangedListener#onServiceChanged()}. {@link #getServiceInfo()} will not be
-         * invoked after this method is called.
-         */
-        void unregister();
-
-        /**
-         * Must be implemented to return the current service selected by this provider. May return
-         * null if no service currently meets the criteria. Only invoked while registered.
-         */
-        @Nullable TBoundServiceInfo getServiceInfo();
-    }
-
-    /**
-     * Information on the service selected as the best option for binding.
-     */
-    class BoundServiceInfo {
-
-        protected final @Nullable String mAction;
-        protected final int mUid;
-        protected final ComponentName mComponentName;
-
-        protected BoundServiceInfo(String action, int uid, ComponentName componentName) {
-            mAction = action;
-            mUid = uid;
-            mComponentName = Objects.requireNonNull(componentName);
-        }
-
-        /** Returns the action associated with this bound service. */
-        public @Nullable String getAction() {
-            return mAction;
-        }
-
-        /** Returns the component of this bound service. */
-        public ComponentName getComponentName() {
-            return mComponentName;
-        }
-
-        @Override
-        public final boolean equals(Object o) {
-            if (this == o) {
-                return true;
-            }
-            if (!(o instanceof BoundServiceInfo)) {
-                return false;
-            }
-
-            BoundServiceInfo that = (BoundServiceInfo) o;
-            return mUid == that.mUid
-                    && Objects.equals(mAction, that.mAction)
-                    && mComponentName.equals(that.mComponentName);
-        }
-
-        @Override
-        public final int hashCode() {
-            return Objects.hash(mAction, mUid, mComponentName);
-        }
-
-        @Override
-        public String toString() {
-            if (mComponentName == null) {
-                return "none";
-            } else {
-                return mUid + "/" + mComponentName.flattenToShortString();
-            }
-        }
-    }
-
-    /**
-     * Creates a new ServiceMonitor instance.
-     */
-    static <TBoundServiceInfo extends BoundServiceInfo> ServiceMonitor create(
-            Context context,
-            String tag,
-            ServiceProvider<TBoundServiceInfo> serviceProvider,
-            @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) {
-        return create(context, ForegroundThread.getHandler(), ForegroundThread.getExecutor(), tag,
-                serviceProvider, serviceListener);
-    }
-
-    /**
-     * Creates a new ServiceMonitor instance that runs on the given handler.
-     */
-    static <TBoundServiceInfo extends BoundServiceInfo> ServiceMonitor create(
-            Context context,
-            Handler handler,
-            Executor executor,
-            String tag,
-            ServiceProvider<TBoundServiceInfo> serviceProvider,
-            @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) {
-        return new ServiceMonitorImpl<>(context, handler, executor, tag, serviceProvider,
-                serviceListener);
-    }
-
-    /**
-     * Returns true if there is at least one service that the ServiceMonitor could hypothetically
-     * bind to, as selected by the {@link ServiceProvider}.
-     */
-    boolean checkServiceResolves();
-
-    /**
-     * Registers the ServiceMonitor, so that it will begin maintaining an active binding to the
-     * service selected by {@link ServiceProvider}, until {@link #unregister()} is called.
-     */
-    void register();
-
-    /**
-     * Unregisters the ServiceMonitor, so that it will release any active bindings. If the
-     * ServiceMonitor is currently bound, this will result in one final
-     * {@link ServiceListener#onUnbind()} invocation, which may happen after this method completes
-     * (but which is guaranteed to occur before any further
-     * {@link ServiceListener#onBind(IBinder, BoundServiceInfo)} invocation in response to a later
-     * call to {@link #register()}).
-     */
-    void unregister();
-
-    /**
-     * Runs the given binder operation on the currently bound service (if available). The operation
-     * will always fail if the ServiceMonitor is not currently registered.
-     */
-    void runOnBinder(BinderOperation operation);
-
-    /**
-     * Dumps ServiceMonitor information.
-     */
-    void dump(PrintWriter pw);
-}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitorImpl.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitorImpl.java
deleted file mode 100644
index d0d6c3b..0000000
--- a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitorImpl.java
+++ /dev/null
@@ -1,305 +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.server.nearby.common.servicemonitor;
-
-import static android.content.Context.BIND_AUTO_CREATE;
-import static android.content.Context.BIND_NOT_FOREGROUND;
-
-import android.annotation.Nullable;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.Preconditions;
-import com.android.server.nearby.common.servicemonitor.ServiceMonitor.BoundServiceInfo;
-import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceChangedListener;
-
-import java.io.PrintWriter;
-import java.util.Objects;
-import java.util.concurrent.Executor;
-
-/**
- * Implementation of ServiceMonitor. Keeping the implementation separate from the interface allows
- * us to store the generic relationship between the service provider and the service listener, while
- * hiding the generics from clients, simplifying the API.
- */
-class ServiceMonitorImpl<TBoundServiceInfo extends BoundServiceInfo> implements ServiceMonitor,
-        ServiceChangedListener {
-
-    private static final String TAG = "ServiceMonitor";
-    private static final boolean D = Log.isLoggable(TAG, Log.DEBUG);
-    private static final long RETRY_DELAY_MS = 15 * 1000;
-
-    // This is the same as Context.BIND_NOT_VISIBLE.
-    private static final int BIND_NOT_VISIBLE = 0x40000000;
-
-    final Context mContext;
-    final Handler mHandler;
-    final Executor mExecutor;
-    final String mTag;
-    final ServiceProvider<TBoundServiceInfo> mServiceProvider;
-    final @Nullable ServiceListener<? super TBoundServiceInfo> mServiceListener;
-
-    private final PackageWatcher mPackageWatcher = new PackageWatcher() {
-        @Override
-        public void onSomePackagesChanged() {
-            onServiceChanged(false);
-        }
-    };
-
-    @GuardedBy("this")
-    private boolean mRegistered = false;
-    @GuardedBy("this")
-    private MyServiceConnection mServiceConnection = new MyServiceConnection(null);
-
-    ServiceMonitorImpl(Context context, Handler handler, Executor executor, String tag,
-            ServiceProvider<TBoundServiceInfo> serviceProvider,
-            ServiceListener<? super TBoundServiceInfo> serviceListener) {
-        mContext = context;
-        mExecutor = executor;
-        mHandler = handler;
-        mTag = tag;
-        mServiceProvider = serviceProvider;
-        mServiceListener = serviceListener;
-    }
-
-    @Override
-    public boolean checkServiceResolves() {
-        return mServiceProvider.hasMatchingService();
-    }
-
-    @Override
-    public synchronized void register() {
-        Preconditions.checkState(!mRegistered);
-
-        mRegistered = true;
-        mPackageWatcher.register(mContext, /*externalStorage=*/ true, mHandler);
-        mServiceProvider.register(this);
-
-        onServiceChanged(false);
-    }
-
-    @Override
-    public synchronized void unregister() {
-        Preconditions.checkState(mRegistered);
-
-        mServiceProvider.unregister();
-        mPackageWatcher.unregister();
-        mRegistered = false;
-
-        onServiceChanged(false);
-    }
-
-    @Override
-    public synchronized void onServiceChanged() {
-        onServiceChanged(false);
-    }
-
-    @Override
-    public synchronized void runOnBinder(BinderOperation operation) {
-        MyServiceConnection serviceConnection = mServiceConnection;
-        mHandler.post(() -> serviceConnection.runOnBinder(operation));
-    }
-
-    synchronized void onServiceChanged(boolean forceRebind) {
-        TBoundServiceInfo newBoundServiceInfo;
-        if (mRegistered) {
-            newBoundServiceInfo = mServiceProvider.getServiceInfo();
-        } else {
-            newBoundServiceInfo = null;
-        }
-
-        if (forceRebind || !Objects.equals(mServiceConnection.getBoundServiceInfo(),
-                newBoundServiceInfo)) {
-            Log.i(TAG, "[" + mTag + "] chose new implementation " + newBoundServiceInfo);
-            MyServiceConnection oldServiceConnection = mServiceConnection;
-            MyServiceConnection newServiceConnection = new MyServiceConnection(newBoundServiceInfo);
-            mServiceConnection = newServiceConnection;
-            mHandler.post(() -> {
-                oldServiceConnection.unbind();
-                newServiceConnection.bind();
-            });
-        }
-    }
-
-    @Override
-    public String toString() {
-        MyServiceConnection serviceConnection;
-        synchronized (this) {
-            serviceConnection = mServiceConnection;
-        }
-
-        return serviceConnection.getBoundServiceInfo().toString();
-    }
-
-    @Override
-    public void dump(PrintWriter pw) {
-        MyServiceConnection serviceConnection;
-        synchronized (this) {
-            serviceConnection = mServiceConnection;
-        }
-
-        pw.println("target service=" + serviceConnection.getBoundServiceInfo());
-        pw.println("connected=" + serviceConnection.isConnected());
-    }
-
-    // runs on the handler thread, and expects most of its methods to be called from that thread
-    private class MyServiceConnection implements ServiceConnection {
-
-        private final @Nullable TBoundServiceInfo mBoundServiceInfo;
-
-        // volatile so that isConnected can be called from any thread easily
-        private volatile @Nullable IBinder mBinder;
-        private @Nullable Runnable mRebinder;
-
-        MyServiceConnection(@Nullable TBoundServiceInfo boundServiceInfo) {
-            mBoundServiceInfo = boundServiceInfo;
-        }
-
-        // may be called from any thread
-        @Nullable TBoundServiceInfo getBoundServiceInfo() {
-            return mBoundServiceInfo;
-        }
-
-        // may be called from any thread
-        boolean isConnected() {
-            return mBinder != null;
-        }
-
-        void bind() {
-            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
-
-            if (mBoundServiceInfo == null) {
-                return;
-            }
-
-            if (D) {
-                Log.d(TAG, "[" + mTag + "] binding to " + mBoundServiceInfo);
-            }
-
-            Intent bindIntent = new Intent(mBoundServiceInfo.getAction())
-                    .setComponent(mBoundServiceInfo.getComponentName());
-            if (!mContext.bindService(bindIntent,
-                    BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE,
-                    mExecutor, this)) {
-                Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later");
-                mRebinder = this::bind;
-                mHandler.postDelayed(mRebinder, RETRY_DELAY_MS);
-            } else {
-                mRebinder = null;
-            }
-        }
-
-        void unbind() {
-            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
-
-            if (mBoundServiceInfo == null) {
-                return;
-            }
-
-            if (D) {
-                Log.d(TAG, "[" + mTag + "] unbinding from " + mBoundServiceInfo);
-            }
-
-            if (mRebinder != null) {
-                mHandler.removeCallbacks(mRebinder);
-                mRebinder = null;
-            } else {
-                mContext.unbindService(this);
-            }
-
-            onServiceDisconnected(mBoundServiceInfo.getComponentName());
-        }
-
-        void runOnBinder(BinderOperation operation) {
-            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
-
-            if (mBinder == null) {
-                operation.onError();
-                return;
-            }
-
-            try {
-                operation.run(mBinder);
-            } catch (RuntimeException | RemoteException e) {
-                // binders may propagate some specific non-RemoteExceptions from the other side
-                // through the binder as well - we cannot allow those to crash the system server
-                Log.e(TAG, "[" + mTag + "] error running operation on " + mBoundServiceInfo, e);
-                operation.onError();
-            }
-        }
-
-        @Override
-        public final void onServiceConnected(ComponentName component, IBinder binder) {
-            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
-            Preconditions.checkState(mBinder == null);
-
-            Log.i(TAG, "[" + mTag + "] connected to " + component.toShortString());
-
-            mBinder = binder;
-
-            if (mServiceListener != null) {
-                try {
-                    mServiceListener.onBind(binder, mBoundServiceInfo);
-                } catch (RuntimeException | RemoteException e) {
-                    // binders may propagate some specific non-RemoteExceptions from the other side
-                    // through the binder as well - we cannot allow those to crash the system server
-                    Log.e(TAG, "[" + mTag + "] error running operation on " + mBoundServiceInfo, e);
-                }
-            }
-        }
-
-        @Override
-        public final void onServiceDisconnected(ComponentName component) {
-            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
-
-            if (mBinder == null) {
-                return;
-            }
-
-            Log.i(TAG, "[" + mTag + "] disconnected from " + mBoundServiceInfo);
-
-            mBinder = null;
-            if (mServiceListener != null) {
-                mServiceListener.onUnbind();
-            }
-        }
-
-        @Override
-        public final void onBindingDied(ComponentName component) {
-            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
-
-            Log.w(TAG, "[" + mTag + "] " + mBoundServiceInfo + " died");
-
-            // introduce a small delay to prevent spamming binding over and over, since the likely
-            // cause of a binding dying is some package event that may take time to recover from
-            mHandler.postDelayed(() -> onServiceChanged(true), 500);
-        }
-
-        @Override
-        public final void onNullBinding(ComponentName component) {
-            Log.e(TAG, "[" + mTag + "] " + mBoundServiceInfo + " has null binding");
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/Constant.java b/nearby/service/java/com/android/server/nearby/fastpair/Constant.java
deleted file mode 100644
index f35703f..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/Constant.java
+++ /dev/null
@@ -1,33 +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.server.nearby.fastpair;
-
-/**
- * String constant for half sheet.
- */
-public class Constant {
-
-    /**
-     * Value represents true for {@link android.provider.Settings.Secure}
-     */
-    public static final int SETTINGS_TRUE_VALUE = 1;
-
-    /**
-     * Tag for Fast Pair service related logs.
-     */
-    public static final String TAG = "FastPairService";
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
deleted file mode 100644
index 412b738..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
+++ /dev/null
@@ -1,331 +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.server.nearby.fastpair;
-
-import static com.android.server.nearby.fastpair.Constant.TAG;
-
-import static com.google.common.primitives.Bytes.concat;
-
-import android.accounts.Account;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.nearby.FastPairDevice;
-import android.nearby.NearbyDevice;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.common.ble.decode.FastPairDecoder;
-import com.android.server.nearby.common.ble.util.RangingUtils;
-import com.android.server.nearby.common.bloomfilter.BloomFilter;
-import com.android.server.nearby.common.bloomfilter.FastPairBloomFilterHasher;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
-import com.android.server.nearby.provider.FastPairDataProvider;
-import com.android.server.nearby.util.ArrayUtils;
-import com.android.server.nearby.util.DataUtils;
-import com.android.server.nearby.util.Hex;
-
-import java.util.List;
-
-import service.proto.Cache;
-import service.proto.Data;
-import service.proto.Rpcs;
-
-/**
- * Handler that handle fast pair related broadcast.
- */
-public class FastPairAdvHandler {
-    Context mContext;
-    String mBleAddress;
-    // TODO(b/247152236): Need to confirm the usage
-    // and deleted this after notification manager in use.
-    private boolean mIsFirst = true;
-    private FastPairDataProvider mPairDataProvider;
-    private static final double NEARBY_DISTANCE_THRESHOLD = 0.6;
-    // The byte, 0bLLLLTTTT, for battery length and type.
-    // Bit 0 - 3: type, 0b0011 (show UI indication) or 0b0100 (hide UI indication).
-    // Bit 4 - 7: length.
-    // https://developers.google.com/nearby/fast-pair/specifications/extensions/batterynotification
-    private static final byte SHOW_UI_INDICATION = 0b0011;
-    private static final byte HIDE_UI_INDICATION = 0b0100;
-    private static final int LENGTH_ADVERTISEMENT_TYPE_BIT = 4;
-
-    /** The types about how the bloomfilter is processed. */
-    public enum ProcessBloomFilterType {
-        IGNORE, // The bloomfilter is not handled. e.g. distance is too far away.
-        CACHE, // The bloomfilter is recognized in the local cache.
-        FOOTPRINT, // Need to check the bloomfilter from the footprints.
-        ACCOUNT_KEY_HIT // The specified account key was hit the bloom filter.
-    }
-
-    /**
-     * Constructor function.
-     */
-    public FastPairAdvHandler(Context context) {
-        mContext = context;
-    }
-
-    @VisibleForTesting
-    FastPairAdvHandler(Context context, FastPairDataProvider dataProvider) {
-        mContext = context;
-        mPairDataProvider = dataProvider;
-    }
-
-    /**
-     * Handles all of the scanner result. Fast Pair will handle model id broadcast bloomfilter
-     * broadcast and battery level broadcast.
-     */
-    public void handleBroadcast(NearbyDevice device) {
-        FastPairDevice fastPairDevice = (FastPairDevice) device;
-        mBleAddress = fastPairDevice.getBluetoothAddress();
-        if (mPairDataProvider == null) {
-            mPairDataProvider = FastPairDataProvider.getInstance();
-        }
-        if (mPairDataProvider == null) {
-            return;
-        }
-
-        if (FastPairDecoder.checkModelId(fastPairDevice.getData())) {
-            byte[] model = FastPairDecoder.getModelId(fastPairDevice.getData());
-            Log.v(TAG, "On discovery model id " + Hex.bytesToStringLowercase(model));
-            // Use api to get anti spoofing key from model id.
-            try {
-                List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts();
-                Rpcs.GetObservedDeviceResponse response =
-                        mPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(model);
-                if (response == null) {
-                    Log.e(TAG, "server does not have model id "
-                            + Hex.bytesToStringLowercase(model));
-                    return;
-                }
-                // Check the distance of the device if the distance is larger than the threshold
-                // do not show half sheet.
-                if (!isNearby(fastPairDevice.getRssi(),
-                        response.getDevice().getBleTxPower() == 0 ? fastPairDevice.getTxPower()
-                                : response.getDevice().getBleTxPower())) {
-                    return;
-                }
-                Locator.get(mContext, FastPairHalfSheetManager.class).showHalfSheet(
-                        DataUtils.toScanFastPairStoreItem(
-                                response, mBleAddress, Hex.bytesToStringLowercase(model),
-                                accountList.isEmpty() ? null : accountList.get(0).name));
-            } catch (IllegalStateException e) {
-                Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
-            }
-        } else {
-            // Start to process bloom filter. Yet to finish.
-            try {
-                subsequentPair(fastPairDevice);
-            } catch (IllegalStateException e) {
-                Log.e(TAG, "handleBroadcast: subsequent pair failed", e);
-            }
-        }
-    }
-
-    @Nullable
-    @VisibleForTesting
-    static byte[] getBloomFilterBytes(byte[] data) {
-        byte[] bloomFilterBytes = FastPairDecoder.getBloomFilter(data);
-        if (bloomFilterBytes == null) {
-            bloomFilterBytes = FastPairDecoder.getBloomFilterNoNotification(data);
-        }
-        if (ArrayUtils.isEmpty(bloomFilterBytes)) {
-            Log.d(TAG, "subsequentPair: bloomFilterByteArray empty");
-            return null;
-        }
-        return bloomFilterBytes;
-    }
-
-    private int getTxPower(FastPairDevice scannedDevice,
-            Data.FastPairDeviceWithAccountKey recognizedDevice) {
-        return recognizedDevice.getDiscoveryItem().getTxPower() == 0
-                ? scannedDevice.getTxPower()
-                : recognizedDevice.getDiscoveryItem().getTxPower();
-    }
-
-    private void subsequentPair(FastPairDevice scannedDevice) {
-        byte[] data = scannedDevice.getData();
-
-        if (ArrayUtils.isEmpty(data)) {
-            Log.d(TAG, "subsequentPair: no valid data");
-            return;
-        }
-
-        byte[] bloomFilterBytes = getBloomFilterBytes(data);
-        if (ArrayUtils.isEmpty(bloomFilterBytes)) {
-            Log.d(TAG, "subsequentPair: no valid bloom filter");
-            return;
-        }
-
-        byte[] salt = FastPairDecoder.getBloomFilterSalt(data);
-        if (ArrayUtils.isEmpty(salt)) {
-            Log.d(TAG, "subsequentPair: no valid salt");
-            return;
-        }
-        byte[] saltWithData = concat(salt, generateBatteryData(data));
-
-        List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts();
-        for (Account account : accountList) {
-            List<Data.FastPairDeviceWithAccountKey> devices =
-                    mPairDataProvider.loadFastPairDeviceWithAccountKey(account);
-            Data.FastPairDeviceWithAccountKey recognizedDevice =
-                    findRecognizedDevice(devices,
-                            new BloomFilter(bloomFilterBytes,
-                                    new FastPairBloomFilterHasher()), saltWithData);
-            if (recognizedDevice == null) {
-                Log.v(TAG, "subsequentPair: recognizedDevice is null");
-                continue;
-            }
-
-            // Check the distance of the device if the distance is larger than the
-            // threshold
-            if (!isNearby(scannedDevice.getRssi(), getTxPower(scannedDevice, recognizedDevice))) {
-                Log.v(TAG,
-                        "subsequentPair: the distance of the device is larger than the threshold");
-                return;
-            }
-
-            // Check if the device is already paired
-            List<Cache.StoredFastPairItem> storedFastPairItemList =
-                    Locator.get(mContext, FastPairCacheManager.class)
-                            .getAllSavedStoredFastPairItem();
-            Cache.StoredFastPairItem recognizedStoredFastPairItem =
-                    findRecognizedDeviceFromCachedItem(storedFastPairItemList,
-                            new BloomFilter(bloomFilterBytes,
-                                    new FastPairBloomFilterHasher()), saltWithData);
-            if (recognizedStoredFastPairItem != null) {
-                // The bloomfilter is recognized in the cache so the device is paired
-                // before
-                Log.d(TAG, "bloom filter is recognized in the cache");
-                continue;
-            }
-            showSubsequentNotification(account, scannedDevice, recognizedDevice);
-        }
-    }
-
-    private void showSubsequentNotification(Account account, FastPairDevice scannedDevice,
-            Data.FastPairDeviceWithAccountKey recognizedDevice) {
-        // Get full info from api the initial request will only return
-        // part of the info due to size limit.
-        List<Data.FastPairDeviceWithAccountKey> devicesWithAccountKeys =
-                mPairDataProvider.loadFastPairDeviceWithAccountKey(account,
-                        List.of(recognizedDevice.getAccountKey().toByteArray()));
-        if (devicesWithAccountKeys == null || devicesWithAccountKeys.isEmpty()) {
-            Log.d(TAG, "No fast pair device with account key is found.");
-            return;
-        }
-
-        // Saved device from footprint does not have ble address.
-        // We need to fill ble address with current scan result.
-        Cache.StoredDiscoveryItem storedDiscoveryItem =
-                devicesWithAccountKeys.get(0).getDiscoveryItem().toBuilder()
-                        .setMacAddress(
-                                scannedDevice.getBluetoothAddress())
-                        .build();
-        // Show notification
-        FastPairNotificationManager fastPairNotificationManager =
-                Locator.get(mContext, FastPairNotificationManager.class);
-        DiscoveryItem item = new DiscoveryItem(mContext, storedDiscoveryItem);
-        Locator.get(mContext, FastPairCacheManager.class).saveDiscoveryItem(item);
-        fastPairNotificationManager.showDiscoveryNotification(item,
-                devicesWithAccountKeys.get(0).getAccountKey().toByteArray());
-    }
-
-    // Battery advertisement format:
-    // Byte 0: Battery length and type, Bit 0 - 3: type, Bit 4 - 7: length.
-    // Byte 1 - 3: Battery values.
-    // Reference:
-    // https://developers.google.com/nearby/fast-pair/specifications/extensions/batterynotification
-    @VisibleForTesting
-    static byte[] generateBatteryData(byte[] data) {
-        byte[] batteryLevelNoNotification = FastPairDecoder.getBatteryLevelNoNotification(data);
-        boolean suppressBatteryNotification =
-                (batteryLevelNoNotification != null && batteryLevelNoNotification.length > 0);
-        byte[] batteryValues =
-                suppressBatteryNotification
-                        ? batteryLevelNoNotification
-                        : FastPairDecoder.getBatteryLevel(data);
-        if (ArrayUtils.isEmpty(batteryValues)) {
-            return new byte[0];
-        }
-        return generateBatteryData(suppressBatteryNotification, batteryValues);
-    }
-
-    @VisibleForTesting
-    static byte[] generateBatteryData(boolean suppressBatteryNotification, byte[] batteryValues) {
-        return concat(
-                new byte[] {
-                        (byte)
-                                (batteryValues.length << LENGTH_ADVERTISEMENT_TYPE_BIT
-                                        | (suppressBatteryNotification
-                                        ? HIDE_UI_INDICATION : SHOW_UI_INDICATION))
-                },
-                batteryValues);
-    }
-
-    /**
-     * Checks the bloom filter to see if any of the devices are recognized and should have a
-     * notification displayed for them. A device is recognized if the account key + salt combination
-     * is inside the bloom filter.
-     */
-    @Nullable
-    @VisibleForTesting
-    static Data.FastPairDeviceWithAccountKey findRecognizedDevice(
-            List<Data.FastPairDeviceWithAccountKey> devices, BloomFilter bloomFilter, byte[] salt) {
-        for (Data.FastPairDeviceWithAccountKey device : devices) {
-            if (device.getAccountKey().toByteArray() == null || salt == null) {
-                return null;
-            }
-            byte[] rotatedKey = concat(device.getAccountKey().toByteArray(), salt);
-
-            StringBuilder sb = new StringBuilder();
-            for (byte b : rotatedKey) {
-                sb.append(b);
-            }
-
-            if (bloomFilter.possiblyContains(rotatedKey)) {
-                return device;
-            }
-        }
-        return null;
-    }
-
-    @Nullable
-    static Cache.StoredFastPairItem findRecognizedDeviceFromCachedItem(
-            List<Cache.StoredFastPairItem> devices, BloomFilter bloomFilter, byte[] salt) {
-        for (Cache.StoredFastPairItem device : devices) {
-            if (device.getAccountKey().toByteArray() == null || salt == null) {
-                return null;
-            }
-            byte[] rotatedKey = concat(device.getAccountKey().toByteArray(), salt);
-            if (bloomFilter.possiblyContains(rotatedKey)) {
-                return device;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Check the device distance for certain rssi value.
-     */
-    boolean isNearby(int rssi, int txPower) {
-        return RangingUtils.distanceFromRssiAndTxPower(rssi, txPower) < NEARBY_DISTANCE_THRESHOLD;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
deleted file mode 100644
index 447d199..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
+++ /dev/null
@@ -1,320 +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.server.nearby.fastpair;
-
-import static com.google.common.primitives.Bytes.concat;
-
-import android.accounts.Account;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.nearby.FastPairDevice;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.annotation.WorkerThread;
-
-import com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress;
-import com.android.server.nearby.common.eventloop.Annotations;
-import com.android.server.nearby.common.eventloop.EventLoop;
-import com.android.server.nearby.common.eventloop.NamedRunnable;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
-import com.android.server.nearby.fastpair.pairinghandler.PairingProgressHandlerBase;
-import com.android.server.nearby.provider.FastPairDataProvider;
-
-import com.google.common.hash.Hashing;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-
-import service.proto.Cache;
-
-/**
- * FastPair controller after get the info from intent handler Fast Pair controller is responsible
- * for pairing control.
- */
-public class FastPairController {
-    private static final String TAG = "FastPairController";
-    private final Context mContext;
-    private final EventLoop mEventLoop;
-    private final FastPairCacheManager mFastPairCacheManager;
-    private final FootprintsDeviceManager mFootprintsDeviceManager;
-    private boolean mIsFastPairing = false;
-    // boolean flag whether upload to footprint or not.
-    private boolean mShouldUpload = false;
-    @Nullable
-    private Callback mCallback;
-
-    public FastPairController(Context context) {
-        mContext = context;
-        mEventLoop = Locator.get(mContext, EventLoop.class);
-        mFastPairCacheManager = Locator.get(mContext, FastPairCacheManager.class);
-        mFootprintsDeviceManager = Locator.get(mContext, FootprintsDeviceManager.class);
-    }
-
-    /**
-     * Should be called on create lifecycle.
-     */
-    @WorkerThread
-    public void onCreate() {
-        mEventLoop.postRunnable(new NamedRunnable("FastPairController::InitializeScanner") {
-            @Override
-            public void run() {
-                // init scanner here and start scan.
-            }
-        });
-    }
-
-    /**
-     * Should be called on destroy lifecycle.
-     */
-    @WorkerThread
-    public void onDestroy() {
-        mEventLoop.postRunnable(new NamedRunnable("FastPairController::DestroyScanner") {
-            @Override
-            public void run() {
-                // Unregister scanner from here
-            }
-        });
-    }
-
-    /**
-     * Pairing function.
-     */
-    public void pair(FastPairDevice fastPairDevice) {
-        byte[] discoveryItem = fastPairDevice.getData();
-        String modelId = fastPairDevice.getModelId();
-
-        Log.v(TAG, "pair: fastPairDevice " + fastPairDevice);
-        mEventLoop.postRunnable(
-                new NamedRunnable("fastPairWith=" + modelId) {
-                    @Override
-                    public void run() {
-                        try {
-                            DiscoveryItem item = new DiscoveryItem(mContext,
-                                    Cache.StoredDiscoveryItem.parseFrom(discoveryItem));
-                            if (TextUtils.isEmpty(item.getMacAddress())) {
-                                Log.w(TAG, "There is no mac address in the DiscoveryItem,"
-                                        + " ignore pairing");
-                                return;
-                            }
-                            // Check enabled state to prevent multiple pair attempts if we get the
-                            // intent more than once (this can happen due to an Android platform
-                            // bug - b/31459521).
-                            if (item.getState()
-                                    != Cache.StoredDiscoveryItem.State.STATE_ENABLED) {
-                                Log.d(TAG, "Incorrect state, ignore pairing");
-                                return;
-                            }
-                            FastPairNotificationManager fastPairNotificationManager =
-                                    Locator.get(mContext, FastPairNotificationManager.class);
-                            FastPairHalfSheetManager fastPairHalfSheetManager =
-                                    Locator.get(mContext, FastPairHalfSheetManager.class);
-                            mFastPairCacheManager.saveDiscoveryItem(item);
-
-                            PairingProgressHandlerBase pairingProgressHandlerBase =
-                                    PairingProgressHandlerBase.create(
-                                            mContext,
-                                            item,
-                                            /* companionApp= */ null,
-                                            /* accountKey= */ null,
-                                            mFootprintsDeviceManager,
-                                            fastPairNotificationManager,
-                                            fastPairHalfSheetManager,
-                                            /* isRetroactivePair= */ false);
-
-                            pair(item,
-                                    /* accountKey= */ null,
-                                    /* companionApp= */ null,
-                                    pairingProgressHandlerBase);
-                        } catch (InvalidProtocolBufferException e) {
-                            Log.w(TAG,
-                                    "Error parsing serialized discovery item with size "
-                                            + discoveryItem.length);
-                        }
-                    }
-                });
-    }
-
-    /**
-     * Subsequent pairing entry.
-     */
-    public void pair(DiscoveryItem item,
-            @Nullable byte[] accountKey,
-            @Nullable String companionApp) {
-        FastPairNotificationManager fastPairNotificationManager =
-                Locator.get(mContext, FastPairNotificationManager.class);
-        FastPairHalfSheetManager fastPairHalfSheetManager =
-                Locator.get(mContext, FastPairHalfSheetManager.class);
-        PairingProgressHandlerBase pairingProgressHandlerBase =
-                PairingProgressHandlerBase.create(
-                        mContext,
-                        item,
-                        /* companionApp= */ null,
-                        /* accountKey= */ accountKey,
-                        mFootprintsDeviceManager,
-                        fastPairNotificationManager,
-                        fastPairHalfSheetManager,
-                        /* isRetroactivePair= */ false);
-        pair(item, accountKey, companionApp, pairingProgressHandlerBase);
-    }
-    /**
-     * Pairing function
-     */
-    @Annotations.EventThread
-    public void pair(
-            DiscoveryItem item,
-            @Nullable byte[] accountKey,
-            @Nullable String companionApp,
-            PairingProgressHandlerBase pairingProgressHandlerBase) {
-        if (mIsFastPairing) {
-            Log.d(TAG, "FastPair: fastpairing, skip pair request");
-            return;
-        }
-        mIsFastPairing = true;
-        Log.d(TAG, "FastPair: start pair");
-
-        // Hide all "tap to pair" notifications until after the flow completes.
-        mEventLoop.removeRunnable(mReEnableAllDeviceItemsRunnable);
-        if (mCallback != null) {
-            mCallback.fastPairUpdateDeviceItemsEnabled(false);
-        }
-
-        Future<Void> task =
-                FastPairManager.pair(
-                        Executors.newSingleThreadExecutor(),
-                        mContext,
-                        item,
-                        accountKey,
-                        companionApp,
-                        mFootprintsDeviceManager,
-                        pairingProgressHandlerBase);
-        mIsFastPairing = false;
-    }
-
-    /** Fixes a companion app package name with extra spaces. */
-    private static String trimCompanionApp(String companionApp) {
-        return companionApp == null ? null : companionApp.trim();
-    }
-
-    /**
-     * Function to handle when scanner find bloomfilter.
-     */
-    @Annotations.EventThread
-    public FastPairAdvHandler.ProcessBloomFilterType onBloomFilterDetect(FastPairAdvHandler handler,
-            boolean advertiseInRange) {
-        if (mIsFastPairing) {
-            return FastPairAdvHandler.ProcessBloomFilterType.IGNORE;
-        }
-        // Check if the device is in the cache or footprint.
-        return FastPairAdvHandler.ProcessBloomFilterType.CACHE;
-    }
-
-    /**
-     * Add newly paired device info to footprint
-     */
-    @WorkerThread
-    public void addDeviceToFootprint(String publicAddress, byte[] accountKey,
-            DiscoveryItem discoveryItem) {
-        if (!mShouldUpload) {
-            return;
-        }
-        Log.d(TAG, "upload device to footprint");
-        FastPairManager.processBackgroundTask(() -> {
-            Cache.StoredDiscoveryItem storedDiscoveryItem =
-                    prepareStoredDiscoveryItemForFootprints(discoveryItem);
-            byte[] hashValue =
-                    Hashing.sha256()
-                            .hashBytes(
-                                    concat(accountKey, BluetoothAddress.decode(publicAddress)))
-                            .asBytes();
-            FastPairUploadInfo uploadInfo =
-                    new FastPairUploadInfo(storedDiscoveryItem, ByteString.copyFrom(accountKey),
-                            ByteString.copyFrom(hashValue));
-            // account data place holder here
-            try {
-                FastPairDataProvider fastPairDataProvider = FastPairDataProvider.getInstance();
-                if (fastPairDataProvider == null) {
-                    return;
-                }
-                List<Account> accountList = fastPairDataProvider.loadFastPairEligibleAccounts();
-                if (accountList.size() > 0) {
-                    fastPairDataProvider.optIn(accountList.get(0));
-                    fastPairDataProvider.upload(accountList.get(0), uploadInfo);
-                }
-            } catch (IllegalStateException e) {
-                Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
-            }
-        });
-    }
-
-    @Nullable
-    private Cache.StoredDiscoveryItem getStoredDiscoveryItemFromAddressForFootprints(
-            String bleAddress) {
-
-        List<DiscoveryItem> discoveryItems = new ArrayList<>();
-        //cacheManager.getAllDiscoveryItems();
-        for (DiscoveryItem discoveryItem : discoveryItems) {
-            if (bleAddress.equals(discoveryItem.getMacAddress())) {
-                return prepareStoredDiscoveryItemForFootprints(discoveryItem);
-            }
-        }
-        return null;
-    }
-
-    static Cache.StoredDiscoveryItem prepareStoredDiscoveryItemForFootprints(
-            DiscoveryItem discoveryItem) {
-        Cache.StoredDiscoveryItem.Builder storedDiscoveryItem =
-                discoveryItem.getCopyOfStoredItem().toBuilder();
-        // Strip the mac address so we aren't storing it in the cloud and ensure the item always
-        // starts as enabled and in a good state.
-        storedDiscoveryItem.clearMacAddress();
-
-        return storedDiscoveryItem.build();
-    }
-
-    /**
-     * FastPairConnection will check whether write account key result if the account key is
-     * generated change the parameter.
-     */
-    public void setShouldUpload(boolean shouldUpload) {
-        mShouldUpload = shouldUpload;
-    }
-
-    private final NamedRunnable mReEnableAllDeviceItemsRunnable =
-            new NamedRunnable("reEnableAllDeviceItems") {
-                @Override
-                public void run() {
-                    if (mCallback != null) {
-                        mCallback.fastPairUpdateDeviceItemsEnabled(true);
-                    }
-                }
-            };
-
-    interface Callback {
-        void fastPairUpdateDeviceItemsEnabled(boolean enabled);
-    }
-}
\ No newline at end of file
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
deleted file mode 100644
index 8a9614e..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
+++ /dev/null
@@ -1,511 +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.server.nearby.fastpair;
-
-import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR;
-import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET;
-import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_CANCEL;
-import static com.android.nearby.halfsheet.constants.Constant.ACTION_HALF_SHEET_FOREGROUND_STATE;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_FOREGROUND;
-import static com.android.nearby.halfsheet.constants.FastPairConstants.EXTRA_MODEL_ID;
-import static com.android.server.nearby.fastpair.Constant.TAG;
-
-import static com.google.common.io.BaseEncoding.base16;
-
-import android.annotation.Nullable;
-import android.annotation.WorkerThread;
-import android.app.KeyguardManager;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.ContentObserver;
-import android.nearby.FastPairDevice;
-import android.nearby.NearbyDevice;
-import android.nearby.NearbyManager;
-import android.nearby.ScanCallback;
-import android.nearby.ScanRequest;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import com.android.server.nearby.common.ble.decode.FastPairDecoder;
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
-import com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection;
-import com.android.server.nearby.common.bluetooth.fastpair.PairingException;
-import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
-import com.android.server.nearby.common.bluetooth.fastpair.ReflectionException;
-import com.android.server.nearby.common.bluetooth.fastpair.SimpleBroadcastReceiver;
-import com.android.server.nearby.common.eventloop.Annotations;
-import com.android.server.nearby.common.eventloop.EventLoop;
-import com.android.server.nearby.common.eventloop.NamedRunnable;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-import com.android.server.nearby.fastpair.pairinghandler.PairingProgressHandlerBase;
-import com.android.server.nearby.util.ForegroundThread;
-import com.android.server.nearby.util.Hex;
-
-import com.google.common.collect.ImmutableList;
-import com.google.protobuf.ByteString;
-
-import java.security.GeneralSecurityException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import service.proto.Cache;
-import service.proto.Rpcs;
-
-/**
- * FastPairManager is the class initiated in nearby service to handle Fast Pair related
- * work.
- */
-
-public class FastPairManager {
-
-    private static final String ACTION_PREFIX = UserActionHandler.PREFIX;
-    private static final int WAIT_FOR_UNLOCK_MILLIS = 5000;
-
-    /** A notification ID which should be dismissed */
-    public static final String EXTRA_NOTIFICATION_ID = ACTION_PREFIX + "EXTRA_NOTIFICATION_ID";
-
-    private static Executor sFastPairExecutor;
-
-    private ContentObserver mFastPairScanChangeContentObserver = null;
-
-    final LocatorContextWrapper mLocatorContextWrapper;
-    final IntentFilter mIntentFilter;
-    final Locator mLocator;
-    private boolean mScanEnabled = false;
-    private final FastPairCacheManager mFastPairCacheManager;
-
-    private final BroadcastReceiver mScreenBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            switch (action) {
-                case Intent.ACTION_SCREEN_ON:
-                    Log.d(TAG, "onReceive: ACTION_SCREEN_ON");
-                    invalidateScan();
-                    break;
-                case Intent.ACTION_BOOT_COMPLETED:
-                    Log.d(TAG, "onReceive: ACTION_BOOT_COMPLETED.");
-                    invalidateScan();
-                    break;
-                case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
-                    Log.d(TAG, "onReceive: ACTION_BOND_STATE_CHANGED");
-                    processBluetoothConnectionEvent(intent);
-                    break;
-                case ACTION_HALF_SHEET_FOREGROUND_STATE:
-                    boolean state = intent.getBooleanExtra(EXTRA_HALF_SHEET_FOREGROUND, false);
-                    Log.d(TAG, "halfsheet report foreground state:  " + state);
-                    Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
-                            .setHalfSheetForeground(state);
-                    break;
-                case ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET:
-                    Log.d(TAG, "onReceive: ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET");
-                    String deviceModelId = intent.getStringExtra(EXTRA_MODEL_ID);
-                    if (deviceModelId == null) {
-                        Log.d(TAG, "HalfSheetManager reset device ban state skipped, "
-                                + "deviceModelId not found");
-                        break;
-                    }
-                    Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
-                            .resetBanState(deviceModelId);
-                    break;
-                case ACTION_FAST_PAIR_HALF_SHEET_CANCEL:
-                    Log.d(TAG, "onReceive: ACTION_FAST_PAIR_HALF_SHEET_CANCEL");
-                    String modelId = intent.getStringExtra(EXTRA_MODEL_ID);
-                    if (modelId == null) {
-                        Log.d(TAG, "skip half sheet cancel action, model id not found");
-                        break;
-                    }
-                    Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
-                            .dismiss(modelId);
-                    break;
-                case ACTION_FAST_PAIR:
-                    Log.d(TAG, "onReceive: ACTION_FAST_PAIR");
-                    String itemId = intent.getStringExtra(UserActionHandler.EXTRA_ITEM_ID);
-                    String accountKeyString = intent
-                            .getStringExtra(UserActionHandler.EXTRA_FAST_PAIR_SECRET);
-                    if (itemId == null || accountKeyString == null) {
-                        Log.d(TAG, "skip pair action, item id "
-                                + "or fast pair account key not found");
-                        break;
-                    }
-                    try {
-                        FastPairController controller =
-                                Locator.getFromContextWrapper(mLocatorContextWrapper,
-                                        FastPairController.class);
-                        if (mFastPairCacheManager != null) {
-                            controller.pair(mFastPairCacheManager.getDiscoveryItem(itemId),
-                                    base16().decode(accountKeyString), /* companionApp= */ null);
-                        }
-                    } catch (IllegalStateException e) {
-                        Log.e(TAG, "Cannot find FastPairController class", e);
-                    }
-            }
-        }
-    };
-
-    public FastPairManager(LocatorContextWrapper contextWrapper) {
-        mLocatorContextWrapper = contextWrapper;
-        mIntentFilter = new IntentFilter();
-        mLocator = mLocatorContextWrapper.getLocator();
-        mLocator.bind(new FastPairModule());
-        Rpcs.GetObservedDeviceResponse getObservedDeviceResponse =
-                Rpcs.GetObservedDeviceResponse.newBuilder().build();
-        mFastPairCacheManager =
-                Locator.getFromContextWrapper(mLocatorContextWrapper, FastPairCacheManager.class);
-    }
-
-    final ScanCallback mScanCallback = new ScanCallback() {
-        @Override
-        public void onDiscovered(@NonNull NearbyDevice device) {
-            Locator.get(mLocatorContextWrapper, FastPairAdvHandler.class).handleBroadcast(device);
-        }
-
-        @Override
-        public void onUpdated(@NonNull NearbyDevice device) {
-            FastPairDevice fastPairDevice = (FastPairDevice) device;
-            byte[] modelArray = FastPairDecoder.getModelId(fastPairDevice.getData());
-            Log.d(TAG, "update model id" + Hex.bytesToStringLowercase(modelArray));
-        }
-
-        @Override
-        public void onLost(@NonNull NearbyDevice device) {
-            FastPairDevice fastPairDevice = (FastPairDevice) device;
-            byte[] modelArray = FastPairDecoder.getModelId(fastPairDevice.getData());
-            Log.d(TAG, "lost model id" + Hex.bytesToStringLowercase(modelArray));
-        }
-
-        @Override
-        public void onError(int errorCode) {
-            Log.w(TAG, "[FastPairManager] Scan error is " + errorCode);
-        }
-    };
-
-    /**
-     * Function called when nearby service start.
-     */
-    public void initiate() {
-        mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
-        mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
-        mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
-        mIntentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
-        mIntentFilter.addAction(ACTION_FAST_PAIR_HALF_SHEET_CANCEL);
-        mIntentFilter.addAction(ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET);
-        mIntentFilter.addAction(ACTION_HALF_SHEET_FOREGROUND_STATE);
-        mIntentFilter.addAction(ACTION_FAST_PAIR);
-
-        mLocatorContextWrapper.getContext().registerReceiver(mScreenBroadcastReceiver,
-                mIntentFilter, Context.RECEIVER_EXPORTED);
-    }
-
-    /**
-     * Function to free up fast pair resource.
-     */
-    public void cleanUp() {
-        mLocatorContextWrapper.getContext().unregisterReceiver(mScreenBroadcastReceiver);
-        if (mFastPairScanChangeContentObserver != null) {
-            mLocatorContextWrapper.getContentResolver().unregisterContentObserver(
-                    mFastPairScanChangeContentObserver);
-        }
-    }
-
-    /**
-     * Starts fast pair process.
-     */
-    @Annotations.EventThread
-    public static Future<Void> pair(
-            ExecutorService executor,
-            Context context,
-            DiscoveryItem item,
-            @Nullable byte[] accountKey,
-            @Nullable String companionApp,
-            FootprintsDeviceManager footprints,
-            PairingProgressHandlerBase pairingProgressHandlerBase) {
-        return executor.submit(
-                () -> pairInternal(context, item, companionApp, accountKey, footprints,
-                        pairingProgressHandlerBase), /* result= */ null);
-    }
-
-    /**
-     * Starts fast pair
-     */
-    @WorkerThread
-    public static void pairInternal(
-            Context context,
-            DiscoveryItem item,
-            @Nullable String companionApp,
-            @Nullable byte[] accountKey,
-            FootprintsDeviceManager footprints,
-            PairingProgressHandlerBase pairingProgressHandlerBase) {
-        FastPairHalfSheetManager fastPairHalfSheetManager =
-                Locator.get(context, FastPairHalfSheetManager.class);
-        try {
-            pairingProgressHandlerBase.onPairingStarted();
-            if (pairingProgressHandlerBase.skipWaitingScreenUnlock()) {
-                // Do nothing due to we are not showing the status notification in some pairing
-                // types, e.g. the retroactive pairing.
-            } else {
-                // If the screen is locked when the user taps to pair, the screen will unlock. We
-                // must wait for the unlock to complete before showing the status notification, or
-                // it won't be heads-up.
-                pairingProgressHandlerBase.onWaitForScreenUnlock();
-                waitUntilScreenIsUnlocked(context);
-                pairingProgressHandlerBase.onScreenUnlocked();
-            }
-            BluetoothAdapter bluetoothAdapter = getBluetoothAdapter(context);
-
-            boolean isBluetoothEnabled = bluetoothAdapter != null && bluetoothAdapter.isEnabled();
-            if (!isBluetoothEnabled) {
-                if (bluetoothAdapter == null || !bluetoothAdapter.enable()) {
-                    Log.d(TAG, "FastPair: Failed to enable bluetooth");
-                    return;
-                }
-                Log.v(TAG, "FastPair: Enabling bluetooth for fast pair");
-
-                Locator.get(context, EventLoop.class)
-                        .postRunnable(
-                                new NamedRunnable("enableBluetoothToast") {
-                                    @Override
-                                    public void run() {
-                                        Log.d(TAG, "Enable bluetooth toast test");
-                                    }
-                                });
-                // Set up call back to call this function again once bluetooth has been
-                // enabled; this does not seem to be a problem as the device connects without a
-                // problem, but in theory the timeout also includes turning on bluetooth now.
-            }
-
-            pairingProgressHandlerBase.onReadyToPair();
-
-            String modelId = item.getTriggerId();
-            Preferences.Builder prefsBuilder =
-                    Preferences.builder()
-                            .setEnableBrEdrHandover(false)
-                            .setIgnoreDiscoveryError(true);
-            pairingProgressHandlerBase.onSetupPreferencesBuilder(prefsBuilder);
-            if (item.getFastPairInformation() != null) {
-                prefsBuilder.setSkipConnectingProfiles(
-                        item.getFastPairInformation().getDataOnlyConnection());
-            }
-            // When add watch and auto device needs to change the config
-            prefsBuilder.setRejectMessageAccess(true);
-            prefsBuilder.setRejectPhonebookAccess(true);
-            prefsBuilder.setHandlePasskeyConfirmationByUi(false);
-
-            FastPairConnection connection = new FastPairDualConnection(
-                    context, item.getMacAddress(),
-                    prefsBuilder.build(),
-                    null);
-            connection.setOnPairedCallback(
-                    address -> {
-                        Log.v(TAG, "connection on paired callback;");
-                        // TODO(b/259150992) add fill Bluetooth metadata values logic
-                        pairingProgressHandlerBase.onPairedCallbackCalled(
-                                connection, accountKey, footprints, address);
-                    });
-            pairingProgressHandlerBase.onPairingSetupCompleted();
-
-            FastPairConnection.SharedSecret sharedSecret;
-            if ((accountKey != null || item.getAuthenticationPublicKeySecp256R1() != null)) {
-                sharedSecret =
-                        connection.pair(
-                                accountKey != null ? accountKey
-                                        : item.getAuthenticationPublicKeySecp256R1());
-                if (accountKey == null) {
-                    // Account key is null so it is initial pairing
-                    if (sharedSecret != null) {
-                        Locator.get(context, FastPairController.class).addDeviceToFootprint(
-                                connection.getPublicAddress(), sharedSecret.getKey(), item);
-                        cacheFastPairDevice(context, connection.getPublicAddress(),
-                                sharedSecret.getKey(), item);
-                    }
-                }
-            } else {
-                // Fast Pair one
-                connection.pair();
-            }
-            // TODO(b/213373051): Merge logic with pairingProgressHandlerBase or delete the
-            // pairingProgressHandlerBase class.
-            fastPairHalfSheetManager.showPairingSuccessHalfSheet(connection.getPublicAddress());
-            pairingProgressHandlerBase.onPairingSuccess(connection.getPublicAddress());
-        } catch (BluetoothException
-                | InterruptedException
-                | ReflectionException
-                | TimeoutException
-                | ExecutionException
-                | PairingException
-                | GeneralSecurityException e) {
-            Log.e(TAG, "Failed to pair.", e);
-
-            // TODO(b/213373051): Merge logic with pairingProgressHandlerBase or delete the
-            // pairingProgressHandlerBase class.
-            fastPairHalfSheetManager.showPairingFailed();
-            pairingProgressHandlerBase.onPairingFailed(e);
-        }
-    }
-
-    private static void cacheFastPairDevice(Context context, String publicAddress, byte[] key,
-            DiscoveryItem item) {
-        try {
-            Locator.get(context, EventLoop.class).postAndWait(
-                    new NamedRunnable("FastPairCacheDevice") {
-                        @Override
-                        public void run() {
-                            Cache.StoredFastPairItem storedFastPairItem =
-                                    Cache.StoredFastPairItem.newBuilder()
-                                            .setMacAddress(publicAddress)
-                                            .setAccountKey(ByteString.copyFrom(key))
-                                            .setModelId(item.getTriggerId())
-                                            .addAllFeatures(item.getFastPairInformation() == null
-                                                    ? ImmutableList.of() :
-                                                    item.getFastPairInformation().getFeaturesList())
-                                            .setDiscoveryItem(item.getCopyOfStoredItem())
-                                            .build();
-                            Locator.get(context, FastPairCacheManager.class)
-                                    .putStoredFastPairItem(storedFastPairItem);
-                        }
-                    }
-            );
-        } catch (InterruptedException e) {
-            Log.e(TAG, "Fail to insert paired device into cache");
-        }
-    }
-
-    /** Checks if the pairing is initial pairing with fast pair 2.0 design. */
-    public static boolean isThroughFastPair2InitialPairing(
-            DiscoveryItem item, @Nullable byte[] accountKey) {
-        return accountKey == null && item.getAuthenticationPublicKeySecp256R1() != null;
-    }
-
-    private static void waitUntilScreenIsUnlocked(Context context)
-            throws InterruptedException, ExecutionException, TimeoutException {
-        KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
-
-        // KeyguardManager's isScreenLocked() counterintuitively returns false when the lock screen
-        // is showing if the user has set "swipe to unlock" (i.e. no required password, PIN, or
-        // pattern) So we use this method instead, which returns true when on the lock screen
-        // regardless.
-        if (keyguardManager.isKeyguardLocked()) {
-            Log.v(TAG, "FastPair: Screen is locked, waiting until unlocked "
-                    + "to show status notifications.");
-            try (SimpleBroadcastReceiver isUnlockedReceiver =
-                         SimpleBroadcastReceiver.oneShotReceiver(
-                                 context, FlagUtils.getPreferencesBuilder().build(),
-                                 Intent.ACTION_USER_PRESENT)) {
-                isUnlockedReceiver.await(WAIT_FOR_UNLOCK_MILLIS, TimeUnit.MILLISECONDS);
-            }
-        }
-    }
-
-    /**
-     * Processed task in a background thread
-     */
-    @Annotations.EventThread
-    public static void processBackgroundTask(Runnable runnable) {
-        getExecutor().execute(runnable);
-    }
-
-    /**
-     * This function should only be called on main thread since there is no lock
-     */
-    private static Executor getExecutor() {
-        if (sFastPairExecutor != null) {
-            return sFastPairExecutor;
-        }
-        sFastPairExecutor = Executors.newSingleThreadExecutor();
-        return sFastPairExecutor;
-    }
-
-    /**
-     * Null when the Nearby Service is not available.
-     */
-    @Nullable
-    private NearbyManager getNearbyManager() {
-        return (NearbyManager) mLocatorContextWrapper
-                .getApplicationContext().getSystemService(Context.NEARBY_SERVICE);
-    }
-
-    /**
-     * Starts or stops scanning according to mAllowScan value.
-     */
-    private void invalidateScan() {
-        NearbyManager nearbyManager = getNearbyManager();
-        if (nearbyManager == null) {
-            Log.w(TAG, "invalidateScan: "
-                    + "failed to start or stop scanning because NearbyManager is null.");
-            return;
-        }
-        if (mScanEnabled) {
-            Log.v(TAG, "invalidateScan: scan is enabled");
-            nearbyManager.startScan(new ScanRequest.Builder()
-                            .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR).build(),
-                    ForegroundThread.getExecutor(),
-                    mScanCallback);
-        } else {
-            Log.v(TAG, "invalidateScan: scan is disabled");
-            nearbyManager.stopScan(mScanCallback);
-        }
-    }
-
-    /**
-     * When certain device is forgotten we need to remove the info from database because the info
-     * is no longer useful.
-     */
-    private void processBluetoothConnectionEvent(Intent intent) {
-        int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
-                BluetoothDevice.ERROR);
-        if (bondState == BluetoothDevice.BOND_NONE) {
-            BluetoothDevice device =
-                    intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-            if (device != null) {
-                Log.d("FastPairService", "Forget device detect");
-                processBackgroundTask(new Runnable() {
-                    @Override
-                    public void run() {
-                        mFastPairCacheManager.removeStoredFastPairItem(device.getAddress());
-                    }
-                });
-            }
-
-        }
-    }
-
-    /**
-     * Helper function to get bluetooth adapter.
-     */
-    @Nullable
-    public static BluetoothAdapter getBluetoothAdapter(Context context) {
-        BluetoothManager manager = context.getSystemService(BluetoothManager.class);
-        return manager == null ? null : manager.getAdapter();
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java
deleted file mode 100644
index 1df4723..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java
+++ /dev/null
@@ -1,87 +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.server.nearby.fastpair;
-
-import android.content.Context;
-
-import com.android.server.nearby.common.eventloop.EventLoop;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.Module;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
-
-import java.time.Clock;
-import java.time.Instant;
-import java.time.ZoneId;
-
-/**
- * Module that associates all of the fast pair related singleton class
- */
-public class FastPairModule extends Module {
-    /**
-     * Initiate the class that needs to be singleton.
-     */
-    @Override
-    public void configure(Context context, Class<?> type, Locator locator) {
-        if (type.equals(FastPairCacheManager.class)) {
-            locator.bind(FastPairCacheManager.class, new FastPairCacheManager(context));
-        } else if (type.equals(FootprintsDeviceManager.class)) {
-            locator.bind(FootprintsDeviceManager.class, new FootprintsDeviceManager());
-        } else if (type.equals(EventLoop.class)) {
-            locator.bind(EventLoop.class, EventLoop.newInstance("NearbyFastPair"));
-        } else if (type.equals(FastPairController.class)) {
-            locator.bind(FastPairController.class, new FastPairController(context));
-        } else if (type.equals(FastPairCacheManager.class)) {
-            locator.bind(FastPairCacheManager.class, new FastPairCacheManager(context));
-        } else if (type.equals(FastPairHalfSheetManager.class)) {
-            locator.bind(FastPairHalfSheetManager.class, new FastPairHalfSheetManager(context));
-        } else if (type.equals(FastPairAdvHandler.class)) {
-            locator.bind(FastPairAdvHandler.class, new FastPairAdvHandler(context));
-        } else if (type.equals(FastPairNotificationManager.class)) {
-            locator.bind(FastPairNotificationManager.class,
-                    new FastPairNotificationManager(context));
-        } else if (type.equals(Clock.class)) {
-            locator.bind(Clock.class, new Clock() {
-                @Override
-                public ZoneId getZone() {
-                    return null;
-                }
-
-                @Override
-                public Clock withZone(ZoneId zone) {
-                    return null;
-                }
-
-                @Override
-                public Instant instant() {
-                    return null;
-                }
-            });
-        }
-
-    }
-
-    /**
-     * Clean up the singleton classes.
-     */
-    @Override
-    public void destroy(Context context, Class<?> type, Object instance) {
-        super.destroy(context, type, instance);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FlagUtils.java b/nearby/service/java/com/android/server/nearby/fastpair/FlagUtils.java
deleted file mode 100644
index 883a1f8..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/FlagUtils.java
+++ /dev/null
@@ -1,202 +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.server.nearby.fastpair;
-
-import android.text.TextUtils;
-
-import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
-
-import com.google.common.base.Splitter;
-import com.google.common.collect.ImmutableSet;
-
-/**
- * This is fast pair connection preference
- */
-public class FlagUtils {
-    private static final int GATT_OPERATION_TIME_OUT_SECOND = 10;
-    private static final int GATT_CONNECTION_TIME_OUT_SECOND = 15;
-    private static final int BLUETOOTH_TOGGLE_TIME_OUT_SECOND = 10;
-    private static final int BLUETOOTH_TOGGLE_SLEEP_TIME_OUT_SECOND = 2;
-    private static final int CLASSIC_DISCOVERY_TIME_OUT_SECOND = 13;
-    private static final int NUM_DISCOVER_ATTEMPTS = 3;
-    private static final int DISCOVERY_RETRY_SLEEP_SECONDS = 1;
-    private static final int SDP_TIME_OUT_SECONDS = 10;
-    private static final int NUM_SDP_ATTEMPTS = 0;
-    private static final int NUM_CREATED_BOND_ATTEMPTS = 3;
-    private static final int NUM_CONNECT_ATTEMPT = 2;
-    private static final int NUM_WRITE_ACCOUNT_KEY_ATTEMPT = 3;
-    private static final boolean TOGGLE_BLUETOOTH_ON_FAILURE = false;
-    private static final boolean BLUETOOTH_STATE_POOLING = true;
-    private static final int BLUETOOTH_STATE_POOLING_MILLIS = 1000;
-    private static final int NUM_ATTEMPTS = 2;
-    private static final short BREDR_HANDOVER_DATA_CHARACTERISTIC_ID = 11265; // 0x2c01
-    private static final short BLUETOOTH_SIG_DATA_CHARACTERISTIC_ID = 11266; // 0x2c02
-    private static final short TRANSPORT_BLOCK_DATA_CHARACTERISTIC_ID = 11267; // 0x2c03
-    private static final boolean WAIT_FOR_UUID_AFTER_BONDING = true;
-    private static final boolean RECEIVE_UUID_AND_BONDED_EVENT_BEFORE_CLOSE = true;
-    private static final int REMOVE_BOND_TIME_OUT_SECONDS = 5;
-    private static final int REMOVE_BOND_SLEEP_MILLIS = 1000;
-    private static final int CREATE_BOND_TIME_OUT_SECONDS = 15;
-    private static final int HIDE_CREATED_BOND_TIME_OUT_SECONDS = 40;
-    private static final int PROXY_TIME_OUT_SECONDS = 2;
-    private static final boolean REJECT_ACCESS = false;
-    private static final boolean ACCEPT_PASSKEY = true;
-    private static final int WRITE_ACCOUNT_KEY_SLEEP_MILLIS = 2000;
-    private static final boolean PROVIDER_INITIATE_BONDING = false;
-    private static final boolean SPECIFY_CREATE_BOND_TRANSPORT_TYPE = false;
-    private static final int CREATE_BOND_TRANSPORT_TYPE = 0;
-    private static final boolean KEEP_SAME_ACCOUNT_KEY_WRITE = true;
-    private static final boolean ENABLE_NAMING_CHARACTERISTIC = true;
-    private static final boolean CHECK_FIRMWARE_VERSION = true;
-    private static final int SDP_ATTEMPTS_AFTER_BONDED = 1;
-    private static final boolean SUPPORT_HID = false;
-    private static final boolean ENABLE_PAIRING_WHILE_DIRECTLY_CONNECTING = true;
-    private static final boolean ACCEPT_CONSENT_FOR_FP_ONE = true;
-    private static final int GATT_CONNECT_RETRY_TIMEOUT_MILLIS = 18000;
-    private static final boolean ENABLE_128BIT_CUSTOM_GATT_CHARACTERISTIC = true;
-    private static final boolean ENABLE_SEND_EXCEPTION_STEP_TO_VALIDATOR = true;
-    private static final boolean ENABLE_ADDITIONAL_DATA_TYPE_WHEN_ACTION_OVER_BLE = true;
-    private static final boolean CHECK_BOND_STATE_WHEN_SKIP_CONNECTING_PROFILE = true;
-    private static final boolean MORE_LOG_FOR_QUALITY = true;
-    private static final boolean RETRY_GATT_CONNECTION_AND_SECRET_HANDSHAKE = true;
-    private static final int GATT_CONNECT_SHORT_TIMEOUT_MS = 7000;
-    private static final int GATT_CONNECTION_LONG_TIME_OUT_MS = 15000;
-    private static final int GATT_CONNECT_SHORT_TIMEOUT_RETRY_MAX_SPENT_TIME_MS = 1000;
-    private static final int ADDRESS_ROTATE_RETRY_MAX_SPENT_TIME_MS = 15000;
-    private static final int PAIRING_RETRY_DELAY_MS = 100;
-    private static final int HANDSHAKE_SHORT_TIMEOUT_MS = 3000;
-    private static final int HANDSHAKE_LONG_TIMEOUT_MS = 1000;
-    private static final int SECRET_HANDSHAKE_SHORT_TIMEOUT_RETRY_MAX_SPENT_TIME_MS = 5000;
-    private static final int SECRET_HANDSHAKE_LONG_TIMEOUT_RETRY_MAX_SPENT_TIME_MS = 7000;
-    private static final int SECRET_HANDSHAKE_RETRY_ATTEMPTS = 3;
-    private static final int SECRET_HANDSHAKE_RETRY_GATT_CONNECTION_MAX_SPENT_TIME_MS = 15000;
-    private static final int SIGNAL_LOST_RETRY_MAX_SPENT_TIME_MS = 15000;
-    private static final boolean RETRY_SECRET_HANDSHAKE_TIMEOUT = false;
-    private static final boolean LOG_USER_MANUAL_RETRY = true;
-    private static final boolean ENABLE_PAIR_FLOW_SHOW_UI_WITHOUT_PROFILE_CONNECTION = false;
-    private static final boolean LOG_USER_MANUAL_CITY = true;
-    private static final boolean LOG_PAIR_WITH_CACHED_MODEL_ID = true;
-    private static final boolean DIRECT_CONNECT_PROFILE_IF_MODEL_ID_IN_CACHE = false;
-
-    public static Preferences.Builder getPreferencesBuilder() {
-        return Preferences.builder()
-                .setGattOperationTimeoutSeconds(GATT_OPERATION_TIME_OUT_SECOND)
-                .setGattConnectionTimeoutSeconds(GATT_CONNECTION_TIME_OUT_SECOND)
-                .setBluetoothToggleTimeoutSeconds(BLUETOOTH_TOGGLE_TIME_OUT_SECOND)
-                .setBluetoothToggleSleepSeconds(BLUETOOTH_TOGGLE_SLEEP_TIME_OUT_SECOND)
-                .setClassicDiscoveryTimeoutSeconds(CLASSIC_DISCOVERY_TIME_OUT_SECOND)
-                .setNumDiscoverAttempts(NUM_DISCOVER_ATTEMPTS)
-                .setDiscoveryRetrySleepSeconds(DISCOVERY_RETRY_SLEEP_SECONDS)
-                .setSdpTimeoutSeconds(SDP_TIME_OUT_SECONDS)
-                .setNumSdpAttempts(NUM_SDP_ATTEMPTS)
-                .setNumCreateBondAttempts(NUM_CREATED_BOND_ATTEMPTS)
-                .setNumConnectAttempts(NUM_CONNECT_ATTEMPT)
-                .setNumWriteAccountKeyAttempts(NUM_WRITE_ACCOUNT_KEY_ATTEMPT)
-                .setToggleBluetoothOnFailure(TOGGLE_BLUETOOTH_ON_FAILURE)
-                .setBluetoothStateUsesPolling(BLUETOOTH_STATE_POOLING)
-                .setBluetoothStatePollingMillis(BLUETOOTH_STATE_POOLING_MILLIS)
-                .setNumAttempts(NUM_ATTEMPTS)
-                .setBrHandoverDataCharacteristicId(BREDR_HANDOVER_DATA_CHARACTERISTIC_ID)
-                .setBluetoothSigDataCharacteristicId(BLUETOOTH_SIG_DATA_CHARACTERISTIC_ID)
-                .setBrTransportBlockDataDescriptorId(TRANSPORT_BLOCK_DATA_CHARACTERISTIC_ID)
-                .setWaitForUuidsAfterBonding(WAIT_FOR_UUID_AFTER_BONDING)
-                .setReceiveUuidsAndBondedEventBeforeClose(
-                        RECEIVE_UUID_AND_BONDED_EVENT_BEFORE_CLOSE)
-                .setRemoveBondTimeoutSeconds(REMOVE_BOND_TIME_OUT_SECONDS)
-                .setRemoveBondSleepMillis(REMOVE_BOND_SLEEP_MILLIS)
-                .setCreateBondTimeoutSeconds(CREATE_BOND_TIME_OUT_SECONDS)
-                .setHidCreateBondTimeoutSeconds(HIDE_CREATED_BOND_TIME_OUT_SECONDS)
-                .setProxyTimeoutSeconds(PROXY_TIME_OUT_SECONDS)
-                .setRejectPhonebookAccess(REJECT_ACCESS)
-                .setRejectMessageAccess(REJECT_ACCESS)
-                .setRejectSimAccess(REJECT_ACCESS)
-                .setAcceptPasskey(ACCEPT_PASSKEY)
-                .setWriteAccountKeySleepMillis(WRITE_ACCOUNT_KEY_SLEEP_MILLIS)
-                .setProviderInitiatesBondingIfSupported(PROVIDER_INITIATE_BONDING)
-                .setAttemptDirectConnectionWhenPreviouslyBonded(true)
-                .setAutomaticallyReconnectGattWhenNeeded(true)
-                .setSkipDisconnectingGattBeforeWritingAccountKey(true)
-                .setIgnoreUuidTimeoutAfterBonded(true)
-                .setSpecifyCreateBondTransportType(SPECIFY_CREATE_BOND_TRANSPORT_TYPE)
-                .setCreateBondTransportType(CREATE_BOND_TRANSPORT_TYPE)
-                .setIncreaseIntentFilterPriority(true)
-                .setEvaluatePerformance(false)
-                .setKeepSameAccountKeyWrite(KEEP_SAME_ACCOUNT_KEY_WRITE)
-                .setEnableNamingCharacteristic(ENABLE_NAMING_CHARACTERISTIC)
-                .setEnableFirmwareVersionCharacteristic(CHECK_FIRMWARE_VERSION)
-                .setNumSdpAttemptsAfterBonded(SDP_ATTEMPTS_AFTER_BONDED)
-                .setSupportHidDevice(SUPPORT_HID)
-                .setEnablePairingWhileDirectlyConnecting(
-                        ENABLE_PAIRING_WHILE_DIRECTLY_CONNECTING)
-                .setAcceptConsentForFastPairOne(ACCEPT_CONSENT_FOR_FP_ONE)
-                .setGattConnectRetryTimeoutMillis(GATT_CONNECT_RETRY_TIMEOUT_MILLIS)
-                .setEnable128BitCustomGattCharacteristicsId(
-                        ENABLE_128BIT_CUSTOM_GATT_CHARACTERISTIC)
-                .setEnableSendExceptionStepToValidator(ENABLE_SEND_EXCEPTION_STEP_TO_VALIDATOR)
-                .setEnableAdditionalDataTypeWhenActionOverBle(
-                        ENABLE_ADDITIONAL_DATA_TYPE_WHEN_ACTION_OVER_BLE)
-                .setCheckBondStateWhenSkipConnectingProfiles(
-                        CHECK_BOND_STATE_WHEN_SKIP_CONNECTING_PROFILE)
-                .setMoreEventLogForQuality(MORE_LOG_FOR_QUALITY)
-                .setRetryGattConnectionAndSecretHandshake(
-                        RETRY_GATT_CONNECTION_AND_SECRET_HANDSHAKE)
-                .setGattConnectShortTimeoutMs(GATT_CONNECT_SHORT_TIMEOUT_MS)
-                .setGattConnectLongTimeoutMs(GATT_CONNECTION_LONG_TIME_OUT_MS)
-                .setGattConnectShortTimeoutRetryMaxSpentTimeMs(
-                        GATT_CONNECT_SHORT_TIMEOUT_RETRY_MAX_SPENT_TIME_MS)
-                .setAddressRotateRetryMaxSpentTimeMs(ADDRESS_ROTATE_RETRY_MAX_SPENT_TIME_MS)
-                .setPairingRetryDelayMs(PAIRING_RETRY_DELAY_MS)
-                .setSecretHandshakeShortTimeoutMs(HANDSHAKE_SHORT_TIMEOUT_MS)
-                .setSecretHandshakeLongTimeoutMs(HANDSHAKE_LONG_TIMEOUT_MS)
-                .setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(
-                        SECRET_HANDSHAKE_SHORT_TIMEOUT_RETRY_MAX_SPENT_TIME_MS)
-                .setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(
-                        SECRET_HANDSHAKE_LONG_TIMEOUT_RETRY_MAX_SPENT_TIME_MS)
-                .setSecretHandshakeRetryAttempts(SECRET_HANDSHAKE_RETRY_ATTEMPTS)
-                .setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(
-                        SECRET_HANDSHAKE_RETRY_GATT_CONNECTION_MAX_SPENT_TIME_MS)
-                .setSignalLostRetryMaxSpentTimeMs(SIGNAL_LOST_RETRY_MAX_SPENT_TIME_MS)
-                .setGattConnectionAndSecretHandshakeNoRetryGattError(
-                        getGattConnectionAndSecretHandshakeNoRetryGattError())
-                .setRetrySecretHandshakeTimeout(RETRY_SECRET_HANDSHAKE_TIMEOUT)
-                .setLogUserManualRetry(LOG_USER_MANUAL_RETRY)
-                .setEnablePairFlowShowUiWithoutProfileConnection(
-                        ENABLE_PAIR_FLOW_SHOW_UI_WITHOUT_PROFILE_CONNECTION)
-                .setLogUserManualRetry(LOG_USER_MANUAL_CITY)
-                .setLogPairWithCachedModelId(LOG_PAIR_WITH_CACHED_MODEL_ID)
-                .setDirectConnectProfileIfModelIdInCache(
-                        DIRECT_CONNECT_PROFILE_IF_MODEL_ID_IN_CACHE);
-    }
-
-    private static ImmutableSet<Integer> getGattConnectionAndSecretHandshakeNoRetryGattError() {
-        ImmutableSet.Builder<Integer> noRetryGattErrorsBuilder = ImmutableSet.builder();
-        // When GATT connection fail we will not retry on error code 257
-        for (String errorCode :
-                Splitter.on(",").split("257,")) {
-            if (!TextUtils.isDigitsOnly(errorCode)) {
-                continue;
-            }
-
-            try {
-                noRetryGattErrorsBuilder.add(Integer.parseInt(errorCode));
-            } catch (NumberFormatException e) {
-                // Ignore
-            }
-        }
-        return noRetryGattErrorsBuilder.build();
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/HalfSheetResources.java b/nearby/service/java/com/android/server/nearby/fastpair/HalfSheetResources.java
deleted file mode 100644
index 86dd44d..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/HalfSheetResources.java
+++ /dev/null
@@ -1,129 +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.server.nearby.fastpair;
-
-import static com.android.server.nearby.fastpair.Constant.TAG;
-
-import android.annotation.ColorInt;
-import android.annotation.ColorRes;
-import android.annotation.DrawableRes;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.StringRes;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-/**
- * Utility to obtain the {@link com.android.nearby.halfsheet} {@link Resources}, in the
- * HalfSheetUX APK.
- * @hide
- */
-public class HalfSheetResources {
-    @NonNull
-    private final Context mContext;
-
-    @Nullable
-    private Context mResourcesContext = null;
-
-    @Nullable
-    private static Context sTestResourcesContext = null;
-
-    public HalfSheetResources(Context context) {
-        mContext = context;
-    }
-
-    /**
-     * Convenience method to mock all resources for the duration of a test.
-     *
-     * Call with a null context to reset after the test.
-     */
-    @VisibleForTesting
-    public static void setResourcesContextForTest(@Nullable Context testContext) {
-        sTestResourcesContext = testContext;
-    }
-
-    /**
-     * Get the {@link Context} of the resources package.
-     */
-    @Nullable
-    public synchronized Context getResourcesContext() {
-        if (sTestResourcesContext != null) {
-            return sTestResourcesContext;
-        }
-
-        if (mResourcesContext != null) {
-            return mResourcesContext;
-        }
-
-        String packageName = PackageUtils.getHalfSheetApkPkgName(mContext);
-        if (packageName == null) {
-            Log.e(TAG, "Resolved package not found");
-            return null;
-        }
-        final Context pkgContext;
-        try {
-            pkgContext = mContext.createPackageContext(packageName, 0 /* flags */);
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.e(TAG, "Resolved package not found");
-            return null;
-        }
-
-        mResourcesContext = pkgContext;
-        return pkgContext;
-    }
-
-    /**
-     * Get the {@link Resources} of the ServiceConnectivityResources APK.
-     */
-    public Resources get() {
-        return getResourcesContext().getResources();
-    }
-
-    /**
-     * Gets the {@code String} with given resource Id.
-     */
-    public String getString(@StringRes int id) {
-        return get().getString(id);
-    }
-
-    /**
-     * Gets the {@code String} with given resource Id and formatted arguments.
-     */
-    public String getString(@StringRes int id, Object... formatArgs) {
-        return get().getString(id, formatArgs);
-    }
-
-    /**
-     * Gets the {@link Drawable} with given resource Id.
-     */
-    public Drawable getDrawable(@DrawableRes int id) {
-        return get().getDrawable(id, getResourcesContext().getTheme());
-    }
-
-    /**
-     * Gets a themed color integer associated with a particular resource ID.
-     */
-    @ColorInt
-    public int getColor(@ColorRes int id) {
-        return get().getColor(id, getResourcesContext().getTheme());
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/PackageUtils.java b/nearby/service/java/com/android/server/nearby/fastpair/PackageUtils.java
deleted file mode 100644
index 0ff8caf..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/PackageUtils.java
+++ /dev/null
@@ -1,72 +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.server.nearby.fastpair;
-
-import static com.android.nearby.halfsheet.constants.Constant.ACTION_RESOURCES_APK;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.util.Log;
-
-import com.android.server.nearby.util.Environment;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * Helper class for package related methods.
- */
-public class PackageUtils {
-
-    /**
-     * Gets the package name of HalfSheet.apk
-     */
-    @Nullable
-    public static String getHalfSheetApkPkgName(Context context) {
-        List<ResolveInfo> resolveInfos = context
-                .getPackageManager().queryIntentActivities(
-                        new Intent(ACTION_RESOURCES_APK),
-                        PackageManager.MATCH_SYSTEM_ONLY);
-
-        // remove apps that don't live in the nearby apex
-        resolveInfos.removeIf(info ->
-                !Environment.isAppInNearbyApex(info.activityInfo.applicationInfo));
-
-        if (resolveInfos.isEmpty()) {
-            // Resource APK not loaded yet, print a stack trace to see where this is called from
-            Log.e("FastPairManager", "Attempted to fetch resources before halfsheet "
-                            + " APK is installed or package manager can't resolve correctly!",
-                    new IllegalStateException());
-            return null;
-        }
-
-        if (resolveInfos.size() > 1) {
-            // multiple apps found, log a warning, but continue
-            Log.w("FastPairManager", "Found > 1 APK that can resolve halfsheet APK intent: "
-                    + resolveInfos.stream()
-                    .map(info -> info.activityInfo.applicationInfo.packageName)
-                    .collect(Collectors.joining(", ")));
-        }
-
-        // Assume the first ResolveInfo is the one we're looking for
-        ResolveInfo info = resolveInfos.get(0);
-        return info.activityInfo.applicationInfo.packageName;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/UserActionHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/UserActionHandler.java
deleted file mode 100644
index 2b00ca5..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/UserActionHandler.java
+++ /dev/null
@@ -1,31 +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.server.nearby.fastpair;
-
-import com.android.nearby.halfsheet.constants.UserActionHandlerBase;
-
-/**
- * User action handler class.
- */
-public class UserActionHandler extends UserActionHandlerBase {
-
-    public static final String EXTRA_ITEM_ID = PREFIX + "EXTRA_DISCOVERY_ITEM";
-    public static final String EXTRA_FAST_PAIR_SECRET = PREFIX + "EXTRA_FAST_PAIR_SECRET";
-
-    public static final String EXTRA_PRIVATE_BLE_ADDRESS =
-            ACTION_PREFIX + "EXTRA_PRIVATE_BLE_ADDRESS";
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/blocklist/Blocklist.java b/nearby/service/java/com/android/server/nearby/fastpair/blocklist/Blocklist.java
deleted file mode 100644
index d8091ba..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/blocklist/Blocklist.java
+++ /dev/null
@@ -1,66 +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.server.nearby.fastpair.blocklist;
-
-
-/**
- * Skeletal implementation of Blocklist
- *
- * <p>Controls the frequency to show the available device to users.
- */
-public interface Blocklist {
-
-    /** Checks certain item is blocked within durationSeconds. */
-    boolean isBlocklisted(int id, int durationSeconds);
-
-    /** Updates the HalfSheet blocklist state for a given id. */
-    boolean updateState(int id, BlocklistState state);
-
-    /** Removes the HalfSheet blocklist. */
-    boolean removeBlocklist(int id);
-
-    /** Resets certain device ban state to active. */
-    void resetBlockState(int id);
-
-    /**
-     * Used for indicate what state is the blocklist item.
-     *
-     * <p>The different states have differing priorities and higher priority states will override
-     * lower one.
-     * More details and state transition diagram,
-     * see: https://docs.google.com/document/d/1wzE5CHXTkzKJY-2AltSrxOVteom2Nebc1sbjw1Tt7BQ/edit?usp=sharing&resourcekey=0-L-wUz3Hw5gZPThm5VPwHOQ
-     */
-    enum BlocklistState {
-        UNKNOWN(0),
-        ACTIVE(1),
-        DISMISSED(2),
-        PAIRING(3),
-        PAIRED(4),
-        DO_NOT_SHOW_AGAIN(5),
-        DO_NOT_SHOW_AGAIN_LONG(6);
-
-        private final int mValue;
-
-        BlocklistState(final int value) {
-            this.mValue = value;
-        }
-
-        public boolean hasHigherPriorityThan(BlocklistState otherState) {
-            return this.mValue > otherState.mValue;
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/blocklist/BlocklistElement.java b/nearby/service/java/com/android/server/nearby/fastpair/blocklist/BlocklistElement.java
deleted file mode 100644
index d058d58..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/blocklist/BlocklistElement.java
+++ /dev/null
@@ -1,39 +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.server.nearby.fastpair.blocklist;
-
-import com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetBlocklist;
-
-/** Element in the {@link FastPairHalfSheetBlocklist} */
-public class BlocklistElement {
-    private final long mTimeStamp;
-    private final BlocklistState mState;
-
-    public BlocklistElement(BlocklistState state, long timeStamp) {
-        this.mState = state;
-        this.mTimeStamp = timeStamp;
-    }
-
-    public Long getTimeStamp() {
-        return mTimeStamp;
-    }
-
-    public BlocklistState getState() {
-        return mState;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
deleted file mode 100644
index 5ce4488..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
+++ /dev/null
@@ -1,463 +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.server.nearby.fastpair.cache;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
-import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_FAST_PAIR_SECRET;
-
-import android.annotation.IntDef;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.common.ble.util.RangingUtils;
-import com.android.server.nearby.common.fastpair.IconUtils;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.net.URISyntaxException;
-import java.time.Clock;
-import java.util.Objects;
-
-import service.proto.Cache;
-
-/**
- * Wrapper class around StoredDiscoveryItem. A centralized place for methods related to
- * updating/parsing StoredDiscoveryItem.
- */
-public class DiscoveryItem implements Comparable<DiscoveryItem> {
-
-    private static final String ACTION_FAST_PAIR =
-            "com.android.server.nearby:ACTION_FAST_PAIR";
-    private static final int BEACON_STALENESS_MILLIS = 120000;
-    private static final int ITEM_EXPIRATION_MILLIS = 20000;
-    private static final int APP_INSTALL_EXPIRATION_MILLIS = 600000;
-    private static final int ITEM_DELETABLE_MILLIS = 15000;
-
-    private final FastPairCacheManager mFastPairCacheManager;
-    private final Clock mClock;
-
-    private Cache.StoredDiscoveryItem mStoredDiscoveryItem;
-
-    /** IntDef for StoredDiscoveryItem.State */
-    @IntDef({
-            Cache.StoredDiscoveryItem.State.STATE_ENABLED_VALUE,
-            Cache.StoredDiscoveryItem.State.STATE_MUTED_VALUE,
-            Cache.StoredDiscoveryItem.State.STATE_DISABLED_BY_SYSTEM_VALUE
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface ItemState {
-    }
-
-    public DiscoveryItem(LocatorContextWrapper locatorContextWrapper,
-            Cache.StoredDiscoveryItem mStoredDiscoveryItem) {
-        this.mFastPairCacheManager =
-                locatorContextWrapper.getLocator().get(FastPairCacheManager.class);
-        this.mClock =
-                locatorContextWrapper.getLocator().get(Clock.class);
-        this.mStoredDiscoveryItem = mStoredDiscoveryItem;
-    }
-
-    public DiscoveryItem(Context context, Cache.StoredDiscoveryItem mStoredDiscoveryItem) {
-        this.mFastPairCacheManager = Locator.get(context, FastPairCacheManager.class);
-        this.mClock = Locator.get(context, Clock.class);
-        this.mStoredDiscoveryItem = mStoredDiscoveryItem;
-    }
-
-    /** @return A new StoredDiscoveryItem with state fields set to their defaults. */
-    public static Cache.StoredDiscoveryItem newStoredDiscoveryItem() {
-        Cache.StoredDiscoveryItem.Builder storedDiscoveryItem =
-                Cache.StoredDiscoveryItem.newBuilder();
-        storedDiscoveryItem.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
-        return storedDiscoveryItem.build();
-    }
-
-    /**
-     * Checks if store discovery item support fast pair or not.
-     */
-    public boolean isFastPair() {
-        Intent intent = parseIntentScheme(mStoredDiscoveryItem.getActionUrl());
-        if (intent == null) {
-            Log.w("FastPairDiscovery", "FastPair: fail to parse action url"
-                    + mStoredDiscoveryItem.getActionUrl());
-            return false;
-        }
-        return ACTION_FAST_PAIR.equals(intent.getAction());
-    }
-
-    /**
-     * Checks if the item is expired. Expired items are those over getItemExpirationMillis() eg. 2
-     * minutes
-     */
-    public static boolean isExpired(
-            long currentTimestampMillis, @Nullable Long lastObservationTimestampMillis) {
-        if (lastObservationTimestampMillis == null) {
-            return true;
-        }
-        return (currentTimestampMillis - lastObservationTimestampMillis)
-                >= ITEM_EXPIRATION_MILLIS;
-    }
-
-    /**
-     * Checks if the item is deletable for saving disk space. Deletable items are those over
-     * getItemDeletableMillis eg. over 25 hrs.
-     */
-    public static boolean isDeletable(
-            long currentTimestampMillis, @Nullable Long lastObservationTimestampMillis) {
-        if (lastObservationTimestampMillis == null) {
-            return true;
-        }
-        return currentTimestampMillis - lastObservationTimestampMillis
-                >= ITEM_DELETABLE_MILLIS;
-    }
-
-    /** Checks if the item has a pending app install */
-    public boolean isPendingAppInstallValid() {
-        return isPendingAppInstallValid(mClock.millis());
-    }
-
-    /**
-     * Checks if pending app valid.
-     */
-    public boolean isPendingAppInstallValid(long appInstallMillis) {
-        return isPendingAppInstallValid(appInstallMillis, mStoredDiscoveryItem);
-    }
-
-    /**
-     * Checks if the app install time expired.
-     */
-    public static boolean isPendingAppInstallValid(
-            long currentMillis, Cache.StoredDiscoveryItem storedItem) {
-        return currentMillis - storedItem.getPendingAppInstallTimestampMillis()
-                < APP_INSTALL_EXPIRATION_MILLIS;
-    }
-
-
-    /** Checks if the item has enough data to be shown */
-    public boolean isReadyForDisplay() {
-        boolean hasUrlOrPopularApp = !mStoredDiscoveryItem.getActionUrl().isEmpty();
-
-        return !TextUtils.isEmpty(mStoredDiscoveryItem.getTitle()) && hasUrlOrPopularApp;
-    }
-
-    /** Checks if the action url is app install */
-    public boolean isApp() {
-        return mStoredDiscoveryItem.getActionUrlType() == Cache.ResolvedUrlType.APP;
-    }
-
-    /** Returns true if an item is muted, or if state is unavailable. */
-    public boolean isMuted() {
-        return mStoredDiscoveryItem.getState() != Cache.StoredDiscoveryItem.State.STATE_ENABLED;
-    }
-
-    /**
-     * Returns the state of store discovery item.
-     */
-    public Cache.StoredDiscoveryItem.State getState() {
-        return mStoredDiscoveryItem.getState();
-    }
-
-    /** Checks if it's device item. e.g. Chromecast / Wear */
-    public static boolean isDeviceType(Cache.NearbyType type) {
-        return type == Cache.NearbyType.NEARBY_CHROMECAST
-                || type == Cache.NearbyType.NEARBY_WEAR
-                || type == Cache.NearbyType.NEARBY_DEVICE;
-    }
-
-    /**
-     * Check if the type is supported.
-     */
-    public static boolean isTypeEnabled(Cache.NearbyType type) {
-        switch (type) {
-            case NEARBY_WEAR:
-            case NEARBY_CHROMECAST:
-            case NEARBY_DEVICE:
-                return true;
-            default:
-                Log.e("FastPairDiscoveryItem", "Invalid item type " + type.name());
-                return false;
-        }
-    }
-
-    /** Gets hash code of UI related data so we can collapse identical items. */
-    public int getUiHashCode() {
-        return Objects.hash(
-                        mStoredDiscoveryItem.getTitle(),
-                        mStoredDiscoveryItem.getDescription(),
-                        mStoredDiscoveryItem.getAppName(),
-                        mStoredDiscoveryItem.getDisplayUrl(),
-                        mStoredDiscoveryItem.getMacAddress());
-    }
-
-    // Getters below
-
-    /**
-     * Returns the id of store discovery item.
-     */
-    @Nullable
-    public String getId() {
-        return mStoredDiscoveryItem.getId();
-    }
-
-    /**
-     * Returns the title of discovery item.
-     */
-    @Nullable
-    public String getTitle() {
-        return mStoredDiscoveryItem.getTitle();
-    }
-
-    /**
-     * Returns the description of discovery item.
-     */
-    @Nullable
-    public String getDescription() {
-        return mStoredDiscoveryItem.getDescription();
-    }
-
-    /**
-     * Returns the mac address of discovery item.
-     */
-    @Nullable
-    public String getMacAddress() {
-        return mStoredDiscoveryItem.getMacAddress();
-    }
-
-    /**
-     * Returns the display url of discovery item.
-     */
-    @Nullable
-    public String getDisplayUrl() {
-        return mStoredDiscoveryItem.getDisplayUrl();
-    }
-
-    /**
-     * Returns the public key of discovery item.
-     */
-    @Nullable
-    public byte[] getAuthenticationPublicKeySecp256R1() {
-        return mStoredDiscoveryItem.getAuthenticationPublicKeySecp256R1().toByteArray();
-    }
-
-    /**
-     * Returns the pairing secret.
-     */
-    @Nullable
-    public String getFastPairSecretKey() {
-        Intent intent = parseIntentScheme(mStoredDiscoveryItem.getActionUrl());
-        if (intent == null) {
-            Log.d("FastPairDiscoveryItem", "FastPair: fail to parse action url "
-                    + mStoredDiscoveryItem.getActionUrl());
-            return null;
-        }
-        return intent.getStringExtra(EXTRA_FAST_PAIR_SECRET);
-    }
-
-    /**
-     * Returns the fast pair info of discovery item.
-     */
-    @Nullable
-    public Cache.FastPairInformation getFastPairInformation() {
-        return mStoredDiscoveryItem.hasFastPairInformation()
-                ? mStoredDiscoveryItem.getFastPairInformation() : null;
-    }
-
-    /**
-     * Returns the app name of discovery item.
-     */
-    @Nullable
-    @VisibleForTesting
-    protected String getAppName() {
-        return mStoredDiscoveryItem.getAppName();
-    }
-
-    /**
-     * Returns the package name of discovery item.
-     */
-    @Nullable
-    public String getAppPackageName() {
-        return mStoredDiscoveryItem.getPackageName();
-    }
-
-    /**
-     * Returns the action url of discovery item.
-     */
-    @Nullable
-    public String getActionUrl() {
-        return mStoredDiscoveryItem.getActionUrl();
-    }
-
-    /**
-     * Returns the rssi value of discovery item.
-     */
-    @Nullable
-    public Integer getRssi() {
-        return mStoredDiscoveryItem.getRssi();
-    }
-
-    /**
-     * Returns the TX power of discovery item.
-     */
-    @Nullable
-    public Integer getTxPower() {
-        return mStoredDiscoveryItem.getTxPower();
-    }
-
-    /**
-     * Returns the first observed time stamp of discovery item.
-     */
-    @Nullable
-    public Long getFirstObservationTimestampMillis() {
-        return mStoredDiscoveryItem.getFirstObservationTimestampMillis();
-    }
-
-    /**
-     * Returns the last observed time stamp of discovery item.
-     */
-    @Nullable
-    public Long getLastObservationTimestampMillis() {
-        return mStoredDiscoveryItem.getLastObservationTimestampMillis();
-    }
-
-    /**
-     * Calculates an estimated distance for the item, computed from the TX power (at 1m) and RSSI.
-     *
-     * @return estimated distance, or null if there is no RSSI or no TX power.
-     */
-    @Nullable
-    public Double getEstimatedDistance() {
-        // In the future, we may want to do a foreground subscription to leverage onDistanceChanged.
-        return RangingUtils.distanceFromRssiAndTxPower(mStoredDiscoveryItem.getRssi(),
-                mStoredDiscoveryItem.getTxPower());
-    }
-
-    /**
-     * Gets icon Bitmap from icon store.
-     *
-     * @return null if no icon or icon size is incorrect.
-     */
-    @Nullable
-    public Bitmap getIcon() {
-        Bitmap icon =
-                BitmapFactory.decodeByteArray(
-                        mStoredDiscoveryItem.getIconPng().toByteArray(),
-                        0 /* offset */, mStoredDiscoveryItem.getIconPng().size());
-        if (IconUtils.isIconSizeCorrect(icon)) {
-            return icon;
-        } else {
-            return null;
-        }
-    }
-
-    /** Gets a FIFE URL of the icon. */
-    @Nullable
-    public String getIconFifeUrl() {
-        return mStoredDiscoveryItem.getIconFifeUrl();
-    }
-
-    /**
-     * Compares this object to the specified object: 1. By device type. Device setups are 'greater
-     * than' beacons. 2. By relevance. More relevant items are 'greater than' less relevant items.
-     * 3.By distance. Nearer items are 'greater than' further items.
-     *
-     * <p>In the list view, we sort in descending order, i.e. we put the most relevant items first.
-     */
-    @Override
-    public int compareTo(DiscoveryItem another) {
-        // For items of the same relevance, compare distance.
-        Double distance1 = getEstimatedDistance();
-        Double distance2 = another.getEstimatedDistance();
-        distance1 = distance1 != null ? distance1 : Double.MAX_VALUE;
-        distance2 = distance2 != null ? distance2 : Double.MAX_VALUE;
-        // Negate because closer items are better ("greater than") further items.
-        return -distance1.compareTo(distance2);
-    }
-
-    @Nullable
-    public String getTriggerId() {
-        return mStoredDiscoveryItem.getTriggerId();
-    }
-
-    @Override
-    public boolean equals(Object another) {
-        if (another instanceof DiscoveryItem) {
-            return ((DiscoveryItem) another).mStoredDiscoveryItem.equals(mStoredDiscoveryItem);
-        }
-        return false;
-    }
-
-    @Override
-    public int hashCode() {
-        return mStoredDiscoveryItem.hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return String.format(
-                "[triggerId=%s], [id=%s], [title=%s], [url=%s], [ready=%s], [macAddress=%s]",
-                getTriggerId(),
-                getId(),
-                getTitle(),
-                getActionUrl(),
-                isReadyForDisplay(),
-                maskBluetoothAddress(getMacAddress()));
-    }
-
-    /**
-     * Gets a copy of the StoredDiscoveryItem proto backing this DiscoveryItem. Currently needed for
-     * Fast Pair 2.0: We store the item in the cloud associated with a user's account, to enable
-     * pairing with other devices owned by the user.
-     */
-    public Cache.StoredDiscoveryItem getCopyOfStoredItem() {
-        return mStoredDiscoveryItem;
-    }
-
-    /**
-     * Gets the StoredDiscoveryItem represented by this DiscoveryItem. This lets tests manipulate
-     * values that production code should not manipulate.
-     */
-
-    public Cache.StoredDiscoveryItem getStoredItemForTest() {
-        return mStoredDiscoveryItem;
-    }
-
-    /**
-     * Sets the StoredDiscoveryItem represented by this DiscoveryItem. This lets tests manipulate
-     * values that production code should not manipulate.
-     */
-    public void setStoredItemForTest(Cache.StoredDiscoveryItem s) {
-        mStoredDiscoveryItem = s;
-    }
-
-    /**
-     * Parse the intent from item url.
-     */
-    public static Intent parseIntentScheme(String uri) {
-        try {
-            return Intent.parseUri(uri, Intent.URI_INTENT_SCHEME);
-        } catch (URISyntaxException e) {
-            return null;
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItemContract.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItemContract.java
deleted file mode 100644
index 61ca3fd..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItemContract.java
+++ /dev/null
@@ -1,35 +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.server.nearby.fastpair.cache;
-
-import android.provider.BaseColumns;
-
-/**
- * Defines DiscoveryItem database schema.
- */
-public class DiscoveryItemContract {
-    private DiscoveryItemContract() {}
-
-    /**
-     * Discovery item entry related info.
-     */
-    public static class DiscoveryItemEntry implements BaseColumns {
-        public static final String TABLE_NAME = "SCAN_RESULT";
-        public static final String COLUMN_MODEL_ID = "MODEL_ID";
-        public static final String COLUMN_SCAN_BYTE = "SCAN_RESULT_BYTE";
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
deleted file mode 100644
index c6134f5..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
+++ /dev/null
@@ -1,272 +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.server.nearby.fastpair.cache;
-
-import android.bluetooth.le.ScanResult;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.util.Log;
-
-import com.android.server.nearby.common.eventloop.Annotations;
-
-import com.google.protobuf.InvalidProtocolBufferException;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import service.proto.Cache;
-import service.proto.Rpcs;
-
-
-/**
- * Save FastPair device info to database to avoid multiple requesting.
- */
-public class FastPairCacheManager {
-    private final Context mContext;
-    private final FastPairDbHelper mFastPairDbHelper;
-
-    public FastPairCacheManager(Context context) {
-        mContext = context;
-        mFastPairDbHelper = new FastPairDbHelper(context);
-    }
-
-    /**
-     * Clean up function to release db
-     */
-    public void cleanUp() {
-        mFastPairDbHelper.close();
-    }
-
-    /**
-     * Saves the response to the db
-     */
-    private void saveDevice() {
-    }
-
-    Cache.ServerResponseDbItem getDeviceFromScanResult(ScanResult scanResult) {
-        return Cache.ServerResponseDbItem.newBuilder().build();
-    }
-
-    /**
-     * Save discovery item into database. Discovery item is item that discovered through Ble before
-     * pairing success.
-     */
-    public boolean saveDiscoveryItem(DiscoveryItem item) {
-
-        SQLiteDatabase db = mFastPairDbHelper.getWritableDatabase();
-        ContentValues values = new ContentValues();
-        values.put(DiscoveryItemContract.DiscoveryItemEntry.COLUMN_MODEL_ID, item.getTriggerId());
-        values.put(DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE,
-                item.getCopyOfStoredItem().toByteArray());
-        db.insert(DiscoveryItemContract.DiscoveryItemEntry.TABLE_NAME, null, values);
-        return true;
-    }
-
-
-    @Annotations.EventThread
-    private Rpcs.GetObservedDeviceResponse getObservedDeviceInfo(ScanResult scanResult) {
-        return Rpcs.GetObservedDeviceResponse.getDefaultInstance();
-    }
-
-    /**
-     * Get discovery item from item id.
-     */
-    public DiscoveryItem getDiscoveryItem(String itemId) {
-        return new DiscoveryItem(mContext, getStoredDiscoveryItem(itemId));
-    }
-
-    /**
-     * Get discovery item from item id.
-     */
-    public Cache.StoredDiscoveryItem getStoredDiscoveryItem(String itemId) {
-        SQLiteDatabase db = mFastPairDbHelper.getReadableDatabase();
-        String[] projection = {
-                DiscoveryItemContract.DiscoveryItemEntry.COLUMN_MODEL_ID,
-                DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE
-        };
-        String selection = DiscoveryItemContract.DiscoveryItemEntry.COLUMN_MODEL_ID + " =? ";
-        String[] selectionArgs = {itemId};
-        Cursor cursor = db.query(
-                DiscoveryItemContract.DiscoveryItemEntry.TABLE_NAME,
-                projection,
-                selection,
-                selectionArgs,
-                null,
-                null,
-                null
-        );
-
-        if (cursor.moveToNext()) {
-            byte[] res = cursor.getBlob(cursor.getColumnIndexOrThrow(
-                    DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE));
-            try {
-                Cache.StoredDiscoveryItem item = Cache.StoredDiscoveryItem.parseFrom(res);
-                return item;
-            } catch (InvalidProtocolBufferException e) {
-                Log.e("FastPairCacheManager", "storediscovery has error");
-            }
-        }
-        cursor.close();
-        return Cache.StoredDiscoveryItem.getDefaultInstance();
-    }
-
-    /**
-     * Get all of the discovery item related info in the cache.
-     */
-    public List<Cache.StoredDiscoveryItem> getAllSavedStoreDiscoveryItem() {
-        List<Cache.StoredDiscoveryItem> storedDiscoveryItemList = new ArrayList<>();
-        SQLiteDatabase db = mFastPairDbHelper.getReadableDatabase();
-        String[] projection = {
-                DiscoveryItemContract.DiscoveryItemEntry.COLUMN_MODEL_ID,
-                DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE
-        };
-        Cursor cursor = db.query(
-                DiscoveryItemContract.DiscoveryItemEntry.TABLE_NAME,
-                projection,
-                null,
-                null,
-                null,
-                null,
-                null
-        );
-
-        while (cursor.moveToNext()) {
-            byte[] res = cursor.getBlob(cursor.getColumnIndexOrThrow(
-                    DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE));
-            try {
-                Cache.StoredDiscoveryItem item = Cache.StoredDiscoveryItem.parseFrom(res);
-                storedDiscoveryItemList.add(item);
-            } catch (InvalidProtocolBufferException e) {
-                Log.e("FastPairCacheManager", "storediscovery has error");
-            }
-
-        }
-        cursor.close();
-        return storedDiscoveryItemList;
-    }
-
-    /**
-     * Get scan result from local database use model id
-     */
-    public Cache.StoredScanResult getStoredScanResult(String modelId) {
-        return Cache.StoredScanResult.getDefaultInstance();
-    }
-
-    /**
-     * Gets the paired Fast Pair item that paired to the phone through mac address.
-     */
-    public Cache.StoredFastPairItem getStoredFastPairItemFromMacAddress(String macAddress) {
-        SQLiteDatabase db = mFastPairDbHelper.getReadableDatabase();
-        String[] projection = {
-                StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_ACCOUNT_KEY,
-                StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS,
-                StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_STORED_FAST_PAIR_BYTE
-        };
-        String selection =
-                StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS + " =? ";
-        String[] selectionArgs = {macAddress};
-        Cursor cursor = db.query(
-                StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME,
-                projection,
-                selection,
-                selectionArgs,
-                null,
-                null,
-                null
-        );
-
-        if (cursor.moveToNext()) {
-            byte[] res = cursor.getBlob(cursor.getColumnIndexOrThrow(
-                    StoredFastPairItemContract.StoredFastPairItemEntry
-                            .COLUMN_STORED_FAST_PAIR_BYTE));
-            try {
-                Cache.StoredFastPairItem item = Cache.StoredFastPairItem.parseFrom(res);
-                return item;
-            } catch (InvalidProtocolBufferException e) {
-                Log.e("FastPairCacheManager", "storediscovery has error");
-            }
-        }
-        cursor.close();
-        return Cache.StoredFastPairItem.getDefaultInstance();
-    }
-
-    /**
-     * Save paired fast pair item into the database.
-     */
-    public boolean putStoredFastPairItem(Cache.StoredFastPairItem storedFastPairItem) {
-        SQLiteDatabase db = mFastPairDbHelper.getWritableDatabase();
-        ContentValues values = new ContentValues();
-        values.put(StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS,
-                storedFastPairItem.getMacAddress());
-        values.put(StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_ACCOUNT_KEY,
-                storedFastPairItem.getAccountKey().toString());
-        values.put(StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_STORED_FAST_PAIR_BYTE,
-                storedFastPairItem.toByteArray());
-        db.insert(StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME, null, values);
-        return true;
-
-    }
-
-    /**
-     * Removes certain storedFastPairItem so that it can update timely.
-     */
-    public void removeStoredFastPairItem(String macAddress) {
-        SQLiteDatabase db = mFastPairDbHelper.getWritableDatabase();
-        int res = db.delete(StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME,
-                StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS + "=?",
-                new String[]{macAddress});
-
-    }
-
-    /**
-     * Get all of the store fast pair item related info in the cache.
-     */
-    public List<Cache.StoredFastPairItem> getAllSavedStoredFastPairItem() {
-        List<Cache.StoredFastPairItem> storedFastPairItemList = new ArrayList<>();
-        SQLiteDatabase db = mFastPairDbHelper.getReadableDatabase();
-        String[] projection = {
-                StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS,
-                StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_ACCOUNT_KEY,
-                StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_STORED_FAST_PAIR_BYTE
-        };
-        Cursor cursor = db.query(
-                StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME,
-                projection,
-                null,
-                null,
-                null,
-                null,
-                null
-        );
-
-        while (cursor.moveToNext()) {
-            byte[] res = cursor.getBlob(cursor.getColumnIndexOrThrow(StoredFastPairItemContract
-                    .StoredFastPairItemEntry.COLUMN_STORED_FAST_PAIR_BYTE));
-            try {
-                Cache.StoredFastPairItem item = Cache.StoredFastPairItem.parseFrom(res);
-                storedFastPairItemList.add(item);
-            } catch (InvalidProtocolBufferException e) {
-                Log.e("FastPairCacheManager", "storediscovery has error");
-            }
-
-        }
-        cursor.close();
-        return storedFastPairItemList;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairDbHelper.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairDbHelper.java
deleted file mode 100644
index d950d8d..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairDbHelper.java
+++ /dev/null
@@ -1,76 +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.server.nearby.fastpair.cache;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-
-/**
- * Fast Pair db helper handle all of the db actions related Fast Pair.
- */
-public class FastPairDbHelper extends SQLiteOpenHelper {
-
-    public static final int DATABASE_VERSION = 1;
-    public static final String DATABASE_NAME = "FastPair.db";
-    private static final String SQL_CREATE_DISCOVERY_ITEM_DB =
-            "CREATE TABLE IF NOT EXISTS " + DiscoveryItemContract.DiscoveryItemEntry.TABLE_NAME
-                    + " (" + DiscoveryItemContract.DiscoveryItemEntry._ID
-                    + "INTEGER PRIMARY KEY,"
-                    + DiscoveryItemContract.DiscoveryItemEntry.COLUMN_MODEL_ID
-                    + " TEXT," + DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE
-                    + " BLOB)";
-    private static final String SQL_DELETE_DISCOVERY_ITEM_DB =
-            "DROP TABLE IF EXISTS " + DiscoveryItemContract.DiscoveryItemEntry.TABLE_NAME;
-    private static final String SQL_CREATE_FAST_PAIR_ITEM_DB =
-            "CREATE TABLE IF NOT EXISTS "
-                    + StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME
-                    + " (" + StoredFastPairItemContract.StoredFastPairItemEntry._ID
-                    + "INTEGER PRIMARY KEY,"
-                    + StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS
-                    + " TEXT,"
-                    + StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_ACCOUNT_KEY
-                    + " TEXT,"
-                    + StoredFastPairItemContract
-                    .StoredFastPairItemEntry.COLUMN_STORED_FAST_PAIR_BYTE
-                    + " BLOB)";
-    private static final String SQL_DELETE_FAST_PAIR_ITEM_DB =
-            "DROP TABLE IF EXISTS " + StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME;
-
-    public FastPairDbHelper(Context context) {
-        super(context, DATABASE_NAME, null, DATABASE_VERSION);
-    }
-
-    @Override
-    public void onCreate(SQLiteDatabase db) {
-        db.execSQL(SQL_CREATE_DISCOVERY_ITEM_DB);
-        db.execSQL(SQL_CREATE_FAST_PAIR_ITEM_DB);
-    }
-
-    @Override
-    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-        // Since the outdated data has no value so just remove the data.
-        db.execSQL(SQL_DELETE_DISCOVERY_ITEM_DB);
-        db.execSQL(SQL_DELETE_FAST_PAIR_ITEM_DB);
-        onCreate(db);
-    }
-
-    @Override
-    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-        super.onDowngrade(db, oldVersion, newVersion);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/StoredFastPairItemContract.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/StoredFastPairItemContract.java
deleted file mode 100644
index 9980565..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/StoredFastPairItemContract.java
+++ /dev/null
@@ -1,37 +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.server.nearby.fastpair.cache;
-
-import android.provider.BaseColumns;
-
-/**
- * Defines fast pair item database schema.
- */
-public class StoredFastPairItemContract {
-    private StoredFastPairItemContract() {}
-
-    /**
-     * StoredFastPairItem entry related info.
-     */
-    public static class StoredFastPairItemEntry implements BaseColumns {
-        public static final String TABLE_NAME = "STORED_FAST_PAIR_ITEM";
-        public static final String COLUMN_MAC_ADDRESS = "MAC_ADDRESS";
-        public static final String COLUMN_ACCOUNT_KEY = "ACCOUNT_KEY";
-
-        public static final String COLUMN_STORED_FAST_PAIR_BYTE = "STORED_FAST_PAIR_BYTE";
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/footprint/FastPairUploadInfo.java b/nearby/service/java/com/android/server/nearby/fastpair/footprint/FastPairUploadInfo.java
deleted file mode 100644
index 6c9aff0..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/footprint/FastPairUploadInfo.java
+++ /dev/null
@@ -1,55 +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.server.nearby.fastpair.footprint;
-
-
-import com.google.protobuf.ByteString;
-
-import service.proto.Cache;
-
-/**
- * Wrapper class that upload the pair info to the footprint.
- */
-public class FastPairUploadInfo {
-
-    private Cache.StoredDiscoveryItem mStoredDiscoveryItem;
-
-    private ByteString mAccountKey;
-
-    private  ByteString mSha256AccountKeyPublicAddress;
-
-
-    public FastPairUploadInfo(Cache.StoredDiscoveryItem storedDiscoveryItem, ByteString accountKey,
-            ByteString sha256AccountKeyPublicAddress) {
-        mStoredDiscoveryItem = storedDiscoveryItem;
-        mAccountKey = accountKey;
-        mSha256AccountKeyPublicAddress = sha256AccountKeyPublicAddress;
-    }
-
-    public Cache.StoredDiscoveryItem getStoredDiscoveryItem() {
-        return mStoredDiscoveryItem;
-    }
-
-    public ByteString getAccountKey() {
-        return mAccountKey;
-    }
-
-
-    public ByteString getSha256AccountKeyPublicAddress() {
-        return mSha256AccountKeyPublicAddress;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/footprint/FootprintsDeviceManager.java b/nearby/service/java/com/android/server/nearby/fastpair/footprint/FootprintsDeviceManager.java
deleted file mode 100644
index 68217c1..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/footprint/FootprintsDeviceManager.java
+++ /dev/null
@@ -1,25 +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.server.nearby.fastpair.footprint;
-
-/**
- * FootprintDeviceManager is responsible for all of the foot print operation. Footprint will
- * store all of device info that already paired with certain account. This class will call AOSP
- * api to let OEM save certain device.
- */
-public class FootprintsDeviceManager {
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklist.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklist.java
deleted file mode 100644
index 146b97a..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklist.java
+++ /dev/null
@@ -1,193 +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.server.nearby.fastpair.halfsheet;
-
-
-import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.ACTIVE;
-import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DISMISSED;
-import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN;
-import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN_LONG;
-
-import android.util.Log;
-import android.util.LruCache;
-
-import androidx.annotation.VisibleForTesting;
-
-import com.android.server.nearby.fastpair.blocklist.Blocklist;
-import com.android.server.nearby.fastpair.blocklist.BlocklistElement;
-import com.android.server.nearby.util.Clock;
-import com.android.server.nearby.util.DefaultClock;
-
-
-/**
- * Maintains a list of half sheet id to tell whether the half sheet should be suppressed or not.
- *
- * <p>When user cancel half sheet, the ble address related half sheet should be in block list and
- * after certain duration of time half sheet can show again.
- */
-public class FastPairHalfSheetBlocklist extends LruCache<Integer, BlocklistElement>
-        implements Blocklist {
-    private static final String TAG = "HalfSheetBlocklist";
-    // Number of entries in the FastPair blocklist
-    private static final int FAST_PAIR_BLOCKLIST_CACHE_SIZE = 16;
-    // Duration between first half sheet dismiss and second half sheet shows: 2 seconds
-    private static final int FAST_PAIR_HALF_SHEET_DISMISS_COOL_DOWN_MILLIS = 2000;
-    // The timeout to ban half sheet after user trigger the ban logic even number of time : 1 day
-    private static final int DURATION_RESURFACE_HALFSHEET_EVEN_NUMBER_BAN_MILLI_SECONDS = 86400000;
-    // Timeout for DISMISSED entries in the blocklist to expire : 1 min
-    private static final int FAST_PAIR_BLOCKLIST_DISMISSED_HALF_SHEET_TIMEOUT_MILLIS = 60000;
-    // The timeout for entries in the blocklist to expire : 1 day
-    private static final int STATE_EXPIRATION_MILLI_SECONDS = 86400000;
-    private long mEndTimeBanAllItems;
-    private final Clock mClock;
-
-
-    public FastPairHalfSheetBlocklist() {
-        // Reuses the size limit from notification cache.
-        // Number of entries in the FastPair blocklist
-        super(FAST_PAIR_BLOCKLIST_CACHE_SIZE);
-        mClock = new DefaultClock();
-    }
-
-    @VisibleForTesting
-    FastPairHalfSheetBlocklist(int size, Clock clock) {
-        super(size);
-        mClock = clock;
-    }
-
-    /**
-     * Checks whether need to show HalfSheet or not.
-     *
-     * <p> When the HalfSheet {@link BlocklistState} is DISMISS, there is a little cool down period
-     * to allow half sheet to reshow.
-     * If the HalfSheet {@link BlocklistState} is DO_NOT_SHOW_AGAIN, within durationMilliSeconds
-     * from banned start time, the function will return true
-     * otherwise it will return false if the status is expired
-     * If the HalfSheet {@link BlocklistState} is DO_NOT_SHOW_AGAIN_LONG, the half sheet will be
-     * baned for a longer duration.
-     *
-     * @param id {@link com.android.nearby.halfsheet.HalfSheetActivity} id
-     * @param durationMilliSeconds the time duration from item is banned to now
-     * @return whether the HalfSheet is blocked to show
-     */
-    @Override
-    public boolean isBlocklisted(int id, int durationMilliSeconds) {
-        if (shouldBanAllItem()) {
-            return true;
-        }
-        BlocklistElement entry = get(id);
-        if (entry == null) {
-            return false;
-        }
-        if (entry.getState().equals(DO_NOT_SHOW_AGAIN)) {
-            Log.d(TAG, "BlocklistState: DO_NOT_SHOW_AGAIN");
-            return mClock.elapsedRealtime() < entry.getTimeStamp() + durationMilliSeconds;
-        }
-        if (entry.getState().equals(DO_NOT_SHOW_AGAIN_LONG)) {
-            Log.d(TAG, "BlocklistState: DO_NOT_SHOW_AGAIN_LONG ");
-            return mClock.elapsedRealtime()
-                    < entry.getTimeStamp()
-                    + DURATION_RESURFACE_HALFSHEET_EVEN_NUMBER_BAN_MILLI_SECONDS;
-        }
-
-        if (entry.getState().equals(ACTIVE)) {
-            Log.d(TAG, "BlocklistState: ACTIVE");
-            return false;
-        }
-        // Get some cool down period for dismiss state
-        if (entry.getState().equals(DISMISSED)) {
-            Log.d(TAG, "BlocklistState: DISMISSED");
-            return mClock.elapsedRealtime()
-                    < entry.getTimeStamp() + FAST_PAIR_HALF_SHEET_DISMISS_COOL_DOWN_MILLIS;
-        }
-        if (dismissStateHasExpired(entry)) {
-            Log.d(TAG, "stateHasExpired: True");
-            return false;
-        }
-        return true;
-    }
-
-    @Override
-    public boolean removeBlocklist(int id) {
-        BlocklistElement oldValue = remove(id);
-        return oldValue != null;
-    }
-
-    /**
-     * Updates the HalfSheet blocklist state
-     *
-     * <p>When the new {@link BlocklistState} has higher priority then old {@link BlocklistState} or
-     * the old {@link BlocklistState} status is expired,the function will update the status.
-     *
-     * @param id HalfSheet id
-     * @param state Blocklist state
-     * @return update status successful or not
-     */
-    @Override
-    public boolean updateState(int id, BlocklistState state) {
-        BlocklistElement entry = get(id);
-        if (entry == null || state.hasHigherPriorityThan(entry.getState())
-                || dismissStateHasExpired(entry)) {
-            Log.d(TAG, "updateState: " + state);
-            put(id, new BlocklistElement(state, mClock.elapsedRealtime()));
-            return true;
-        }
-        return false;
-    }
-
-    /** Enables lower state to override the higher value state. */
-    public void forceUpdateState(int id, BlocklistState state) {
-        put(id, new BlocklistElement(state, mClock.elapsedRealtime()));
-    }
-
-    /** Resets certain device ban state to active. */
-    @Override
-    public void resetBlockState(int id) {
-        BlocklistElement entry = get(id);
-        if (entry != null) {
-            put(id, new BlocklistElement(ACTIVE, mClock.elapsedRealtime()));
-        }
-    }
-
-    /** Checks whether certain device state has expired. */
-    public boolean isStateExpired(int id) {
-        BlocklistElement entry = get(id);
-        if (entry != null) {
-            return mClock.elapsedRealtime() > entry.getTimeStamp() + STATE_EXPIRATION_MILLI_SECONDS;
-        }
-        return false;
-    }
-
-    private boolean dismissStateHasExpired(BlocklistElement entry) {
-        return mClock.elapsedRealtime()
-                > entry.getTimeStamp() + FAST_PAIR_BLOCKLIST_DISMISSED_HALF_SHEET_TIMEOUT_MILLIS;
-    }
-
-    /**
-     * Updates the end time that all half sheet will be banned.
-     */
-    void banAllItem(long banDurationTimeMillis) {
-        long endTime = mClock.elapsedRealtime() + banDurationTimeMillis;
-        if (endTime > mEndTimeBanAllItems) {
-            mEndTimeBanAllItems = endTime;
-        }
-    }
-
-    private boolean shouldBanAllItem() {
-        return mClock.elapsedRealtime() < mEndTimeBanAllItems;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java
deleted file mode 100644
index 7b266a7..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java
+++ /dev/null
@@ -1,571 +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.server.nearby.fastpair.halfsheet;
-
-import static com.android.nearby.halfsheet.constants.Constant.DEVICE_PAIRING_FRAGMENT_TYPE;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_BINDER;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_BUNDLE;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_CONTENT;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_INFO;
-import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_TYPE;
-import static com.android.nearby.halfsheet.constants.Constant.FAST_PAIR_HALF_SHEET_HELP_URL;
-import static com.android.nearby.halfsheet.constants.Constant.RESULT_FAIL;
-
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import android.annotation.UiThread;
-import android.app.ActivityManager;
-import android.app.KeyguardManager;
-import android.bluetooth.BluetoothDevice;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.nearby.FastPairDevice;
-import android.nearby.FastPairStatusCallback;
-import android.nearby.PairStatusMetadata;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.Log;
-import android.util.LruCache;
-import android.widget.Toast;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.nearby.halfsheet.R;
-import com.android.server.nearby.common.eventloop.Annotations;
-import com.android.server.nearby.common.eventloop.EventLoop;
-import com.android.server.nearby.common.eventloop.NamedRunnable;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.FastPairController;
-import com.android.server.nearby.fastpair.PackageUtils;
-import com.android.server.nearby.fastpair.blocklist.Blocklist;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import service.proto.Cache;
-
-/**
- * Fast Pair ux manager for half sheet.
- */
-public class FastPairHalfSheetManager {
-    private static final String ACTIVITY_INTENT_ACTION = "android.nearby.SHOW_HALFSHEET";
-    private static final String HALF_SHEET_CLASS_NAME =
-            "com.android.nearby.halfsheet.HalfSheetActivity";
-    private static final String TAG = "FPHalfSheetManager";
-    public static final String FINISHED_STATE = "FINISHED_STATE";
-    @VisibleForTesting static final String DISMISS_HALFSHEET_RUNNABLE_NAME = "DismissHalfSheet";
-    @VisibleForTesting static final String SHOW_TOAST_RUNNABLE_NAME = "SuccessPairingToast";
-
-    // The timeout to ban half sheet after user trigger the ban logic odd number of time: 5 mins
-    private static final int DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS = 300000;
-    // Number of seconds half sheet will show after the advertisement is no longer seen.
-    private static final int HALF_SHEET_TIME_OUT_SECONDS = 12;
-
-    static final int HALFSHEET_ID_SEED = "new_fast_pair_half_sheet".hashCode();
-
-    private String mHalfSheetApkPkgName;
-    private boolean mIsHalfSheetForeground = false;
-    private boolean mIsActivePairing = false;
-    private Cache.ScanFastPairStoreItem mCurrentScanFastPairStoreItem = null;
-    private final LocatorContextWrapper mLocatorContextWrapper;
-    private final AtomicInteger mNotificationIds = new AtomicInteger(HALFSHEET_ID_SEED);
-    private FastPairHalfSheetBlocklist mHalfSheetBlocklist;
-    // Todo: Make "16" a flag, which can be updated from the server side.
-    final LruCache<String, Integer> mModelIdMap = new LruCache<>(16);
-    HalfSheetDismissState mHalfSheetDismissState = HalfSheetDismissState.ACTIVE;
-    // Ban count map track the number of ban happens to certain model id
-    // If the model id is baned by the odd number of time it is banned for 5 mins
-    // if the model id is banned even number of time ban 24 hours.
-    private final Map<Integer, Integer> mBanCountMap = new HashMap<>();
-
-    FastPairUiServiceImpl mFastPairUiService;
-    private NamedRunnable mDismissRunnable;
-
-    /**
-     * Half sheet state default is active. If user dismiss half sheet once controller will mark half
-     * sheet as dismiss state. If user dismiss half sheet twice controller will mark half sheet as
-     * ban state for certain period of time.
-     */
-    enum HalfSheetDismissState {
-        ACTIVE,
-        DISMISS,
-        BAN
-    }
-
-    public FastPairHalfSheetManager(Context context) {
-        this(new LocatorContextWrapper(context));
-        mHalfSheetBlocklist = new FastPairHalfSheetBlocklist();
-    }
-
-    @VisibleForTesting
-    public FastPairHalfSheetManager(LocatorContextWrapper locatorContextWrapper) {
-        mLocatorContextWrapper = locatorContextWrapper;
-        mFastPairUiService = new FastPairUiServiceImpl();
-        mHalfSheetBlocklist = new FastPairHalfSheetBlocklist();
-    }
-
-    /**
-     * Invokes half sheet in the other apk. This function can only be called in Nearby because other
-     * app can't get the correct component name.
-     */
-    public void showHalfSheet(Cache.ScanFastPairStoreItem scanFastPairStoreItem) {
-        String modelId = scanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT);
-        if (modelId == null) {
-            Log.d(TAG, "model id not found");
-            return;
-        }
-
-        synchronized (mModelIdMap) {
-            if (mModelIdMap.get(modelId) == null) {
-                mModelIdMap.put(modelId, createNewHalfSheetId());
-            }
-        }
-        int halfSheetId = mModelIdMap.get(modelId);
-
-        if (!allowedToShowHalfSheet(halfSheetId)) {
-            Log.d(TAG, "Not allow to show initial Half sheet");
-            return;
-        }
-
-        // If currently half sheet UI is in the foreground,
-        // DO NOT request start-activity to avoid unnecessary memory usage
-        if (mIsHalfSheetForeground) {
-            updateForegroundHalfSheet(scanFastPairStoreItem);
-            return;
-        } else {
-            // If the half sheet is not in foreground but the system is still pairing
-            // with the same device, mark as duplicate request and skip.
-            if (mCurrentScanFastPairStoreItem != null && mIsActivePairing
-                    && mCurrentScanFastPairStoreItem.getAddress().toLowerCase(Locale.ROOT)
-                    .equals(scanFastPairStoreItem.getAddress().toLowerCase(Locale.ROOT))) {
-                Log.d(TAG, "Same device is pairing.");
-                return;
-            }
-        }
-
-        try {
-            if (mLocatorContextWrapper != null) {
-                String packageName = getHalfSheetApkPkgName();
-                if (packageName == null) {
-                    Log.e(TAG, "package name is null");
-                    return;
-                }
-                mFastPairUiService.setFastPairController(
-                        mLocatorContextWrapper.getLocator().get(FastPairController.class));
-                Bundle bundle = new Bundle();
-                bundle.putBinder(EXTRA_BINDER, mFastPairUiService);
-                mLocatorContextWrapper
-                        .startActivityAsUser(new Intent(ACTIVITY_INTENT_ACTION)
-                                        .putExtra(EXTRA_HALF_SHEET_INFO,
-                                                scanFastPairStoreItem.toByteArray())
-                                        .putExtra(EXTRA_HALF_SHEET_TYPE,
-                                                DEVICE_PAIRING_FRAGMENT_TYPE)
-                                        .putExtra(EXTRA_BUNDLE, bundle)
-                                        .setComponent(new ComponentName(packageName,
-                                                HALF_SHEET_CLASS_NAME)),
-                                UserHandle.CURRENT);
-                mHalfSheetBlocklist.updateState(halfSheetId, Blocklist.BlocklistState.ACTIVE);
-            }
-        } catch (IllegalStateException e) {
-            Log.e(TAG, "Can't resolve package that contains half sheet");
-        }
-        Log.d(TAG, "show initial half sheet.");
-        mCurrentScanFastPairStoreItem = scanFastPairStoreItem;
-        mIsHalfSheetForeground = true;
-        enableAutoDismiss(scanFastPairStoreItem.getAddress(), HALF_SHEET_TIME_OUT_SECONDS);
-    }
-
-    /**
-     * Auto dismiss half sheet after timeout
-     */
-    @VisibleForTesting
-    void enableAutoDismiss(String address, long timeoutDuration) {
-        if (mDismissRunnable == null
-                || !mDismissRunnable.name.equals(DISMISS_HALFSHEET_RUNNABLE_NAME)) {
-            mDismissRunnable =
-                new NamedRunnable(DISMISS_HALFSHEET_RUNNABLE_NAME) {
-                    @Override
-                    public void run() {
-                        Log.d(TAG, "Dismiss the half sheet after "
-                                + timeoutDuration + " seconds");
-                        // BMW car kit will advertise even after pairing start,
-                        // to avoid the half sheet be dismissed during active pairing,
-                        // If the half sheet is in the pairing state, disable the auto dismiss.
-                        // See b/182396106
-                        if (mIsActivePairing) {
-                            return;
-                        }
-                        mIsHalfSheetForeground = false;
-                        FastPairStatusCallback pairStatusCallback =
-                                mFastPairUiService.getPairStatusCallback();
-                        if (pairStatusCallback != null) {
-                            pairStatusCallback.onPairUpdate(new FastPairDevice.Builder()
-                                            .setBluetoothAddress(address).build(),
-                                    new PairStatusMetadata(PairStatusMetadata.Status.DISMISS));
-                        } else {
-                            Log.w(TAG, "pairStatusCallback is null,"
-                                    + " failed to enable auto dismiss ");
-                        }
-                    }
-                };
-        }
-        if (Locator.get(mLocatorContextWrapper, EventLoop.class).isPosted(mDismissRunnable)) {
-            disableDismissRunnable();
-        }
-        Locator.get(mLocatorContextWrapper, EventLoop.class)
-            .postRunnableDelayed(mDismissRunnable, SECONDS.toMillis(timeoutDuration));
-    }
-
-    private void updateForegroundHalfSheet(Cache.ScanFastPairStoreItem scanFastPairStoreItem) {
-        if (mCurrentScanFastPairStoreItem == null) {
-            return;
-        }
-        if (mCurrentScanFastPairStoreItem.getAddress().toLowerCase(Locale.ROOT)
-                .equals(scanFastPairStoreItem.getAddress().toLowerCase(Locale.ROOT))) {
-            // If current address is the same, reset the timeout.
-            Log.d(TAG, "same Address device, reset the auto dismiss timeout");
-            enableAutoDismiss(scanFastPairStoreItem.getAddress(), HALF_SHEET_TIME_OUT_SECONDS);
-        } else {
-            // If current address is different, not reset timeout
-            // wait for half sheet auto dismiss or manually dismiss to start new pair.
-            if (mCurrentScanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT)
-                    .equals(scanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT))) {
-                Log.d(TAG, "same model id device is also nearby");
-            }
-            Log.d(TAG, "showInitialHalfsheet: address changed, from "
-                    +  mCurrentScanFastPairStoreItem.getAddress()
-                    + " to " + scanFastPairStoreItem.getAddress());
-        }
-    }
-
-    /**
-     * Show passkey confirmation info on half sheet
-     */
-    public void showPasskeyConfirmation(BluetoothDevice device, int passkey) {
-    }
-
-    /**
-     * This function will handle pairing steps for half sheet.
-     */
-    public void showPairingHalfSheet(DiscoveryItem item) {
-        Log.d(TAG, "show pairing half sheet");
-    }
-
-    /**
-     * Shows pairing success info.
-     * If the half sheet is not shown, show toast to remind user.
-     */
-    public void showPairingSuccessHalfSheet(String address) {
-        resetPairingStateDisableAutoDismiss();
-        if (mIsHalfSheetForeground) {
-            FastPairStatusCallback pairStatusCallback = mFastPairUiService.getPairStatusCallback();
-            if (pairStatusCallback == null) {
-                Log.w(TAG, "FastPairHalfSheetManager failed to show success half sheet because "
-                        + "the pairStatusCallback is null");
-                return;
-            }
-            Log.d(TAG, "showPairingSuccess: pairStatusCallback not NULL");
-            pairStatusCallback.onPairUpdate(
-                    new FastPairDevice.Builder().setBluetoothAddress(address).build(),
-                    new PairStatusMetadata(PairStatusMetadata.Status.SUCCESS));
-        } else {
-            Locator.get(mLocatorContextWrapper, EventLoop.class)
-                    .postRunnable(
-                            new NamedRunnable(SHOW_TOAST_RUNNABLE_NAME) {
-                                @Override
-                                public void run() {
-                                    try {
-                                        Toast.makeText(mLocatorContextWrapper,
-                                                mLocatorContextWrapper
-                                                        .getPackageManager()
-                                                        .getResourcesForApplication(
-                                                                getHalfSheetApkPkgName())
-                                                        .getString(R.string.fast_pair_device_ready),
-                                                Toast.LENGTH_LONG).show();
-                                    } catch (PackageManager.NameNotFoundException e) {
-                                        Log.d(TAG, "showPairingSuccess fail:"
-                                                + " package name cannot be found ");
-                                        e.printStackTrace();
-                                    }
-                                }
-                            });
-        }
-    }
-
-    /**
-     * Shows pairing fail half sheet.
-     * If the half sheet is not shown, create a new half sheet to help user go to Setting
-     * to manually pair with the device.
-     */
-    public void showPairingFailed() {
-        resetPairingStateDisableAutoDismiss();
-        if (mCurrentScanFastPairStoreItem == null) {
-            return;
-        }
-        if (mIsHalfSheetForeground) {
-            FastPairStatusCallback pairStatusCallback = mFastPairUiService.getPairStatusCallback();
-            if (pairStatusCallback != null) {
-                Log.v(TAG, "showPairingFailed: pairStatusCallback not NULL");
-                pairStatusCallback.onPairUpdate(
-                        new FastPairDevice.Builder()
-                                .setBluetoothAddress(mCurrentScanFastPairStoreItem.getAddress())
-                                .build(),
-                        new PairStatusMetadata(PairStatusMetadata.Status.FAIL));
-            } else {
-                Log.w(TAG, "FastPairHalfSheetManager failed to show fail half sheet because "
-                        + "the pairStatusCallback is null");
-            }
-        } else {
-            String packageName = getHalfSheetApkPkgName();
-            if (packageName == null) {
-                Log.e(TAG, "package name is null");
-                return;
-            }
-            Bundle bundle = new Bundle();
-            bundle.putBinder(EXTRA_BINDER, mFastPairUiService);
-            mLocatorContextWrapper
-                    .startActivityAsUser(new Intent(ACTIVITY_INTENT_ACTION)
-                                    .putExtra(EXTRA_HALF_SHEET_INFO,
-                                            mCurrentScanFastPairStoreItem.toByteArray())
-                                    .putExtra(EXTRA_HALF_SHEET_TYPE,
-                                            DEVICE_PAIRING_FRAGMENT_TYPE)
-                                    .putExtra(EXTRA_HALF_SHEET_CONTENT, RESULT_FAIL)
-                                    .putExtra(EXTRA_BUNDLE, bundle)
-                                    .setComponent(new ComponentName(packageName,
-                                            HALF_SHEET_CLASS_NAME)),
-                            UserHandle.CURRENT);
-            Log.d(TAG, "Starts a new half sheet to showPairingFailed");
-            String modelId = mCurrentScanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT);
-            if (modelId == null || mModelIdMap.get(modelId) == null) {
-                Log.d(TAG, "info not enough");
-                return;
-            }
-            int halfSheetId = mModelIdMap.get(modelId);
-            mHalfSheetBlocklist.updateState(halfSheetId, Blocklist.BlocklistState.ACTIVE);
-        }
-    }
-
-    /**
-     * Removes dismiss half sheet runnable. When half sheet shows, there is timer for half sheet to
-     * dismiss. But when user is pairing, half sheet should not dismiss.
-     * So this function disable the runnable.
-     */
-    public void disableDismissRunnable() {
-        if (mDismissRunnable == null) {
-            return;
-        }
-        Log.d(TAG, "remove dismiss runnable");
-        Locator.get(mLocatorContextWrapper, EventLoop.class).removeRunnable(mDismissRunnable);
-    }
-
-    /**
-     * When user first click back button or click the empty space in half sheet the half sheet will
-     * be banned for certain short period of time for that device model id. When user click cancel
-     * or dismiss half sheet for the second time the half sheet related item should be added to
-     * blocklist so the half sheet will not show again to interrupt user.
-     *
-     * @param modelId half sheet display item modelId.
-     */
-    @Annotations.EventThread
-    public void dismiss(String modelId) {
-        Log.d(TAG, "HalfSheetManager report dismiss device modelId: " + modelId);
-        mIsHalfSheetForeground = false;
-        Integer halfSheetId = mModelIdMap.get(modelId);
-        if (mDismissRunnable != null
-                && Locator.get(mLocatorContextWrapper, EventLoop.class)
-                          .isPosted(mDismissRunnable)) {
-            disableDismissRunnable();
-        }
-        if (halfSheetId != null) {
-            Log.d(TAG, "id: " + halfSheetId + " half sheet is dismissed");
-            boolean isDontShowAgain =
-                    !mHalfSheetBlocklist.updateState(halfSheetId,
-                            Blocklist.BlocklistState.DISMISSED);
-            if (isDontShowAgain) {
-                if (!mBanCountMap.containsKey(halfSheetId)) {
-                    mBanCountMap.put(halfSheetId, 0);
-                }
-                int dismissCountTrack = mBanCountMap.get(halfSheetId) + 1;
-                mBanCountMap.put(halfSheetId, dismissCountTrack);
-                if (dismissCountTrack % 2 == 1) {
-                    Log.d(TAG, "id: " + halfSheetId + " half sheet is short time banned");
-                    mHalfSheetBlocklist.forceUpdateState(halfSheetId,
-                            Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN);
-                } else {
-                    Log.d(TAG, "id: " + halfSheetId +  " half sheet is long time banned");
-                    mHalfSheetBlocklist.updateState(halfSheetId,
-                            Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN_LONG);
-                }
-            }
-        }
-    }
-
-    /**
-     * Changes the half sheet ban state to active.
-     */
-    @UiThread
-    public void resetBanState(String modelId) {
-        Log.d(TAG, "HalfSheetManager reset device ban state modelId: " + modelId);
-        Integer halfSheetId = mModelIdMap.get(modelId);
-        if (halfSheetId == null) {
-            Log.d(TAG, "halfSheetId not found.");
-            return;
-        }
-        mHalfSheetBlocklist.resetBlockState(halfSheetId);
-    }
-
-    // Invokes this method to reset some states when showing the pairing result.
-    private void resetPairingStateDisableAutoDismiss() {
-        mIsActivePairing = false;
-        if (mDismissRunnable != null && Locator.get(mLocatorContextWrapper, EventLoop.class)
-                .isPosted(mDismissRunnable)) {
-            disableDismissRunnable();
-        }
-    }
-
-    /**
-     * When the device pairing finished should remove the suppression for the model id
-     * so the user canntry twice if the user want to.
-     */
-    public void reportDonePairing(int halfSheetId) {
-        mHalfSheetBlocklist.removeBlocklist(halfSheetId);
-    }
-
-    @VisibleForTesting
-    public FastPairHalfSheetBlocklist getHalfSheetBlocklist() {
-        return mHalfSheetBlocklist;
-    }
-
-    /**
-     * Destroys the bluetooth pairing controller.
-     */
-    public void destroyBluetoothPairController() {
-    }
-
-    /**
-     * Notifies manager the pairing has finished.
-     */
-    public void notifyPairingProcessDone(boolean success, String address, DiscoveryItem item) {
-        mCurrentScanFastPairStoreItem = null;
-        mIsHalfSheetForeground = false;
-    }
-
-    private boolean allowedToShowHalfSheet(int halfSheetId) {
-        // Half Sheet will not show when the screen is locked so disable half sheet
-        KeyguardManager keyguardManager =
-                mLocatorContextWrapper.getSystemService(KeyguardManager.class);
-        if (keyguardManager != null && keyguardManager.isKeyguardLocked()) {
-            Log.d(TAG, "device is locked");
-            return false;
-        }
-
-        // Check whether the blocklist state has expired
-        if (mHalfSheetBlocklist.isStateExpired(halfSheetId)) {
-            mHalfSheetBlocklist.removeBlocklist(halfSheetId);
-            mBanCountMap.remove(halfSheetId);
-        }
-
-        // Half Sheet will not show when the model id is banned
-        if (mHalfSheetBlocklist.isBlocklisted(
-                halfSheetId, DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)) {
-            Log.d(TAG, "id: " + halfSheetId + " is blocked");
-            return false;
-        }
-        return  !isHelpPageForeground();
-    }
-
-    /**
-    * Checks if the user already open the info page, return true to suppress half sheet.
-    * ActivityManager#getRunningTasks to get the most recent task and check the baseIntent's
-    * url to see if we should suppress half sheet.
-    */
-    private boolean isHelpPageForeground() {
-        ActivityManager activityManager =
-                mLocatorContextWrapper.getSystemService(ActivityManager.class);
-        if (activityManager == null) {
-            Log.d(TAG, "ActivityManager is null");
-            return false;
-        }
-        try {
-            List<ActivityManager.RunningTaskInfo> taskInfos = activityManager.getRunningTasks(1);
-            if (taskInfos.isEmpty()) {
-                Log.d(TAG, "Empty running tasks");
-                return false;
-            }
-            String url = taskInfos.get(0).baseIntent.getDataString();
-            Log.d(TAG, "Info page url:" + url);
-            if (FAST_PAIR_HALF_SHEET_HELP_URL.equals(url)) {
-                return true;
-            }
-        } catch (SecurityException e) {
-            Log.d(TAG, "Unable to get running tasks");
-        }
-        return false;
-    }
-
-    /** Report actively pairing when the Fast Pair starts. */
-    public void reportActivelyPairing() {
-        mIsActivePairing = true;
-    }
-
-
-    private Integer createNewHalfSheetId() {
-        return mNotificationIds.getAndIncrement();
-    }
-
-    /** Gets the half sheet status whether it is foreground or dismissed */
-    public boolean getHalfSheetForeground() {
-        return mIsHalfSheetForeground;
-    }
-
-    /** Sets whether the half sheet is at the foreground or not. */
-    public void setHalfSheetForeground(boolean state) {
-        mIsHalfSheetForeground = state;
-    }
-
-    /** Returns whether the fast pair is actively pairing . */
-    @VisibleForTesting
-    public boolean isActivePairing() {
-        return mIsActivePairing;
-    }
-
-    /** Sets fast pair to be active pairing or not, used for testing. */
-    @VisibleForTesting
-    public void setIsActivePairing(boolean isActivePairing) {
-        mIsActivePairing = isActivePairing;
-    }
-
-    /**
-     * Gets the package name of HalfSheet.apk
-     * getHalfSheetApkPkgName may invoke PackageManager multiple times and it does not have
-     * race condition check. Since there is no lock for mHalfSheetApkPkgName.
-     */
-    private String getHalfSheetApkPkgName() {
-        if (mHalfSheetApkPkgName != null) {
-            return mHalfSheetApkPkgName;
-        }
-        mHalfSheetApkPkgName = PackageUtils.getHalfSheetApkPkgName(mLocatorContextWrapper);
-        Log.v(TAG, "Found halfsheet APK at: " + mHalfSheetApkPkgName);
-        return mHalfSheetApkPkgName;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairUiServiceImpl.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairUiServiceImpl.java
deleted file mode 100644
index eb1fb85..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairUiServiceImpl.java
+++ /dev/null
@@ -1,110 +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.server.nearby.fastpair.halfsheet;
-
-import static com.android.server.nearby.fastpair.Constant.TAG;
-
-import android.nearby.FastPairDevice;
-import android.nearby.FastPairStatusCallback;
-import android.nearby.PairStatusMetadata;
-import android.nearby.aidl.IFastPairStatusCallback;
-import android.nearby.aidl.IFastPairUiService;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.fastpair.FastPairController;
-
-/**
- * Service implementing Fast Pair functionality.
- *
- * @hide
- */
-public class FastPairUiServiceImpl extends IFastPairUiService.Stub {
-
-    private IBinder mStatusCallbackProxy;
-    private FastPairController mFastPairController;
-    private FastPairStatusCallback mFastPairStatusCallback;
-
-    /**
-     * Registers the Binder call back in the server notifies the proxy when there is an update
-     * in the server.
-     */
-    @Override
-    public void registerCallback(IFastPairStatusCallback iFastPairStatusCallback) {
-        mStatusCallbackProxy = iFastPairStatusCallback.asBinder();
-        mFastPairStatusCallback = new FastPairStatusCallback() {
-            @Override
-            public void onPairUpdate(FastPairDevice fastPairDevice,
-                    PairStatusMetadata pairStatusMetadata) {
-                try {
-                    iFastPairStatusCallback.onPairUpdate(fastPairDevice, pairStatusMetadata);
-                } catch (RemoteException e) {
-                    Log.w(TAG, "Failed to update pair status.", e);
-                }
-            }
-        };
-    }
-
-    /**
-     * Unregisters the Binder call back in the server.
-     */
-    @Override
-    public void unregisterCallback(IFastPairStatusCallback iFastPairStatusCallback) {
-        mStatusCallbackProxy = null;
-        mFastPairStatusCallback = null;
-    }
-
-    /**
-     * Asks the Fast Pair service to pair the device. initial pairing.
-     */
-    @Override
-    public void connect(FastPairDevice fastPairDevice) {
-        if (mFastPairController != null) {
-            mFastPairController.pair(fastPairDevice);
-        } else {
-            Log.w(TAG, "Failed to connect because there is no FastPairController.");
-        }
-    }
-
-    /**
-     * Cancels Fast Pair connection and dismisses half sheet.
-     */
-    @Override
-    public void cancel(FastPairDevice fastPairDevice) {
-    }
-
-    public FastPairStatusCallback getPairStatusCallback() {
-        return mFastPairStatusCallback;
-    }
-
-    /**
-     * Sets fastPairStatusCallback.
-     */
-    @VisibleForTesting
-    public void setFastPairStatusCallback(FastPairStatusCallback fastPairStatusCallback) {
-        mFastPairStatusCallback = fastPairStatusCallback;
-    }
-
-    /**
-     * Sets function for Fast Pair controller.
-     */
-    public void setFastPairController(FastPairController fastPairController) {
-        mFastPairController = fastPairController;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilder.java b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilder.java
deleted file mode 100644
index 4260235..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilder.java
+++ /dev/null
@@ -1,67 +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.server.nearby.fastpair.notification;
-
-import android.app.Notification;
-import android.content.Context;
-import android.os.Bundle;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.nearby.halfsheet.R;
-import com.android.server.nearby.fastpair.HalfSheetResources;
-import com.android.server.nearby.fastpair.PackageUtils;
-
-/** Wrapper class for Fast Pair specific logic for notification builder. */
-public class FastPairNotificationBuilder extends Notification.Builder {
-
-    @VisibleForTesting
-    static final String NOTIFICATION_OVERRIDE_NAME_EXTRA = "android.substName";
-    final String mPackageName;
-    final Context mContext;
-    final HalfSheetResources mResources;
-
-    public FastPairNotificationBuilder(Context context, String channelId) {
-        super(context, channelId);
-        this.mContext = context;
-        this.mPackageName = PackageUtils.getHalfSheetApkPkgName(context);
-        this.mResources = new HalfSheetResources(context);
-    }
-
-    /**
-     * If the flag is enabled, all the devices notification should use "Devices" as the source name,
-     * and links/Apps uses "Nearby". If the flag is not enabled, all notifications use "Nearby" as
-     * source name.
-     */
-    public FastPairNotificationBuilder setIsDevice(boolean isDevice) {
-        Bundle extras = new Bundle();
-        String notificationOverrideName =
-                isDevice
-                        ? mResources.get().getString(R.string.common_devices)
-                        : mResources.get().getString(R.string.common_nearby_title);
-        extras.putString(NOTIFICATION_OVERRIDE_NAME_EXTRA, notificationOverrideName);
-        addExtras(extras);
-        return this;
-    }
-
-    /** Set the "ticker" text which is sent to accessibility services. */
-    public FastPairNotificationBuilder setTickerForAccessibility(String tickerText) {
-        // On Lollipop and above, setTicker() tells Accessibility what to say about the notification
-        // (e.g. this is what gets announced when a HUN appears).
-        setTicker(tickerText);
-        return this;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java
deleted file mode 100644
index c74249c..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java
+++ /dev/null
@@ -1,280 +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.server.nearby.fastpair.notification;
-
-import static com.android.server.nearby.fastpair.Constant.TAG;
-
-import android.annotation.Nullable;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationChannelGroup;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.nearby.halfsheet.R;
-import com.android.server.nearby.fastpair.HalfSheetResources;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-
-import com.google.common.base.Objects;
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * Responsible for show notification logic.
- */
-public class FastPairNotificationManager {
-
-    private static int sInstanceId = 0;
-    // Notification channel group ID  for Devices notification channels.
-    private static final String DEVICES_CHANNEL_GROUP_ID = "DEVICES_CHANNEL_GROUP_ID";
-    // These channels are rebranded string because they are migrated from different channel ID they
-    // should not be changed.
-    // Channel ID for channel "Devices within reach".
-    static final String DEVICES_WITHIN_REACH_CHANNEL_ID = "DEVICES_WITHIN_REACH_REBRANDED";
-    // Channel ID for channel "Devices".
-    static final String DEVICES_CHANNEL_ID = "DEVICES_REBRANDED";
-    // Channel ID for channel "Devices with your account".
-    public static final String DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_ID = "DEVICES_WITH_YOUR_ACCOUNT";
-
-    // Default channel importance for channel "Devices within reach".
-    private static final int DEFAULT_DEVICES_WITHIN_REACH_CHANNEL_IMPORTANCE =
-            NotificationManager.IMPORTANCE_HIGH;
-    // Default channel importance for channel "Devices".
-    private static final int DEFAULT_DEVICES_CHANNEL_IMPORTANCE =
-            NotificationManager.IMPORTANCE_LOW;
-    // Default channel importance for channel "Devices with your account".
-    private static final int DEFAULT_DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_IMPORTANCE =
-            NotificationManager.IMPORTANCE_MIN;
-
-    /** Fixed notification ID that won't duplicated with {@code notificationId}. */
-    private static final int MAGIC_PAIR_NOTIFICATION_ID = "magic_pair_notification_id".hashCode();
-    /** Fixed notification ID that won't duplicated with {@code mNotificationId}. */
-    @VisibleForTesting
-    static final int PAIR_SUCCESS_NOTIFICATION_ID = MAGIC_PAIR_NOTIFICATION_ID - 1;
-    /** Fixed notification ID for showing the pairing failure notification. */
-    @VisibleForTesting static final int PAIR_FAILURE_NOTIFICATION_ID =
-            MAGIC_PAIR_NOTIFICATION_ID - 3;
-
-    /**
-     * The amount of delay enforced between notifications. The system only allows 10 notifications /
-     * second, but delays in the binder IPC can cause overlap.
-     */
-    private static final long MIN_NOTIFICATION_DELAY_MILLIS = 300;
-
-    // To avoid a (really unlikely) race where the user pairs and succeeds quickly more than once,
-    // use a unique ID per session, so we can delay cancellation without worrying.
-    // This is for connecting related notifications only. Discovery notification will use item id
-    // as notification id.
-    @VisibleForTesting
-    final int mNotificationId;
-    private HalfSheetResources mResources;
-    private final FastPairNotifications mNotifications;
-    private boolean mDiscoveryNotificationEnable = true;
-    // A static cache that remembers all recently shown notifications. We use this to throttle
-    // ourselves from showing notifications too rapidly. If we attempt to show a notification faster
-    // than once every 100ms, the later notifications will be dropped and we'll show stale state.
-    // Maps from Key -> Uptime Millis
-    private final Cache<Key, Long> mNotificationCache =
-            CacheBuilder.newBuilder()
-                    .maximumSize(100)
-                    .expireAfterWrite(MIN_NOTIFICATION_DELAY_MILLIS, TimeUnit.MILLISECONDS)
-                    .build();
-    private NotificationManager mNotificationManager;
-
-    /**
-     * FastPair notification manager that handle notification ui for fast pair.
-     */
-    @VisibleForTesting
-    public FastPairNotificationManager(Context context, int notificationId,
-            NotificationManager notificationManager, HalfSheetResources resources) {
-        mNotificationId = notificationId;
-        mNotificationManager = notificationManager;
-        mResources = resources;
-        mNotifications = new FastPairNotifications(context, mResources);
-
-        configureDevicesNotificationChannels();
-    }
-
-    /**
-     * FastPair notification manager that handle notification ui for fast pair.
-     */
-    public FastPairNotificationManager(Context context, int notificationId) {
-        this(context, notificationId, context.getSystemService(NotificationManager.class),
-                new HalfSheetResources(context));
-    }
-
-    /**
-     * FastPair notification manager that handle notification ui for fast pair.
-     */
-    public FastPairNotificationManager(Context context) {
-        this(context, /* notificationId= */ MAGIC_PAIR_NOTIFICATION_ID + sInstanceId);
-
-        sInstanceId++;
-    }
-
-    /**
-     *  Shows the notification when found saved device. A notification will be like
-     *  "Your saved device is available."
-     *  This uses item id as notification Id. This should be disabled when connecting starts.
-     */
-    public void showDiscoveryNotification(DiscoveryItem item, byte[] accountKey) {
-        if (mDiscoveryNotificationEnable) {
-            Log.v(TAG, "the discovery notification is disabled");
-            return;
-        }
-
-        show(item.getId().hashCode(), mNotifications.discoveryNotification(item, accountKey));
-    }
-
-    /**
-     * Shows pairing in progress notification.
-     */
-    public void showConnectingNotification(DiscoveryItem item) {
-        disableShowDiscoveryNotification();
-        cancel(PAIR_FAILURE_NOTIFICATION_ID);
-        show(mNotificationId, mNotifications.progressNotification(item));
-    }
-
-    /**
-     * Shows when Fast Pair successfully pairs the headset.
-     */
-    public void showPairingSucceededNotification(
-            DiscoveryItem item,
-            int batteryLevel,
-            @Nullable String deviceName) {
-        enableShowDiscoveryNotification();
-        cancel(mNotificationId);
-        show(PAIR_SUCCESS_NOTIFICATION_ID,
-                mNotifications
-                        .pairingSucceededNotification(
-                                batteryLevel, deviceName, item.getTitle(), item));
-    }
-
-    /**
-     * Shows failed notification.
-     */
-    public synchronized void showPairingFailedNotification(DiscoveryItem item, byte[] accountKey) {
-        enableShowDiscoveryNotification();
-        cancel(mNotificationId);
-        show(PAIR_FAILURE_NOTIFICATION_ID,
-                mNotifications.showPairingFailedNotification(item, accountKey));
-    }
-
-    /**
-     * Notify the pairing process is done.
-     */
-    public void notifyPairingProcessDone(boolean success, boolean forceNotify,
-            String privateAddress, String publicAddress) {}
-
-    /** Enables the discovery notification when pairing is in progress */
-    public void enableShowDiscoveryNotification() {
-        Log.v(TAG, "enabling discovery notification");
-        mDiscoveryNotificationEnable = true;
-    }
-
-    /** Disables the discovery notification when pairing is in progress */
-    public synchronized void disableShowDiscoveryNotification() {
-        Log.v(TAG, "disabling discovery notification");
-        mDiscoveryNotificationEnable = false;
-    }
-
-    private void show(int id, Notification notification) {
-        mNotificationManager.notify(id, notification);
-    }
-
-    /**
-     * Configures devices related notification channels, including "Devices" and "Devices within
-     * reach" channels.
-     */
-    private void configureDevicesNotificationChannels() {
-        mNotificationManager.createNotificationChannelGroup(
-                new NotificationChannelGroup(
-                        DEVICES_CHANNEL_GROUP_ID,
-                        mResources.get().getString(R.string.common_devices)));
-        mNotificationManager.createNotificationChannel(
-                createNotificationChannel(
-                        DEVICES_WITHIN_REACH_CHANNEL_ID,
-                        mResources.get().getString(R.string.devices_within_reach_channel_name),
-                        DEFAULT_DEVICES_WITHIN_REACH_CHANNEL_IMPORTANCE,
-                        DEVICES_CHANNEL_GROUP_ID));
-        mNotificationManager.createNotificationChannel(
-                createNotificationChannel(
-                        DEVICES_CHANNEL_ID,
-                        mResources.get().getString(R.string.common_devices),
-                        DEFAULT_DEVICES_CHANNEL_IMPORTANCE,
-                        DEVICES_CHANNEL_GROUP_ID));
-        mNotificationManager.createNotificationChannel(
-                createNotificationChannel(
-                        DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_ID,
-                        mResources.get().getString(R.string.devices_with_your_account_channel_name),
-                        DEFAULT_DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_IMPORTANCE,
-                        DEVICES_CHANNEL_GROUP_ID));
-    }
-
-    private NotificationChannel createNotificationChannel(
-            String channelId, String channelName, int channelImportance, String channelGroupId) {
-        NotificationChannel channel =
-                new NotificationChannel(channelId, channelName, channelImportance);
-        channel.setGroup(channelGroupId);
-        if (channelImportance >= NotificationManager.IMPORTANCE_HIGH) {
-            channel.setSound(/* sound= */ null, /* audioAttributes= */ null);
-            // Disable vibration. Otherwise, the silent sound triggers a vibration if your
-            // ring volume is set to vibrate (aka turned down all the way).
-            channel.enableVibration(false);
-        }
-
-        return channel;
-    }
-
-    /** Cancel a previously shown notification. */
-    public void cancel(int id) {
-        try {
-            mNotificationManager.cancel(id);
-        } catch (SecurityException e) {
-            Log.e(TAG, "Failed to cancel notification " + id, e);
-        }
-        mNotificationCache.invalidate(new Key(id));
-    }
-
-    private static final class Key {
-        @Nullable final String mTag;
-        final int mId;
-
-        Key(int id) {
-            this.mTag = null;
-            this.mId = id;
-        }
-
-        @Override
-        public boolean equals(@Nullable Object o) {
-            if (o instanceof Key) {
-                Key that = (Key) o;
-                return Objects.equal(mTag, that.mTag) && (mId == that.mId);
-            }
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hashCode(mTag == null ? 0 : mTag, mId);
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotifications.java b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotifications.java
deleted file mode 100644
index 3b4eef8..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotifications.java
+++ /dev/null
@@ -1,207 +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.server.nearby.fastpair.notification;
-
-import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR;
-import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_FAST_PAIR_SECRET;
-import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_ITEM_ID;
-import static com.android.server.nearby.fastpair.notification.FastPairNotificationManager.DEVICES_WITHIN_REACH_CHANNEL_ID;
-
-import static com.google.common.io.BaseEncoding.base16;
-
-import android.annotation.Nullable;
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.drawable.Icon;
-import android.os.SystemClock;
-import android.provider.Settings;
-
-import com.android.nearby.halfsheet.R;
-import com.android.server.nearby.common.fastpair.IconUtils;
-import com.android.server.nearby.fastpair.HalfSheetResources;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-
-import service.proto.Cache;
-
-/**
- * Collection of utilities to create {@link Notification} objects that are displayed through {@link
- * FastPairNotificationManager}.
- */
-public class FastPairNotifications {
-
-    private final Context mContext;
-    private final HalfSheetResources mResources;
-    /**
-     * Note: Idea copied from Google.
-     *
-     * <p>Request code used for notification pending intents (executed on tap, dismiss).
-     *
-     * <p>Android only keeps one PendingIntent instance if it thinks multiple pending intents match.
-     * As comparing PendingIntents/Intents does not inspect the data in the extras, multiple pending
-     * intents can conflict. This can have surprising consequences (see b/68702692#comment8).
-     *
-     * <p>We also need to avoid conflicts with notifications started by an earlier launch of the app
-     * so use the truncated uptime of when the class was instantiated. The uptime will only overflow
-     * every ~50 days, and even then chances of conflict will be rare.
-     */
-    private static int sRequestCode = (int) SystemClock.elapsedRealtime();
-
-    public FastPairNotifications(Context context, HalfSheetResources resources) {
-        this.mContext = context;
-        this.mResources = resources;
-    }
-
-    /**
-     * Creates the initial "Your saved device is available" notification when subsequent pairing
-     * is available.
-     * @param item discovered item which contains title and item id
-     * @param accountKey used for generating intent for pairing
-     */
-    public Notification discoveryNotification(DiscoveryItem item, byte[] accountKey) {
-        Notification.Builder builder =
-                newBaseBuilder(item)
-                        .setContentTitle(mResources.getString(R.string.fast_pair_your_device))
-                        .setContentText(item.getTitle())
-                        .setContentIntent(getPairIntent(item.getCopyOfStoredItem(), accountKey))
-                        .setCategory(Notification.CATEGORY_RECOMMENDATION)
-                        .setAutoCancel(false);
-        return builder.build();
-    }
-
-    /**
-     * Creates the in progress "Connecting" notification when the device and headset are paring.
-     */
-    public Notification progressNotification(DiscoveryItem item) {
-        String summary = mResources.getString(R.string.common_connecting);
-        Notification.Builder builder =
-                newBaseBuilder(item)
-                        .setTickerForAccessibility(summary)
-                        .setCategory(Notification.CATEGORY_PROGRESS)
-                        .setContentTitle(mResources.getString(R.string.fast_pair_your_device))
-                        .setContentText(summary)
-                        // Intermediate progress bar.
-                        .setProgress(0, 0, true)
-                        // Tapping does not dismiss this.
-                        .setAutoCancel(false);
-
-        return builder.build();
-    }
-
-    /**
-     * Creates paring failed notification.
-     */
-    public Notification showPairingFailedNotification(DiscoveryItem item, byte[] accountKey) {
-        String couldNotPair = mResources.getString(R.string.fast_pair_unable_to_connect);
-        String notificationContent;
-        if (accountKey != null) {
-            notificationContent = mResources.getString(
-                    R.string.fast_pair_turn_on_bt_device_pairing_mode);
-        } else {
-            notificationContent =
-                    mResources.getString(R.string.fast_pair_unable_to_connect_description);
-        }
-        Notification.Builder builder =
-                newBaseBuilder(item)
-                        .setTickerForAccessibility(couldNotPair)
-                        .setCategory(Notification.CATEGORY_ERROR)
-                        .setContentTitle(couldNotPair)
-                        .setContentText(notificationContent)
-                        .setContentIntent(getBluetoothSettingsIntent())
-                        // Dismissing completes the attempt.
-                        .setDeleteIntent(getBluetoothSettingsIntent());
-        return builder.build();
-
-    }
-
-    /**
-     * Creates paring successfully notification.
-     */
-    public Notification pairingSucceededNotification(
-            int batteryLevel,
-            @Nullable String deviceName,
-            String modelName,
-            DiscoveryItem item) {
-        final String contentText;
-        StringBuilder contentTextBuilder = new StringBuilder();
-        contentTextBuilder.append(modelName);
-        if (batteryLevel >= 0 && batteryLevel <= 100) {
-            contentTextBuilder
-                    .append("\n")
-                    .append(mResources.getString(R.string.common_battery_level, batteryLevel));
-        }
-        String pairingComplete =
-                deviceName == null
-                        ? mResources.getString(R.string.fast_pair_device_ready)
-                        : mResources.getString(
-                                R.string.fast_pair_device_ready_with_device_name, deviceName);
-        contentText = contentTextBuilder.toString();
-        Notification.Builder builder =
-                newBaseBuilder(item)
-                        .setTickerForAccessibility(pairingComplete)
-                        .setCategory(Notification.CATEGORY_STATUS)
-                        .setContentTitle(pairingComplete)
-                        .setContentText(contentText);
-
-        return builder.build();
-    }
-
-    private PendingIntent getPairIntent(Cache.StoredDiscoveryItem item, byte[] accountKey) {
-        Intent intent =
-                new Intent(ACTION_FAST_PAIR)
-                        .putExtra(EXTRA_ITEM_ID, item.getId())
-                        // Encode account key as a string instead of bytes so that it can be passed
-                        // to the string representation of the intent.
-                        .putExtra(EXTRA_FAST_PAIR_SECRET, base16().encode(accountKey))
-                        .setPackage(mContext.getPackageName());
-        return PendingIntent.getBroadcast(mContext, sRequestCode++, intent,
-                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
-    }
-
-    private PendingIntent getBluetoothSettingsIntent() {
-        Intent intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
-        return PendingIntent.getActivity(mContext, sRequestCode++, intent,
-                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
-    }
-
-    private LargeHeadsUpNotificationBuilder newBaseBuilder(DiscoveryItem item) {
-        LargeHeadsUpNotificationBuilder builder =
-                (LargeHeadsUpNotificationBuilder)
-                        (new LargeHeadsUpNotificationBuilder(
-                                mContext,
-                                DEVICES_WITHIN_REACH_CHANNEL_ID,
-                                /* largeIcon= */ true)
-                                .setIsDevice(true)
-                                // Tapping does not dismiss this.
-                                .setSmallIcon(Icon.createWithResource(
-                                        mResources.getResourcesContext(),
-                                        R.drawable.quantum_ic_devices_other_vd_theme_24)))
-                                .setLargeIcon(IconUtils.addWhiteCircleBackground(
-                                        mResources.getResourcesContext(), item.getIcon()))
-                                // Dismissible.
-                                .setOngoing(false)
-                                // Timestamp is not relevant, hide it.
-                                .setShowWhen(false)
-                                .setColor(mResources.getColor(R.color.discovery_activity_accent))
-                                .setLocalOnly(true)
-                                // don't show these notifications on wear devices
-                                .setAutoCancel(true);
-
-        return builder;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/notification/LargeHeadsUpNotificationBuilder.java b/nearby/service/java/com/android/server/nearby/fastpair/notification/LargeHeadsUpNotificationBuilder.java
deleted file mode 100644
index ec41d76..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/notification/LargeHeadsUpNotificationBuilder.java
+++ /dev/null
@@ -1,188 +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.server.nearby.fastpair.notification;
-
-import static com.android.server.nearby.fastpair.Constant.TAG;
-
-import android.annotation.LayoutRes;
-import android.annotation.Nullable;
-import android.app.Notification;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.util.Log;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.RemoteViews;
-
-import com.android.nearby.halfsheet.R;
-
-/** Wrapper class for creating larger heads up notifications. */
-public class LargeHeadsUpNotificationBuilder extends FastPairNotificationBuilder {
-    private final boolean mLargeIcon;
-    private final RemoteViews mNotification;
-    private final RemoteViews mNotificationCollapsed;
-
-    @Nullable private Runnable mLargeIconAction;
-    @Nullable private Runnable mProgressAction;
-
-    public LargeHeadsUpNotificationBuilder(Context context, String channelId, boolean largeIcon) {
-        super(context, channelId);
-
-        this.mLargeIcon = largeIcon;
-        this.mNotification = getRemoteViews(R.layout.fast_pair_heads_up_notification);
-        this.mNotificationCollapsed = getRemoteViews(R.layout.fast_pair_heads_up_notification);
-
-        if (mNotification != null) {
-            mNotificationCollapsed.setViewVisibility(android.R.id.secondaryProgress, View.GONE);
-        }
-    }
-
-    /**
-     * Create a new RemoteViews object that will display the views contained
-     * fast_pair_heads_up_notification layout.
-     */
-    @Nullable
-    public RemoteViews getRemoteViews(@LayoutRes int layoutId) {
-        return new RemoteViews(mPackageName, layoutId);
-    }
-
-    @Override
-    public Notification.Builder setContentTitle(@Nullable CharSequence title) {
-        if (mNotification != null) {
-            mNotification.setTextViewText(android.R.id.title, title);
-        }
-        if (mNotificationCollapsed != null) {
-            mNotificationCollapsed.setTextViewText(android.R.id.title, title);
-            // Collapsed mode does not need additional lines.
-            mNotificationCollapsed.setInt(android.R.id.title, "setMaxLines", 1);
-        }
-        return super.setContentTitle(title);
-    }
-
-    @Override
-    public Notification.Builder setContentText(@Nullable CharSequence text) {
-        if (mNotification != null) {
-            mNotification.setTextViewText(android.R.id.text1, text);
-        }
-        if (mNotificationCollapsed != null) {
-            mNotificationCollapsed.setTextViewText(android.R.id.text1, text);
-            // Collapsed mode does not need additional lines.
-            mNotificationCollapsed.setInt(android.R.id.text1, "setMaxLines", 1);
-        }
-
-        return super.setContentText(text);
-    }
-
-    @Override
-    public Notification.Builder setSubText(@Nullable CharSequence subText) {
-        if (mNotification != null) {
-            mNotification.setTextViewText(android.R.id.text2, subText);
-        }
-        if (mNotificationCollapsed != null) {
-            mNotificationCollapsed.setTextViewText(android.R.id.text2, subText);
-        }
-        return super.setSubText(subText);
-    }
-
-    @Override
-    public Notification.Builder setLargeIcon(@Nullable Bitmap bitmap) {
-        RemoteViews image =
-                getRemoteViews(
-                        useLargeIcon()
-                                ? R.layout.fast_pair_heads_up_notification_large_image
-                                : R.layout.fast_pair_heads_up_notification_small_image);
-        if (image == null) {
-            return super.setLargeIcon(bitmap);
-        }
-        image.setImageViewBitmap(android.R.id.icon, bitmap);
-
-        if (mNotification != null) {
-            mNotification.removeAllViews(android.R.id.icon1);
-            mNotification.addView(android.R.id.icon1, image);
-        }
-        if (mNotificationCollapsed != null) {
-            mNotificationCollapsed.removeAllViews(android.R.id.icon1);
-            mNotificationCollapsed.addView(android.R.id.icon1, image);
-            // In Android S, if super.setLargeIcon() is called, there will be an extra icon on
-            // top-right.
-            // But we still need to store this setting for the default UI when something wrong.
-            mLargeIconAction = () -> super.setLargeIcon(bitmap);
-            return this;
-        }
-        return super.setLargeIcon(bitmap);
-    }
-
-    @Override
-    public Notification.Builder setProgress(int max, int progress, boolean indeterminate) {
-        if (mNotification != null) {
-            mNotification.setViewVisibility(android.R.id.secondaryProgress, View.VISIBLE);
-            mNotification.setProgressBar(android.R.id.progress, max, progress, indeterminate);
-        }
-        if (mNotificationCollapsed != null) {
-            mNotificationCollapsed.setViewVisibility(android.R.id.secondaryProgress, View.VISIBLE);
-            mNotificationCollapsed.setProgressBar(android.R.id.progress, max, progress,
-                    indeterminate);
-            // In Android S, if super.setProgress() is called, there will be an extra progress bar.
-            // But we still need to store this setting for the default UI when something wrong.
-            mProgressAction = () -> super.setProgress(max, progress, indeterminate);
-            return this;
-        }
-        return super.setProgress(max, progress, indeterminate);
-    }
-
-    @Override
-    public Notification build() {
-        if (mNotification != null) {
-            boolean buildSuccess = false;
-            try {
-                // Attempt to apply the remote views. This verifies that all of the resources are
-                // correctly available.
-                // If it fails, fall back to a non-custom notification.
-                mNotification.apply(mContext, new LinearLayout(mContext));
-                if (mNotificationCollapsed != null) {
-                    mNotificationCollapsed.apply(mContext, new LinearLayout(mContext));
-                }
-                buildSuccess = true;
-            } catch (Resources.NotFoundException e) {
-                Log.w(TAG, "Failed to build notification, not setting custom view.", e);
-            }
-
-            if (buildSuccess) {
-                if (mNotificationCollapsed != null) {
-                    setStyle(new Notification.DecoratedCustomViewStyle());
-                    setCustomContentView(mNotificationCollapsed);
-                    setCustomBigContentView(mNotification);
-                } else {
-                    setCustomHeadsUpContentView(mNotification);
-                }
-            } else {
-                if (mLargeIconAction != null) {
-                    mLargeIconAction.run();
-                }
-                if (mProgressAction != null) {
-                    mProgressAction.run();
-                }
-            }
-        }
-        return super.build();
-    }
-
-    private boolean useLargeIcon() {
-        return mLargeIcon;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandler.java
deleted file mode 100644
index e56f1ea..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandler.java
+++ /dev/null
@@ -1,113 +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.server.nearby.fastpair.pairinghandler;
-
-
-import android.annotation.Nullable;
-import android.bluetooth.BluetoothDevice;
-import android.content.Context;
-
-import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-import com.android.server.nearby.intdefs.NearbyEventIntDefs;
-
-/** Pairing progress handler that handle pairing come from half sheet. */
-public final class HalfSheetPairingProgressHandler extends PairingProgressHandlerBase {
-
-    private final FastPairHalfSheetManager mFastPairHalfSheetManager;
-    private final boolean mIsSubsequentPair;
-    private final DiscoveryItem mItemResurface;
-
-    HalfSheetPairingProgressHandler(
-            Context context,
-            DiscoveryItem item,
-            @Nullable String companionApp,
-            @Nullable byte[] accountKey) {
-        super(context, item);
-        this.mFastPairHalfSheetManager = Locator.get(context, FastPairHalfSheetManager.class);
-        this.mIsSubsequentPair =
-                item.getAuthenticationPublicKeySecp256R1() != null && accountKey != null;
-        this.mItemResurface = item;
-    }
-
-    @Override
-    protected int getPairStartEventCode() {
-        return mIsSubsequentPair ? NearbyEventIntDefs.EventCode.SUBSEQUENT_PAIR_START
-                : NearbyEventIntDefs.EventCode.MAGIC_PAIR_START;
-    }
-
-    @Override
-    protected int getPairEndEventCode() {
-        return mIsSubsequentPair ? NearbyEventIntDefs.EventCode.SUBSEQUENT_PAIR_END
-                : NearbyEventIntDefs.EventCode.MAGIC_PAIR_END;
-    }
-
-    @Override
-    public void onPairingStarted() {
-        super.onPairingStarted();
-        // Half sheet is not in the foreground reshow half sheet, also avoid showing HalfSheet on TV
-        if (!mFastPairHalfSheetManager.getHalfSheetForeground()) {
-            mFastPairHalfSheetManager.showPairingHalfSheet(mItemResurface);
-        }
-        mFastPairHalfSheetManager.reportActivelyPairing();
-        mFastPairHalfSheetManager.disableDismissRunnable();
-    }
-
-    @Override
-    public void onHandlePasskeyConfirmation(BluetoothDevice device, int passkey) {
-        super.onHandlePasskeyConfirmation(device, passkey);
-        mFastPairHalfSheetManager.showPasskeyConfirmation(device, passkey);
-    }
-
-    @Nullable
-    @Override
-    public String onPairedCallbackCalled(
-            FastPairConnection connection,
-            byte[] accountKey,
-            FootprintsDeviceManager footprints,
-            String address) {
-        String deviceName = super.onPairedCallbackCalled(connection, accountKey,
-                footprints, address);
-        mFastPairHalfSheetManager.showPairingSuccessHalfSheet(address);
-        mFastPairHalfSheetManager.disableDismissRunnable();
-        return deviceName;
-    }
-
-    @Override
-    public void onPairingFailed(Throwable throwable) {
-        super.onPairingFailed(throwable);
-        mFastPairHalfSheetManager.disableDismissRunnable();
-        mFastPairHalfSheetManager.showPairingFailed();
-        mFastPairHalfSheetManager.notifyPairingProcessDone(
-                /* success= */ false, /* publicAddress= */ null, mItem);
-        // fix auto rebond issue
-        mFastPairHalfSheetManager.destroyBluetoothPairController();
-    }
-
-    @Override
-    public void onPairingSuccess(String address) {
-        super.onPairingSuccess(address);
-        mFastPairHalfSheetManager.disableDismissRunnable();
-        mFastPairHalfSheetManager
-                .notifyPairingProcessDone(/* success= */ true, address, mItem);
-        mFastPairHalfSheetManager.destroyBluetoothPairController();
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java
deleted file mode 100644
index 5317673..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java
+++ /dev/null
@@ -1,129 +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.server.nearby.fastpair.pairinghandler;
-
-import android.annotation.Nullable;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothManager;
-import android.content.Context;
-import android.util.Log;
-
-import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
-import com.android.server.nearby.intdefs.NearbyEventIntDefs;
-
-/** Pairing progress handler for pairing coming from notifications. */
-@SuppressWarnings("nullness")
-public class NotificationPairingProgressHandler extends PairingProgressHandlerBase {
-    private final FastPairNotificationManager mFastPairNotificationManager;
-    @Nullable
-    private final String mCompanionApp;
-    @Nullable
-    private final byte[] mAccountKey;
-    private final boolean mIsSubsequentPair;
-
-    NotificationPairingProgressHandler(
-            Context context,
-            DiscoveryItem item,
-            @Nullable String companionApp,
-            @Nullable byte[] accountKey,
-            FastPairNotificationManager mFastPairNotificationManager) {
-        super(context, item);
-        this.mFastPairNotificationManager = mFastPairNotificationManager;
-        this.mCompanionApp = companionApp;
-        this.mAccountKey = accountKey;
-        this.mIsSubsequentPair =
-                item.getAuthenticationPublicKeySecp256R1() != null && accountKey != null;
-    }
-
-    @Override
-    public int getPairStartEventCode() {
-        return mIsSubsequentPair ? NearbyEventIntDefs.EventCode.SUBSEQUENT_PAIR_START
-                : NearbyEventIntDefs.EventCode.MAGIC_PAIR_START;
-    }
-
-    @Override
-    public int getPairEndEventCode() {
-        return mIsSubsequentPair ? NearbyEventIntDefs.EventCode.SUBSEQUENT_PAIR_END
-                : NearbyEventIntDefs.EventCode.MAGIC_PAIR_END;
-    }
-
-    @Override
-    public void onReadyToPair() {
-        super.onReadyToPair();
-        mFastPairNotificationManager.showConnectingNotification(mItem);
-    }
-
-    @Override
-    public String onPairedCallbackCalled(
-            FastPairConnection connection,
-            byte[] accountKey,
-            FootprintsDeviceManager footprints,
-            String address) {
-        String deviceName = super.onPairedCallbackCalled(connection, accountKey, footprints,
-                address);
-        int batteryLevel = -1;
-
-        BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
-        BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
-        if (address != null && bluetoothAdapter != null) {
-            batteryLevel = bluetoothAdapter.getRemoteDevice(address).getBatteryLevel();
-        } else {
-            Log.v(TAG, "onPairedCallbackCalled getBatteryLevel failed");
-        }
-        mFastPairNotificationManager
-                .showPairingSucceededNotification(mItem, batteryLevel, deviceName);
-        return deviceName;
-    }
-
-    @Override
-    public void onPairingFailed(Throwable throwable) {
-        super.onPairingFailed(throwable);
-        mFastPairNotificationManager.showPairingFailedNotification(mItem, mAccountKey);
-        mFastPairNotificationManager.notifyPairingProcessDone(
-                /* success= */ false,
-                /* forceNotify= */ false,
-                /* privateAddress= */ mItem.getMacAddress(),
-                /* publicAddress= */ null);
-    }
-
-    @Override
-    public void onPairingSuccess(String address) {
-        super.onPairingSuccess(address);
-        int batteryLevel = -1;
-
-        BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
-        BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
-        String deviceName = null;
-        if (address != null && bluetoothAdapter != null) {
-            deviceName = bluetoothAdapter.getRemoteDevice(address).getName();
-            batteryLevel = bluetoothAdapter.getRemoteDevice(address).getBatteryLevel();
-        } else {
-            Log.v(TAG, "onPairedCallbackCalled getBatteryLevel failed");
-        }
-        mFastPairNotificationManager
-                .showPairingSucceededNotification(mItem, batteryLevel, deviceName);
-        mFastPairNotificationManager.notifyPairingProcessDone(
-                /* success= */ true,
-                /* forceNotify= */ false,
-                /* privateAddress= */ mItem.getMacAddress(),
-                /* publicAddress= */ address);
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
deleted file mode 100644
index 6f2dc40..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
+++ /dev/null
@@ -1,206 +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.server.nearby.fastpair.pairinghandler;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
-import static com.android.server.nearby.fastpair.FastPairManager.isThroughFastPair2InitialPairing;
-
-import android.annotation.Nullable;
-import android.bluetooth.BluetoothDevice;
-import android.content.Context;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
-import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
-import com.android.server.nearby.intdefs.FastPairEventIntDefs;
-
-/** Base class for pairing progress handler. */
-public abstract class PairingProgressHandlerBase {
-    protected static final String TAG = "FPPairingHandler";
-    protected final Context mContext;
-    protected final DiscoveryItem mItem;
-    @Nullable
-    private FastPairEventIntDefs.ErrorCode mRescueFromError;
-
-    protected abstract int getPairStartEventCode();
-
-    protected abstract int getPairEndEventCode();
-
-    protected PairingProgressHandlerBase(Context context, DiscoveryItem item) {
-        this.mContext = context;
-        this.mItem = item;
-    }
-
-    /**
-     * Pairing progress init function.
-     */
-    public static PairingProgressHandlerBase create(
-            Context context,
-            DiscoveryItem item,
-            @Nullable String companionApp,
-            @Nullable byte[] accountKey,
-            FootprintsDeviceManager footprints,
-            FastPairNotificationManager notificationManager,
-            FastPairHalfSheetManager fastPairHalfSheetManager,
-            boolean isRetroactivePair) {
-        PairingProgressHandlerBase pairingProgressHandlerBase;
-        // Disable half sheet on subsequent pairing
-        if (item.getAuthenticationPublicKeySecp256R1() != null
-                && accountKey != null) {
-            // Subsequent pairing
-            pairingProgressHandlerBase =
-                    new NotificationPairingProgressHandler(
-                            context, item, companionApp, accountKey, notificationManager);
-        } else {
-            pairingProgressHandlerBase =
-                    new HalfSheetPairingProgressHandler(context, item, companionApp, accountKey);
-        }
-
-        Log.v(TAG, "PairingProgressHandler:Create " + item.getMacAddress() + " for pairing");
-        return pairingProgressHandlerBase;
-    }
-
-    /**
-     * Function calls when pairing start.
-     */
-    public void onPairingStarted() {
-        Log.v(TAG, "PairingProgressHandler:onPairingStarted");
-    }
-
-    /**
-     * Waits for screen to unlock.
-     */
-    public void onWaitForScreenUnlock() {
-        Log.v(TAG, "PairingProgressHandler:onWaitForScreenUnlock");
-    }
-
-    /**
-     * Function calls when screen unlock.
-     */
-    public void onScreenUnlocked() {
-        Log.v(TAG, "PairingProgressHandler:onScreenUnlocked");
-    }
-
-    /**
-     * Calls when the handler is ready to pair.
-     */
-    public void onReadyToPair() {
-        Log.v(TAG, "PairingProgressHandler:onReadyToPair");
-    }
-
-    /**
-     * Helps to set up pairing preference.
-     */
-    public void onSetupPreferencesBuilder(Preferences.Builder builder) {
-        Log.v(TAG, "PairingProgressHandler:onSetupPreferencesBuilder");
-    }
-
-    /**
-     * Calls when pairing setup complete.
-     */
-    public void onPairingSetupCompleted() {
-        Log.v(TAG, "PairingProgressHandler:onPairingSetupCompleted");
-    }
-
-    /** Called while pairing if needs to handle the passkey confirmation by Ui. */
-    public void onHandlePasskeyConfirmation(BluetoothDevice device, int passkey) {
-        Log.v(TAG, "PairingProgressHandler:onHandlePasskeyConfirmation");
-    }
-
-    /**
-     * In this callback, we know if it is a real initial pairing by existing account key, and do
-     * following things:
-     * <li>1, optIn footprint for initial pairing.
-     * <li>2, write the device name to provider
-     * <li>2.1, generate default personalized name for initial pairing or get the personalized name
-     * from footprint for subsequent pairing.
-     * <li>2.2, set alias name for the bluetooth device.
-     * <li>2.3, update the device name for connection to write into provider for initial pair.
-     * <li>3, suppress battery notifications until oobe finishes.
-     *
-     * @return display name of the pairing device
-     */
-    @Nullable
-    public String onPairedCallbackCalled(
-            FastPairConnection connection,
-            byte[] accountKey,
-            FootprintsDeviceManager footprints,
-            String address) {
-        Log.v(TAG,
-                "PairingProgressHandler:onPairedCallbackCalled with address: "
-                        + address);
-
-        byte[] existingAccountKey = connection.getExistingAccountKey();
-        optInFootprintsForInitialPairing(footprints, mItem, accountKey, existingAccountKey);
-        // Add support for naming the device
-        return null;
-    }
-
-    /**
-     * Gets the related info from db use account key.
-     */
-    @Nullable
-    public byte[] getKeyForLocalCache(
-            byte[] accountKey, FastPairConnection connection,
-            FastPairConnection.SharedSecret sharedSecret) {
-        Log.v(TAG, "PairingProgressHandler:getKeyForLocalCache");
-        return accountKey != null ? accountKey : connection.getExistingAccountKey();
-    }
-
-    /**
-     * Function handles pairing fail.
-     */
-    public void onPairingFailed(Throwable throwable) {
-        Log.w(TAG, "PairingProgressHandler:onPairingFailed");
-    }
-
-    /**
-     * Function handles pairing success.
-     */
-    public void onPairingSuccess(String address) {
-        Log.v(TAG, "PairingProgressHandler:onPairingSuccess with address: "
-                + maskBluetoothAddress(address));
-    }
-
-    @VisibleForTesting
-    static void optInFootprintsForInitialPairing(
-            FootprintsDeviceManager footprints,
-            DiscoveryItem item,
-            byte[] accountKey,
-            @Nullable byte[] existingAccountKey) {
-        if (isThroughFastPair2InitialPairing(item, accountKey) && existingAccountKey == null) {
-            // enable the save to footprint
-            Log.v(TAG, "footprint should call opt in here");
-        }
-    }
-
-    /**
-     * Returns {@code true} if the PairingProgressHandler is running at the background.
-     *
-     * <p>In order to keep the following status notification shows as a heads up, we must wait for
-     * the screen unlocked to continue.
-     */
-    public boolean skipWaitingScreenUnlock() {
-        return false;
-    }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/intdefs/FastPairEventIntDefs.java b/nearby/service/java/com/android/server/nearby/intdefs/FastPairEventIntDefs.java
deleted file mode 100644
index 8bb7980..0000000
--- a/nearby/service/java/com/android/server/nearby/intdefs/FastPairEventIntDefs.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright 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.server.nearby.intdefs;
-
-import androidx.annotation.IntDef;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/** Holds integer definitions for FastPair. */
-public class FastPairEventIntDefs {
-
-    /** Fast Pair Bond State. */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(
-            value = {
-                    BondState.UNKNOWN_BOND_STATE,
-                    BondState.NONE,
-                    BondState.BONDING,
-                    BondState.BONDED,
-            })
-    public @interface BondState {
-        int UNKNOWN_BOND_STATE = 0;
-        int NONE = 10;
-        int BONDING = 11;
-        int BONDED = 12;
-    }
-
-    /** Fast Pair error code. */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(
-            value = {
-                    ErrorCode.UNKNOWN_ERROR_CODE,
-                    ErrorCode.OTHER_ERROR,
-                    ErrorCode.TIMEOUT,
-                    ErrorCode.INTERRUPTED,
-                    ErrorCode.REFLECTIVE_OPERATION_EXCEPTION,
-                    ErrorCode.EXECUTION_EXCEPTION,
-                    ErrorCode.PARSE_EXCEPTION,
-                    ErrorCode.MDH_REMOTE_EXCEPTION,
-                    ErrorCode.SUCCESS_RETRY_GATT_ERROR,
-                    ErrorCode.SUCCESS_RETRY_GATT_TIMEOUT,
-                    ErrorCode.SUCCESS_RETRY_SECRET_HANDSHAKE_ERROR,
-                    ErrorCode.SUCCESS_RETRY_SECRET_HANDSHAKE_TIMEOUT,
-                    ErrorCode.SUCCESS_SECRET_HANDSHAKE_RECONNECT,
-                    ErrorCode.SUCCESS_ADDRESS_ROTATE,
-                    ErrorCode.SUCCESS_SIGNAL_LOST,
-            })
-    public @interface ErrorCode {
-        int UNKNOWN_ERROR_CODE = 0;
-
-        // Check the other fields for a more specific error code.
-        int OTHER_ERROR = 1;
-
-        // The operation timed out.
-        int TIMEOUT = 2;
-
-        // The thread was interrupted.
-        int INTERRUPTED = 3;
-
-        // Some reflective call failed (should never happen).
-        int REFLECTIVE_OPERATION_EXCEPTION = 4;
-
-        // A Future threw an exception (should never happen).
-        int EXECUTION_EXCEPTION = 5;
-
-        // Parsing something (e.g. BR/EDR Handover data) failed.
-        int PARSE_EXCEPTION = 6;
-
-        // A failure at MDH.
-        int MDH_REMOTE_EXCEPTION = 7;
-
-        // For errors on GATT connection and retry success
-        int SUCCESS_RETRY_GATT_ERROR = 8;
-
-        // For timeout on GATT connection and retry success
-        int SUCCESS_RETRY_GATT_TIMEOUT = 9;
-
-        // For errors on secret handshake and retry success
-        int SUCCESS_RETRY_SECRET_HANDSHAKE_ERROR = 10;
-
-        // For timeout on secret handshake and retry success
-        int SUCCESS_RETRY_SECRET_HANDSHAKE_TIMEOUT = 11;
-
-        // For secret handshake fail and restart GATT connection success
-        int SUCCESS_SECRET_HANDSHAKE_RECONNECT = 12;
-
-        // For address rotate and retry with new address success
-        int SUCCESS_ADDRESS_ROTATE = 13;
-
-        // For signal lost and retry with old address still success
-        int SUCCESS_SIGNAL_LOST = 14;
-    }
-
-    /** Fast Pair BrEdrHandover Error Code. */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(
-            value = {
-                    BrEdrHandoverErrorCode.UNKNOWN_BR_EDR_HANDOVER_ERROR_CODE,
-                    BrEdrHandoverErrorCode.CONTROL_POINT_RESULT_CODE_NOT_SUCCESS,
-                    BrEdrHandoverErrorCode.BLUETOOTH_MAC_INVALID,
-                    BrEdrHandoverErrorCode.TRANSPORT_BLOCK_INVALID,
-            })
-    public @interface BrEdrHandoverErrorCode {
-        int UNKNOWN_BR_EDR_HANDOVER_ERROR_CODE = 0;
-        int CONTROL_POINT_RESULT_CODE_NOT_SUCCESS = 1;
-        int BLUETOOTH_MAC_INVALID = 2;
-        int TRANSPORT_BLOCK_INVALID = 3;
-    }
-
-    /** Fast Pair CreateBound Error Code. */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(
-            value = {
-                    CreateBondErrorCode.UNKNOWN_BOND_ERROR_CODE,
-                    CreateBondErrorCode.BOND_BROKEN,
-                    CreateBondErrorCode.POSSIBLE_MITM,
-                    CreateBondErrorCode.NO_PERMISSION,
-                    CreateBondErrorCode.INCORRECT_VARIANT,
-                    CreateBondErrorCode.FAILED_BUT_ALREADY_RECEIVE_PASS_KEY,
-            })
-    public @interface CreateBondErrorCode {
-        int UNKNOWN_BOND_ERROR_CODE = 0;
-        int BOND_BROKEN = 1;
-        int POSSIBLE_MITM = 2;
-        int NO_PERMISSION = 3;
-        int INCORRECT_VARIANT = 4;
-        int FAILED_BUT_ALREADY_RECEIVE_PASS_KEY = 5;
-    }
-
-    /** Fast Pair Connect Error Code. */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(
-            value = {
-                    ConnectErrorCode.UNKNOWN_CONNECT_ERROR_CODE,
-                    ConnectErrorCode.UNSUPPORTED_PROFILE,
-                    ConnectErrorCode.GET_PROFILE_PROXY_FAILED,
-                    ConnectErrorCode.DISCONNECTED,
-                    ConnectErrorCode.LINK_KEY_CLEARED,
-                    ConnectErrorCode.FAIL_TO_DISCOVERY,
-                    ConnectErrorCode.DISCOVERY_NOT_FINISHED,
-            })
-    public @interface ConnectErrorCode {
-        int UNKNOWN_CONNECT_ERROR_CODE = 0;
-        int UNSUPPORTED_PROFILE = 1;
-        int GET_PROFILE_PROXY_FAILED = 2;
-        int DISCONNECTED = 3;
-        int LINK_KEY_CLEARED = 4;
-        int FAIL_TO_DISCOVERY = 5;
-        int DISCOVERY_NOT_FINISHED = 6;
-    }
-
-    private FastPairEventIntDefs() {}
-}
diff --git a/nearby/service/java/com/android/server/nearby/intdefs/NearbyEventIntDefs.java b/nearby/service/java/com/android/server/nearby/intdefs/NearbyEventIntDefs.java
deleted file mode 100644
index 91bf49a..0000000
--- a/nearby/service/java/com/android/server/nearby/intdefs/NearbyEventIntDefs.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright 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.server.nearby.intdefs;
-
-import androidx.annotation.IntDef;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/** Holds integer definitions for NearbyEvent. */
-public class NearbyEventIntDefs {
-
-    /** NearbyEvent Code. */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(
-            value = {
-                    EventCode.UNKNOWN_EVENT_TYPE,
-                    EventCode.MAGIC_PAIR_START,
-                    EventCode.WAIT_FOR_SCREEN_UNLOCK,
-                    EventCode.GATT_CONNECT,
-                    EventCode.BR_EDR_HANDOVER_WRITE_CONTROL_POINT_REQUEST,
-                    EventCode.BR_EDR_HANDOVER_READ_BLUETOOTH_MAC,
-                    EventCode.BR_EDR_HANDOVER_READ_TRANSPORT_BLOCK,
-                    EventCode.GET_PROFILES_VIA_SDP,
-                    EventCode.DISCOVER_DEVICE,
-                    EventCode.CANCEL_DISCOVERY,
-                    EventCode.REMOVE_BOND,
-                    EventCode.CANCEL_BOND,
-                    EventCode.CREATE_BOND,
-                    EventCode.CONNECT_PROFILE,
-                    EventCode.DISABLE_BLUETOOTH,
-                    EventCode.ENABLE_BLUETOOTH,
-                    EventCode.MAGIC_PAIR_END,
-                    EventCode.SECRET_HANDSHAKE,
-                    EventCode.WRITE_ACCOUNT_KEY,
-                    EventCode.WRITE_TO_FOOTPRINTS,
-                    EventCode.PASSKEY_EXCHANGE,
-                    EventCode.DEVICE_RECOGNIZED,
-                    EventCode.GET_LOCAL_PUBLIC_ADDRESS,
-                    EventCode.DIRECTLY_CONNECTED_TO_PROFILE,
-                    EventCode.DEVICE_ALIAS_CHANGED,
-                    EventCode.WRITE_DEVICE_NAME,
-                    EventCode.UPDATE_PROVIDER_NAME_START,
-                    EventCode.UPDATE_PROVIDER_NAME_END,
-                    EventCode.READ_FIRMWARE_VERSION,
-                    EventCode.RETROACTIVE_PAIR_START,
-                    EventCode.RETROACTIVE_PAIR_END,
-                    EventCode.SUBSEQUENT_PAIR_START,
-                    EventCode.SUBSEQUENT_PAIR_END,
-                    EventCode.BISTO_PAIR_START,
-                    EventCode.BISTO_PAIR_END,
-                    EventCode.REMOTE_PAIR_START,
-                    EventCode.REMOTE_PAIR_END,
-                    EventCode.BEFORE_CREATE_BOND,
-                    EventCode.BEFORE_CREATE_BOND_BONDING,
-                    EventCode.BEFORE_CREATE_BOND_BONDED,
-                    EventCode.BEFORE_CONNECT_PROFILE,
-                    EventCode.HANDLE_PAIRING_REQUEST,
-                    EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION,
-                    EventCode.GATT_CONNECTION_AND_SECRET_HANDSHAKE,
-                    EventCode.CHECK_SIGNAL_AFTER_HANDSHAKE,
-                    EventCode.RECOVER_BY_RETRY_GATT,
-                    EventCode.RECOVER_BY_RETRY_HANDSHAKE,
-                    EventCode.RECOVER_BY_RETRY_HANDSHAKE_RECONNECT,
-                    EventCode.GATT_HANDSHAKE_MANUAL_RETRY_ATTEMPTS,
-                    EventCode.PAIR_WITH_CACHED_MODEL_ID,
-                    EventCode.DIRECTLY_CONNECT_PROFILE_WITH_CACHED_ADDRESS,
-                    EventCode.PAIR_WITH_NEW_MODEL,
-            })
-    public @interface EventCode {
-        int UNKNOWN_EVENT_TYPE = 0;
-
-        // Codes for Magic Pair.
-        // Starting at 1000 to not conflict with other existing codes (e.g.
-        // DiscoveryEvent) that may be migrated to become official Event Codes.
-        int MAGIC_PAIR_START = 1010;
-        int WAIT_FOR_SCREEN_UNLOCK = 1020;
-        int GATT_CONNECT = 1030;
-        int BR_EDR_HANDOVER_WRITE_CONTROL_POINT_REQUEST = 1040;
-        int BR_EDR_HANDOVER_READ_BLUETOOTH_MAC = 1050;
-        int BR_EDR_HANDOVER_READ_TRANSPORT_BLOCK = 1060;
-        int GET_PROFILES_VIA_SDP = 1070;
-        int DISCOVER_DEVICE = 1080;
-        int CANCEL_DISCOVERY = 1090;
-        int REMOVE_BOND = 1100;
-        int CANCEL_BOND = 1110;
-        int CREATE_BOND = 1120;
-        int CONNECT_PROFILE = 1130;
-        int DISABLE_BLUETOOTH = 1140;
-        int ENABLE_BLUETOOTH = 1150;
-        int MAGIC_PAIR_END = 1160;
-        int SECRET_HANDSHAKE = 1170;
-        int WRITE_ACCOUNT_KEY = 1180;
-        int WRITE_TO_FOOTPRINTS = 1190;
-        int PASSKEY_EXCHANGE = 1200;
-        int DEVICE_RECOGNIZED = 1210;
-        int GET_LOCAL_PUBLIC_ADDRESS = 1220;
-        int DIRECTLY_CONNECTED_TO_PROFILE = 1230;
-        int DEVICE_ALIAS_CHANGED = 1240;
-        int WRITE_DEVICE_NAME = 1250;
-        int UPDATE_PROVIDER_NAME_START = 1260;
-        int UPDATE_PROVIDER_NAME_END = 1270;
-        int READ_FIRMWARE_VERSION = 1280;
-        int RETROACTIVE_PAIR_START = 1290;
-        int RETROACTIVE_PAIR_END = 1300;
-        int SUBSEQUENT_PAIR_START = 1310;
-        int SUBSEQUENT_PAIR_END = 1320;
-        int BISTO_PAIR_START = 1330;
-        int BISTO_PAIR_END = 1340;
-        int REMOTE_PAIR_START = 1350;
-        int REMOTE_PAIR_END = 1360;
-        int BEFORE_CREATE_BOND = 1370;
-        int BEFORE_CREATE_BOND_BONDING = 1380;
-        int BEFORE_CREATE_BOND_BONDED = 1390;
-        int BEFORE_CONNECT_PROFILE = 1400;
-        int HANDLE_PAIRING_REQUEST = 1410;
-        int SECRET_HANDSHAKE_GATT_COMMUNICATION = 1420;
-        int GATT_CONNECTION_AND_SECRET_HANDSHAKE = 1430;
-        int CHECK_SIGNAL_AFTER_HANDSHAKE = 1440;
-        int RECOVER_BY_RETRY_GATT = 1450;
-        int RECOVER_BY_RETRY_HANDSHAKE = 1460;
-        int RECOVER_BY_RETRY_HANDSHAKE_RECONNECT = 1470;
-        int GATT_HANDSHAKE_MANUAL_RETRY_ATTEMPTS = 1480;
-        int PAIR_WITH_CACHED_MODEL_ID = 1490;
-        int DIRECTLY_CONNECT_PROFILE_WITH_CACHED_ADDRESS = 1500;
-        int PAIR_WITH_NEW_MODEL = 1510;
-    }
-
-    private NearbyEventIntDefs() {}
-}
diff --git a/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
new file mode 100644
index 0000000..365b099
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
@@ -0,0 +1,166 @@
+/*
+ * 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.nearby.managers;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.TargetApi;
+import android.hardware.bluetooth.finder.Eid;
+import android.hardware.bluetooth.finder.IBluetoothFinder;
+import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+
+import java.util.List;
+
+/** Connects to {@link IBluetoothFinder} HAL and invokes its API. */
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class BluetoothFinderManager {
+
+    private static final String HAL_INSTANCE_NAME = IBluetoothFinder.DESCRIPTOR + "/default";
+
+    private IBluetoothFinder mBluetoothFinder;
+    private IBinder.DeathRecipient mServiceDeathRecipient;
+    private final Object mLock = new Object();
+
+    private boolean initBluetoothFinderHal() {
+        final String methodStr = "initBluetoothFinderHal";
+        if (!SdkLevel.isAtLeastV()) return false;
+        synchronized (mLock) {
+            if (mBluetoothFinder != null) {
+                Log.i(TAG, "Bluetooth Finder HAL is already initialized");
+                return true;
+            }
+            try {
+                mBluetoothFinder = getServiceMockable();
+                if (mBluetoothFinder == null) {
+                    Log.e(TAG, "Unable to obtain IBluetoothFinder");
+                    return false;
+                }
+                Log.i(TAG, "Obtained IBluetoothFinder. Local ver: " + IBluetoothFinder.VERSION
+                        + ", Remote ver: " + mBluetoothFinder.getInterfaceVersion());
+
+                IBinder serviceBinder = getServiceBinderMockable();
+                if (serviceBinder == null) {
+                    Log.e(TAG, "Unable to obtain the service binder for IBluetoothFinder");
+                    return false;
+                }
+                mServiceDeathRecipient = new BluetoothFinderDeathRecipient();
+                serviceBinder.linkToDeath(mServiceDeathRecipient, /* flags= */ 0);
+
+                Log.i(TAG, "Bluetooth Finder HAL initialization was successful");
+                return true;
+            } catch (RemoteException e) {
+                handleRemoteException(e, methodStr);
+            } catch (Exception e) {
+                Log.e(TAG, methodStr + " encountered an exception: "  + e);
+            }
+            return false;
+        }
+    }
+
+    @VisibleForTesting
+    protected IBluetoothFinder getServiceMockable() {
+        return IBluetoothFinder.Stub.asInterface(
+                ServiceManager.waitForDeclaredService(HAL_INSTANCE_NAME));
+    }
+
+    @VisibleForTesting
+    protected IBinder getServiceBinderMockable() {
+        return mBluetoothFinder.asBinder();
+    }
+
+    private class BluetoothFinderDeathRecipient implements IBinder.DeathRecipient {
+        @Override
+        public void binderDied() {
+            Log.e(TAG, "BluetoothFinder service died.");
+            synchronized (mLock) {
+                mBluetoothFinder = null;
+            }
+        }
+    }
+
+    /** See comments for {@link IBluetoothFinder#sendEids(Eid[])} */
+    public void sendEids(List<PoweredOffFindingEphemeralId> eids) {
+        final String methodStr = "sendEids";
+        if (!checkHalAndLogFailure(methodStr)) return;
+        Eid[] eidArray = eids.stream().map(
+                ephmeralId -> {
+                    Eid eid = new Eid();
+                    eid.bytes = ephmeralId.bytes;
+                    return eid;
+                }).toArray(Eid[]::new);
+        try {
+            mBluetoothFinder.sendEids(eidArray);
+        } catch (RemoteException e) {
+            handleRemoteException(e, methodStr);
+        } catch (ServiceSpecificException e) {
+            handleServiceSpecificException(e, methodStr);
+        }
+    }
+
+    /** See comments for {@link IBluetoothFinder#setPoweredOffFinderMode(boolean)} */
+    public void setPoweredOffFinderMode(boolean enable) {
+        final String methodStr = "setPoweredOffMode";
+        if (!checkHalAndLogFailure(methodStr)) return;
+        try {
+            mBluetoothFinder.setPoweredOffFinderMode(enable);
+        } catch (RemoteException e) {
+            handleRemoteException(e, methodStr);
+        } catch (ServiceSpecificException e) {
+            handleServiceSpecificException(e, methodStr);
+        }
+    }
+
+    /** See comments for {@link IBluetoothFinder#getPoweredOffFinderMode()} */
+    public boolean getPoweredOffFinderMode() {
+        final String methodStr = "getPoweredOffMode";
+        if (!checkHalAndLogFailure(methodStr)) return false;
+        try {
+            return mBluetoothFinder.getPoweredOffFinderMode();
+        } catch (RemoteException e) {
+            handleRemoteException(e, methodStr);
+        } catch (ServiceSpecificException e) {
+            handleServiceSpecificException(e, methodStr);
+        }
+        return false;
+    }
+
+    private boolean checkHalAndLogFailure(String methodStr) {
+        if ((mBluetoothFinder == null) && !initBluetoothFinderHal()) {
+            Log.e(TAG, "Unable to call " + methodStr + " because IBluetoothFinder is null.");
+            return false;
+        }
+        return true;
+    }
+
+    private void handleRemoteException(RemoteException e, String methodStr) {
+        mBluetoothFinder = null;
+        Log.e(TAG, methodStr + " failed with remote exception: " + e);
+    }
+
+    private void handleServiceSpecificException(ServiceSpecificException e, String methodStr) {
+        Log.e(TAG, methodStr + " failed with service-specific exception: " + e);
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
index 024bff8..28a33fa 100644
--- a/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
@@ -22,6 +22,7 @@
 import android.nearby.BroadcastRequest;
 import android.nearby.IBroadcastListener;
 import android.nearby.PresenceBroadcastRequest;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
 
@@ -49,6 +50,10 @@
     private final NearbyConfiguration mNearbyConfiguration;
 
     private IBroadcastListener mBroadcastListener;
+    // Used with mBroadcastListener. Now we only support single client, for multi clients, a map
+    // between live binder to the information over the binder is needed.
+    // TODO: Finish multi-client logic for broadcast.
+    private BroadcastListenerDeathRecipient mDeathRecipient;
 
     public BroadcastProviderManager(Context context, Injector injector) {
         this(ForegroundThread.getExecutor(),
@@ -62,6 +67,7 @@
         mLock = new Object();
         mNearbyConfiguration = new NearbyConfiguration();
         mBroadcastListener = null;
+        mDeathRecipient = null;
     }
 
     /**
@@ -70,6 +76,15 @@
     public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
         synchronized (mLock) {
             mExecutor.execute(() -> {
+                if (listener == null) {
+                    return;
+                }
+                if (mBroadcastListener != null) {
+                    Log.i(TAG, "We do not support multi clients yet,"
+                            + " please stop previous broadcast first.");
+                    reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+                    return;
+                }
                 if (!mNearbyConfiguration.isTestAppSupported()) {
                     NearbyConfiguration configuration = new NearbyConfiguration();
                     if (!configuration.isPresenceBroadcastLegacyEnabled()) {
@@ -89,7 +104,17 @@
                     reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
                     return;
                 }
+                BroadcastListenerDeathRecipient deathRecipient =
+                        new BroadcastListenerDeathRecipient(listener);
+                try {
+                    listener.asBinder().linkToDeath(deathRecipient, 0);
+                } catch (RemoteException e) {
+                    // This binder has already died, so call the DeathRecipient as if we had
+                    // called linkToDeath in time.
+                    deathRecipient.binderDied();
+                }
                 mBroadcastListener = listener;
+                mDeathRecipient = deathRecipient;
                 mBleBroadcastProvider.start(presenceBroadcastRequest.getVersion(),
                         advertisement.toBytes(), this);
             });
@@ -113,13 +138,19 @@
      */
     public void stopBroadcast(IBroadcastListener listener) {
         synchronized (mLock) {
-            if (!mNearbyConfiguration.isTestAppSupported()
-                    && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
-                reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
-                return;
+            if (listener != null) {
+                if (!mNearbyConfiguration.isTestAppSupported()
+                        && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+                    reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+                    return;
+                }
+                if (mDeathRecipient != null) {
+                    listener.asBinder().unlinkToDeath(mDeathRecipient, 0);
+                }
             }
             mBroadcastListener = null;
-            mExecutor.execute(() -> mBleBroadcastProvider.stop());
+            mDeathRecipient = null;
+            mExecutor.execute(mBleBroadcastProvider::stop);
         }
     }
 
@@ -142,4 +173,21 @@
             Log.e(TAG, "remote exception when reporting status");
         }
     }
+
+    /**
+     * Class to make listener unregister after the binder is dead.
+     */
+    public class BroadcastListenerDeathRecipient implements IBinder.DeathRecipient {
+        public IBroadcastListener mListener;
+
+        BroadcastListenerDeathRecipient(IBroadcastListener listener) {
+            mListener = listener;
+        }
+
+        @Override
+        public void binderDied() {
+            Log.d(TAG, "Binder is dead - stop broadcast listener");
+            stopBroadcast(mListener);
+        }
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
index 0c41426..8995232 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -21,6 +21,7 @@
 import static com.android.server.nearby.NearbyService.TAG;
 
 import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -35,6 +36,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.managers.registration.DiscoveryRegistration;
 import com.android.server.nearby.provider.AbstractDiscoveryProvider;
@@ -66,6 +68,7 @@
     private final BleDiscoveryProvider mBleDiscoveryProvider;
     private final Injector mInjector;
     private final Executor mExecutor;
+    private final NearbyConfiguration mNearbyConfiguration;
 
     public DiscoveryProviderManager(Context context, Injector injector) {
         Log.v(TAG, "DiscoveryProviderManager: ");
@@ -75,6 +78,7 @@
         mChreDiscoveryProvider = new ChreDiscoveryProvider(mContext,
                 new ChreCommunication(injector, mContext, mExecutor), mExecutor);
         mInjector = injector;
+        mNearbyConfiguration = new NearbyConfiguration();
     }
 
     @VisibleForTesting
@@ -86,6 +90,7 @@
         mInjector = injector;
         mBleDiscoveryProvider = bleDiscoveryProvider;
         mChreDiscoveryProvider = chreDiscoveryProvider;
+        mNearbyConfiguration = new NearbyConfiguration();
     }
 
     private static boolean isChreOnly(Set<ScanFilter> scanFilters) {
@@ -141,6 +146,10 @@
 
     /** Called after boot completed. */
     public void init() {
+        // Register BLE only scan when Bluetooth is turned off
+        if (mNearbyConfiguration.enableBleInInit()) {
+            setBleScanEnabled();
+        }
         if (mInjector.getContextHubManager() != null) {
             mChreDiscoveryProvider.init();
         }
@@ -242,7 +251,7 @@
     @GuardedBy("mMultiplexerLock")
     private void startBleProvider(Set<ScanFilter> scanFilters) {
         if (!mBleDiscoveryProvider.getController().isStarted()) {
-            Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+            Log.d(TAG, "DiscoveryProviderManager starts BLE scanning.");
             mBleDiscoveryProvider.getController().setListener(this);
             mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
             mBleDiscoveryProvider.getController().setProviderScanFilters(
@@ -313,4 +322,29 @@
     public void onMergedRegistrationsUpdated() {
         invalidateProviderScanMode();
     }
+
+    /**
+     * Registers Nearby service to Ble scan if Bluetooth is off. (Even when Bluetooth is off)
+     * @return {@code true} when Nearby currently can scan through Bluetooth or Ble or successfully
+     * registers Nearby service to Ble scan when Blutooth is off.
+     */
+    public boolean setBleScanEnabled() {
+        BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+        if (adapter == null) {
+            Log.e(TAG, "BluetoothAdapter is null.");
+            return false;
+        }
+        if (adapter.isEnabled() || adapter.isLeEnabled()) {
+            return true;
+        }
+        if (!adapter.isBleScanAlwaysAvailable()) {
+            Log.v(TAG, "Ble always on scan is disabled.");
+            return false;
+        }
+        if (!adapter.enableBLE()) {
+            Log.e(TAG, "Failed to register Ble scan.");
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
index 4b76eba..3ef8fb7 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
@@ -22,6 +22,7 @@
 
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -38,6 +39,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.metrics.NearbyMetrics;
 import com.android.server.nearby.presence.PresenceDiscoveryResult;
@@ -69,6 +71,7 @@
     private final Context mContext;
     private final BleDiscoveryProvider mBleDiscoveryProvider;
     private final Injector mInjector;
+    private final NearbyConfiguration mNearbyConfiguration;
     @ScanRequest.ScanMode
     private int mScanMode;
     @GuardedBy("mLock")
@@ -83,6 +86,7 @@
                         mContext, new ChreCommunication(injector, mContext, executor), executor);
         mScanTypeScanListenerRecordMap = new HashMap<>();
         mInjector = injector;
+        mNearbyConfiguration = new NearbyConfiguration();
         Log.v(TAG, "DiscoveryProviderManagerLegacy: ");
     }
 
@@ -96,6 +100,7 @@
         mBleDiscoveryProvider = bleDiscoveryProvider;
         mChreDiscoveryProvider = chreDiscoveryProvider;
         mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+        mNearbyConfiguration = new NearbyConfiguration();
     }
 
     private static boolean isChreOnly(List<ScanFilter> scanFilters) {
@@ -142,18 +147,18 @@
             for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
                 ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
                 if (record == null) {
-                    Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy cannot find the scan record.");
                     continue;
                 }
                 CallerIdentity callerIdentity = record.getCallerIdentity();
                 if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
                         appOpsManager, callerIdentity)) {
-                    Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+                    Log.w(TAG, "[DiscoveryProviderManagerLegacy] scan permission revoked "
                             + "- not forwarding results");
                     try {
                         record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
                     } catch (RemoteException e) {
-                        Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+                        Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report error.", e);
                     }
                     return;
                 }
@@ -180,7 +185,7 @@
                     NearbyMetrics.logScanDeviceDiscovered(
                             record.hashCode(), record.getScanRequest(), nearbyDevice);
                 } catch (RemoteException e) {
-                    Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report onDiscovered.", e);
                 }
             }
         }
@@ -193,18 +198,18 @@
             for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
                 ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
                 if (record == null) {
-                    Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy cannot find the scan record.");
                     continue;
                 }
                 CallerIdentity callerIdentity = record.getCallerIdentity();
                 if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
                         appOpsManager, callerIdentity)) {
-                    Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+                    Log.w(TAG, "[DiscoveryProviderManagerLegacy] scan permission revoked "
                             + "- not forwarding results");
                     try {
                         record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
                     } catch (RemoteException e) {
-                        Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+                        Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report error.", e);
                     }
                     return;
                 }
@@ -212,7 +217,7 @@
                 try {
                     record.getScanListener().onError(errorCode);
                 } catch (RemoteException e) {
-                    Log.w(TAG, "DiscoveryProviderManager failed to report onError.", e);
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report onError.", e);
                 }
             }
         }
@@ -220,6 +225,10 @@
 
     /** Called after boot completed. */
     public void init() {
+        // Register BLE only scan when Bluetooth is turned off
+        if (mNearbyConfiguration.enableBleInInit()) {
+            setBleScanEnabled();
+        }
         if (mInjector.getContextHubManager() != null) {
             mChreDiscoveryProvider.init();
         }
@@ -293,10 +302,10 @@
             if (listenerBinder != null && deathRecipient != null) {
                 listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
             }
-            Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
+            Log.v(TAG, "DiscoveryProviderManagerLegacy unregistered scan listener.");
             NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
             if (mScanTypeScanListenerRecordMap.isEmpty()) {
-                Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
+                Log.v(TAG, "DiscoveryProviderManagerLegacy stops provider because there is no "
                         + "scan listener registered.");
                 stopProviders();
                 return;
@@ -306,8 +315,8 @@
 
             // Removes current highest scan mode requested and sets the next highest scan mode.
             if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
-                Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
-                        + "because the highest scan mode listener was unregistered.");
+                Log.v(TAG, "DiscoveryProviderManagerLegacy starts to find the new highest "
+                        + "scan mode because the highest scan mode listener was unregistered.");
                 @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
                 // find the next highest scan mode;
                 for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
@@ -377,7 +386,7 @@
 
     private void startBleProvider(List<ScanFilter> scanFilters) {
         if (!mBleDiscoveryProvider.getController().isStarted()) {
-            Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+            Log.d(TAG, "DiscoveryProviderManagerLegacy starts BLE scanning.");
             mBleDiscoveryProvider.getController().setListener(this);
             mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
             mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
@@ -387,7 +396,7 @@
 
     @VisibleForTesting
     void startChreProvider(List<ScanFilter> scanFilters) {
-        Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+        Log.d(TAG, "DiscoveryProviderManagerLegacy starts CHRE scanning.");
         mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
         mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
         mChreDiscoveryProvider.getController().start();
@@ -503,4 +512,29 @@
             unregisterScanListener(listener);
         }
     }
+
+    /**
+     * Registers Nearby service to Ble scan if Bluetooth is off. (Even when Bluetooth is off)
+     * @return {@code true} when Nearby currently can scan through Bluetooth or Ble or successfully
+     * registers Nearby service to Ble scan when Blutooth is off.
+     */
+    public boolean setBleScanEnabled() {
+        BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+        if (adapter == null) {
+            Log.e(TAG, "BluetoothAdapter is null.");
+            return false;
+        }
+        if (adapter.isEnabled() || adapter.isLeEnabled()) {
+            return true;
+        }
+        if (!adapter.isBleScanAlwaysAvailable()) {
+            Log.v(TAG, "Ble always on scan is disabled.");
+            return false;
+        }
+        if (!adapter.enableBLE()) {
+            Log.e(TAG, "Failed to register Ble scan.");
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
new file mode 100644
index 0000000..ac1e18f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
@@ -0,0 +1,90 @@
+/*
+ * 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.nearby.presence;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.nearby.DataElement;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.nearby.util.ArrayUtils;
+
+import java.util.Arrays;
+
+/**
+ * Data Element that indicates the presence of extended 16 bytes salt instead of 2 byte salt and to
+ * indicate which encoding scheme (MIC tag or Signature) is used for a section. If present, it must
+ * be the first DE in a section.
+ */
+public class EncryptionInfo {
+
+    @IntDef({EncodingScheme.MIC, EncodingScheme.SIGNATURE})
+    public @interface EncodingScheme {
+        int MIC = 0;
+        int SIGNATURE = 1;
+    }
+
+    // 1st byte : encryption scheme
+    // 2nd to 17th bytes: salt
+    public static final int ENCRYPTION_INFO_LENGTH = 17;
+    private static final int ENCODING_SCHEME_MASK = 0b01111000;
+    private static final int ENCODING_SCHEME_OFFSET = 3;
+    @EncodingScheme
+    private final int mEncodingScheme;
+    private final byte[] mSalt;
+
+    /**
+     * Constructs a {@link DataElement}.
+     */
+    public EncryptionInfo(@NonNull byte[] value) {
+        Preconditions.checkArgument(value.length == ENCRYPTION_INFO_LENGTH);
+        mEncodingScheme = (value[0] & ENCODING_SCHEME_MASK) >> ENCODING_SCHEME_OFFSET;
+        Preconditions.checkArgument(isValidEncodingScheme(mEncodingScheme));
+        mSalt = Arrays.copyOfRange(value, 1, ENCRYPTION_INFO_LENGTH);
+    }
+
+    private boolean isValidEncodingScheme(int scheme) {
+        return scheme == EncodingScheme.MIC || scheme == EncodingScheme.SIGNATURE;
+    }
+
+    @EncodingScheme
+    public int getEncodingScheme() {
+        return mEncodingScheme;
+    }
+
+    public byte[] getSalt() {
+        return mSalt;
+    }
+
+    /** Combines the encoding scheme and salt to a byte array
+     * that represents an {@link EncryptionInfo}.
+     */
+    @Nullable
+    public static byte[] toByte(@EncodingScheme int encodingScheme, byte[] salt) {
+        if (ArrayUtils.isEmpty(salt)) {
+            return null;
+        }
+        if (salt.length != ENCRYPTION_INFO_LENGTH - 1) {
+            return null;
+        }
+        byte schemeByte =
+                (byte) ((encodingScheme << ENCODING_SCHEME_OFFSET) & ENCODING_SCHEME_MASK);
+        return ArrayUtils.append(schemeByte, salt);
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
index 34a7514..c2304cc 100644
--- a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
@@ -16,22 +16,27 @@
 
 package com.android.server.nearby.presence;
 
+import static android.nearby.BroadcastRequest.PRESENCE_VERSION_V1;
+
 import static com.android.server.nearby.NearbyService.TAG;
+import static com.android.server.nearby.presence.EncryptionInfo.ENCRYPTION_INFO_LENGTH;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID_BYTES;
 
 import android.annotation.Nullable;
-import android.nearby.BroadcastRequest;
+import android.nearby.BroadcastRequest.BroadcastVersion;
 import android.nearby.DataElement;
+import android.nearby.DataElement.DataType;
 import android.nearby.PresenceBroadcastRequest;
 import android.nearby.PresenceCredential;
 import android.nearby.PublicCredential;
 import android.util.Log;
 
+import com.android.server.nearby.util.ArrayUtils;
 import com.android.server.nearby.util.encryption.Cryptor;
-import com.android.server.nearby.util.encryption.CryptorImpFake;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
-import com.android.server.nearby.util.encryption.CryptorImpV1;
+import com.android.server.nearby.util.encryption.CryptorMicImp;
 
 import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -51,37 +56,51 @@
  * The header contains:
  * version (3 bits) | 5 bit reserved for future use (RFU)
  */
-public class ExtendedAdvertisement extends Advertisement{
+public class ExtendedAdvertisement extends Advertisement {
 
     public static final int SALT_DATA_LENGTH = 2;
-
     static final int HEADER_LENGTH = 1;
 
     static final int IDENTITY_DATA_LENGTH = 16;
-
+    // Identity Index is always 2 .
+    // 0 is reserved, 1 is Salt or Credential Element.
+    private static final int CIPHER_START_INDEX = 2;
     private final List<DataElement> mDataElements;
+    private final byte[] mKeySeed;
 
-    private final byte[] mAuthenticityKey;
+    private final byte[] mData;
 
-    // All Data Elements including salt and identity.
-    // Each list item (byte array) is a Data Element (with its header).
-    private final List<byte[]> mCompleteDataElementsBytes;
-    // Signature generated from data elements.
-    private final byte[] mHmacTag;
+    private ExtendedAdvertisement(
+            @PresenceCredential.IdentityType int identityType,
+            byte[] identity,
+            byte[] salt,
+            byte[] keySeed,
+            List<Integer> actions,
+            List<DataElement> dataElements) {
+        this.mVersion = PRESENCE_VERSION_V1;
+        this.mIdentityType = identityType;
+        this.mIdentity = identity;
+        this.mSalt = salt;
+        this.mKeySeed = keySeed;
+        this.mDataElements = dataElements;
+        this.mActions = actions;
+        mData = toBytesInternal();
+    }
 
     /**
      * Creates an {@link ExtendedAdvertisement} from a Presence Broadcast Request.
+     *
      * @return {@link ExtendedAdvertisement} object. {@code null} when the request is illegal.
      */
     @Nullable
     public static ExtendedAdvertisement createFromRequest(PresenceBroadcastRequest request) {
-        if (request.getVersion() != BroadcastRequest.PRESENCE_VERSION_V1) {
+        if (request.getVersion() != PRESENCE_VERSION_V1) {
             Log.v(TAG, "ExtendedAdvertisement only supports V1 now.");
             return null;
         }
 
         byte[] salt = request.getSalt();
-        if (salt.length != SALT_DATA_LENGTH) {
+        if (salt.length != SALT_DATA_LENGTH && salt.length != ENCRYPTION_INFO_LENGTH - 1) {
             Log.v(TAG, "Salt does not match correct length");
             return null;
         }
@@ -94,12 +113,12 @@
         }
 
         List<Integer> actions = request.getActions();
-        if (actions.isEmpty()) {
-            Log.v(TAG, "ExtendedAdvertisement must contain at least one action");
-            return null;
-        }
-
         List<DataElement> dataElements = request.getExtendedProperties();
+        // DataElements should include actions.
+        for (int action : actions) {
+            dataElements.add(
+                    new DataElement(DataType.ACTION, new byte[]{(byte) action}));
+        }
         return new ExtendedAdvertisement(
                 request.getCredential().getIdentityType(),
                 identity,
@@ -109,149 +128,252 @@
                 dataElements);
     }
 
-    /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
-    @Nullable
-    public byte[] toBytes() {
-        ByteBuffer buffer = ByteBuffer.allocate(getLength());
-
-        // Header
-        buffer.put(ExtendedAdvertisementUtils.constructHeader(getVersion()));
-
-        // Salt
-        buffer.put(mCompleteDataElementsBytes.get(0));
-
-        // Identity
-        buffer.put(mCompleteDataElementsBytes.get(1));
-
-        List<Byte> rawDataBytes = new ArrayList<>();
-        // Data Elements (Already includes salt and identity)
-        for (int i = 2; i < mCompleteDataElementsBytes.size(); i++) {
-            byte[] dataElementBytes = mCompleteDataElementsBytes.get(i);
-            for (Byte b : dataElementBytes) {
-                rawDataBytes.add(b);
-            }
-        }
-
-        byte[] dataElements = new byte[rawDataBytes.size()];
-        for (int i = 0; i < rawDataBytes.size(); i++) {
-            dataElements[i] = rawDataBytes.get(i);
-        }
-
-        buffer.put(
-                getCryptor(/* encrypt= */ true).encrypt(dataElements, getSalt(), mAuthenticityKey));
-
-        buffer.put(mHmacTag);
-
-        return buffer.array();
-    }
-
-    /** Deserialize from bytes into an {@link ExtendedAdvertisement} object.
-     * {@code null} when there is something when parsing.
+    /**
+     * Deserialize from bytes into an {@link ExtendedAdvertisement} object.
+     * Return {@code null} when there is an error in parsing.
      */
     @Nullable
-    public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential publicCredential) {
-        @BroadcastRequest.BroadcastVersion
+    public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential sharedCredential) {
+        @BroadcastVersion
         int version = ExtendedAdvertisementUtils.getVersion(bytes);
-        if (version != PresenceBroadcastRequest.PRESENCE_VERSION_V1) {
+        if (version != PRESENCE_VERSION_V1) {
             Log.v(TAG, "ExtendedAdvertisement is used in V1 only and version is " + version);
             return null;
         }
 
-        byte[] authenticityKey = publicCredential.getAuthenticityKey();
-
-        int index = HEADER_LENGTH;
-        // Salt
-        byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
-        DataElementHeader saltHeader = DataElementHeader.fromBytes(version, saltHeaderArray);
-        if (saltHeader == null || saltHeader.getDataType() != DataElement.DataType.SALT) {
-            Log.v(TAG, "First data element has to be salt.");
+        byte[] keySeed = sharedCredential.getAuthenticityKey();
+        byte[] metadataEncryptionKeyUnsignedAdvTag = sharedCredential.getEncryptedMetadataKeyTag();
+        if (keySeed == null || metadataEncryptionKeyUnsignedAdvTag == null) {
             return null;
         }
-        index += saltHeaderArray.length;
-        byte[] salt = new byte[saltHeader.getDataLength()];
-        for (int i = 0; i < saltHeader.getDataLength(); i++) {
-            salt[i] = bytes[index++];
-        }
 
-        // Identity
+        int index = 0;
+        // Header
+        byte[] header = new byte[]{bytes[index]};
+        index += HEADER_LENGTH;
+        // Section header
+        byte[] sectionHeader = new byte[]{bytes[index]};
+        index += HEADER_LENGTH;
+        // Salt or Encryption Info
+        byte[] firstHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+        DataElementHeader firstHeader = DataElementHeader.fromBytes(version, firstHeaderArray);
+        if (firstHeader == null) {
+            Log.v(TAG, "Cannot find salt.");
+            return null;
+        }
+        @DataType int firstType = firstHeader.getDataType();
+        if (firstType != DataType.SALT && firstType != DataType.ENCRYPTION_INFO) {
+            Log.v(TAG, "First data element has to be Salt or Encryption Info.");
+            return null;
+        }
+        index += firstHeaderArray.length;
+        byte[] firstDeBytes = new byte[firstHeader.getDataLength()];
+        for (int i = 0; i < firstHeader.getDataLength(); i++) {
+            firstDeBytes[i] = bytes[index++];
+        }
+        byte[] nonce = getNonce(firstType, firstDeBytes);
+        if (nonce == null) {
+            return null;
+        }
+        byte[] saltBytes = firstType == DataType.SALT
+                ? firstDeBytes : (new EncryptionInfo(firstDeBytes)).getSalt();
+
+        // Identity header
         byte[] identityHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
         DataElementHeader identityHeader =
                 DataElementHeader.fromBytes(version, identityHeaderArray);
-        if (identityHeader == null) {
-            Log.v(TAG, "The second element has to be identity.");
+        if (identityHeader == null || identityHeader.getDataLength() != IDENTITY_DATA_LENGTH) {
+            Log.v(TAG, "The second element has to be a 16-bytes identity.");
             return null;
         }
         index += identityHeaderArray.length;
         @PresenceCredential.IdentityType int identityType =
                 toPresenceCredentialIdentityType(identityHeader.getDataType());
-        if (identityType == PresenceCredential.IDENTITY_TYPE_UNKNOWN) {
-            Log.v(TAG, "The identity type is unknown.");
+        if (identityType != PresenceCredential.IDENTITY_TYPE_PRIVATE
+                && identityType != PresenceCredential.IDENTITY_TYPE_TRUSTED) {
+            Log.v(TAG, "Only supports encrypted advertisement.");
             return null;
         }
-        byte[] encryptedIdentity = new byte[identityHeader.getDataLength()];
-        for (int i = 0; i < identityHeader.getDataLength(); i++) {
-            encryptedIdentity[i] = bytes[index++];
-        }
-        byte[] identity =
-                CryptorImpIdentityV1
-                        .getInstance().decrypt(encryptedIdentity, salt, authenticityKey);
-
-        Cryptor cryptor = getCryptor(/* encrypt= */ true);
-        byte[] encryptedDataElements =
-                new byte[bytes.length - index - cryptor.getSignatureLength()];
-        // Decrypt other data elements
-        System.arraycopy(bytes, index, encryptedDataElements, 0, encryptedDataElements.length);
-        byte[] decryptedDataElements =
-                cryptor.decrypt(encryptedDataElements, salt, authenticityKey);
-        if (decryptedDataElements == null) {
+        // Ciphertext
+        Cryptor cryptor = CryptorMicImp.getInstance();
+        byte[] ciphertext = new byte[bytes.length - index - cryptor.getSignatureLength()];
+        System.arraycopy(bytes, index, ciphertext, 0, ciphertext.length);
+        byte[] plaintext = cryptor.decrypt(ciphertext, nonce, keySeed);
+        if (plaintext == null) {
             return null;
         }
 
+        // Verification
+        // Verify the computed metadata encryption key tag
+        // First 16 bytes is metadata encryption key data
+        byte[] metadataEncryptionKey = new byte[IDENTITY_DATA_LENGTH];
+        System.arraycopy(plaintext, 0, metadataEncryptionKey, 0, IDENTITY_DATA_LENGTH);
+        // Verify metadata encryption key tag
+        byte[] computedMetadataEncryptionKeyTag =
+                CryptorMicImp.generateMetadataEncryptionKeyTag(metadataEncryptionKey,
+                        keySeed);
+        if (!Arrays.equals(computedMetadataEncryptionKeyTag, metadataEncryptionKeyUnsignedAdvTag)) {
+            Log.w(TAG,
+                    "The calculated metadata encryption key tag is different from the metadata "
+                            + "encryption key unsigned adv tag in the SharedCredential.");
+            return null;
+        }
         // Verify the computed HMAC tag is equal to HMAC tag in advertisement
-        if (cryptor.getSignatureLength() > 0) {
-            byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
-            System.arraycopy(
-                    bytes, bytes.length - cryptor.getSignatureLength(),
-                    expectedHmacTag, 0, cryptor.getSignatureLength());
-            if (!cryptor.verify(decryptedDataElements, authenticityKey, expectedHmacTag)) {
-                Log.e(TAG, "HMAC tags not match.");
-                return null;
-            }
+        byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
+        System.arraycopy(
+                bytes, bytes.length - cryptor.getSignatureLength(),
+                expectedHmacTag, 0, cryptor.getSignatureLength());
+        byte[] micInput =  ArrayUtils.concatByteArrays(
+                PRESENCE_UUID_BYTES, header, sectionHeader,
+                firstHeaderArray, firstDeBytes,
+                nonce, identityHeaderArray, ciphertext);
+        if (!cryptor.verify(micInput, keySeed, expectedHmacTag)) {
+            Log.e(TAG, "HMAC tag not match.");
+            return null;
         }
 
-        int dataElementArrayIndex = 0;
-        // Other Data Elements
-        List<Integer> actions = new ArrayList<>();
-        List<DataElement> dataElements = new ArrayList<>();
-        while (dataElementArrayIndex < decryptedDataElements.length) {
-            byte[] deHeaderArray = ExtendedAdvertisementUtils
-                    .getDataElementHeader(decryptedDataElements, dataElementArrayIndex);
-            DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
-            dataElementArrayIndex += deHeaderArray.length;
+        byte[] otherDataElements = new byte[plaintext.length - IDENTITY_DATA_LENGTH];
+        System.arraycopy(plaintext, IDENTITY_DATA_LENGTH,
+                otherDataElements, 0, otherDataElements.length);
+        List<DataElement> dataElements = getDataElementsFromBytes(version, otherDataElements);
+        if (dataElements.isEmpty()) {
+            return null;
+        }
+        List<Integer> actions = getActionsFromDataElements(dataElements);
+        if (actions == null) {
+            return null;
+        }
+        return new ExtendedAdvertisement(identityType, metadataEncryptionKey, saltBytes, keySeed,
+                actions, dataElements);
+    }
 
-            @DataElement.DataType int type = Objects.requireNonNull(deHeader).getDataType();
-            if (type == DataElement.DataType.ACTION) {
-                if (deHeader.getDataLength() != 1) {
-                    Log.v(TAG, "Action id should only 1 byte.");
+    @PresenceCredential.IdentityType
+    private static int toPresenceCredentialIdentityType(@DataType int type) {
+        switch (type) {
+            case DataType.PRIVATE_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_PRIVATE;
+            case DataType.PROVISIONED_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
+            case DataType.TRUSTED_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_TRUSTED;
+            case DataType.PUBLIC_IDENTITY:
+            default:
+                return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
+        }
+    }
+
+    @DataType
+    private static int toDataType(@PresenceCredential.IdentityType int identityType) {
+        switch (identityType) {
+            case PresenceCredential.IDENTITY_TYPE_PRIVATE:
+                return DataType.PRIVATE_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
+                return DataType.PROVISIONED_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_TRUSTED:
+                return DataType.TRUSTED_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
+            default:
+                return DataType.PUBLIC_IDENTITY;
+        }
+    }
+
+    /**
+     * Returns {@code true} if the given {@link DataType} is salt, or one of the
+     * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
+     */
+    private static boolean isSaltOrIdentity(@DataType int type) {
+        return type == DataType.SALT || type == DataType.ENCRYPTION_INFO
+                || type == DataType.PRIVATE_IDENTITY
+                || type == DataType.TRUSTED_IDENTITY
+                || type == DataType.PROVISIONED_IDENTITY
+                || type == DataType.PUBLIC_IDENTITY;
+    }
+
+    /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+    @Nullable
+    public byte[] toBytes() {
+        return mData.clone();
+    }
+
+    /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+    @Nullable
+    public byte[] toBytesInternal() {
+        int sectionLength = 0;
+        // Salt
+        DataElement saltDe;
+        byte[] nonce;
+        try {
+            switch (mSalt.length) {
+                case SALT_DATA_LENGTH:
+                    saltDe = new DataElement(DataType.SALT, mSalt);
+                    nonce = CryptorMicImp.generateAdvNonce(mSalt);
+                    break;
+                case ENCRYPTION_INFO_LENGTH - 1:
+                    saltDe = new DataElement(DataType.ENCRYPTION_INFO,
+                            EncryptionInfo.toByte(EncryptionInfo.EncodingScheme.MIC, mSalt));
+                    nonce = CryptorMicImp.generateAdvNonce(mSalt, CIPHER_START_INDEX);
+                    break;
+                default:
+                    Log.w(TAG, "Invalid salt size.");
                     return null;
-                }
-                actions.add((int) decryptedDataElements[dataElementArrayIndex++]);
-            } else {
-                if (isSaltOrIdentity(type)) {
-                    Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
-                            + " and one identity in the advertisement.");
-                    return null;
-                }
-                byte[] deData = new byte[deHeader.getDataLength()];
-                for (int i = 0; i < deHeader.getDataLength(); i++) {
-                    deData[i] = decryptedDataElements[dataElementArrayIndex++];
-                }
-                dataElements.add(new DataElement(type, deData));
             }
+        } catch (GeneralSecurityException e) {
+            Log.w(TAG, "Failed to generate the IV for encryption.", e);
+            return null;
         }
 
-        return new ExtendedAdvertisement(identityType, identity, salt, authenticityKey, actions,
-                dataElements);
+        byte[] saltOrEncryptionInfoBytes =
+                ExtendedAdvertisementUtils.convertDataElementToBytes(saltDe);
+        sectionLength += saltOrEncryptionInfoBytes.length;
+        // 16 bytes encrypted identity
+        @DataType int identityDataType = toDataType(getIdentityType());
+        byte[] identityHeaderBytes = new DataElementHeader(PRESENCE_VERSION_V1,
+                identityDataType, mIdentity.length).toBytes();
+        sectionLength += identityHeaderBytes.length;
+        final List<DataElement> dataElementList = getDataElements();
+        byte[] ciphertext = getCiphertext(nonce, dataElementList);
+        if (ciphertext == null) {
+            return null;
+        }
+        sectionLength += ciphertext.length;
+        // mic
+        sectionLength += CryptorMicImp.MIC_LENGTH;
+        mLength = sectionLength;
+        // header
+        byte header = ExtendedAdvertisementUtils.constructHeader(getVersion());
+        mLength += HEADER_LENGTH;
+        // section header
+        if (sectionLength > 255) {
+            Log.e(TAG, "A section should be shorter than 255 bytes.");
+            return null;
+        }
+        byte sectionHeader = (byte) sectionLength;
+        mLength += HEADER_LENGTH;
+
+        // generates mic
+        ByteBuffer micInputBuffer = ByteBuffer.allocate(
+                mLength + PRESENCE_UUID_BYTES.length + nonce.length - CryptorMicImp.MIC_LENGTH);
+        micInputBuffer.put(PRESENCE_UUID_BYTES);
+        micInputBuffer.put(header);
+        micInputBuffer.put(sectionHeader);
+        micInputBuffer.put(saltOrEncryptionInfoBytes);
+        micInputBuffer.put(nonce);
+        micInputBuffer.put(identityHeaderBytes);
+        micInputBuffer.put(ciphertext);
+        byte[] micInput = micInputBuffer.array();
+        byte[] mic = CryptorMicImp.getInstance().sign(micInput, mKeySeed);
+        if (mic == null) {
+            return null;
+        }
+
+        ByteBuffer buffer = ByteBuffer.allocate(mLength);
+        buffer.put(header);
+        buffer.put(sectionHeader);
+        buffer.put(saltOrEncryptionInfoBytes);
+        buffer.put(identityHeaderBytes);
+        buffer.put(ciphertext);
+        buffer.put(mic);
+        return buffer.array();
     }
 
     /** Returns the {@link DataElement}s in the advertisement. */
@@ -260,7 +382,7 @@
     }
 
     /** Returns the {@link DataElement}s in the advertisement according to the key. */
-    public List<DataElement> getDataElements(@DataElement.DataType int key) {
+    public List<DataElement> getDataElements(@DataType int key) {
         List<DataElement> res = new ArrayList<>();
         for (DataElement dataElement : mDataElements) {
             if (key == dataElement.getKey()) {
@@ -285,125 +407,86 @@
                 getActions());
     }
 
-    ExtendedAdvertisement(
-            @PresenceCredential.IdentityType int identityType,
-            byte[] identity,
-            byte[] salt,
-            byte[] authenticityKey,
-            List<Integer> actions,
-            List<DataElement> dataElements) {
-        this.mVersion = BroadcastRequest.PRESENCE_VERSION_V1;
-        this.mIdentityType = identityType;
-        this.mIdentity = identity;
-        this.mSalt = salt;
-        this.mAuthenticityKey = authenticityKey;
-        this.mActions = actions;
-        this.mDataElements = dataElements;
-        this.mCompleteDataElementsBytes = new ArrayList<>();
+    @Nullable
+    private byte[] getCiphertext(byte[] nonce, List<DataElement> dataElements) {
+        Cryptor cryptor = CryptorMicImp.getInstance();
+        byte[] rawDeBytes = mIdentity;
+        for (DataElement dataElement : dataElements) {
+            rawDeBytes = ArrayUtils.concatByteArrays(rawDeBytes,
+                    ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement));
+        }
+        return cryptor.encrypt(rawDeBytes, nonce, mKeySeed);
+    }
 
-        int length = HEADER_LENGTH; // header
+    private static List<DataElement> getDataElementsFromBytes(
+            @BroadcastVersion int version, byte[] bytes) {
+        List<DataElement> res = new ArrayList<>();
+        if (ArrayUtils.isEmpty(bytes)) {
+            return res;
+        }
+        int index = 0;
+        while (index < bytes.length) {
+            byte[] deHeaderArray = ExtendedAdvertisementUtils
+                    .getDataElementHeader(bytes, index);
+            DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
+            index += deHeaderArray.length;
+            @DataType int type = Objects.requireNonNull(deHeader).getDataType();
+            if (isSaltOrIdentity(type)) {
+                Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
+                        + " and one identity in the advertisement.");
+                return new ArrayList<>();
+            }
+            byte[] deData = new byte[deHeader.getDataLength()];
+            for (int i = 0; i < deHeader.getDataLength(); i++) {
+                deData[i] = bytes[index++];
+            }
+            res.add(new DataElement(type, deData));
+        }
+        return res;
+    }
 
-        // Salt
-        DataElement saltElement = new DataElement(DataElement.DataType.SALT, salt);
-        byte[] saltByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(saltElement);
-        mCompleteDataElementsBytes.add(saltByteArray);
-        length += saltByteArray.length;
+    @Nullable
+    private static byte[] getNonce(@DataType int type, byte[] data) {
+        try {
+            if (type == DataType.SALT) {
+                if (data.length != SALT_DATA_LENGTH) {
+                    Log.v(TAG, "Salt DataElement needs to be 2 bytes.");
+                    return null;
+                }
+                return CryptorMicImp.generateAdvNonce(data);
+            } else if (type == DataType.ENCRYPTION_INFO) {
+                try {
+                    EncryptionInfo info = new EncryptionInfo(data);
+                    if (info.getEncodingScheme() != EncryptionInfo.EncodingScheme.MIC) {
+                        Log.v(TAG, "Not support Signature yet.");
+                        return null;
+                    }
+                    return CryptorMicImp.generateAdvNonce(info.getSalt(), CIPHER_START_INDEX);
+                } catch (IllegalArgumentException e) {
+                    Log.w(TAG, "Salt DataElement needs to be 17 bytes.", e);
+                    return null;
+                }
+            }
+        }  catch (GeneralSecurityException e) {
+            Log.w(TAG, "Failed to decrypt metadata encryption key.", e);
+            return null;
+        }
+        return null;
+    }
 
-        // Identity
-        byte[] encryptedIdentity =
-                CryptorImpIdentityV1.getInstance().encrypt(identity, salt, authenticityKey);
-        DataElement identityElement = new DataElement(toDataType(identityType), encryptedIdentity);
-        byte[] identityByteArray =
-                ExtendedAdvertisementUtils.convertDataElementToBytes(identityElement);
-        mCompleteDataElementsBytes.add(identityByteArray);
-        length += identityByteArray.length;
-
-        List<Byte> dataElementBytes = new ArrayList<>();
-        // Intents
-        for (int action : mActions) {
-            DataElement actionElement = new DataElement(DataElement.DataType.ACTION,
-                    new byte[] {(byte) action});
-            byte[] intentByteArray =
-                    ExtendedAdvertisementUtils.convertDataElementToBytes(actionElement);
-            mCompleteDataElementsBytes.add(intentByteArray);
-            for (Byte b : intentByteArray) {
-                dataElementBytes.add(b);
+    @Nullable
+    private static List<Integer> getActionsFromDataElements(List<DataElement> dataElements) {
+        List<Integer> actions = new ArrayList<>();
+        for (DataElement dataElement : dataElements) {
+            if (dataElement.getKey() == DataElement.DataType.ACTION) {
+                byte[] value = dataElement.getValue();
+                if (value.length != 1) {
+                    Log.w(TAG, "Action should be only 1 byte.");
+                    return null;
+                }
+                actions.add(Byte.toUnsignedInt(value[0]));
             }
         }
-
-        // Data Elements (Extended properties)
-        for (DataElement dataElement : mDataElements) {
-            byte[] deByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement);
-            mCompleteDataElementsBytes.add(deByteArray);
-            for (Byte b : deByteArray) {
-                dataElementBytes.add(b);
-            }
-        }
-
-        byte[] data = new byte[dataElementBytes.size()];
-        for (int i = 0; i < dataElementBytes.size(); i++) {
-            data[i] = dataElementBytes.get(i);
-        }
-        Cryptor cryptor = getCryptor(/* encrypt= */ true);
-        byte[] encryptedDeBytes = cryptor.encrypt(data, salt, authenticityKey);
-
-        length += encryptedDeBytes.length;
-
-        // Signature
-        byte[] hmacTag = Objects.requireNonNull(cryptor.sign(data, authenticityKey));
-        mHmacTag = hmacTag;
-        length += hmacTag.length;
-
-        this.mLength = length;
-    }
-
-    @PresenceCredential.IdentityType
-    private static int toPresenceCredentialIdentityType(@DataElement.DataType int type) {
-        switch (type) {
-            case DataElement.DataType.PRIVATE_IDENTITY:
-                return PresenceCredential.IDENTITY_TYPE_PRIVATE;
-            case DataElement.DataType.PROVISIONED_IDENTITY:
-                return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
-            case DataElement.DataType.TRUSTED_IDENTITY:
-                return PresenceCredential.IDENTITY_TYPE_TRUSTED;
-            case DataElement.DataType.PUBLIC_IDENTITY:
-            default:
-                return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
-        }
-    }
-
-    @DataElement.DataType
-    private static int toDataType(@PresenceCredential.IdentityType int identityType) {
-        switch (identityType) {
-            case PresenceCredential.IDENTITY_TYPE_PRIVATE:
-                return DataElement.DataType.PRIVATE_IDENTITY;
-            case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
-                return DataElement.DataType.PROVISIONED_IDENTITY;
-            case PresenceCredential.IDENTITY_TYPE_TRUSTED:
-                return DataElement.DataType.TRUSTED_IDENTITY;
-            case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
-            default:
-                return DataElement.DataType.PUBLIC_IDENTITY;
-        }
-    }
-
-    /**
-     * Returns {@code true} if the given {@link DataElement.DataType} is salt, or one of the
-     * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
-     */
-    private static boolean isSaltOrIdentity(@DataElement.DataType int type) {
-        return type == DataElement.DataType.SALT || type == DataElement.DataType.PRIVATE_IDENTITY
-                || type == DataElement.DataType.TRUSTED_IDENTITY
-                || type == DataElement.DataType.PROVISIONED_IDENTITY
-                || type == DataElement.DataType.PUBLIC_IDENTITY;
-    }
-
-    private static Cryptor getCryptor(boolean encrypt) {
-        if (encrypt) {
-            Log.d(TAG, "get V1 Cryptor");
-            return CryptorImpV1.getInstance();
-        }
-        Log.d(TAG, "get fake Cryptor");
-        return CryptorImpFake.getInstance();
+        return actions;
     }
 }
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
index c355df2..50dada2 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
@@ -16,15 +16,18 @@
 
 package com.android.server.nearby.presence;
 
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid;
+import android.os.ParcelUuid;
 
-import java.util.UUID;
+import com.android.server.nearby.util.ArrayUtils;
 
 /**
  * Constants for Nearby Presence operations.
  */
 public class PresenceConstants {
+    /** The Presence UUID value in byte array format. */
+    public static final byte[] PRESENCE_UUID_BYTES = ArrayUtils.intToByteArray(0xFCF1);
 
     /** Presence advertisement service data uuid. */
-    public static final UUID PRESENCE_UUID = to128BitUuid((short) 0xFCF1);
+    public static final ParcelUuid PRESENCE_UUID =
+            ParcelUuid.fromString("0000fcf1-0000-1000-8000-00805f9b34fb");
 }
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
index deb5167..0a51068 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
@@ -36,9 +36,6 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.Constant;
 
 import java.util.Arrays;
 import java.util.List;
@@ -48,8 +45,7 @@
 /** PresenceManager is the class initiated in nearby service to handle presence related work. */
 public class PresenceManager {
 
-    final LocatorContextWrapper mLocatorContextWrapper;
-    final Locator mLocator;
+    final Context mContext;
     private final IntentFilter mIntentFilter;
 
     @VisibleForTesting
@@ -76,7 +72,7 @@
 
                 @Override
                 public void onError(int errorCode) {
-                    Log.w(Constant.TAG, "[PresenceManager] Scan error is " + errorCode);
+                    Log.w(TAG, "[PresenceManager] Scan error is " + errorCode);
                 }
             };
 
@@ -123,9 +119,8 @@
                 }
             };
 
-    public PresenceManager(LocatorContextWrapper contextWrapper) {
-        mLocatorContextWrapper = contextWrapper;
-        mLocator = mLocatorContextWrapper.getLocator();
+    public PresenceManager(Context context) {
+        mContext = context;
         mIntentFilter = new IntentFilter();
     }
 
@@ -133,8 +128,7 @@
     @Nullable
     private NearbyManager getNearbyManager() {
         return (NearbyManager)
-                mLocatorContextWrapper
-                        .getApplicationContext()
+                mContext.getApplicationContext()
                         .getSystemService(Context.NEARBY_SERVICE);
     }
 
@@ -142,8 +136,6 @@
     public void initiate() {
         mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
         mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
-        mLocatorContextWrapper
-                .getContext()
-                .registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
+        mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
     }
 }
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 5632ab5..66ae79c 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -29,7 +29,6 @@
 import android.bluetooth.le.BluetoothLeAdvertiser;
 import android.nearby.BroadcastCallback;
 import android.nearby.BroadcastRequest;
-import android.os.ParcelUuid;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -79,14 +78,14 @@
                 advertiseStarted = true;
                 AdvertiseData advertiseData =
                         new AdvertiseData.Builder()
-                                .addServiceData(new ParcelUuid(PRESENCE_UUID),
-                                        advertisementPackets).build();
+                                .addServiceData(PRESENCE_UUID, advertisementPackets).build();
                 try {
                     mBroadcastListener = listener;
                     switch (version) {
                         case BroadcastRequest.PRESENCE_VERSION_V0:
                             bluetoothLeAdvertiser.startAdvertising(getAdvertiseSettings(),
                                     advertiseData, this);
+                            Log.v(TAG, "Start to broadcast V0 advertisement.");
                             break;
                         case BroadcastRequest.PRESENCE_VERSION_V1:
                             if (adapter.isLeExtendedAdvertisingSupported()) {
@@ -94,6 +93,7 @@
                                         getAdvertisingSetParameters(),
                                         advertiseData,
                                         null, null, null, mAdvertisingSetCallback);
+                                Log.v(TAG, "Start to broadcast V1 advertisement.");
                             } else {
                                 Log.w(TAG, "Failed to start advertising set because the chipset"
                                         + " does not supports LE Extended Advertising feature.");
@@ -105,7 +105,8 @@
                                     + " is wrong.");
                             advertiseStarted = false;
                     }
-                } catch (NullPointerException | IllegalStateException | SecurityException e) {
+                } catch (NullPointerException | IllegalStateException | SecurityException
+                    | IllegalArgumentException e) {
                     Log.w(TAG, "Failed to start advertising.", e);
                     advertiseStarted = false;
                 }
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index c9098a6..e4651b7 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -19,6 +19,7 @@
 import static android.nearby.ScanCallback.ERROR_UNKNOWN;
 
 import static com.android.server.nearby.NearbyService.TAG;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID;
 
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothAdapter;
@@ -40,13 +41,10 @@
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants;
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.presence.ExtendedAdvertisement;
-import com.android.server.nearby.presence.PresenceConstants;
 import com.android.server.nearby.util.ArrayUtils;
 import com.android.server.nearby.util.ForegroundThread;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -61,10 +59,6 @@
  */
 public class BleDiscoveryProvider extends AbstractDiscoveryProvider {
 
-    @VisibleForTesting
-    static final ParcelUuid FAST_PAIR_UUID = new ParcelUuid(Constants.FastPairService.ID);
-    private static final ParcelUuid PRESENCE_UUID = new ParcelUuid(PresenceConstants.PRESENCE_UUID);
-
     // Don't block the thread as it may be used by other services.
     private static final Executor NEARBY_EXECUTOR = ForegroundThread.getExecutor();
     private final Injector mInjector;
@@ -74,15 +68,6 @@
     @GuardedBy("mLock")
     @Nullable
     private List<android.nearby.ScanFilter> mScanFilters;
-    private android.bluetooth.le.ScanCallback mScanCallbackLegacy =
-            new android.bluetooth.le.ScanCallback() {
-                @Override
-                public void onScanResult(int callbackType, ScanResult scanResult) {
-                }
-                @Override
-                public void onScanFailed(int errorCode) {
-                }
-            };
     private android.bluetooth.le.ScanCallback mScanCallback =
             new android.bluetooth.le.ScanCallback() {
                 @Override
@@ -103,15 +88,10 @@
                         }
                         Map<ParcelUuid, byte[]> serviceDataMap = record.getServiceData();
                         if (serviceDataMap != null) {
-                            byte[] fastPairData = serviceDataMap.get(FAST_PAIR_UUID);
-                            if (fastPairData != null) {
-                                builder.setData(serviceDataMap.get(FAST_PAIR_UUID));
-                            } else {
-                                byte[] presenceData = serviceDataMap.get(PRESENCE_UUID);
-                                if (presenceData != null) {
-                                    setPresenceDevice(presenceData, builder, deviceName,
-                                            scanResult.getRssi());
-                                }
+                            byte[] presenceData = serviceDataMap.get(PRESENCE_UUID);
+                            if (presenceData != null) {
+                                setPresenceDevice(presenceData, builder, deviceName,
+                                        scanResult.getRssi());
                             }
                         }
                     }
@@ -143,10 +123,6 @@
                         .addMedium(NearbyDevice.Medium.BLE)
                         .setName(deviceName)
                         .setRssi(rssi);
-        for (int i : advertisement.getActions()) {
-            builder.addExtendedProperty(new DataElement(DataElement.DataType.ACTION,
-                    new byte[]{(byte) i}));
-        }
         for (DataElement dataElement : advertisement.getDataElements()) {
             builder.addExtendedProperty(dataElement);
         }
@@ -157,10 +133,6 @@
         List<ScanFilter> scanFilterList = new ArrayList<>();
         scanFilterList.add(
                 new ScanFilter.Builder()
-                        .setServiceData(FAST_PAIR_UUID, new byte[]{0}, new byte[]{0})
-                        .build());
-        scanFilterList.add(
-                new ScanFilter.Builder()
                         .setServiceData(PRESENCE_UUID, new byte[]{0}, new byte[]{0})
                         .build());
         return scanFilterList;
@@ -189,7 +161,6 @@
         if (isBleAvailable()) {
             Log.d(TAG, "BleDiscoveryProvider started");
             startScan(getScanFilters(), getScanSettings(/* legacy= */ false), mScanCallback);
-            startScan(getScanFilters(), getScanSettings(/* legacy= */ true), mScanCallbackLegacy);
             return;
         }
         Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
@@ -206,7 +177,6 @@
         }
         Log.v(TAG, "Ble scan stopped.");
         bluetoothLeScanner.stopScan(mScanCallback);
-        bluetoothLeScanner.stopScan(mScanCallbackLegacy);
         synchronized (mLock) {
             if (mScanFilters != null) {
                 mScanFilters = null;
@@ -298,17 +268,13 @@
                         if (advertisement == null) {
                             continue;
                         }
-                        if (CryptorImpIdentityV1.getInstance().verify(
-                                advertisement.getIdentity(),
-                                credential.getEncryptedMetadataKeyTag())) {
-                            builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
-                                    rssi));
-                            builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
-                            if (!ArrayUtils.isEmpty(credential.getSecretId())) {
-                                builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
-                            }
-                            return;
+                        builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
+                                rssi));
+                        builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
+                        if (!ArrayUtils.isEmpty(credential.getSecretId())) {
+                            builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
                         }
+                        return;
                     }
                 }
             }
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index 7ab0523..21ec252 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -20,6 +20,9 @@
 
 import static com.android.server.nearby.NearbyService.TAG;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -132,7 +135,6 @@
     protected void onSetScanFilters(List<ScanFilter> filters) {
         synchronized (mLock) {
             mScanFilters = filters == null ? null : List.copyOf(filters);
-            updateFiltersLocked();
         }
     }
 
@@ -156,7 +158,7 @@
             builder.setFastPairSupported(version != ChreCommunication.INVALID_NANO_APP_VERSION);
             try {
                 callback.onQueryComplete(builder.build());
-            } catch (RemoteException e) {
+            } catch (RemoteException | NullPointerException e) {
                 e.printStackTrace();
             }
         });
@@ -351,7 +353,11 @@
                                             DataElement.DataType.ACTION,
                                             new byte[]{(byte) filterResult.getIntent()}));
                         }
-
+                        if (filterResult.hasTimestampNs()) {
+                            presenceDeviceBuilder
+                                    .setDiscoveryTimestampMillis(MILLISECONDS.convert(
+                                            filterResult.getTimestampNs(), NANOSECONDS));
+                        }
                         PublicCredential publicCredential =
                                 new PublicCredential.Builder(
                                         secretId,
diff --git a/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
deleted file mode 100644
index d925f07..0000000
--- a/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
+++ /dev/null
@@ -1,196 +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.server.nearby.provider;
-
-import android.accounts.Account;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.nearby.FastPairDataProviderService;
-import android.nearby.aidl.ByteArrayParcel;
-import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
-import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
-import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
-import android.nearby.aidl.FastPairManageAccountRequestParcel;
-import android.util.Log;
-
-import androidx.annotation.WorkerThread;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import service.proto.Data;
-import service.proto.Rpcs;
-
-/**
- * FastPairDataProvider is a singleton that implements APIs to get FastPair data.
- */
-public class FastPairDataProvider {
-
-    private static final String TAG = "FastPairDataProvider";
-
-    private static FastPairDataProvider sInstance;
-
-    private ProxyFastPairDataProvider mProxyFastPairDataProvider;
-
-    /**
-     * Initializes FastPairDataProvider singleton.
-     */
-    public static synchronized FastPairDataProvider init(Context context) {
-        if (sInstance == null) {
-            sInstance = new FastPairDataProvider(context);
-        }
-        if (sInstance.mProxyFastPairDataProvider == null) {
-            Log.w(TAG, "no proxy fast pair data provider found");
-        } else {
-            sInstance.mProxyFastPairDataProvider.register();
-        }
-        return sInstance;
-    }
-
-    @Nullable
-    public static synchronized FastPairDataProvider getInstance() {
-        return sInstance;
-    }
-
-    private FastPairDataProvider(Context context) {
-        mProxyFastPairDataProvider = ProxyFastPairDataProvider.create(
-                context, FastPairDataProviderService.ACTION_FAST_PAIR_DATA_PROVIDER);
-        if (mProxyFastPairDataProvider == null) {
-            Log.d("FastPairService", "fail to initiate the fast pair proxy provider");
-        } else {
-            Log.d("FastPairService", "the fast pair proxy provider initiated");
-        }
-    }
-
-    @VisibleForTesting
-    void setProxyDataProvider(ProxyFastPairDataProvider proxyFastPairDataProvider) {
-        this.mProxyFastPairDataProvider = proxyFastPairDataProvider;
-    }
-
-    /**
-     * Loads FastPairAntispoofKeyDeviceMetadata.
-     *
-     * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
-     */
-    @WorkerThread
-    @Nullable
-    public Rpcs.GetObservedDeviceResponse loadFastPairAntispoofKeyDeviceMetadata(byte[] modelId) {
-        if (mProxyFastPairDataProvider != null) {
-            FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel =
-                    new FastPairAntispoofKeyDeviceMetadataRequestParcel();
-            requestParcel.modelId = modelId;
-            return Utils.convertToGetObservedDeviceResponse(
-                    mProxyFastPairDataProvider
-                            .loadFastPairAntispoofKeyDeviceMetadata(requestParcel));
-        }
-        throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
-    }
-
-    /**
-     * Enrolls an account to Fast Pair.
-     *
-     * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
-     */
-    public void optIn(Account account) {
-        if (mProxyFastPairDataProvider != null) {
-            FastPairManageAccountRequestParcel requestParcel =
-                    new FastPairManageAccountRequestParcel();
-            requestParcel.account = account;
-            requestParcel.requestType = FastPairDataProviderService.MANAGE_REQUEST_ADD;
-            mProxyFastPairDataProvider.manageFastPairAccount(requestParcel);
-            return;
-        }
-        throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
-    }
-
-    /**
-     * Uploads the device info to Fast Pair account.
-     *
-     * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
-     */
-    public void upload(Account account, FastPairUploadInfo uploadInfo) {
-        if (mProxyFastPairDataProvider != null) {
-            FastPairManageAccountDeviceRequestParcel requestParcel =
-                    new FastPairManageAccountDeviceRequestParcel();
-            requestParcel.account = account;
-            requestParcel.requestType = FastPairDataProviderService.MANAGE_REQUEST_ADD;
-            requestParcel.accountKeyDeviceMetadata =
-                    Utils.convertToFastPairAccountKeyDeviceMetadata(uploadInfo);
-            mProxyFastPairDataProvider.manageFastPairAccountDevice(requestParcel);
-            return;
-        }
-        throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
-    }
-
-    /**
-     * Loads FastPair device accountKeys for a given account, but not other detailed fields.
-     *
-     * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
-     */
-    public List<Data.FastPairDeviceWithAccountKey> loadFastPairDeviceWithAccountKey(
-            Account account) {
-        return loadFastPairDeviceWithAccountKey(account, new ArrayList<byte[]>(0));
-    }
-
-    /**
-     * Loads FastPair devices for a list of accountKeys of a given account.
-     *
-     * @param account The account of the FastPair devices.
-     * @param deviceAccountKeys The allow list of FastPair devices if it is not empty. Otherwise,
-     *                    the function returns accountKeys of all FastPair devices under the
-     *                    account, without detailed fields.
-     *
-     * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
-     */
-    public List<Data.FastPairDeviceWithAccountKey> loadFastPairDeviceWithAccountKey(
-            Account account, List<byte[]> deviceAccountKeys) {
-        if (mProxyFastPairDataProvider != null) {
-            FastPairAccountDevicesMetadataRequestParcel requestParcel =
-                    new FastPairAccountDevicesMetadataRequestParcel();
-            requestParcel.account = account;
-            requestParcel.deviceAccountKeys = new ByteArrayParcel[deviceAccountKeys.size()];
-            int i = 0;
-            for (byte[] deviceAccountKey : deviceAccountKeys) {
-                requestParcel.deviceAccountKeys[i] = new ByteArrayParcel();
-                requestParcel.deviceAccountKeys[i].byteArray = deviceAccountKey;
-                i = i + 1;
-            }
-            return Utils.convertToFastPairDevicesWithAccountKey(
-                    mProxyFastPairDataProvider.loadFastPairAccountDevicesMetadata(requestParcel));
-        }
-        throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
-    }
-
-    /**
-     * Loads FastPair Eligible Accounts.
-     *
-     * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
-     */
-    public List<Account> loadFastPairEligibleAccounts() {
-        if (mProxyFastPairDataProvider != null) {
-            FastPairEligibleAccountsRequestParcel requestParcel =
-                    new FastPairEligibleAccountsRequestParcel();
-            return Utils.convertToAccountList(
-                    mProxyFastPairDataProvider.loadFastPairEligibleAccounts(requestParcel));
-        }
-        throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ProxyFastPairDataProvider.java b/nearby/service/java/com/android/server/nearby/provider/ProxyFastPairDataProvider.java
deleted file mode 100644
index f0ade6c..0000000
--- a/nearby/service/java/com/android/server/nearby/provider/ProxyFastPairDataProvider.java
+++ /dev/null
@@ -1,307 +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.server.nearby.provider;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
-import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
-import android.nearby.aidl.FastPairEligibleAccountParcel;
-import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
-import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
-import android.nearby.aidl.FastPairManageAccountRequestParcel;
-import android.nearby.aidl.IFastPairAccountDevicesMetadataCallback;
-import android.nearby.aidl.IFastPairAntispoofKeyDeviceMetadataCallback;
-import android.nearby.aidl.IFastPairDataProvider;
-import android.nearby.aidl.IFastPairEligibleAccountsCallback;
-import android.nearby.aidl.IFastPairManageAccountCallback;
-import android.nearby.aidl.IFastPairManageAccountDeviceCallback;
-import android.os.IBinder;
-import android.os.RemoteException;
-
-import androidx.annotation.WorkerThread;
-
-import com.android.server.nearby.common.servicemonitor.CurrentUserServiceProvider;
-import com.android.server.nearby.common.servicemonitor.CurrentUserServiceProvider.BoundServiceInfo;
-import com.android.server.nearby.common.servicemonitor.ServiceMonitor;
-import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceListener;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Proxy for IFastPairDataProvider implementations.
- */
-public class ProxyFastPairDataProvider implements ServiceListener<BoundServiceInfo> {
-
-    private static final int TIME_OUT_MILLIS = 10000;
-
-    /**
-     * Creates and registers this proxy. If no suitable service is available for the proxy, returns
-     * null.
-     */
-    @Nullable
-    public static ProxyFastPairDataProvider create(Context context, String action) {
-        ProxyFastPairDataProvider proxy = new ProxyFastPairDataProvider(context, action);
-        if (proxy.checkServiceResolves()) {
-            return proxy;
-        } else {
-            return null;
-        }
-    }
-
-    private final ServiceMonitor mServiceMonitor;
-
-    private ProxyFastPairDataProvider(Context context, String action) {
-        // safe to use direct executor since our locks are not acquired in a code path invoked by
-        // our owning provider
-
-        mServiceMonitor = ServiceMonitor.create(context, "FAST_PAIR_DATA_PROVIDER",
-                CurrentUserServiceProvider.create(context, action), this);
-    }
-
-    private boolean checkServiceResolves() {
-        return mServiceMonitor.checkServiceResolves();
-    }
-
-    /**
-     * User service watch to connect to actually services implemented by OEMs.
-     */
-    public void register() {
-        mServiceMonitor.register();
-    }
-
-    // Fast Pair Data Provider doesn't maintain a long running state.
-    // Therefore, it doesn't need setup at bind time.
-    @Override
-    public void onBind(IBinder binder, BoundServiceInfo boundServiceInfo) throws RemoteException {
-    }
-
-    // Fast Pair Data Provider doesn't maintain a long running state.
-    // Therefore, it doesn't need tear down at unbind time.
-    @Override
-    public void onUnbind() {
-    }
-
-    /**
-     * Invokes system api loadFastPairEligibleAccounts.
-     *
-     * @return an array of acccounts and their opt in status.
-     */
-    @WorkerThread
-    @Nullable
-    public FastPairEligibleAccountParcel[] loadFastPairEligibleAccounts(
-            FastPairEligibleAccountsRequestParcel requestParcel) {
-        final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
-        final AtomicReference<FastPairEligibleAccountParcel[]> response = new AtomicReference<>();
-        mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
-            @Override
-            public void run(IBinder binder) throws RemoteException {
-                IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
-                IFastPairEligibleAccountsCallback callback =
-                        new IFastPairEligibleAccountsCallback.Stub() {
-                            public void onFastPairEligibleAccountsReceived(
-                                    FastPairEligibleAccountParcel[] accountParcels) {
-                                response.set(accountParcels);
-                                waitForCompletionLatch.countDown();
-                            }
-
-                            public void onError(int code, String message) {
-                                waitForCompletionLatch.countDown();
-                            }
-                        };
-                provider.loadFastPairEligibleAccounts(requestParcel, callback);
-            }
-
-            @Override
-            public void onError() {
-                waitForCompletionLatch.countDown();
-            }
-        });
-        try {
-            waitForCompletionLatch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            // skip.
-        }
-        return response.get();
-    }
-
-    /**
-     * Invokes system api manageFastPairAccount to opt in account, or opt out account.
-     */
-    @WorkerThread
-    public void manageFastPairAccount(FastPairManageAccountRequestParcel requestParcel) {
-        final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
-        mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
-            @Override
-            public void run(IBinder binder) throws RemoteException {
-                IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
-                IFastPairManageAccountCallback callback =
-                        new IFastPairManageAccountCallback.Stub() {
-                            public void onSuccess() {
-                                waitForCompletionLatch.countDown();
-                            }
-
-                            public void onError(int code, String message) {
-                                waitForCompletionLatch.countDown();
-                            }
-                        };
-                provider.manageFastPairAccount(requestParcel, callback);
-            }
-
-            @Override
-            public void onError() {
-                waitForCompletionLatch.countDown();
-            }
-        });
-        try {
-            waitForCompletionLatch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            // skip.
-        }
-        return;
-    }
-
-    /**
-     * Invokes system api manageFastPairAccountDevice to add or remove a device from a Fast Pair
-     * account.
-     */
-    @WorkerThread
-    public void manageFastPairAccountDevice(
-            FastPairManageAccountDeviceRequestParcel requestParcel) {
-        final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
-        mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
-            @Override
-            public void run(IBinder binder) throws RemoteException {
-                IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
-                IFastPairManageAccountDeviceCallback callback =
-                        new IFastPairManageAccountDeviceCallback.Stub() {
-                            public void onSuccess() {
-                                waitForCompletionLatch.countDown();
-                            }
-
-                            public void onError(int code, String message) {
-                                waitForCompletionLatch.countDown();
-                            }
-                        };
-                provider.manageFastPairAccountDevice(requestParcel, callback);
-            }
-
-            @Override
-            public void onError() {
-                waitForCompletionLatch.countDown();
-            }
-        });
-        try {
-            waitForCompletionLatch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            // skip.
-        }
-        return;
-    }
-
-    /**
-     * Invokes system api loadFastPairAntispoofKeyDeviceMetadata.
-     *
-     * @return the Fast Pair AntispoofKeyDeviceMetadata of a given device.
-     */
-    @WorkerThread
-    @Nullable
-    FastPairAntispoofKeyDeviceMetadataParcel loadFastPairAntispoofKeyDeviceMetadata(
-            FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel) {
-        final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
-        final AtomicReference<FastPairAntispoofKeyDeviceMetadataParcel> response =
-                new AtomicReference<>();
-        mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
-            @Override
-            public void run(IBinder binder) throws RemoteException {
-                IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
-                IFastPairAntispoofKeyDeviceMetadataCallback callback =
-                        new IFastPairAntispoofKeyDeviceMetadataCallback.Stub() {
-                            public void onFastPairAntispoofKeyDeviceMetadataReceived(
-                                    FastPairAntispoofKeyDeviceMetadataParcel metadata) {
-                                response.set(metadata);
-                                waitForCompletionLatch.countDown();
-                            }
-
-                            public void onError(int code, String message) {
-                                waitForCompletionLatch.countDown();
-                            }
-                        };
-                provider.loadFastPairAntispoofKeyDeviceMetadata(requestParcel, callback);
-            }
-
-            @Override
-            public void onError() {
-                waitForCompletionLatch.countDown();
-            }
-        });
-        try {
-            waitForCompletionLatch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            // skip.
-        }
-        return response.get();
-    }
-
-    /**
-     * Invokes loadFastPairAccountDevicesMetadata.
-     *
-     * @return the metadata of Fast Pair devices that are associated with a given account.
-     */
-    @WorkerThread
-    @Nullable
-    FastPairAccountKeyDeviceMetadataParcel[] loadFastPairAccountDevicesMetadata(
-            FastPairAccountDevicesMetadataRequestParcel requestParcel) {
-        final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
-        final AtomicReference<FastPairAccountKeyDeviceMetadataParcel[]> response =
-                new AtomicReference<>();
-        mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
-            @Override
-            public void run(IBinder binder) throws RemoteException {
-                IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
-                IFastPairAccountDevicesMetadataCallback callback =
-                        new IFastPairAccountDevicesMetadataCallback.Stub() {
-                            public void onFastPairAccountDevicesMetadataReceived(
-                                    FastPairAccountKeyDeviceMetadataParcel[] metadatas) {
-                                response.set(metadatas);
-                                waitForCompletionLatch.countDown();
-                            }
-
-                            public void onError(int code, String message) {
-                                waitForCompletionLatch.countDown();
-                            }
-                        };
-                provider.loadFastPairAccountDevicesMetadata(requestParcel, callback);
-            }
-
-            @Override
-            public void onError() {
-                waitForCompletionLatch.countDown();
-            }
-        });
-        try {
-            waitForCompletionLatch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            // skip.
-        }
-        return response.get();
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/provider/Utils.java b/nearby/service/java/com/android/server/nearby/provider/Utils.java
deleted file mode 100644
index 0f1c567..0000000
--- a/nearby/service/java/com/android/server/nearby/provider/Utils.java
+++ /dev/null
@@ -1,465 +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.server.nearby.provider;
-
-import android.accounts.Account;
-import android.annotation.Nullable;
-import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairDeviceMetadataParcel;
-import android.nearby.aidl.FastPairDiscoveryItemParcel;
-import android.nearby.aidl.FastPairEligibleAccountParcel;
-
-import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
-
-import com.google.protobuf.ByteString;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import service.proto.Cache;
-import service.proto.Data;
-import service.proto.FastPairString.FastPairStrings;
-import service.proto.Rpcs;
-
-/**
- * Utility functions to convert between different data classes.
- */
-class Utils {
-
-    static List<Data.FastPairDeviceWithAccountKey> convertToFastPairDevicesWithAccountKey(
-            @Nullable FastPairAccountKeyDeviceMetadataParcel[] metadataParcels) {
-        if (metadataParcels == null) {
-            return new ArrayList<Data.FastPairDeviceWithAccountKey>(0);
-        }
-
-        List<Data.FastPairDeviceWithAccountKey> fpDeviceList =
-                new ArrayList<>(metadataParcels.length);
-        for (FastPairAccountKeyDeviceMetadataParcel metadataParcel : metadataParcels) {
-            if (metadataParcel == null) {
-                continue;
-            }
-            Data.FastPairDeviceWithAccountKey.Builder fpDeviceBuilder =
-                    Data.FastPairDeviceWithAccountKey.newBuilder();
-            if (metadataParcel.deviceAccountKey != null) {
-                fpDeviceBuilder.setAccountKey(
-                        ByteString.copyFrom(metadataParcel.deviceAccountKey));
-            }
-            if (metadataParcel.sha256DeviceAccountKeyPublicAddress != null) {
-                fpDeviceBuilder.setSha256AccountKeyPublicAddress(
-                        ByteString.copyFrom(metadataParcel.sha256DeviceAccountKeyPublicAddress));
-            }
-
-            Cache.StoredDiscoveryItem.Builder storedDiscoveryItemBuilder =
-                    Cache.StoredDiscoveryItem.newBuilder();
-
-            if (metadataParcel.discoveryItem != null) {
-                if (metadataParcel.discoveryItem.actionUrl != null) {
-                    storedDiscoveryItemBuilder.setActionUrl(metadataParcel.discoveryItem.actionUrl);
-                }
-                Cache.ResolvedUrlType urlType = Cache.ResolvedUrlType.forNumber(
-                        metadataParcel.discoveryItem.actionUrlType);
-                if (urlType != null) {
-                    storedDiscoveryItemBuilder.setActionUrlType(urlType);
-                }
-                if (metadataParcel.discoveryItem.appName != null) {
-                    storedDiscoveryItemBuilder.setAppName(metadataParcel.discoveryItem.appName);
-                }
-                if (metadataParcel.discoveryItem.authenticationPublicKeySecp256r1 != null) {
-                    storedDiscoveryItemBuilder.setAuthenticationPublicKeySecp256R1(
-                            ByteString.copyFrom(
-                                    metadataParcel.discoveryItem.authenticationPublicKeySecp256r1));
-                }
-                if (metadataParcel.discoveryItem.description != null) {
-                    storedDiscoveryItemBuilder.setDescription(
-                            metadataParcel.discoveryItem.description);
-                }
-                if (metadataParcel.discoveryItem.deviceName != null) {
-                    storedDiscoveryItemBuilder.setDeviceName(
-                            metadataParcel.discoveryItem.deviceName);
-                }
-                if (metadataParcel.discoveryItem.displayUrl != null) {
-                    storedDiscoveryItemBuilder.setDisplayUrl(
-                            metadataParcel.discoveryItem.displayUrl);
-                }
-                storedDiscoveryItemBuilder.setFirstObservationTimestampMillis(
-                        metadataParcel.discoveryItem.firstObservationTimestampMillis);
-                if (metadataParcel.discoveryItem.iconFifeUrl != null) {
-                    storedDiscoveryItemBuilder.setIconFifeUrl(
-                            metadataParcel.discoveryItem.iconFifeUrl);
-                }
-                if (metadataParcel.discoveryItem.iconPng != null) {
-                    storedDiscoveryItemBuilder.setIconPng(
-                            ByteString.copyFrom(metadataParcel.discoveryItem.iconPng));
-                }
-                if (metadataParcel.discoveryItem.id != null) {
-                    storedDiscoveryItemBuilder.setId(metadataParcel.discoveryItem.id);
-                }
-                storedDiscoveryItemBuilder.setLastObservationTimestampMillis(
-                        metadataParcel.discoveryItem.lastObservationTimestampMillis);
-                if (metadataParcel.discoveryItem.macAddress != null) {
-                    storedDiscoveryItemBuilder.setMacAddress(
-                            metadataParcel.discoveryItem.macAddress);
-                }
-                if (metadataParcel.discoveryItem.packageName != null) {
-                    storedDiscoveryItemBuilder.setPackageName(
-                            metadataParcel.discoveryItem.packageName);
-                }
-                storedDiscoveryItemBuilder.setPendingAppInstallTimestampMillis(
-                        metadataParcel.discoveryItem.pendingAppInstallTimestampMillis);
-                storedDiscoveryItemBuilder.setRssi(metadataParcel.discoveryItem.rssi);
-                Cache.StoredDiscoveryItem.State state =
-                        Cache.StoredDiscoveryItem.State.forNumber(
-                                metadataParcel.discoveryItem.state);
-                if (state != null) {
-                    storedDiscoveryItemBuilder.setState(state);
-                }
-                if (metadataParcel.discoveryItem.title != null) {
-                    storedDiscoveryItemBuilder.setTitle(metadataParcel.discoveryItem.title);
-                }
-                if (metadataParcel.discoveryItem.triggerId != null) {
-                    storedDiscoveryItemBuilder.setTriggerId(metadataParcel.discoveryItem.triggerId);
-                }
-                storedDiscoveryItemBuilder.setTxPower(metadataParcel.discoveryItem.txPower);
-            }
-            if (metadataParcel.metadata != null) {
-                FastPairStrings.Builder stringsBuilder = FastPairStrings.newBuilder();
-                if (metadataParcel.metadata.connectSuccessCompanionAppInstalled != null) {
-                    stringsBuilder.setPairingFinishedCompanionAppInstalled(
-                            metadataParcel.metadata.connectSuccessCompanionAppInstalled);
-                }
-                if (metadataParcel.metadata.connectSuccessCompanionAppNotInstalled != null) {
-                    stringsBuilder.setPairingFinishedCompanionAppNotInstalled(
-                            metadataParcel.metadata.connectSuccessCompanionAppNotInstalled);
-                }
-                if (metadataParcel.metadata.failConnectGoToSettingsDescription != null) {
-                    stringsBuilder.setPairingFailDescription(
-                            metadataParcel.metadata.failConnectGoToSettingsDescription);
-                }
-                if (metadataParcel.metadata.initialNotificationDescription != null) {
-                    stringsBuilder.setTapToPairWithAccount(
-                            metadataParcel.metadata.initialNotificationDescription);
-                }
-                if (metadataParcel.metadata.initialNotificationDescriptionNoAccount != null) {
-                    stringsBuilder.setTapToPairWithoutAccount(
-                            metadataParcel.metadata.initialNotificationDescriptionNoAccount);
-                }
-                if (metadataParcel.metadata.initialPairingDescription != null) {
-                    stringsBuilder.setInitialPairingDescription(
-                            metadataParcel.metadata.initialPairingDescription);
-                }
-                if (metadataParcel.metadata.retroactivePairingDescription != null) {
-                    stringsBuilder.setRetroactivePairingDescription(
-                            metadataParcel.metadata.retroactivePairingDescription);
-                }
-                if (metadataParcel.metadata.subsequentPairingDescription != null) {
-                    stringsBuilder.setSubsequentPairingDescription(
-                            metadataParcel.metadata.subsequentPairingDescription);
-                }
-                if (metadataParcel.metadata.waitLaunchCompanionAppDescription != null) {
-                    stringsBuilder.setWaitAppLaunchDescription(
-                            metadataParcel.metadata.waitLaunchCompanionAppDescription);
-                }
-                storedDiscoveryItemBuilder.setFastPairStrings(stringsBuilder.build());
-
-                Cache.FastPairInformation.Builder fpInformationBuilder =
-                        Cache.FastPairInformation.newBuilder();
-                Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
-                        Rpcs.TrueWirelessHeadsetImages.newBuilder();
-                if (metadataParcel.metadata.trueWirelessImageUrlCase != null) {
-                    imagesBuilder.setCaseUrl(metadataParcel.metadata.trueWirelessImageUrlCase);
-                }
-                if (metadataParcel.metadata.trueWirelessImageUrlLeftBud != null) {
-                    imagesBuilder.setLeftBudUrl(
-                            metadataParcel.metadata.trueWirelessImageUrlLeftBud);
-                }
-                if (metadataParcel.metadata.trueWirelessImageUrlRightBud != null) {
-                    imagesBuilder.setRightBudUrl(
-                            metadataParcel.metadata.trueWirelessImageUrlRightBud);
-                }
-                fpInformationBuilder.setTrueWirelessImages(imagesBuilder.build());
-                Rpcs.DeviceType deviceType =
-                        Rpcs.DeviceType.forNumber(metadataParcel.metadata.deviceType);
-                if (deviceType != null) {
-                    fpInformationBuilder.setDeviceType(deviceType);
-                }
-
-                storedDiscoveryItemBuilder.setFastPairInformation(fpInformationBuilder.build());
-            }
-            fpDeviceBuilder.setDiscoveryItem(storedDiscoveryItemBuilder.build());
-            fpDeviceList.add(fpDeviceBuilder.build());
-        }
-        return fpDeviceList;
-    }
-
-    static List<Account> convertToAccountList(
-            @Nullable FastPairEligibleAccountParcel[] accountParcels) {
-        if (accountParcels == null) {
-            return new ArrayList<Account>(0);
-        }
-        List<Account> accounts = new ArrayList<Account>(accountParcels.length);
-        for (FastPairEligibleAccountParcel parcel : accountParcels) {
-            if (parcel != null && parcel.account != null) {
-                accounts.add(parcel.account);
-            }
-        }
-        return accounts;
-    }
-
-    private static @Nullable Rpcs.Device convertToDevice(
-            FastPairAntispoofKeyDeviceMetadataParcel metadata) {
-
-        Rpcs.Device.Builder deviceBuilder = Rpcs.Device.newBuilder();
-        if (metadata.antispoofPublicKey != null) {
-            deviceBuilder.setAntiSpoofingKeyPair(Rpcs.AntiSpoofingKeyPair.newBuilder()
-                    .setPublicKey(ByteString.copyFrom(metadata.antispoofPublicKey))
-                    .build());
-        }
-        if (metadata.deviceMetadata != null) {
-            Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
-                    Rpcs.TrueWirelessHeadsetImages.newBuilder();
-            if (metadata.deviceMetadata.trueWirelessImageUrlLeftBud != null) {
-                imagesBuilder.setLeftBudUrl(metadata.deviceMetadata.trueWirelessImageUrlLeftBud);
-            }
-            if (metadata.deviceMetadata.trueWirelessImageUrlRightBud != null) {
-                imagesBuilder.setRightBudUrl(metadata.deviceMetadata.trueWirelessImageUrlRightBud);
-            }
-            if (metadata.deviceMetadata.trueWirelessImageUrlCase != null) {
-                imagesBuilder.setCaseUrl(metadata.deviceMetadata.trueWirelessImageUrlCase);
-            }
-            deviceBuilder.setTrueWirelessImages(imagesBuilder.build());
-            if (metadata.deviceMetadata.imageUrl != null) {
-                deviceBuilder.setImageUrl(metadata.deviceMetadata.imageUrl);
-            }
-            if (metadata.deviceMetadata.intentUri != null) {
-                deviceBuilder.setIntentUri(metadata.deviceMetadata.intentUri);
-            }
-            if (metadata.deviceMetadata.name != null) {
-                deviceBuilder.setName(metadata.deviceMetadata.name);
-            }
-            Rpcs.DeviceType deviceType =
-                    Rpcs.DeviceType.forNumber(metadata.deviceMetadata.deviceType);
-            if (deviceType != null) {
-                deviceBuilder.setDeviceType(deviceType);
-            }
-            deviceBuilder.setBleTxPower(metadata.deviceMetadata.bleTxPower)
-                    .setTriggerDistance(metadata.deviceMetadata.triggerDistance);
-        }
-
-        return deviceBuilder.build();
-    }
-
-    private static @Nullable ByteString convertToImage(
-            FastPairAntispoofKeyDeviceMetadataParcel metadata) {
-        if (metadata.deviceMetadata == null || metadata.deviceMetadata.image == null) {
-            return null;
-        }
-
-        return ByteString.copyFrom(metadata.deviceMetadata.image);
-    }
-
-    private static @Nullable Rpcs.ObservedDeviceStrings
-            convertToObservedDeviceStrings(FastPairAntispoofKeyDeviceMetadataParcel metadata) {
-        if (metadata.deviceMetadata == null) {
-            return null;
-        }
-
-        Rpcs.ObservedDeviceStrings.Builder stringsBuilder = Rpcs.ObservedDeviceStrings.newBuilder();
-        if (metadata.deviceMetadata.connectSuccessCompanionAppInstalled != null) {
-            stringsBuilder.setConnectSuccessCompanionAppInstalled(
-                    metadata.deviceMetadata.connectSuccessCompanionAppInstalled);
-        }
-        if (metadata.deviceMetadata.connectSuccessCompanionAppNotInstalled != null) {
-            stringsBuilder.setConnectSuccessCompanionAppNotInstalled(
-                    metadata.deviceMetadata.connectSuccessCompanionAppNotInstalled);
-        }
-        if (metadata.deviceMetadata.downloadCompanionAppDescription != null) {
-            stringsBuilder.setDownloadCompanionAppDescription(
-                    metadata.deviceMetadata.downloadCompanionAppDescription);
-        }
-        if (metadata.deviceMetadata.failConnectGoToSettingsDescription != null) {
-            stringsBuilder.setFailConnectGoToSettingsDescription(
-                    metadata.deviceMetadata.failConnectGoToSettingsDescription);
-        }
-        if (metadata.deviceMetadata.initialNotificationDescription != null) {
-            stringsBuilder.setInitialNotificationDescription(
-                    metadata.deviceMetadata.initialNotificationDescription);
-        }
-        if (metadata.deviceMetadata.initialNotificationDescriptionNoAccount != null) {
-            stringsBuilder.setInitialNotificationDescriptionNoAccount(
-                    metadata.deviceMetadata.initialNotificationDescriptionNoAccount);
-        }
-        if (metadata.deviceMetadata.initialPairingDescription != null) {
-            stringsBuilder.setInitialPairingDescription(
-                    metadata.deviceMetadata.initialPairingDescription);
-        }
-        if (metadata.deviceMetadata.openCompanionAppDescription != null) {
-            stringsBuilder.setOpenCompanionAppDescription(
-                    metadata.deviceMetadata.openCompanionAppDescription);
-        }
-        if (metadata.deviceMetadata.retroactivePairingDescription != null) {
-            stringsBuilder.setRetroactivePairingDescription(
-                    metadata.deviceMetadata.retroactivePairingDescription);
-        }
-        if (metadata.deviceMetadata.subsequentPairingDescription != null) {
-            stringsBuilder.setSubsequentPairingDescription(
-                    metadata.deviceMetadata.subsequentPairingDescription);
-        }
-        if (metadata.deviceMetadata.unableToConnectDescription != null) {
-            stringsBuilder.setUnableToConnectDescription(
-                    metadata.deviceMetadata.unableToConnectDescription);
-        }
-        if (metadata.deviceMetadata.unableToConnectTitle != null) {
-            stringsBuilder.setUnableToConnectTitle(
-                    metadata.deviceMetadata.unableToConnectTitle);
-        }
-        if (metadata.deviceMetadata.updateCompanionAppDescription != null) {
-            stringsBuilder.setUpdateCompanionAppDescription(
-                    metadata.deviceMetadata.updateCompanionAppDescription);
-        }
-        if (metadata.deviceMetadata.waitLaunchCompanionAppDescription != null) {
-            stringsBuilder.setWaitLaunchCompanionAppDescription(
-                    metadata.deviceMetadata.waitLaunchCompanionAppDescription);
-        }
-
-        return stringsBuilder.build();
-    }
-
-    static @Nullable Rpcs.GetObservedDeviceResponse
-            convertToGetObservedDeviceResponse(
-                    @Nullable FastPairAntispoofKeyDeviceMetadataParcel metadata) {
-        if (metadata == null) {
-            return null;
-        }
-
-        Rpcs.GetObservedDeviceResponse.Builder responseBuilder =
-                Rpcs.GetObservedDeviceResponse.newBuilder();
-
-        Rpcs.Device device = convertToDevice(metadata);
-        if (device != null) {
-            responseBuilder.setDevice(device);
-        }
-        ByteString image = convertToImage(metadata);
-        if (image != null) {
-            responseBuilder.setImage(image);
-        }
-        Rpcs.ObservedDeviceStrings strings = convertToObservedDeviceStrings(metadata);
-        if (strings != null) {
-            responseBuilder.setStrings(strings);
-        }
-
-        return responseBuilder.build();
-    }
-
-    static @Nullable FastPairAccountKeyDeviceMetadataParcel
-            convertToFastPairAccountKeyDeviceMetadata(
-            @Nullable FastPairUploadInfo uploadInfo) {
-        if (uploadInfo == null) {
-            return null;
-        }
-
-        FastPairAccountKeyDeviceMetadataParcel accountKeyDeviceMetadataParcel =
-                new FastPairAccountKeyDeviceMetadataParcel();
-        if (uploadInfo.getAccountKey() != null) {
-            accountKeyDeviceMetadataParcel.deviceAccountKey =
-                    uploadInfo.getAccountKey().toByteArray();
-        }
-        if (uploadInfo.getSha256AccountKeyPublicAddress() != null) {
-            accountKeyDeviceMetadataParcel.sha256DeviceAccountKeyPublicAddress =
-                    uploadInfo.getSha256AccountKeyPublicAddress().toByteArray();
-        }
-        if (uploadInfo.getStoredDiscoveryItem() != null) {
-            accountKeyDeviceMetadataParcel.metadata =
-                    convertToFastPairDeviceMetadata(uploadInfo.getStoredDiscoveryItem());
-            accountKeyDeviceMetadataParcel.discoveryItem =
-                    convertToFastPairDiscoveryItem(uploadInfo.getStoredDiscoveryItem());
-        }
-
-        return accountKeyDeviceMetadataParcel;
-    }
-
-    private static @Nullable FastPairDiscoveryItemParcel
-            convertToFastPairDiscoveryItem(Cache.StoredDiscoveryItem storedDiscoveryItem) {
-        FastPairDiscoveryItemParcel discoveryItemParcel = new FastPairDiscoveryItemParcel();
-        discoveryItemParcel.actionUrl = storedDiscoveryItem.getActionUrl();
-        discoveryItemParcel.actionUrlType = storedDiscoveryItem.getActionUrlType().getNumber();
-        discoveryItemParcel.appName = storedDiscoveryItem.getAppName();
-        discoveryItemParcel.authenticationPublicKeySecp256r1 =
-                storedDiscoveryItem.getAuthenticationPublicKeySecp256R1().toByteArray();
-        discoveryItemParcel.description = storedDiscoveryItem.getDescription();
-        discoveryItemParcel.deviceName = storedDiscoveryItem.getDeviceName();
-        discoveryItemParcel.displayUrl = storedDiscoveryItem.getDisplayUrl();
-        discoveryItemParcel.firstObservationTimestampMillis =
-                storedDiscoveryItem.getFirstObservationTimestampMillis();
-        discoveryItemParcel.iconFifeUrl = storedDiscoveryItem.getIconFifeUrl();
-        discoveryItemParcel.iconPng = storedDiscoveryItem.getIconPng().toByteArray();
-        discoveryItemParcel.id = storedDiscoveryItem.getId();
-        discoveryItemParcel.lastObservationTimestampMillis =
-                storedDiscoveryItem.getLastObservationTimestampMillis();
-        discoveryItemParcel.macAddress = storedDiscoveryItem.getMacAddress();
-        discoveryItemParcel.packageName = storedDiscoveryItem.getPackageName();
-        discoveryItemParcel.pendingAppInstallTimestampMillis =
-                storedDiscoveryItem.getPendingAppInstallTimestampMillis();
-        discoveryItemParcel.rssi = storedDiscoveryItem.getRssi();
-        discoveryItemParcel.state = storedDiscoveryItem.getState().getNumber();
-        discoveryItemParcel.title = storedDiscoveryItem.getTitle();
-        discoveryItemParcel.triggerId = storedDiscoveryItem.getTriggerId();
-        discoveryItemParcel.txPower = storedDiscoveryItem.getTxPower();
-
-        return discoveryItemParcel;
-    }
-
-    /*  Do we upload these?
-        String downloadCompanionAppDescription =
-             bundle.getString("downloadCompanionAppDescription");
-        String locale = bundle.getString("locale");
-        String openCompanionAppDescription = bundle.getString("openCompanionAppDescription");
-        float triggerDistance = bundle.getFloat("triggerDistance");
-        String unableToConnectDescription = bundle.getString("unableToConnectDescription");
-        String unableToConnectTitle = bundle.getString("unableToConnectTitle");
-        String updateCompanionAppDescription = bundle.getString("updateCompanionAppDescription");
-    */
-    private static @Nullable FastPairDeviceMetadataParcel
-            convertToFastPairDeviceMetadata(Cache.StoredDiscoveryItem storedDiscoveryItem) {
-        FastPairStrings fpStrings = storedDiscoveryItem.getFastPairStrings();
-
-        FastPairDeviceMetadataParcel metadataParcel = new FastPairDeviceMetadataParcel();
-        metadataParcel.connectSuccessCompanionAppInstalled =
-                fpStrings.getPairingFinishedCompanionAppInstalled();
-        metadataParcel.connectSuccessCompanionAppNotInstalled =
-                fpStrings.getPairingFinishedCompanionAppNotInstalled();
-        metadataParcel.failConnectGoToSettingsDescription = fpStrings.getPairingFailDescription();
-        metadataParcel.initialNotificationDescription = fpStrings.getTapToPairWithAccount();
-        metadataParcel.initialNotificationDescriptionNoAccount =
-                fpStrings.getTapToPairWithoutAccount();
-        metadataParcel.initialPairingDescription = fpStrings.getInitialPairingDescription();
-        metadataParcel.retroactivePairingDescription = fpStrings.getRetroactivePairingDescription();
-        metadataParcel.subsequentPairingDescription = fpStrings.getSubsequentPairingDescription();
-        metadataParcel.waitLaunchCompanionAppDescription = fpStrings.getWaitAppLaunchDescription();
-
-        Cache.FastPairInformation fpInformation = storedDiscoveryItem.getFastPairInformation();
-        metadataParcel.trueWirelessImageUrlCase =
-                fpInformation.getTrueWirelessImages().getCaseUrl();
-        metadataParcel.trueWirelessImageUrlLeftBud =
-                fpInformation.getTrueWirelessImages().getLeftBudUrl();
-        metadataParcel.trueWirelessImageUrlRightBud =
-                fpInformation.getTrueWirelessImages().getRightBudUrl();
-        metadataParcel.deviceType = fpInformation.getDeviceType().getNumber();
-
-        return metadataParcel;
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
index 35251d8..e69d004 100644
--- a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
@@ -22,6 +22,10 @@
  * ArrayUtils class that help manipulate array.
  */
 public class ArrayUtils {
+    private static final char[] HEX_UPPERCASE = {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+    };
+
     /** Concatenate N arrays of bytes into a single array. */
     public static byte[] concatByteArrays(byte[]... arrays) {
         // Degenerate case - no input provided.
@@ -52,4 +56,67 @@
     public static boolean isEmpty(byte[] bytes) {
         return bytes == null || bytes.length == 0;
     }
+
+    /** Appends a byte array to a byte. */
+    public static byte[] append(byte a, byte[] b) {
+        if (b == null) {
+            return new byte[]{a};
+        }
+
+        int length = b.length;
+        byte[] result = new byte[length + 1];
+        result[0] = a;
+        System.arraycopy(b, 0, result, 1, length);
+        return result;
+    }
+
+    /**
+     * Converts an Integer to a 2-byte array.
+     */
+    public static byte[] intToByteArray(int value) {
+        return new byte[] {(byte) (value >> 8), (byte) value};
+    }
+
+    /** Appends a byte to a byte array. */
+    public static byte[] append(byte[] a, byte b) {
+        if (a == null) {
+            return new byte[]{b};
+        }
+
+        int length = a.length;
+        byte[] result = new byte[length + 1];
+        System.arraycopy(a, 0, result, 0, length);
+        result[length] = b;
+        return result;
+    }
+
+    /** Convert an hex string to a byte array. */
+
+    public static byte[] stringToBytes(String hex) throws IllegalArgumentException {
+        int length = hex.length();
+        if (length % 2 != 0) {
+            throw new IllegalArgumentException("Hex string has odd number of characters");
+        }
+        byte[] out = new byte[length / 2];
+        for (int i = 0; i < length; i += 2) {
+            // Byte.parseByte() doesn't work here because it expects a hex value in -128, 127, and
+            // our hex values are in 0, 255.
+            out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
+        }
+        return out;
+    }
+
+    /** Encodes a byte array as a hexadecimal representation of bytes. */
+    public static String bytesToStringUppercase(byte[] bytes) {
+        int length = bytes.length;
+        StringBuilder out = new StringBuilder(length * 2);
+        for (int i = 0; i < length; i++) {
+            if (i == length - 1 && (bytes[i] & 0xff) == 0) {
+                break;
+            }
+            out.append(HEX_UPPERCASE[(bytes[i] & 0xf0) >>> 4]);
+            out.append(HEX_UPPERCASE[bytes[i] & 0x0f]);
+        }
+        return out.toString();
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/util/Clock.java b/nearby/service/java/com/android/server/nearby/util/Clock.java
deleted file mode 100644
index 037b6f9..0000000
--- a/nearby/service/java/com/android/server/nearby/util/Clock.java
+++ /dev/null
@@ -1,55 +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.server.nearby.util;
-
-import android.os.SystemClock;
-
-/** Wrapper interface for time operations. Allows replacement of clock operations for testing. */
-public interface Clock {
-
-    /**
-     * Get the current time of the clock in milliseconds.
-     *
-     * @return Current time in milliseconds.
-     */
-    long currentTimeMillis();
-
-    /**
-     * Returns milliseconds since boot, including time spent in sleep.
-     *
-     * @return Current time since boot in milliseconds.
-     */
-    long elapsedRealtime();
-
-    /**
-     * Returns the current timestamp of the most precise timer available on the local system, in
-     * nanoseconds.
-     *
-     * @return Current time in nanoseconds.
-     */
-    long nanoTime();
-
-    /**
-     * Returns the time spent in the current thread, in milliseconds
-     *
-     * @return Thread time in milliseconds.
-     */
-    @SuppressWarnings("StaticOrDefaultInterfaceMethod")
-    default long currentThreadTimeMillis() {
-        return SystemClock.currentThreadTimeMillis();
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/DataUtils.java b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
deleted file mode 100644
index 12bf384..0000000
--- a/nearby/service/java/com/android/server/nearby/util/DataUtils.java
+++ /dev/null
@@ -1,110 +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.server.nearby.util;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import service.proto.Cache.ScanFastPairStoreItem;
-import service.proto.Cache.StoredDiscoveryItem;
-import service.proto.FastPairString.FastPairStrings;
-import service.proto.Rpcs.Device;
-import service.proto.Rpcs.GetObservedDeviceResponse;
-import service.proto.Rpcs.ObservedDeviceStrings;
-
-/**
- * Utils class converts different data types {@link ScanFastPairStoreItem},
- * {@link StoredDiscoveryItem} and {@link GetObservedDeviceResponse},
- *
- */
-public final class DataUtils {
-
-    /**
-     * Converts a {@link GetObservedDeviceResponse} to a {@link ScanFastPairStoreItem}.
-     */
-    public static ScanFastPairStoreItem toScanFastPairStoreItem(
-            GetObservedDeviceResponse observedDeviceResponse,
-            @NonNull String bleAddress, @NonNull String modelId, @Nullable String account) {
-        Device device = observedDeviceResponse.getDevice();
-        String deviceName = device.getName();
-        return ScanFastPairStoreItem.newBuilder()
-                .setAddress(bleAddress)
-                .setModelId(modelId)
-                .setActionUrl(device.getIntentUri())
-                .setDeviceName(deviceName)
-                .setIconPng(observedDeviceResponse.getImage())
-                .setIconFifeUrl(device.getImageUrl())
-                .setAntiSpoofingPublicKey(device.getAntiSpoofingKeyPair().getPublicKey())
-                .setFastPairStrings(getFastPairStrings(observedDeviceResponse, deviceName, account))
-                .build();
-    }
-
-    /**
-     * Prints readable string for a {@link ScanFastPairStoreItem}.
-     */
-    public static String toString(ScanFastPairStoreItem item) {
-        return "ScanFastPairStoreItem=[address:" + item.getAddress()
-                + ", actionUrl:" + item.getActionUrl()
-                + ", deviceName:" + item.getDeviceName()
-                + ", iconFifeUrl:" + item.getIconFifeUrl()
-                + ", fastPairStrings:" + toString(item.getFastPairStrings())
-                + "]";
-    }
-
-    /**
-     * Prints readable string for a {@link FastPairStrings}
-     */
-    public static String toString(FastPairStrings fastPairStrings) {
-        return "FastPairStrings["
-                + "tapToPairWithAccount=" + fastPairStrings.getTapToPairWithAccount()
-                + ", tapToPairWithoutAccount=" + fastPairStrings.getTapToPairWithoutAccount()
-                + ", initialPairingDescription=" + fastPairStrings.getInitialPairingDescription()
-                + ", pairingFinishedCompanionAppInstalled="
-                + fastPairStrings.getPairingFinishedCompanionAppInstalled()
-                + ", pairingFinishedCompanionAppNotInstalled="
-                + fastPairStrings.getPairingFinishedCompanionAppNotInstalled()
-                + ", subsequentPairingDescription="
-                + fastPairStrings.getSubsequentPairingDescription()
-                + ", retroactivePairingDescription="
-                + fastPairStrings.getRetroactivePairingDescription()
-                + ", waitAppLaunchDescription=" + fastPairStrings.getWaitAppLaunchDescription()
-                + ", pairingFailDescription=" + fastPairStrings.getPairingFailDescription()
-                + "]";
-    }
-
-    private static FastPairStrings getFastPairStrings(GetObservedDeviceResponse response,
-            String deviceName, @Nullable String account) {
-        ObservedDeviceStrings strings = response.getStrings();
-        return FastPairStrings.newBuilder()
-                .setTapToPairWithAccount(strings.getInitialNotificationDescription())
-                .setTapToPairWithoutAccount(
-                        strings.getInitialNotificationDescriptionNoAccount())
-                .setInitialPairingDescription(account == null
-                        ? strings.getInitialNotificationDescriptionNoAccount()
-                        : String.format(strings.getInitialPairingDescription(),
-                                deviceName, account))
-                .setPairingFinishedCompanionAppInstalled(
-                        strings.getConnectSuccessCompanionAppInstalled())
-                .setPairingFinishedCompanionAppNotInstalled(
-                        strings.getConnectSuccessCompanionAppNotInstalled())
-                .setSubsequentPairingDescription(strings.getSubsequentPairingDescription())
-                .setRetroactivePairingDescription(strings.getRetroactivePairingDescription())
-                .setWaitAppLaunchDescription(strings.getWaitLaunchCompanionAppDescription())
-                .setPairingFailDescription(strings.getFailConnectGoToSettingsDescription())
-                .build();
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/DefaultClock.java b/nearby/service/java/com/android/server/nearby/util/DefaultClock.java
deleted file mode 100644
index 61998e9..0000000
--- a/nearby/service/java/com/android/server/nearby/util/DefaultClock.java
+++ /dev/null
@@ -1,52 +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.server.nearby.util;
-
-import android.os.SystemClock;
-
-/** Default implementation of Clock. Instances of this class handle time operations. */
-public class DefaultClock implements Clock {
-
-    private static final DefaultClock sInstance = new DefaultClock();
-
-    /** Returns an instance of DefaultClock. */
-    public static Clock getsInstance() {
-        return sInstance;
-    }
-
-    @Override
-    public long currentTimeMillis() {
-        return System.currentTimeMillis();
-    }
-
-    @Override
-    public long elapsedRealtime() {
-        return SystemClock.elapsedRealtime();
-    }
-
-    @Override
-    public long nanoTime() {
-        return System.nanoTime();
-    }
-
-    @Override
-    public long currentThreadTimeMillis() {
-        return SystemClock.currentThreadTimeMillis();
-    }
-
-    public DefaultClock() {}
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/Environment.java b/nearby/service/java/com/android/server/nearby/util/Environment.java
deleted file mode 100644
index d397862..0000000
--- a/nearby/service/java/com/android/server/nearby/util/Environment.java
+++ /dev/null
@@ -1,63 +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.server.nearby.util;
-
-import android.content.ApexEnvironment;
-import android.content.pm.ApplicationInfo;
-import android.os.UserHandle;
-
-import java.io.File;
-
-/**
- * Provides function to make sure the function caller is from the same apex.
- */
-public class Environment {
-    /**
-     * NEARBY apex name.
-     */
-    private static final String NEARBY_APEX_NAME = "com.android.tethering";
-
-    /**
-     * The path where the Nearby apex is mounted.
-     * Current value = "/apex/com.android.tethering"
-     */
-    private static final String NEARBY_APEX_PATH =
-            new File("/apex", NEARBY_APEX_NAME).getAbsolutePath();
-
-    /**
-     * Nearby shared folder.
-     */
-    public static File getNearbyDirectory() {
-        return ApexEnvironment.getApexEnvironment(NEARBY_APEX_NAME).getDeviceProtectedDataDir();
-    }
-
-    /**
-     * Nearby user specific folder.
-     */
-    public static File getNearbyDirectory(int userId) {
-        return ApexEnvironment.getApexEnvironment(NEARBY_APEX_NAME)
-                .getCredentialProtectedDataDirForUser(UserHandle.of(userId));
-    }
-
-    /**
-     * Returns true if the app is in the nearby apex, false otherwise.
-     * Checks if the app's path starts with "/apex/com.android.tethering".
-     */
-    public static boolean isAppInNearbyApex(ApplicationInfo appInfo) {
-        return appInfo.sourceDir.startsWith(NEARBY_APEX_PATH);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
index 3c5132d..ba9ca41 100644
--- a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
@@ -22,6 +22,10 @@
 
 import androidx.annotation.Nullable;
 
+import com.google.common.hash.Hashing;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 
@@ -30,22 +34,28 @@
 
 /** Class for encryption/decryption functionality. */
 public abstract class Cryptor {
+    /**
+     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+     */
+    public static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+    public static final byte[] NP_HKDF_SALT = "Google Nearby".getBytes(StandardCharsets.US_ASCII);
 
     /** AES only supports key sizes of 16, 24 or 32 bytes. */
     static final int AUTHENTICITY_KEY_BYTE_SIZE = 16;
 
-    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+    public static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
 
     /**
      * Encrypt the provided data blob.
      *
      * @param data data blob to be encrypted.
-     * @param salt used for IV
+     * @param iv advertisement nonce
      * @param secretKeyBytes secrete key accessed from credentials
      * @return encrypted data, {@code null} if failed to encrypt.
      */
     @Nullable
-    public byte[] encrypt(byte[] data, byte[] salt, byte[] secretKeyBytes) {
+    public byte[] encrypt(byte[] data, byte[] iv, byte[] secretKeyBytes) {
         return data;
     }
 
@@ -53,12 +63,12 @@
      * Decrypt the original data blob from the provided byte array.
      *
      * @param encryptedData data blob to be decrypted.
-     * @param salt used for IV
+     * @param iv advertisement nonce
      * @param secretKeyBytes secrete key accessed from credentials
      * @return decrypted data, {@code null} if failed to decrypt.
      */
     @Nullable
-    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] secretKeyBytes) {
+    public byte[] decrypt(byte[] encryptedData, byte[] iv, byte[] secretKeyBytes) {
         return encryptedData;
     }
 
@@ -90,7 +100,7 @@
      * A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
      * given size.
      */
-    // Based on google3/third_party/tink/java/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+    // Based on crypto/tink/subtle/Hkdf.java
     @Nullable
     static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
         Mac mac;
@@ -146,4 +156,66 @@
 
         return result;
     }
+
+    /**
+     * Computes an HKDF.
+     *
+     * @param macAlgorithm the MAC algorithm used for computing the Hkdf. I.e., "HMACSHA1" or
+     *                     "HMACSHA256".
+     * @param ikm          the input keying material.
+     * @param salt         optional salt. A possibly non-secret random value.
+     *                     (If no salt is provided i.e. if salt has length 0)
+     *                     then an array of 0s of the same size as the hash
+     *                     digest is used as salt.
+     * @param info         optional context and application specific information.
+     * @param size         The length of the generated pseudorandom string in bytes.
+     * @throws GeneralSecurityException if the {@code macAlgorithm} is not supported or if {@code
+     *                                  size} is too large or if {@code salt} is not a valid key for
+     *                                  macAlgorithm (which should not
+     *                                  happen since HMAC allows key sizes up to 2^64).
+     */
+    public static byte[] computeHkdf(
+            String macAlgorithm, final byte[] ikm, final byte[] salt, final byte[] info, int size)
+            throws GeneralSecurityException {
+        Mac mac = Mac.getInstance(macAlgorithm);
+        if (size > 255 * mac.getMacLength()) {
+            throw new GeneralSecurityException("size too large");
+        }
+        if (salt == null || salt.length == 0) {
+            // According to RFC 5869, Section 2.2 the salt is optional. If no salt is provided
+            // then HKDF uses a salt that is an array of zeros of the same length as the hash
+            // digest.
+            mac.init(new SecretKeySpec(new byte[mac.getMacLength()], macAlgorithm));
+        } else {
+            mac.init(new SecretKeySpec(salt, macAlgorithm));
+        }
+        byte[] prk = mac.doFinal(ikm);
+        byte[] result = new byte[size];
+        int ctr = 1;
+        int pos = 0;
+        mac.init(new SecretKeySpec(prk, macAlgorithm));
+        byte[] digest = new byte[0];
+        while (true) {
+            mac.update(digest);
+            mac.update(info);
+            mac.update((byte) ctr);
+            digest = mac.doFinal();
+            if (pos + digest.length < size) {
+                System.arraycopy(digest, 0, result, pos, digest.length);
+                pos += digest.length;
+                ctr++;
+            } else {
+                System.arraycopy(digest, 0, result, pos, size - pos);
+                break;
+            }
+        }
+        return result;
+    }
+
+    /** Generates the HMAC bytes. */
+    public static byte[] generateHmac(String algorithm, byte[] input, byte[] key) {
+        return Hashing.hmacSha256(new SecretKeySpec(key, algorithm))
+                .hashBytes(input)
+                .asBytes();
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
deleted file mode 100644
index 1c0ec9e..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
+++ /dev/null
@@ -1,39 +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.server.nearby.util.encryption;
-
-import androidx.annotation.Nullable;
-
-/**
- * A Cryptor that returns the original data without actual encryption
- */
-public class CryptorImpFake extends Cryptor {
-    // Lazily instantiated when {@link #getInstance()} is called.
-    @Nullable
-    private static CryptorImpFake sCryptor;
-
-    /** Returns an instance of CryptorImpFake. */
-    public static CryptorImpFake getInstance() {
-        if (sCryptor == null) {
-            sCryptor = new CryptorImpFake();
-        }
-        return sCryptor;
-    }
-
-    private CryptorImpFake() {
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
deleted file mode 100644
index b0e19b4..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
+++ /dev/null
@@ -1,208 +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.server.nearby.util.encryption;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for identity
- * encryption and decryption.
- */
-public class CryptorImpIdentityV1 extends Cryptor {
-
-    // 3 16 byte arrays known by both the encryptor and decryptor.
-    private static final byte[] EK_IV =
-            new byte[] {14, -123, -39, 42, 109, 127, 83, 27, 27, 11, 91, -38, 92, 17, -84, 66};
-    private static final byte[] ESALT_IV =
-            new byte[] {46, 83, -19, 10, -127, -31, -31, 12, 31, 76, 63, -9, 33, -66, 15, -10};
-    private static final byte[] KTAG_IV =
-            {-22, -83, -6, 67, 16, -99, -13, -9, 8, -3, -16, 37, -75, 47, 1, -56};
-
-    /** Length of encryption key required by AES/GCM encryption. */
-    private static final int ENCRYPTION_KEY_SIZE = 32;
-
-    /** Length of salt required by AES/GCM encryption. */
-    private static final int AES_CTR_IV_SIZE = 16;
-
-    /** Length HMAC tag */
-    public static final int HMAC_TAG_SIZE = 8;
-
-    /**
-     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
-     */
-    private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
-
-    @VisibleForTesting
-    static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
-
-    // Lazily instantiated when {@link #getInstance()} is called.
-    @Nullable private static CryptorImpIdentityV1 sCryptor;
-
-    /** Returns an instance of CryptorImpIdentityV1. */
-    public static CryptorImpIdentityV1 getInstance() {
-        if (sCryptor == null) {
-            sCryptor = new CryptorImpIdentityV1();
-        }
-        return sCryptor;
-    }
-
-    @Nullable
-    @Override
-    public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Encrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-        byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
-        if (esalt == null) {
-            Log.e(TAG, "Failed to generate salt.");
-            return null;
-        }
-        try {
-            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(esalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-        try {
-            return cipher.doFinal(data);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-    }
-
-    @Nullable
-    @Override
-    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Decrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to get cipher instance.", e);
-            return null;
-        }
-        byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
-        if (esalt == null) {
-            return null;
-        }
-        try {
-            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(esalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-
-        try {
-            return cipher.doFinal(encryptedData);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
-            return null;
-        }
-    }
-
-    /**
-     * Generates a digital signature for the data.
-     *
-     * @return signature {@code null} if failed to sign
-     */
-    @Nullable
-    @Override
-    public byte[] sign(byte[] data, byte[] salt) {
-        if (data == null) {
-            Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
-            return null;
-        }
-
-        // Generates a 8 bytes HMAC tag
-        return Cryptor.computeHkdf(data, salt, HMAC_TAG_SIZE);
-    }
-
-    /**
-     * Generates a digital signature for the data.
-     * Uses KTAG_IV as salt value.
-     */
-    @Nullable
-    public byte[] sign(byte[] data) {
-        // Generates a 8 bytes HMAC tag
-        return sign(data, KTAG_IV);
-    }
-
-    @Override
-    public boolean verify(byte[] data, byte[] key, byte[] signature) {
-        return Arrays.equals(sign(data, key), signature);
-    }
-
-    /**
-     * Verifies the signature generated by data and key, with the original signed data. Uses
-     * KTAG_IV as salt value.
-     */
-    public boolean verify(byte[] data, byte[] signature) {
-        return verify(data, KTAG_IV, signature);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
deleted file mode 100644
index 15073fb..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
+++ /dev/null
@@ -1,212 +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.server.nearby.util.encryption;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for encryption and decryption.
- */
-public class CryptorImpV1 extends Cryptor {
-
-    /**
-     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
-     */
-    private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
-
-    @VisibleForTesting
-    static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
-
-    /** Length of encryption key required by AES/GCM encryption. */
-    private static final int ENCRYPTION_KEY_SIZE = 32;
-
-    /** Length of salt required by AES/GCM encryption. */
-    private static final int AES_CTR_IV_SIZE = 16;
-
-    /** Length HMAC tag */
-    public static final int HMAC_TAG_SIZE = 16;
-
-    // 3 16 byte arrays known by both the encryptor and decryptor.
-    private static final byte[] AK_IV =
-            new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
-    private static final byte[] ASALT_IV =
-            new byte[] {111, 48, -83, -79, -10, -102, -16, 73, 43, 55, 102, -127, 58, -19, -113, 4};
-    private static final byte[] HK_IV =
-            new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
-
-    // Lazily instantiated when {@link #getInstance()} is called.
-    @Nullable private static CryptorImpV1 sCryptor;
-
-    /** Returns an instance of CryptorImpV1. */
-    public static CryptorImpV1 getInstance() {
-        if (sCryptor == null) {
-            sCryptor = new CryptorImpV1();
-        }
-        return sCryptor;
-    }
-
-    private CryptorImpV1() {
-    }
-
-    @Nullable
-    @Override
-    public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Encrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-        byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
-        if (asalt == null) {
-            Log.e(TAG, "Failed to generate salt.");
-            return null;
-        }
-        try {
-            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(asalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-        try {
-            return cipher.doFinal(data);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-    }
-
-    @Nullable
-    @Override
-    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Decrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to get cipher instance.", e);
-            return null;
-        }
-        byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
-        if (asalt == null) {
-            return null;
-        }
-        try {
-            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(asalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-
-        try {
-            return cipher.doFinal(encryptedData);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
-            return null;
-        }
-    }
-
-    @Override
-    @Nullable
-    public byte[] sign(byte[] data, byte[] key) {
-        return generateHmacTag(data, key);
-    }
-
-    @Override
-    public int getSignatureLength() {
-        return HMAC_TAG_SIZE;
-    }
-
-    @Override
-    public boolean verify(byte[] data, byte[] key, byte[] signature) {
-        return Arrays.equals(sign(data, key), signature);
-    }
-
-    /** Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
-     * is equal to HMAC tag in advertisement to see data integrity. */
-    @Nullable
-    @VisibleForTesting
-    byte[] generateHmacTag(byte[] data, byte[] authenticityKey) {
-        if (data == null || authenticityKey == null) {
-            Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
-            return null;
-        }
-
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.e(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes HMAC key from authenticity_key
-        byte[] hmacKey = Cryptor.computeHkdf(authenticityKey, HK_IV, AES_CTR_IV_SIZE);
-        if (hmacKey == null) {
-            Log.e(TAG, "Failed to generate HMAC key.");
-            return null;
-        }
-
-        // Generates a 16 bytes HMAC tag from authenticity_key
-        return Cryptor.computeHkdf(data, hmacKey, HMAC_TAG_SIZE);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java
new file mode 100644
index 0000000..3dbf85c
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java
@@ -0,0 +1,286 @@
+/*
+ * 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.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.util.ArrayUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * MIC encryption and decryption for {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1}
+ * advertisement
+ */
+public class CryptorMicImp extends Cryptor {
+
+    public static final int MIC_LENGTH = 16;
+
+    private static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+    private static final byte[] AES_KEY_INFO_BYTES = "Unsigned Section AES key".getBytes(
+            StandardCharsets.US_ASCII);
+    private static final byte[] ADV_NONCE_INFO_BYTES_SALT_DE = "Unsigned Section IV".getBytes(
+            StandardCharsets.US_ASCII);
+    private static final byte[] ADV_NONCE_INFO_BYTES_ENCRYPTION_INFO_DE =
+            "V1 derived salt".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] METADATA_KEY_HMAC_KEY_INFO_BYTES =
+            "Unsigned Section metadata key HMAC key".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] MIC_HMAC_KEY_INFO_BYTES = "Unsigned Section HMAC key".getBytes(
+            StandardCharsets.US_ASCII);
+    private static final int AES_KEY_SIZE = 16;
+    private static final int ADV_NONCE_SIZE_SALT_DE = 16;
+    private static final int ADV_NONCE_SIZE_ENCRYPTION_INFO_DE = 12;
+    private static final int HMAC_KEY_SIZE = 32;
+
+    // Lazily instantiated when {@link #getInstance()} is called.
+    @Nullable
+    private static CryptorMicImp sCryptor;
+
+    private CryptorMicImp() {
+    }
+
+    /** Returns an instance of CryptorImpV1. */
+    public static CryptorMicImp getInstance() {
+        if (sCryptor == null) {
+            sCryptor = new CryptorMicImp();
+        }
+        return sCryptor;
+    }
+
+    /**
+     * Generate the meta data encryption key tag
+     * @param metadataEncryptionKey used as identity
+     * @param keySeed authenticity key saved in local and shared credential
+     * @return bytes generated by hmac or {@code null} when there is an error
+     */
+    @Nullable
+    public static byte[] generateMetadataEncryptionKeyTag(byte[] metadataEncryptionKey,
+            byte[] keySeed) {
+        try {
+            byte[] metadataKeyHmacKey = generateMetadataKeyHmacKey(keySeed);
+            return Cryptor.generateHmac(/* algorithm= */ HMAC_SHA256_ALGORITHM, /* input= */
+                    metadataEncryptionKey, /* key= */ metadataKeyHmacKey);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Failed to generate Metadata encryption key tag.", e);
+            return null;
+        }
+    }
+
+    /**
+     * @param salt from the 2 bytes Salt Data Element
+     */
+    @Nullable
+    public static byte[] generateAdvNonce(byte[] salt) throws GeneralSecurityException {
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ salt,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ ADV_NONCE_INFO_BYTES_SALT_DE,
+                /* size= */ ADV_NONCE_SIZE_SALT_DE);
+    }
+
+    /** Generates the 12 bytes nonce with salt from the 2 bytes Salt Data Element */
+    @Nullable
+    public static byte[] generateAdvNonce(byte[] salt, int deIndex)
+            throws GeneralSecurityException {
+        // go/nearby-specs-working-doc
+        // Indices are encoded as big-endian unsigned 32-bit integers, starting at 1.
+        // Index 0 is reserved
+        byte[] indexBytes = new byte[4];
+        indexBytes[3] = (byte) deIndex;
+        byte[] info =
+                ArrayUtils.concatByteArrays(ADV_NONCE_INFO_BYTES_ENCRYPTION_INFO_DE, indexBytes);
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ salt,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ info,
+                /* size= */ ADV_NONCE_SIZE_ENCRYPTION_INFO_DE);
+    }
+
+    @Nullable
+    @Override
+    public byte[] encrypt(byte[] input, byte[] iv, byte[] keySeed) {
+        if (input == null || iv == null || keySeed == null) {
+            return null;
+        }
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            Log.e(TAG, "Failed to encrypt with secret key.", e);
+            return null;
+        }
+
+        byte[] aesKey;
+        try {
+            aesKey = generateAesKey(keySeed);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Encryption failed because failed to generate the AES key.", e);
+            return null;
+        }
+        if (aesKey == null) {
+            Log.i(TAG, "Failed to generate the AES key.");
+            return null;
+        }
+        SecretKey secretKey = new SecretKeySpec(aesKey, ENCRYPT_ALGORITHM);
+        try {
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Failed to initialize cipher.", e);
+            return null;
+
+        }
+        try {
+            return cipher.doFinal(input);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            Log.e(TAG, "Failed to encrypt with secret key.", e);
+            return null;
+        }
+    }
+
+    @Nullable
+    @Override
+    public byte[] decrypt(byte[] encryptedData, byte[] iv, byte[] keySeed) {
+        if (encryptedData == null || iv == null || keySeed == null) {
+            return null;
+        }
+
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            Log.e(TAG, "Failed to get cipher instance.", e);
+            return null;
+        }
+        byte[] aesKey;
+        try {
+            aesKey = generateAesKey(keySeed);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Decryption failed because failed to generate the AES key.", e);
+            return null;
+        }
+        SecretKey secretKey = new SecretKeySpec(aesKey, ENCRYPT_ALGORITHM);
+        try {
+            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Failed to initialize cipher.", e);
+            return null;
+        }
+
+        try {
+            return cipher.doFinal(encryptedData);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+            return null;
+        }
+    }
+
+    @Override
+    @Nullable
+    public byte[] sign(byte[] data, byte[] key) {
+        byte[] res = generateHmacTag(data, key);
+        return res;
+    }
+
+    @Override
+    public int getSignatureLength() {
+        return MIC_LENGTH;
+    }
+
+    @Override
+    public boolean verify(byte[] data, byte[] key, byte[] signature) {
+        return Arrays.equals(sign(data, key), signature);
+    }
+
+    /**
+     * Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
+     * is equal to HMAC tag in advertisement to see data integrity.
+     *
+     * @param input   concatenated advertisement UUID, header, section header, derived salt, and
+     *                section content
+     * @param keySeed the MIC HMAC key is calculated using the derived key
+     * @return the first 16 bytes of HMAC-SHA256 result
+     */
+    @Nullable
+    @VisibleForTesting
+    byte[] generateHmacTag(byte[] input, byte[] keySeed) {
+        try {
+            if (input == null || keySeed == null) {
+                return null;
+            }
+            byte[] micHmacKey = generateMicHmacKey(keySeed);
+            byte[] hmac = Cryptor.generateHmac(/* algorithm= */ HMAC_SHA256_ALGORITHM, /* input= */
+                    input, /* key= */ micHmacKey);
+            if (ArrayUtils.isEmpty(hmac)) {
+                return null;
+            }
+            return Arrays.copyOf(hmac, MIC_LENGTH);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Failed to generate mic hmac key.", e);
+            return null;
+        }
+    }
+
+    @Nullable
+    private static byte[] generateAesKey(byte[] keySeed) throws GeneralSecurityException {
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ keySeed,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ AES_KEY_INFO_BYTES,
+                /* size= */ AES_KEY_SIZE);
+    }
+
+    private static byte[] generateMetadataKeyHmacKey(byte[] keySeed)
+            throws GeneralSecurityException {
+        return generateHmacKey(keySeed, METADATA_KEY_HMAC_KEY_INFO_BYTES);
+    }
+
+    private static byte[] generateMicHmacKey(byte[] keySeed) throws GeneralSecurityException {
+        return generateHmacKey(keySeed, MIC_HMAC_KEY_INFO_BYTES);
+    }
+
+    private static byte[] generateHmacKey(byte[] keySeed, byte[] info)
+            throws GeneralSecurityException {
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ keySeed,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ info,
+                /* size= */ HMAC_KEY_SIZE);
+    }
+}
diff --git a/nearby/service/lint-baseline.xml b/nearby/service/lint-baseline.xml
index a4761ab..3477594 100644
--- a/nearby/service/lint-baseline.xml
+++ b/nearby/service/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
@@ -8,7 +8,7 @@
         errorLine2="                                                     ~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java"
-            line="263"
+            line="289"
             column="54"/>
     </issue>
 
diff --git a/nearby/service/proto/Android.bp b/nearby/service/proto/Android.bp
index 1b00cf6..be5a0b3 100644
--- a/nearby/service/proto/Android.bp
+++ b/nearby/service/proto/Android.bp
@@ -12,6 +12,7 @@
 // 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"],
 }
 
@@ -41,4 +42,4 @@
     apex_available: [
         "com.android.tethering",
     ],
-}
\ No newline at end of file
+}
diff --git a/nearby/service/proto/src/fastpair/cache.proto b/nearby/service/proto/src/fastpair/cache.proto
deleted file mode 100644
index d4c7c3d..0000000
--- a/nearby/service/proto/src/fastpair/cache.proto
+++ /dev/null
@@ -1,427 +0,0 @@
-syntax = "proto3";
-package service.proto;
-import "src/fastpair/rpcs.proto";
-import "src/fastpair/fast_pair_string.proto";
-
-// db information for Fast Pair that gets from server.
-message ServerResponseDbItem {
-  // Device's model id.
-  string model_id = 1;
-
-  // Response was received from the server. Contains data needed to display
-  // FastPair notification such as device name, txPower of device, image used
-  // in the notification, etc.
-  GetObservedDeviceResponse get_observed_device_response = 2;
-
-  // The timestamp that make the server fetch.
-  int64 last_fetch_info_timestamp_millis = 3;
-
-  // Whether the item in the cache is expirable or not (when offline mode this
-  // will be false).
-  bool expirable = 4;
-}
-
-
-// Client side scan result.
-message StoredScanResult {
-  // REQUIRED
-  // Unique ID generated based on scan result
-  string id = 1;
-
-  // REQUIRED
-  NearbyType type = 2;
-
-  // REQUIRED
-  // The most recent all upper case mac associated with this item.
-  // (Mac-to-DiscoveryItem is a many-to-many relationship)
-  string mac_address = 4;
-
-  // Beacon's RSSI value
-  int32 rssi = 10;
-
-  // Beacon's tx power
-  int32 tx_power = 11;
-
-  // The mac address encoded in beacon advertisement. Currently only used by
-  // chromecast.
-  string device_setup_mac = 12;
-
-  // Uptime of the device in minutes. Stops incrementing at 255.
-  int32 uptime_minutes = 13;
-
-  // REQUIRED
-  // Client timestamp when the beacon was first observed in BLE scan.
-  int64 first_observation_timestamp_millis = 14;
-
-  // REQUIRED
-  // Client timestamp when the beacon was last observed in BLE scan.
-  int64 last_observation_timestamp_millis = 15;
-
-  // Deprecated fields.
-  reserved 3, 5, 6, 7, 8, 9;
-}
-
-
-// Data for a DiscoveryItem created from server response and client scan result.
-// Only caching original data from scan result, server response, timestamps
-// and user actions. Do not save generated data in this object.
-// Next ID: 50
-message StoredDiscoveryItem {
-  enum State {
-    // Default unknown state.
-    STATE_UNKNOWN = 0;
-
-    // The item is normal.
-    STATE_ENABLED = 1;
-
-    // The item has been muted by user.
-    STATE_MUTED = 2;
-
-    // The item has been disabled by us (likely temporarily).
-    STATE_DISABLED_BY_SYSTEM = 3;
-  }
-
-  // The status of the item.
-  // TODO(b/204409421) remove enum
-  enum DebugMessageCategory {
-    // Default unknown state.
-    STATUS_UNKNOWN = 0;
-
-    // The item is valid and visible in notification.
-    STATUS_VALID_NOTIFICATION = 1;
-
-    // The item made it to list but not to notification.
-    STATUS_VALID_LIST_VIEW = 2;
-
-    // The item is filtered out on client. Never made it to list view.
-    STATUS_DISABLED_BY_CLIENT = 3;
-
-    // The item is filtered out by server. Never made it to client.
-    STATUS_DISABLED_BY_SERVER = 4;
-  }
-
-  enum ExperienceType {
-    EXPERIENCE_UNKNOWN = 0;
-    EXPERIENCE_GOOD = 1;
-    EXPERIENCE_BAD = 2;
-  }
-
-  // REQUIRED
-  // Offline item: unique ID generated on client.
-  // Online item: unique ID generated on server.
-  string id = 1;
-
-  // REQUIRED
-  // The most recent all upper case mac associated with this item.
-  // (Mac-to-DiscoveryItem is a many-to-many relationship)
-  string mac_address = 4;
-
-  // REQUIRED
-  string action_url = 5;
-
-  // The bluetooth device name from advertisment
-  string device_name = 6;
-
-  // REQUIRED
-  // Item's title
-  string title = 7;
-
-  // Item's description.
-  string description = 8;
-
-  // The URL for display
-  string display_url = 9;
-
-  // REQUIRED
-  // Client timestamp when the beacon was last observed in BLE scan.
-  int64 last_observation_timestamp_millis = 10;
-
-  // REQUIRED
-  // Client timestamp when the beacon was first observed in BLE scan.
-  int64 first_observation_timestamp_millis = 11;
-
-  // REQUIRED
-  // Item's current state. e.g. if the item is blocked.
-  State state = 17;
-
-  // The resolved url type for the action_url.
-  ResolvedUrlType action_url_type = 19;
-
-  // The timestamp when the user is redirected to Play Store after clicking on
-  // the item.
-  int64 pending_app_install_timestamp_millis = 20;
-
-  // Beacon's RSSI value
-  int32 rssi = 22;
-
-  // Beacon's tx power
-  int32 tx_power = 23;
-
-  // Human readable name of the app designated to open the uri
-  // Used in the second line of the notification, "Open in {} app"
-  string app_name = 25;
-
-  // The timestamp when the attachment was created on PBS server. In case there
-  // are duplicate
-  // items with the same scanId/groupID, only show the one with the latest
-  // timestamp.
-  int64 attachment_creation_sec = 28;
-
-  // Package name of the App that owns this item.
-  string package_name = 30;
-
-  // The average star rating of the app.
-  float star_rating = 31;
-
-  // TriggerId identifies the trigger/beacon that is attached with a message.
-  // It's generated from server for online messages to synchronize formatting
-  // across client versions.
-  // Example:
-  // * BLE_UID: 3||deadbeef
-  // * BLE_URL: http://trigger.id
-  // See go/discovery-store-message-and-trigger-id for more details.
-  string trigger_id = 34;
-
-  // Bytes of item icon in PNG format displayed in Discovery item list.
-  bytes icon_png = 36;
-
-  // A FIFE URL of the item icon displayed in Discovery item list.
-  string icon_fife_url = 49;
-
-  // See equivalent field in NearbyItem.
-  bytes authentication_public_key_secp256r1 = 45;
-
-  // See equivalent field in NearbyItem.
-  FastPairInformation fast_pair_information = 46;
-
-  // Companion app detail.
-  CompanionAppDetails companion_detail = 47;
-
-  // Fast pair strings
-  FastPairStrings fast_pair_strings = 48;
-
-  // Deprecated fields.
-  reserved 2, 3, 12, 13, 14, 15, 16, 18, 21, 24, 26, 27, 29, 32, 33, 35, 37, 38, 39, 40, 41, 42, 43, 44;
-}
-enum ResolvedUrlType {
-  RESOLVED_URL_TYPE_UNKNOWN = 0;
-
-  // The url is resolved to a web page that is not a play store app.
-  // This can be considered as the default resolved type when it's
-  // not the other specific types.
-  WEBPAGE = 1;
-
-  // The url is resolved to the Google Play store app
-  // ie. play.google.com/store
-  APP = 2;
-}
-enum DiscoveryAttachmentType {
-  DISCOVERY_ATTACHMENT_TYPE_UNKNOWN = 0;
-
-  // The attachment is posted in the prod namespace (without "-debug")
-  DISCOVERY_ATTACHMENT_TYPE_NORMAL = 1;
-
-  // The attachment is posted in the debug namespace (with "-debug")
-  DISCOVERY_ATTACHMENT_TYPE_DEBUG = 2;
-}
-// Additional information relevant only for Fast Pair devices.
-message FastPairInformation {
-  // When true, Fast Pair will only create a bond with the device and not
-  // attempt to connect any profiles (for example, A2DP or HFP).
-  bool data_only_connection = 1;
-
-  // Additional images that are attached specifically for true wireless Fast
-  // Pair devices.
-  TrueWirelessHeadsetImages true_wireless_images = 3;
-
-  // When true, this device can support assistant function.
-  bool assistant_supported = 4;
-
-  // Features supported by the Fast Pair device.
-  repeated FastPairFeature features = 5;
-
-  // Optional, the name of the company producing this Fast Pair device.
-  string company_name = 6;
-
-  // Optional, the type of device.
-  DeviceType device_type = 7;
-
-  reserved 2;
-}
-
-
-enum NearbyType {
-  NEARBY_TYPE_UNKNOWN = 0;
-  // Proximity Beacon Service (PBS). This is the only type of nearbyItems which
-  // can be customized by 3p and therefore the intents passed should not be
-  // completely trusted. Deprecated already.
-  NEARBY_PROXIMITY_BEACON = 1;
-  // Physical Web URL beacon. Deprecated already.
-  NEARBY_PHYSICAL_WEB = 2;
-  // Chromecast beacon. Used on client-side only.
-  NEARBY_CHROMECAST = 3;
-  // Wear beacon. Used on client-side only.
-  NEARBY_WEAR = 4;
-  // A device (e.g. a Magic Pair device that needs to be set up). The special-
-  // case devices above (e.g. ChromeCast, Wear) might migrate to this type.
-  NEARBY_DEVICE = 6;
-  // Popular apps/urls based on user's current geo-location.
-  NEARBY_POPULAR_HERE = 7;
-
-  reserved 5;
-}
-
-// A locally cached Fast Pair device associating an account key with the
-// bluetooth address of the device.
-message StoredFastPairItem {
-  // The device's public mac address.
-  string mac_address = 1;
-
-  // The account key written to the device.
-  bytes account_key = 2;
-
-  // When user need to update provider name, enable this value to trigger
-  // writing new name to provider.
-  bool need_to_update_provider_name = 3;
-
-  // The retry times to update name into provider.
-  int32 update_name_retries = 4;
-
-  // Latest firmware version from the server.
-  string latest_firmware_version = 5;
-
-  // The firmware version that is on the device.
-  string device_firmware_version = 6;
-
-  // The timestamp from the last time we fetched the firmware version from the
-  // device.
-  int64 last_check_firmware_timestamp_millis = 7;
-
-  // The timestamp from the last time we fetched the firmware version from
-  // server.
-  int64 last_server_query_timestamp_millis = 8;
-
-  // Only allows one bloom filter check process to create gatt connection and
-  // try to read the firmware version value.
-  bool can_read_firmware = 9;
-
-  // Device's model id.
-  string model_id = 10;
-
-  // Features that this Fast Pair device supports.
-  repeated FastPairFeature features = 11;
-
-  // Keeps the stored discovery item in local cache, we can have most
-  // information of fast pair device locally without through footprints, i.e. we
-  // can have most fast pair features locally.
-  StoredDiscoveryItem discovery_item = 12;
-
-  // When true, the latest uploaded event to FMA is connected. We use
-  // it as the previous ACL state when getting the BluetoothAdapter STATE_OFF to
-  // determine if need to upload the disconnect event to FMA.
-  bool fma_state_is_connected = 13;
-
-  // Device's buffer size range.
-  repeated BufferSizeRange buffer_size_range = 18;
-
-  // The additional account key if this device could be associated with multiple
-  // accounts. Notes that for this device, the account_key field is the basic
-  // one which will not be associated with the accounts.
-  repeated bytes additional_account_key = 19;
-
-  // Deprecated fields.
-  reserved 14, 15, 16, 17;
-}
-
-// Contains information about Fast Pair devices stored through our scanner.
-// Next ID: 29
-message ScanFastPairStoreItem {
-  // Device's model id.
-  string model_id = 1;
-
-  // Device's RSSI value
-  int32 rssi = 2;
-
-  // Device's tx power
-  int32 tx_power = 3;
-
-  // Bytes of item icon in PNG format displayed in Discovery item list.
-  bytes icon_png = 4;
-
-  // A FIFE URL of the item icon displayed in Discovery item list.
-  string icon_fife_url = 28;
-
-  // Device name like "Bose QC 35".
-  string device_name = 5;
-
-  // Client timestamp when user last saw Fast Pair device.
-  int64 last_observation_timestamp_millis = 6;
-
-  // Action url after user click the notification.
-  string action_url = 7;
-
-  // Device's bluetooth address.
-  string address = 8;
-
-  // The computed threshold rssi value that would trigger FastPair notifications
-  int32 threshold_rssi = 9;
-
-  // Populated with the contents of the bloom filter in the event that
-  // the scanned device is advertising a bloom filter instead of a model id
-  bytes bloom_filter = 10;
-
-  // Device name from the BLE scan record
-  string ble_device_name = 11;
-
-  // Strings used for the FastPair UI
-  FastPairStrings fast_pair_strings = 12;
-
-  // A key used to authenticate advertising device.
-  // See NearbyItem.authentication_public_key_secp256r1 for more information.
-  bytes anti_spoofing_public_key = 13;
-
-  // When true, Fast Pair will only create a bond with the device and not
-  // attempt to connect any profiles (for example, A2DP or HFP).
-  bool data_only_connection = 14;
-
-  // The type of the manufacturer (first party, third party, etc).
-  int32 manufacturer_type_num = 15;
-
-  // Additional images that are attached specifically for true wireless Fast
-  // Pair devices.
-  TrueWirelessHeadsetImages true_wireless_images = 16;
-
-  // When true, this device can support assistant function.
-  bool assistant_supported = 17;
-
-  // Optional, the name of the company producing this Fast Pair device.
-  string company_name = 18;
-
-  // Features supported by the Fast Pair device.
-  FastPairFeature features = 19;
-
-  // The interaction type that this scan should trigger
-  InteractionType interaction_type = 20;
-
-  // The copy of the advertisement bytes, used to pass along to other
-  // apps that use Fast Pair as the discovery vehicle.
-  bytes full_ble_record = 21;
-
-  // Companion app related information
-  CompanionAppDetails companion_detail = 22;
-
-  // Client timestamp when user first saw Fast Pair device.
-  int64 first_observation_timestamp_millis = 23;
-
-  // The type of the device (wearable, headphones, etc).
-  int32 device_type_num = 24;
-
-  // The type of notification (app launch smart setup, etc).
-  NotificationType notification_type = 25;
-
-  // The customized title.
-  string customized_title = 26;
-
-  // The customized description.
-  string customized_description = 27;
-}
diff --git a/nearby/service/proto/src/fastpair/data.proto b/nearby/service/proto/src/fastpair/data.proto
deleted file mode 100644
index 6f4fadd..0000000
--- a/nearby/service/proto/src/fastpair/data.proto
+++ /dev/null
@@ -1,26 +0,0 @@
-syntax = "proto3";
-
-package service.proto;
-import "src/fastpair/cache.proto";
-
-// A device that has been Fast Paired with.
-message FastPairDeviceWithAccountKey {
-  // The account key which was written to the device after pairing completed.
-  bytes account_key = 1;
-
-  // The stored discovery item which represents the notification that should be
-  // associated with the device. Note, this is stored as a raw byte array
-  // instead of StoredDiscoveryItem because icing only supports proto lite and
-  // StoredDiscoveryItem is handed around as a nano proto in implementation,
-  // which are not compatible with each other.
-  StoredDiscoveryItem discovery_item = 3;
-
-  // SHA256 of "account key + headset's public address", this is used to
-  // identify the paired headset. Because of adding account key to generate the
-  // hash value, it makes the information anonymous, even for the same headset,
-  // different accounts have different values.
-  bytes sha256_account_key_public_address = 4;
-
-  // Deprecated fields.
-  reserved 2;
-}
diff --git a/nearby/service/proto/src/fastpair/fast_pair_string.proto b/nearby/service/proto/src/fastpair/fast_pair_string.proto
deleted file mode 100644
index f318c1a..0000000
--- a/nearby/service/proto/src/fastpair/fast_pair_string.proto
+++ /dev/null
@@ -1,40 +0,0 @@
-syntax = "proto2";
-
-package service.proto;
-
-message FastPairStrings {
-  // Required for initial pairing, used when there is a Google account on the
-  // device
-  optional string tap_to_pair_with_account = 1;
-
-  // Required for initial pairing, used when there is no Google account on the
-  // device
-  optional string tap_to_pair_without_account = 2;
-
-  // Description for initial pairing
-  optional string initial_pairing_description = 3;
-
-  // Description after successfully paired the device with companion app
-  // installed
-  optional string pairing_finished_companion_app_installed = 4;
-
-  // Description after successfully paired the device with companion app not
-  // installed
-  optional string pairing_finished_companion_app_not_installed = 5;
-
-  // Description when phone found the device that associates with user's account
-  // before remind user to pair with new device.
-  optional string subsequent_pairing_description = 6;
-
-  // Description when fast pair finds the user paired with device manually
-  // reminds user to opt the device into cloud.
-  optional string retroactive_pairing_description = 7;
-
-  // Description when user click setup device while device is still pairing
-  optional string wait_app_launch_description = 8;
-
-  // Description when user fail to pair with device
-  optional string pairing_fail_description = 9;
-
-  reserved 10, 11, 12, 13, 14,15, 16, 17, 18;
-}
diff --git a/nearby/service/proto/src/fastpair/rpcs.proto b/nearby/service/proto/src/fastpair/rpcs.proto
deleted file mode 100644
index bce4378..0000000
--- a/nearby/service/proto/src/fastpair/rpcs.proto
+++ /dev/null
@@ -1,301 +0,0 @@
-// RPCs for the Nearby Console service.
-syntax = "proto3";
-
-package service.proto;
-// Response containing an observed device.
-message GetObservedDeviceResponse {
-  // The device from the request.
-  Device device = 1;
-
-  // The image icon that shows in the notification
-  bytes image = 3;
-
-  // Strings to be displayed on notifications during the pairing process.
-  ObservedDeviceStrings strings = 4;
-
-  reserved 2;
-}
-
-message Device {
-  // Output only. The server-generated ID of the device.
-  int64 id = 1;
-
-  // The pantheon project number the device is created under. Only Nearby admins
-  // can change this.
-  int64 project_number = 2;
-
-  // How the notification will be displayed to the user
-  NotificationType notification_type = 3;
-
-  // The image to show on the notification.
-  string image_url = 4;
-
-  // The name of the device.
-  string name = 5;
-
-  // The intent that will be launched via the notification.
-  string intent_uri = 6;
-
-  // The transmit power of the device's BLE chip.
-  int32 ble_tx_power = 7;
-
-  // The distance that the device must be within to show a notification.
-  // If no distance is set, we default to 0.6 meters. Only Nearby admins can
-  // change this.
-  float trigger_distance = 8;
-
-  // Output only. Fast Pair only - The anti-spoofing key pair for the device.
-  AntiSpoofingKeyPair anti_spoofing_key_pair = 9;
-
-  // Output only. The current status of the device.
-  Status status = 10;
-
-
-  // DEPRECATED - check for published_version instead.
-  // Output only.
-  // Whether the device has a different, already published version.
-  bool has_published_version = 12;
-
-  // Fast Pair only - The type of device being registered.
-  DeviceType device_type = 13;
-
-
-  // Fast Pair only - Additional images for true wireless headsets.
-  TrueWirelessHeadsetImages true_wireless_images = 15;
-
-  // Fast Pair only - When true, this device can support assistant function.
-  bool assistant_supported = 16;
-
-  // Output only.
-  // The published version of a device that has been approved to be displayed
-  // as a notification - only populated if the device has a different published
-  // version. (A device that only has a published version would not have this
-  // populated).
-  Device published_version = 17;
-
-  // Fast Pair only - When true, Fast Pair will only create a bond with the
-  // device and not attempt to connect any profiles (for example, A2DP or HFP).
-  bool data_only_connection = 18;
-
-  // Name of the company/brand that will be selling the product.
-  string company_name = 19;
-
-  repeated FastPairFeature features = 20;
-
-  // Name of the device that is displayed on the console.
-  string display_name = 21;
-
-  // How the device will be interacted with by the user when the scan record
-  // is detected.
-  InteractionType interaction_type = 22;
-
-  // Companion app information.
-  CompanionAppDetails companion_detail = 23;
-
-  reserved 11, 14;
-}
-
-
-// Represents the format of the final device notification (which is directly
-// correlated to the action taken by the notification).
-enum NotificationType {
-  // Unspecified notification type.
-  NOTIFICATION_TYPE_UNSPECIFIED = 0;
-  // Notification launches the fast pair intent.
-  // Example Notification Title: "Bose SoundLink II"
-  // Notification Description: "Tap to pair with this device"
-  FAST_PAIR = 1;
-  // Notification launches an app.
-  // Notification Title: "[X]" where X is type/name of the device.
-  // Notification Description: "Tap to setup this device"
-  APP_LAUNCH = 2;
-  // Notification launches for Nearby Setup. The notification title and
-  // description is the same as APP_LAUNCH.
-  NEARBY_SETUP = 3;
-  // Notification launches the fast pair intent, but doesn't include an anti-
-  // spoofing key. The notification title and description is the same as
-  // FAST_PAIR.
-  FAST_PAIR_ONE = 4;
-  // Notification launches Smart Setup on devices.
-  // These notifications are identical to APP_LAUNCH except that they always
-  // launch Smart Setup intents within GMSCore.
-  SMART_SETUP = 5;
-}
-
-// How the device will be interacted with when it is seen.
-enum InteractionType {
-  INTERACTION_TYPE_UNKNOWN = 0;
-  AUTO_LAUNCH = 1;
-  NOTIFICATION = 2;
-}
-
-// Features that can be enabled for a Fast Pair device.
-enum FastPairFeature {
-  FAST_PAIR_FEATURE_UNKNOWN = 0;
-  SILENCE_MODE = 1;
-  WIRELESS_CHARGING = 2;
-  DYNAMIC_BUFFER_SIZE = 3;
-  NO_PERSONALIZED_NAME = 4;
-  EDDYSTONE_TRACKING = 5;
-}
-
-message CompanionAppDetails {
-  // Companion app slice provider's authority.
-  string authority = 1;
-
-  // Companion app certificate value.
-  string certificate_hash = 2;
-
-  // Deprecated fields.
-  reserved 3;
-}
-
-// Additional images for True Wireless Fast Pair devices.
-message TrueWirelessHeadsetImages {
-  // Image URL for the left bud.
-  string left_bud_url = 1;
-
-  // Image URL for the right bud.
-  string right_bud_url = 2;
-
-  // Image URL for the case.
-  string case_url = 3;
-}
-
-// Represents the type of device that is being registered.
-enum DeviceType {
-  DEVICE_TYPE_UNSPECIFIED = 0;
-  HEADPHONES = 1;
-  SPEAKER = 2;
-  WEARABLE = 3;
-  INPUT_DEVICE = 4;
-  AUTOMOTIVE = 5;
-  OTHER = 6;
-  TRUE_WIRELESS_HEADPHONES = 7;
-  WEAR_OS = 8;
-  ANDROID_AUTO = 9;
-}
-
-// An anti-spoofing key pair for a device that allows us to verify the device is
-// broadcasting legitimately.
-message AntiSpoofingKeyPair {
-  // The private key (restricted to only be viewable by trusted clients).
-  bytes private_key = 1;
-
-  // The public key.
-  bytes public_key = 2;
-}
-
-// Various states that a customer-configured device notification can be in.
-// PUBLISHED is the only state that shows notifications to the public.
-message Status {
-  // Status types available for each device.
-  enum StatusType {
-    // Unknown status.
-    TYPE_UNSPECIFIED = 0;
-    // Drafted device.
-    DRAFT = 1;
-    // Submitted and waiting for approval.
-    SUBMITTED = 2;
-    // Fully approved and available for end users.
-    PUBLISHED = 3;
-    // Rejected and not available for end users.
-    REJECTED = 4;
-  }
-
-  // Details about a device that has been rejected.
-  message RejectionDetails {
-    // The reason for the rejection.
-    enum RejectionReason {
-      // Unspecified reason.
-      REASON_UNSPECIFIED = 0;
-      // Name is not valid.
-      NAME = 1;
-      // Image is not valid.
-      IMAGE = 2;
-      // Tests have failed.
-      TESTS = 3;
-      // Other reason.
-      OTHER = 4;
-    }
-
-    // A list of reasons the device was rejected.
-    repeated RejectionReason reasons = 1;
-    // Comment about an OTHER rejection reason.
-    string additional_comment = 2;
-  }
-
-  // The status of the device.
-  StatusType status_type = 1;
-
-  // Accompanies Status.REJECTED.
-  RejectionDetails rejection_details = 2;
-}
-
-// Strings to be displayed in notifications surfaced for a device.
-message ObservedDeviceStrings {
-  // The notification description for when the device is initially discovered.
-  string initial_notification_description = 2;
-
-  // The notification description for when the device is initially discovered
-  // and no account is logged in.
-  string initial_notification_description_no_account = 3;
-
-  // The notification description for once we have finished pairing and the
-  // companion app has been opened. For google assistant devices, this string will point
-  // users to setting up the assistant.
-  string open_companion_app_description = 4;
-
-  // The notification description for once we have finished pairing and the
-  // companion app needs to be updated before use.
-  string update_companion_app_description = 5;
-
-  // The notification description for once we have finished pairing and the
-  // companion app needs to be installed.
-  string download_companion_app_description = 6;
-
-  // The notification title when a pairing fails.
-  string unable_to_connect_title = 7;
-
-  // The notification summary when a pairing fails.
-  string unable_to_connect_description = 8;
-
-  // The description that helps user initially paired with device.
-  string initial_pairing_description = 9;
-
-  // The description that let user open the companion app.
-  string connect_success_companion_app_installed = 10;
-
-  // The description that let user download the companion app.
-  string connect_success_companion_app_not_installed = 11;
-
-  // The description that reminds user there is a paired device nearby.
-  string subsequent_pairing_description = 12;
-
-  // The description that reminds users opt in their device.
-  string retroactive_pairing_description = 13;
-
-  // The description that indicates companion app is about to launch.
-  string wait_launch_companion_app_description = 14;
-
-  // The description that indicates go to bluetooth settings when connection
-  // fail.
-  string fail_connect_go_to_settings_description = 15;
-
-  reserved 1, 16, 17, 18, 19, 20, 21, 22, 23, 24;
-}
-
-// The buffer size range of a Fast Pair devices support dynamic buffer size.
-message BufferSizeRange {
-  // The max buffer size in ms.
-  int32 max_size = 1;
-
-  // The min buffer size in ms.
-  int32 min_size = 2;
-
-  // The default buffer size in ms.
-  int32 default_size = 3;
-
-  // The codec of this buffer size range.
-  int32 codec = 4;
-}
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
index e1bf455..bf9357b 100644
--- a/nearby/service/proto/src/presence/blefilter.proto
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -115,6 +115,9 @@
   repeated DataElement data_element = 7;
   optional bytes ble_service_data = 8;
   optional ResultType result_type = 9;
+  // Timestamp when the device is discovered, in nanoseconds,
+  // relative to Android SystemClock.elapsedRealtimeNanos().
+  optional uint64 timestamp_ns = 10;
 }
 
 message BleFilterResults {
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 5a681f2..8009303 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -26,13 +27,14 @@
         "bluetooth-test-util-lib",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
-        "truth-prebuilt",
+        "truth",
     ],
     libs: [
         "android.test.base",
         "framework-bluetooth.stubs.module_lib",
         "framework-configinfrastructure",
         "framework-connectivity-t.impl",
+        "framework-location.stubs.module_lib",
     ],
     srcs: ["src/**/*.java"],
     test_suites: [
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 bc9691d..1e36676 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -25,12 +25,12 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
+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;
 import android.nearby.BroadcastRequest;
 import android.nearby.NearbyDevice;
@@ -42,6 +42,8 @@
 import android.nearby.ScanCallback;
 import android.nearby.ScanRequest;
 import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
 import android.provider.DeviceConfig;
 
 import androidx.annotation.NonNull;
@@ -50,13 +52,16 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 
+import com.android.compatibility.common.util.SystemUtil;
 import com.android.modules.utils.build.SdkLevel;
 
 import org.junit.Before;
+import org.junit.ClassRule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
@@ -70,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];
@@ -122,8 +130,6 @@
 
         mContext = InstrumentationRegistry.getContext();
         mNearbyManager = mContext.getSystemService(NearbyManager.class);
-
-        enableBluetooth();
     }
 
     @Test
@@ -189,12 +195,97 @@
         mScanCallback.onError(ERROR_UNSUPPORTED);
     }
 
-    private void enableBluetooth() {
-        BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
-        BluetoothAdapter bluetoothAdapter = manager.getAdapter();
-        if (!bluetoothAdapter.isEnabled()) {
-            assertThat(BTAdapterUtils.enableAdapter(bluetoothAdapter, mContext)).isTrue();
-        }
+    @Test
+    public void testsetPoweredOffFindingEphemeralIds() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20], new byte[20]));
+    }
+
+    @Test
+    public void testsetPoweredOffFindingEphemeralIds_noPrivilegedPermission() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        mUiAutomation.dropShellPermissionIdentity();
+
+        assertThrows(SecurityException.class,
+                () -> mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20])));
+    }
+
+
+    @Test
+    public void testSetAndGetPoweredOffFindingMode_enabled() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        enableLocation();
+        // enableLocation() has dropped shell permission identity.
+        mUiAutomation.adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
+
+        mNearbyManager.setPoweredOffFindingMode(
+                NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+        assertThat(mNearbyManager.getPoweredOffFindingMode())
+                .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+    }
+
+    @Test
+    public void testSetAndGetPoweredOffFindingMode_disabled() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        mNearbyManager.setPoweredOffFindingMode(
+                NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+        assertThat(mNearbyManager.getPoweredOffFindingMode())
+                .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+    }
+
+    @Test
+    public void testSetPoweredOffFindingMode_noPrivilegedPermission() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        enableLocation();
+        mUiAutomation.dropShellPermissionIdentity();
+
+        assertThrows(SecurityException.class, () -> mNearbyManager
+                .setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
+    }
+
+    @Test
+    public void testGetPoweredOffFindingMode_noPrivilegedPermission() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        mUiAutomation.dropShellPermissionIdentity();
+
+        assertThrows(SecurityException.class, () -> mNearbyManager.getPoweredOffFindingMode());
+    }
+
+    private void enableLocation() {
+        LocationManager locationManager = mContext.getSystemService(LocationManager.class);
+        UserHandle user = Process.myUserHandle();
+        SystemUtil.runWithShellPermissionIdentity(
+                mUiAutomation, () -> locationManager.setLocationEnabledForUser(true, user));
     }
 
     private static class OffloadCallback implements Consumer<OffloadCapability> {
diff --git a/nearby/tests/integration/privileged/Android.bp b/nearby/tests/integration/privileged/Android.bp
index e3250f6..5e64009 100644
--- a/nearby/tests/integration/privileged/Android.bp
+++ b/nearby/tests/integration/privileged/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -27,7 +28,7 @@
         "androidx.test.ext.junit",
         "androidx.test.rules",
         "junit",
-        "truth-prebuilt",
+        "truth",
     ],
     test_suites: ["device-tests"],
 }
diff --git a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/FastPairSettingsProviderTest.kt b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/FastPairSettingsProviderTest.kt
deleted file mode 100644
index af3f75f..0000000
--- a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/FastPairSettingsProviderTest.kt
+++ /dev/null
@@ -1,56 +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.nearby.integration.privileged
-
-import android.content.Context
-import android.provider.Settings
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.junit.runners.Parameterized.Parameters
-
-data class FastPairSettingsFlag(val name: String, val value: Int) {
-    override fun toString() = name
-}
-
-@RunWith(Parameterized::class)
-class FastPairSettingsProviderTest(private val flag: FastPairSettingsFlag) {
-
-    /** Verify privileged app can enable/disable Fast Pair scan. */
-    @Test
-    fun testSettingsFastPairScan_fromPrivilegedApp() {
-        val appContext = ApplicationProvider.getApplicationContext<Context>()
-        val contentResolver = appContext.contentResolver
-
-        Settings.Secure.putInt(contentResolver, "fast_pair_scan_enabled", flag.value)
-
-        val actualValue = Settings.Secure.getInt(
-                contentResolver, "fast_pair_scan_enabled", /* default value */ -1)
-        assertThat(actualValue).isEqualTo(flag.value)
-    }
-
-    companion object {
-        @JvmStatic
-        @Parameters(name = "{0}Succeed")
-        fun fastPairScanFlags() = listOf(
-            FastPairSettingsFlag(name = "disable", value = 0),
-            FastPairSettingsFlag(name = "enable", value = 1),
-        )
-    }
-}
diff --git a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
index 506b4e2..b949720 100644
--- a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
+++ b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
@@ -29,6 +29,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -96,4 +97,49 @@
         )
         nearbyManager.stopBroadcast(broadcastCallback)
     }
+
+    /** Verify privileged app can set powered off finding ephemeral IDs without exception. */
+    @Test
+    fun testNearbyManagerSetPoweredOffFindingEphemeralIds_fromPrivilegedApp_succeed() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+        // Only test supporting devices.
+        if (nearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+        val eid = ByteArray(20)
+
+        nearbyManager.setPoweredOffFindingEphemeralIds(listOf(eid))
+    }
+
+    /**
+     * Verifies that [NearbyManager.setPoweredOffFindingEphemeralIds] checkes the ephemeral ID
+     * length.
+     */
+    @Test
+    fun testNearbyManagerSetPoweredOffFindingEphemeralIds_wrongSize_throwsException() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+        // Only test supporting devices.
+        if (nearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+        assertThrows(IllegalArgumentException::class.java) {
+            nearbyManager.setPoweredOffFindingEphemeralIds(listOf(ByteArray(21)))
+        }
+        assertThrows(IllegalArgumentException::class.java) {
+            nearbyManager.setPoweredOffFindingEphemeralIds(listOf(ByteArray(19)))
+        }
+    }
+
+    /** Verify privileged app can set and get powered off finding mode without exception. */
+    @Test
+    fun testNearbyManagerSetGetPoweredOffMode_fromPrivilegedApp_succeed() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+        // Only test supporting devices.
+        if (nearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+        nearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED)
+        assertThat(nearbyManager.getPoweredOffFindingMode())
+                .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED)
+    }
 }
diff --git a/nearby/tests/integration/ui/Android.bp b/nearby/tests/integration/ui/Android.bp
deleted file mode 100644
index 524c838..0000000
--- a/nearby/tests/integration/ui/Android.bp
+++ /dev/null
@@ -1,40 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test {
-    name: "NearbyIntegrationUiTests",
-    defaults: ["mts-target-sdk-version-current"],
-    sdk_version: "test_current",
-    static_libs: ["NearbyIntegrationUiTestsLib"],
-    test_suites: ["device-tests"],
-}
-
-android_library {
-    name: "NearbyIntegrationUiTestsLib",
-    srcs: ["src/**/*.kt"],
-    sdk_version: "test_current",
-    static_libs: [
-        "androidx.test.ext.junit",
-        "androidx.test.rules",
-        "androidx.test.uiautomator_uiautomator",
-        "junit",
-        "platform-test-rules",
-        "service-nearby-pre-jarjar",
-        "truth-prebuilt",
-    ],
-}
diff --git a/nearby/tests/integration/ui/AndroidManifest.xml b/nearby/tests/integration/ui/AndroidManifest.xml
deleted file mode 100644
index 9aea0c1..0000000
--- a/nearby/tests/integration/ui/AndroidManifest.xml
+++ /dev/null
@@ -1,29 +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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.nearby.integration.ui">
-
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
-
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.nearby.integration.ui"
-        android:label="Nearby Mainline Module Integration UI Tests" />
-
-</manifest>
diff --git a/nearby/tests/integration/ui/AndroidTest.xml b/nearby/tests/integration/ui/AndroidTest.xml
deleted file mode 100644
index 9dfcf7b..0000000
--- a/nearby/tests/integration/ui/AndroidTest.xml
+++ /dev/null
@@ -1,46 +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.
-  -->
-<configuration description="Runs Nearby Mainline Module Integration UI Tests">
-    <!-- Needed for pulling the screen record files. -->
-    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="test-file-name" value="NearbyIntegrationUiTests.apk" />
-    </target_preparer>
-
-    <option name="test-suite-tag" value="apct" />
-    <option name="test-tag" value="NearbyIntegrationUiTests" />
-    <option name="config-descriptor:metadata" key="mainline-param"
-            value="com.google.android.tethering.next.apex" />
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="android.nearby.integration.ui" />
-        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
-        <option name="hidden-api-checks" value="false"/>
-        <!-- test-timeout unit is ms, value = 5 min -->
-        <option name="test-timeout" value="300000" />
-    </test>
-
-    <!-- Only run NearbyIntegrationUiTests in MTS if the Nearby 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>
-
-    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
-        <option name="directory-keys" value="/data/user/0/android.nearby.integration.ui/files" />
-        <option name="collect-on-run-ended-only" value="true" />
-    </metrics_collector>
-</configuration>
diff --git a/nearby/tests/integration/ui/src/android/nearby/integration/ui/BaseUiTest.kt b/nearby/tests/integration/ui/src/android/nearby/integration/ui/BaseUiTest.kt
deleted file mode 100644
index 658775b..0000000
--- a/nearby/tests/integration/ui/src/android/nearby/integration/ui/BaseUiTest.kt
+++ /dev/null
@@ -1,41 +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.nearby.integration.ui
-
-import android.platform.test.rule.ArtifactSaver
-import android.platform.test.rule.ScreenRecordRule
-import android.platform.test.rule.TestWatcher
-import org.junit.Rule
-import org.junit.rules.TestRule
-import org.junit.rules.Timeout
-import org.junit.runner.Description
-
-abstract class BaseUiTest {
-    @get:Rule
-    var mGlobalTimeout: Timeout = Timeout.seconds(100) // Test times out in 1.67 minutes
-
-    @get:Rule
-    val mTestWatcherRule: TestRule = object : TestWatcher() {
-        override fun failed(throwable: Throwable?, description: Description?) {
-            super.failed(throwable, description)
-            ArtifactSaver.onError(description, throwable)
-        }
-    }
-
-    @get:Rule
-    val mScreenRecordRule: TestRule = ScreenRecordRule()
-}
\ No newline at end of file
diff --git a/nearby/tests/integration/ui/src/android/nearby/integration/ui/CheckNearbyHalfSheetUiTest.kt b/nearby/tests/integration/ui/src/android/nearby/integration/ui/CheckNearbyHalfSheetUiTest.kt
deleted file mode 100644
index 5a3538e..0000000
--- a/nearby/tests/integration/ui/src/android/nearby/integration/ui/CheckNearbyHalfSheetUiTest.kt
+++ /dev/null
@@ -1,154 +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.nearby.integration.ui
-
-import android.content.Context
-import android.os.Bundle
-import android.platform.test.rule.ScreenRecordRule.ScreenRecord
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.Until
-import com.android.server.nearby.common.eventloop.EventLoop
-import com.android.server.nearby.common.locator.Locator
-import com.android.server.nearby.common.locator.LocatorContextWrapper
-import com.android.server.nearby.fastpair.FastPairController
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.AfterClass
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import service.proto.Cache
-import service.proto.FastPairString.FastPairStrings
-import java.time.Clock
-
-/** An instrumented test to check Nearby half sheet UI showed correctly.
- *
- * To run this test directly:
- * am instrument -w -r \
- * -e class android.nearby.integration.ui.CheckNearbyHalfSheetUiTest \
- * android.nearby.integration.ui/androidx.test.runner.AndroidJUnitRunner
- */
-@RunWith(AndroidJUnit4::class)
-class CheckNearbyHalfSheetUiTest : BaseUiTest() {
-    private var waitHalfSheetPopupTimeoutMs: Long
-    private var halfSheetTitleText: String
-    private var halfSheetSubtitleText: String
-
-    init {
-        val arguments: Bundle = InstrumentationRegistry.getArguments()
-        waitHalfSheetPopupTimeoutMs = arguments.getLong(
-            WAIT_HALF_SHEET_POPUP_TIMEOUT_KEY,
-            DEFAULT_WAIT_HALF_SHEET_POPUP_TIMEOUT_MS
-        )
-        halfSheetTitleText =
-            arguments.getString(HALF_SHEET_TITLE_KEY, DEFAULT_HALF_SHEET_TITLE_TEXT)
-        halfSheetSubtitleText =
-            arguments.getString(HALF_SHEET_SUBTITLE_KEY, DEFAULT_HALF_SHEET_SUBTITLE_TEXT)
-
-        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-    }
-
-    /** For multidevice test snippet only. Force overwrites the test arguments. */
-    fun updateTestArguments(
-        waitHalfSheetPopupTimeoutSeconds: Int,
-        halfSheetTitleText: String,
-        halfSheetSubtitleText: String
-    ) {
-        this.waitHalfSheetPopupTimeoutMs = waitHalfSheetPopupTimeoutSeconds * 1000L
-        this.halfSheetTitleText = halfSheetTitleText
-        this.halfSheetSubtitleText = halfSheetSubtitleText
-    }
-
-    @Before
-    fun setUp() {
-        val appContext = ApplicationProvider.getApplicationContext<Context>()
-        val locator = Locator(appContext).apply {
-            overrideBindingForTest(EventLoop::class.java, EventLoop.newInstance("test"))
-            overrideBindingForTest(
-                FastPairCacheManager::class.java,
-                FastPairCacheManager(appContext)
-            )
-            overrideBindingForTest(FootprintsDeviceManager::class.java, FootprintsDeviceManager())
-            overrideBindingForTest(Clock::class.java, Clock.systemDefaultZone())
-        }
-        val locatorContextWrapper = LocatorContextWrapper(appContext, locator)
-        locator.overrideBindingForTest(
-            FastPairController::class.java,
-            FastPairController(locatorContextWrapper)
-        )
-        val scanFastPairStoreItem = Cache.ScanFastPairStoreItem.newBuilder()
-            .setDeviceName(DEFAULT_HALF_SHEET_TITLE_TEXT)
-            .setFastPairStrings(
-                FastPairStrings.newBuilder()
-                    .setInitialPairingDescription(DEFAULT_HALF_SHEET_SUBTITLE_TEXT).build()
-            )
-            .build()
-        FastPairHalfSheetManager(locatorContextWrapper).showHalfSheet(scanFastPairStoreItem)
-    }
-
-    @Test
-    @ScreenRecord
-    fun checkNearbyHalfSheetUi() {
-        // Check Nearby half sheet showed by checking button "Connect" on the DevicePairingFragment.
-        val isConnectButtonShowed = device.wait(
-            Until.hasObject(NearbyHalfSheetUiMap.DevicePairingFragment.connectButton),
-            waitHalfSheetPopupTimeoutMs
-        )
-        assertWithMessage("Nearby half sheet didn't show within $waitHalfSheetPopupTimeoutMs ms.")
-            .that(isConnectButtonShowed).isTrue()
-
-        val halfSheetTitle =
-            device.findObject(NearbyHalfSheetUiMap.DevicePairingFragment.halfSheetTitle)
-        assertThat(halfSheetTitle).isNotNull()
-        assertThat(halfSheetTitle.text).isEqualTo(halfSheetTitleText)
-
-        val halfSheetSubtitle =
-            device.findObject(NearbyHalfSheetUiMap.DevicePairingFragment.halfSheetSubtitle)
-        assertThat(halfSheetSubtitle).isNotNull()
-        assertThat(halfSheetSubtitle.text).isEqualTo(halfSheetSubtitleText)
-
-        val deviceImage = device.findObject(NearbyHalfSheetUiMap.DevicePairingFragment.deviceImage)
-        assertThat(deviceImage).isNotNull()
-
-        val infoButton = device.findObject(NearbyHalfSheetUiMap.DevicePairingFragment.infoButton)
-        assertThat(infoButton).isNotNull()
-    }
-
-    companion object {
-        private const val DEFAULT_WAIT_HALF_SHEET_POPUP_TIMEOUT_MS = 30 * 1000L
-        private const val DEFAULT_HALF_SHEET_TITLE_TEXT = "Fast Pair Provider Simulator"
-        private const val DEFAULT_HALF_SHEET_SUBTITLE_TEXT = "Fast Pair Provider Simulator will " +
-                "appear on devices linked with nearby-mainline-fpseeker@google.com"
-        private const val WAIT_HALF_SHEET_POPUP_TIMEOUT_KEY = "WAIT_HALF_SHEET_POPUP_TIMEOUT_MS"
-        private const val HALF_SHEET_TITLE_KEY = "HALF_SHEET_TITLE"
-        private const val HALF_SHEET_SUBTITLE_KEY = "HALF_SHEET_SUBTITLE"
-        private lateinit var device: UiDevice
-
-        @AfterClass
-        @JvmStatic
-        fun teardownClass() {
-            // Cleans up after saving screenshot in TestWatcher, leaves nothing dirty behind.
-            DismissNearbyHalfSheetUiTest().dismissHalfSheet()
-        }
-    }
-}
\ No newline at end of file
diff --git a/nearby/tests/integration/ui/src/android/nearby/integration/ui/DismissNearbyHalfSheetUiTest.kt b/nearby/tests/integration/ui/src/android/nearby/integration/ui/DismissNearbyHalfSheetUiTest.kt
deleted file mode 100644
index 52d202a..0000000
--- a/nearby/tests/integration/ui/src/android/nearby/integration/ui/DismissNearbyHalfSheetUiTest.kt
+++ /dev/null
@@ -1,48 +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.nearby.integration.ui
-
-import android.platform.test.rule.ScreenRecordRule.ScreenRecord
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.UiDevice
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/** An instrumented test to dismiss Nearby half sheet UI.
- *
- * To run this test directly:
- * am instrument -w -r \
- * -e class android.nearby.integration.ui.DismissNearbyHalfSheetUiTest \
- * android.nearby.integration.ui/androidx.test.runner.AndroidJUnitRunner
- */
-@RunWith(AndroidJUnit4::class)
-class DismissNearbyHalfSheetUiTest : BaseUiTest() {
-    private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-
-    @Test
-    @ScreenRecord
-    fun dismissHalfSheet() {
-        device.pressHome()
-        device.waitForIdle()
-
-        assertWithMessage("Fail to dismiss Nearby half sheet.").that(
-            device.findObject(NearbyHalfSheetUiMap.DevicePairingFragment.connectButton)
-        ).isNull()
-    }
-}
\ No newline at end of file
diff --git a/nearby/tests/integration/ui/src/android/nearby/integration/ui/NearbyHalfSheetUiMap.kt b/nearby/tests/integration/ui/src/android/nearby/integration/ui/NearbyHalfSheetUiMap.kt
deleted file mode 100644
index 4098865..0000000
--- a/nearby/tests/integration/ui/src/android/nearby/integration/ui/NearbyHalfSheetUiMap.kt
+++ /dev/null
@@ -1,70 +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.nearby.integration.ui
-
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager.MATCH_SYSTEM_ONLY
-import android.content.pm.PackageManager.ResolveInfoFlags
-import android.content.pm.ResolveInfo
-import android.util.Log
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import com.android.server.nearby.util.Environment
-import com.google.common.truth.Truth.assertThat
-
-/** UiMap for Nearby Mainline Half Sheet. */
-object NearbyHalfSheetUiMap {
-    private val PACKAGE_NAME: String = getHalfSheetApkPkgName()
-    private const val ACTION_RESOURCES_APK = "android.nearby.SHOW_HALFSHEET"
-    private const val ANDROID_WIDGET_BUTTON = "android.widget.Button"
-    private const val ANDROID_WIDGET_IMAGE_VIEW = "android.widget.ImageView"
-    private const val ANDROID_WIDGET_TEXT_VIEW = "android.widget.TextView"
-
-    object DevicePairingFragment {
-        val halfSheetTitle: BySelector =
-            By.res(PACKAGE_NAME, "toolbar_title").clazz(ANDROID_WIDGET_TEXT_VIEW)
-        val halfSheetSubtitle: BySelector =
-            By.res(PACKAGE_NAME, "header_subtitle").clazz(ANDROID_WIDGET_TEXT_VIEW)
-        val deviceImage: BySelector =
-            By.res(PACKAGE_NAME, "pairing_pic").clazz(ANDROID_WIDGET_IMAGE_VIEW)
-        val connectButton: BySelector =
-            By.res(PACKAGE_NAME, "connect_btn").clazz(ANDROID_WIDGET_BUTTON).text("Connect")
-        val infoButton: BySelector =
-            By.res(PACKAGE_NAME, "info_icon").clazz(ANDROID_WIDGET_IMAGE_VIEW)
-    }
-
-    // Vendors might override HalfSheetUX in their vendor partition, query the package name
-    // instead of hard coding. ex: Google overrides it in vendor/google/modules/TetheringGoogle.
-    fun getHalfSheetApkPkgName(): String {
-        val appContext = ApplicationProvider.getApplicationContext<Context>()
-        val resolveInfos: MutableList<ResolveInfo> =
-            appContext.packageManager.queryIntentActivities(Intent(ACTION_RESOURCES_APK),
-                ResolveInfoFlags.of(MATCH_SYSTEM_ONLY.toLong())
-            )
-
-        // remove apps that don't live in the nearby apex
-        resolveInfos.removeIf { !Environment.isAppInNearbyApex(it.activityInfo.applicationInfo) }
-
-        assertThat(resolveInfos).hasSize(1)
-
-        val halfSheetApkPkgName: String = resolveInfos[0].activityInfo.applicationInfo.packageName
-        Log.i("NearbyHalfSheetUiMap", "Found half-sheet APK at: $halfSheetApkPkgName")
-        return halfSheetApkPkgName
-    }
-}
diff --git a/nearby/tests/integration/ui/src/android/nearby/integration/ui/PairByNearbyHalfSheetUiTest.kt b/nearby/tests/integration/ui/src/android/nearby/integration/ui/PairByNearbyHalfSheetUiTest.kt
deleted file mode 100644
index 27264b51..0000000
--- a/nearby/tests/integration/ui/src/android/nearby/integration/ui/PairByNearbyHalfSheetUiTest.kt
+++ /dev/null
@@ -1,70 +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.nearby.integration.ui
-
-import android.platform.test.rule.ScreenRecordRule.ScreenRecord
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.Until
-import org.junit.AfterClass
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/** An instrumented test to start pairing by interacting with Nearby half sheet UI.
- *
- * To run this test directly:
- * am instrument -w -r \
- * -e class android.nearby.integration.ui.PairByNearbyHalfSheetUiTest \
- * android.nearby.integration.ui/androidx.test.runner.AndroidJUnitRunner
- */
-@RunWith(AndroidJUnit4::class)
-class PairByNearbyHalfSheetUiTest : BaseUiTest() {
-    init {
-        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-    }
-
-    @Before
-    fun setUp() {
-        CheckNearbyHalfSheetUiTest().apply {
-            setUp()
-            checkNearbyHalfSheetUi()
-        }
-    }
-
-    @Test
-    @ScreenRecord
-    fun clickConnectButton() {
-        val connectButton = NearbyHalfSheetUiMap.DevicePairingFragment.connectButton
-        device.findObject(connectButton).click()
-        device.wait(Until.gone(connectButton), CONNECT_BUTTON_TIMEOUT_MILLS)
-    }
-
-    companion object {
-        private const val CONNECT_BUTTON_TIMEOUT_MILLS = 3000L
-        private lateinit var device: UiDevice
-
-        @AfterClass
-        @JvmStatic
-        fun teardownClass() {
-            // Cleans up after saving screenshot in TestWatcher, leaves nothing dirty behind.
-            device.pressBack()
-            DismissNearbyHalfSheetUiTest().dismissHalfSheet()
-        }
-    }
-}
\ No newline at end of file
diff --git a/nearby/tests/integration/untrusted/Android.bp b/nearby/tests/integration/untrusted/Android.bp
index 57499e4..e6259c5 100644
--- a/nearby/tests/integration/untrusted/Android.bp
+++ b/nearby/tests/integration/untrusted/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -31,7 +32,7 @@
         "androidx.test.uiautomator_uiautomator",
         "junit",
         "kotlin-test",
-        "truth-prebuilt",
+        "truth",
     ],
     test_suites: ["device-tests"],
 }
diff --git a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/FastPairSettingsProviderTest.kt b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/FastPairSettingsProviderTest.kt
deleted file mode 100644
index c549073..0000000
--- a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/FastPairSettingsProviderTest.kt
+++ /dev/null
@@ -1,53 +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.nearby.integration.untrusted
-
-import android.content.Context
-import android.content.ContentResolver
-import android.provider.Settings
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import kotlin.test.assertFailsWith
-
-
-@RunWith(AndroidJUnit4::class)
-class FastPairSettingsProviderTest {
-    private lateinit var contentResolver: ContentResolver
-
-    @Before
-    fun setUp() {
-        contentResolver = ApplicationProvider.getApplicationContext<Context>().contentResolver
-    }
-
-    /** Verify untrusted app can read Fast Pair scan enabled setting. */
-    @Test
-    fun testSettingsFastPairScan_fromUnTrustedApp_readsSucceed() {
-        Settings.Secure.getInt(contentResolver,
-                "fast_pair_scan_enabled", /* default value */ -1)
-    }
-
-    /** Verify untrusted app can't write Fast Pair scan enabled setting. */
-    @Test
-    fun testSettingsFastPairScan_fromUnTrustedApp_writesFailed() {
-        assertFailsWith<SecurityException> {
-            Settings.Secure.putInt(contentResolver, "fast_pair_scan_enabled", 1)
-        }
-    }
-}
diff --git a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
index 7bf9f63..015d022 100644
--- a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
+++ b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
@@ -30,12 +30,12 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.uiautomator.LogcatWaitMixin
 import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.util.Calendar
 import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.time.Duration
-import java.util.Calendar
 
 @RunWith(AndroidJUnit4::class)
 class NearbyManagerTest {
@@ -151,6 +151,46 @@
         ).isTrue()
     }
 
+    /**
+     * Verify untrusted app can't set powered off finding ephemeral IDs because it needs
+     * BLUETOOTH_PRIVILEGED permission which is not for use by third-party applications.
+     */
+    @Test
+    fun testNearbyManagerSetPoweredOffFindingEphemeralIds_fromUnTrustedApp_throwsException() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+        val eid = ByteArray(20)
+
+        assertThrows(SecurityException::class.java) {
+            nearbyManager.setPoweredOffFindingEphemeralIds(listOf(eid))
+        }
+    }
+
+    /**
+     * Verify untrusted app can't set powered off finding mode because it needs BLUETOOTH_PRIVILEGED
+     * permission which is not for use by third-party applications.
+     */
+    @Test
+    fun testNearbyManagerSetPoweredOffFindingMode_fromUnTrustedApp_throwsException() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+
+        assertThrows(SecurityException::class.java) {
+            nearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED)
+        }
+    }
+
+    /**
+     * Verify untrusted app can't get powered off finding mode because it needs BLUETOOTH_PRIVILEGED
+     * permission which is not for use by third-party applications.
+     */
+    @Test
+    fun testNearbyManagerGetPoweredOffFindingMode_fromUnTrustedApp_throwsException() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+
+        assertThrows(SecurityException::class.java) {
+            nearbyManager.getPoweredOffFindingMode()
+        }
+    }
+
     companion object {
         private val WAIT_INVALID_OPERATIONS_LOGS_TIMEOUT = Duration.ofSeconds(5)
     }
diff --git a/nearby/tests/multidevices/OWNERS b/nearby/tests/multidevices/OWNERS
deleted file mode 100644
index f4dbde2..0000000
--- a/nearby/tests/multidevices/OWNERS
+++ /dev/null
@@ -1,4 +0,0 @@
-# Bug component: 1092133
-
-ericth@google.com
-ryancllin@google.com
\ No newline at end of file
diff --git a/nearby/tests/multidevices/README.md b/nearby/tests/multidevices/README.md
deleted file mode 100644
index 9d086de..0000000
--- a/nearby/tests/multidevices/README.md
+++ /dev/null
@@ -1,155 +0,0 @@
-# Nearby Mainline Fast Pair end-to-end tests
-
-This document refers to the Mainline Fast Pair project source code in the
-packages/modules/Connectivity/nearby. This is not an officially supported Google
-product.
-
-## About the Fast Pair Project
-
-The Connectivity Nearby mainline module is created in the Android T to host
-Better Together related functionality. Fast Pair is one of the main
-functionalities to provide seamless onboarding and integrated experiences for
-peripheral devices (for example, headsets like Google Pixel Buds) in the Nearby
-component.
-
-## Fully automated test
-
-### Prerequisites
-
-The fully automated end-to-end (e2e) tests are host-driven tests (which means
-test logics are in the host test scripts) using Mobly runner in Python. The two
-phones are installed with the test snippet
-`NearbyMultiDevicesClientsSnippets.apk` in the test time to let the host scripts
-control both sides for testing. Here's the overview of the test environment.
-
-Workstation (runs Python test scripts and controls Android devices through USB
-ADB) \
-├── Phone 1: As Fast Pair seeker role, to scan, pair Fast Pair devices nearby \
-└── Phone 2: As Fast Pair provider role, to simulate a Fast Pair device (for
-example, a Bluetooth headset)
-
-Note: These two phones need to be physically within 0.3 m of each other.
-
-### Prepare Phone 1 (Fast Pair seeker role)
-
-This is the phone to scan/pair Fast Pair devices nearby using the Nearby
-Mainline module. Test it by flashing with the Android T ROM.
-
-### Prepare Phone 2 (Fast Pair provider role)
-
-This is the phone to simulate a Fast Pair device (for example, a Bluetooth
-headset). Flash it with a customized ROM with the following changes:
-
-*   Adjust Bluetooth profile configurations. \
-    The Fast Pair provider simulator is an opposite role to the seeker. It needs
-    to enable/disable the following Bluetooth profile:
-    *   Disable A2DP source (bluetooth.profile.a2dp.source.enabled)
-    *   Enable A2DP sink (bluetooth.profile.a2dp.sink.enabled)
-    *   Disable the AVRCP controller (bluetooth.profile.avrcp.controller.enabled)
-    *   Enable the AVRCP target (bluetooth.profile.avrcp.target.enabled)
-    *   Enable the HFP service (bluetooth.profile.hfp.ag.enabled, bluetooth.profile.hfp.hf.enabled)
-
-```makefile
-# The Bluetooth profiles that Fast Pair provider simulator expect to have enabled.
-PRODUCT_PRODUCT_PROPERTIES += \
-    bluetooth.device.default_name=FastPairProviderSimulator \
-    bluetooth.profile.a2dp.source.enabled=false \
-    bluetooth.profile.a2dp.sink.enabled=true \
-    bluetooth.profile.avrcp.controller.enabled=false \
-    bluetooth.profile.avrcp.target.enabled=true \
-    bluetooth.profile.hfp.ag.enabled=true \
-    bluetooth.profile.hfp.hf.enabled=true
-```
-
-*   Adjust Bluetooth TX power limitation in Bluetooth module and disable the
-    Fast Pair in Google Play service (aka GMS)
-
-```shell
-adb root
-adb shell am broadcast \
-  -a 'com.google.android.gms.phenotype.FLAG_OVERRIDE' \
-  --es package "com.google.android.gms.nearby" \
-  --es user "\*" \
-  --esa flags "enabled" \
-  --esa types "boolean" \
-  --esa values "false" \
-  com.google.android.gms
-```
-
-### Running tests
-
-To run the tests, enter:
-
-```shell
-atest -v CtsNearbyMultiDevicesTestSuite
-```
-
-## Manual testing the seeker side with headsets
-
-Use this testing with headsets such as Google Pixel buds.
-
-The `FastPairTestDataProviderService.apk` is a run-time configurable Fast Pair
-data provider service (`FastPairDataProviderService`):
-
-`packages/modules/Connectivity/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider`
-
-It has a test data manager(`FastPairTestDataManager`) to receive intent
-broadcasts to add or clear the test data cache (`FastPairTestDataCache`). This
-cache provides the data to return to the Fast Pair module for onXXX calls (for
-example, `onLoadFastPairAntispoofKeyDeviceMetadata`) so you can feed the
-metadata for your device.
-
-Here are some sample uses:
-
-*   Send FastPairAntispoofKeyDeviceMetadata for PixelBuds-A to
-    FastPairTestDataCache \
-    `./fast_pair_data_provider_shell.sh -m=718c17
-    -a=../test_data/fastpair/pixelbuds-a_antispoofkey_devicemeta_json.txt`
-*   Send FastPairAccountDevicesMetadata for PixelBuds-A to FastPairTestDataCache
-    \
-    `./fast_pair_data_provider_shell.sh
-    -d=../test_data/fastpair/pixelbuds-a_account_devicemeta_json.txt`
-*   Send FastPairAntispoofKeyDeviceMetadata for Provider Simulator to
-    FastPairTestDataCache \
-    `./fast_pair_data_provider_shell.sh -m=00000c
-    -a=../test_data/fastpair/simulator_antispoofkey_devicemeta_json.txt`
-*   Send FastPairAccountDevicesMetadata for Provider Simulator to
-    FastPairTestDataCache \
-    `./fast_pair_data_provider_shell.sh
-    -d=../test_data/fastpair/simulator_account_devicemeta_json.txt`
-*   Clear FastPairTestDataCache \
-    `./fast_pair_data_provider_shell.sh -c`
-
-See
-[host/tool/fast_pair_data_provider_shell.sh](host/tool/fast_pair_data_provider_shell.sh)
-for more documentation.
-
-To install the data provider as system private app, consider remounting the
-system partition:
-
-```
-adb root && adb remount
-```
-
-Push it in:
-
-```
-adb push ${ANDROID_PRODUCT_OUT}/system/app/NearbyFastPairSeekerDataProvider
-/system/priv-app/
-```
-
-Then reboot:
-
-```
-adb reboot
-```
-
-## Manual testing the seeker side with provider simulator app
-
-The `NearbyFastPairProviderSimulatorApp.apk` is a simple Android app to let you
-control the state of the Fast Pair provider simulator. Install this app on phone
-2 (Fast Pair provider role) to work correctly.
-
-See
-[clients/test_support/fastpair_provider/simulator_app/Android.bp](clients/test_support/fastpair_provider/simulator_app/Android.bp)
-for more documentation.
diff --git a/nearby/tests/multidevices/clients/Android.bp b/nearby/tests/multidevices/clients/Android.bp
deleted file mode 100644
index db6d191..0000000
--- a/nearby/tests/multidevices/clients/Android.bp
+++ /dev/null
@@ -1,49 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_library {
-    name: "NearbyMultiDevicesClientsLib",
-    srcs: ["src/**/*.kt"],
-    sdk_version: "test_current",
-    static_libs: [
-        "MoblySnippetHelperLib",
-        "NearbyFastPairProviderLib",
-        "NearbyFastPairSeekerSharedLib",
-        "NearbyIntegrationUiTestsLib",
-        "androidx.test.core",
-        "androidx.test.ext.junit",
-        "kotlin-stdlib",
-        "mobly-snippet-lib",
-        "truth-prebuilt",
-    ],
-}
-
-android_app {
-    name: "NearbyMultiDevicesClientsSnippets",
-    sdk_version: "test_current",
-    certificate: "platform",
-    static_libs: ["NearbyMultiDevicesClientsLib"],
-    optimize: {
-        enabled: true,
-        shrink: false,
-        // Required to avoid class collisions from static and shared linking
-        // of MessageNano.
-        proguard_compatibility: true,
-        proguard_flags_files: ["proguard.flags"],
-    },
-}
diff --git a/nearby/tests/multidevices/clients/AndroidManifest.xml b/nearby/tests/multidevices/clients/AndroidManifest.xml
deleted file mode 100644
index 86c10b2..0000000
--- a/nearby/tests/multidevices/clients/AndroidManifest.xml
+++ /dev/null
@@ -1,53 +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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.nearby.multidevices">
-
-    <uses-feature android:name="android.hardware.bluetooth" />
-    <uses-feature android:name="android.hardware.bluetooth_le" />
-    <uses-permission android:name="android.permission.BLUETOOTH" />
-    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
-    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
-    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
-    <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
-    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
-    <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-
-    <application>
-        <meta-data
-            android:name="mobly-log-tag"
-            android:value="NearbyMainlineSnippet" />
-        <meta-data
-            android:name="mobly-snippets"
-            android:value="android.nearby.multidevices.fastpair.seeker.FastPairSeekerSnippet,
-                           android.nearby.multidevices.fastpair.provider.FastPairProviderSimulatorSnippet" />
-    </application>
-
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="Nearby Mainline Module Instrumentation Test"
-        android:targetPackage="android.nearby.multidevices" />
-
-    <instrumentation
-        android:name="com.google.android.mobly.snippet.SnippetRunner"
-        android:label="Nearby Mainline Module Mobly Snippet"
-        android:targetPackage="android.nearby.multidevices" />
-
-</manifest>
diff --git a/nearby/tests/multidevices/clients/proguard.flags b/nearby/tests/multidevices/clients/proguard.flags
deleted file mode 100644
index 11938cd..0000000
--- a/nearby/tests/multidevices/clients/proguard.flags
+++ /dev/null
@@ -1,29 +0,0 @@
-# Keep all snippet classes.
--keep class android.nearby.multidevices.** {
-     *;
-}
-
-# Keep AdvertisingSetCallback#onOwnAddressRead callback.
--keep class * extends android.bluetooth.le.AdvertisingSetCallback {
-     *;
-}
-
-# Do not touch Mobly.
--keep class com.google.android.mobly.** {
-  *;
-}
-
-# Keep names for easy debugging.
--dontobfuscate
-
-# Necessary to allow debugging.
--keepattributes *
-
-# By default, proguard leaves all classes in their original package, which
-# needlessly repeats com.google.android.apps.etc.
--repackageclasses ""
-
-# Allows proguard to make private and protected methods and fields public as
-# part of optimization. This lets proguard inline trivial getter/setter
-# methods.
--allowaccessmodification
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt
deleted file mode 100644
index 922e950..0000000
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt
+++ /dev/null
@@ -1,79 +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.nearby.multidevices.fastpair.provider
-
-import android.annotation.TargetApi
-import android.content.Context
-import android.nearby.multidevices.fastpair.provider.controller.FastPairProviderSimulatorController
-import android.nearby.multidevices.fastpair.provider.events.ProviderStatusEvents
-import android.os.Build
-import androidx.test.platform.app.InstrumentationRegistry
-import com.google.android.mobly.snippet.Snippet
-import com.google.android.mobly.snippet.rpc.AsyncRpc
-import com.google.android.mobly.snippet.rpc.Rpc
-
-/** Expose Mobly RPC methods for Python side to simulate fast pair provider role. */
-@TargetApi(Build.VERSION_CODES.LOLLIPOP)
-class FastPairProviderSimulatorSnippet : Snippet {
-    private val context: Context = InstrumentationRegistry.getInstrumentation().context
-    private val fastPairProviderSimulatorController = FastPairProviderSimulatorController(context)
-
-    /** Sets up the Fast Pair provider simulator. */
-    @AsyncRpc(description = "Sets up FP provider simulator.")
-    fun setupProviderSimulator(callbackId: String) {
-        fastPairProviderSimulatorController.setupProviderSimulator(ProviderStatusEvents(callbackId))
-    }
-
-    /**
-     * Starts model id advertising for scanning and initial pairing.
-     *
-     * @param callbackId the callback ID corresponding to the
-     * [FastPairProviderSimulatorSnippet#startProviderSimulator] call that started the scanning.
-     * @param modelId a 3-byte hex string for seeker side to recognize the device (ex: 0x00000C).
-     * @param antiSpoofingKeyString a public key for registered headsets.
-     */
-    @AsyncRpc(description = "Starts model id advertising for scanning and initial pairing.")
-    fun startModelIdAdvertising(
-        callbackId: String,
-        modelId: String,
-        antiSpoofingKeyString: String
-    ) {
-        fastPairProviderSimulatorController.startModelIdAdvertising(
-            modelId,
-            antiSpoofingKeyString,
-            ProviderStatusEvents(callbackId)
-        )
-    }
-
-    /** Tears down the Fast Pair provider simulator. */
-    @Rpc(description = "Tears down FP provider simulator.")
-    fun teardownProviderSimulator() {
-        fastPairProviderSimulatorController.teardownProviderSimulator()
-    }
-
-    /** Gets BLE mac address of the Fast Pair provider simulator. */
-    @Rpc(description = "Gets BLE mac address of the Fast Pair provider simulator.")
-    fun getBluetoothLeAddress(): String {
-        return fastPairProviderSimulatorController.getProviderSimulatorBleAddress()
-    }
-
-    /** Gets the latest account key received on the Fast Pair provider simulator */
-    @Rpc(description = "Gets the latest account key received on the Fast Pair provider simulator.")
-    fun getLatestReceivedAccountKey(): String? {
-        return fastPairProviderSimulatorController.getLatestReceivedAccountKey()
-    }
-}
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/controller/FastPairProviderSimulatorController.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/controller/FastPairProviderSimulatorController.kt
deleted file mode 100644
index a2d2659..0000000
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/controller/FastPairProviderSimulatorController.kt
+++ /dev/null
@@ -1,133 +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.nearby.multidevices.fastpair.provider.controller
-
-import android.bluetooth.le.AdvertiseSettings
-import android.content.Context
-import android.nearby.fastpair.provider.FastPairSimulator
-import android.nearby.fastpair.provider.bluetooth.BluetoothController
-import com.google.android.mobly.snippet.util.Log
-import com.google.common.io.BaseEncoding.base64
-
-class FastPairProviderSimulatorController(private val context: Context) :
-    FastPairSimulator.AdvertisingChangedCallback, BluetoothController.EventListener {
-    private lateinit var bluetoothController: BluetoothController
-    private lateinit var eventListener: EventListener
-    private var simulator: FastPairSimulator? = null
-
-    fun setupProviderSimulator(listener: EventListener) {
-        eventListener = listener
-
-        bluetoothController = BluetoothController(context, this)
-        bluetoothController.registerBluetoothStateReceiver()
-        bluetoothController.enableBluetooth()
-        bluetoothController.connectA2DPSinkProfile()
-    }
-
-    fun teardownProviderSimulator() {
-        simulator?.destroy()
-        bluetoothController.unregisterBluetoothStateReceiver()
-    }
-
-    fun startModelIdAdvertising(
-        modelId: String,
-        antiSpoofingKeyString: String,
-        listener: EventListener
-    ) {
-        eventListener = listener
-
-        val antiSpoofingKey = base64().decode(antiSpoofingKeyString)
-        simulator = FastPairSimulator(
-            context, FastPairSimulator.Options.builder(modelId)
-                .setAdvertisingModelId(modelId)
-                .setBluetoothAddress(null)
-                .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
-                .setAdvertisingChangedCallback(this)
-                .setAntiSpoofingPrivateKey(antiSpoofingKey)
-                .setUseRandomSaltForAccountKeyRotation(false)
-                .setDataOnlyConnection(false)
-                .setShowsPasskeyConfirmation(false)
-                .setRemoveAllDevicesDuringPairing(true)
-                .build()
-        )
-    }
-
-    fun getProviderSimulatorBleAddress() = simulator!!.bleAddress!!
-
-    fun getLatestReceivedAccountKey() =
-        simulator!!.accountKey?.let { base64().encode(it.toByteArray()) }
-
-    /**
-     * Called when we change our BLE advertisement.
-     *
-     * @param isAdvertising the advertising status.
-     */
-    override fun onAdvertisingChanged(isAdvertising: Boolean) {
-        Log.i("FastPairSimulator onAdvertisingChanged(isAdvertising: $isAdvertising)")
-        eventListener.onAdvertisingChange(isAdvertising)
-    }
-
-    /** The callback for the first onServiceConnected of A2DP sink profile. */
-    override fun onA2DPSinkProfileConnected() {
-        eventListener.onA2DPSinkProfileConnected()
-    }
-
-    /**
-     * Reports the current bond state of the remote device.
-     *
-     * @param bondState the bond state of the remote device.
-     */
-    override fun onBondStateChanged(bondState: Int) {
-    }
-
-    /**
-     * Reports the current connection state of the remote device.
-     *
-     * @param connectionState the bond state of the remote device.
-     */
-    override fun onConnectionStateChanged(connectionState: Int) {
-    }
-
-    /**
-     * Reports the current scan mode of the local Adapter.
-     *
-     * @param mode the current scan mode of the local Adapter.
-     */
-    override fun onScanModeChange(mode: Int) {
-        eventListener.onScanModeChange(FastPairSimulator.scanModeToString(mode))
-    }
-
-    /** Interface for listening the events from Fast Pair Provider Simulator. */
-    interface EventListener {
-        /** Reports the first onServiceConnected of A2DP sink profile. */
-        fun onA2DPSinkProfileConnected()
-
-        /**
-         * Reports the current scan mode of the local Adapter.
-         *
-         * @param mode the current scan mode in string.
-         */
-        fun onScanModeChange(mode: String)
-
-        /**
-         * Indicates the advertising state of the Fast Pair provider simulator has changed.
-         *
-         * @param isAdvertising the current advertising state, true if advertising otherwise false.
-         */
-        fun onAdvertisingChange(isAdvertising: Boolean)
-    }
-}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/events/ProviderStatusEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/events/ProviderStatusEvents.kt
deleted file mode 100644
index 2addd77..0000000
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/events/ProviderStatusEvents.kt
+++ /dev/null
@@ -1,50 +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.nearby.multidevices.fastpair.provider.events
-
-import android.nearby.multidevices.fastpair.provider.controller.FastPairProviderSimulatorController
-import com.google.android.mobly.snippet.util.postSnippetEvent
-
-/** The Mobly snippet events to report to the Python side. */
-class ProviderStatusEvents(private val callbackId: String) :
-    FastPairProviderSimulatorController.EventListener {
-
-    /** Reports the first onServiceConnected of A2DP sink profile. */
-    override fun onA2DPSinkProfileConnected() {
-        postSnippetEvent(callbackId, "onA2DPSinkProfileConnected") {}
-    }
-
-    /**
-     * Indicates the Bluetooth scan mode of the Fast Pair provider simulator has changed.
-     *
-     * @param mode the current scan mode in String mapping by [FastPairSimulator#scanModeToString].
-     */
-    override fun onScanModeChange(mode: String) {
-        postSnippetEvent(callbackId, "onScanModeChange") { putString("mode", mode) }
-    }
-
-    /**
-     * Indicates the advertising state of the Fast Pair provider simulator has changed.
-     *
-     * @param isAdvertising the current advertising state, true if advertising otherwise false.
-     */
-    override fun onAdvertisingChange(isAdvertising: Boolean) {
-        postSnippetEvent(callbackId, "onAdvertisingChange") {
-            putBoolean("isAdvertising", isAdvertising)
-        }
-    }
-}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt
deleted file mode 100644
index a2c39f7..0000000
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt
+++ /dev/null
@@ -1,174 +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.nearby.multidevices.fastpair.seeker
-
-import android.content.Context
-import android.nearby.FastPairDeviceMetadata
-import android.nearby.NearbyManager
-import android.nearby.ScanCallback
-import android.nearby.ScanRequest
-import android.nearby.fastpair.seeker.FAKE_TEST_ACCOUNT_NAME
-import android.nearby.integration.ui.CheckNearbyHalfSheetUiTest
-import android.nearby.integration.ui.DismissNearbyHalfSheetUiTest
-import android.nearby.integration.ui.PairByNearbyHalfSheetUiTest
-import android.nearby.multidevices.fastpair.seeker.data.FastPairTestDataManager
-import android.nearby.multidevices.fastpair.seeker.events.PairingCallbackEvents
-import android.nearby.multidevices.fastpair.seeker.events.ScanCallbackEvents
-import android.provider.Settings
-import androidx.test.core.app.ApplicationProvider
-import com.google.android.mobly.snippet.Snippet
-import com.google.android.mobly.snippet.rpc.AsyncRpc
-import com.google.android.mobly.snippet.rpc.Rpc
-import com.google.android.mobly.snippet.util.Log
-
-/** Expose Mobly RPC methods for Python side to test fast pair seeker role. */
-class FastPairSeekerSnippet : Snippet {
-    private val appContext = ApplicationProvider.getApplicationContext<Context>()
-    private val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
-    private val fastPairTestDataManager = FastPairTestDataManager(appContext)
-    private lateinit var scanCallback: ScanCallback
-
-    /**
-     * Starts scanning as a Fast Pair seeker to find provider devices.
-     *
-     * @param callbackId the callback ID corresponding to the {@link FastPairSeekerSnippet#startScan}
-     * call that started the scanning.
-     */
-    @AsyncRpc(description = "Starts scanning as Fast Pair seeker to find provider devices.")
-    fun startScan(callbackId: String) {
-        val scanRequest = ScanRequest.Builder()
-            .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
-            .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
-            .setBleEnabled(true)
-            .build()
-        scanCallback = ScanCallbackEvents(callbackId)
-
-        Log.i("Start Fast Pair scanning via BLE...")
-        nearbyManager.startScan(scanRequest, /* executor */ { it.run() }, scanCallback)
-    }
-
-    /** Stops the Fast Pair seeker scanning. */
-    @Rpc(description = "Stops the Fast Pair seeker scanning.")
-    fun stopScan() {
-        Log.i("Stop Fast Pair scanning.")
-        nearbyManager.stopScan(scanCallback)
-    }
-
-    /** Waits and asserts the HalfSheet showed for Fast Pair pairing.
-     *
-     * @param modelId the expected model id to be associated with the HalfSheet.
-     * @param timeout the number of seconds to wait before giving up.
-     */
-    @Rpc(description = "Waits the HalfSheet showed for Fast Pair pairing.")
-    fun waitAndAssertHalfSheetShowed(modelId: String, timeout: Int) {
-        Log.i("Waits and asserts the HalfSheet showed for Fast Pair model $modelId.")
-
-        val deviceMetadata: FastPairDeviceMetadata =
-            fastPairTestDataManager.testDataCache.getFastPairDeviceMetadata(modelId)
-                ?: throw IllegalArgumentException(
-                    "Can't find $modelId-FastPairAntispoofKeyDeviceMetadata pair in " +
-                            "FastPairTestDataCache."
-                )
-        val deviceName = deviceMetadata.name!!
-        val initialPairingDescriptionTemplateText = deviceMetadata.initialPairingDescription!!
-
-        CheckNearbyHalfSheetUiTest().apply {
-            updateTestArguments(
-                waitHalfSheetPopupTimeoutSeconds = timeout,
-                halfSheetTitleText = deviceName,
-                halfSheetSubtitleText = initialPairingDescriptionTemplateText.format(
-                    deviceName,
-                    FAKE_TEST_ACCOUNT_NAME
-                )
-            )
-            checkNearbyHalfSheetUi()
-        }
-    }
-
-    /** Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into test data cache.
-     *
-     * @param modelId a string of model id to be associated with.
-     * @param json a string of FastPairAntispoofKeyDeviceMetadata JSON object.
-     */
-    @Rpc(
-        description =
-        "Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into test data cache."
-    )
-    fun putAntispoofKeyDeviceMetadata(modelId: String, json: String) {
-        Log.i("Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into test data cache.")
-        fastPairTestDataManager.sendAntispoofKeyDeviceMetadata(modelId, json)
-    }
-
-    /** Puts an array of FastPairAccountKeyDeviceMetadata into test data cache.
-     *
-     * @param json a string of FastPairAccountKeyDeviceMetadata JSON array.
-     */
-    @Rpc(description = "Puts an array of FastPairAccountKeyDeviceMetadata into test data cache.")
-    fun putAccountKeyDeviceMetadata(json: String) {
-        Log.i("Puts an array of FastPairAccountKeyDeviceMetadata into test data cache.")
-        fastPairTestDataManager.sendAccountKeyDeviceMetadataJsonArray(json)
-    }
-
-    /** Dumps all FastPairAccountKeyDeviceMetadata from the test data cache. */
-    @Rpc(description = "Dumps all FastPairAccountKeyDeviceMetadata from the test data cache.")
-    fun dumpAccountKeyDeviceMetadata(): String {
-        Log.i("Dumps all FastPairAccountKeyDeviceMetadata from the test data cache.")
-        return fastPairTestDataManager.testDataCache.dumpAccountKeyDeviceMetadataListAsJson()
-    }
-
-    /** Writes into {@link Settings} whether Fast Pair scan is enabled.
-     *
-     * @param enable whether the Fast Pair scan should be enabled.
-     */
-    @Rpc(description = "Writes into Settings whether Fast Pair scan is enabled.")
-    fun setFastPairScanEnabled(enable: Boolean) {
-        Log.i("Writes into Settings whether Fast Pair scan is enabled.")
-        // TODO(b/228406038): Change back to use NearbyManager.setFastPairScanEnabled once un-hide.
-        val resolver = appContext.contentResolver
-        Settings.Secure.putInt(resolver, "fast_pair_scan_enabled", if (enable) 1 else 0)
-    }
-
-    /** Dismisses the half sheet UI if showed. */
-    @Rpc(description = "Dismisses the half sheet UI if showed.")
-    fun dismissHalfSheet() {
-        Log.i("Dismisses the half sheet UI if showed.")
-
-        DismissNearbyHalfSheetUiTest().dismissHalfSheet()
-    }
-
-    /** Starts pairing by interacting with half sheet UI.
-     *
-     * @param callbackId the callback ID corresponding to the
-     * {@link FastPairSeekerSnippet#startPairing} call that started the pairing.
-     */
-    @AsyncRpc(description = "Starts pairing by interacting with half sheet UI.")
-    fun startPairing(callbackId: String) {
-        Log.i("Starts pairing by interacting with half sheet UI.")
-
-        PairByNearbyHalfSheetUiTest().clickConnectButton()
-        fastPairTestDataManager.registerDataReceiveListener(PairingCallbackEvents(callbackId))
-    }
-
-    /** Invokes when the snippet runner shutting down. */
-    override fun shutdown() {
-        super.shutdown()
-
-        Log.i("Resets the Fast Pair test data cache.")
-        fastPairTestDataManager.unregisterDataReceiveListener()
-        fastPairTestDataManager.sendResetCache()
-    }
-}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt
deleted file mode 100644
index 239ac61..0000000
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt
+++ /dev/null
@@ -1,114 +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.nearby.multidevices.fastpair.seeker.data
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.nearby.fastpair.seeker.ACTION_RESET_TEST_DATA_CACHE
-import android.nearby.fastpair.seeker.ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA
-import android.nearby.fastpair.seeker.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA
-import android.nearby.fastpair.seeker.ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA
-import android.nearby.fastpair.seeker.DATA_JSON_STRING_KEY
-import android.nearby.fastpair.seeker.DATA_MODEL_ID_STRING_KEY
-import android.nearby.fastpair.seeker.FastPairTestDataCache
-import android.util.Log
-
-/** Manage local FastPairTestDataCache and send to/sync from the remote cache in data provider. */
-class FastPairTestDataManager(private val context: Context) : BroadcastReceiver() {
-    val testDataCache = FastPairTestDataCache()
-    var listener: EventListener? = null
-
-    /** Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into local and remote cache.
-     *
-     * @param modelId a string of model id to be associated with.
-     * @param json a string of FastPairAntispoofKeyDeviceMetadata JSON object.
-     */
-    fun sendAntispoofKeyDeviceMetadata(modelId: String, json: String) {
-        Intent().also { intent ->
-            intent.action = ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA
-            intent.putExtra(DATA_MODEL_ID_STRING_KEY, modelId)
-            intent.putExtra(DATA_JSON_STRING_KEY, json)
-            context.sendBroadcast(intent)
-        }
-        testDataCache.putAntispoofKeyDeviceMetadata(modelId, json)
-    }
-
-    /** Puts account key device metadata array to local and remote cache.
-     *
-     * @param json a string of FastPairAccountKeyDeviceMetadata JSON array.
-     */
-    fun sendAccountKeyDeviceMetadataJsonArray(json: String) {
-        Intent().also { intent ->
-            intent.action = ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA
-            intent.putExtra(DATA_JSON_STRING_KEY, json)
-            context.sendBroadcast(intent)
-        }
-        testDataCache.putAccountKeyDeviceMetadataJsonArray(json)
-    }
-
-    /** Clears local and remote cache. */
-    fun sendResetCache() {
-        context.sendBroadcast(Intent(ACTION_RESET_TEST_DATA_CACHE))
-        testDataCache.reset()
-    }
-
-    /**
-     * Callback method for receiving Intent broadcast from FastPairTestDataProvider.
-     *
-     * See [BroadcastReceiver#onReceive].
-     *
-     * @param context the Context in which the receiver is running.
-     * @param intent the Intent being received.
-     */
-    override fun onReceive(context: Context, intent: Intent) {
-        when (intent.action) {
-            ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA -> {
-                Log.d(TAG, "ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA received!")
-                val json = intent.getStringExtra(DATA_JSON_STRING_KEY)!!
-                testDataCache.putAccountKeyDeviceMetadataJsonObject(json)
-                listener?.onManageFastPairAccountDevice(json)
-            }
-            else -> Log.d(TAG, "Unknown action received!")
-        }
-    }
-
-    fun registerDataReceiveListener(listener: EventListener) {
-        this.listener = listener
-        val bondStateFilter = IntentFilter(ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA)
-        context.registerReceiver(this, bondStateFilter)
-    }
-
-    fun unregisterDataReceiveListener() {
-        this.listener = null
-        context.unregisterReceiver(this)
-    }
-
-    /** Interface for listening the data receive from the remote cache in data provider. */
-    interface EventListener {
-        /** Reports a FastPairAccountKeyDeviceMetadata write into the cache.
-         *
-         * @param json the FastPairAccountKeyDeviceMetadata as JSON object string.
-         */
-        fun onManageFastPairAccountDevice(json: String)
-    }
-
-    companion object {
-        private const val TAG = "FastPairTestDataManager"
-    }
-}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/PairingCallbackEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/PairingCallbackEvents.kt
deleted file mode 100644
index 19de1d9..0000000
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/PairingCallbackEvents.kt
+++ /dev/null
@@ -1,35 +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.nearby.multidevices.fastpair.seeker.events
-
-import android.nearby.multidevices.fastpair.seeker.data.FastPairTestDataManager
-import com.google.android.mobly.snippet.util.postSnippetEvent
-
-/** The Mobly snippet events to report to the Python side. */
-class PairingCallbackEvents(private val callbackId: String) :
-    FastPairTestDataManager.EventListener {
-
-    /** Reports a FastPairAccountKeyDeviceMetadata write into the cache.
-     *
-     * @param json the FastPairAccountKeyDeviceMetadata as JSON object string.
-     */
-    override fun onManageFastPairAccountDevice(json: String) {
-        postSnippetEvent(callbackId, "onManageAccountDevice") {
-            putString("accountDeviceJsonString", json)
-        }
-    }
-}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/ScanCallbackEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/ScanCallbackEvents.kt
deleted file mode 100644
index 02847b5..0000000
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/ScanCallbackEvents.kt
+++ /dev/null
@@ -1,49 +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.nearby.multidevices.fastpair.seeker.events
-
-import android.nearby.NearbyDevice
-import android.nearby.ScanCallback
-import com.google.android.mobly.snippet.util.postSnippetEvent
-
-/** The Mobly snippet events to report to the Python side. */
-class ScanCallbackEvents(private val callbackId: String) : ScanCallback {
-
-    override fun onDiscovered(device: NearbyDevice) {
-        postSnippetEvent(callbackId, "onDiscovered") {
-            putString("device", device.toString())
-        }
-    }
-
-    override fun onUpdated(device: NearbyDevice) {
-        postSnippetEvent(callbackId, "onUpdated") {
-            putString("device", device.toString())
-        }
-    }
-
-    override fun onLost(device: NearbyDevice) {
-        postSnippetEvent(callbackId, "onLost") {
-            putString("device", device.toString())
-        }
-    }
-
-    override fun onError(errorCode: Int) {
-        postSnippetEvent(callbackId, "onError") {
-            putString("error", errorCode.toString())
-        }
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/Android.bp b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/Android.bp
deleted file mode 100644
index b406776..0000000
--- a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/Android.bp
+++ /dev/null
@@ -1,48 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_library {
-    name: "NearbyFastPairSeekerSharedLib",
-    srcs: ["shared/**/*.kt"],
-    sdk_version: "test_current",
-    static_libs: [
-        // TODO(b/228406038): Remove "framework-nearby-static" once Fast Pair system APIs add back.
-        "framework-nearby-static",
-        "gson",
-        "guava",
-    ],
-}
-
-android_library {
-    name: "NearbyFastPairSeekerDataProviderLib",
-    srcs: ["src/**/*.kt"],
-    sdk_version: "test_current",
-    static_libs: ["NearbyFastPairSeekerSharedLib"],
-}
-
-android_app {
-    name: "NearbyFastPairSeekerDataProvider",
-    sdk_version: "test_current",
-    certificate: "platform",
-    static_libs: ["NearbyFastPairSeekerDataProviderLib"],
-    optimize: {
-        enabled: true,
-        shrink: true,
-        proguard_flags_files: ["proguard.flags"],
-    },
-}
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/AndroidManifest.xml
deleted file mode 100644
index 1d62f04..0000000
--- a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/AndroidManifest.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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.nearby.fastpair.seeker.dataprovider">
-
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-
-    <application>
-        <!-- Fast Pair Data Provider Service which acts as an "overlay" to the
-             framework Fast Pair Data Provider. Only supported on Android T and later.
-             All overlays are protected from non-system access via WRITE_SECURE_SETTINGS.
-             Must stay in the same process as Nearby Discovery Service.
-        -->
-        <service
-            android:name=".FastPairTestDataProviderService"
-            android:exported="true"
-            android:permission="android.permission.WRITE_SECURE_SETTINGS"
-            android:visibleToInstantApps="true">
-            <intent-filter>
-                <action android:name="android.nearby.action.FAST_PAIR_DATA_PROVIDER" />
-            </intent-filter>
-
-            <meta-data
-                android:name="instantapps.clients.allowed"
-                android:value="true" />
-            <meta-data
-                android:name="serviceVersion"
-                android:value="1" />
-        </service>
-    </application>
-
-</manifest>
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/proguard.flags b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/proguard.flags
deleted file mode 100644
index 15debab..0000000
--- a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/proguard.flags
+++ /dev/null
@@ -1,19 +0,0 @@
-# Keep all receivers/service classes.
--keep class android.nearby.fastpair.seeker.** {
-     *;
-}
-
-# Keep names for easy debugging.
--dontobfuscate
-
-# Necessary to allow debugging.
--keepattributes *
-
-# By default, proguard leaves all classes in their original package, which
-# needlessly repeats com.google.android.apps.etc.
--repackageclasses ""
-
-# Allows proguard to make private and protected methods and fields public as
-# part of optimization. This lets proguard inline trivial getter/setter
-# methods.
--allowaccessmodification
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/Constants.kt b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/Constants.kt
deleted file mode 100644
index 6070140..0000000
--- a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/Constants.kt
+++ /dev/null
@@ -1,30 +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.nearby.fastpair.seeker
-
-const val FAKE_TEST_ACCOUNT_NAME = "nearby-mainline-fpseeker@google.com"
-
-const val ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA =
-    "android.nearby.fastpair.seeker.action.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA"
-const val ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA =
-    "android.nearby.fastpair.seeker.action.ACCOUNT_KEY_DEVICE_METADATA"
-const val ACTION_RESET_TEST_DATA_CACHE = "android.nearby.fastpair.seeker.action.RESET"
-const val ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA =
-    "android.nearby.fastpair.seeker.action.WRITE_ACCOUNT_KEY_DEVICE_METADATA"
-
-const val DATA_JSON_STRING_KEY = "json"
-const val DATA_MODEL_ID_STRING_KEY = "modelId"
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt
deleted file mode 100644
index 4fb8832..0000000
--- a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt
+++ /dev/null
@@ -1,265 +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.nearby.fastpair.seeker
-
-import android.nearby.FastPairAccountKeyDeviceMetadata
-import android.nearby.FastPairAntispoofKeyDeviceMetadata
-import android.nearby.FastPairDeviceMetadata
-import android.nearby.FastPairDiscoveryItem
-import com.google.common.io.BaseEncoding
-import com.google.gson.GsonBuilder
-import com.google.gson.annotations.SerializedName
-
-/** Manage a cache of Fast Pair test data for testing. */
-class FastPairTestDataCache {
-    private val gson = GsonBuilder().disableHtmlEscaping().create()
-    private val accountKeyDeviceMetadataList = mutableListOf<FastPairAccountKeyDeviceMetadata>()
-    private val antispoofKeyDeviceMetadataDataMap =
-        mutableMapOf<String, FastPairAntispoofKeyDeviceMetadataData>()
-
-    fun putAccountKeyDeviceMetadataJsonArray(json: String) {
-        accountKeyDeviceMetadataList +=
-            gson.fromJson(json, Array<FastPairAccountKeyDeviceMetadataData>::class.java)
-                .map { it.toFastPairAccountKeyDeviceMetadata() }
-    }
-
-    fun putAccountKeyDeviceMetadataJsonObject(json: String) {
-        accountKeyDeviceMetadataList +=
-            gson.fromJson(json, FastPairAccountKeyDeviceMetadataData::class.java)
-                .toFastPairAccountKeyDeviceMetadata()
-    }
-
-    fun putAccountKeyDeviceMetadata(accountKeyDeviceMetadata: FastPairAccountKeyDeviceMetadata) {
-        accountKeyDeviceMetadataList += accountKeyDeviceMetadata
-    }
-
-    fun getAccountKeyDeviceMetadataList(): List<FastPairAccountKeyDeviceMetadata> =
-        accountKeyDeviceMetadataList.toList()
-
-    fun dumpAccountKeyDeviceMetadataAsJson(metadata: FastPairAccountKeyDeviceMetadata): String =
-        gson.toJson(FastPairAccountKeyDeviceMetadataData(metadata))
-
-    fun dumpAccountKeyDeviceMetadataListAsJson(): String =
-        gson.toJson(accountKeyDeviceMetadataList.map { FastPairAccountKeyDeviceMetadataData(it) })
-
-    fun putAntispoofKeyDeviceMetadata(modelId: String, json: String) {
-        antispoofKeyDeviceMetadataDataMap[modelId] =
-            gson.fromJson(json, FastPairAntispoofKeyDeviceMetadataData::class.java)
-    }
-
-    fun getAntispoofKeyDeviceMetadata(modelId: String): FastPairAntispoofKeyDeviceMetadata? {
-        return antispoofKeyDeviceMetadataDataMap[modelId]?.toFastPairAntispoofKeyDeviceMetadata()
-    }
-
-    fun getFastPairDeviceMetadata(modelId: String): FastPairDeviceMetadata? =
-        antispoofKeyDeviceMetadataDataMap[modelId]?.deviceMeta?.toFastPairDeviceMetadata()
-
-    fun reset() {
-        accountKeyDeviceMetadataList.clear()
-        antispoofKeyDeviceMetadataDataMap.clear()
-    }
-
-    data class FastPairAccountKeyDeviceMetadataData(
-        @SerializedName("account_key") val accountKey: String?,
-        @SerializedName("sha256_account_key_public_address") val accountKeyPublicAddress: String?,
-        @SerializedName("fast_pair_device_metadata") val deviceMeta: FastPairDeviceMetadataData?,
-        @SerializedName("fast_pair_discovery_item") val discoveryItem: FastPairDiscoveryItemData?
-    ) {
-        constructor(meta: FastPairAccountKeyDeviceMetadata) : this(
-            accountKey = meta.deviceAccountKey?.base64Encode(),
-            accountKeyPublicAddress = meta.sha256DeviceAccountKeyPublicAddress?.base64Encode(),
-            deviceMeta = meta.fastPairDeviceMetadata?.let { FastPairDeviceMetadataData(it) },
-            discoveryItem = meta.fastPairDiscoveryItem?.let { FastPairDiscoveryItemData(it) }
-        )
-
-        fun toFastPairAccountKeyDeviceMetadata(): FastPairAccountKeyDeviceMetadata {
-            return FastPairAccountKeyDeviceMetadata.Builder()
-                .setDeviceAccountKey(accountKey?.base64Decode())
-                .setSha256DeviceAccountKeyPublicAddress(accountKeyPublicAddress?.base64Decode())
-                .setFastPairDeviceMetadata(deviceMeta?.toFastPairDeviceMetadata())
-                .setFastPairDiscoveryItem(discoveryItem?.toFastPairDiscoveryItem())
-                .build()
-        }
-    }
-
-    data class FastPairAntispoofKeyDeviceMetadataData(
-        @SerializedName("anti_spoofing_public_key_str") val antispoofPublicKey: String?,
-        @SerializedName("fast_pair_device_metadata") val deviceMeta: FastPairDeviceMetadataData?
-    ) {
-        fun toFastPairAntispoofKeyDeviceMetadata(): FastPairAntispoofKeyDeviceMetadata {
-            return FastPairAntispoofKeyDeviceMetadata.Builder()
-                .setAntispoofPublicKey(antispoofPublicKey?.base64Decode())
-                .setFastPairDeviceMetadata(deviceMeta?.toFastPairDeviceMetadata())
-                .build()
-        }
-    }
-
-    data class FastPairDeviceMetadataData(
-        @SerializedName("ble_tx_power") val bleTxPower: Int,
-        @SerializedName("connect_success_companion_app_installed") val compAppInstalled: String?,
-        @SerializedName("connect_success_companion_app_not_installed") val comAppNotIns: String?,
-        @SerializedName("device_type") val deviceType: Int,
-        @SerializedName("download_companion_app_description") val downloadComApp: String?,
-        @SerializedName("fail_connect_go_to_settings_description") val failConnectDes: String?,
-        @SerializedName("image_url") val imageUrl: String?,
-        @SerializedName("initial_notification_description") val initNotification: String?,
-        @SerializedName("initial_notification_description_no_account") val initNoAccount: String?,
-        @SerializedName("initial_pairing_description") val initialPairingDescription: String?,
-        @SerializedName("intent_uri") val intentUri: String?,
-        @SerializedName("name") val name: String?,
-        @SerializedName("open_companion_app_description") val openCompanionAppDescription: String?,
-        @SerializedName("retroactive_pairing_description") val retroactivePairingDes: String?,
-        @SerializedName("subsequent_pairing_description") val subsequentPairingDescription: String?,
-        @SerializedName("trigger_distance") val triggerDistance: Double,
-        @SerializedName("case_url") val trueWirelessImageUrlCase: String?,
-        @SerializedName("left_bud_url") val trueWirelessImageUrlLeftBud: String?,
-        @SerializedName("right_bud_url") val trueWirelessImageUrlRightBud: String?,
-        @SerializedName("unable_to_connect_description") val unableToConnectDescription: String?,
-        @SerializedName("unable_to_connect_title") val unableToConnectTitle: String?,
-        @SerializedName("update_companion_app_description") val updateCompAppDes: String?,
-        @SerializedName("wait_launch_companion_app_description") val waitLaunchCompApp: String?
-    ) {
-        constructor(meta: FastPairDeviceMetadata) : this(
-            bleTxPower = meta.bleTxPower,
-            compAppInstalled = meta.connectSuccessCompanionAppInstalled,
-            comAppNotIns = meta.connectSuccessCompanionAppNotInstalled,
-            deviceType = meta.deviceType,
-            downloadComApp = meta.downloadCompanionAppDescription,
-            failConnectDes = meta.failConnectGoToSettingsDescription,
-            imageUrl = meta.imageUrl,
-            initNotification = meta.initialNotificationDescription,
-            initNoAccount = meta.initialNotificationDescriptionNoAccount,
-            initialPairingDescription = meta.initialPairingDescription,
-            intentUri = meta.intentUri,
-            name = meta.name,
-            openCompanionAppDescription = meta.openCompanionAppDescription,
-            retroactivePairingDes = meta.retroactivePairingDescription,
-            subsequentPairingDescription = meta.subsequentPairingDescription,
-            triggerDistance = meta.triggerDistance.toDouble(),
-            trueWirelessImageUrlCase = meta.trueWirelessImageUrlCase,
-            trueWirelessImageUrlLeftBud = meta.trueWirelessImageUrlLeftBud,
-            trueWirelessImageUrlRightBud = meta.trueWirelessImageUrlRightBud,
-            unableToConnectDescription = meta.unableToConnectDescription,
-            unableToConnectTitle = meta.unableToConnectTitle,
-            updateCompAppDes = meta.updateCompanionAppDescription,
-            waitLaunchCompApp = meta.waitLaunchCompanionAppDescription
-        )
-
-        fun toFastPairDeviceMetadata(): FastPairDeviceMetadata {
-            return FastPairDeviceMetadata.Builder()
-                .setBleTxPower(bleTxPower)
-                .setConnectSuccessCompanionAppInstalled(compAppInstalled)
-                .setConnectSuccessCompanionAppNotInstalled(comAppNotIns)
-                .setDeviceType(deviceType)
-                .setDownloadCompanionAppDescription(downloadComApp)
-                .setFailConnectGoToSettingsDescription(failConnectDes)
-                .setImageUrl(imageUrl)
-                .setInitialNotificationDescription(initNotification)
-                .setInitialNotificationDescriptionNoAccount(initNoAccount)
-                .setInitialPairingDescription(initialPairingDescription)
-                .setIntentUri(intentUri)
-                .setName(name)
-                .setOpenCompanionAppDescription(openCompanionAppDescription)
-                .setRetroactivePairingDescription(retroactivePairingDes)
-                .setSubsequentPairingDescription(subsequentPairingDescription)
-                .setTriggerDistance(triggerDistance.toFloat())
-                .setTrueWirelessImageUrlCase(trueWirelessImageUrlCase)
-                .setTrueWirelessImageUrlLeftBud(trueWirelessImageUrlLeftBud)
-                .setTrueWirelessImageUrlRightBud(trueWirelessImageUrlRightBud)
-                .setUnableToConnectDescription(unableToConnectDescription)
-                .setUnableToConnectTitle(unableToConnectTitle)
-                .setUpdateCompanionAppDescription(updateCompAppDes)
-                .setWaitLaunchCompanionAppDescription(waitLaunchCompApp)
-                .build()
-        }
-    }
-
-    data class FastPairDiscoveryItemData(
-        @SerializedName("action_url") val actionUrl: String?,
-        @SerializedName("action_url_type") val actionUrlType: Int,
-        @SerializedName("app_name") val appName: String?,
-        @SerializedName("authentication_public_key_secp256r1") val authenticationPublicKey: String?,
-        @SerializedName("description") val description: String?,
-        @SerializedName("device_name") val deviceName: String?,
-        @SerializedName("display_url") val displayUrl: String?,
-        @SerializedName("first_observation_timestamp_millis") val firstObservationMs: Long,
-        @SerializedName("icon_fife_url") val iconFfeUrl: String?,
-        @SerializedName("icon_png") val iconPng: String?,
-        @SerializedName("id") val id: String?,
-        @SerializedName("last_observation_timestamp_millis") val lastObservationMs: Long,
-        @SerializedName("mac_address") val macAddress: String?,
-        @SerializedName("package_name") val packageName: String?,
-        @SerializedName("pending_app_install_timestamp_millis") val pendingAppInstallMs: Long,
-        @SerializedName("rssi") val rssi: Int,
-        @SerializedName("state") val state: Int,
-        @SerializedName("title") val title: String?,
-        @SerializedName("trigger_id") val triggerId: String?,
-        @SerializedName("tx_power") val txPower: Int
-    ) {
-        constructor(item: FastPairDiscoveryItem) : this(
-            actionUrl = item.actionUrl,
-            actionUrlType = item.actionUrlType,
-            appName = item.appName,
-            authenticationPublicKey = item.authenticationPublicKeySecp256r1?.base64Encode(),
-            description = item.description,
-            deviceName = item.deviceName,
-            displayUrl = item.displayUrl,
-            firstObservationMs = item.firstObservationTimestampMillis,
-            iconFfeUrl = item.iconFfeUrl,
-            iconPng = item.iconPng?.base64Encode(),
-            id = item.id,
-            lastObservationMs = item.lastObservationTimestampMillis,
-            macAddress = item.macAddress,
-            packageName = item.packageName,
-            pendingAppInstallMs = item.pendingAppInstallTimestampMillis,
-            rssi = item.rssi,
-            state = item.state,
-            title = item.title,
-            triggerId = item.triggerId,
-            txPower = item.txPower
-        )
-
-        fun toFastPairDiscoveryItem(): FastPairDiscoveryItem {
-            return FastPairDiscoveryItem.Builder()
-                .setActionUrl(actionUrl)
-                .setActionUrlType(actionUrlType)
-                .setAppName(appName)
-                .setAuthenticationPublicKeySecp256r1(authenticationPublicKey?.base64Decode())
-                .setDescription(description)
-                .setDeviceName(deviceName)
-                .setDisplayUrl(displayUrl)
-                .setFirstObservationTimestampMillis(firstObservationMs)
-                .setIconFfeUrl(iconFfeUrl)
-                .setIconPng(iconPng?.base64Decode())
-                .setId(id)
-                .setLastObservationTimestampMillis(lastObservationMs)
-                .setMacAddress(macAddress)
-                .setPackageName(packageName)
-                .setPendingAppInstallTimestampMillis(pendingAppInstallMs)
-                .setRssi(rssi)
-                .setState(state)
-                .setTitle(title)
-                .setTriggerId(triggerId)
-                .setTxPower(txPower)
-                .build()
-        }
-    }
-}
-
-private fun String.base64Decode(): ByteArray = BaseEncoding.base64().decode(this)
-
-private fun ByteArray.base64Encode(): String = BaseEncoding.base64().encode(this)
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt
deleted file mode 100644
index e924da1..0000000
--- a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt
+++ /dev/null
@@ -1,88 +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.nearby.fastpair.seeker.data
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.nearby.FastPairAccountKeyDeviceMetadata
-import android.nearby.fastpair.seeker.ACTION_RESET_TEST_DATA_CACHE
-import android.nearby.fastpair.seeker.ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA
-import android.nearby.fastpair.seeker.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA
-import android.nearby.fastpair.seeker.ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA
-import android.nearby.fastpair.seeker.DATA_JSON_STRING_KEY
-import android.nearby.fastpair.seeker.DATA_MODEL_ID_STRING_KEY
-import android.nearby.fastpair.seeker.FastPairTestDataCache
-import android.util.Log
-
-/** Manage local FastPairTestDataCache and receive/update the remote cache in test snippet. */
-class FastPairTestDataManager(private val context: Context) : BroadcastReceiver() {
-    val testDataCache = FastPairTestDataCache()
-
-    /** Writes a FastPairAccountKeyDeviceMetadata into local and remote cache.
-     *
-     * @param accountKeyDeviceMetadata the FastPairAccountKeyDeviceMetadata to write.
-     * @return a json object string of the accountKeyDeviceMetadata.
-     */
-    fun writeAccountKeyDeviceMetadata(
-        accountKeyDeviceMetadata: FastPairAccountKeyDeviceMetadata
-    ): String {
-        testDataCache.putAccountKeyDeviceMetadata(accountKeyDeviceMetadata)
-
-        val json =
-            testDataCache.dumpAccountKeyDeviceMetadataAsJson(accountKeyDeviceMetadata)
-        Intent().also { intent ->
-            intent.action = ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA
-            intent.putExtra(DATA_JSON_STRING_KEY, json)
-            context.sendBroadcast(intent)
-        }
-        return json
-    }
-
-    /**
-     * Callback method for receiving Intent broadcast from test snippet.
-     *
-     * See [BroadcastReceiver#onReceive].
-     *
-     * @param context the Context in which the receiver is running.
-     * @param intent the Intent being received.
-     */
-    override fun onReceive(context: Context, intent: Intent) {
-        when (intent.action) {
-            ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA -> {
-                Log.d(TAG, "ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA received!")
-                val modelId = intent.getStringExtra(DATA_MODEL_ID_STRING_KEY)!!
-                val json = intent.getStringExtra(DATA_JSON_STRING_KEY)!!
-                testDataCache.putAntispoofKeyDeviceMetadata(modelId, json)
-            }
-            ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA -> {
-                Log.d(TAG, "ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA received!")
-                val json = intent.getStringExtra(DATA_JSON_STRING_KEY)!!
-                testDataCache.putAccountKeyDeviceMetadataJsonArray(json)
-            }
-            ACTION_RESET_TEST_DATA_CACHE -> {
-                Log.d(TAG, "ACTION_RESET_TEST_DATA_CACHE received!")
-                testDataCache.reset()
-            }
-            else -> Log.d(TAG, "Unknown action received!")
-        }
-    }
-
-    companion object {
-        private const val TAG = "FastPairTestDataManager"
-    }
-}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/dataprovider/FastPairTestDataProviderService.kt b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/dataprovider/FastPairTestDataProviderService.kt
deleted file mode 100644
index aec1379..0000000
--- a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/dataprovider/FastPairTestDataProviderService.kt
+++ /dev/null
@@ -1,142 +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.nearby.fastpair.seeker.dataprovider
-
-import android.accounts.Account
-import android.content.IntentFilter
-import android.nearby.FastPairDataProviderService
-import android.nearby.FastPairEligibleAccount
-import android.nearby.fastpair.seeker.ACTION_RESET_TEST_DATA_CACHE
-import android.nearby.fastpair.seeker.ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA
-import android.nearby.fastpair.seeker.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA
-import android.nearby.fastpair.seeker.FAKE_TEST_ACCOUNT_NAME
-import android.nearby.fastpair.seeker.data.FastPairTestDataManager
-import android.util.Log
-
-/**
- * Fast Pair Test Data Provider Service entry point for platform overlay.
- */
-class FastPairTestDataProviderService : FastPairDataProviderService(TAG) {
-    private lateinit var testDataManager: FastPairTestDataManager
-
-    override fun onCreate() {
-        Log.d(TAG, "onCreate()")
-        testDataManager = FastPairTestDataManager(this)
-
-        val bondStateFilter = IntentFilter(ACTION_RESET_TEST_DATA_CACHE).apply {
-            addAction(ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA)
-            addAction(ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA)
-        }
-        registerReceiver(testDataManager, bondStateFilter)
-    }
-
-    override fun onDestroy() {
-        Log.d(TAG, "onDestroy()")
-        unregisterReceiver(testDataManager)
-
-        super.onDestroy()
-    }
-
-    override fun onLoadFastPairAntispoofKeyDeviceMetadata(
-        request: FastPairAntispoofKeyDeviceMetadataRequest,
-        callback: FastPairAntispoofKeyDeviceMetadataCallback
-    ) {
-        val requestedModelId = request.modelId.bytesToStringLowerCase()
-        Log.d(TAG, "onLoadFastPairAntispoofKeyDeviceMetadata(modelId: $requestedModelId)")
-
-        val fastPairAntispoofKeyDeviceMetadata =
-            testDataManager.testDataCache.getAntispoofKeyDeviceMetadata(requestedModelId)
-        if (fastPairAntispoofKeyDeviceMetadata != null) {
-            callback.onFastPairAntispoofKeyDeviceMetadataReceived(
-                fastPairAntispoofKeyDeviceMetadata
-            )
-        } else {
-            Log.d(TAG, "No metadata available for $requestedModelId!")
-            callback.onError(ERROR_CODE_BAD_REQUEST, "No metadata available for $requestedModelId")
-        }
-    }
-
-    override fun onLoadFastPairAccountDevicesMetadata(
-        request: FastPairAccountDevicesMetadataRequest,
-        callback: FastPairAccountDevicesMetadataCallback
-    ) {
-        val requestedAccount = request.account
-        val requestedAccountKeys = request.deviceAccountKeys
-        Log.d(
-            TAG, "onLoadFastPairAccountDevicesMetadata(" +
-                    "account: $requestedAccount, accountKeys:$requestedAccountKeys)"
-        )
-        Log.d(TAG, testDataManager.testDataCache.dumpAccountKeyDeviceMetadataListAsJson())
-
-        callback.onFastPairAccountDevicesMetadataReceived(
-            testDataManager.testDataCache.getAccountKeyDeviceMetadataList()
-        )
-    }
-
-    override fun onLoadFastPairEligibleAccounts(
-        request: FastPairEligibleAccountsRequest,
-        callback: FastPairEligibleAccountsCallback
-    ) {
-        Log.d(TAG, "onLoadFastPairEligibleAccounts()")
-        callback.onFastPairEligibleAccountsReceived(ELIGIBLE_ACCOUNTS_TEST_CONSTANT)
-    }
-
-    override fun onManageFastPairAccount(
-        request: FastPairManageAccountRequest,
-        callback: FastPairManageActionCallback
-    ) {
-        val requestedAccount = request.account
-        val requestType = request.requestType
-        Log.d(TAG, "onManageFastPairAccount(account: $requestedAccount, requestType: $requestType)")
-
-        callback.onSuccess()
-    }
-
-    override fun onManageFastPairAccountDevice(
-        request: FastPairManageAccountDeviceRequest,
-        callback: FastPairManageActionCallback
-    ) {
-        val requestedAccount = request.account
-        val requestType = request.requestType
-        val requestTypeString = if (requestType == MANAGE_REQUEST_ADD) "Add" else "Remove"
-        val requestedAccountKeyDeviceMetadata = request.accountKeyDeviceMetadata
-        Log.d(
-            TAG,
-            "onManageFastPairAccountDevice(requestedAccount: $requestedAccount, " +
-                    "requestType: $requestTypeString,"
-        )
-
-        val requestedAccountKeyDeviceMetadataInJson =
-            testDataManager.writeAccountKeyDeviceMetadata(requestedAccountKeyDeviceMetadata)
-        Log.d(TAG, "requestedAccountKeyDeviceMetadata: $requestedAccountKeyDeviceMetadataInJson)")
-
-        callback.onSuccess()
-    }
-
-    companion object {
-        private const val TAG = "FastPairTestDataProviderService"
-        private val ELIGIBLE_ACCOUNTS_TEST_CONSTANT = listOf(
-            FastPairEligibleAccount.Builder()
-                .setAccount(Account(FAKE_TEST_ACCOUNT_NAME, "FakeTestAccount"))
-                .setOptIn(true)
-                .build()
-        )
-
-        private fun ByteArray.bytesToStringLowerCase(): String =
-            joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
deleted file mode 100644
index 298c9dc..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
+++ /dev/null
@@ -1,46 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_library {
-    name: "NearbyFastPairProviderLib",
-    srcs: [
-        "src/**/*.java",
-        "src/**/*.kt",
-    ],
-    sdk_version: "core_platform",
-    libs: [
-        // order matters: classes in framework-bluetooth are resolved before framework, meaning
-        // @hide APIs in framework-bluetooth are resolved before @SystemApi stubs in framework
-        "framework-bluetooth.impl",
-        "framework",
-
-        // if sdk_version="" this gets automatically included, but here we need to add manually.
-        "framework-res",
-    ],
-    static_libs: [
-        "NearbyFastPairProviderLiteProtos",
-        "androidx.core_core",
-        "androidx.test.core",
-        "error_prone_annotations",
-        "fast-pair-lite-protos",
-        "framework-annotations-lib",
-        "guava",
-        "kotlin-stdlib",
-        "nearby-common-lib",
-    ],
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/AndroidManifest.xml
deleted file mode 100644
index 400a434..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/AndroidManifest.xml
+++ /dev/null
@@ -1,32 +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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.nearby.fastpair.provider">
-
-    <uses-feature android:name="android.hardware.bluetooth" />
-    <uses-feature android:name="android.hardware.bluetooth_le" />
-    <uses-permission android:name="android.permission.BLUETOOTH" />
-    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
-    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
-    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
-    <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
-    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
-    <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
-
-</manifest>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/Android.bp
deleted file mode 100644
index 7ae43e5..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/Android.bp
+++ /dev/null
@@ -1,30 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_library {
-    name: "NearbyFastPairProviderLiteProtos",
-    proto: {
-        type: "lite",
-        canonical_path_from_root: false,
-    },
-    sdk_version: "system_current",
-    min_sdk_version: "30",
-    srcs: ["*.proto"],
-}
-
-
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/event_stream_protocol.proto b/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/event_stream_protocol.proto
deleted file mode 100644
index 54db34a..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/event_stream_protocol.proto
+++ /dev/null
@@ -1,85 +0,0 @@
-syntax = "proto2";
-
-package android.nearby.fastpair.provider;
-
-option java_package = "android.nearby.fastpair.provider";
-option java_outer_classname = "EventStreamProtocol";
-
-enum EventGroup {
-  UNSPECIFIED = 0;
-  BLUETOOTH = 1;
-  LOGGING = 2;
-  DEVICE = 3;
-  DEVICE_ACTION = 4;
-  DEVICE_CONFIGURATION = 5;
-  DEVICE_CAPABILITY_SYNC = 6;
-  SMART_AUDIO_SOURCE_SWITCHING = 7;
-  ACKNOWLEDGEMENT = 255;
-}
-
-enum BluetoothEventCode {
-  BLUETOOTH_UNSPECIFIED = 0;
-  BLUETOOTH_ENABLE_SILENCE_MODE = 1;
-  BLUETOOTH_DISABLE_SILENCE_MODE = 2;
-}
-
-enum LoggingEventCode {
-  LOG_UNSPECIFIED = 0;
-  LOG_FULL = 1;
-  LOG_SAVE_TO_BUFFER = 2;
-}
-
-enum DeviceEventCode {
-  DEVICE_UNSPECIFIED = 0;
-  DEVICE_MODEL_ID = 1;
-  DEVICE_BLE_ADDRESS = 2;
-  DEVICE_BATTERY_INFO = 3;
-  ACTIVE_COMPONENTS_REQUEST = 5;
-  ACTIVE_COMPONENTS_RESPONSE = 6;
-  DEVICE_CAPABILITY = 7;
-  PLATFORM_TYPE = 8;
-  FIRMWARE_VERSION = 9;
-  SECTION_NONCE = 10;
-}
-
-enum DeviceActionEventCode {
-  DEVICE_ACTION_UNSPECIFIED = 0;
-  DEVICE_ACTION_RING = 1;
-}
-
-enum DeviceConfigurationEventCode {
-  CONFIGURATION_UNSPECIFIED = 0;
-  CONFIGURATION_BUFFER_SIZE = 1;
-}
-
-enum DeviceCapabilitySyncEventCode {
-  REQUEST_UNSPECIFIED = 0;
-  REQUEST_CAPABILITY_UPDATE = 1;
-  CONFIGURABLE_BUFFER_SIZE_RANGE = 2;
-}
-
-enum AcknowledgementEventCode {
-  ACKNOWLEDGEMENT_UNSPECIFIED = 0;
-  ACKNOWLEDGEMENT_ACK = 1;
-  ACKNOWLEDGEMENT_NAK = 2;
-}
-
-enum PlatformType {
-  PLATFORM_TYPE_UNKNOWN = 0;
-  ANDROID = 1;
-}
-
-enum SassEventCode {
-  EVENT_UNSPECIFIED = 0;
-  EVENT_GET_CAPABILITY_OF_SASS = 0x10;
-  EVENT_NOTIFY_CAPABILITY_OF_SASS = 0x11;
-  EVENT_SET_MULTI_POINT_STATE = 0x12;
-  EVENT_SWITCH_AUDIO_SOURCE_BETWEEN_CONNECTED_DEVICES = 0x30;
-  EVENT_SWITCH_BACK = 0x31;
-  EVENT_NOTIFY_MULTIPOINT_SWITCH_EVENT = 0x32;
-  EVENT_GET_CONNECTION_STATUS = 0x33;
-  EVENT_NOTIFY_CONNECTION_STATUS = 0x34;
-  EVENT_SASS_INITIATED_CONNECTION = 0x40;
-  EVENT_INDICATE_IN_USE_ACCOUNT_KEY = 0x41;
-  EVENT_SET_CUSTOM_DATA = 0x42;
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
deleted file mode 100644
index 125c34e..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
+++ /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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-// Build and install NearbyFastPairProviderSimulatorApp to your phone:
-// m NearbyFastPairProviderSimulatorApp
-// adb root
-// adb remount && adb reboot (make first time remount work)
-//
-// adb root
-// adb remount
-// adb push ${ANDROID_PRODUCT_OUT}/system/app/NearbyFastPairProviderSimulatorApp /system/app/
-// adb reboot
-// Grant all permissions requested to NearbyFastPairProviderSimulatorApp before launching it.
-android_app {
-    name: "NearbyFastPairProviderSimulatorApp",
-    sdk_version: "test_current",
-    // Sign with "platform" certificate for accessing Bluetooth @SystemAPI
-    certificate: "platform",
-    static_libs: ["NearbyFastPairProviderSimulatorLib"],
-    optimize: {
-        enabled: true,
-        shrink: true,
-        proguard_flags_files: ["proguard.flags"],
-    },
-}
-
-android_library {
-    name: "NearbyFastPairProviderSimulatorLib",
-    sdk_version: "test_current",
-    srcs: [
-        "src/**/*.java",
-        "src/**/*.kt",
-    ],
-    static_libs: [
-        "NearbyFastPairProviderLib",
-        "NearbyFastPairProviderLiteProtos",
-        "NearbyFastPairProviderSimulatorLiteProtos",
-        "androidx.annotation_annotation",
-        "error_prone_annotations",
-        "fast-pair-lite-protos",
-    ],
-}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/AndroidManifest.xml
deleted file mode 100644
index 8880b11..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/AndroidManifest.xml
+++ /dev/null
@@ -1,42 +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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.nearby.fastpair.provider.simulator.app" >
-
-    <uses-permission android:name="android.permission.BLUETOOTH" />
-    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
-    <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
-
-    <application
-        android:allowBackup="true"
-        android:label="@string/app_name" >
-        <activity
-            android:name=".MainActivity"
-            android:windowSoftInputMode="stateHidden"
-            android:screenOrientation="portrait"
-            android:exported="true">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT"/>
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags
deleted file mode 100644
index 0827c60..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags
+++ /dev/null
@@ -1,19 +0,0 @@
-# Keep AdvertisingSetCallback#onOwnAddressRead callback.
--keep class * extends android.bluetooth.le.AdvertisingSetCallback {
-     *;
-}
-
-# Keep names for easy debugging.
--dontobfuscate
-
-# Necessary to allow debugging.
--keepattributes *
-
-# By default, proguard leaves all classes in their original package, which
-# needlessly repeats com.google.android.apps.etc.
--repackageclasses ""
-
-# Allows proguard to make private and protected methods and fields public as
-# part of optimization. This lets proguard inline trivial getter/setter
-# methods.
--allowaccessmodification
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/Android.bp
deleted file mode 100644
index e964800..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/Android.bp
+++ /dev/null
@@ -1,30 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_library {
-    name: "NearbyFastPairProviderSimulatorLiteProtos",
-    proto: {
-        type: "lite",
-        canonical_path_from_root: false,
-    },
-    sdk_version: "system_current",
-    min_sdk_version: "30",
-    srcs: ["*.proto"],
-}
-
-
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/simulator_stream_protocol.proto b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/simulator_stream_protocol.proto
deleted file mode 100644
index 9b17fda..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/simulator_stream_protocol.proto
+++ /dev/null
@@ -1,110 +0,0 @@
-syntax = "proto2";
-
-package android.nearby.fastpair.provider.simulator;
-
-option java_package = "android.nearby.fastpair.provider.simulator";
-option java_outer_classname = "SimulatorStreamProtocol";
-
-// Used by remote devices to control simulator behaviors.
-message Command {
-  // Type of this command.
-  required Code code = 1;
-
-  // Required for SHOW_BATTERY.
-  optional BatteryInfo battery_info = 2;
-
-  enum Code {
-    // Request for simulator's acknowledge message.
-    POLLING = 0;
-
-    // Reset and clear bluetooth state.
-    RESET = 1;
-
-    // Present battery information in the advertisement.
-    SHOW_BATTERY = 2;
-
-    // Remove battery information in the advertisement.
-    HIDE_BATTERY = 3;
-
-    // Request for BR/EDR address.
-    REQUEST_BLUETOOTH_ADDRESS_PUBLIC = 4;
-
-    // Request for BLE address.
-    REQUEST_BLUETOOTH_ADDRESS_BLE = 5;
-
-    // Request for account key.
-    REQUEST_ACCOUNT_KEY = 6;
-  }
-
-  // Battery information for true wireless headsets.
-  // https://devsite.googleplex.com/nearby/fast-pair/early-access/spec#BatteryNotification
-  message BatteryInfo {
-    // Show or hide the battery UI notification.
-    optional bool suppress_notification = 1;
-    repeated BatteryValue battery_values = 2;
-
-    // Advertised battery level data.
-    message BatteryValue {
-      // The charging flag.
-      required bool charging = 1;
-
-      // Battery level from 0 to 100.
-      required uint32 level = 2;
-    }
-  }
-}
-
-// Notify the remote devices when states are changed or response the command on
-// the simulator.
-message Event {
-  // Type of this event.
-  required Code code = 1;
-
-  // Required for BLUETOOTH_STATE_BOND.
-  optional int32 bond_state = 2;
-
-  // Required for BLUETOOTH_STATE_CONNECTION.
-  optional int32 connection_state = 3;
-
-  // Required for BLUETOOTH_STATE_SCAN_MODE.
-  optional int32 scan_mode = 4;
-
-  // Required for BLUETOOTH_ADDRESS_PUBLIC.
-  optional string public_address = 5;
-
-  // Required for BLUETOOTH_ADDRESS_BLE.
-  optional string ble_address = 6;
-
-  // Required for BLUETOOTH_ALIAS_NAME.
-  optional string alias_name = 7;
-
-  // Required for REQUEST_ACCOUNT_KEY.
-  optional bytes account_key = 8;
-
-  enum Code {
-    // Response the polling.
-    ACKNOWLEDGE = 0;
-
-    // Notify the event android.bluetooth.device.action.BOND_STATE_CHANGED
-    BLUETOOTH_STATE_BOND = 1;
-
-    // Notify the event
-    // android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED
-    BLUETOOTH_STATE_CONNECTION = 2;
-
-    // Notify the event android.bluetooth.adapter.action.SCAN_MODE_CHANGED
-    BLUETOOTH_STATE_SCAN_MODE = 3;
-
-    // Notify the current BR/EDR address
-    BLUETOOTH_ADDRESS_PUBLIC = 4;
-
-    // Notify the current BLE address
-    BLUETOOTH_ADDRESS_BLE = 5;
-
-    // Notify the event android.bluetooth.device.action.ALIAS_CHANGED
-    BLUETOOTH_ALIAS_NAME = 6;
-
-    // Response the REQUEST_ACCOUNT_KEY.
-    ACCOUNT_KEY = 7;
-  }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/activity_main.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/activity_main.xml
deleted file mode 100644
index b7e85eb..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/activity_main.xml
+++ /dev/null
@@ -1,190 +0,0 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:layout_margin="16dp"
-    android:keepScreenOn="true"
-    tools:context=".MainActivity">
-
-    <TextView
-        android:id="@+id/bluetooth_address_text_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textSize="14dp"
-        android:textStyle="bold"
-        android:padding="8dp"/>
-
-    <TextView
-        android:id="@+id/device_name_text_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textSize="14dp"
-        android:textStyle="bold"
-        android:padding="8dp"/>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:padding="8dp"
-        android:orientation="horizontal">
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:textSize="14dp"
-                android:textStyle="bold"
-                android:text="Model ID:"/>
-            <Spinner
-                android:id="@+id/model_id_spinner"
-                android:textSize="14dp"
-                android:textStyle="bold"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_weight="1" />
-        <TextView
-            android:id="@+id/tx_power_text_view"
-            android:layout_width="0dp"
-            android:layout_weight="1"
-            android:layout_height="wrap_content"
-            android:textSize="14dp"
-            android:textStyle="bold" />
-    </LinearLayout>
-
-    <TextView
-        android:id="@+id/anti_spoofing_private_key_text_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textSize="14dp"
-        android:textStyle="bold"
-        android:padding="8dp"/>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal">
-        <TextView
-            android:id="@+id/is_advertising_text_view"
-            android:layout_width="0dp"
-            android:layout_weight="1"
-            android:layout_height="wrap_content"
-            android:textSize="14dp"
-            android:textStyle="bold"
-            android:padding="8dp"/>
-        <TextView
-            android:id="@+id/scan_mode_text_view"
-            android:layout_width="0dp"
-            android:layout_weight="1"
-            android:layout_height="wrap_content"
-            android:textSize="14dp"
-            android:textStyle="bold"
-            android:padding="8dp"/>
-    </LinearLayout>
-
-    <TextView
-        android:id="@+id/remote_device_text_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textSize="14dp"
-        android:textStyle="bold"
-        android:padding="8dp"/>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal">
-        <TextView
-            android:id="@+id/is_paired_text_view"
-            android:layout_width="0dp"
-            android:layout_weight="1"
-            android:layout_height="wrap_content"
-            android:textSize="14dp"
-            android:textStyle="bold"
-            android:padding="8dp"/>
-        <TextView
-            android:id="@+id/is_connected_text_view"
-            android:layout_width="0dp"
-            android:layout_weight="1"
-            android:layout_height="wrap_content"
-            android:textSize="14dp"
-            android:textStyle="bold"
-            android:padding="8dp"/>
-    </LinearLayout>
-
-    <Button
-        android:id="@+id/reset_button"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Reset"
-        android:onClick="onResetButtonClicked"/>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="8dp"
-        android:layout_marginBottom="8dp"
-        android:orientation="horizontal"
-        android:layout_gravity="center_vertical">
-
-      <Spinner
-          android:id="@+id/event_stream_spinner"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"/>
-
-      <Button
-          android:id="@+id/send_event_message_button"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:text="Send Event Message"
-          android:onClick="onSendEventStreamMessageButtonClicked"/>
-
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:padding="8dp"
-        android:orientation="horizontal"
-        android:layout_gravity="center_vertical">
-        <Switch
-            android:id="@+id/fail_switch"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="Force Fail" />
-        <Switch
-            android:id="@+id/app_launch_switch"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="Trigger app launch"
-            android:paddingLeft="8dp"/>
-    </LinearLayout>
-
-    <LinearLayout
-        android:id="@+id/adv_options"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="8dp"
-        android:layout_marginBottom="8dp"
-        android:orientation="horizontal"
-        android:layout_gravity="center_vertical">
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginRight="8dp"
-            android:textColor="@android:color/black"
-            android:text="adv options"/>
-
-        <Spinner
-            android:id="@+id/adv_option_spinner"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"/>
-
-    </LinearLayout>
-
-    <TextView
-        android:id="@+id/text_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:gravity="bottom"
-        android:scrollbars="vertical"/>
-</LinearLayout>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/user_input_dialog.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/user_input_dialog.xml
deleted file mode 100644
index 980b057..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/user_input_dialog.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:padding="16dp">
-
-  <EditText
-      android:id="@+id/userInputDialog"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:hint="@string/firmware_input_hint"
-      android:inputType="text" />
-
-</LinearLayout>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/menu/menu.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/menu/menu.xml
deleted file mode 100644
index f225522..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/menu/menu.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    tools:context=".MainActivity">
-
-  <item
-      android:id="@+id/sign_out_menu_item"
-      android:title="Sign out"/>
-  <item
-      android:id="@+id/reset_account_keys_menu_item"
-      android:title="Reset Account Keys"/>
-  <item
-      android:id="@+id/reset_device_name_menu_item"
-      android:title="Reset Device Name"/>
-  <item
-    android:id="@+id/set_firmware_version"
-    android:title="Set Firmware Version"/>
-  <item
-      android:id="@+id/set_simulator_capability"
-      android:title="Set Simulator Capability"/>
-  <item
-    android:id="@+id/use_new_gatt_characteristics_id"
-    android:checkable="true"
-    android:checked="false"
-    android:title="Use new GATT characteristics id"/>
-</menu>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/dimens.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/dimens.xml
deleted file mode 100644
index 47c8224..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/dimens.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<resources>
-    <!-- Default screen margins, per the Android Design guidelines. -->
-    <dimen name="activity_horizontal_margin">16dp</dimen>
-    <dimen name="activity_vertical_margin">16dp</dimen>
-</resources>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/strings.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/strings.xml
deleted file mode 100644
index 5123038..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/strings.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<resources>
-    <string name="app_name">Fast Pair Provider Simulator</string>
-    <string-array name="adv_options">
-        <item>0: No battery info</item>
-        <item>1: Show L(⬆) + R(⬆) + C(⬆)</item>
-        <item>2: Show L + R + C(unknown)</item>
-        <item>3: Show L(low 10) + R(low 9) + C(low 25)</item>
-        <item>4: Suppress battery w/o level changes</item>
-        <item>5: Suppress L(low 10) + R(11) + C</item>
-        <item>6: Suppress L(low ⬆) + R(low ⬆) + C(low 10)</item>
-        <item>7: Suppress L(low ⬆) + R(low ⬆) + C(low ⬆)</item>
-        <item>8: Show subsequent pairing notification</item>
-        <item>9: Suppress subsequent pairing notification</item>
-    </string-array>
-    <string-array name="event_stream_options">
-        <item>OHD event</item>
-        <item>Log event</item>
-        <item>Battery event</item>
-    </string-array>
-    <string name="firmware_dialog_title">Firmware version number</string>
-    <string name="firmware_input_hint">Type in version number</string>
-    <string name="passkey_dialog_title">Passkey needed</string>
-    <string name="passkey_input_hint">Type in passkey</string>
-    <!-- Passkey confirmation dialog title. [CHAR_LIMIT=NONE]-->
-    <string name="confirm_passkey">Confirm passkey</string>
-    <string name="model_id_progress_title">Get models from server</string>
-
-    <!-- Fast Pair Simulator: pair one device only. -->
-    <string name="fast_pair_simulator" translatable="false">Fast Pair Simulator</string>
-
-</resources>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/FutureCallbackWrapper.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/FutureCallbackWrapper.java
deleted file mode 100644
index 4db8560..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/FutureCallbackWrapper.java
+++ /dev/null
@@ -1,69 +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.nearby.fastpair.provider.simulator.app;
-
-import android.util.Log;
-
-import com.google.common.util.concurrent.FutureCallback;
-
-/** Wrapper for {@link FutureCallback} to prevent the memory linkage. */
-public abstract class FutureCallbackWrapper<T> implements FutureCallback<T> {
-    private static final String TAG = FutureCallback.class.getSimpleName();
-
-    public static FutureCallbackWrapper<Void> createRegisterCallback(MainActivity activity) {
-        String id = activity.mRemoteDeviceId;
-        return new FutureCallbackWrapper<Void>() {
-            @Override
-            public void onSuccess(Void result) {
-                Log.d(TAG, String.format("%s was registered", id));
-            }
-
-            @Override
-            public void onFailure(Throwable t) {
-                Log.w(TAG, String.format("Failed to register %s", id), t);
-            }
-        };
-    }
-
-    public static FutureCallbackWrapper<Void> createDefaultIOCallback(MainActivity activity) {
-        String id = activity.mRemoteDeviceId;
-        return new FutureCallbackWrapper<Void>() {
-            @Override
-            public void onSuccess(Void result) {
-            }
-
-            @Override
-            public void onFailure(Throwable t) {
-                Log.w(TAG, String.format("IO stream error on %s", id), t);
-            }
-        };
-    }
-
-    public static FutureCallbackWrapper<Void> createDestroyCallback() {
-        return new FutureCallbackWrapper<Void>() {
-            @Override
-            public void onSuccess(Void result) {
-                Log.d(TAG, "remote devices manager is destroyed");
-            }
-
-            @Override
-            public void onFailure(Throwable t) {
-                Log.w(TAG, "Failed to destroy remote devices manager", t);
-            }
-        };
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/MainActivity.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/MainActivity.java
deleted file mode 100644
index 75fafb0..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/MainActivity.java
+++ /dev/null
@@ -1,1040 +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.nearby.fastpair.provider.simulator.app;
-
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_STATE_BOND;
-import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_STATE_CONNECTION;
-import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_STATE_SCAN_MODE;
-
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.io.BaseEncoding.base64;
-
-import android.Manifest.permission;
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.AdvertiseSettings;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.graphics.Color;
-import android.nearby.fastpair.provider.EventStreamProtocol.EventGroup;
-import android.nearby.fastpair.provider.FastPairSimulator;
-import android.nearby.fastpair.provider.FastPairSimulator.BatteryValue;
-import android.nearby.fastpair.provider.FastPairSimulator.KeyInputCallback;
-import android.nearby.fastpair.provider.FastPairSimulator.PasskeyEventCallback;
-import android.nearby.fastpair.provider.bluetooth.BluetoothController;
-import android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event;
-import android.nearby.fastpair.provider.simulator.testing.RemoteDevice;
-import android.nearby.fastpair.provider.simulator.testing.RemoteDevicesManager;
-import android.nearby.fastpair.provider.simulator.testing.StreamIOHandlerFactory;
-import android.nearby.fastpair.provider.utils.Logger;
-import android.net.Uri;
-import android.os.Bundle;
-import android.text.InputType;
-import android.text.TextUtils;
-import android.text.method.ScrollingMovementMethod;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemSelectedListener;
-import android.widget.ArrayAdapter;
-import android.widget.Button;
-import android.widget.CompoundButton;
-import android.widget.EditText;
-import android.widget.Spinner;
-import android.widget.Switch;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.core.util.Consumer;
-
-import com.google.common.base.Ascii;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-import com.google.errorprone.annotations.FormatMethod;
-import com.google.protobuf.ByteString;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.LinkedHashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.Executors;
-
-import service.proto.Rpcs.AntiSpoofingKeyPair;
-import service.proto.Rpcs.Device;
-import service.proto.Rpcs.DeviceType;
-
-/**
- * Simulates a Fast Pair device (e.g. a headset).
- *
- * <p>See README in this directory, and {http://go/fast-pair-spec}.
- */
-@SuppressLint("SetTextI18n")
-public class MainActivity extends Activity {
-    public static final String TAG = "FastPairProviderSimulatorApp";
-    private final Logger mLogger = new Logger(TAG);
-
-    /** Device has a display and the ability to input Yes/No. */
-    private static final int IO_CAPABILITY_IO = 1;
-
-    /** Device only has a keyboard for entry but no display. */
-    private static final int IO_CAPABILITY_IN = 2;
-
-    /** Device has no Input or Output capability. */
-    private static final int IO_CAPABILITY_NONE = 3;
-
-    /** Device has a display and a full keyboard. */
-    private static final int IO_CAPABILITY_KBDISP = 4;
-
-    private static final String SHARED_PREFS_NAME =
-            "android.nearby.fastpair.provider.simulator.app";
-    private static final String EXTRA_MODEL_ID = "MODEL_ID";
-    private static final String EXTRA_BLUETOOTH_ADDRESS = "BLUETOOTH_ADDRESS";
-    private static final String EXTRA_TX_POWER_LEVEL = "TX_POWER_LEVEL";
-    private static final String EXTRA_FIRMWARE_VERSION = "FIRMWARE_VERSION";
-    private static final String EXTRA_SUPPORT_DYNAMIC_SIZE = "SUPPORT_DYNAMIC_SIZE";
-    private static final String EXTRA_USE_RANDOM_SALT_FOR_ACCOUNT_KEY_ROTATION =
-            "USE_RANDOM_SALT_FOR_ACCOUNT_KEY_ROTATION";
-    private static final String EXTRA_REMOTE_DEVICE_ID = "REMOTE_DEVICE_ID";
-    private static final String EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID =
-            "USE_NEW_GATT_CHARACTERISTICS_ID";
-    public static final String EXTRA_REMOVE_ALL_DEVICES_DURING_PAIRING =
-            "REMOVE_ALL_DEVICES_DURING_PAIRING";
-    private static final String KEY_ACCOUNT_NAME = "ACCOUNT_NAME";
-    private static final String[] PERMISSIONS =
-            new String[]{permission.BLUETOOTH, permission.BLUETOOTH_ADMIN, permission.GET_ACCOUNTS};
-    private static final int LIGHT_GREEN = 0xFFC8FFC8;
-    private static final String ANTI_SPOOFING_KEY_LABEL = "Anti-spoofing key";
-
-    private static final ImmutableMap<String, String> ANTI_SPOOFING_PRIVATE_KEY_MAP =
-            new ImmutableMap.Builder<String, String>()
-                    .put("361A2E", "/1rMqyJRGeOK6vkTNgM70xrytxdKg14mNQkITeusK20=")
-                    .put("00000D", "03/MAmUPTGNsN+2iA/1xASXoPplDh3Ha5/lk2JgEBx4=")
-                    .put("00000C", "Cbj9eCJrTdDgSYxLkqtfADQi86vIaMvxJsQ298sZYWE=")
-                    // BLE only devices
-                    .put("49426D", "I5QFOJW0WWFgKKZiwGchuseXsq/p9RN/aYtNsGEVGT0=")
-                    .put("01E5CE", "FbHt8STpHJDd4zFQFjimh4Zt7IU94U28MOEIXgUEeCw=")
-                    .put("8D13B9", "mv++LcJB1n0mbLNGWlXCv/8Gb6aldctrJC4/Ma/Q3Rg=")
-                    .put("9AB0F6", "9eKQNwJUr5vCg0c8rtOXkJcWTAsBmmvEKSgXIqAd50Q=")
-                    // Android Auto
-                    .put("8E083D", "hGQeREDKM/H1834zWMmTIe0Ap4Zl5igThgE62OtdcKA=")
-                    .buildOrThrow();
-
-    private static final Uri REMOTE_DEVICE_INPUT_STREAM_URI =
-            Uri.fromFile(new File("/data/local/nearby/tmp/read.pipe"));
-
-    private static final Uri REMOTE_DEVICE_OUTPUT_STREAM_URI =
-            Uri.fromFile(new File("/data/local/nearby/tmp/write.pipe"));
-
-    private static final String MODEL_ID_DEFAULT = "00000C";
-
-    private static final String MODEL_ID_APP_LAUNCH = "60EB56";
-
-    private static final int MODEL_ID_LENGTH = 6;
-
-    private BluetoothController mBluetoothController;
-    private final BluetoothController.EventListener mEventListener =
-            new BluetoothController.EventListener() {
-
-                @Override
-                public void onBondStateChanged(int bondState) {
-                    sendEventToRemoteDevice(
-                            Event.newBuilder().setCode(BLUETOOTH_STATE_BOND).setBondState(
-                                    bondState));
-                    updateStatusView();
-                }
-
-                @Override
-                public void onConnectionStateChanged(int connectionState) {
-                    sendEventToRemoteDevice(
-                            Event.newBuilder()
-                                    .setCode(BLUETOOTH_STATE_CONNECTION)
-                                    .setConnectionState(connectionState));
-                    updateStatusView();
-                }
-
-                @Override
-                public void onScanModeChange(int mode) {
-                    sendEventToRemoteDevice(
-                            Event.newBuilder().setCode(BLUETOOTH_STATE_SCAN_MODE).setScanMode(
-                                    mode));
-                    updateStatusView();
-                }
-
-                @Override
-                public void onA2DPSinkProfileConnected() {
-                    reset();
-                }
-            };
-
-    @Nullable
-    private FastPairSimulator mFastPairSimulator;
-    @Nullable
-    private AlertDialog mInputPasskeyDialog;
-    private Switch mFailSwitch;
-    private Switch mAppLaunchSwitch;
-    private Spinner mAdvOptionSpinner;
-    private Spinner mEventStreamSpinner;
-    private EventGroup mEventGroup;
-    private SharedPreferences mSharedPreferences;
-    private Spinner mModelIdSpinner;
-    private final RemoteDevicesManager mRemoteDevicesManager = new RemoteDevicesManager();
-    @Nullable
-    private RemoteDeviceListener mInputStreamListener;
-    @Nullable
-    String mRemoteDeviceId;
-    private final Map<String, Device> mModelsMap = new LinkedHashMap<>();
-    private boolean mRemoveAllDevicesDuringPairing = true;
-
-    void sendEventToRemoteDevice(Event.Builder eventBuilder) {
-        if (mRemoteDeviceId == null) {
-            return;
-        }
-
-        mLogger.log("Send data to output stream: %s", eventBuilder.getCode().getNumber());
-        mRemoteDevicesManager.writeDataToRemoteDevice(
-                mRemoteDeviceId,
-                eventBuilder.build().toByteString(),
-                FutureCallbackWrapper.createDefaultIOCallback(this));
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.activity_main);
-
-        mSharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
-
-        mRemoveAllDevicesDuringPairing =
-                getIntent().getBooleanExtra(EXTRA_REMOVE_ALL_DEVICES_DURING_PAIRING, true);
-
-        mFailSwitch = findViewById(R.id.fail_switch);
-        mFailSwitch.setOnCheckedChangeListener((CompoundButton buttonView, boolean isChecked) -> {
-            if (mFastPairSimulator != null) {
-                mFastPairSimulator.setShouldFailPairing(isChecked);
-            }
-        });
-
-        mAppLaunchSwitch = findViewById(R.id.app_launch_switch);
-        mAppLaunchSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> reset());
-
-        mAdvOptionSpinner = findViewById(R.id.adv_option_spinner);
-        mEventStreamSpinner = findViewById(R.id.event_stream_spinner);
-        ArrayAdapter<CharSequence> advOptionAdapter =
-                ArrayAdapter.createFromResource(
-                        this, R.array.adv_options, android.R.layout.simple_spinner_item);
-        ArrayAdapter<CharSequence> eventStreamAdapter =
-                ArrayAdapter.createFromResource(
-                        this, R.array.event_stream_options, android.R.layout.simple_spinner_item);
-        advOptionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
-        mAdvOptionSpinner.setAdapter(advOptionAdapter);
-        mEventStreamSpinner.setAdapter(eventStreamAdapter);
-        mAdvOptionSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
-            @Override
-            public void onItemSelected(AdapterView<?> adapterView, View view, int position,
-                    long id) {
-                startAdvertisingBatteryInformationBasedOnOption(position);
-            }
-
-            @Override
-            public void onNothingSelected(AdapterView<?> adapterView) {
-            }
-        });
-        mEventStreamSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
-            @Override
-            public void onItemSelected(AdapterView<?> parent, View view, int position,
-                    long id) {
-                switch (EventGroup.forNumber(position + 1)) {
-                    case BLUETOOTH:
-                        mEventGroup = EventGroup.BLUETOOTH;
-                        break;
-                    case LOGGING:
-                        mEventGroup = EventGroup.LOGGING;
-                        break;
-                    case DEVICE:
-                        mEventGroup = EventGroup.DEVICE;
-                        break;
-                    default:
-                        // fall through
-                }
-            }
-
-            @Override
-            public void onNothingSelected(AdapterView<?> parent) {
-            }
-        });
-        setupModelIdSpinner();
-        setupRemoteDevices();
-        if (checkPermissions(PERMISSIONS)) {
-            mBluetoothController = new BluetoothController(this, mEventListener);
-            mBluetoothController.registerBluetoothStateReceiver();
-            mBluetoothController.enableBluetooth();
-            mBluetoothController.connectA2DPSinkProfile();
-
-            if (mSharedPreferences.getString(KEY_ACCOUNT_NAME, "").isEmpty()) {
-                putFixedModelLocal();
-                resetModelIdSpinner();
-                reset();
-            }
-        } else {
-            requestPermissions(PERMISSIONS, 0 /* requestCode */);
-        }
-    }
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        getMenuInflater().inflate(R.menu.menu, menu);
-        menu.findItem(R.id.use_new_gatt_characteristics_id).setChecked(
-                getFromIntentOrPrefs(
-                        EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID, /* defaultValue= */ false));
-        return true;
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        if (item.getItemId() == R.id.sign_out_menu_item) {
-            recreate();
-            return true;
-        } else if (item.getItemId() == R.id.reset_account_keys_menu_item) {
-            resetAccountKeys();
-            return true;
-        } else if (item.getItemId() == R.id.reset_device_name_menu_item) {
-            resetDeviceName();
-            return true;
-        } else if (item.getItemId() == R.id.set_firmware_version) {
-            setFirmware();
-            return true;
-        } else if (item.getItemId() == R.id.set_simulator_capability) {
-            setSimulatorCapability();
-            return true;
-        } else if (item.getItemId() == R.id.use_new_gatt_characteristics_id) {
-            if (!item.isChecked()) {
-                item.setChecked(true);
-                mSharedPreferences.edit()
-                        .putBoolean(EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID, true).apply();
-            } else {
-                item.setChecked(false);
-                mSharedPreferences.edit()
-                        .putBoolean(EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID, false).apply();
-            }
-            reset();
-            return true;
-        }
-        return super.onOptionsItemSelected(item);
-    }
-
-    private void setFirmware() {
-        View firmwareInputView =
-                LayoutInflater.from(getApplicationContext()).inflate(R.layout.user_input_dialog,
-                        null);
-        EditText userInputDialogEditText = firmwareInputView.findViewById(R.id.userInputDialog);
-        new AlertDialog.Builder(MainActivity.this)
-                .setView(firmwareInputView)
-                .setCancelable(false)
-                .setPositiveButton(android.R.string.ok, (dialogBox, id) -> {
-                    String input = userInputDialogEditText.getText().toString();
-                    mSharedPreferences.edit().putString(EXTRA_FIRMWARE_VERSION,
-                            input).apply();
-                    reset();
-                })
-                .setNegativeButton(android.R.string.cancel, null)
-                .setTitle(R.string.firmware_dialog_title)
-                .show();
-    }
-
-    private void setSimulatorCapability() {
-        String[] capabilityKeys = new String[]{EXTRA_SUPPORT_DYNAMIC_SIZE};
-        String[] capabilityNames = new String[]{"Dynamic Buffer Size"};
-        // Default values.
-        boolean[] capabilitySelected = new boolean[]{false};
-        // Get from preferences if exist.
-        for (int i = 0; i < capabilityKeys.length; i++) {
-            capabilitySelected[i] =
-                    mSharedPreferences.getBoolean(capabilityKeys[i], capabilitySelected[i]);
-        }
-
-        new AlertDialog.Builder(MainActivity.this)
-                .setMultiChoiceItems(
-                        capabilityNames,
-                        capabilitySelected,
-                        (dialog, which, isChecked) -> capabilitySelected[which] = isChecked)
-                .setCancelable(false)
-                .setPositiveButton(
-                        android.R.string.ok,
-                        (dialogBox, id) -> {
-                            for (int i = 0; i < capabilityKeys.length; i++) {
-                                mSharedPreferences
-                                        .edit()
-                                        .putBoolean(capabilityKeys[i], capabilitySelected[i])
-                                        .apply();
-                            }
-                            setCapabilityToSimulator();
-                        })
-                .setNegativeButton(android.R.string.cancel, null)
-                .setTitle("Simulator Capability")
-                .show();
-    }
-
-    private void setCapabilityToSimulator() {
-        if (mFastPairSimulator != null) {
-            mFastPairSimulator.setDynamicBufferSize(
-                    getFromIntentOrPrefs(EXTRA_SUPPORT_DYNAMIC_SIZE, false));
-        }
-    }
-
-    private static String getModelIdString(long id) {
-        String result = Ascii.toUpperCase(Long.toHexString(id));
-        while (result.length() < MODEL_ID_LENGTH) {
-            result = "0" + result;
-        }
-        return result;
-    }
-
-    private void putFixedModelLocal() {
-        mModelsMap.put(
-                "00000C",
-                Device.newBuilder()
-                        .setId(12)
-                        .setAntiSpoofingKeyPair(AntiSpoofingKeyPair.newBuilder().build())
-                        .setDeviceType(DeviceType.HEADPHONES)
-                        .build());
-    }
-
-    private void setupModelIdSpinner() {
-        mModelIdSpinner = findViewById(R.id.model_id_spinner);
-
-        ArrayAdapter<String> modelIdAdapter =
-                new ArrayAdapter<>(this, android.R.layout.simple_spinner_item);
-        modelIdAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
-        mModelIdSpinner.setAdapter(modelIdAdapter);
-        resetModelIdSpinner();
-        mModelIdSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
-            @Override
-            public void onItemSelected(AdapterView<?> parent, View view, int position,
-                    long id) {
-                setModelId(mModelsMap.keySet().toArray(new String[0])[position]);
-            }
-
-            @Override
-            public void onNothingSelected(AdapterView<?> adapterView) {
-            }
-        });
-    }
-
-    private void setupRemoteDevices() {
-        if (Strings.isNullOrEmpty(getIntent().getStringExtra(EXTRA_REMOTE_DEVICE_ID))) {
-            mLogger.log("Can't get remote device id");
-            return;
-        }
-        mRemoteDeviceId = getIntent().getStringExtra(EXTRA_REMOTE_DEVICE_ID);
-        mInputStreamListener = new RemoteDeviceListener(this);
-
-        try {
-            mRemoteDevicesManager.registerRemoteDevice(
-                    mRemoteDeviceId,
-                    new RemoteDevice(
-                            mRemoteDeviceId,
-                            StreamIOHandlerFactory.createStreamIOHandler(
-                                    StreamIOHandlerFactory.Type.LOCAL_FILE,
-                                    REMOTE_DEVICE_INPUT_STREAM_URI,
-                                    REMOTE_DEVICE_OUTPUT_STREAM_URI),
-                            mInputStreamListener));
-        } catch (IOException e) {
-            mLogger.log(e, "Failed to create stream IO handler");
-        }
-    }
-
-    @SuppressWarnings({"unchecked", "rawtypes"})
-    @UiThread
-    private void resetModelIdSpinner() {
-        ArrayAdapter adapter = (ArrayAdapter) mModelIdSpinner.getAdapter();
-        if (adapter == null) {
-            return;
-        }
-
-        adapter.clear();
-        if (!mModelsMap.isEmpty()) {
-            for (String modelId : mModelsMap.keySet()) {
-                adapter.add(modelId + "-" + mModelsMap.get(modelId).getName());
-            }
-            mModelIdSpinner.setEnabled(true);
-            int newPos = getPositionFromModelId(getModelId());
-            if (newPos < 0) {
-                String newModelId = mModelsMap.keySet().iterator().next();
-                Toast.makeText(this,
-                        "Can't find Model ID " + getModelId() + " from console, reset it to "
-                                + newModelId, Toast.LENGTH_SHORT).show();
-                setModelId(newModelId);
-                newPos = 0;
-            }
-            mModelIdSpinner.setSelection(newPos, /* animate= */ false);
-        } else {
-            mModelIdSpinner.setEnabled(false);
-        }
-    }
-
-    private String getModelId() {
-        return getFromIntentOrPrefs(EXTRA_MODEL_ID, MODEL_ID_DEFAULT).toUpperCase(Locale.US);
-    }
-
-    private boolean setModelId(String modelId) {
-        String validModelId = getValidModelId(modelId);
-        if (TextUtils.isEmpty(validModelId)) {
-            mLogger.log("Can't do setModelId because inputted modelId is invalid!");
-            return false;
-        }
-
-        if (getModelId().equals(validModelId)) {
-            return false;
-        }
-        mSharedPreferences.edit().putString(EXTRA_MODEL_ID, validModelId).apply();
-        reset();
-        return true;
-    }
-
-    @Nullable
-    private static String getValidModelId(String modelId) {
-        if (TextUtils.isEmpty(modelId) || modelId.length() < MODEL_ID_LENGTH) {
-            return null;
-        }
-
-        return modelId.substring(0, MODEL_ID_LENGTH).toUpperCase(Locale.US);
-    }
-
-    private int getPositionFromModelId(String modelId) {
-        int i = 0;
-        for (String id : mModelsMap.keySet()) {
-            if (id.equals(modelId)) {
-                return i;
-            }
-            i++;
-        }
-        return -1;
-    }
-
-    private void resetAccountKeys() {
-        if (mFastPairSimulator != null) {
-            mFastPairSimulator.resetAccountKeys();
-            mFastPairSimulator.startAdvertising();
-        }
-    }
-
-    private void resetDeviceName() {
-        if (mFastPairSimulator != null) {
-            mFastPairSimulator.resetDeviceName();
-        }
-    }
-
-    /** Called via activity_main.xml */
-    public void onResetButtonClicked(View view) {
-        reset();
-    }
-
-    /** Called via activity_main.xml */
-    public void onSendEventStreamMessageButtonClicked(View view) {
-        if (mFastPairSimulator != null) {
-            mFastPairSimulator.sendEventStreamMessageToRfcommDevices(mEventGroup);
-        }
-    }
-
-    void reset() {
-        Button resetButton = findViewById(R.id.reset_button);
-        if (mModelsMap.isEmpty() || !resetButton.isEnabled()) {
-            return;
-        }
-        resetButton.setText("Resetting...");
-        resetButton.setEnabled(false);
-        mModelIdSpinner.setEnabled(false);
-        mAppLaunchSwitch.setEnabled(false);
-
-        if (mFastPairSimulator != null) {
-            mFastPairSimulator.stopAdvertising();
-
-            if (mBluetoothController.getRemoteDevice() != null) {
-                if (mRemoveAllDevicesDuringPairing) {
-                    mFastPairSimulator.removeBond(mBluetoothController.getRemoteDevice());
-                }
-                mBluetoothController.clearRemoteDevice();
-            }
-            // To be safe, also unpair from all phones (this covers the case where you kill +
-            // relaunch the
-            // simulator while paired).
-            if (mRemoveAllDevicesDuringPairing) {
-                mFastPairSimulator.disconnectAllBondedDevices();
-            }
-            // Sometimes a device will still be connected even though it's not bonded. :( Clear
-            // that too.
-            BluetoothProfile profileProxy = mBluetoothController.getA2DPSinkProfileProxy();
-            for (BluetoothDevice device : profileProxy.getConnectedDevices()) {
-                mFastPairSimulator.disconnect(profileProxy, device);
-            }
-        }
-        updateStatusView();
-
-        if (mFastPairSimulator != null) {
-            mFastPairSimulator.destroy();
-        }
-        TextView textView = (TextView) findViewById(R.id.text_view);
-        textView.setText("");
-        textView.setMovementMethod(new ScrollingMovementMethod());
-
-        String modelId = getModelId();
-
-        String txPower = getFromIntentOrPrefs(EXTRA_TX_POWER_LEVEL, "HIGH");
-        updateStringStatusView(R.id.tx_power_text_view, "TxPower", txPower);
-
-        String bluetoothAddress = getFromIntentOrPrefs(EXTRA_BLUETOOTH_ADDRESS, "");
-
-        String firmwareVersion = getFromIntentOrPrefs(EXTRA_FIRMWARE_VERSION, "1.1");
-        try {
-            Preconditions.checkArgument(base16().decode(bluetoothAddress).length == 6);
-        } catch (IllegalArgumentException e) {
-            mLogger.log("Invalid BLUETOOTH_ADDRESS extra (%s), using default.", bluetoothAddress);
-            bluetoothAddress = null;
-        }
-        final String finalBluetoothAddress = bluetoothAddress;
-
-        updateStringStatusView(
-                R.id.anti_spoofing_private_key_text_view, ANTI_SPOOFING_KEY_LABEL, "Loading...");
-
-        boolean useRandomSaltForAccountKeyRotation =
-                getFromIntentOrPrefs(EXTRA_USE_RANDOM_SALT_FOR_ACCOUNT_KEY_ROTATION, false);
-
-        Executors.newSingleThreadExecutor().execute(() -> {
-            // Fetch the anti-spoofing key corresponding to this model ID (if it
-            // exists).
-            // The account must have Project Viewer permission for the project
-            // that owns
-            // the model ID (normally discoverer-test or discoverer-devices).
-            byte[] antiSpoofingKey = getAntiSpoofingKey(modelId);
-            String antiSpoofingKeyString;
-            Device device = mModelsMap.get(modelId);
-            if (antiSpoofingKey != null) {
-                antiSpoofingKeyString = base64().encode(antiSpoofingKey);
-            } else {
-                if (mSharedPreferences.getString(KEY_ACCOUNT_NAME, "").isEmpty()) {
-                    antiSpoofingKeyString = "Can't fetch, no account";
-                } else {
-                    if (device == null) {
-                        antiSpoofingKeyString = String.format(Locale.US,
-                                "Can't find model %s from console", modelId);
-                    } else if (!device.hasAntiSpoofingKeyPair()) {
-                        antiSpoofingKeyString = String.format(Locale.US,
-                                "Can't find AntiSpoofingKeyPair for model %s", modelId);
-                    } else if (device.getAntiSpoofingKeyPair().getPrivateKey().isEmpty()) {
-                        antiSpoofingKeyString = String.format(Locale.US,
-                                "Can't find privateKey for model %s", modelId);
-                    } else {
-                        antiSpoofingKeyString = "Unknown error";
-                    }
-                }
-            }
-
-            int desiredIoCapability = getIoCapabilityFromModelId(modelId);
-
-            mBluetoothController.setIoCapability(desiredIoCapability);
-
-            runOnUiThread(() -> {
-                updateStringStatusView(
-                        R.id.anti_spoofing_private_key_text_view,
-                        ANTI_SPOOFING_KEY_LABEL,
-                        antiSpoofingKeyString);
-                FastPairSimulator.Options option = FastPairSimulator.Options.builder(modelId)
-                        .setAdvertisingModelId(
-                                mAppLaunchSwitch.isChecked() ? MODEL_ID_APP_LAUNCH : modelId)
-                        .setBluetoothAddress(finalBluetoothAddress)
-                        .setTxPowerLevel(toTxPowerLevel(txPower))
-                        .setAdvertisingChangedCallback(isAdvertising -> updateStatusView())
-                        .setAntiSpoofingPrivateKey(antiSpoofingKey)
-                        .setUseRandomSaltForAccountKeyRotation(useRandomSaltForAccountKeyRotation)
-                        .setDataOnlyConnection(device != null && device.getDataOnlyConnection())
-                        .setShowsPasskeyConfirmation(
-                                device.getDeviceType().equals(DeviceType.ANDROID_AUTO))
-                        .setRemoveAllDevicesDuringPairing(mRemoveAllDevicesDuringPairing)
-                        .build();
-                Logger textViewLogger = new Logger(FastPairSimulator.TAG) {
-
-                    @FormatMethod
-                    public void log(@Nullable Throwable exception, String message,
-                            Object... objects) {
-                        super.log(exception, message, objects);
-
-                        String exceptionMessage = (exception == null) ? ""
-                                : " - " + exception.getMessage();
-                        final String finalMessage =
-                                String.format(message, objects) + exceptionMessage;
-
-                        textView.post(() -> {
-                            String newText =
-                                    textView.getText() + "\n\n" + finalMessage;
-                            textView.setText(newText);
-                        });
-                    }
-                };
-                mFastPairSimulator =
-                        new FastPairSimulator(this, option, textViewLogger);
-                mFastPairSimulator.setFirmwareVersion(firmwareVersion);
-                mFailSwitch.setChecked(
-                        mFastPairSimulator.getShouldFailPairing());
-                mAdvOptionSpinner.setSelection(0);
-                setCapabilityToSimulator();
-
-                updateStringStatusView(R.id.bluetooth_address_text_view,
-                        "Bluetooth address",
-                        mFastPairSimulator.getBluetoothAddress());
-
-                updateStringStatusView(R.id.device_name_text_view,
-                        "Device name",
-                        mFastPairSimulator.getDeviceName());
-
-                resetButton.setText("Reset");
-                resetButton.setEnabled(true);
-                mModelIdSpinner.setEnabled(true);
-                mAppLaunchSwitch.setEnabled(true);
-                mFastPairSimulator.setDeviceNameCallback(deviceName ->
-                        updateStringStatusView(
-                                R.id.device_name_text_view,
-                                "Device name", deviceName));
-
-                if (desiredIoCapability == IO_CAPABILITY_IN
-                        || device.getDeviceType().equals(DeviceType.ANDROID_AUTO)) {
-                    mFastPairSimulator.setPasskeyEventCallback(mPasskeyEventCallback);
-                }
-                if (mInputStreamListener != null) {
-                    mInputStreamListener.setFastPairSimulator(mFastPairSimulator);
-                }
-            });
-        });
-    }
-
-    private int getIoCapabilityFromModelId(String modelId) {
-        Device device = mModelsMap.get(modelId);
-        if (device == null) {
-            return IO_CAPABILITY_NONE;
-        } else {
-            if (getAntiSpoofingKey(modelId) == null) {
-                return IO_CAPABILITY_NONE;
-            } else {
-                switch (device.getDeviceType()) {
-                    case INPUT_DEVICE:
-                        return IO_CAPABILITY_IN;
-
-                    case DEVICE_TYPE_UNSPECIFIED:
-                        return IO_CAPABILITY_NONE;
-
-                    // Treats wearable to IO_CAPABILITY_KBDISP for simulator because there seems
-                    // no suitable
-                    // type.
-                    case WEARABLE:
-                        return IO_CAPABILITY_KBDISP;
-
-                    default:
-                        return IO_CAPABILITY_IO;
-                }
-            }
-        }
-    }
-
-    @Nullable
-    ByteString getAccontKey() {
-        if (mFastPairSimulator == null) {
-            return null;
-        }
-        return mFastPairSimulator.getAccountKey();
-    }
-
-    @Nullable
-    private byte[] getAntiSpoofingKey(String modelId) {
-        Device device = mModelsMap.get(modelId);
-        if (device != null
-                && device.hasAntiSpoofingKeyPair()
-                && !device.getAntiSpoofingKeyPair().getPrivateKey().isEmpty()) {
-            return base64().decode(device.getAntiSpoofingKeyPair().getPrivateKey().toStringUtf8());
-        } else if (ANTI_SPOOFING_PRIVATE_KEY_MAP.containsKey(modelId)) {
-            return base64().decode(ANTI_SPOOFING_PRIVATE_KEY_MAP.get(modelId));
-        } else {
-            return null;
-        }
-    }
-
-    private final PasskeyEventCallback mPasskeyEventCallback = new PasskeyEventCallback() {
-        @Override
-        public void onPasskeyRequested(KeyInputCallback keyInputCallback) {
-            showInputPasskeyDialog(keyInputCallback);
-        }
-
-        @Override
-        public void onPasskeyConfirmation(int passkey, Consumer<Boolean> isConfirmed) {
-            showConfirmPasskeyDialog(passkey, isConfirmed);
-        }
-
-        @Override
-        public void onRemotePasskeyReceived(int passkey) {
-            if (mInputPasskeyDialog == null) {
-                return;
-            }
-
-            EditText userInputDialogEditText = mInputPasskeyDialog.findViewById(
-                    R.id.userInputDialog);
-            if (userInputDialogEditText == null) {
-                return;
-            }
-
-            userInputDialogEditText.setText(String.format("%d", passkey));
-        }
-    };
-
-    private void showInputPasskeyDialog(KeyInputCallback keyInputCallback) {
-        if (mInputPasskeyDialog == null) {
-            View userInputView =
-                    LayoutInflater.from(getApplicationContext()).inflate(R.layout.user_input_dialog,
-                            null);
-            EditText userInputDialogEditText = userInputView.findViewById(R.id.userInputDialog);
-            userInputDialogEditText.setHint(R.string.passkey_input_hint);
-            userInputDialogEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
-            mInputPasskeyDialog = new AlertDialog.Builder(MainActivity.this)
-                    .setView(userInputView)
-                    .setCancelable(false)
-                    .setPositiveButton(
-                            android.R.string.ok,
-                            (DialogInterface dialogBox, int id) -> {
-                                String input = userInputDialogEditText.getText().toString();
-                                keyInputCallback.onKeyInput(Integer.parseInt(input));
-                            })
-                    .setNegativeButton(android.R.string.cancel, /* listener= */ null)
-                    .setTitle(R.string.passkey_dialog_title)
-                    .create();
-        }
-        if (!mInputPasskeyDialog.isShowing()) {
-            mInputPasskeyDialog.show();
-        }
-    }
-
-    private void showConfirmPasskeyDialog(int passkey, Consumer<Boolean> isConfirmed) {
-        runOnUiThread(() -> new AlertDialog.Builder(MainActivity.this)
-                .setCancelable(false)
-                .setTitle(R.string.confirm_passkey)
-                .setMessage(String.valueOf(passkey))
-                .setPositiveButton(android.R.string.ok,
-                        (d, w) -> isConfirmed.accept(true))
-                .setNegativeButton(android.R.string.cancel,
-                        (d, w) -> isConfirmed.accept(false))
-                .create()
-                .show());
-    }
-
-    @UiThread
-    private void updateStringStatusView(int id, String name, String value) {
-        ((TextView) findViewById(id)).setText(name + ": " + value);
-    }
-
-    @UiThread
-    private void updateStatusView() {
-        TextView remoteDeviceTextView = (TextView) findViewById(R.id.remote_device_text_view);
-        remoteDeviceTextView.setBackgroundColor(
-                mBluetoothController.getRemoteDevice() != null ? LIGHT_GREEN : Color.LTGRAY);
-        String remoteDeviceString = mBluetoothController.getRemoteDeviceAsString();
-        remoteDeviceTextView.setText("Remote device: " + remoteDeviceString);
-
-        updateBooleanStatusView(
-                R.id.is_advertising_text_view,
-                "BLE advertising",
-                mFastPairSimulator != null && mFastPairSimulator.isAdvertising());
-
-        updateStringStatusView(
-                R.id.scan_mode_text_view,
-                "Mode",
-                FastPairSimulator.scanModeToString(mBluetoothController.getScanMode()));
-
-        boolean isPaired = mBluetoothController.isPaired();
-        updateBooleanStatusView(R.id.is_paired_text_view, "Paired", isPaired);
-
-        updateBooleanStatusView(
-                R.id.is_connected_text_view, "Connected", mBluetoothController.isConnected());
-    }
-
-    @UiThread
-    private void updateBooleanStatusView(int id, String name, boolean value) {
-        TextView view = (TextView) findViewById(id);
-        view.setBackgroundColor(value ? LIGHT_GREEN : Color.LTGRAY);
-        view.setText(name + ": " + (value ? "Yes" : "No"));
-    }
-
-    private String getFromIntentOrPrefs(String key, String defaultValue) {
-        Bundle extras = getIntent().getExtras();
-        extras = extras != null ? extras : new Bundle();
-        SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
-        String value = extras.getString(key, prefs.getString(key, defaultValue));
-        if (value == null) {
-            prefs.edit().remove(key).apply();
-        } else {
-            prefs.edit().putString(key, value).apply();
-        }
-        return value;
-    }
-
-    private boolean getFromIntentOrPrefs(String key, boolean defaultValue) {
-        Bundle extras = getIntent().getExtras();
-        extras = extras != null ? extras : new Bundle();
-        SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
-        boolean value = extras.getBoolean(key, prefs.getBoolean(key, defaultValue));
-        prefs.edit().putBoolean(key, value).apply();
-        return value;
-    }
-
-    private static int toTxPowerLevel(String txPowerLevelString) {
-        switch (txPowerLevelString.toUpperCase()) {
-            case "3":
-            case "HIGH":
-                return AdvertiseSettings.ADVERTISE_TX_POWER_HIGH;
-            case "2":
-            case "MEDIUM":
-                return AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM;
-            case "1":
-            case "LOW":
-                return AdvertiseSettings.ADVERTISE_TX_POWER_LOW;
-            case "0":
-            case "ULTRA_LOW":
-                return AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW;
-            default:
-                throw new IllegalArgumentException(
-                        "Unexpected TxPower="
-                                + txPowerLevelString
-                                + ", please provide HIGH, MEDIUM, LOW, or ULTRA_LOW.");
-        }
-    }
-
-    private boolean checkPermissions(String[] permissions) {
-        for (String permission : permissions) {
-            if (checkSelfPermission(permission) != PERMISSION_GRANTED) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    @Override
-    protected void onDestroy() {
-        mRemoteDevicesManager.destroy();
-
-        if (mFastPairSimulator != null) {
-            mFastPairSimulator.destroy();
-            mBluetoothController.unregisterBluetoothStateReceiver();
-        }
-
-        // Recover the IO capability.
-        mBluetoothController.setIoCapability(IO_CAPABILITY_IO);
-
-        super.onDestroy();
-    }
-
-    @Override
-    public void onRequestPermissionsResult(
-            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-        // Relaunch this activity.
-        recreate();
-    }
-
-    void startAdvertisingBatteryInformationBasedOnOption(int option) {
-        if (mFastPairSimulator == null) {
-            return;
-        }
-
-        // Option 0 is "No battery info", it means simulator will not pack battery information when
-        // advertising. For the others with battery info, since we are simulating the Presto's
-        // behavior,
-        // there will always be three battery values.
-        switch (option) {
-            case 0:
-                // Option "0: No battery info"
-                mFastPairSimulator.clearBatteryValues();
-                break;
-            case 1:
-                // Option "1: Show L(⬆) + R(⬆) + C(⬆)"
-                mFastPairSimulator.setSuppressBatteryNotification(false);
-                mFastPairSimulator.setBatteryValues(new BatteryValue(true, 60),
-                        new BatteryValue(true, 61),
-                        new BatteryValue(true, 62));
-                break;
-            case 2:
-                // Option "2: Show L + R + C(unknown)"
-                mFastPairSimulator.setSuppressBatteryNotification(false);
-                mFastPairSimulator.setBatteryValues(new BatteryValue(false, 70),
-                        new BatteryValue(false, 71),
-                        new BatteryValue(false, -1));
-                break;
-            case 3:
-                // Option "3: Show L(low 10) + R(low 9) + C(low 25)"
-                mFastPairSimulator.setSuppressBatteryNotification(false);
-                mFastPairSimulator.setBatteryValues(new BatteryValue(false, 10),
-                        new BatteryValue(false, 9),
-                        new BatteryValue(false, 25));
-                break;
-            case 4:
-                // Option "4: Suppress battery w/o level changes"
-                // Just change the suppress bit and keep the battery values the same as before.
-                mFastPairSimulator.setSuppressBatteryNotification(true);
-                break;
-            case 5:
-                // Option "5: Suppress L(low 10) + R(11) + C"
-                mFastPairSimulator.setSuppressBatteryNotification(true);
-                mFastPairSimulator.setBatteryValues(new BatteryValue(false, 10),
-                        new BatteryValue(false, 11),
-                        new BatteryValue(false, 82));
-                break;
-            case 6:
-                // Option "6: Suppress L(low ⬆) + R(low ⬆) + C(low 10)"
-                mFastPairSimulator.setSuppressBatteryNotification(true);
-                mFastPairSimulator.setBatteryValues(new BatteryValue(true, 10),
-                        new BatteryValue(true, 9),
-                        new BatteryValue(false, 10));
-                break;
-            case 7:
-                // Option "7: Suppress L(low ⬆) + R(low ⬆) + C(low ⬆)"
-                mFastPairSimulator.setSuppressBatteryNotification(true);
-                mFastPairSimulator.setBatteryValues(new BatteryValue(true, 10),
-                        new BatteryValue(true, 9),
-                        new BatteryValue(true, 25));
-                break;
-            case 8:
-                // Option "8: Show subsequent pairing notification"
-                mFastPairSimulator.setSuppressSubsequentPairingNotification(false);
-                break;
-            case 9:
-                // Option "9: Suppress subsequent pairing notification"
-                mFastPairSimulator.setSuppressSubsequentPairingNotification(true);
-                break;
-            default:
-                // Unknown option, do nothing.
-                return;
-        }
-
-        mFastPairSimulator.startAdvertising();
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/RemoteDeviceListener.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/RemoteDeviceListener.java
deleted file mode 100644
index fac8cb5..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/RemoteDeviceListener.java
+++ /dev/null
@@ -1,164 +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.nearby.fastpair.provider.simulator.app;
-
-import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.ACCOUNT_KEY;
-import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.ACKNOWLEDGE;
-import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_ADDRESS_BLE;
-import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_ADDRESS_PUBLIC;
-
-import android.nearby.fastpair.provider.FastPairSimulator;
-import android.nearby.fastpair.provider.FastPairSimulator.BatteryValue;
-import android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Command;
-import android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Command.BatteryInfo;
-import android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event;
-import android.nearby.fastpair.provider.simulator.testing.InputStreamListener;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-
-/** Listener for input stream of the remote device. */
-public class RemoteDeviceListener implements InputStreamListener {
-    private static final String TAG = RemoteDeviceListener.class.getSimpleName();
-
-    private final MainActivity mMainActivity;
-    @Nullable
-    private FastPairSimulator mFastPairSimulator;
-
-    public RemoteDeviceListener(MainActivity mainActivity) {
-        this.mMainActivity = mainActivity;
-    }
-
-    @Override
-    public void onInputData(ByteString byteString) {
-        Command command;
-        try {
-            command = Command.parseFrom(byteString);
-        } catch (InvalidProtocolBufferException e) {
-            Log.w(TAG, String.format("%s input data is not a Command",
-                    mMainActivity.mRemoteDeviceId), e);
-            return;
-        }
-
-        mMainActivity.runOnUiThread(() -> {
-            Log.d(TAG, String.format("%s new command %s",
-                    mMainActivity.mRemoteDeviceId, command.getCode()));
-            switch (command.getCode()) {
-                case POLLING:
-                    mMainActivity.sendEventToRemoteDevice(
-                            Event.newBuilder().setCode(ACKNOWLEDGE));
-                    break;
-                case RESET:
-                    mMainActivity.reset();
-                    break;
-                case SHOW_BATTERY:
-                    onShowBattery(command.getBatteryInfo());
-                    break;
-                case HIDE_BATTERY:
-                    onHideBattery();
-                    break;
-                case REQUEST_BLUETOOTH_ADDRESS_BLE:
-                    onRequestBleAddress();
-                    break;
-                case REQUEST_BLUETOOTH_ADDRESS_PUBLIC:
-                    onRequestPublicAddress();
-                    break;
-                case REQUEST_ACCOUNT_KEY:
-                    ByteString accountKey = mMainActivity.getAccontKey();
-                    if (accountKey == null) {
-                        break;
-                    }
-                    mMainActivity.sendEventToRemoteDevice(
-                            Event.newBuilder().setCode(ACCOUNT_KEY)
-                                    .setAccountKey(accountKey));
-                    break;
-            }
-        });
-    }
-
-    @Override
-    public void onClose() {
-        Log.d(TAG, String.format("%s input stream is closed", mMainActivity.mRemoteDeviceId));
-    }
-
-    void setFastPairSimulator(FastPairSimulator fastPairSimulator) {
-        this.mFastPairSimulator = fastPairSimulator;
-    }
-
-    private void onShowBattery(@Nullable BatteryInfo batteryInfo) {
-        if (mFastPairSimulator == null || batteryInfo == null) {
-            Log.w(TAG, "skip showing battery");
-            return;
-        }
-
-        if (batteryInfo.getBatteryValuesCount() != 3) {
-            Log.w(TAG, String.format("skip showing battery: count is not valid %d",
-                    batteryInfo.getBatteryValuesCount()));
-            return;
-        }
-
-        Log.d(TAG, String.format("Show battery %s", batteryInfo));
-
-        if (batteryInfo.hasSuppressNotification()) {
-            mFastPairSimulator.setSuppressBatteryNotification(
-                    batteryInfo.getSuppressNotification());
-        }
-        mFastPairSimulator.setBatteryValues(
-                convertFrom(batteryInfo.getBatteryValues(0)),
-                convertFrom(batteryInfo.getBatteryValues(1)),
-                convertFrom(batteryInfo.getBatteryValues(2)));
-        mFastPairSimulator.startAdvertising();
-    }
-
-    private void onHideBattery() {
-        if (mFastPairSimulator == null) {
-            return;
-        }
-
-        mFastPairSimulator.clearBatteryValues();
-        mFastPairSimulator.startAdvertising();
-    }
-
-    private void onRequestBleAddress() {
-        if (mFastPairSimulator == null) {
-            return;
-        }
-
-        mMainActivity.sendEventToRemoteDevice(
-                Event.newBuilder()
-                        .setCode(BLUETOOTH_ADDRESS_BLE)
-                        .setBleAddress(mFastPairSimulator.getBleAddress()));
-    }
-
-    private void onRequestPublicAddress() {
-        if (mFastPairSimulator == null) {
-            return;
-        }
-
-        mMainActivity.sendEventToRemoteDevice(
-                Event.newBuilder()
-                        .setCode(BLUETOOTH_ADDRESS_PUBLIC)
-                        .setPublicAddress(mFastPairSimulator.getBluetoothAddress()));
-    }
-
-    private static BatteryValue convertFrom(BatteryInfo.BatteryValue batteryValue) {
-        return new BatteryValue(batteryValue.getCharging(), batteryValue.getLevel());
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/InputStreamListener.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/InputStreamListener.java
deleted file mode 100644
index b29225a..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/InputStreamListener.java
+++ /dev/null
@@ -1,29 +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.nearby.fastpair.provider.simulator.testing;
-
-import com.google.protobuf.ByteString;
-
-/** Listener for input stream. */
-public interface InputStreamListener {
-
-    /** Called when new data {@code byteString} is read from the input stream. */
-    void onInputData(ByteString byteString);
-
-    /** Called when the input stream is closed. */
-    void onClose();
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/LocalFileStreamIOHandler.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/LocalFileStreamIOHandler.java
deleted file mode 100644
index cf8b022..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/LocalFileStreamIOHandler.java
+++ /dev/null
@@ -1,119 +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.nearby.fastpair.provider.simulator.testing;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.io.BaseEncoding.base16;
-
-import android.net.Uri;
-
-import androidx.annotation.Nullable;
-
-import com.google.protobuf.ByteString;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-
-/**
- * Opens the {@code inputUri} and {@code outputUri} as local files and provides reading/writing
- * data operations.
- *
- * To support bluetooth testing on real devices, the named pipes are created as local files and the
- * pipe data are transferred via usb cable, then (1) the peripheral device writes {@code Event} to
- * the output stream and reads {@code Command} from the input stream (2) the central devices write
- * {@code Command} to the output stream and read {@code Event} from the input stream.
- *
- * The {@code Event} and {@code Command} are special protocols which are defined at
- * simulator_stream_protocol.proto.
- */
-public class LocalFileStreamIOHandler implements StreamIOHandler {
-
-    private static final int MAX_IO_DATA_LENGTH_BYTE = 65535;
-
-    private final String mInputPath;
-    private final String mOutputPath;
-
-    LocalFileStreamIOHandler(Uri inputUri, Uri outputUri) throws IOException {
-        if (!isFileExists(inputUri.getPath())) {
-            throw new FileNotFoundException("Input path is not exists.");
-        }
-        if (!isFileExists(outputUri.getPath())) {
-            throw new FileNotFoundException("Output path is not exists.");
-        }
-
-        this.mInputPath = inputUri.getPath();
-        this.mOutputPath = outputUri.getPath();
-    }
-
-    /**
-     * Reads a {@code ByteString} from the input stream. The input stream must be opened before
-     * calling this method.
-     */
-    @Override
-    public ByteString read() throws IOException {
-        try (InputStreamReader inputStream = new InputStreamReader(
-                new FileInputStream(mInputPath))) {
-            int size = inputStream.read();
-            if (size == 0) {
-                throw new IOException(String.format("Missing data size %d", size));
-            }
-
-            if (size > MAX_IO_DATA_LENGTH_BYTE) {
-                throw new IOException("Exceed the maximum data length when reading.");
-            }
-
-            char[] data = new char[size];
-            int count = inputStream.read(data);
-            if (count != size) {
-                throw new IOException(
-                        String.format("Expected size was %s but got %s", size, count));
-            }
-
-            return ByteString.copyFrom(base16().decode(new String(data)));
-        }
-    }
-
-    /**
-     * Writes a {@code output} into the output stream. The output stream must be opened before
-     * calling this method.
-     */
-    @Override
-    public void write(ByteString output) throws IOException {
-        checkArgument(output.size() > 0, "Output data is empty.");
-
-        if (output.size() > MAX_IO_DATA_LENGTH_BYTE) {
-            throw new IOException("Exceed the maximum data length when writing.");
-        }
-
-        try (OutputStreamWriter outputStream =
-                     new OutputStreamWriter(new FileOutputStream(mOutputPath))) {
-            String base16Output = base16().encode(output.toByteArray());
-            outputStream.write(base16Output.length());
-            outputStream.write(base16Output);
-        }
-    }
-
-    private static boolean isFileExists(@Nullable String path) {
-        return path != null && new File(path).exists();
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevice.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevice.java
deleted file mode 100644
index 11ec9cb..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevice.java
+++ /dev/null
@@ -1,46 +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.nearby.fastpair.provider.simulator.testing;
-
-/** Represents a remote device and provides a {@link StreamIOHandler} to communicate with it. */
-public class RemoteDevice {
-    private final String mId;
-    private final StreamIOHandler mStreamIOHandler;
-    private final InputStreamListener mInputStreamListener;
-
-    public RemoteDevice(
-            String id, StreamIOHandler streamIOHandler, InputStreamListener inputStreamListener) {
-        this.mId = id;
-        this.mStreamIOHandler = streamIOHandler;
-        this.mInputStreamListener = inputStreamListener;
-    }
-
-    /** The id used by this device. */
-    public String getId() {
-        return mId;
-    }
-
-    /** The handler processes input and output data channels. */
-    public StreamIOHandler getStreamIOHandler() {
-        return mStreamIOHandler;
-    }
-
-    /** Listener for the input stream. */
-    public InputStreamListener getInputStreamListener() {
-        return mInputStreamListener;
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevicesManager.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevicesManager.java
deleted file mode 100644
index 02260c2..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevicesManager.java
+++ /dev/null
@@ -1,140 +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.nearby.fastpair.provider.simulator.testing;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import android.util.Log;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-import com.google.protobuf.ByteString;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Executors;
-
-/**
- * Manages the IO streams with remote devices.
- *
- * <p>The caller must invoke {@link #registerRemoteDevice} before starting to communicate with the
- * remote device, and invoke {@link #unregisterRemoteDevice} after finishing tasks. If this instance
- * is not used anymore, the caller need to invoke {@link #destroy} to release all resources.
- *
- * <p>All of the methods are thread-safe.
- */
-public class RemoteDevicesManager {
-    private static final String TAG = "RemoteDevicesManager";
-
-    private final Map<String, RemoteDevice> mRemoteDeviceMap = new HashMap<>();
-    private final ListeningExecutorService mBackgroundExecutor =
-            MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
-    private final ListeningExecutorService mListenInputStreamExecutors =
-            MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
-    private final Map<String, ListenableFuture<Void>> mListeningTaskMap = new HashMap<>();
-
-    /**
-     * Opens input and output data streams for {@code remoteDevice} in the background and notifies
-     * the
-     * open result via {@code callback}, and assigns a dedicated executor to listen the input data
-     * stream if data streams are opened successfully. The dedicated executor will invoke the
-     * {@code
-     * remoteDevice.inputStreamListener().onInputData()} directly if the new data exists in the
-     * input
-     * stream and invoke the {@code remoteDevice.inputStreamListener().onClose()} if the input
-     * stream
-     * is closed.
-     */
-    public synchronized void registerRemoteDevice(String id, RemoteDevice remoteDevice) {
-        checkState(mRemoteDeviceMap.put(id, remoteDevice) == null,
-                "The %s is already registered", id);
-        startListeningInputStreamTask(remoteDevice);
-    }
-
-    /**
-     * Closes the data streams for specific remote device {@code id} in the background and notifies
-     * the result via {@code callback}.
-     */
-    public synchronized void unregisterRemoteDevice(String id) {
-        RemoteDevice remoteDevice = mRemoteDeviceMap.remove(id);
-        checkState(remoteDevice != null, "The %s is not registered", id);
-        if (mListeningTaskMap.containsKey(id)) {
-            mListeningTaskMap.remove(id).cancel(/* mayInterruptIfRunning= */ true);
-        }
-    }
-
-    /** Closes all data streams of registered remote devices and stop all background tasks. */
-    public synchronized void destroy() {
-        mRemoteDeviceMap.clear();
-        mListeningTaskMap.clear();
-        mListenInputStreamExecutors.shutdownNow();
-    }
-
-    /**
-     * Writes {@code data} into the output data stream of specific remote device {@code id} in the
-     * background and notifies the result via {@code callback}.
-     */
-    public synchronized void writeDataToRemoteDevice(
-            String id, ByteString data, FutureCallback<Void> callback) {
-        RemoteDevice remoteDevice = mRemoteDeviceMap.get(id);
-        checkState(remoteDevice != null, "The %s is not registered", id);
-
-        runInBackground(() -> {
-            remoteDevice.getStreamIOHandler().write(data);
-            return null;
-        }, callback);
-    }
-
-    private void runInBackground(Callable<Void> callable, FutureCallback<Void> callback) {
-        Futures.addCallback(
-                mBackgroundExecutor.submit(callable), callback, MoreExecutors.directExecutor());
-    }
-
-    private void startListeningInputStreamTask(RemoteDevice remoteDevice) {
-        ListenableFuture<Void> listenFuture = mListenInputStreamExecutors.submit(() -> {
-            Log.i(TAG, "Start listening " + remoteDevice.getId());
-            while (true) {
-                ByteString data;
-                try {
-                    data = remoteDevice.getStreamIOHandler().read();
-                } catch (IOException | IllegalStateException e) {
-                    break;
-                }
-                remoteDevice.getInputStreamListener().onInputData(data);
-            }
-        }, /* result= */ null);
-        Futures.addCallback(listenFuture, new FutureCallback<Void>() {
-            @Override
-            public void onSuccess(Void result) {
-                Log.i(TAG, "Stop listening " + remoteDevice.getId());
-                remoteDevice.getInputStreamListener().onClose();
-            }
-
-            @Override
-            public void onFailure(Throwable t) {
-                Log.w(TAG, "Stop listening " + remoteDevice.getId() + ", cause: " + t);
-                remoteDevice.getInputStreamListener().onClose();
-            }
-        }, MoreExecutors.directExecutor());
-        mListeningTaskMap.put(remoteDevice.getId(), listenFuture);
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandler.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandler.java
deleted file mode 100644
index d5fdb9e..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandler.java
+++ /dev/null
@@ -1,43 +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.nearby.fastpair.provider.simulator.testing;
-
-import com.google.protobuf.ByteString;
-
-import java.io.IOException;
-
-/**
- * Opens input and output data channels, then provides read and write operations to the data
- * channels.
- */
-public interface StreamIOHandler {
-    /**
-     * Reads stream data from the input channel.
-     *
-     * @return a protocol buffer contains the input message
-     * @throws IOException errors occur when reading the input stream
-     */
-    ByteString read() throws IOException;
-
-    /**
-     * Writes stream data to the output channel.
-     *
-     * @param output a protocol buffer contains the output message
-     * @throws IOException errors occur when writing the output message to output stream
-     */
-    void write(ByteString output) throws IOException;
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandlerFactory.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandlerFactory.java
deleted file mode 100644
index 24cfe56..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandlerFactory.java
+++ /dev/null
@@ -1,44 +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.nearby.fastpair.provider.simulator.testing;
-
-import android.net.Uri;
-
-import java.io.IOException;
-
-/** A simple factory creating {@link StreamIOHandler} according to {@link Type}. */
-public class StreamIOHandlerFactory {
-
-    /** Types for creating {@link StreamIOHandler}. */
-    public enum Type {
-
-        /**
-         * A {@link StreamIOHandler} accepts local file uris and provides reading/writing file
-         * operations.
-         */
-        LOCAL_FILE
-    }
-
-    /** Creates an instance of {@link StreamIOHandler}. */
-    public static StreamIOHandler createStreamIOHandler(Type type, Uri input, Uri output)
-            throws IOException {
-        if (type.equals(Type.LOCAL_FILE)) {
-            return new LocalFileStreamIOHandler(input, output);
-        }
-        throw new IllegalArgumentException(String.format("Can't support %s", type));
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairAdvertiser.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairAdvertiser.java
deleted file mode 100644
index 95c077b..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairAdvertiser.java
+++ /dev/null
@@ -1,27 +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.nearby.fastpair.provider;
-
-import androidx.annotation.Nullable;
-
-/** Helper for advertising Fast Pair data. */
-public interface FastPairAdvertiser {
-
-    void startAdvertising(@Nullable byte[] serviceData);
-
-    void stopAdvertising();
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java
deleted file mode 100644
index 0d5563e..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java
+++ /dev/null
@@ -1,2391 +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.nearby.fastpair.provider;
-
-import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
-import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
-import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
-import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
-import static android.bluetooth.BluetoothAdapter.STATE_OFF;
-import static android.bluetooth.BluetoothAdapter.STATE_ON;
-import static android.bluetooth.BluetoothDevice.ERROR;
-import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_READ;
-import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_WRITE;
-import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_INDICATE;
-import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY;
-import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_READ;
-import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE;
-import static android.nearby.fastpair.provider.bluetooth.BluetoothManager.wrap;
-import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.CONNECTED;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.encrypt;
-import static com.android.server.nearby.common.bluetooth.fastpair.Bytes.toBytes;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.A2DP_SINK_SERVICE_UUID;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService.BLUETOOTH_SIG_ORGANIZATION_ID;
-import static com.android.server.nearby.common.bluetooth.fastpair.EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder.SECTION_NONCE_LENGTH;
-
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.primitives.Bytes.concat;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothClass.Device.Major;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.AdvertiseSettings;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.nearby.fastpair.provider.EventStreamProtocol.AcknowledgementEventCode;
-import android.nearby.fastpair.provider.EventStreamProtocol.DeviceActionEventCode;
-import android.nearby.fastpair.provider.EventStreamProtocol.DeviceCapabilitySyncEventCode;
-import android.nearby.fastpair.provider.EventStreamProtocol.DeviceConfigurationEventCode;
-import android.nearby.fastpair.provider.EventStreamProtocol.DeviceEventCode;
-import android.nearby.fastpair.provider.EventStreamProtocol.EventGroup;
-import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerConfig;
-import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerConfig.ServiceConfig;
-import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerConnection;
-import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerConnection.Notifier;
-import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerHelper;
-import android.nearby.fastpair.provider.bluetooth.BluetoothGattServlet;
-import android.nearby.fastpair.provider.bluetooth.RfcommServer;
-import android.nearby.fastpair.provider.crypto.Crypto;
-import android.nearby.fastpair.provider.crypto.E2eeCalculator;
-import android.nearby.fastpair.provider.utils.Logger;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
-import android.provider.Settings;
-import android.text.TextUtils;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.core.util.Consumer;
-
-import com.android.server.nearby.common.bloomfilter.BloomFilter;
-import com.android.server.nearby.common.bloomfilter.FastPairBloomFilterHasher;
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption;
-import com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress;
-import com.android.server.nearby.common.bluetooth.fastpair.Bytes.Value;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.BeaconActionsCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.FirmwareVersionCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.NameCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.PasskeyCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService.BrHandoverDataCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService.ControlPointCharacteristic;
-import com.android.server.nearby.common.bluetooth.fastpair.EllipticCurveDiffieHellmanExchange;
-import com.android.server.nearby.common.bluetooth.fastpair.Ltv;
-import com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder;
-import com.android.server.nearby.common.bluetooth.fastpair.NamingEncoder;
-
-import com.google.common.base.Ascii;
-import com.google.common.primitives.Bytes;
-import com.google.protobuf.ByteString;
-
-import java.lang.reflect.Method;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Random;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Simulates a Fast Pair device (e.g. a headset).
- *
- * <p>Note: There are two deviations from the spec:
- *
- * <ul>
- *   <li>Instead of using the public address when in pairing mode (discoverable), it always uses the
- *       random private address (RPA), because that's how stock Android works. To work around this,
- *       it implements the BR/EDR Handover profile (which is no longer part of the Fast Pair spec)
- *       when simulating a keyless device (i.e. Fast Pair 1.0), which allows the phone to ask for
- *       the public address. When there is an anti-spoofing key, i.e. Fast Pair 2.0, the public
- *       address is delivered via the Key-based Pairing handshake. b/79374759 tracks fixing this.
- *   <li>The simulator always identifies its device capabilities as Keyboard/Display, even when
- *       simulating a keyless (Fast Pair 1.0) device that should identify as NoInput/NoOutput.
- *       b/79377125 tracks fixing this.
- * </ul>
- *
- * @see {http://go/fast-pair-2-spec}
- */
-public class FastPairSimulator {
-    public static final String TAG = "FastPairSimulator";
-    private final Logger mLogger;
-
-    private static final int BECOME_DISCOVERABLE_TIMEOUT_SEC = 3;
-
-    private static final int SCAN_MODE_REFRESH_SEC = 30;
-
-    /**
-     * Headphones. Generated by
-     * http://bluetooth-pentest.narod.ru/software/bluetooth_class_of_device-service_generator.html
-     */
-    private static final Value CLASS_OF_DEVICE =
-            new Value(base16().decode("200418"), ByteOrder.BIG_ENDIAN);
-
-    private static final byte[] SUPPORTED_SERVICES_LTV = new Ltv(
-            TransportDiscoveryService.SERVICE_UUIDS_16_BIT_LIST_TYPE,
-            toBytes(ByteOrder.LITTLE_ENDIAN, A2DP_SINK_SERVICE_UUID)
-    ).getBytes();
-    private static final byte[] TDS_CONTROL_POINT_RESPONSE_PARAMETER =
-            Bytes.concat(new byte[]{BLUETOOTH_SIG_ORGANIZATION_ID}, SUPPORTED_SERVICES_LTV);
-
-    private static final String SIMULATOR_FAKE_BLE_ADDRESS = "11:22:33:44:55:66";
-
-    private static final long ADVERTISING_REFRESH_DELAY_1_MIN = TimeUnit.MINUTES.toMillis(1);
-
-    /**
-     * The size of account key filter in bytes is (1.2*n + 3), n represents the size of account key,
-     * see https://developers.google.com/nearby/fast-pair/spec#advertising_when_not_discoverable.
-     * However we'd like to advertise something else, so we could only afford 8 account keys.
-     *
-     * <ul>
-     *   <li>BLE flags: 3 bytes
-     *   <li>TxPower: 3 bytes
-     *   <li>FastPair: max 25 bytes
-     *       <ul>
-     *         <li>FastPair service data: 4 bytes
-     *         <li>Flags: 1 byte
-     *         <li>Account key filter: max 14 bytes (1 byte: length + type, 13 bytes: max 8 account
-     *             keys)
-     *         <li>Salt: 2 bytes
-     *         <li>Battery: 4 bytes
-     *       </ul>
-     * </ul>
-     */
-    private String mDeviceFirmwareVersion = "1.1.0";
-
-    private byte[] mSessionNonce;
-
-    private boolean mUseLogFullEvent = true;
-
-    private enum ResultCode {
-        SUCCESS((byte) 0x00),
-        OP_CODE_NOT_SUPPORTED((byte) 0x01),
-        INVALID_PARAMETER((byte) 0x02),
-        UNSUPPORTED_ORGANIZATION_ID((byte) 0x03),
-        OPERATION_FAILED((byte) 0x04);
-
-        private final byte mByteValue;
-
-        ResultCode(byte byteValue) {
-            this.mByteValue = byteValue;
-        }
-    }
-
-    private enum TransportState {
-        OFF((byte) 0x00),
-        ON((byte) 0x01),
-        TEMPORARILY_UNAVAILABLE((byte) 0x10);
-
-        private final byte mByteValue;
-
-        TransportState(byte byteValue) {
-            this.mByteValue = byteValue;
-        }
-    }
-
-    private final Context mContext;
-    private final Options mOptions;
-    private final Handler mUiThreadHandler = new Handler(Looper.getMainLooper());
-    // No thread pool: Only used in test app (outside gmscore) and in javatests/.../gmscore/.
-    private final ScheduledExecutorService mExecutor =
-            Executors.newSingleThreadScheduledExecutor(); // exempt
-    private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
-    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (mShouldFailPairing) {
-                mLogger.log("Pairing disabled by test app switch");
-                return;
-            }
-            if (mIsDestroyed) {
-                // Sometimes this receiver does not successfully unregister in destroy()
-                // which causes events to occur after the simulator is stopped, so ignore
-                // those events.
-                mLogger.log("Intent received after simulator destroyed, ignoring");
-                return;
-            }
-            BluetoothDevice device = intent.getParcelableExtra(
-                    BluetoothDevice.EXTRA_DEVICE);
-            switch (intent.getAction()) {
-                case BluetoothAdapter.ACTION_SCAN_MODE_CHANGED:
-                    if (isDiscoverable()) {
-                        mIsDiscoverableLatch.countDown();
-                    }
-                    break;
-                case BluetoothDevice.ACTION_PAIRING_REQUEST:
-                    int variant = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT,
-                            ERROR);
-                    int key = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, ERROR);
-                    mLogger.log(
-                            "Pairing request, variant=%d, key=%s", variant,
-                            key == ERROR ? "(none)" : key);
-
-                    // Prevent Bluetooth Settings from getting the pairing request.
-                    abortBroadcast();
-
-                    mPairingDevice = device;
-                    if (mSecret == null) {
-                        // We haven't done the handshake over GATT to agree on the shared
-                        // secret. For now, just accept anyway (so we can still simulate
-                        // old 1.0 model IDs).
-                        mLogger.log("No handshake, auto-accepting anyway.");
-                        setPasskeyConfirmation(true);
-                    } else if (variant
-                            == BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION) {
-                        // Store the passkey. And check it, since there's a race (see
-                        // method for why). Usually this check is a no-op and we'll get
-                        // the passkey later over GATT.
-                        mLocalPasskey = key;
-                        checkPasskey();
-                    } else if (variant == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
-                        if (mPasskeyEventCallback != null) {
-                            mPasskeyEventCallback.onPasskeyRequested(
-                                    FastPairSimulator.this::enterPassKey);
-                        } else {
-                            mLogger.log("passkeyEventCallback is not set!");
-                            enterPassKey(key);
-                        }
-                    } else if (variant == BluetoothDevice.PAIRING_VARIANT_CONSENT) {
-                        setPasskeyConfirmation(true);
-
-                    } else if (variant == BluetoothDevice.PAIRING_VARIANT_PIN) {
-                        if (mPasskeyEventCallback != null) {
-                            mPasskeyEventCallback.onPasskeyRequested(
-                                    (int pin) -> {
-                                        byte[] newPin = convertPinToBytes(
-                                                String.format(Locale.ENGLISH, "%d", pin));
-                                        mPairingDevice.setPin(newPin);
-                                    });
-                        }
-                    } else {
-                        // Reject the pairing request if it's not using the Numeric
-                        // Comparison (aka Passkey Confirmation) method.
-                        setPasskeyConfirmation(false);
-                    }
-                    break;
-                case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
-                    int bondState =
-                            intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
-                                    BluetoothDevice.BOND_NONE);
-                    mLogger.log("Bond state to %s changed to %d", device, bondState);
-                    switch (bondState) {
-                        case BluetoothDevice.BOND_BONDING:
-                            // If we've started bonding, we shouldn't be advertising.
-                            mAdvertiser.stopAdvertising();
-                            // Not discoverable anymore, but still connectable.
-                            setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
-                            break;
-                        case BluetoothDevice.BOND_BONDED:
-                            // Once bonded, advertise the account keys.
-                            mAdvertiser.startAdvertising(accountKeysServiceData());
-                            setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
-
-                            // If it is subsequent pair, we need to add paired device here.
-                            if (mIsSubsequentPair
-                                    && mSecret != null
-                                    && mSecret.length == AES_BLOCK_LENGTH) {
-                                addAccountKey(mSecret, mPairingDevice);
-                            }
-                            break;
-                        case BluetoothDevice.BOND_NONE:
-                            // If the bonding process fails, we should be advertising again.
-                            mAdvertiser.startAdvertising(getServiceData());
-                            break;
-                        default:
-                            break;
-                    }
-                    break;
-                case BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED:
-                    mLogger.log(
-                            "Connection state to %s changed to %d",
-                            device,
-                            intent.getIntExtra(
-                                    BluetoothAdapter.EXTRA_CONNECTION_STATE,
-                                    BluetoothAdapter.STATE_DISCONNECTED));
-                    break;
-                case BluetoothAdapter.ACTION_STATE_CHANGED:
-                    int state = intent.getIntExtra(EXTRA_STATE, -1);
-                    mLogger.log("Bluetooth adapter state=%s", state);
-                    switch (state) {
-                        case STATE_ON:
-                            startRfcommServer();
-                            break;
-                        case STATE_OFF:
-                            stopRfcommServer();
-                            break;
-                        default: // fall out
-                    }
-                    break;
-                default:
-                    mLogger.log(new IllegalArgumentException(intent.toString()),
-                            "Received unexpected intent");
-                    break;
-            }
-        }
-    };
-
-    @Nullable
-    private byte[] convertPinToBytes(@Nullable String pin) {
-        if (TextUtils.isEmpty(pin)) {
-            return null;
-        }
-        byte[] pinBytes;
-        pinBytes = pin.getBytes(StandardCharsets.UTF_8);
-        if (pinBytes.length <= 0 || pinBytes.length > 16) {
-            return null;
-        }
-        return pinBytes;
-    }
-
-    private final NotifiableGattServlet mPasskeyServlet =
-            new NotifiableGattServlet() {
-                @Override
-                // Simulating deprecated API {@code PasskeyCharacteristic.ID} for testing.
-                @SuppressWarnings("deprecation")
-                public BluetoothGattCharacteristic getBaseCharacteristic() {
-                    return new BluetoothGattCharacteristic(
-                            PasskeyCharacteristic.CUSTOM_128_BIT_UUID,
-                            PROPERTY_WRITE | PROPERTY_INDICATE,
-                            PERMISSION_WRITE);
-                }
-
-                @Override
-                public void write(
-                        BluetoothGattServerConnection connection, int offset, byte[] value) {
-                    mLogger.log("Got value from passkey servlet: %s", base16().encode(value));
-                    if (mSecret == null) {
-                        mLogger.log("Ignoring write to passkey characteristic, no pairing secret.");
-                        return;
-                    }
-
-                    try {
-                        mRemotePasskey = PasskeyCharacteristic.decrypt(
-                                PasskeyCharacteristic.Type.SEEKER, mSecret, value);
-                        if (mPasskeyEventCallback != null) {
-                            mPasskeyEventCallback.onRemotePasskeyReceived(mRemotePasskey);
-                        }
-                        checkPasskey();
-                    } catch (GeneralSecurityException e) {
-                        mLogger.log(
-                                "Decrypting passkey value %s failed using key %s",
-                                base16().encode(value), base16().encode(mSecret));
-                    }
-                }
-            };
-
-    private final NotifiableGattServlet mDeviceNameServlet =
-            new NotifiableGattServlet() {
-                @Override
-                // Simulating deprecated API {@code NameCharacteristic.ID} for testing.
-                @SuppressWarnings("deprecation")
-                BluetoothGattCharacteristic getBaseCharacteristic() {
-                    return new BluetoothGattCharacteristic(
-                            NameCharacteristic.CUSTOM_128_BIT_UUID,
-                            PROPERTY_WRITE | PROPERTY_INDICATE,
-                            PERMISSION_WRITE);
-                }
-
-                @Override
-                public void write(
-                        BluetoothGattServerConnection connection, int offset, byte[] value) {
-                    mLogger.log("Got value from device naming servlet: %s", base16().encode(value));
-                    if (mSecret == null) {
-                        mLogger.log("Ignoring write to name characteristic, no pairing secret.");
-                        return;
-                    }
-                    // Parse the device name from seeker to write name into provider.
-                    mLogger.log("Got name byte array size = %d", value.length);
-                    try {
-                        String decryptedDeviceName =
-                                NamingEncoder.decodeNamingPacket(mSecret, value);
-                        if (decryptedDeviceName != null) {
-                            setDeviceName(decryptedDeviceName.getBytes(StandardCharsets.UTF_8));
-                            mLogger.log("write device name = %s", decryptedDeviceName);
-                        }
-                    } catch (GeneralSecurityException e) {
-                        mLogger.log(e, "Failed to decrypt device name.");
-                    }
-                    // For testing to make sure we get the new provider name from simulator.
-                    if (mWriteNameCountDown != null) {
-                        mLogger.log("finish count down latch to write device name.");
-                        mWriteNameCountDown.countDown();
-                    }
-                }
-            };
-
-    private Value mBluetoothAddress;
-    private final FastPairAdvertiser mAdvertiser;
-    private final Map<String, BluetoothGattServerHelper> mBluetoothGattServerHelpers =
-            new HashMap<>();
-    private CountDownLatch mIsDiscoverableLatch = new CountDownLatch(1);
-    private ScheduledFuture<?> mRevertDiscoverableFuture;
-    private boolean mShouldFailPairing = false;
-    private boolean mIsDestroyed = false;
-    private boolean mIsAdvertising;
-    @Nullable
-    private String mBleAddress;
-    private BluetoothDevice mPairingDevice;
-    private int mLocalPasskey;
-    private int mRemotePasskey;
-    @Nullable
-    private byte[] mSecret;
-    @Nullable
-    private byte[] mAccountKey; // The latest account key added.
-    // The first account key added. Eddystone treats that account as the owner of the device.
-    @Nullable
-    private byte[] mOwnerAccountKey;
-    @Nullable
-    private PasskeyConfirmationCallback mPasskeyConfirmationCallback;
-    @Nullable
-    private DeviceNameCallback mDeviceNameCallback;
-    @Nullable
-    private PasskeyEventCallback mPasskeyEventCallback;
-    private final List<BatteryValue> mBatteryValues;
-    private boolean mSuppressBatteryNotification = false;
-    private boolean mSuppressSubsequentPairingNotification = false;
-    HandshakeRequest mHandshakeRequest;
-    @Nullable
-    private CountDownLatch mWriteNameCountDown;
-    private final RfcommServer mRfcommServer = new RfcommServer();
-    private boolean mSupportDynamicBufferSize = false;
-    private NotifiableGattServlet mBeaconActionsServlet;
-    private final FastPairSimulatorDatabase mFastPairSimulatorDatabase;
-    private boolean mIsSubsequentPair = false;
-
-    /** Sets the flag for failing paring for debug purpose. */
-    public void setShouldFailPairing(boolean shouldFailPairing) {
-        this.mShouldFailPairing = shouldFailPairing;
-    }
-
-    /** Gets the flag for failing paring for debug purpose. */
-    public boolean getShouldFailPairing() {
-        return mShouldFailPairing;
-    }
-
-    /** Clear the battery values, then no battery information is packed when advertising. */
-    public void clearBatteryValues() {
-        mBatteryValues.clear();
-    }
-
-    /** Sets the battery items which will be included in the advertisement packet. */
-    public void setBatteryValues(BatteryValue... batteryValues) {
-        this.mBatteryValues.clear();
-        Collections.addAll(this.mBatteryValues, batteryValues);
-    }
-
-    /** Sets whether the battery advertisement packet is within suppress type or not. */
-    public void setSuppressBatteryNotification(boolean suppressBatteryNotification) {
-        this.mSuppressBatteryNotification = suppressBatteryNotification;
-    }
-
-    /** Sets whether the account key data is within suppress type or not. */
-    public void setSuppressSubsequentPairingNotification(boolean isSuppress) {
-        mSuppressSubsequentPairingNotification = isSuppress;
-    }
-
-    /** Calls this to start advertising after some values are changed. */
-    public void startAdvertising() {
-        mAdvertiser.startAdvertising(getServiceData());
-    }
-
-    /** Send Event Message on to rfcomm connected devices. */
-    public void sendEventStreamMessageToRfcommDevices(EventGroup eventGroup) {
-        // Send fake log when event code is logging and type is not using Log_Full event.
-        if (eventGroup == EventGroup.LOGGING && !mUseLogFullEvent) {
-            mRfcommServer.sendFakeEventStreamLoggingMessage(
-                    getDeviceName()
-                            + " "
-                            + getBleAddress()
-                            + " send log at "
-                            + new SimpleDateFormat("HH:mm:ss:SSS", Locale.US)
-                            .format(Calendar.getInstance().getTime()));
-        } else {
-            mRfcommServer.sendFakeEventStreamMessage(eventGroup);
-        }
-    }
-
-    public void setUseLogFullEvent(boolean useLogFullEvent) {
-        this.mUseLogFullEvent = useLogFullEvent;
-    }
-
-    /** An optional way to get advertising status updates. */
-    public interface AdvertisingChangedCallback {
-        /**
-         * Called when we change our BLE advertisement.
-         *
-         * @param isAdvertising the advertising status.
-         */
-        void onAdvertisingChanged(boolean isAdvertising);
-    }
-
-    /** A way for tests to get callbacks when passkey confirmation is invoked. */
-    public interface PasskeyConfirmationCallback {
-        void onPasskeyConfirmation(boolean confirm);
-    }
-
-    /** A way for simulator UI update to get callback when device name is changed. */
-    public interface DeviceNameCallback {
-        void onNameChanged(String deviceName);
-    }
-
-    /**
-     * Callback when there comes a passkey input request from BT service, or receiving remote
-     * device's passkey.
-     */
-    public interface PasskeyEventCallback {
-        void onPasskeyRequested(KeyInputCallback keyInputCallback);
-
-        void onRemotePasskeyReceived(int passkey);
-
-        default void onPasskeyConfirmation(int passkey, Consumer<Boolean> isConfirmed) {
-        }
-    }
-
-    /** Options for the simulator. */
-    public static class Options {
-        private final String mModelId;
-
-        // TODO(b/143117318):Remove this when app-launch type has its own anti-spoofing key.
-        private final String mAdvertisingModelId;
-
-        @Nullable
-        private final String mBluetoothAddress;
-
-        @Nullable
-        private final String mBleAddress;
-
-        private final boolean mDataOnlyConnection;
-
-        private final int mTxPowerLevel;
-
-        private final boolean mEnableNameCharacteristic;
-
-        private final AdvertisingChangedCallback mAdvertisingChangedCallback;
-
-        private final boolean mIncludeTransportDataDescriptor;
-
-        @Nullable
-        private final byte[] mAntiSpoofingPrivateKey;
-
-        private final boolean mUseRandomSaltForAccountKeyRotation;
-
-        private final boolean mBecomeDiscoverable;
-
-        private final boolean mShowsPasskeyConfirmation;
-
-        private final boolean mEnableBeaconActionsCharacteristic;
-
-        private final boolean mRemoveAllDevicesDuringPairing;
-
-        @Nullable
-        private final ByteString mEddystoneIdentityKey;
-
-        private Options(
-                String modelId,
-                String advertisingModelId,
-                @Nullable String bluetoothAddress,
-                @Nullable String bleAddress,
-                boolean dataOnlyConnection,
-                int txPowerLevel,
-                boolean enableNameCharacteristic,
-                AdvertisingChangedCallback advertisingChangedCallback,
-                boolean includeTransportDataDescriptor,
-                @Nullable byte[] antiSpoofingPrivateKey,
-                boolean useRandomSaltForAccountKeyRotation,
-                boolean becomeDiscoverable,
-                boolean showsPasskeyConfirmation,
-                boolean enableBeaconActionsCharacteristic,
-                boolean removeAllDevicesDuringPairing,
-                @Nullable ByteString eddystoneIdentityKey) {
-            this.mModelId = modelId;
-            this.mAdvertisingModelId = advertisingModelId;
-            this.mBluetoothAddress = bluetoothAddress;
-            this.mBleAddress = bleAddress;
-            this.mDataOnlyConnection = dataOnlyConnection;
-            this.mTxPowerLevel = txPowerLevel;
-            this.mEnableNameCharacteristic = enableNameCharacteristic;
-            this.mAdvertisingChangedCallback = advertisingChangedCallback;
-            this.mIncludeTransportDataDescriptor = includeTransportDataDescriptor;
-            this.mAntiSpoofingPrivateKey = antiSpoofingPrivateKey;
-            this.mUseRandomSaltForAccountKeyRotation = useRandomSaltForAccountKeyRotation;
-            this.mBecomeDiscoverable = becomeDiscoverable;
-            this.mShowsPasskeyConfirmation = showsPasskeyConfirmation;
-            this.mEnableBeaconActionsCharacteristic = enableBeaconActionsCharacteristic;
-            this.mRemoveAllDevicesDuringPairing = removeAllDevicesDuringPairing;
-            this.mEddystoneIdentityKey = eddystoneIdentityKey;
-        }
-
-        public String getModelId() {
-            return mModelId;
-        }
-
-        // TODO(b/143117318):Remove this when app-launch type has its own anti-spoofing key.
-        public String getAdvertisingModelId() {
-            return mAdvertisingModelId;
-        }
-
-        @Nullable
-        public String getBluetoothAddress() {
-            return mBluetoothAddress;
-        }
-
-        @Nullable
-        public String getBleAddress() {
-            return mBleAddress;
-        }
-
-        public boolean getDataOnlyConnection() {
-            return mDataOnlyConnection;
-        }
-
-        public int getTxPowerLevel() {
-            return mTxPowerLevel;
-        }
-
-        public boolean getEnableNameCharacteristic() {
-            return mEnableNameCharacteristic;
-        }
-
-        public AdvertisingChangedCallback getAdvertisingChangedCallback() {
-            return mAdvertisingChangedCallback;
-        }
-
-        public boolean getIncludeTransportDataDescriptor() {
-            return mIncludeTransportDataDescriptor;
-        }
-
-        @Nullable
-        public byte[] getAntiSpoofingPrivateKey() {
-            return mAntiSpoofingPrivateKey;
-        }
-
-        public boolean getUseRandomSaltForAccountKeyRotation() {
-            return mUseRandomSaltForAccountKeyRotation;
-        }
-
-        public boolean getBecomeDiscoverable() {
-            return mBecomeDiscoverable;
-        }
-
-        public boolean getShowsPasskeyConfirmation() {
-            return mShowsPasskeyConfirmation;
-        }
-
-        public boolean getEnableBeaconActionsCharacteristic() {
-            return mEnableBeaconActionsCharacteristic;
-        }
-
-        public boolean getRemoveAllDevicesDuringPairing() {
-            return mRemoveAllDevicesDuringPairing;
-        }
-
-        @Nullable
-        public ByteString getEddystoneIdentityKey() {
-            return mEddystoneIdentityKey;
-        }
-
-        /** Converts an instance to a builder. */
-        public Builder toBuilder() {
-            return new Options.Builder(this);
-        }
-
-        /** Constructs a builder. */
-        public static Builder builder() {
-            return new Options.Builder();
-        }
-
-        /** @param modelId Must be a 3-byte hex string. */
-        public static Builder builder(String modelId) {
-            return new Options.Builder()
-                    .setModelId(Ascii.toUpperCase(modelId))
-                    .setAdvertisingModelId(Ascii.toUpperCase(modelId))
-                    .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
-                    .setAdvertisingChangedCallback(isAdvertising -> {
-                    })
-                    .setIncludeTransportDataDescriptor(true)
-                    .setUseRandomSaltForAccountKeyRotation(false)
-                    .setEnableNameCharacteristic(true)
-                    .setDataOnlyConnection(false)
-                    .setBecomeDiscoverable(true)
-                    .setShowsPasskeyConfirmation(false)
-                    .setEnableBeaconActionsCharacteristic(true)
-                    .setRemoveAllDevicesDuringPairing(true);
-        }
-
-        /** A builder for {@link Options}. */
-        public static class Builder {
-
-            private String mModelId;
-
-            // TODO(b/143117318):Remove this when app-launch type has its own anti-spoofing key.
-            private String mAdvertisingModelId;
-
-            @Nullable
-            private String mBluetoothAddress;
-
-            @Nullable
-            private String mBleAddress;
-
-            private boolean mDataOnlyConnection;
-
-            private int mTxPowerLevel;
-
-            private boolean mEnableNameCharacteristic;
-
-            private AdvertisingChangedCallback mAdvertisingChangedCallback;
-
-            private boolean mIncludeTransportDataDescriptor;
-
-            @Nullable
-            private byte[] mAntiSpoofingPrivateKey;
-
-            private boolean mUseRandomSaltForAccountKeyRotation;
-
-            private boolean mBecomeDiscoverable;
-
-            private boolean mShowsPasskeyConfirmation;
-
-            private boolean mEnableBeaconActionsCharacteristic;
-
-            private boolean mRemoveAllDevicesDuringPairing;
-
-            @Nullable
-            private ByteString mEddystoneIdentityKey;
-
-            private Builder() {
-            }
-
-            private Builder(Options option) {
-                this.mModelId = option.mModelId;
-                this.mAdvertisingModelId = option.mAdvertisingModelId;
-                this.mBluetoothAddress = option.mBluetoothAddress;
-                this.mBleAddress = option.mBleAddress;
-                this.mDataOnlyConnection = option.mDataOnlyConnection;
-                this.mTxPowerLevel = option.mTxPowerLevel;
-                this.mEnableNameCharacteristic = option.mEnableNameCharacteristic;
-                this.mAdvertisingChangedCallback = option.mAdvertisingChangedCallback;
-                this.mIncludeTransportDataDescriptor = option.mIncludeTransportDataDescriptor;
-                this.mAntiSpoofingPrivateKey = option.mAntiSpoofingPrivateKey;
-                this.mUseRandomSaltForAccountKeyRotation =
-                        option.mUseRandomSaltForAccountKeyRotation;
-                this.mBecomeDiscoverable = option.mBecomeDiscoverable;
-                this.mShowsPasskeyConfirmation = option.mShowsPasskeyConfirmation;
-                this.mEnableBeaconActionsCharacteristic = option.mEnableBeaconActionsCharacteristic;
-                this.mRemoveAllDevicesDuringPairing = option.mRemoveAllDevicesDuringPairing;
-                this.mEddystoneIdentityKey = option.mEddystoneIdentityKey;
-            }
-
-            /**
-             * Must be one of the {@code ADVERTISE_TX_POWER_*} levels in {@link AdvertiseSettings}.
-             * Default is HIGH.
-             */
-            public Builder setTxPowerLevel(int txPowerLevel) {
-                this.mTxPowerLevel = txPowerLevel;
-                return this;
-            }
-
-            /**
-             * Must be a 6-byte hex string (optionally with colons).
-             * Default is this device's BT MAC.
-             */
-            public Builder setBluetoothAddress(@Nullable String bluetoothAddress) {
-                this.mBluetoothAddress = bluetoothAddress;
-                return this;
-            }
-
-            public Builder setBleAddress(@Nullable String bleAddress) {
-                this.mBleAddress = bleAddress;
-                return this;
-            }
-
-            /** A boolean to decide if enable name characteristic as simulator characteristic. */
-            public Builder setEnableNameCharacteristic(boolean enable) {
-                this.mEnableNameCharacteristic = enable;
-                return this;
-            }
-
-            /** @see AdvertisingChangedCallback */
-            public Builder setAdvertisingChangedCallback(
-                    AdvertisingChangedCallback advertisingChangedCallback) {
-                this.mAdvertisingChangedCallback = advertisingChangedCallback;
-                return this;
-            }
-
-            public Builder setDataOnlyConnection(boolean dataOnlyConnection) {
-                this.mDataOnlyConnection = dataOnlyConnection;
-                return this;
-            }
-
-            /**
-             * Set whether to include the Transport Data descriptor, which has the list of supported
-             * profiles. This is required by the spec, but if we can't get it, we recover gracefully
-             * by assuming support for one of {A2DP, Headset}. Default is true.
-             */
-            public Builder setIncludeTransportDataDescriptor(
-                    boolean includeTransportDataDescriptor) {
-                this.mIncludeTransportDataDescriptor = includeTransportDataDescriptor;
-                return this;
-            }
-
-            public Builder setAntiSpoofingPrivateKey(@Nullable byte[] antiSpoofingPrivateKey) {
-                this.mAntiSpoofingPrivateKey = antiSpoofingPrivateKey;
-                return this;
-            }
-
-            public Builder setUseRandomSaltForAccountKeyRotation(
-                    boolean useRandomSaltForAccountKeyRotation) {
-                this.mUseRandomSaltForAccountKeyRotation = useRandomSaltForAccountKeyRotation;
-                return this;
-            }
-
-            // TODO(b/143117318):Remove this when app-launch type has its own anti-spoofing key.
-            public Builder setAdvertisingModelId(String modelId) {
-                this.mAdvertisingModelId = modelId;
-                return this;
-            }
-
-            public Builder setBecomeDiscoverable(boolean becomeDiscoverable) {
-                this.mBecomeDiscoverable = becomeDiscoverable;
-                return this;
-            }
-
-            public Builder setShowsPasskeyConfirmation(boolean showsPasskeyConfirmation) {
-                this.mShowsPasskeyConfirmation = showsPasskeyConfirmation;
-                return this;
-            }
-
-            public Builder setEnableBeaconActionsCharacteristic(
-                    boolean enableBeaconActionsCharacteristic) {
-                this.mEnableBeaconActionsCharacteristic = enableBeaconActionsCharacteristic;
-                return this;
-            }
-
-            public Builder setRemoveAllDevicesDuringPairing(boolean removeAllDevicesDuringPairing) {
-                this.mRemoveAllDevicesDuringPairing = removeAllDevicesDuringPairing;
-                return this;
-            }
-
-            /**
-             * Non-public because this is required to create a builder. See
-             * {@link Options#builder}.
-             */
-            public Builder setModelId(String modelId) {
-                this.mModelId = modelId;
-                return this;
-            }
-
-            public Builder setEddystoneIdentityKey(@Nullable ByteString eddystoneIdentityKey) {
-                this.mEddystoneIdentityKey = eddystoneIdentityKey;
-                return this;
-            }
-
-            // Custom builder in order to normalize properties. go/autovalue/builders-howto
-            public Options build() {
-                return new Options(
-                        Ascii.toUpperCase(mModelId),
-                        Ascii.toUpperCase(mAdvertisingModelId),
-                        mBluetoothAddress,
-                        mBleAddress,
-                        mDataOnlyConnection,
-                        mTxPowerLevel,
-                        mEnableNameCharacteristic,
-                        mAdvertisingChangedCallback,
-                        mIncludeTransportDataDescriptor,
-                        mAntiSpoofingPrivateKey,
-                        mUseRandomSaltForAccountKeyRotation,
-                        mBecomeDiscoverable,
-                        mShowsPasskeyConfirmation,
-                        mEnableBeaconActionsCharacteristic,
-                        mRemoveAllDevicesDuringPairing,
-                        mEddystoneIdentityKey);
-            }
-        }
-    }
-
-    public FastPairSimulator(Context context, Options options) {
-        this(context, options, new Logger(TAG));
-    }
-
-    public FastPairSimulator(Context context, Options options, Logger logger) {
-        this.mContext = context;
-        this.mOptions = options;
-        this.mLogger = logger;
-
-        this.mBatteryValues = new ArrayList<>();
-
-        String bluetoothAddress =
-                !TextUtils.isEmpty(options.getBluetoothAddress())
-                        ? options.getBluetoothAddress()
-                        : Settings.Secure.getString(context.getContentResolver(),
-                                "bluetooth_address");
-        if (bluetoothAddress == null && VERSION.SDK_INT >= VERSION_CODES.O) {
-            // Requires a modified Android O build for access to bluetoothAdapter.getAddress().
-            // See http://google3/java/com/google/location/nearby/apps/fastpair/simulator/README.md.
-            bluetoothAddress = mBluetoothAdapter.getAddress();
-        }
-        this.mBluetoothAddress =
-                new Value(BluetoothAddress.decode(bluetoothAddress), ByteOrder.BIG_ENDIAN);
-        this.mBleAddress = options.getBleAddress();
-        this.mAdvertiser = new OreoFastPairAdvertiser(this);
-
-        mFastPairSimulatorDatabase = new FastPairSimulatorDatabase(context);
-
-        byte[] deviceName = getDeviceNameInBytes();
-        mLogger.log(
-                "Provider default device name is %s",
-                deviceName != null ? new String(deviceName, StandardCharsets.UTF_8) : null);
-
-        if (mOptions.getDataOnlyConnection()) {
-            // To get BLE address, we need to start advertising first, and then
-            // {@code#setBleAddress} will be called with BLE address.
-            mAdvertiser.startAdvertising(modelIdServiceData(/* forAdvertising= */ true));
-        } else {
-            // Make this so that the simulator doesn't start automatically.
-            // This is tricky since the simulator is used in our integ tests as well.
-            start(mBleAddress != null ? mBleAddress : bluetoothAddress);
-        }
-    }
-
-    public void start(String address) {
-        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
-        filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST);
-        filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
-        filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
-        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
-        mContext.registerReceiver(mBroadcastReceiver, filter);
-
-        BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
-        BluetoothGattServerHelper bluetoothGattServerHelper =
-                new BluetoothGattServerHelper(mContext, wrap(bluetoothManager));
-        mBluetoothGattServerHelpers.put(address, bluetoothGattServerHelper);
-
-        if (mOptions.getBecomeDiscoverable()) {
-            try {
-                becomeDiscoverable();
-            } catch (InterruptedException | TimeoutException e) {
-                mLogger.log(e, "Error becoming discoverable");
-            }
-        }
-
-        mAdvertiser.startAdvertising(modelIdServiceData(/* forAdvertising= */ true));
-        startGattServer(bluetoothGattServerHelper);
-        startRfcommServer();
-        scheduleAdvertisingRefresh();
-    }
-
-    /**
-     * Regenerate service data on a fixed interval.
-     * This causes the bloom filter to be refreshed and a different salt to be used for rotation.
-     */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    private void scheduleAdvertisingRefresh() {
-        mExecutor.scheduleAtFixedRate(() -> {
-            if (mIsAdvertising) {
-                mAdvertiser.startAdvertising(getServiceData());
-            }
-        }, ADVERTISING_REFRESH_DELAY_1_MIN, ADVERTISING_REFRESH_DELAY_1_MIN, TimeUnit.MILLISECONDS);
-    }
-
-    public void destroy() {
-        try {
-            mLogger.log("Destroying simulator");
-            mIsDestroyed = true;
-            mContext.unregisterReceiver(mBroadcastReceiver);
-            mAdvertiser.stopAdvertising();
-            for (BluetoothGattServerHelper helper : mBluetoothGattServerHelpers.values()) {
-                helper.close();
-            }
-            stopRfcommServer();
-            mDeviceNameCallback = null;
-            mExecutor.shutdownNow();
-        } catch (IllegalArgumentException ignored) {
-            // Happens if you haven't given us permissions yet, so we didn't register the receiver.
-        }
-    }
-
-    public boolean isDestroyed() {
-        return mIsDestroyed;
-    }
-
-    @Nullable
-    public String getBluetoothAddress() {
-        return BluetoothAddress.encode(mBluetoothAddress.getBytes(ByteOrder.BIG_ENDIAN));
-    }
-
-    public boolean isAdvertising() {
-        return mIsAdvertising;
-    }
-
-    public void setIsAdvertising(boolean isAdvertising) {
-        if (this.mIsAdvertising != isAdvertising) {
-            this.mIsAdvertising = isAdvertising;
-            mOptions.getAdvertisingChangedCallback().onAdvertisingChanged(isAdvertising);
-        }
-    }
-
-    public void stopAdvertising() {
-        mAdvertiser.stopAdvertising();
-    }
-
-    public void setBleAddress(String bleAddress) {
-        this.mBleAddress = bleAddress;
-        if (mOptions.getDataOnlyConnection()) {
-            mBluetoothAddress = new Value(BluetoothAddress.decode(bleAddress),
-                    ByteOrder.BIG_ENDIAN);
-            start(bleAddress);
-        }
-        // When BLE address changes, needs to send BLE address to the client again.
-        sendDeviceBleAddress(bleAddress);
-
-        // If we are advertising something other than the model id (e.g. the bloom filter), restart
-        // the advertisement so that it is updated with the new address.
-        if (isAdvertising() && !isDiscoverable()) {
-            mAdvertiser.startAdvertising(getServiceData());
-        }
-    }
-
-    @Nullable
-    public String getBleAddress() {
-        return mBleAddress;
-    }
-
-    // This method is only for testing to make test block until write name success or time out.
-    @VisibleForTesting
-    public void setCountDownLatchToWriteName(CountDownLatch countDownLatch) {
-        mLogger.log("Set up count down latch to write device name.");
-        mWriteNameCountDown = countDownLatch;
-    }
-
-    public boolean areBeaconActionsNotificationsEnabled() {
-        return mBeaconActionsServlet.areNotificationsEnabled();
-    }
-
-    private abstract class NotifiableGattServlet extends BluetoothGattServlet {
-        private final Map<BluetoothGattServerConnection, Notifier> mConnections = new HashMap<>();
-
-        abstract BluetoothGattCharacteristic getBaseCharacteristic();
-
-        @Override
-        public BluetoothGattCharacteristic getCharacteristic() {
-            // Enabling indication requires the Client Characteristic Configuration descriptor.
-            BluetoothGattCharacteristic characteristic = getBaseCharacteristic();
-            characteristic.addDescriptor(
-                    new BluetoothGattDescriptor(
-                            Constants.CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID,
-                            BluetoothGattDescriptor.PERMISSION_READ
-                                    | BluetoothGattDescriptor.PERMISSION_WRITE));
-            return characteristic;
-        }
-
-        @Override
-        public void enableNotification(BluetoothGattServerConnection connection, Notifier notifier)
-                throws BluetoothGattException {
-            mLogger.log("Registering notifier for %s", getCharacteristic());
-            mConnections.put(connection, notifier);
-        }
-
-        @Override
-        public void disableNotification(BluetoothGattServerConnection connection, Notifier notifier)
-                throws BluetoothGattException {
-            mLogger.log("Removing notifier for %s", getCharacteristic());
-            mConnections.remove(connection);
-        }
-
-        boolean areNotificationsEnabled() {
-            return !mConnections.isEmpty();
-        }
-
-        void sendNotification(byte[] data) {
-            if (mConnections.isEmpty()) {
-                mLogger.log("Not sending notify as no notifier registered");
-                return;
-            }
-            // Needs to be on a separate thread to avoid deadlocking and timing out (waits for a
-            // callback from OS, which happens on the main thread).
-            mExecutor.execute(
-                    () -> {
-                        for (Map.Entry<BluetoothGattServerConnection, Notifier> entry :
-                                mConnections.entrySet()) {
-                            try {
-                                mLogger.log("Sending notify %s to %s",
-                                        getCharacteristic(),
-                                        entry.getKey().getDevice().getAddress());
-                                entry.getValue().notify(data);
-                            } catch (BluetoothException e) {
-                                mLogger.log(
-                                        e,
-                                        "Failed to notify (indicate) result of %s to %s",
-                                        getCharacteristic(),
-                                        entry.getKey().getDevice().getAddress());
-                            }
-                        }
-                    });
-        }
-    }
-
-    private void startRfcommServer() {
-        mRfcommServer.setRequestHandler(this::handleRfcommServerRequest);
-        mRfcommServer.setStateMonitor(state -> {
-            mLogger.log("RfcommServer is in %s state", state);
-            if (CONNECTED.equals(state)) {
-                sendModelId();
-                sendDeviceBleAddress(mBleAddress);
-                sendFirmwareVersion();
-                sendSessionNonce();
-            }
-        });
-        mRfcommServer.start();
-    }
-
-    private void handleRfcommServerRequest(int eventGroup, int eventCode, byte[] data) {
-        switch (eventGroup) {
-            case EventGroup.DEVICE_VALUE:
-                if (data == null) {
-                    break;
-                }
-
-                String deviceValue = base16().encode(data);
-                if (eventCode == DeviceEventCode.DEVICE_CAPABILITY_VALUE) {
-                    mLogger.log("Received phone capability: %s", deviceValue);
-                } else if (eventCode == DeviceEventCode.PLATFORM_TYPE_VALUE) {
-                    mLogger.log("Received platform type: %s", deviceValue);
-                }
-                break;
-            case EventGroup.DEVICE_ACTION_VALUE:
-                if (eventCode == DeviceActionEventCode.DEVICE_ACTION_RING_VALUE) {
-                    mLogger.log("receive device action with ring value, data = %d",
-                            data[0]);
-                    sendDeviceRingActionResponse();
-                    // Simulate notifying the seeker that the ringing has stopped due
-                    // to user interaction (such as tapping the bud).
-                    mUiThreadHandler.postDelayed(this::sendDeviceRingStoppedAction,
-                            5000);
-                }
-                break;
-            case EventGroup.DEVICE_CONFIGURATION_VALUE:
-                if (eventCode == DeviceConfigurationEventCode.CONFIGURATION_BUFFER_SIZE_VALUE) {
-                    mLogger.log(
-                            "receive device action with buffer size value, data = %s",
-                            base16().encode(data));
-                    sendSetBufferActionResponse(data);
-                }
-                break;
-            case EventGroup.DEVICE_CAPABILITY_SYNC_VALUE:
-                if (eventCode == DeviceCapabilitySyncEventCode.REQUEST_CAPABILITY_UPDATE_VALUE) {
-                    mLogger.log("receive device capability update request.");
-                    sendCapabilitySync();
-                }
-                break;
-            default: // fall out
-                break;
-        }
-    }
-
-    private void stopRfcommServer() {
-        mRfcommServer.stop();
-        mRfcommServer.setRequestHandler(null);
-        mRfcommServer.setStateMonitor(null);
-    }
-
-    private void sendModelId() {
-        mLogger.log("Send model ID to the client");
-        mRfcommServer.send(
-                EventGroup.DEVICE_VALUE,
-                DeviceEventCode.DEVICE_MODEL_ID_VALUE,
-                modelIdServiceData(/* forAdvertising= */ false));
-    }
-
-    private void sendDeviceBleAddress(String bleAddress) {
-        mLogger.log("Send BLE address (%s) to the client", bleAddress);
-        if (bleAddress != null) {
-            mRfcommServer.send(
-                    EventGroup.DEVICE_VALUE,
-                    DeviceEventCode.DEVICE_BLE_ADDRESS_VALUE,
-                    BluetoothAddress.decode(bleAddress));
-        }
-    }
-
-    private void sendFirmwareVersion() {
-        mLogger.log("Send Firmware Version (%s) to the client", mDeviceFirmwareVersion);
-        mRfcommServer.send(
-                EventGroup.DEVICE_VALUE,
-                DeviceEventCode.FIRMWARE_VERSION_VALUE,
-                mDeviceFirmwareVersion.getBytes());
-    }
-
-    private void sendSessionNonce() {
-        mLogger.log("Send SessionNonce (%s) to the client", mDeviceFirmwareVersion);
-        SecureRandom secureRandom = new SecureRandom();
-        mSessionNonce = new byte[SECTION_NONCE_LENGTH];
-        secureRandom.nextBytes(mSessionNonce);
-        mRfcommServer.send(
-                EventGroup.DEVICE_VALUE, DeviceEventCode.SECTION_NONCE_VALUE, mSessionNonce);
-    }
-
-    private void sendDeviceRingActionResponse() {
-        mLogger.log("Send device ring action response to the client");
-        mRfcommServer.send(
-                EventGroup.ACKNOWLEDGEMENT_VALUE,
-                AcknowledgementEventCode.ACKNOWLEDGEMENT_ACK_VALUE,
-                new byte[]{
-                        EventGroup.DEVICE_ACTION_VALUE,
-                        DeviceActionEventCode.DEVICE_ACTION_RING_VALUE
-                });
-    }
-
-    private void sendSetBufferActionResponse(byte[] data) {
-        boolean hmacPassed = false;
-        for (ByteString accountKey : getAccountKeys()) {
-            try {
-                if (MessageStreamHmacEncoder.verifyHmac(
-                        accountKey.toByteArray(), mSessionNonce, data)) {
-                    hmacPassed = true;
-                    mLogger.log("Buffer size data matches account key %s",
-                            base16().encode(accountKey.toByteArray()));
-                    break;
-                }
-            } catch (GeneralSecurityException e) {
-                // Ignore.
-            }
-        }
-        if (hmacPassed) {
-            mLogger.log("Send buffer size action response %s to the client", base16().encode(data));
-            mRfcommServer.send(
-                    EventGroup.ACKNOWLEDGEMENT_VALUE,
-                    AcknowledgementEventCode.ACKNOWLEDGEMENT_ACK_VALUE,
-                    new byte[]{
-                            EventGroup.DEVICE_CONFIGURATION_VALUE,
-                            DeviceConfigurationEventCode.CONFIGURATION_BUFFER_SIZE_VALUE,
-                            data[0],
-                            data[1],
-                            data[2]
-                    });
-        } else {
-            mLogger.log("No matched account key for sendSetBufferActionResponse");
-        }
-    }
-
-    private void sendCapabilitySync() {
-        mLogger.log("Send capability sync to the client");
-        if (mSupportDynamicBufferSize) {
-            mLogger.log("Send dynamic buffer size range to the client");
-            mRfcommServer.send(
-                    EventGroup.DEVICE_CAPABILITY_SYNC_VALUE,
-                    DeviceCapabilitySyncEventCode.CONFIGURABLE_BUFFER_SIZE_RANGE_VALUE,
-                    new byte[]{
-                            0x00, 0x01, (byte) 0xf4, 0x00, 0x64, 0x00, (byte) 0xc8,
-                            0x01, 0x00, (byte) 0xff, 0x00, 0x01, 0x00, (byte) 0x88,
-                            0x02, 0x01, (byte) 0xff, 0x01, 0x01, 0x01, (byte) 0x88,
-                            0x03, 0x02, (byte) 0xff, 0x02, 0x01, 0x02, (byte) 0x88,
-                            0x04, 0x03, (byte) 0xff, 0x03, 0x01, 0x03, (byte) 0x88
-                    });
-        }
-    }
-
-    private void sendDeviceRingStoppedAction() {
-        mLogger.log("Sending device ring stopped action to the client");
-        mRfcommServer.send(
-                EventGroup.DEVICE_ACTION_VALUE,
-                DeviceActionEventCode.DEVICE_ACTION_RING_VALUE,
-                // Additional data for stopping ringing on all components.
-                new byte[]{0x00});
-    }
-
-    private void startGattServer(BluetoothGattServerHelper helper) {
-        BluetoothGattServlet tdsControlPointServlet =
-                new NotifiableGattServlet() {
-                    @Override
-                    public BluetoothGattCharacteristic getBaseCharacteristic() {
-                        return new BluetoothGattCharacteristic(ControlPointCharacteristic.ID,
-                                PROPERTY_WRITE | PROPERTY_INDICATE, PERMISSION_WRITE);
-                    }
-
-                    @Override
-                    public void write(
-                            BluetoothGattServerConnection connection, int offset, byte[] value)
-                            throws BluetoothGattException {
-                        mLogger.log("Requested TDS Control Point write, value=%s",
-                                base16().encode(value));
-
-                        ResultCode resultCode = checkTdsControlPointRequest(value);
-                        if (resultCode == ResultCode.SUCCESS) {
-                            try {
-                                becomeDiscoverable();
-                            } catch (TimeoutException | InterruptedException e) {
-                                mLogger.log(e, "Failed to become discoverable");
-                                resultCode = ResultCode.OPERATION_FAILED;
-                            }
-                        }
-
-                        mLogger.log("Request complete, resultCode=%s", resultCode);
-
-                        mLogger.log("Sending TDS Control Point response indication");
-                        sendNotification(
-                                Bytes.concat(
-                                        new byte[]{
-                                                getTdsControlPointOpCode(value),
-                                                resultCode.mByteValue,
-                                        },
-                                        resultCode == ResultCode.SUCCESS
-                                                ? TDS_CONTROL_POINT_RESPONSE_PARAMETER
-                                                : new byte[0]));
-                    }
-                };
-
-        BluetoothGattServlet brHandoverDataServlet =
-                new BluetoothGattServlet() {
-
-                    @Override
-                    public BluetoothGattCharacteristic getCharacteristic() {
-                        return new BluetoothGattCharacteristic(BrHandoverDataCharacteristic.ID,
-                                PROPERTY_READ, PERMISSION_READ);
-                    }
-
-                    @Override
-                    public byte[] read(BluetoothGattServerConnection connection, int offset) {
-                        return Bytes.concat(
-                                new byte[]{BrHandoverDataCharacteristic.BR_EDR_FEATURES},
-                                mBluetoothAddress.getBytes(ByteOrder.LITTLE_ENDIAN),
-                                CLASS_OF_DEVICE.getBytes(ByteOrder.LITTLE_ENDIAN));
-                    }
-                };
-
-        BluetoothGattServlet bluetoothSigServlet =
-                new BluetoothGattServlet() {
-
-                    @Override
-                    public BluetoothGattCharacteristic getCharacteristic() {
-                        BluetoothGattCharacteristic characteristic =
-                                new BluetoothGattCharacteristic(
-                                        TransportDiscoveryService.BluetoothSigDataCharacteristic.ID,
-                                        0 /* no properties */,
-                                        0 /* no permissions */);
-
-                        if (mOptions.getIncludeTransportDataDescriptor()) {
-                            characteristic.addDescriptor(
-                                    new BluetoothGattDescriptor(
-                                            TransportDiscoveryService.BluetoothSigDataCharacteristic
-                                                    .BrTransportBlockDataDescriptor.ID,
-                                            BluetoothGattDescriptor.PERMISSION_READ));
-                        }
-                        return characteristic;
-                    }
-
-                    @Override
-                    public byte[] readDescriptor(
-                            BluetoothGattServerConnection connection,
-                            BluetoothGattDescriptor descriptor,
-                            int offset)
-                            throws BluetoothGattException {
-                        return transportDiscoveryData();
-                    }
-                };
-
-        BluetoothGattServlet accountKeyServlet =
-                new BluetoothGattServlet() {
-                    @Override
-                    // Simulating deprecated API {@code AccountKeyCharacteristic.ID} for testing.
-                    @SuppressWarnings("deprecation")
-                    public BluetoothGattCharacteristic getCharacteristic() {
-                        return new BluetoothGattCharacteristic(
-                                AccountKeyCharacteristic.CUSTOM_128_BIT_UUID,
-                                PROPERTY_WRITE,
-                                PERMISSION_WRITE);
-                    }
-
-                    @Override
-                    public void write(
-                            BluetoothGattServerConnection connection, int offset, byte[] value) {
-                        mLogger.log("Got value from account key servlet: %s",
-                                base16().encode(value));
-                        try {
-                            addAccountKey(AesEcbSingleBlockEncryption.decrypt(mSecret, value),
-                                    mPairingDevice);
-                        } catch (GeneralSecurityException e) {
-                            mLogger.log(e, "Failed to decrypt account key.");
-                        }
-                        mUiThreadHandler.post(
-                                () -> mAdvertiser.startAdvertising(accountKeysServiceData()));
-                    }
-                };
-
-        BluetoothGattServlet firmwareVersionServlet =
-                new BluetoothGattServlet() {
-                    @Override
-                    public BluetoothGattCharacteristic getCharacteristic() {
-                        return new BluetoothGattCharacteristic(
-                                FirmwareVersionCharacteristic.ID, PROPERTY_READ, PERMISSION_READ);
-                    }
-
-                    @Override
-                    public byte[] read(BluetoothGattServerConnection connection, int offset) {
-                        return mDeviceFirmwareVersion.getBytes();
-                    }
-                };
-
-        BluetoothGattServlet keyBasedPairingServlet =
-                new NotifiableGattServlet() {
-                    @Override
-                    // Simulating deprecated API {@code KeyBasedPairingCharacteristic.ID} for
-                    // testing.
-                    @SuppressWarnings("deprecation")
-                    public BluetoothGattCharacteristic getBaseCharacteristic() {
-                        return new BluetoothGattCharacteristic(
-                                KeyBasedPairingCharacteristic.CUSTOM_128_BIT_UUID,
-                                PROPERTY_WRITE | PROPERTY_INDICATE,
-                                PERMISSION_WRITE);
-                    }
-
-                    @Override
-                    public void write(
-                            BluetoothGattServerConnection connection, int offset, byte[] value) {
-                        mLogger.log("Requesting key based pairing handshake, value=%s",
-                                base16().encode(value));
-
-                        mSecret = null;
-                        byte[] seekerPublicAddress = null;
-                        if (value.length == AES_BLOCK_LENGTH) {
-
-                            for (ByteString key : getAccountKeys()) {
-                                byte[] candidateSecret = key.toByteArray();
-                                try {
-                                    seekerPublicAddress = handshake(candidateSecret, value);
-                                    mSecret = candidateSecret;
-                                    mIsSubsequentPair = true;
-                                    break;
-                                } catch (GeneralSecurityException e) {
-                                    mLogger.log(e, "Failed to decrypt with %s",
-                                            base16().encode(candidateSecret));
-                                }
-                            }
-                        } else if (value.length == AES_BLOCK_LENGTH + PUBLIC_KEY_LENGTH
-                                && mOptions.getAntiSpoofingPrivateKey() != null) {
-                            try {
-                                byte[] encryptedRequest = Arrays.copyOf(value, AES_BLOCK_LENGTH);
-                                byte[] receivedPublicKey =
-                                        Arrays.copyOfRange(value, AES_BLOCK_LENGTH, value.length);
-                                byte[] candidateSecret =
-                                        EllipticCurveDiffieHellmanExchange.create(
-                                                        mOptions.getAntiSpoofingPrivateKey())
-                                                .generateSecret(receivedPublicKey);
-                                seekerPublicAddress = handshake(candidateSecret, encryptedRequest);
-                                mSecret = candidateSecret;
-                            } catch (Exception e) {
-                                mLogger.log(
-                                        e,
-                                        "Failed to decrypt with anti-spoofing private key %s",
-                                        base16().encode(mOptions.getAntiSpoofingPrivateKey()));
-                            }
-                        } else {
-                            mLogger.log("Packet length invalid, %d", value.length);
-                            return;
-                        }
-
-                        if (mSecret == null) {
-                            mLogger.log("Couldn't find a usable key to decrypt with.");
-                            return;
-                        }
-
-                        mLogger.log("Found valid decryption key, %s", base16().encode(mSecret));
-                        byte[] salt = new byte[9];
-                        new Random().nextBytes(salt);
-                        try {
-                            byte[] data = concat(
-                                    new byte[]{KeyBasedPairingCharacteristic.Response.TYPE},
-                                    mBluetoothAddress.getBytes(ByteOrder.BIG_ENDIAN), salt);
-                            byte[] encryptedAddress = encrypt(mSecret, data);
-                            mLogger.log(
-                                    "Sending handshake response %s with size %d",
-                                    base16().encode(encryptedAddress), encryptedAddress.length);
-                            sendNotification(encryptedAddress);
-
-                            // Notify seeker for NameCharacteristic to get provider device name
-                            // when seeker request device name flag is true.
-                            if (mOptions.getEnableNameCharacteristic()
-                                    && mHandshakeRequest.requestDeviceName()) {
-                                byte[] encryptedResponse =
-                                        getDeviceNameInBytes() != null ? createEncryptedDeviceName()
-                                                : new byte[0];
-                                mLogger.log(
-                                        "Sending device name response %s with size %d",
-                                        base16().encode(encryptedResponse),
-                                        encryptedResponse.length);
-                                mDeviceNameServlet.sendNotification(encryptedResponse);
-                            }
-
-                            // Disconnects the current connection to allow the following pairing
-                            // request. Needs to be on a separate thread to avoid deadlocking and
-                            // timing out (waits for a callback from OS, which happens on this
-                            // thread).
-                            //
-                            // Note: The spec does not require you to disconnect from other
-                            // devices at this point.
-                            // If headphones support multiple simultaneous connections, they
-                            // should stay connected. But Android fails to pair with the new
-                            // device if we don't first disconnect from any other device.
-                            mLogger.log("Skip remove bond, value=%s",
-                                    mOptions.getRemoveAllDevicesDuringPairing());
-                            if (mOptions.getRemoveAllDevicesDuringPairing()
-                                    && mHandshakeRequest.getType()
-                                    == HandshakeRequest.Type.KEY_BASED_PAIRING_REQUEST
-                                    && !mHandshakeRequest.requestRetroactivePair()) {
-                                mExecutor.execute(() -> disconnectAllBondedDevices());
-                            }
-
-                            if (mHandshakeRequest.getType()
-                                    == HandshakeRequest.Type.KEY_BASED_PAIRING_REQUEST
-                                    && mHandshakeRequest.requestProviderInitialBonding()) {
-                                // Run on executor to ensure it doesn't happen until after the
-                                // notify (which tells the remote device what address to expect).
-                                String seekerPublicAddressString =
-                                        BluetoothAddress.encode(seekerPublicAddress);
-                                mExecutor.execute(() -> {
-                                    mLogger.log("Sending pairing request to %s",
-                                            seekerPublicAddressString);
-                                    mBluetoothAdapter.getRemoteDevice(
-                                            seekerPublicAddressString).createBond();
-                                });
-                            }
-                        } catch (GeneralSecurityException e) {
-                            mLogger.log(e, "Failed to notify of static mac address");
-                        }
-                    }
-
-                    @Nullable
-                    private byte[] handshake(byte[] key, byte[] encryptedPairingRequest)
-                            throws GeneralSecurityException {
-                        mHandshakeRequest = new HandshakeRequest(key, encryptedPairingRequest);
-
-                        byte[] decryptedAddress = mHandshakeRequest.getVerificationData();
-                        if (mBleAddress != null
-                                && Arrays.equals(decryptedAddress,
-                                BluetoothAddress.decode(mBleAddress))
-                                || Arrays.equals(decryptedAddress,
-                                mBluetoothAddress.getBytes(ByteOrder.BIG_ENDIAN))) {
-                            mLogger.log("Address matches: %s", base16().encode(decryptedAddress));
-                        } else {
-                            throw new GeneralSecurityException(
-                                    "Address (BLE or BR/EDR) is not correct: "
-                                            + base16().encode(decryptedAddress)
-                                            + ", "
-                                            + mBleAddress
-                                            + ", "
-                                            + getBluetoothAddress());
-                        }
-
-                        switch (mHandshakeRequest.getType()) {
-                            case KEY_BASED_PAIRING_REQUEST:
-                                return handleKeyBasedPairingRequest(mHandshakeRequest);
-                            case ACTION_OVER_BLE:
-                                return handleActionOverBleRequest(mHandshakeRequest);
-                            case UNKNOWN:
-                                // continue to throw the exception;
-                        }
-                        throw new GeneralSecurityException(
-                                "Type is not correct: " + mHandshakeRequest.getType());
-                    }
-
-                    @Nullable
-                    private byte[] handleKeyBasedPairingRequest(HandshakeRequest handshakeRequest)
-                            throws GeneralSecurityException {
-                        if (handshakeRequest.requestDiscoverable()) {
-                            mLogger.log("Requested discoverability");
-                            setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
-                        }
-
-                        mLogger.log(
-                                "KeyBasedPairing: initialBonding=%s, requestDeviceName=%s, "
-                                        + "retroactivePair=%s",
-                                handshakeRequest.requestProviderInitialBonding(),
-                                handshakeRequest.requestDeviceName(),
-                                handshakeRequest.requestRetroactivePair());
-
-                        byte[] seekerPublicAddress = null;
-                        if (handshakeRequest.requestProviderInitialBonding()
-                                || handshakeRequest.requestRetroactivePair()) {
-                            seekerPublicAddress = handshakeRequest.getSeekerPublicAddress();
-                            mLogger.log(
-                                    "Seeker sends BR/EDR address %s to provider",
-                                    BluetoothAddress.encode(seekerPublicAddress));
-                        }
-
-                        if (handshakeRequest.requestRetroactivePair()) {
-                            if (mBluetoothAdapter.getRemoteDevice(
-                                    seekerPublicAddress).getBondState()
-                                    != BluetoothDevice.BOND_BONDED) {
-                                throw new GeneralSecurityException(
-                                        "Address (BR/EDR) is not bonded: "
-                                                + BluetoothAddress.encode(seekerPublicAddress));
-                            }
-                        }
-
-                        return seekerPublicAddress;
-                    }
-
-                    @Nullable
-                    private byte[] handleActionOverBleRequest(HandshakeRequest handshakeRequest) {
-                        // TODO(wollohchou): implement action over ble request.
-                        if (handshakeRequest.requestDeviceAction()) {
-                            mLogger.log("Requesting action over BLE, device action");
-                        } else if (handshakeRequest.requestFollowedByAdditionalData()) {
-                            mLogger.log(
-                                    "Requesting action over BLE, followed by additional data, "
-                                            + "type:%s",
-                                    handshakeRequest.getAdditionalDataType());
-                        } else {
-                            mLogger.log("Requesting action over BLE");
-                        }
-                        return null;
-                    }
-
-                    /**
-                     * @return The encrypted device name from provider for seeker to use.
-                     */
-                    private byte[] createEncryptedDeviceName() throws GeneralSecurityException {
-                        byte[] deviceName = getDeviceNameInBytes();
-                        String providerName = new String(deviceName, StandardCharsets.UTF_8);
-                        mLogger.log(
-                                "Sending handshake response for device name %s with size %d",
-                                providerName, deviceName.length);
-                        return NamingEncoder.encodeNamingPacket(mSecret, providerName);
-                    }
-                };
-
-        mBeaconActionsServlet =
-                new NotifiableGattServlet() {
-                    private static final int GATT_ERROR_UNAUTHENTICATED = 0x80;
-                    private static final int GATT_ERROR_INVALID_VALUE = 0x81;
-                    private static final int NONCE_LENGTH = 8;
-                    private static final int ONE_TIME_AUTH_KEY_OFFSET = 2;
-                    private static final int ONE_TIME_AUTH_KEY_LENGTH = 8;
-                    private static final int IDENTITY_KEY_LENGTH = 32;
-                    private static final byte TRANSMISSION_POWER = 0;
-
-                    private final SecureRandom mRandom = new SecureRandom();
-                    private final MessageDigest mSha256;
-                    @Nullable
-                    private byte[] mLastNonce;
-                    @Nullable
-                    private ByteString mIdentityKey = mOptions.getEddystoneIdentityKey();
-
-                    {
-                        try {
-                            mSha256 = MessageDigest.getInstance("SHA-256");
-                            mSha256.reset();
-                        } catch (NoSuchAlgorithmException e) {
-                            throw new IllegalStateException(
-                                    "System missing SHA-256 implementation.", e);
-                        }
-                    }
-
-                    @Override
-                    // Simulating deprecated API {@code BeaconActionsCharacteristic.ID} for testing.
-                    @SuppressWarnings("deprecation")
-                    public BluetoothGattCharacteristic getBaseCharacteristic() {
-                        return new BluetoothGattCharacteristic(
-                                BeaconActionsCharacteristic.CUSTOM_128_BIT_UUID,
-                                PROPERTY_READ | PROPERTY_WRITE | PROPERTY_NOTIFY,
-                                PERMISSION_READ | PERMISSION_WRITE);
-                    }
-
-                    @Override
-                    public byte[] read(BluetoothGattServerConnection connection, int offset) {
-                        mLastNonce = new byte[NONCE_LENGTH];
-                        mRandom.nextBytes(mLastNonce);
-                        return mLastNonce;
-                    }
-
-                    @Override
-                    public void write(
-                            BluetoothGattServerConnection connection, int offset, byte[] value)
-                            throws BluetoothGattException {
-                        mLogger.log("Got value from beacon actions servlet: %s",
-                                base16().encode(value));
-                        if (value.length == 0) {
-                            mLogger.log("Packet length invalid, %d", value.length);
-                            throw new BluetoothGattException("Packet length invalid",
-                                    GATT_ERROR_INVALID_VALUE);
-                        }
-                        switch (value[0]) {
-                            case BeaconActionType.READ_BEACON_PARAMETERS:
-                                handleReadBeaconParameters(value);
-                                break;
-                            case BeaconActionType.READ_PROVISIONING_STATE:
-                                handleReadProvisioningState(value);
-                                break;
-                            case BeaconActionType.SET_EPHEMERAL_IDENTITY_KEY:
-                                handleSetEphemeralIdentityKey(value);
-                                break;
-                            case BeaconActionType.CLEAR_EPHEMERAL_IDENTITY_KEY:
-                            case BeaconActionType.READ_EPHEMERAL_IDENTITY_KEY:
-                            case BeaconActionType.RING:
-                            case BeaconActionType.READ_RINGING_STATE:
-                                throw new BluetoothGattException(
-                                        "Unimplemented beacon action",
-                                        BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-                            default:
-                                throw new BluetoothGattException(
-                                        "Unknown beacon action",
-                                        BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-                        }
-                    }
-
-                    private boolean verifyAccountKeyToken(byte[] value, boolean ownerOnly)
-                            throws BluetoothGattException {
-                        if (value.length < ONE_TIME_AUTH_KEY_LENGTH + ONE_TIME_AUTH_KEY_OFFSET) {
-                            mLogger.log("Packet length invalid, %d", value.length);
-                            throw new BluetoothGattException(
-                                    "Packet length invalid", GATT_ERROR_INVALID_VALUE);
-                        }
-                        byte[] hashedAccountKey =
-                                Arrays.copyOfRange(
-                                        value,
-                                        ONE_TIME_AUTH_KEY_OFFSET,
-                                        ONE_TIME_AUTH_KEY_LENGTH + ONE_TIME_AUTH_KEY_OFFSET);
-                        if (mLastNonce == null) {
-                            throw new BluetoothGattException(
-                                    "Nonce wasn't set", GATT_ERROR_UNAUTHENTICATED);
-                        }
-                        if (ownerOnly) {
-                            ByteString accountKey = getOwnerAccountKey();
-                            if (accountKey != null) {
-                                mSha256.update(accountKey.toByteArray());
-                                mSha256.update(mLastNonce);
-                                return Arrays.equals(
-                                        hashedAccountKey,
-                                        Arrays.copyOf(mSha256.digest(), ONE_TIME_AUTH_KEY_LENGTH));
-                            }
-                        } else {
-                            Set<ByteString> accountKeys = getAccountKeys();
-                            for (ByteString accountKey : accountKeys) {
-                                mSha256.update(accountKey.toByteArray());
-                                mSha256.update(mLastNonce);
-                                if (Arrays.equals(
-                                        hashedAccountKey,
-                                        Arrays.copyOf(mSha256.digest(),
-                                                ONE_TIME_AUTH_KEY_LENGTH))) {
-                                    return true;
-                                }
-                            }
-                        }
-                        return false;
-                    }
-
-                    private int getBeaconClock() {
-                        return (int) TimeUnit.MILLISECONDS.toSeconds(SystemClock.elapsedRealtime());
-                    }
-
-                    private ByteString fromBytes(byte... bytes) {
-                        return ByteString.copyFrom(bytes);
-                    }
-
-                    private byte[] intToByteArray(int value) {
-                        byte[] data = new byte[4];
-                        data[3] = (byte) value;
-                        data[2] = (byte) (value >>> 8);
-                        data[1] = (byte) (value >>> 16);
-                        data[0] = (byte) (value >>> 24);
-                        return data;
-                    }
-
-                    private void handleReadBeaconParameters(byte[] value)
-                            throws BluetoothGattException {
-                        if (!verifyAccountKeyToken(value, /* ownerOnly= */ false)) {
-                            throw new BluetoothGattException(
-                                    "failed to authenticate account key",
-                                    GATT_ERROR_UNAUTHENTICATED);
-                        }
-                        sendNotification(
-                                fromBytes(
-                                        (byte) BeaconActionType.READ_BEACON_PARAMETERS,
-                                        (byte) 5 /* data length */,
-                                        TRANSMISSION_POWER)
-                                        .concat(ByteString.copyFrom(
-                                                intToByteArray(getBeaconClock())))
-                                        .toByteArray());
-                    }
-
-                    private void handleReadProvisioningState(byte[] value)
-                            throws BluetoothGattException {
-                        if (!verifyAccountKeyToken(value, /* ownerOnly= */ false)) {
-                            throw new BluetoothGattException(
-                                    "failed to authenticate account key",
-                                    GATT_ERROR_UNAUTHENTICATED);
-                        }
-                        byte flags = 0;
-                        if (verifyAccountKeyToken(value, /* ownerOnly= */ true)) {
-                            flags |= (byte) (1 << 1);
-                        }
-                        if (mIdentityKey == null) {
-                            sendNotification(
-                                    fromBytes(
-                                            (byte) BeaconActionType.READ_PROVISIONING_STATE,
-                                            (byte) 1 /* data length */,
-                                            flags)
-                                            .toByteArray());
-                        } else {
-                            flags |= (byte) 1;
-                            sendNotification(
-                                    fromBytes(
-                                            (byte) BeaconActionType.READ_PROVISIONING_STATE,
-                                            (byte) 21 /* data length */,
-                                            flags)
-                                            .concat(
-                                                    E2eeCalculator.computeE2eeEid(
-                                                            mIdentityKey, /* exponent= */ 10,
-                                                            getBeaconClock()))
-                                            .toByteArray());
-                        }
-                    }
-
-                    private void handleSetEphemeralIdentityKey(byte[] value)
-                            throws BluetoothGattException {
-                        if (!verifyAccountKeyToken(value, /* ownerOnly= */ true)) {
-                            throw new BluetoothGattException(
-                                    "failed to authenticate owner account key",
-                                    GATT_ERROR_UNAUTHENTICATED);
-                        }
-                        if (value.length
-                                != ONE_TIME_AUTH_KEY_LENGTH + ONE_TIME_AUTH_KEY_OFFSET
-                                + IDENTITY_KEY_LENGTH) {
-                            mLogger.log("Packet length invalid, %d", value.length);
-                            throw new BluetoothGattException("Packet length invalid",
-                                    GATT_ERROR_INVALID_VALUE);
-                        }
-                        if (mIdentityKey != null) {
-                            throw new BluetoothGattException(
-                                    "Device is already provisioned as Eddystone",
-                                    GATT_ERROR_UNAUTHENTICATED);
-                        }
-                        mIdentityKey = Crypto.aesEcbNoPaddingDecrypt(
-                                ByteString.copyFrom(mOwnerAccountKey),
-                                ByteString.copyFrom(value)
-                                        .substring(ONE_TIME_AUTH_KEY_LENGTH
-                                                + ONE_TIME_AUTH_KEY_OFFSET));
-                    }
-                };
-
-        ServiceConfig fastPairServiceConfig =
-                new ServiceConfig()
-                        .addCharacteristic(accountKeyServlet)
-                        .addCharacteristic(keyBasedPairingServlet)
-                        .addCharacteristic(mPasskeyServlet)
-                        .addCharacteristic(firmwareVersionServlet);
-        if (mOptions.getEnableBeaconActionsCharacteristic()) {
-            fastPairServiceConfig.addCharacteristic(mBeaconActionsServlet);
-        }
-
-        BluetoothGattServerConfig config =
-                new BluetoothGattServerConfig()
-                        .addService(
-                                TransportDiscoveryService.ID,
-                                new ServiceConfig()
-                                        .addCharacteristic(tdsControlPointServlet)
-                                        .addCharacteristic(brHandoverDataServlet)
-                                        .addCharacteristic(bluetoothSigServlet))
-                        .addService(
-                                FastPairService.ID,
-                                mOptions.getEnableNameCharacteristic()
-                                        ? fastPairServiceConfig.addCharacteristic(
-                                        mDeviceNameServlet)
-                                        : fastPairServiceConfig);
-
-        mLogger.log(
-                "Starting GATT server, support name characteristic %b",
-                mOptions.getEnableNameCharacteristic());
-        try {
-            helper.open(config);
-        } catch (BluetoothException e) {
-            mLogger.log(e, "Error starting GATT server");
-        }
-    }
-
-    /** Callback for passkey/pin input. */
-    public interface KeyInputCallback {
-        void onKeyInput(int key);
-    }
-
-    public void enterPassKey(int passkey) {
-        mLogger.log("enterPassKey called with passkey %d.", passkey);
-        mPairingDevice.setPairingConfirmation(true);
-    }
-
-    private void checkPasskey() {
-        // There's a race between the PAIRING_REQUEST broadcast from the OS giving us the local
-        // passkey, and the remote passkey received over GATT. Skip the check until we have both.
-        if (mLocalPasskey == 0 || mRemotePasskey == 0) {
-            mLogger.log(
-                    "Skipping passkey check, missing local (%s) or remote (%s).",
-                    mLocalPasskey, mRemotePasskey);
-            return;
-        }
-
-        // Regardless of whether it matches, send our (encrypted) passkey to the seeker.
-        sendPasskeyToRemoteDevice(mLocalPasskey);
-
-        mLogger.log("Checking localPasskey %s == remotePasskey %s", mLocalPasskey, mRemotePasskey);
-        boolean passkeysMatched = mLocalPasskey == mRemotePasskey;
-        if (mOptions.getShowsPasskeyConfirmation() && passkeysMatched
-                && mPasskeyEventCallback != null) {
-            mLogger.log("callbacks the UI for passkey confirmation.");
-            mPasskeyEventCallback.onPasskeyConfirmation(mLocalPasskey,
-                    this::setPasskeyConfirmation);
-        } else {
-            setPasskeyConfirmation(passkeysMatched);
-        }
-    }
-
-    private void sendPasskeyToRemoteDevice(int passkey) {
-        try {
-            mPasskeyServlet.sendNotification(
-                    PasskeyCharacteristic.encrypt(
-                            PasskeyCharacteristic.Type.PROVIDER, mSecret, passkey));
-        } catch (GeneralSecurityException e) {
-            mLogger.log(e, "Failed to encrypt passkey response.");
-        }
-    }
-
-    public void setFirmwareVersion(String versionNumber) {
-        mDeviceFirmwareVersion = versionNumber;
-    }
-
-    public void setDynamicBufferSize(boolean support) {
-        if (mSupportDynamicBufferSize != support) {
-            mSupportDynamicBufferSize = support;
-            sendCapabilitySync();
-        }
-    }
-
-    @VisibleForTesting
-    void setPasskeyConfirmationCallback(PasskeyConfirmationCallback callback) {
-        this.mPasskeyConfirmationCallback = callback;
-    }
-
-    public void setDeviceNameCallback(DeviceNameCallback callback) {
-        this.mDeviceNameCallback = callback;
-    }
-
-    public void setPasskeyEventCallback(PasskeyEventCallback passkeyEventCallback) {
-        this.mPasskeyEventCallback = passkeyEventCallback;
-    }
-
-    private void setPasskeyConfirmation(boolean confirm) {
-        mPairingDevice.setPairingConfirmation(confirm);
-        if (mPasskeyConfirmationCallback != null) {
-            mPasskeyConfirmationCallback.onPasskeyConfirmation(confirm);
-        }
-        mLocalPasskey = 0;
-        mRemotePasskey = 0;
-    }
-
-    private void becomeDiscoverable() throws InterruptedException, TimeoutException {
-        setDiscoverable(true);
-    }
-
-    public void cancelDiscovery() throws InterruptedException, TimeoutException {
-        setDiscoverable(false);
-    }
-
-    private void setDiscoverable(boolean discoverable)
-            throws InterruptedException, TimeoutException {
-        mIsDiscoverableLatch = new CountDownLatch(1);
-        setScanMode(discoverable ? SCAN_MODE_CONNECTABLE_DISCOVERABLE : SCAN_MODE_CONNECTABLE);
-        // If we're already discoverable, count down the latch right away. Otherwise,
-        // we'll get a broadcast when we successfully become discoverable.
-        if (isDiscoverable()) {
-            mIsDiscoverableLatch.countDown();
-        }
-        if (mIsDiscoverableLatch.await(BECOME_DISCOVERABLE_TIMEOUT_SEC, TimeUnit.SECONDS)) {
-            mLogger.log("Successfully became switched discoverable mode %s", discoverable);
-        } else {
-            throw new TimeoutException();
-        }
-    }
-
-    private void setScanMode(int scanMode) {
-        if (mRevertDiscoverableFuture != null) {
-            mRevertDiscoverableFuture.cancel(false /* may interrupt if running */);
-        }
-
-        mLogger.log("Setting scan mode to %s", scanModeToString(scanMode));
-        try {
-            Method method = mBluetoothAdapter.getClass().getMethod("setScanMode", Integer.TYPE);
-            method.invoke(mBluetoothAdapter, scanMode);
-
-            if (scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
-                mRevertDiscoverableFuture =
-                        mExecutor.schedule(() -> setScanMode(SCAN_MODE_CONNECTABLE),
-                                SCAN_MODE_REFRESH_SEC, TimeUnit.SECONDS);
-            }
-        } catch (Exception e) {
-            mLogger.log(e, "Error setting scan mode to %d", scanMode);
-        }
-    }
-
-    public static String scanModeToString(int scanMode) {
-        switch (scanMode) {
-            case SCAN_MODE_CONNECTABLE_DISCOVERABLE:
-                return "DISCOVERABLE";
-            case SCAN_MODE_CONNECTABLE:
-                return "CONNECTABLE";
-            case SCAN_MODE_NONE:
-                return "NOT CONNECTABLE";
-            default:
-                return "UNKNOWN(" + scanMode + ")";
-        }
-    }
-
-    private ResultCode checkTdsControlPointRequest(byte[] request) {
-        if (request.length < 2) {
-            mLogger.log(
-                    new IllegalArgumentException(), "Expected length >= 2 for %s",
-                    base16().encode(request));
-            return ResultCode.INVALID_PARAMETER;
-        }
-        byte opCode = getTdsControlPointOpCode(request);
-        if (opCode != ControlPointCharacteristic.ACTIVATE_TRANSPORT_OP_CODE) {
-            mLogger.log(
-                    new IllegalArgumentException(),
-                    "Expected Activate Transport op code (0x01), got %d",
-                    opCode);
-            return ResultCode.OP_CODE_NOT_SUPPORTED;
-        }
-        if (request[1] != BLUETOOTH_SIG_ORGANIZATION_ID) {
-            mLogger.log(
-                    new IllegalArgumentException(),
-                    "Expected Bluetooth SIG organization ID (0x01), got %d",
-                    request[1]);
-            return ResultCode.UNSUPPORTED_ORGANIZATION_ID;
-        }
-        return ResultCode.SUCCESS;
-    }
-
-    private static byte getTdsControlPointOpCode(byte[] request) {
-        return request.length < 1 ? 0x00 : request[0];
-    }
-
-    private boolean isDiscoverable() {
-        return mBluetoothAdapter.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
-    }
-
-    private byte[] modelIdServiceData(boolean forAdvertising) {
-        // Note: This used to be little-endian but is now big-endian. See b/78229467 for details.
-        byte[] modelIdPacket =
-                base16().decode(
-                        forAdvertising ? mOptions.getAdvertisingModelId() : mOptions.getModelId());
-        if (!mBatteryValues.isEmpty()) {
-            // If we are going to advertise battery values with the packet, then switch to the
-            // non-3-byte model ID format.
-            modelIdPacket = concat(new byte[]{0b00000110}, modelIdPacket);
-        }
-        return modelIdPacket;
-    }
-
-    private byte[] accountKeysServiceData() {
-        try {
-            return concat(new byte[]{0x00}, generateBloomFilterFields());
-        } catch (NoSuchAlgorithmException e) {
-            throw new IllegalStateException("Unable to build bloom filter.", e);
-        }
-    }
-
-    private byte[] transportDiscoveryData() {
-        byte[] transportData = SUPPORTED_SERVICES_LTV;
-        return Bytes.concat(
-                new byte[]{BLUETOOTH_SIG_ORGANIZATION_ID},
-                new byte[]{tdsFlags(isDiscoverable() ? TransportState.ON : TransportState.OFF)},
-                new byte[]{(byte) transportData.length},
-                transportData);
-    }
-
-    private byte[] generateBloomFilterFields() throws NoSuchAlgorithmException {
-        Set<ByteString> accountKeys = getAccountKeys();
-        if (accountKeys.isEmpty()) {
-            return new byte[0];
-        }
-        BloomFilter bloomFilter =
-                new BloomFilter(
-                        new byte[(int) (1.2 * accountKeys.size()) + 3],
-                        new FastPairBloomFilterHasher());
-        String address = mBleAddress == null ? SIMULATOR_FAKE_BLE_ADDRESS : mBleAddress;
-
-        // Simulator supports Central Address Resolution characteristic, so when paired, the BLE
-        // address in Seeker will be resolved to BR/EDR address. This caused Seeker fails on
-        // checking the bloom filter due to different address is used for salting. In order to
-        // let battery values notification be shown on paired device, we use random salt to
-        // workaround it.
-        boolean advertisingBatteryValues = !mBatteryValues.isEmpty();
-        byte[] salt;
-        if (mOptions.getUseRandomSaltForAccountKeyRotation() || advertisingBatteryValues) {
-            salt = new byte[1];
-            new SecureRandom().nextBytes(salt);
-            mLogger.log("Using random salt %s for bloom filter", base16().encode(salt));
-        } else {
-            salt = BluetoothAddress.decode(address);
-            mLogger.log("Using address %s for bloom filter", address);
-        }
-
-        // To prevent tampering, account filter shall be slightly modified to include battery data
-        // when the battery values are included in the advertisement. Normally, when building the
-        // account filter, a value V is produce by combining the account key with a salt. Instead,
-        // when battery values are also being advertised, it be constructed as follows:
-        // - the first 16 bytes are account key.
-        // - the next bytes are the salt.
-        // - the remaining bytes are the battery data.
-        byte[] saltAndBatteryData =
-                advertisingBatteryValues ? concat(salt, generateBatteryData()) : salt;
-
-        for (ByteString accountKey : accountKeys) {
-            bloomFilter.add(concat(accountKey.toByteArray(), saltAndBatteryData));
-        }
-        byte[] packet = generateAccountKeyData(bloomFilter);
-        return mOptions.getUseRandomSaltForAccountKeyRotation() || advertisingBatteryValues
-                // Create a header with length 1 and type 1 for a random salt.
-                ? concat(packet, createField((byte) 0x11, salt))
-                // Exclude the salt from the packet, BLE address will be assumed by the client.
-                : packet;
-    }
-
-    /**
-     * Creates a new field for the packet.
-     *
-     * The header is formatted 0xLLLLTTTT where LLLL is the
-     * length of the field and TTTT is the type (0 for bloom filter, 1 for salt).
-     */
-    private byte[] createField(byte header, byte[] value) {
-        return concat(new byte[]{header}, value);
-    }
-
-    public int getTxPower() {
-        return mOptions.getTxPowerLevel();
-    }
-
-    @Nullable
-    byte[] getServiceData() {
-        byte[] packet =
-                isDiscoverable()
-                        ? modelIdServiceData(/* forAdvertising= */ true)
-                        : !getAccountKeys().isEmpty() ? accountKeysServiceData() : null;
-        return addBatteryValues(packet);
-    }
-
-    @Nullable
-    private byte[] addBatteryValues(byte[] packet) {
-        if (mBatteryValues.isEmpty() || packet == null) {
-            return packet;
-        }
-
-        return concat(packet, generateBatteryData());
-    }
-
-    private byte[] generateBatteryData() {
-        // Byte 0: Battery length and type, first 4 bits are the number of battery values, second
-        // 4 are the type.
-        // Byte 1 - length: Battery values, the first bit is charging status, the remaining bits are
-        // the actual value between 0 and 100, or -1 for unknown.
-        byte[] batteryData = new byte[mBatteryValues.size() + 1];
-        batteryData[0] = (byte) (mBatteryValues.size() << 4
-                | (mSuppressBatteryNotification ? 0b0100 : 0b0011));
-
-        int batteryValueIndex = 1;
-        for (BatteryValue batteryValue : mBatteryValues) {
-            batteryData[batteryValueIndex++] =
-                    (byte)
-                            ((batteryValue.mCharging ? 0b10000000 : 0b00000000)
-                                    | (0b01111111 & batteryValue.mLevel));
-        }
-
-        return batteryData;
-    }
-
-    private byte[] generateAccountKeyData(BloomFilter bloomFilter) {
-        // Byte 0: length and type, first 4 bits are the length of bloom filter, second 4 are the
-        // type which indicating the subsequent pairing notification is suppressed or not.
-        // The following bytes are the data of bloom filter.
-        byte[] filterBytes = bloomFilter.asBytes();
-        byte lengthAndType = (byte) (filterBytes.length << 4
-                | (mSuppressSubsequentPairingNotification ? 0b0010 : 0b0000));
-        mLogger.log(
-                "Generate bloom filter with suppress subsequent pairing notification:%b",
-                mSuppressSubsequentPairingNotification);
-        return createField(lengthAndType, filterBytes);
-    }
-
-    /** Disconnects all bonded devices. */
-    public void disconnectAllBondedDevices() {
-        for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) {
-            if (device.getBluetoothClass().getMajorDeviceClass() == Major.PHONE) {
-                removeBond(device);
-            }
-        }
-    }
-
-    public void disconnect(BluetoothProfile profile, BluetoothDevice device) {
-        device.disconnect();
-    }
-
-    public void removeBond(BluetoothDevice device) {
-        device.removeBond();
-    }
-
-    public void resetAccountKeys() {
-        mFastPairSimulatorDatabase.setAccountKeys(new HashSet<>());
-        mFastPairSimulatorDatabase.setFastPairSeekerDevices(new HashSet<>());
-        mAccountKey = null;
-        mOwnerAccountKey = null;
-        mLogger.log("Remove all account keys");
-    }
-
-    public void addAccountKey(byte[] key) {
-        addAccountKey(key, /* device= */ null);
-    }
-
-    private void addAccountKey(byte[] key, @Nullable BluetoothDevice device) {
-        mAccountKey = key;
-        if (mOwnerAccountKey == null) {
-            mOwnerAccountKey = key;
-        }
-
-        mFastPairSimulatorDatabase.addAccountKey(key);
-        mFastPairSimulatorDatabase.addFastPairSeekerDevice(device, key);
-        mLogger.log("Add account key: key=%s, device=%s", base16().encode(key), device);
-    }
-
-    private Set<ByteString> getAccountKeys() {
-        return mFastPairSimulatorDatabase.getAccountKeys();
-    }
-
-    /** Get the latest account key. */
-    @Nullable
-    public ByteString getAccountKey() {
-        if (mAccountKey == null) {
-            return null;
-        }
-        return ByteString.copyFrom(mAccountKey);
-    }
-
-    /** Get the owner account key (the first account key registered). */
-    @Nullable
-    public ByteString getOwnerAccountKey() {
-        if (mOwnerAccountKey == null) {
-            return null;
-        }
-        return ByteString.copyFrom(mOwnerAccountKey);
-    }
-
-    public void resetDeviceName() {
-        mFastPairSimulatorDatabase.setLocalDeviceName(null);
-        // Trigger simulator to update device name text view.
-        if (mDeviceNameCallback != null) {
-            mDeviceNameCallback.onNameChanged(getDeviceName());
-        }
-    }
-
-    // This method is used in test case with default name in provider.
-    public void setDeviceName(String deviceName) {
-        setDeviceName(deviceName.getBytes(StandardCharsets.UTF_8));
-    }
-
-    private void setDeviceName(@Nullable byte[] deviceName) {
-        mFastPairSimulatorDatabase.setLocalDeviceName(deviceName);
-
-        mLogger.log("Save device name : %s", getDeviceName());
-        // Trigger simulator to update device name text view.
-        if (mDeviceNameCallback != null) {
-            mDeviceNameCallback.onNameChanged(getDeviceName());
-        }
-    }
-
-    @Nullable
-    private byte[] getDeviceNameInBytes() {
-        return mFastPairSimulatorDatabase.getLocalDeviceName();
-    }
-
-    @Nullable
-    public String getDeviceName() {
-        String providerDeviceName =
-                getDeviceNameInBytes() != null
-                        ? new String(getDeviceNameInBytes(), StandardCharsets.UTF_8)
-                        : null;
-        mLogger.log("get device name = %s", providerDeviceName);
-        return providerDeviceName;
-    }
-
-    /**
-     * Bit index: Description - Value
-     *
-     * <ul>
-     *   <li>0-1: Role - 0b10 (Provider only)
-     *   <li>2: Transport Data Incomplete: 0 (false)
-     *   <li>3-4: Transport State (0b00: Off, 0b01: On, 0b10: Temporarily Unavailable)
-     *   <li>5-7: Reserved for future use
-     * </ul>
-     */
-    private static byte tdsFlags(TransportState transportState) {
-        return (byte) (0b00000010 & (transportState.mByteValue << 3));
-    }
-
-    /** Detailed information about battery value. */
-    public static class BatteryValue {
-        boolean mCharging;
-
-        // The range is 0 ~ 100, and -1 represents the battery level is unknown.
-        int mLevel;
-
-        public BatteryValue(boolean charging, int level) {
-            this.mCharging = charging;
-            this.mLevel = level;
-        }
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulatorDatabase.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulatorDatabase.java
deleted file mode 100644
index cbe39ff..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulatorDatabase.java
+++ /dev/null
@@ -1,264 +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.nearby.fastpair.provider;
-
-import static com.google.common.io.BaseEncoding.base16;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.content.Context;
-import android.content.SharedPreferences;
-
-import androidx.annotation.Nullable;
-
-import com.google.protobuf.ByteString;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.StringTokenizer;
-
-/** Stores fast pair related information for each paired device */
-public class FastPairSimulatorDatabase {
-
-    private static final String SHARED_PREF_NAME =
-            "android.nearby.fastpair.provider.fastpairsimulator";
-    private static final String KEY_DEVICE_NAME = "DEVICE_NAME";
-    private static final String KEY_ACCOUNT_KEYS = "ACCOUNT_KEYS";
-    private static final int MAX_NUMBER_OF_ACCOUNT_KEYS = 8;
-
-    // [for SASS]
-    private static final String KEY_FAST_PAIR_SEEKER_DEVICE = "FAST_PAIR_SEEKER_DEVICE";
-
-    private final SharedPreferences mSharedPreferences;
-
-    public FastPairSimulatorDatabase(Context context) {
-        mSharedPreferences = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
-    }
-
-    /** Adds single account key. */
-    public void addAccountKey(byte[] accountKey) {
-        if (mSharedPreferences == null) {
-            return;
-        }
-
-        Set<ByteString> accountKeys = new HashSet<>(getAccountKeys());
-        if (accountKeys.size() >= MAX_NUMBER_OF_ACCOUNT_KEYS) {
-            Set<ByteString> removedKeys = new HashSet<>();
-            int removedCount = accountKeys.size() - MAX_NUMBER_OF_ACCOUNT_KEYS + 1;
-            for (ByteString key : accountKeys) {
-                if (removedKeys.size() == removedCount) {
-                    break;
-                }
-                removedKeys.add(key);
-            }
-
-            accountKeys.removeAll(removedKeys);
-        }
-
-        // Just make sure the newest key will not be removed.
-        accountKeys.add(ByteString.copyFrom(accountKey));
-        setAccountKeys(accountKeys);
-    }
-
-    /** Sets account keys, overrides all. */
-    public void setAccountKeys(Set<ByteString> accountKeys) {
-        if (mSharedPreferences == null) {
-            return;
-        }
-
-        Set<String> keys = new HashSet<>();
-        for (ByteString item : accountKeys) {
-            keys.add(base16().encode(item.toByteArray()));
-        }
-
-        mSharedPreferences.edit().putStringSet(KEY_ACCOUNT_KEYS, keys).apply();
-    }
-
-    /** Gets all account keys. */
-    public Set<ByteString> getAccountKeys() {
-        if (mSharedPreferences == null) {
-            return new HashSet<>();
-        }
-
-        Set<String> keys = mSharedPreferences.getStringSet(KEY_ACCOUNT_KEYS, new HashSet<>());
-        Set<ByteString> accountKeys = new HashSet<>();
-        // Add new account keys one by one.
-        for (String key : keys) {
-            accountKeys.add(ByteString.copyFrom(base16().decode(key)));
-        }
-
-        return accountKeys;
-    }
-
-    /** Sets local device name. */
-    public void setLocalDeviceName(byte[] deviceName) {
-        if (mSharedPreferences == null) {
-            return;
-        }
-
-        String humanReadableName = deviceName != null ? new String(deviceName, UTF_8) : null;
-        if (humanReadableName == null) {
-            mSharedPreferences.edit().remove(KEY_DEVICE_NAME).apply();
-        } else {
-            mSharedPreferences.edit().putString(KEY_DEVICE_NAME, humanReadableName).apply();
-        }
-    }
-
-    /** Gets local device name. */
-    @Nullable
-    public byte[] getLocalDeviceName() {
-        if (mSharedPreferences == null) {
-            return null;
-        }
-
-        String deviceName = mSharedPreferences.getString(KEY_DEVICE_NAME, null);
-        return deviceName != null ? deviceName.getBytes(UTF_8) : null;
-    }
-
-    /**
-     * [for SASS] Adds seeker device info. <a
-     * href="http://go/smart-audio-source-switching-design">Sass design doc</a>
-     */
-    public void addFastPairSeekerDevice(@Nullable BluetoothDevice device, byte[] accountKey) {
-        if (mSharedPreferences == null) {
-            return;
-        }
-
-        if (device == null) {
-            return;
-        }
-
-        // When hitting size limitation, choose the existing items to delete.
-        Set<FastPairSeekerDevice> fastPairSeekerDevices = getFastPairSeekerDevices();
-        if (fastPairSeekerDevices.size() > MAX_NUMBER_OF_ACCOUNT_KEYS) {
-            int removedCount = fastPairSeekerDevices.size() - MAX_NUMBER_OF_ACCOUNT_KEYS + 1;
-            Set<FastPairSeekerDevice> removedFastPairDevices = new HashSet<>();
-            for (FastPairSeekerDevice fastPairDevice : fastPairSeekerDevices) {
-                if (removedFastPairDevices.size() == removedCount) {
-                    break;
-                }
-                removedFastPairDevices.add(fastPairDevice);
-            }
-            fastPairSeekerDevices.removeAll(removedFastPairDevices);
-        }
-
-        fastPairSeekerDevices.add(new FastPairSeekerDevice(device, accountKey));
-        setFastPairSeekerDevices(fastPairSeekerDevices);
-    }
-
-    /** [for SASS] Sets all seeker device info, overrides all. */
-    public void setFastPairSeekerDevices(Set<FastPairSeekerDevice> fastPairSeekerDeviceSet) {
-        if (mSharedPreferences == null) {
-            return;
-        }
-
-        Set<String> rawStringSet = new HashSet<>();
-        for (FastPairSeekerDevice item : fastPairSeekerDeviceSet) {
-            rawStringSet.add(item.toRawString());
-        }
-
-        mSharedPreferences.edit().putStringSet(KEY_FAST_PAIR_SEEKER_DEVICE, rawStringSet).apply();
-    }
-
-    /** [for SASS] Gets all seeker device info. */
-    public Set<FastPairSeekerDevice> getFastPairSeekerDevices() {
-        if (mSharedPreferences == null) {
-            return new HashSet<>();
-        }
-
-        Set<FastPairSeekerDevice> fastPairSeekerDevices = new HashSet<>();
-        Set<String> rawStringSet =
-                mSharedPreferences.getStringSet(KEY_FAST_PAIR_SEEKER_DEVICE, new HashSet<>());
-        for (String rawString : rawStringSet) {
-            FastPairSeekerDevice fastPairDevice = FastPairSeekerDevice.fromRawString(rawString);
-            if (fastPairDevice == null) {
-                continue;
-            }
-            fastPairSeekerDevices.add(fastPairDevice);
-        }
-
-        return fastPairSeekerDevices;
-    }
-
-    /** Defines data structure for the paired Fast Pair device. */
-    public static class FastPairSeekerDevice {
-        private static final int INDEX_DEVICE = 0;
-        private static final int INDEX_ACCOUNT_KEY = 1;
-
-        private final BluetoothDevice mDevice;
-        private final byte[] mAccountKey;
-
-        private FastPairSeekerDevice(BluetoothDevice device, byte[] accountKey) {
-            this.mDevice = device;
-            this.mAccountKey = accountKey;
-        }
-
-        public BluetoothDevice getBluetoothDevice() {
-            return mDevice;
-        }
-
-        public byte[] getAccountKey() {
-            return mAccountKey;
-        }
-
-        public String toRawString() {
-            return String.format("%s,%s", mDevice, base16().encode(mAccountKey));
-        }
-
-        /** Decodes the raw string if possible. */
-        @Nullable
-        public static FastPairSeekerDevice fromRawString(String rawString) {
-            BluetoothDevice device = null;
-            byte[] accountKey = null;
-            int step = INDEX_DEVICE;
-
-            StringTokenizer tokenizer = new StringTokenizer(rawString, ",");
-            while (tokenizer.hasMoreElements()) {
-                boolean shouldStop = false;
-                String token = tokenizer.nextToken();
-                switch (step) {
-                    case INDEX_DEVICE:
-                        try {
-                            device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(token);
-                        } catch (IllegalArgumentException e) {
-                            device = null;
-                        }
-                        break;
-                    case INDEX_ACCOUNT_KEY:
-                        accountKey = base16().decode(token);
-                        if (accountKey.length != 16) {
-                            accountKey = null;
-                        }
-                        break;
-                    default:
-                        shouldStop = true;
-                }
-
-                if (shouldStop) {
-                    break;
-                }
-                step++;
-            }
-            if (device != null && accountKey != null) {
-                return new FastPairSeekerDevice(device, accountKey);
-            }
-            return null;
-        }
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/HandshakeRequest.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/HandshakeRequest.java
deleted file mode 100644
index 9cfffd8..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/HandshakeRequest.java
+++ /dev/null
@@ -1,157 +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.nearby.fastpair.provider;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.decrypt;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.BLUETOOTH_ADDRESS_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.DEVICE_ACTION;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DISCOVERABLE;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.ADDITIONAL_DATA_TYPE_INDEX;
-
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request;
-
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-
-/**
- * A wrapper for Fast Pair Provider to access decoded handshake request from the Seeker.
- *
- * @see {go/fast-pair-early-spec-handshake}
- */
-public class HandshakeRequest {
-
-    /**
-     * 16 bytes data: 1-byte for type, 1-byte for flags, 6-bytes for provider's BLE address, 8 bytes
-     * optional data.
-     *
-     * @see {go/fast-pair-spec-handshake-message1}
-     */
-    private final byte[] mDecryptedMessage;
-
-    /** Enumerates the handshake message types. */
-    public enum Type {
-        KEY_BASED_PAIRING_REQUEST(Request.TYPE_KEY_BASED_PAIRING_REQUEST),
-        ACTION_OVER_BLE(Request.TYPE_ACTION_OVER_BLE),
-        UNKNOWN((byte) 0xFF);
-
-        private final byte mValue;
-
-        Type(byte type) {
-            mValue = type;
-        }
-
-        public byte getValue() {
-            return mValue;
-        }
-
-        public static Type valueOf(byte value) {
-            for (Type type : Type.values()) {
-                if (type.getValue() == value) {
-                    return type;
-                }
-            }
-            return UNKNOWN;
-        }
-    }
-
-    public HandshakeRequest(byte[] key, byte[] encryptedPairingRequest)
-            throws GeneralSecurityException {
-        mDecryptedMessage = decrypt(key, encryptedPairingRequest);
-    }
-
-    /**
-     * Gets the type of this handshake request. Currently, we have 2 types: 0x00 for Key-based
-     * Pairing Request and 0x10 for Action Request.
-     */
-    public Type getType() {
-        return Type.valueOf(mDecryptedMessage[Request.TYPE_INDEX]);
-    }
-
-    /**
-     * Gets verification data of this handshake request.
-     * Currently, we use Provider's BLE address.
-     */
-    public byte[] getVerificationData() {
-        return Arrays.copyOfRange(
-                mDecryptedMessage,
-                Request.VERIFICATION_DATA_INDEX,
-                Request.VERIFICATION_DATA_INDEX + Request.VERIFICATION_DATA_LENGTH);
-    }
-
-    /** Gets Seeker's public address of the handshake request. */
-    public byte[] getSeekerPublicAddress() {
-        return Arrays.copyOfRange(
-                mDecryptedMessage,
-                Request.SEEKER_PUBLIC_ADDRESS_INDEX,
-                Request.SEEKER_PUBLIC_ADDRESS_INDEX + BLUETOOTH_ADDRESS_LENGTH);
-    }
-
-    /** Checks whether the Seeker request discoverability from flags byte. */
-    public boolean requestDiscoverable() {
-        return (getFlags() & REQUEST_DISCOVERABLE) != 0;
-    }
-
-    /**
-     * Checks whether the Seeker requests that the Provider shall initiate bonding from flags byte.
-     */
-    public boolean requestProviderInitialBonding() {
-        return (getFlags() & PROVIDER_INITIATES_BONDING) != 0;
-    }
-
-    /** Checks whether the Seeker requests that the Provider shall notify the existing name. */
-    public boolean requestDeviceName() {
-        return (getFlags() & REQUEST_DEVICE_NAME) != 0;
-    }
-
-    /** Checks whether this is for retroactively writing account key. */
-    public boolean requestRetroactivePair() {
-        return (getFlags() & REQUEST_RETROACTIVE_PAIR) != 0;
-    }
-
-    /** Gets the flags of this handshake request. */
-    private byte getFlags() {
-        return mDecryptedMessage[Request.FLAGS_INDEX];
-    }
-
-    /** Checks whether the Seeker requests a device action. */
-    public boolean requestDeviceAction() {
-        return (getFlags() & DEVICE_ACTION) != 0;
-    }
-
-    /**
-     * Checks whether the Seeker requests an action which will be followed by an additional data
-     * .
-     */
-    public boolean requestFollowedByAdditionalData() {
-        return (getFlags() & ADDITIONAL_DATA_CHARACTERISTIC) != 0;
-    }
-
-    /** Gets the {@link AdditionalDataType} of this handshake request. */
-    @AdditionalDataType
-    public int getAdditionalDataType() {
-        if (!requestFollowedByAdditionalData()
-                || mDecryptedMessage.length <= ADDITIONAL_DATA_TYPE_INDEX) {
-            return AdditionalDataType.UNKNOWN;
-        }
-        return mDecryptedMessage[ADDITIONAL_DATA_TYPE_INDEX];
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java
deleted file mode 100644
index bc0cdfe..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java
+++ /dev/null
@@ -1,163 +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.nearby.fastpair.provider;
-
-import static com.google.common.io.BaseEncoding.base16;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.bluetooth.le.AdvertisingSet;
-import android.bluetooth.le.AdvertisingSetCallback;
-import android.bluetooth.le.AdvertisingSetParameters;
-import android.bluetooth.le.BluetoothLeAdvertiser;
-import android.nearby.fastpair.provider.utils.Logger;
-import android.os.ParcelUuid;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Locale;
-
-/** Fast Pair advertiser taking advantage of new Android Oreo advertising features. */
-public final class OreoFastPairAdvertiser implements FastPairAdvertiser {
-    private static final String TAG = "OreoFastPairAdvertiser";
-    private final Logger mLogger = new Logger(TAG);
-
-    private final FastPairSimulator mSimulator;
-    private final BluetoothLeAdvertiser mAdvertiser;
-    private final AdvertisingSetCallback mAdvertisingSetCallback;
-    private AdvertisingSet mAdvertisingSet;
-
-    public OreoFastPairAdvertiser(FastPairSimulator simulator) {
-        this.mSimulator = simulator;
-        this.mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
-        this.mAdvertisingSetCallback = new AdvertisingSetCallback() {
-
-            @Override
-            public void onAdvertisingSetStarted(
-                    AdvertisingSet set, int txPower, int status) {
-                if (status == AdvertisingSetCallback.ADVERTISE_SUCCESS) {
-                    mLogger.log("Advertising succeeded, advertising at %s dBm", txPower);
-                    simulator.setIsAdvertising(true);
-                    mAdvertisingSet = set;
-                    mAdvertisingSet.getOwnAddress();
-                } else {
-                    mLogger.log(
-                            new IllegalStateException(),
-                            "Advertising failed, error code=%d", status);
-                }
-            }
-
-            @Override
-            public void onAdvertisingDataSet(AdvertisingSet set, int status) {
-                if (status != AdvertisingSetCallback.ADVERTISE_SUCCESS) {
-                    mLogger.log(
-                            new IllegalStateException(),
-                            "Updating advertisement failed, error code=%d",
-                            status);
-                    stopAdvertising();
-                }
-            }
-
-            // Callback for AdvertisingSet.getOwnAddress().
-            @Override
-            public void onOwnAddressRead(
-                    AdvertisingSet set, int addressType, String address) {
-                if (!address.equals(simulator.getBleAddress())) {
-                    mLogger.log(
-                            "Read own BLE address=%s at %s",
-                            address,
-                            new SimpleDateFormat("HH:mm:ss:SSS", Locale.US)
-                                    .format(Calendar.getInstance().getTime()));
-                    // Implicitly start the advertising once BLE address callback arrived.
-                    simulator.setBleAddress(address);
-                }
-            }
-        };
-    }
-
-    @Override
-    public void startAdvertising(@Nullable byte[] serviceData) {
-        // To be informed that BLE address is rotated, we need to polling query it asynchronously.
-        if (mAdvertisingSet != null) {
-            mAdvertisingSet.getOwnAddress();
-        }
-
-        if (mSimulator.isDestroyed()) {
-            return;
-        }
-
-        if (serviceData == null) {
-            mLogger.log("Service data is null, stop advertising");
-            stopAdvertising();
-            return;
-        }
-
-        AdvertiseData data =
-                new AdvertiseData.Builder()
-                        .addServiceData(new ParcelUuid(FastPairService.ID), serviceData)
-                        .setIncludeTxPowerLevel(true)
-                        .build();
-
-        mLogger.log("Advertising FE2C service data=%s", base16().encode(serviceData));
-
-        if (mAdvertisingSet != null) {
-            mAdvertisingSet.setAdvertisingData(data);
-            return;
-        }
-
-        stopAdvertising();
-        AdvertisingSetParameters parameters =
-                new AdvertisingSetParameters.Builder()
-                        .setLegacyMode(true)
-                        .setConnectable(true)
-                        .setScannable(true)
-                        .setInterval(AdvertisingSetParameters.INTERVAL_LOW)
-                        .setTxPowerLevel(convertAdvertiseSettingsTxPower(mSimulator.getTxPower()))
-                        .build();
-        mAdvertiser.startAdvertisingSet(parameters, data, null, null, null,
-                mAdvertisingSetCallback);
-    }
-
-    private static int convertAdvertiseSettingsTxPower(int txPower) {
-        switch (txPower) {
-            case AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW:
-                return AdvertisingSetParameters.TX_POWER_ULTRA_LOW;
-            case AdvertiseSettings.ADVERTISE_TX_POWER_LOW:
-                return AdvertisingSetParameters.TX_POWER_LOW;
-            case AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM:
-                return AdvertisingSetParameters.TX_POWER_MEDIUM;
-            default:
-                return AdvertisingSetParameters.TX_POWER_HIGH;
-        }
-    }
-
-    @Override
-    public void stopAdvertising() {
-        if (mSimulator.isDestroyed()) {
-            return;
-        }
-
-        mAdvertiser.stopAdvertisingSet(mAdvertisingSetCallback);
-        mAdvertisingSet = null;
-        mSimulator.setIsAdvertising(false);
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothController.kt b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothController.kt
deleted file mode 100644
index 345e8d2..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothController.kt
+++ /dev/null
@@ -1,269 +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.nearby.fastpair.provider.bluetooth
-
-import android.bluetooth.BluetoothAdapter
-import android.bluetooth.BluetoothDevice
-import android.bluetooth.BluetoothManager
-import android.bluetooth.BluetoothProfile
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.nearby.fastpair.provider.FastPairSimulator
-import android.nearby.fastpair.provider.utils.Logger
-import android.os.SystemClock
-import android.provider.Settings
-
-/** Controls the local Bluetooth adapter for Fast Pair testing. */
-class BluetoothController(
-    private val context: Context,
-    private val listener: EventListener
-) : BroadcastReceiver() {
-    private val mLogger = Logger(TAG)
-    private val bluetoothAdapter: BluetoothAdapter =
-        (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter!!
-    private var remoteDevice: BluetoothDevice? = null
-    private var remoteDeviceConnectionState: Int = BluetoothAdapter.STATE_DISCONNECTED
-    private var a2dpSinkProxy: BluetoothProfile? = null
-
-    /** Turns on the local Bluetooth adapter */
-    fun enableBluetooth() {
-        if (!bluetoothAdapter.isEnabled) {
-            bluetoothAdapter.enable()
-            waitForBluetoothState(BluetoothAdapter.STATE_ON)
-        }
-    }
-
-    /**
-     * Sets the Input/Output capability of the device for classic Bluetooth operations.
-     * Note: In order to let changes take effect, this method will make sure the Bluetooth stack is
-     * restarted by blocking calling thread.
-     *
-     * @param ioCapabilityClassic One of {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_NONE},
-     * ```
-     *     {@link #IO_CAPABILITY_KBDISP} or more in {@link BluetoothAdapter}.
-     */
-    fun setIoCapability(ioCapabilityClassic: Int) {
-        bluetoothAdapter.ioCapability = ioCapabilityClassic
-
-        // Toggling airplane mode on/off to restart Bluetooth stack and reset the BLE.
-        try {
-            Settings.Global.putInt(
-                context.contentResolver,
-                Settings.Global.AIRPLANE_MODE_ON,
-                TURN_AIRPLANE_MODE_ON
-            )
-        } catch (expectedOnNonCustomAndroid: SecurityException) {
-            mLogger.log(
-                expectedOnNonCustomAndroid,
-                "Requires custom Android to toggle airplane mode"
-            )
-            // Fall back to turn off Bluetooth.
-            bluetoothAdapter.disable()
-        }
-        waitForBluetoothState(BluetoothAdapter.STATE_OFF)
-        try {
-            Settings.Global.putInt(
-                context.contentResolver,
-                Settings.Global.AIRPLANE_MODE_ON,
-                TURN_AIRPLANE_MODE_OFF
-            )
-        } catch (expectedOnNonCustomAndroid: SecurityException) {
-            mLogger.log(
-                expectedOnNonCustomAndroid,
-                "SecurityException while toggled airplane mode."
-            )
-        } finally {
-            // Double confirm that Bluetooth is turned on.
-            bluetoothAdapter.enable()
-        }
-        waitForBluetoothState(BluetoothAdapter.STATE_ON)
-    }
-
-    /** Registers this Bluetooth state change receiver. */
-    fun registerBluetoothStateReceiver() {
-        val bondStateFilter =
-            IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED).apply {
-                addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
-                addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)
-            }
-        context.registerReceiver(
-            this,
-            bondStateFilter,
-            /* broadcastPermission= */ null,
-            /* scheduler= */ null
-        )
-    }
-
-    /** Unregisters this Bluetooth state change receiver. */
-    fun unregisterBluetoothStateReceiver() {
-        context.unregisterReceiver(this)
-    }
-
-    /** Clears current remote device. */
-    fun clearRemoteDevice() {
-        remoteDevice = null
-    }
-
-    /** Gets current remote device. */
-    fun getRemoteDevice(): BluetoothDevice? = remoteDevice
-
-    /** Gets current remote device as string. */
-    fun getRemoteDeviceAsString(): String = remoteDevice?.remoteDeviceToString() ?: "none"
-
-    /** Connects the Bluetooth A2DP sink profile service. */
-    fun connectA2DPSinkProfile() {
-        // Get the A2DP proxy before continuing with initialization.
-        bluetoothAdapter.getProfileProxy(
-            context,
-            object : BluetoothProfile.ServiceListener {
-                override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
-                    // When Bluetooth turns off and then on again, this is called again. But we only care
-                    // the first time. There doesn't seem to be a way to unregister our listener.
-                    if (a2dpSinkProxy == null) {
-                        a2dpSinkProxy = proxy
-                        listener.onA2DPSinkProfileConnected()
-                    }
-                }
-
-                override fun onServiceDisconnected(profile: Int) {}
-            },
-            BluetoothProfile.A2DP_SINK
-        )
-    }
-
-    /** Get the current Bluetooth scan mode of the local Bluetooth adapter. */
-    fun getScanMode(): Int = bluetoothAdapter.scanMode
-
-    /** Return true if the remote device is connected to the local adapter. */
-    fun isConnected(): Boolean = remoteDeviceConnectionState == BluetoothAdapter.STATE_CONNECTED
-
-    /** Return true if the remote device is bonded (paired) to the local adapter. */
-    fun isPaired(): Boolean = bluetoothAdapter.bondedDevices.contains(remoteDevice)
-
-    /** Gets the A2DP sink profile proxy. */
-    fun getA2DPSinkProfileProxy(): BluetoothProfile? = a2dpSinkProxy
-
-    /**
-     * Callback method for receiving Intent broadcast of Bluetooth state.
-     *
-     * See [BroadcastReceiver#onReceive].
-     *
-     * @param context the Context in which the receiver is running.
-     * @param intent the Intent being received.
-     */
-    override fun onReceive(context: Context, intent: Intent) {
-        when (intent.action) {
-            BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
-                // After a device starts bonding, we only pay attention to intents about that device.
-                val device =
-                    intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
-                val bondState =
-                    intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)
-                remoteDevice =
-                    when (bondState) {
-                        BluetoothDevice.BOND_BONDING, BluetoothDevice.BOND_BONDED -> device
-                        BluetoothDevice.BOND_NONE -> null
-                        else -> remoteDevice
-                    }
-                mLogger.log(
-                    "ACTION_BOND_STATE_CHANGED, the bound state of " +
-                            "the remote device (%s) change to %s.",
-                    remoteDevice?.remoteDeviceToString(),
-                    bondState.bondStateToString()
-                )
-                listener.onBondStateChanged(bondState)
-            }
-            BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED -> {
-                remoteDeviceConnectionState =
-                    intent.getIntExtra(
-                        BluetoothAdapter.EXTRA_CONNECTION_STATE,
-                        BluetoothAdapter.STATE_DISCONNECTED
-                    )
-                mLogger.log(
-                    "ACTION_CONNECTION_STATE_CHANGED, the new connectionState: %s",
-                    remoteDeviceConnectionState
-                )
-                listener.onConnectionStateChanged(remoteDeviceConnectionState)
-            }
-            BluetoothAdapter.ACTION_SCAN_MODE_CHANGED -> {
-                val scanMode =
-                    intent.getIntExtra(
-                        BluetoothAdapter.EXTRA_SCAN_MODE,
-                        BluetoothAdapter.SCAN_MODE_NONE
-                    )
-                mLogger.log(
-                    "ACTION_SCAN_MODE_CHANGED, the new scanMode: %s",
-                    FastPairSimulator.scanModeToString(scanMode)
-                )
-                listener.onScanModeChange(scanMode)
-            }
-            else -> {}
-        }
-    }
-
-    private fun waitForBluetoothState(state: Int) {
-        while (bluetoothAdapter.state != state) {
-            SystemClock.sleep(1000)
-        }
-    }
-
-    private fun BluetoothDevice.remoteDeviceToString(): String = "${this.name}-${this.address}"
-
-    private fun Int.bondStateToString(): String =
-        when (this) {
-            BluetoothDevice.BOND_NONE -> "BOND_NONE"
-            BluetoothDevice.BOND_BONDING -> "BOND_BONDING"
-            BluetoothDevice.BOND_BONDED -> "BOND_BONDED"
-            else -> "BOND_ERROR"
-        }
-
-    /** Interface for listening the events from Bluetooth controller. */
-    interface EventListener {
-        /** The callback for the first onServiceConnected of A2DP sink profile. */
-        fun onA2DPSinkProfileConnected()
-
-        /**
-         * Reports the current bond state of the remote device.
-         *
-         * @param bondState the bond state of the remote device.
-         */
-        fun onBondStateChanged(bondState: Int)
-
-        /**
-         * Reports the current connection state of the remote device.
-         *
-         * @param connectionState the bond state of the remote device.
-         */
-        fun onConnectionStateChanged(connectionState: Int)
-
-        /**
-         * Reports the current scan mode of the local Adapter.
-         *
-         * @param mode the current scan mode of the local Adapter.
-         */
-        fun onScanModeChange(mode: Int)
-    }
-
-    companion object {
-        private const val TAG = "BluetoothController"
-
-        private const val TURN_AIRPLANE_MODE_OFF = 0
-        private const val TURN_AIRPLANE_MODE_ON = 1
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConfig.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConfig.java
deleted file mode 100644
index 3cacd55..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConfig.java
+++ /dev/null
@@ -1,151 +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.nearby.fastpair.provider.bluetooth;
-
-import android.annotation.TargetApi;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.nearby.common.bluetooth.BluetoothConsts;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.UUID;
-
-/** Configuration of a GATT server. */
-@TargetApi(18)
-public class BluetoothGattServerConfig {
-    private final Map<UUID, ServiceConfig> mServiceConfigs = new HashMap<UUID, ServiceConfig>();
-
-    @Nullable
-    private BluetoothGattServerHelper.Listener mServerlistener = null;
-
-    public BluetoothGattServerConfig addService(UUID uuid, ServiceConfig serviceConfig) {
-        mServiceConfigs.put(uuid, serviceConfig);
-        return this;
-    }
-
-    public BluetoothGattServerConfig setServerConnectionListener(
-            BluetoothGattServerHelper.Listener listener) {
-        mServerlistener = listener;
-        return this;
-    }
-
-    @Nullable
-    public BluetoothGattServerHelper.Listener getServerListener() {
-        return mServerlistener;
-    }
-
-    /**
-     * Adds a service and a characteristic to indicate that the server has dynamic services.
-     * This is a workaround for b/21587710.
-     * TODO(lingjunl): remove them when b/21587710 is fixed.
-     */
-    public BluetoothGattServerConfig addSelfDefinedDynamicService() {
-        ServiceConfig serviceConfig = new ServiceConfig().addCharacteristic(
-                new BluetoothGattServlet() {
-                    @Override
-                    public BluetoothGattCharacteristic getCharacteristic() {
-                        return new BluetoothGattCharacteristic(
-                                BluetoothConsts.SERVICE_DYNAMIC_CHARACTERISTIC,
-                                BluetoothGattCharacteristic.PROPERTY_READ,
-                                BluetoothGattCharacteristic.PERMISSION_READ);
-                    }
-                });
-        return addService(BluetoothConsts.SERVICE_DYNAMIC_SERVICE, serviceConfig);
-    }
-
-    public List<BluetoothGattService> getBluetoothGattServices() {
-        List<BluetoothGattService> result = new ArrayList<BluetoothGattService>();
-        for (Entry<UUID, ServiceConfig> serviceEntry : mServiceConfigs.entrySet()) {
-            UUID serviceUuid = serviceEntry.getKey();
-            ServiceConfig serviceConfig = serviceEntry.getValue();
-            if (serviceUuid == null || serviceConfig == null) {
-                // This is not supposed to happen
-                throw new IllegalStateException();
-            }
-            BluetoothGattService gattService = new BluetoothGattService(serviceUuid,
-                    BluetoothGattService.SERVICE_TYPE_PRIMARY);
-            for (Entry<BluetoothGattCharacteristic, BluetoothGattServlet> servletEntry :
-                    serviceConfig.getServlets().entrySet()) {
-                BluetoothGattCharacteristic characteristic = servletEntry.getKey();
-                if (characteristic == null) {
-                    // This is not supposed to happen
-                    throw new IllegalStateException();
-                }
-                gattService.addCharacteristic(characteristic);
-            }
-            result.add(gattService);
-        }
-        return result;
-    }
-
-    public List<UUID> getAdvertisedUuids() {
-        List<UUID> result = new ArrayList<UUID>();
-        for (Entry<UUID, ServiceConfig> serviceEntry : mServiceConfigs.entrySet()) {
-            UUID serviceUuid = serviceEntry.getKey();
-            ServiceConfig serviceConfig = serviceEntry.getValue();
-            if (serviceUuid == null || serviceConfig == null) {
-                // This is not supposed to happen
-                throw new IllegalStateException();
-            }
-            if (serviceConfig.isAdvertised()) {
-                result.add(serviceUuid);
-            }
-        }
-        return result;
-    }
-
-    public Map<BluetoothGattCharacteristic, BluetoothGattServlet> getServlets() {
-        Map<BluetoothGattCharacteristic, BluetoothGattServlet> result =
-                new HashMap<BluetoothGattCharacteristic, BluetoothGattServlet>();
-        for (ServiceConfig serviceConfig : mServiceConfigs.values()) {
-            result.putAll(serviceConfig.getServlets());
-        }
-        return result;
-    }
-
-    /** Configuration of a GATT service. */
-    public static class ServiceConfig {
-        private final Map<BluetoothGattCharacteristic, BluetoothGattServlet> mServlets =
-                new HashMap<BluetoothGattCharacteristic, BluetoothGattServlet>();
-        private boolean mAdvertise = false;
-
-        public ServiceConfig addCharacteristic(BluetoothGattServlet servlet) {
-            mServlets.put(servlet.getCharacteristic(), servlet);
-            return this;
-        }
-
-        public ServiceConfig setAdvertise(boolean advertise) {
-            mAdvertise = advertise;
-            return this;
-        }
-
-        public Map<BluetoothGattCharacteristic, BluetoothGattServlet> getServlets() {
-            return mServlets;
-        }
-
-        public boolean isAdvertised() {
-            return mAdvertise;
-        }
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConnection.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConnection.java
deleted file mode 100644
index fae6951..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConnection.java
+++ /dev/null
@@ -1,468 +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.nearby.fastpair.provider.bluetooth;
-
-import android.annotation.TargetApi;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.ReservedUuids;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
-import com.google.common.io.BaseEncoding;
-
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Connection to a bluetooth LE device over Gatt.
- */
-@TargetApi(18)
-public class BluetoothGattServerConnection implements Closeable {
-    @SuppressWarnings("unused")
-    private static final String TAG = BluetoothGattServerConnection.class.getSimpleName();
-
-    /** See {@link BluetoothGattDescriptor#DISABLE_NOTIFICATION_VALUE}. */
-    private static final short DISABLE_NOTIFICATION_VALUE = 0x0000;
-
-    /** See {@link BluetoothGattDescriptor#ENABLE_NOTIFICATION_VALUE}. */
-    private static final short ENABLE_NOTIFICATION_VALUE = 0x0001;
-
-    /** See {@link BluetoothGattDescriptor#ENABLE_INDICATION_VALUE}. */
-    private static final short ENABLE_INDICATION_VALUE = 0x0002;
-
-    /** Default MTU when value is unknown. */
-    public static final int DEFAULT_MTU = 23;
-
-    @VisibleForTesting
-    static final long OPERATION_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
-
-    /** Notification types as defined by the BLE spec vol 4, sec G, part 3.3.3.3 */
-    public enum NotificationType {
-        NOTIFICATION,
-        INDICATION
-    }
-
-    /** BT operation types that can be in flight. */
-    public enum OperationType {
-        SEND_NOTIFICATION
-    }
-
-    private final Map<ScopedKey, Object> mContextValues = new HashMap<ScopedKey, Object>();
-    private final List<Listener> mCloseListeners = new ArrayList<Listener>();
-
-    private final BluetoothGattServerHelper mBluetoothGattServerHelper;
-    private final BluetoothDevice mBluetoothDevice;
-
-    @VisibleForTesting
-    BluetoothOperationExecutor mBluetoothOperationScheduler =
-            new BluetoothOperationExecutor(1);
-
-    /** Stores pending writes. For each UUID, we store an offset and a byte[] of data. */
-    @VisibleForTesting
-    final Map<BluetoothGattServlet, SortedMap<Integer, byte[]>> mQueuedCharacteristicWrites =
-            new HashMap<BluetoothGattServlet, SortedMap<Integer, byte[]>>();
-
-    @VisibleForTesting
-    final Map<BluetoothGattCharacteristic, Notifier> mRegisteredNotifications =
-            new HashMap<BluetoothGattCharacteristic, Notifier>();
-
-    private final Map<BluetoothGattCharacteristic, BluetoothGattServlet> mServlets;
-
-    public BluetoothGattServerConnection(
-            BluetoothGattServerHelper bluetoothGattServerHelper,
-            BluetoothDevice device,
-            BluetoothGattServerConfig serverConfig) {
-        mBluetoothGattServerHelper = bluetoothGattServerHelper;
-        mBluetoothDevice = device;
-        mServlets = serverConfig.getServlets();
-    }
-
-    public void setContextValue(Object scope, String key, @Nullable Object value) {
-        mContextValues.put(new ScopedKey(scope, key), value);
-    }
-
-    @Nullable
-    public Object getContextValue(Object scope, String key) {
-        return mContextValues.get(new ScopedKey(scope, key));
-    }
-
-    public BluetoothDevice getDevice() {
-        return mBluetoothDevice;
-    }
-
-    public int getMtu() {
-        return DEFAULT_MTU;
-    }
-
-    public int getMaxDataPacketSize() {
-        // Per BT specs (3.2.9), only MTU - 3 bytes can be used to transmit data
-        return getMtu() - 3;
-    }
-
-    public void addCloseListener(Listener listener) {
-        synchronized (mCloseListeners) {
-            mCloseListeners.add(listener);
-        }
-    }
-
-    public void removeCloseListener(Listener listener) {
-        synchronized (mCloseListeners) {
-            mCloseListeners.remove(listener);
-        }
-    }
-
-    private BluetoothGattServlet getServlet(BluetoothGattCharacteristic characteristic)
-            throws BluetoothGattException {
-        BluetoothGattServlet servlet = mServlets.get(characteristic);
-        if (servlet == null) {
-            throw new BluetoothGattException(
-                    String.format("No handler registered for characteristic %s.",
-                            characteristic.getUuid()),
-                    BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-        }
-        return servlet;
-    }
-
-    public byte[] readCharacteristic(int offset, BluetoothGattCharacteristic characteristic)
-            throws BluetoothGattException {
-        return getServlet(characteristic).read(this, offset);
-    }
-
-    public void writeCharacteristic(BluetoothGattCharacteristic characteristic,
-            boolean preparedWrite,
-            int offset, byte[] value) throws BluetoothGattException {
-        Log.d(TAG, String.format(
-                "Received %d bytes at offset %d on %s from device %s, prepareWrite=%s.",
-                value.length,
-                offset,
-                BluetoothGattUtils.toString(characteristic),
-                mBluetoothDevice,
-                preparedWrite));
-        BluetoothGattServlet servlet = getServlet(characteristic);
-        if (preparedWrite) {
-            SortedMap<Integer, byte[]> bytePackets = mQueuedCharacteristicWrites.get(servlet);
-            if (bytePackets == null) {
-                bytePackets = new TreeMap<Integer, byte[]>();
-                mQueuedCharacteristicWrites.put(servlet, bytePackets);
-            }
-            bytePackets.put(offset, value);
-            return;
-        }
-
-        Log.d(TAG, servlet.toString());
-        servlet.write(this, offset, value);
-    }
-
-    public byte[] readDescriptor(int offset, BluetoothGattDescriptor descriptor)
-            throws BluetoothGattException {
-        BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
-        if (characteristic == null) {
-            throw new BluetoothGattException(String.format(
-                    "Descriptor %s not associated with a characteristics!",
-                    BluetoothGattUtils.toString(descriptor)), BluetoothGatt.GATT_FAILURE);
-        }
-        return getServlet(characteristic).readDescriptor(this, descriptor, offset);
-    }
-
-    public void writeDescriptor(
-            BluetoothGattDescriptor descriptor,
-            boolean preparedWrite,
-            int offset,
-            byte[] value) throws BluetoothGattException {
-        Log.d(TAG, String.format(
-                "Received %d bytes at offset %d on %s from device %s, prepareWrite=%s.",
-                value.length,
-                offset,
-                BluetoothGattUtils.toString(descriptor),
-                mBluetoothDevice,
-                preparedWrite));
-        if (preparedWrite) {
-            throw new BluetoothGattException(
-                    String.format("Prepare write not supported for descriptor %s.", descriptor),
-                    BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-        }
-
-        BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
-        if (characteristic == null) {
-            throw new BluetoothGattException(String.format(
-                    "Descriptor %s not associated with a characteristics!",
-                    BluetoothGattUtils.toString(descriptor)), BluetoothGatt.GATT_FAILURE);
-        }
-        BluetoothGattServlet servlet = getServlet(characteristic);
-        if (descriptor.getUuid().equals(
-                ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION)) {
-            handleCharacteristicConfigurationChange(characteristic, servlet, offset, value);
-            return;
-        }
-        servlet.writeDescriptor(this, descriptor, offset, value);
-    }
-
-    private void handleCharacteristicConfigurationChange(
-            final BluetoothGattCharacteristic characteristic, BluetoothGattServlet servlet,
-            int offset,
-            byte[] value)
-            throws BluetoothGattException {
-        if (offset != 0) {
-            throw new BluetoothGattException(String.format(
-                    "Offset should be 0 when changing the client characteristic config: %d.",
-                    offset),
-                    BluetoothGatt.GATT_INVALID_OFFSET);
-        }
-        if (value.length != 2) {
-            throw new BluetoothGattException(String.format(
-                    "Value 0x%s is undefined for the client characteristic config",
-                    BaseEncoding.base16().encode(value)),
-                    BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH);
-        }
-
-        boolean notificationRegistered = mRegisteredNotifications.containsKey(characteristic);
-        Notifier notifier;
-        switch (toShort(value)) {
-            case ENABLE_NOTIFICATION_VALUE:
-                if (!notificationRegistered) {
-                    notifier = new Notifier() {
-                        @Override
-                        public void notify(byte[] data) throws BluetoothException {
-                            sendNotification(characteristic, NotificationType.NOTIFICATION, data);
-                        }
-                    };
-                    mRegisteredNotifications.put(characteristic, notifier);
-                    servlet.enableNotification(this, notifier);
-                }
-                break;
-            case ENABLE_INDICATION_VALUE:
-                if (!notificationRegistered) {
-                    notifier = new Notifier() {
-                        @Override
-                        public void notify(byte[] data) throws BluetoothException {
-                            sendNotification(characteristic, NotificationType.INDICATION, data);
-                        }
-                    };
-                    mRegisteredNotifications.put(characteristic, notifier);
-                    servlet.enableNotification(this, notifier);
-                }
-                break;
-            case DISABLE_NOTIFICATION_VALUE:
-                // Note: this disables notifications or indications.
-                if (notificationRegistered) {
-                    notifier = mRegisteredNotifications.remove(characteristic);
-                    if (notifier == null) {
-                        return; // this is not supposed to happen
-                    }
-                    servlet.disableNotification(this, notifier);
-                }
-                break;
-            default:
-                throw new BluetoothGattException(String.format(
-                        "Value 0x%s is undefined for the client characteristic config",
-                        BaseEncoding.base16().encode(value)),
-                        BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-        }
-    }
-
-    private static short toShort(byte[] value) {
-        Preconditions.checkNotNull(value);
-        Preconditions.checkArgument(value.length == 2, "Length should be 2 bytes.");
-
-        return (short) ((value[0] & 0x00FF) | (value[1] << 8));
-    }
-
-    public void executeWrite(boolean execute) throws BluetoothGattException {
-        if (!execute) {
-            mQueuedCharacteristicWrites.clear();
-            return;
-        }
-
-        try {
-            for (Entry<BluetoothGattServlet, SortedMap<Integer, byte[]>> queuedWrite :
-                    mQueuedCharacteristicWrites.entrySet()) {
-                BluetoothGattServlet servlet = queuedWrite.getKey();
-                SortedMap<Integer, byte[]> chunks = queuedWrite.getValue();
-                if (servlet == null || chunks == null) {
-                    // This is not supposed to happen
-                    throw new IllegalStateException();
-                }
-                assembleByteChunksAndHandle(servlet, chunks);
-            }
-        } finally {
-            mQueuedCharacteristicWrites.clear();
-        }
-    }
-
-    /**
-     * Assembles the specified queued writes and calls the provided write handler on the assembled
-     * chunks. Tries to assemble all the chunks into one write request. For example, if the content
-     * of byteChunks is:
-     * <code>
-     * offset data_size
-     * 0       10
-     * 10        1
-     * 11        5
-     * </code>
-     *
-     * then this method would call <code>writeHandler.onWrite(0, byte[16])</code>
-     *
-     * However, if all the chunks cannot be assembled into a continuous byte[], then onWrite() will
-     * be called multiple times with the largest continuous chunks. For example, if the content of
-     * byteChunks is:
-     * <code>
-     * offset data_size
-     * 10       12
-     * 30        5
-     * 35        9
-     * </code>
-     *
-     * then this method would call <code>writeHandler.onWrite(10, byte[12)</code> and
-     * <code>writeHandler.onWrite(30, byte[14]).
-     */
-    private void assembleByteChunksAndHandle(BluetoothGattServlet servlet,
-            SortedMap<Integer, byte[]> byteChunks) throws BluetoothGattException {
-        ByteArrayOutputStream assembledRequest = new ByteArrayOutputStream();
-        Integer startWritingAtOffset = 0;
-
-        while (!byteChunks.isEmpty()) {
-            Integer offset = byteChunks.firstKey();
-
-            if (offset.intValue() < startWritingAtOffset + assembledRequest.size()) {
-                throw new BluetoothGattException(
-                        "Expected offset of at least " + assembledRequest.size()
-                                + ", but got offset " + offset, BluetoothGatt.GATT_INVALID_OFFSET);
-            }
-
-            // If we have a hole, then write what we've already assembled and start assembling a new
-            // long write
-            if (offset.intValue() > startWritingAtOffset + assembledRequest.size()) {
-                servlet.write(this, startWritingAtOffset.intValue(),
-                        assembledRequest.toByteArray());
-                startWritingAtOffset = offset;
-                assembledRequest.reset();
-            }
-
-            try {
-                byte[] dataChunk = byteChunks.remove(offset);
-                if (dataChunk == null) {
-                    // This is not supposed to happen
-                    throw new IllegalStateException();
-                }
-                assembledRequest.write(dataChunk);
-            } catch (IOException e) {
-                throw new BluetoothGattException("Error assembling request",
-                        BluetoothGatt.GATT_FAILURE);
-            }
-        }
-
-        // If there is anything to write, write it
-        if (assembledRequest.size() > 0) {
-            Preconditions.checkNotNull(startWritingAtOffset); // should never be null at this point
-            servlet.write(this, startWritingAtOffset.intValue(), assembledRequest.toByteArray());
-        }
-    }
-
-    private void sendNotification(final BluetoothGattCharacteristic characteristic,
-            final NotificationType notificationType, final byte[] data)
-            throws BluetoothException {
-        mBluetoothOperationScheduler.execute(
-                new Operation<Void>(OperationType.SEND_NOTIFICATION) {
-                    @Override
-                    public void run() throws BluetoothException {
-                        mBluetoothGattServerHelper.sendNotification(mBluetoothDevice,
-                                characteristic,
-                                data,
-                                notificationType == NotificationType.INDICATION ? true : false);
-                    }
-                },
-                OPERATION_TIMEOUT);
-    }
-
-    @Override
-    public void close() throws IOException {
-        try {
-            mBluetoothGattServerHelper.closeConnection(mBluetoothDevice);
-        } catch (BluetoothException e) {
-            throw new IOException("Failed to close connection", e);
-        }
-    }
-
-    public void notifyNotificationSent(int status) {
-        mBluetoothOperationScheduler.notifyCompletion(
-                new Operation<Void>(OperationType.SEND_NOTIFICATION), status);
-    }
-
-    public void onClose() {
-        synchronized (mCloseListeners) {
-            for (Listener listener : mCloseListeners) {
-                listener.onClose();
-            }
-        }
-    }
-
-    /** Scope/key pair to use to reference contextual values. */
-    private static class ScopedKey {
-        private final Object mScope;
-        private final String mKey;
-
-        ScopedKey(Object scope, String key) {
-            mScope = scope;
-            mKey = key;
-        }
-
-        @Override
-        public boolean equals(@Nullable Object o) {
-            if (!(o instanceof ScopedKey)) {
-                return false;
-            }
-            ScopedKey other = (ScopedKey) o;
-            return other.mScope.equals(mScope) && other.mKey.equals(mKey);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hashCode(mScope, mKey);
-        }
-    }
-
-    /** Listener to be notified when the connection closes. */
-    public interface Listener {
-        void onClose();
-    }
-
-    /** Notifier to notify data over notification or indication. */
-    public interface Notifier {
-        void notify(byte[] data) throws BluetoothException;
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerHelper.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerHelper.java
deleted file mode 100644
index 9339e14..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerHelper.java
+++ /dev/null
@@ -1,449 +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.nearby.fastpair.provider.bluetooth;
-
-import android.annotation.TargetApi;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothProfile;
-import android.content.Context;
-import android.os.Build.VERSION_CODES;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.testability.VersionProvider;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattServer;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattServerCallback;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
-
-import com.google.common.base.Preconditions;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Helper for simplifying operations on {@link BluetoothGattServer}.
- */
-@TargetApi(18)
-public class BluetoothGattServerHelper {
-    private static final String TAG = BluetoothGattServerHelper.class.getSimpleName();
-
-    @VisibleForTesting
-    static final long OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(1);
-    private static final int MAX_PARALLEL_OPERATIONS = 5;
-
-    /** BT operation types that can be in flight. */
-    public enum OperationType {
-        ADD_SERVICE,
-        CLOSE_CONNECTION,
-        START_ADVERTISING
-    }
-
-    private final Object mOperationLock = new Object();
-    @VisibleForTesting
-    final BluetoothGattServerCallback mGattServerCallback =
-            new GattServerCallback();
-    @VisibleForTesting
-    BluetoothOperationExecutor mBluetoothOperationScheduler =
-            new BluetoothOperationExecutor(MAX_PARALLEL_OPERATIONS);
-
-    private final Context mContext;
-    private final BluetoothManager mBluetoothManager;
-    private final VersionProvider mVersionProvider;
-
-    @Nullable
-    @VisibleForTesting
-    volatile BluetoothGattServerConfig mServerConfig = null;
-
-    @Nullable
-    @VisibleForTesting
-    volatile BluetoothGattServer mBluetoothGattServer = null;
-
-    @VisibleForTesting
-    final ConcurrentMap<BluetoothDevice, BluetoothGattServerConnection>
-            mConnections = new ConcurrentHashMap<BluetoothDevice, BluetoothGattServerConnection>();
-
-    public BluetoothGattServerHelper(Context context, BluetoothManager bluetoothManager) {
-        this(
-                Preconditions.checkNotNull(context),
-                Preconditions.checkNotNull(bluetoothManager),
-                new VersionProvider()
-        );
-    }
-
-    @VisibleForTesting
-    BluetoothGattServerHelper(
-            Context context, BluetoothManager bluetoothManager, VersionProvider versionProvider) {
-        mContext = context;
-        mBluetoothManager = bluetoothManager;
-        mVersionProvider = versionProvider;
-    }
-
-    @Nullable
-    public BluetoothGattServerConfig getConfig() {
-        return mServerConfig;
-    }
-
-    public void open(final BluetoothGattServerConfig gattServerConfig) throws BluetoothException {
-        synchronized (mOperationLock) {
-            Preconditions.checkState(mBluetoothGattServer == null, "Gatt server is already open.");
-            final BluetoothGattServer server =
-                    mBluetoothManager.openGattServer(mContext, mGattServerCallback);
-            if (server == null) {
-                throw new BluetoothException(
-                        "Failed to open the GATT server, openGattServer returned null.");
-            }
-
-            try {
-                for (final BluetoothGattService service :
-                        gattServerConfig.getBluetoothGattServices()) {
-                    if (service == null) {
-                        continue;
-                    }
-                    mBluetoothOperationScheduler.execute(
-                            new Operation<Void>(OperationType.ADD_SERVICE, service) {
-                                @Override
-                                public void run() throws BluetoothException {
-                                    boolean success = server.addService(service);
-                                    if (!success) {
-                                        throw new BluetoothException("Fails on adding service");
-                                    }
-                                }
-                            }, OPERATION_TIMEOUT_MILLIS);
-                }
-                mBluetoothGattServer = server;
-                mServerConfig = gattServerConfig;
-            } catch (BluetoothException e) {
-                server.close();
-                throw e;
-            }
-        }
-    }
-
-    public boolean isOpen() {
-        synchronized (mOperationLock) {
-            return mBluetoothGattServer != null;
-        }
-    }
-
-    public void close() {
-        synchronized (mOperationLock) {
-            BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
-            if (bluetoothGattServer == null) {
-                return;
-            }
-            bluetoothGattServer.close();
-            mBluetoothGattServer = null;
-        }
-    }
-
-    private BluetoothGattServerConnection getConnectionByDevice(BluetoothDevice device)
-            throws BluetoothGattException {
-        BluetoothGattServerConnection bluetoothLeConnection = mConnections.get(device);
-        if (bluetoothLeConnection == null) {
-            throw new BluetoothGattException(
-                    String.format("Received operation on an unknown device: %s", device),
-                    BluetoothGatt.GATT_FAILURE);
-        }
-        return bluetoothLeConnection;
-    }
-
-    public void sendNotification(
-            BluetoothDevice device,
-            BluetoothGattCharacteristic characteristic,
-            byte[] data,
-            boolean confirm)
-            throws BluetoothException {
-        Log.d(TAG, String.format("Sending a %s of %d bytes on characteristics %s on device %s.",
-                confirm ? "indication" : "notification",
-                data.length,
-                characteristic.getUuid(),
-                device));
-        synchronized (mOperationLock) {
-            BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
-            if (bluetoothGattServer == null) {
-                throw new BluetoothException("Server is not open.");
-            }
-            BluetoothGattCharacteristic clonedCharacteristic =
-                    BluetoothGattUtils.clone(characteristic);
-            clonedCharacteristic.setValue(data);
-            bluetoothGattServer.notifyCharacteristicChanged(device, clonedCharacteristic, confirm);
-        }
-    }
-
-    public void closeConnection(final BluetoothDevice bluetoothDevice) throws BluetoothException {
-        final BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
-        if (bluetoothGattServer == null) {
-            throw new BluetoothException("Server is not open.");
-        }
-        int connectionSate =
-                mBluetoothManager.getConnectionState(bluetoothDevice, BluetoothProfile.GATT);
-        if (connectionSate != BluetoothGatt.STATE_CONNECTED) {
-            return;
-        }
-        mBluetoothOperationScheduler.execute(
-                new Operation<Void>(OperationType.CLOSE_CONNECTION) {
-                    @Override
-                    public void run() throws BluetoothException {
-                        bluetoothGattServer.cancelConnection(bluetoothDevice);
-                    }
-                },
-                OPERATION_TIMEOUT_MILLIS);
-    }
-
-    private class GattServerCallback extends BluetoothGattServerCallback {
-        @Override
-        public void onServiceAdded(int status, BluetoothGattService service) {
-            mBluetoothOperationScheduler.notifyCompletion(
-                    new Operation<Void>(OperationType.ADD_SERVICE, service), status);
-        }
-
-        @Override
-        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
-            BluetoothGattServerConfig serverConfig = mServerConfig;
-            BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
-            BluetoothGattServerConnection bluetoothLeConnection;
-            if (serverConfig == null || bluetoothGattServer == null) {
-                return;
-            }
-            switch (newState) {
-                case BluetoothGattServer.STATE_CONNECTED:
-                    if (status != BluetoothGatt.GATT_SUCCESS) {
-                        Log.e(TAG, String.format("Connection to %s failed: %s", device,
-                                BluetoothGattUtils.getMessageForStatusCode(status)));
-                        return;
-                    }
-                    Log.i(TAG, String.format("Connected to device %s.", device));
-                    if (mConnections.containsKey(device)) {
-                        Log.w(TAG, String.format("A connection is already open with device %s. "
-                                + "Keeping existing one.", device));
-                        return;
-                    }
-
-                    BluetoothGattServerConnection connection = new BluetoothGattServerConnection(
-                            BluetoothGattServerHelper.this,
-                            device,
-                            serverConfig);
-                    if (serverConfig.getServerListener() != null) {
-                        serverConfig.getServerListener().onConnection(connection);
-                    }
-                    mConnections.put(device, connection);
-
-                    // By default, Android disconnects active GATT server connection if the
-                    // advertisement is
-                    // stop (or sometime stopScanning also disconnect, see b/62667394). Asking
-                    // the server to
-                    // reverse connect will tell Android to keep the connection open.
-                    // Code handling connect() on Android OS is: btif_gatt_server.c
-                    // Note: for Android < P, unknown type devices don't connect. See b/62827460.
-                    //       for Android P+, unknown type devices always use LE to connect (see
-                    //       code)
-                    // Note: for Android < N, dual mode devices always connect using BT classic,
-                    // so connect()
-                    //       should *NOT* be called for those devices. See b/29819614.
-                    if (mVersionProvider.getSdkInt() >= VERSION_CODES.N
-                            || device.getType() != BluetoothDevice.DEVICE_TYPE_DUAL) {
-                        boolean success = bluetoothGattServer.connect(device, /* autoConnect */
-                                false);
-                        if (!success) {
-                            Log.w(TAG, String.format(
-                                    "Keeping connection open on stop advertising failed for "
-                                            + "device %s.",
-                                    device));
-                        }
-                    }
-                    break;
-                case BluetoothGattServer.STATE_DISCONNECTED:
-                    if (status != BluetoothGatt.GATT_SUCCESS) {
-                        Log.w(TAG, String.format(
-                                "Disconnection from %s error: %s. Proceeding anyway.",
-                                device, BluetoothGattUtils.getMessageForStatusCode(status)));
-                    }
-                    bluetoothLeConnection = mConnections.remove(device);
-                    if (bluetoothLeConnection != null) {
-                        // Disconnect the server, required after connecting to it.
-                        bluetoothGattServer.cancelConnection(device);
-                        bluetoothLeConnection.onClose();
-                    }
-                    mBluetoothOperationScheduler.notifyCompletion(
-                            new Operation<Void>(OperationType.CLOSE_CONNECTION), status);
-                    break;
-                default:
-                    Log.e(TAG, String.format("Unexpected connection state: %d", newState));
-            }
-        }
-
-        @Override
-        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
-                BluetoothGattCharacteristic characteristic) {
-            BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
-            if (bluetoothGattServer == null) {
-                return;
-            }
-            try {
-                byte[] value =
-                        getConnectionByDevice(device).readCharacteristic(offset, characteristic);
-                bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
-                        offset,
-                        value);
-            } catch (BluetoothGattException e) {
-                Log.e(TAG,
-                        String.format(
-                                "Could not read  %s on device %s at offset %d",
-                                BluetoothGattUtils.toString(characteristic),
-                                device,
-                                offset),
-                        e);
-                bluetoothGattServer.sendResponse(
-                        device, requestId, e.getGattErrorCode(), offset, null);
-            }
-        }
-
-        @Override
-        public void onCharacteristicWriteRequest(BluetoothDevice device,
-                int requestId,
-                BluetoothGattCharacteristic characteristic,
-                boolean preparedWrite,
-                boolean responseNeeded,
-                int offset,
-                byte[] value) {
-            BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
-            if (bluetoothGattServer == null) {
-                return;
-            }
-            try {
-                getConnectionByDevice(device).writeCharacteristic(characteristic,
-                        preparedWrite,
-                        offset,
-                        value);
-                if (responseNeeded) {
-                    bluetoothGattServer.sendResponse(
-                            device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
-                }
-            } catch (BluetoothGattException e) {
-                Log.e(TAG,
-                        String.format(
-                                "Could not write %s on device %s at offset %d",
-                                BluetoothGattUtils.toString(characteristic),
-                                device,
-                                offset),
-                        e);
-                bluetoothGattServer.sendResponse(
-                        device, requestId, e.getGattErrorCode(), offset, null);
-            }
-        }
-
-        @Override
-        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
-                BluetoothGattDescriptor descriptor) {
-            BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
-            if (bluetoothGattServer == null) {
-                return;
-            }
-            try {
-                byte[] value = getConnectionByDevice(device).readDescriptor(offset, descriptor);
-                bluetoothGattServer.sendResponse(
-                        device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
-            } catch (BluetoothGattException e) {
-                Log.e(TAG, String.format(
-                                "Could not read %s on device %s at %d",
-                                BluetoothGattUtils.toString(descriptor),
-                                device,
-                                offset),
-                        e);
-                bluetoothGattServer.sendResponse(
-                        device, requestId, e.getGattErrorCode(), offset, null);
-            }
-        }
-
-        @Override
-        public void onDescriptorWriteRequest(BluetoothDevice device,
-                int requestId,
-                BluetoothGattDescriptor descriptor,
-                boolean preparedWrite,
-                boolean responseNeeded,
-                int offset,
-                byte[] value) {
-            BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
-            if (bluetoothGattServer == null) {
-                return;
-            }
-            try {
-                getConnectionByDevice(device)
-                        .writeDescriptor(descriptor, preparedWrite, offset, value);
-                if (responseNeeded) {
-                    bluetoothGattServer.sendResponse(
-                            device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
-                }
-                Log.d(TAG, "Operation onDescriptorWriteRequest successful.");
-            } catch (BluetoothGattException e) {
-                Log.e(TAG,
-                        String.format(
-                                "Could not write %s on device %s at %d",
-                                BluetoothGattUtils.toString(descriptor),
-                                device,
-                                offset),
-                        e);
-                bluetoothGattServer.sendResponse(
-                        device, requestId, e.getGattErrorCode(), offset, null);
-            }
-        }
-
-        @Override
-        public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
-            BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
-            if (bluetoothGattServer == null) {
-                return;
-            }
-            try {
-                getConnectionByDevice(device).executeWrite(execute);
-                bluetoothGattServer.sendResponse(
-                        device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
-            } catch (BluetoothGattException e) {
-                Log.e(TAG, "Could not execute write.", e);
-                bluetoothGattServer.sendResponse(device, requestId, e.getGattErrorCode(), 0, null);
-            }
-        }
-
-        @Override
-        public void onNotificationSent(BluetoothDevice device, int status) {
-            Log.d(TAG,
-                    String.format("Received onNotificationSent for device %s with status %s",
-                            device, status));
-            try {
-                getConnectionByDevice(device).notifyNotificationSent(status);
-            } catch (BluetoothGattException e) {
-                Log.e(TAG, "An error occurred when receiving onNotificationSent: " + e);
-            }
-        }
-    }
-
-    /** Listener for {@link BluetoothGattServerHelper}'s events. */
-    public interface Listener {
-        /** Called when a new connection to the server is established. */
-        void onConnection(BluetoothGattServerConnection connection);
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServlet.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServlet.java
deleted file mode 100644
index e25e223..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServlet.java
+++ /dev/null
@@ -1,71 +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.nearby.fastpair.provider.bluetooth;
-
-import android.annotation.TargetApi;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerConnection.Notifier;
-
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-
-/** Servlet to handle GATT operations on a characteristic. */
-@TargetApi(18)
-public abstract class BluetoothGattServlet {
-    public byte[] read(BluetoothGattServerConnection connection,
-            @SuppressWarnings("unused") int offset) throws BluetoothGattException {
-        throw new BluetoothGattException("Read not supported.",
-                BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-    }
-
-    public void write(BluetoothGattServerConnection connection,
-            @SuppressWarnings("unused") int offset, @SuppressWarnings("unused") byte[] value)
-            throws BluetoothGattException {
-        throw new BluetoothGattException("Write not supported.",
-                BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-    }
-
-    public byte[] readDescriptor(BluetoothGattServerConnection connection,
-            BluetoothGattDescriptor descriptor, @SuppressWarnings("unused") int offset)
-            throws BluetoothGattException {
-        throw new BluetoothGattException("Read not supported.",
-                BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-    }
-
-    public void writeDescriptor(BluetoothGattServerConnection connection,
-            BluetoothGattDescriptor descriptor,
-            @SuppressWarnings("unused") int offset, @SuppressWarnings("unused") byte[] value)
-            throws BluetoothGattException {
-        throw new BluetoothGattException("Write not supported.",
-                BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-    }
-
-    public void enableNotification(BluetoothGattServerConnection connection, Notifier notifier)
-            throws BluetoothGattException {
-        throw new BluetoothGattException("Notification not supported.",
-                BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-    }
-
-    public void disableNotification(BluetoothGattServerConnection connection, Notifier notifier)
-            throws BluetoothGattException {
-        throw new BluetoothGattException("Notification not supported.",
-                BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
-    }
-
-    public abstract BluetoothGattCharacteristic getCharacteristic();
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattUtils.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattUtils.java
deleted file mode 100644
index 7ac26ee..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattUtils.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 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 android.nearby.fastpair.provider.bluetooth;
-
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-
-import java.lang.reflect.Field;
-import java.util.Arrays;
-
-/**
- * Utils for Gatt profile.
- */
-public class BluetoothGattUtils {
-
-    /**
-     * Returns a string message for a BluetoothGatt status codes.
-     */
-    public static String getMessageForStatusCode(int statusCode) {
-        switch (statusCode) {
-            case BluetoothGatt.GATT_SUCCESS:
-                return "GATT_SUCCESS";
-            case BluetoothGatt.GATT_FAILURE:
-                return "GATT_FAILURE";
-            case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
-                return "GATT_INSUFFICIENT_AUTHENTICATION";
-            case BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION:
-                return "GATT_INSUFFICIENT_AUTHORIZATION";
-            case BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION:
-                return "GATT_INSUFFICIENT_ENCRYPTION";
-            case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH:
-                return "GATT_INVALID_ATTRIBUTE_LENGTH";
-            case BluetoothGatt.GATT_INVALID_OFFSET:
-                return "GATT_INVALID_OFFSET";
-            case BluetoothGatt.GATT_READ_NOT_PERMITTED:
-                return "GATT_READ_NOT_PERMITTED";
-            case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
-                return "GATT_REQUEST_NOT_SUPPORTED";
-            case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
-                return "GATT_WRITE_NOT_PERMITTED";
-            case BluetoothGatt.GATT_CONNECTION_CONGESTED:
-                return "GATT_CONNECTION_CONGESTED";
-            default:
-                return "Unknown error code";
-        }
-    }
-
-    /** Clones a {@link BluetoothGattCharacteristic} so the value can be changed thread-safely. */
-    public static BluetoothGattCharacteristic clone(BluetoothGattCharacteristic characteristic)
-            throws BluetoothException {
-        BluetoothGattCharacteristic result = new BluetoothGattCharacteristic(
-                characteristic.getUuid(),
-                characteristic.getProperties(), characteristic.getPermissions());
-        try {
-            Field instanceIdField = BluetoothGattCharacteristic.class.getDeclaredField("mInstance");
-            Field serviceField = BluetoothGattCharacteristic.class.getDeclaredField("mService");
-            Field descriptorField = BluetoothGattCharacteristic.class.getDeclaredField(
-                    "mDescriptors");
-            instanceIdField.setAccessible(true);
-            serviceField.setAccessible(true);
-            descriptorField.setAccessible(true);
-            instanceIdField.set(result, instanceIdField.get(characteristic));
-            serviceField.set(result, serviceField.get(characteristic));
-            descriptorField.set(result, descriptorField.get(characteristic));
-            byte[] value = characteristic.getValue();
-            if (value != null) {
-                result.setValue(Arrays.copyOf(value, value.length));
-            }
-            result.setWriteType(characteristic.getWriteType());
-        } catch (NoSuchFieldException e) {
-            throw new BluetoothException("Cannot clone characteristic.", e);
-        } catch (IllegalAccessException e) {
-            throw new BluetoothException("Cannot clone characteristic.", e);
-        } catch (IllegalArgumentException e) {
-            throw new BluetoothException("Cannot clone characteristic.", e);
-        }
-        return result;
-    }
-
-    /** Creates a user-readable string from a {@link BluetoothGattDescriptor}. */
-    public static String toString(@Nullable BluetoothGattDescriptor descriptor) {
-        if (descriptor == null) {
-            return "null descriptor";
-        }
-        return String.format("descriptor %s on %s",
-                descriptor.getUuid(),
-                toString(descriptor.getCharacteristic()));
-    }
-
-    /** Creates a user-readable string from a {@link BluetoothGattCharacteristic}. */
-    public static String toString(@Nullable BluetoothGattCharacteristic characteristic) {
-        if (characteristic == null) {
-            return "null characteristic";
-        }
-        return String.format("characteristic %s on %s",
-                characteristic.getUuid(),
-                toString(characteristic.getService()));
-    }
-
-    /** Creates a user-readable string from a {@link BluetoothGattService}. */
-    public static String toString(@Nullable BluetoothGattService service) {
-        if (service == null) {
-            return "null service";
-        }
-        return String.format("service %s", service.getUuid());
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothManager.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothManager.java
deleted file mode 100644
index bf241f1..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothManager.java
+++ /dev/null
@@ -1,80 +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.nearby.fastpair.provider.bluetooth;
-
-import android.content.Context;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattServer;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattServerCallback;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Mockable wrapper of {@link android.bluetooth.BluetoothManager}.
- */
-public class BluetoothManager {
-
-    private android.bluetooth.BluetoothManager mWrappedInstance;
-
-    private BluetoothManager(android.bluetooth.BluetoothManager instance) {
-        mWrappedInstance = instance;
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothManager#openGattServer(Context,
-     * android.bluetooth.BluetoothGattServerCallback)}.
-     */
-    @Nullable
-    public BluetoothGattServer openGattServer(Context context,
-            BluetoothGattServerCallback callback) {
-        return BluetoothGattServer.wrap(
-                mWrappedInstance.openGattServer(context, callback.unwrap()));
-    }
-
-    /**
-     * See {@link android.bluetooth.BluetoothManager#getConnectionState(
-     *android.bluetooth.BluetoothDevice, int)}.
-     */
-    public int getConnectionState(BluetoothDevice device, int profile) {
-        return mWrappedInstance.getConnectionState(device.unwrap(), profile);
-    }
-
-    /** See {@link android.bluetooth.BluetoothManager#getConnectedDevices(int)}. */
-    public List<BluetoothDevice> getConnectedDevices(int profile) {
-        List<android.bluetooth.BluetoothDevice> devices = mWrappedInstance.getConnectedDevices(
-                profile);
-        List<BluetoothDevice> wrappedDevices = new ArrayList<>(devices.size());
-        for (android.bluetooth.BluetoothDevice device : devices) {
-            wrappedDevices.add(BluetoothDevice.wrap(device));
-        }
-        return wrappedDevices;
-    }
-
-    /** See {@link android.bluetooth.BluetoothManager#getAdapter()}. */
-    public BluetoothAdapter getAdapter() {
-        return BluetoothAdapter.wrap(mWrappedInstance.getAdapter());
-    }
-
-    public static BluetoothManager wrap(android.bluetooth.BluetoothManager bluetoothManager) {
-        return new BluetoothManager(bluetoothManager);
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/RfcommServer.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/RfcommServer.java
deleted file mode 100644
index 9ed95ac..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/RfcommServer.java
+++ /dev/null
@@ -1,419 +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.nearby.fastpair.provider.bluetooth;
-
-import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.ACCEPTING;
-import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.CONNECTED;
-import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.RESTARTING;
-import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.STARTING;
-import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.STOPPED;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothServerSocket;
-import android.bluetooth.BluetoothSocket;
-import android.nearby.fastpair.provider.EventStreamProtocol;
-import android.nearby.fastpair.provider.utils.Logger;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-/**
- * Listens for a rfcomm client to connect and supports both sending messages to the client and
- * receiving messages from the client.
- */
-public class RfcommServer {
-    private static final String TAG = "RfcommServer";
-    private final Logger mLogger = new Logger(TAG);
-
-    private static final String FAST_PAIR_RFCOMM_SERVICE_NAME = "FastPairServer";
-    public static final UUID FAST_PAIR_RFCOMM_UUID =
-            UUID.fromString("df21fe2c-2515-4fdb-8886-f12c4d67927c");
-
-    /** A single thread executor where all state checks are performed. */
-    private final ExecutorService mControllerExecutor = Executors.newSingleThreadExecutor();
-
-    private final ExecutorService mSendMessageExecutor = Executors.newSingleThreadExecutor();
-    private final ExecutorService mReceiveMessageExecutor = Executors.newSingleThreadExecutor();
-
-    @Nullable
-    private BluetoothServerSocket mServerSocket;
-    @Nullable
-    private BluetoothSocket mSocket;
-
-    private State mState = STOPPED;
-    private boolean mIsStopRequested = false;
-
-    @Nullable
-    private RequestHandler mRequestHandler;
-
-    @Nullable
-    private CountDownLatch mCountDownLatch;
-    @Nullable
-    private StateMonitor mStateMonitor;
-
-    /**
-     * Manages RfcommServer status.
-     *
-     * <pre>{@code
-     *      +------------------------------------------------+
-     *      +-------------------------------+                |
-     *      v                               |                |
-     * +---------+    +----------+    +-----+-----+    +-----+-----+
-     * | STOPPED +--> | STARTING +--> | ACCEPTING +--> | CONNECTED |
-     * +---------+    +-----+----+    +-------+---+    +-----+-----+
-     *      ^               |             ^   v              |
-     *      +---------------+         +---+--------+         |
-     *                                | RESTARTING | <-------+
-     *                                +------------+
-     * }</pre>
-     *
-     * If Stop action is not requested, the server will restart forever. Otherwise, go stopped.
-     */
-    public enum State {
-        STOPPED,
-        STARTING,
-        RESTARTING,
-        ACCEPTING,
-        CONNECTED,
-    }
-
-    /** Starts the rfcomm server. */
-    public void start() {
-        runInControllerExecutor(this::startServer);
-    }
-
-    private void startServer() {
-        log("Start RfcommServer");
-
-        if (!mState.equals(STOPPED)) {
-            log("Server is not stopped, skip start request.");
-            return;
-        }
-        updateState(STARTING);
-        mIsStopRequested = false;
-
-        startAccept();
-    }
-
-    private void restartServer() {
-        log("Restart RfcommServer");
-        updateState(RESTARTING);
-        startAccept();
-    }
-
-    private void startAccept() {
-        try {
-            // Gets server socket in controller thread for stop() API.
-            mServerSocket =
-                    BluetoothAdapter.getDefaultAdapter()
-                            .listenUsingRfcommWithServiceRecord(
-                                    FAST_PAIR_RFCOMM_SERVICE_NAME, FAST_PAIR_RFCOMM_UUID);
-        } catch (IOException e) {
-            log("Create service record failed, stop server");
-            stopServer();
-            return;
-        }
-
-        updateState(ACCEPTING);
-        new Thread(() -> accept(mServerSocket)).start();
-    }
-
-    private void accept(BluetoothServerSocket serverSocket) {
-        triggerCountdownLatch();
-
-        try {
-            BluetoothSocket socket = serverSocket.accept();
-            serverSocket.close();
-
-            runInControllerExecutor(() -> startListen(socket));
-        } catch (IOException e) {
-            log("IOException when accepting new connection");
-            runInControllerExecutor(() -> handleAcceptException(serverSocket));
-        }
-    }
-
-    private void handleAcceptException(BluetoothServerSocket serverSocket) {
-        if (mIsStopRequested) {
-            stopServer();
-        } else {
-            closeServerSocket(serverSocket);
-            restartServer();
-        }
-    }
-
-    private void startListen(BluetoothSocket bluetoothSocket) {
-        if (mIsStopRequested) {
-            closeSocket(bluetoothSocket);
-            stopServer();
-            return;
-        }
-
-        updateState(CONNECTED);
-        // Sets method parameter to global socket for stop() API.
-        this.mSocket = bluetoothSocket;
-        new Thread(() -> listen(bluetoothSocket)).start();
-    }
-
-    private void listen(BluetoothSocket bluetoothSocket) {
-        triggerCountdownLatch();
-
-        try {
-            DataInputStream dataInputStream = new DataInputStream(bluetoothSocket.getInputStream());
-            while (true) {
-                int eventGroup = dataInputStream.readUnsignedByte();
-                int eventCode = dataInputStream.readUnsignedByte();
-                int additionalLength = dataInputStream.readUnsignedShort();
-
-                byte[] data = new byte[additionalLength];
-                if (additionalLength > 0) {
-                    int count = 0;
-                    do {
-                        count += dataInputStream.read(data, count, additionalLength - count);
-                    } while (count < additionalLength);
-                }
-
-                if (mRequestHandler != null) {
-                    // In order not to block listening thread, use different thread to dispatch
-                    // message.
-                    mReceiveMessageExecutor.execute(
-                            () -> {
-                                mRequestHandler.handleRequest(eventGroup, eventCode, data);
-                                triggerCountdownLatch();
-                            });
-                }
-            }
-        } catch (IOException e) {
-            log(
-                    String.format(
-                            "IOException when listening to %s",
-                            bluetoothSocket.getRemoteDevice().getAddress()));
-            runInControllerExecutor(() -> handleListenException(bluetoothSocket));
-        }
-    }
-
-    private void handleListenException(BluetoothSocket bluetoothSocket) {
-        if (mIsStopRequested) {
-            stopServer();
-        } else {
-            closeSocket(bluetoothSocket);
-            restartServer();
-        }
-    }
-
-    public void sendFakeEventStreamMessage(EventStreamProtocol.EventGroup eventGroup) {
-        switch (eventGroup) {
-            case BLUETOOTH:
-                send(EventStreamProtocol.EventGroup.BLUETOOTH_VALUE,
-                        EventStreamProtocol.BluetoothEventCode.BLUETOOTH_ENABLE_SILENCE_MODE_VALUE,
-                        new byte[0]);
-                break;
-            case LOGGING:
-                send(EventStreamProtocol.EventGroup.LOGGING_VALUE,
-                        EventStreamProtocol.LoggingEventCode.LOG_FULL_VALUE,
-                        new byte[0]);
-                break;
-            case DEVICE:
-                send(EventStreamProtocol.EventGroup.DEVICE_VALUE,
-                        EventStreamProtocol.DeviceEventCode.DEVICE_BATTERY_INFO_VALUE,
-                        new byte[]{0x11, 0x12, 0x13});
-                break;
-            default: // fall out
-        }
-    }
-
-    public void sendFakeEventStreamLoggingMessage(@Nullable String logContent) {
-        send(EventStreamProtocol.EventGroup.LOGGING_VALUE,
-                EventStreamProtocol.LoggingEventCode.LOG_SAVE_TO_BUFFER_VALUE,
-                logContent != null ? logContent.getBytes(UTF_8) : new byte[0]);
-    }
-
-    public void send(int eventGroup, int eventCode, byte[] data) {
-        runInControllerExecutor(
-                () -> {
-                    if (!CONNECTED.equals(mState)) {
-                        log("Server is not in CONNECTED state, skip send request");
-                        return;
-                    }
-                    BluetoothSocket bluetoothSocket = this.mSocket;
-                    mSendMessageExecutor.execute(() -> {
-                        String address = bluetoothSocket.getRemoteDevice().getAddress();
-                        try {
-                            DataOutputStream dataOutputStream =
-                                    new DataOutputStream(bluetoothSocket.getOutputStream());
-                            dataOutputStream.writeByte(eventGroup);
-                            dataOutputStream.writeByte(eventCode);
-                            dataOutputStream.writeShort(data.length);
-                            if (data.length > 0) {
-                                dataOutputStream.write(data);
-                            }
-                            dataOutputStream.flush();
-                            log(
-                                    String.format(
-                                            "Send message to %s: %s, %s, %s.",
-                                            address, eventGroup, eventCode, data.length));
-                        } catch (IOException e) {
-                            log(
-                                    String.format(
-                                            "Failed to send message to %s: %s, %s, %s.",
-                                            address, eventGroup, eventCode, data.length),
-                                    e);
-                        }
-                    });
-                });
-    }
-
-    /** Stops the rfcomm server. */
-    public void stop() {
-        runInControllerExecutor(() -> {
-            log("Stop RfcommServer");
-
-            if (STOPPED.equals(mState)) {
-                log("Server is stopped, skip stop request.");
-                return;
-            }
-
-            if (mIsStopRequested) {
-                log("Stop is already requested, skip stop request.");
-                return;
-            }
-            mIsStopRequested = true;
-
-            if (ACCEPTING.equals(mState)) {
-                closeServerSocket(mServerSocket);
-            }
-
-            if (CONNECTED.equals(mState)) {
-                closeSocket(mSocket);
-            }
-        });
-    }
-
-    private void stopServer() {
-        updateState(STOPPED);
-        triggerCountdownLatch();
-    }
-
-    private void updateState(State newState) {
-        log(String.format("Change state from %s to %s", mState, newState));
-        if (mStateMonitor != null) {
-            mStateMonitor.onStateChanged(newState);
-        }
-        mState = newState;
-    }
-
-    private void closeServerSocket(BluetoothServerSocket serverSocket) {
-        try {
-            if (serverSocket != null) {
-                log(String.format("Close server socket: %s", serverSocket));
-                serverSocket.close();
-            }
-        } catch (IOException | NullPointerException e) {
-            // NullPointerException is used to skip robolectric test failure.
-            // In unit test, different virtual devices are set up in different threads, calling
-            // ServerSocket.close() in wrong thread will result in NullPointerException since there
-            // is no corresponding service record.
-            // TODO(hylo): Remove NullPointerException when the solution is submitted to test cases.
-            log("Failed to stop server", e);
-        }
-    }
-
-    private void closeSocket(BluetoothSocket socket) {
-        try {
-            if (socket != null && socket.isConnected()) {
-                log(String.format("Close socket: %s", socket.getRemoteDevice().getAddress()));
-                socket.close();
-            }
-        } catch (IOException e) {
-            log(String.format("IOException when close socket %s",
-                    socket.getRemoteDevice().getAddress()));
-        }
-    }
-
-    private void runInControllerExecutor(Runnable runnable) {
-        mControllerExecutor.execute(runnable);
-    }
-
-    private void log(String message) {
-        mLogger.log("Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message);
-    }
-
-    private void log(String message, Throwable e) {
-        mLogger.log(e, "Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message);
-    }
-
-    private void triggerCountdownLatch() {
-        if (mCountDownLatch != null) {
-            mCountDownLatch.countDown();
-        }
-    }
-
-    /** Interface to handle incoming request from clients. */
-    public interface RequestHandler {
-        void handleRequest(int eventGroup, int eventCode, byte[] data);
-    }
-
-    public void setRequestHandler(@Nullable RequestHandler requestHandler) {
-        this.mRequestHandler = requestHandler;
-    }
-
-    /** A state monitor to send signal when state is changed. */
-    public interface StateMonitor {
-        void onStateChanged(State state);
-    }
-
-    public void setStateMonitor(@Nullable StateMonitor stateMonitor) {
-        this.mStateMonitor = stateMonitor;
-    }
-
-    @VisibleForTesting
-    void setCountDownLatch(@Nullable CountDownLatch countDownLatch) {
-        this.mCountDownLatch = countDownLatch;
-    }
-
-    @VisibleForTesting
-    void setIsStopRequested(boolean isStopRequested) {
-        this.mIsStopRequested = isStopRequested;
-    }
-
-    @VisibleForTesting
-    void simulateAcceptIOException() {
-        runInControllerExecutor(() -> {
-            if (ACCEPTING.equals(mState)) {
-                closeServerSocket(mServerSocket);
-            }
-        });
-    }
-
-    @VisibleForTesting
-    void simulateListenIOException() {
-        runInControllerExecutor(() -> {
-            if (CONNECTED.equals(mState)) {
-                closeSocket(mSocket);
-            }
-        });
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/Crypto.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/Crypto.java
deleted file mode 100644
index 0aa4f6e..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/Crypto.java
+++ /dev/null
@@ -1,70 +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.nearby.fastpair.provider.crypto;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
-
-import android.annotation.SuppressLint;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.protobuf.ByteString;
-
-import java.nio.ByteBuffer;
-import java.security.GeneralSecurityException;
-
-import javax.crypto.Cipher;
-import javax.crypto.spec.SecretKeySpec;
-
-/** Cryptography utilities for ephemeral IDs. */
-public final class Crypto {
-    private static final int AES_BLOCK_SIZE = 16;
-    private static final ImmutableSet<Integer> VALID_AES_KEY_SIZES = ImmutableSet.of(16, 24, 32);
-    private static final String AES_ECB_NOPADDING_ENCRYPTION_ALGO = "AES/ECB/NoPadding";
-    private static final String AES_ENCRYPTION_ALGO = "AES";
-
-    /** Encrypts the provided data with the provided key using the AES/ECB/NoPadding algorithm. */
-    public static ByteString aesEcbNoPaddingEncrypt(ByteString key, ByteString data) {
-        return aesEcbOperation(key, data, Cipher.ENCRYPT_MODE);
-    }
-
-    /** Decrypts the provided data with the provided key using the AES/ECB/NoPadding algorithm. */
-    public static ByteString aesEcbNoPaddingDecrypt(ByteString key, ByteString data) {
-        return aesEcbOperation(key, data, Cipher.DECRYPT_MODE);
-    }
-
-    @SuppressLint("GetInstance")
-    private static ByteString aesEcbOperation(ByteString key, ByteString data, int operation) {
-        checkArgument(VALID_AES_KEY_SIZES.contains(key.size()));
-        checkArgument(data.size() % AES_BLOCK_SIZE == 0);
-        try {
-            Cipher aesCipher = Cipher.getInstance(AES_ECB_NOPADDING_ENCRYPTION_ALGO);
-            SecretKeySpec secretKeySpec = new SecretKeySpec(key.toByteArray(), AES_ENCRYPTION_ALGO);
-            aesCipher.init(operation, secretKeySpec);
-            ByteBuffer output = ByteBuffer.allocate(data.size());
-            checkState(aesCipher.doFinal(data.asReadOnlyByteBuffer(), output) == data.size());
-            output.rewind();
-            return ByteString.copyFrom(output);
-        } catch (GeneralSecurityException e) {
-            // Should never happen.
-            throw new AssertionError(e);
-        }
-    }
-
-    private Crypto() {
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/E2eeCalculator.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/E2eeCalculator.java
deleted file mode 100644
index 794c19d..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/E2eeCalculator.java
+++ /dev/null
@@ -1,188 +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.nearby.fastpair.provider.crypto;
-
-import com.google.common.base.Preconditions;
-import com.google.common.base.Verify;
-import com.google.common.primitives.Bytes;
-import com.google.common.primitives.Ints;
-import com.google.protobuf.ByteString;
-
-import java.math.BigInteger;
-import java.security.spec.ECFieldFp;
-import java.security.spec.ECParameterSpec;
-import java.security.spec.ECPoint;
-import java.security.spec.EllipticCurve;
-import java.util.Collections;
-
-/** Provides methods for calculating E2EE EIDs and E2E encryption/decryption based on E2EE EIDs. */
-public final class E2eeCalculator {
-
-    private static final byte[] TEMP_KEY_PADDING_1 =
-            Bytes.toArray(Collections.nCopies(11, (byte) 0xFF));
-    private static final byte[] TEMP_KEY_PADDING_2 = new byte[11];
-    private static final ECParameterSpec CURVE_SPEC = getCurveSpec();
-    private static final BigInteger P = ((ECFieldFp) CURVE_SPEC.getCurve().getField()).getP();
-    private static final BigInteger TWO = new BigInteger("2");
-    private static final BigInteger THREE = new BigInteger("3");
-    private static final int E2EE_EID_IDENTITY_KEY_SIZE = 32;
-    private static final int E2EE_EID_SIZE = 20;
-
-    /**
-     * Computes the E2EE EID value for the given device clock based time. Note that Eddystone
-     * beacons start advertising the new EID at a random time within the window, therefore the
-     * currently advertised EID for beacon time <em>t</em> may be either
-     * {@code computeE2eeEid(eik, k, t)} or {@code computeE2eeEid(eik, k, t - (1 << k))}.
-     *
-     * <p>The E2EE EID computation is based on https://goto.google.com/e2ee-eid-computation.
-     *
-     * @param identityKey        the beacon's 32-byte Eddystone E2EE identity key
-     * @param exponent           rotation period exponent as configured on the beacon, must be in
-     *                           range the [0,15]
-     * @param deviceClockSeconds the value of the beacon's 32-bit seconds time counter (treated as
-     *                           an unsigned value)
-     * @return E2EE EID value.
-     */
-    public static ByteString computeE2eeEid(
-            ByteString identityKey, int exponent, int deviceClockSeconds) {
-        return computePublicKey(computePrivateKey(identityKey, exponent, deviceClockSeconds));
-    }
-
-    private static ByteString computePublicKey(BigInteger privateKey) {
-        return getXCoordinateBytes(toPoint(privateKey));
-    }
-
-    private static BigInteger computePrivateKey(
-            ByteString identityKey, int exponent, int deviceClockSeconds) {
-        Preconditions.checkArgument(
-                Preconditions.checkNotNull(identityKey).size() == E2EE_EID_IDENTITY_KEY_SIZE);
-        Preconditions.checkArgument(exponent >= 0 && exponent < 16);
-
-        byte[] exponentByte = new byte[]{(byte) exponent};
-        byte[] paddedCounter = Ints.toByteArray((deviceClockSeconds >>> exponent) << exponent);
-        byte[] data =
-                Bytes.concat(
-                        TEMP_KEY_PADDING_1,
-                        exponentByte,
-                        paddedCounter,
-                        TEMP_KEY_PADDING_2,
-                        exponentByte,
-                        paddedCounter);
-
-        byte[] rTag =
-                Crypto.aesEcbNoPaddingEncrypt(identityKey, ByteString.copyFrom(data)).toByteArray();
-        return new BigInteger(1, rTag).mod(CURVE_SPEC.getOrder());
-    }
-
-    private static ECPoint toPoint(BigInteger privateKey) {
-        return multiplyPoint(CURVE_SPEC.getGenerator(), privateKey);
-    }
-
-    private static ByteString getXCoordinateBytes(ECPoint point) {
-        byte[] unalignedBytes = point.getAffineX().toByteArray();
-
-        // The unalignedBytes may have length < 32 if the leading E2EE EID bytes are zero, or
-        // it may be E2EE_EID_SIZE + 1 if the leading bit is 1, in which case the first byte is
-        // always zero.
-        Verify.verify(
-                unalignedBytes.length <= E2EE_EID_SIZE
-                        || (unalignedBytes.length == E2EE_EID_SIZE + 1 && unalignedBytes[0] == 0));
-
-        byte[] bytes;
-        if (unalignedBytes.length < E2EE_EID_SIZE) {
-            bytes = new byte[E2EE_EID_SIZE];
-            System.arraycopy(
-                    unalignedBytes, 0, bytes, bytes.length - unalignedBytes.length,
-                    unalignedBytes.length);
-        } else if (unalignedBytes.length == E2EE_EID_SIZE + 1) {
-            bytes = new byte[E2EE_EID_SIZE];
-            System.arraycopy(unalignedBytes, 1, bytes, 0, E2EE_EID_SIZE);
-        } else { // unalignedBytes.length ==  GattE2EE_EID_SIZE
-            bytes = unalignedBytes;
-        }
-        return ByteString.copyFrom(bytes);
-    }
-
-    /** Returns a secp160r1 curve spec. */
-    private static ECParameterSpec getCurveSpec() {
-        final BigInteger p = new BigInteger("ffffffffffffffffffffffffffffffff7fffffff", 16);
-        final BigInteger n = new BigInteger("0100000000000000000001f4c8f927aed3ca752257", 16);
-        final BigInteger a = new BigInteger("ffffffffffffffffffffffffffffffff7ffffffc", 16);
-        final BigInteger b = new BigInteger("1c97befc54bd7a8b65acf89f81d4d4adc565fa45", 16);
-        final BigInteger gx = new BigInteger("4a96b5688ef573284664698968c38bb913cbfc82", 16);
-        final BigInteger gy = new BigInteger("23a628553168947d59dcc912042351377ac5fb32", 16);
-        final int h = 1;
-        ECFieldFp fp = new ECFieldFp(p);
-        EllipticCurve spec = new EllipticCurve(fp, a, b);
-        ECPoint g = new ECPoint(gx, gy);
-        return new ECParameterSpec(spec, g, n, h);
-    }
-
-    /** Returns the scalar multiplication result of k*p in Fp. */
-    private static ECPoint multiplyPoint(ECPoint p, BigInteger k) {
-        ECPoint r = ECPoint.POINT_INFINITY;
-        ECPoint s = p;
-        BigInteger kModP = k.mod(P);
-        int length = kModP.bitLength();
-        for (int i = 0; i <= length - 1; i++) {
-            if (kModP.mod(TWO).byteValue() == 1) {
-                r = addPoint(r, s);
-            }
-            s = doublePoint(s);
-            kModP = kModP.divide(TWO);
-        }
-        return r;
-    }
-
-    /** Returns the point addition r+s in Fp. */
-    private static ECPoint addPoint(ECPoint r, ECPoint s) {
-        if (r.equals(s)) {
-            return doublePoint(r);
-        } else if (r.equals(ECPoint.POINT_INFINITY)) {
-            return s;
-        } else if (s.equals(ECPoint.POINT_INFINITY)) {
-            return r;
-        }
-        BigInteger slope =
-                r.getAffineY()
-                        .subtract(s.getAffineY())
-                        .multiply(r.getAffineX().subtract(s.getAffineX()).modInverse(P))
-                        .mod(P);
-        BigInteger x =
-                slope.modPow(TWO, P).subtract(r.getAffineX()).subtract(s.getAffineX()).mod(P);
-        BigInteger y = s.getAffineY().negate().mod(P);
-        y = y.add(slope.multiply(s.getAffineX().subtract(x))).mod(P);
-        return new ECPoint(x, y);
-    }
-
-    /** Returns the point doubling 2*r in Fp. */
-    private static ECPoint doublePoint(ECPoint r) {
-        if (r.equals(ECPoint.POINT_INFINITY)) {
-            return r;
-        }
-        BigInteger slope = r.getAffineX().pow(2).multiply(THREE);
-        slope = slope.add(CURVE_SPEC.getCurve().getA());
-        slope = slope.multiply(r.getAffineY().multiply(TWO).modInverse(P));
-        BigInteger x = slope.pow(2).subtract(r.getAffineX().multiply(TWO)).mod(P);
-        BigInteger y =
-                r.getAffineY().negate().add(slope.multiply(r.getAffineX().subtract(x))).mod(P);
-        return new ECPoint(x, y);
-    }
-
-    private E2eeCalculator() {
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Logger.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Logger.java
deleted file mode 100644
index 794f100..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Logger.java
+++ /dev/null
@@ -1,50 +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.nearby.fastpair.provider.utils;
-
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.google.errorprone.annotations.FormatMethod;
-
-/**
- * The base context for a logging statement.
- */
-public class Logger {
-    private final String mString;
-
-    public Logger(String tag) {
-        this.mString = tag;
-    }
-
-    @FormatMethod
-    public void log(String message, Object... objects) {
-        log(null, message, objects);
-    }
-
-    /** Logs to the console. */
-    @FormatMethod
-    public void log(@Nullable Throwable exception, String message, Object... objects) {
-        if (exception == null) {
-            Log.i(mString, String.format(message, objects));
-        } else {
-            Log.w(mString, String.format(message, objects));
-            Log.w(mString, String.format("Cause: %s", exception));
-        }
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/Android.bp b/nearby/tests/multidevices/clients/test_support/snippet_helper/Android.bp
deleted file mode 100644
index 697c88d..0000000
--- a/nearby/tests/multidevices/clients/test_support/snippet_helper/Android.bp
+++ /dev/null
@@ -1,24 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_library {
-    name: "MoblySnippetHelperLib",
-    srcs: ["src/**/*.kt"],
-    sdk_version: "test_current",
-    static_libs: ["mobly-snippet-lib",],
-}
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_support/snippet_helper/AndroidManifest.xml
deleted file mode 100644
index 4858f46..0000000
--- a/nearby/tests/multidevices/clients/test_support/snippet_helper/AndroidManifest.xml
+++ /dev/null
@@ -1,21 +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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.google.android.mobly.snippet.util">
-
-</manifest>
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/src/com/google/android/mobly/snippet/util/SnippetEventHelper.kt b/nearby/tests/multidevices/clients/test_support/snippet_helper/src/com/google/android/mobly/snippet/util/SnippetEventHelper.kt
deleted file mode 100644
index 0dbcb57..0000000
--- a/nearby/tests/multidevices/clients/test_support/snippet_helper/src/com/google/android/mobly/snippet/util/SnippetEventHelper.kt
+++ /dev/null
@@ -1,43 +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.google.android.mobly.snippet.util
-
-import android.os.Bundle
-import com.google.android.mobly.snippet.event.EventCache
-import com.google.android.mobly.snippet.event.SnippetEvent
-
-/**
- * Posts an {@link SnippetEvent} to the event cache with data bundle [fill] by the given function.
- *
- * This is a helper function to make your client side codes more concise. Sample usage:
- * ```
- *   postSnippetEvent(callbackId, "onReceiverFound") {
- *     putLong("discoveryTimeMs", discoveryTimeMs)
- *     putBoolean("isKnown", isKnown)
- *   }
- * ```
- *
- * @param callbackId the callbackId passed to the {@link
- * com.google.android.mobly.snippet.rpc.AsyncRpc} method.
- * @param eventName the name of the event.
- * @param fill the function to fill the data bundle.
- */
-fun postSnippetEvent(callbackId: String, eventName: String, fill: Bundle.() -> Unit) {
-  val eventData = Bundle().apply(fill)
-  val snippetEvent = SnippetEvent(callbackId, eventName).apply { data.putAll(eventData) }
-  EventCache.getInstance().postEvent(snippetEvent)
-}
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/Android.bp b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/Android.bp
deleted file mode 100644
index ec0392c..0000000
--- a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/Android.bp
+++ /dev/null
@@ -1,39 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-// Run the tests: atest --host MoblySnippetHelperRoboTest
-android_robolectric_test {
-    name: "MoblySnippetHelperRoboTest",
-    srcs: ["src/**/*.kt"],
-    instrumentation_for: "NearbyMultiDevicesClientsSnippets",
-    java_resources: ["robolectric.properties"],
-
-    static_libs: [
-        "MoblySnippetHelperLib",
-        "androidx.test.ext.junit",
-        "androidx.test.rules",
-        "junit",
-        "mobly-snippet-lib",
-        "truth-prebuilt",
-    ],
-    test_options: {
-        // timeout in seconds.
-        timeout: 36000,
-    },
-    upstream: true,
-}
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/AndroidManifest.xml
deleted file mode 100644
index f1fef23..0000000
--- a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,19 +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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.google.android.mobly.snippet.util"/>
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/robolectric.properties b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/robolectric.properties
deleted file mode 100644
index 2ea03bb..0000000
--- a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/robolectric.properties
+++ /dev/null
@@ -1,16 +0,0 @@
-#
-# Copyright (C) 2022 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-sdk=NEWEST_SDK
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/src/com/google/android/mobly/snippet/util/SnippetEventHelperTest.kt b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/src/com/google/android/mobly/snippet/util/SnippetEventHelperTest.kt
deleted file mode 100644
index 641ab82..0000000
--- a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/src/com/google/android/mobly/snippet/util/SnippetEventHelperTest.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 com.google.android.mobly.snippet.util
-
-import androidx.test.platform.app.InstrumentationRegistry
-import com.google.android.mobly.snippet.event.EventSnippet
-import com.google.android.mobly.snippet.util.Log
-import com.google.common.truth.Truth.assertThat
-import org.json.JSONObject
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
-
-/** Robolectric tests for SnippetEventHelper.kt. */
-@RunWith(RobolectricTestRunner::class)
-class SnippetEventHelperTest {
-
-  @Test
-  fun testPostSnippetEvent_withDataBundle_writesEventCache() {
-    val testCallbackId = "test_1234"
-    val testEventName = "onTestEvent"
-    val testBundleDataStrKey = "testStrKey"
-    val testBundleDataStrValue = "testStrValue"
-    val testBundleDataIntKey = "testIntKey"
-    val testBundleDataIntValue = 777
-    val eventSnippet = EventSnippet()
-    Log.initLogTag(InstrumentationRegistry.getInstrumentation().context)
-
-    postSnippetEvent(testCallbackId, testEventName) {
-      putString(testBundleDataStrKey, testBundleDataStrValue)
-      putInt(testBundleDataIntKey, testBundleDataIntValue)
-    }
-
-    val event = eventSnippet.eventWaitAndGet(testCallbackId, testEventName, null)
-    assertThat(event.getJSONObject("data").toString())
-      .isEqualTo(
-        JSONObject()
-          .put(testBundleDataIntKey, testBundleDataIntValue)
-          .put(testBundleDataStrKey, testBundleDataStrValue)
-          .toString()
-      )
-  }
-}
diff --git a/nearby/tests/multidevices/host/Android.bp b/nearby/tests/multidevices/host/Android.bp
deleted file mode 100644
index b6c1c9d..0000000
--- a/nearby/tests/multidevices/host/Android.bp
+++ /dev/null
@@ -1,54 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-// Run the tests: atest -v NearbyMultiDevicesTestSuite
-// Check go/run-nearby-mainline-e2e for more details.
-python_test_host {
-    name: "NearbyMultiDevicesTestSuite",
-    main: "suite_main.py",
-    srcs: ["*.py"],
-    libs: [
-        "NearbyMultiDevicesHostHelper",
-        "mobly",
-    ],
-    test_suites: [
-        "general-tests",
-        "mts-tethering",
-    ],
-    test_options: {
-        unit_test: false,
-    },
-    data: [
-        // Package the snippet with the Mobly test.
-        ":NearbyMultiDevicesClientsSnippets",
-        // Package the data provider with the Mobly test.
-        ":NearbyFastPairSeekerDataProvider",
-        // Package the JSON metadata with the Mobly test.
-        "test_data/**/*",
-    ],
-    version: {
-        py3: {
-            embedded_launcher: true,
-        },
-    },
-}
-
-python_library_host {
-    name: "NearbyMultiDevicesHostHelper",
-    srcs: ["test_helper/*.py"],
-}
diff --git a/nearby/tests/multidevices/host/AndroidTest.xml b/nearby/tests/multidevices/host/AndroidTest.xml
deleted file mode 100644
index fff0ed1..0000000
--- a/nearby/tests/multidevices/host/AndroidTest.xml
+++ /dev/null
@@ -1,137 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2021 The Android Open Source Project
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-          http://www.apache.org/licenses/LICENSE-2.0
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<configuration description="Config for CTS Nearby Mainline multi devices end-to-end test suite">
-    <!-- Only run tests if the device under test is SDK version 33 (Android 13) or above. -->
-    <object type="module_controller"
-            class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController" />
-    <!-- Only run NearbyMultiDevicesTestSuite in MTS if the Nearby 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>
-
-    <option name="test-suite-tag" value="apct" />
-    <option name="test-tag" value="NearbyMultiDevicesTestSuite" />
-    <option name="config-descriptor:metadata" key="component" value="wifi" />
-    <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="not_secondary_user" />
-
-    <device name="device1">
-        <!-- For coverage to work, the APK should not be uninstalled until after coverage is pulled.
-             So it's a lot easier to install APKs outside the python code.
-        -->
-        <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
-        <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
-            <option name="remount-system" value="true" />
-            <option name="push" value="NearbyMultiDevicesClientsSnippets.apk->/system/app/NearbyMultiDevicesClientsSnippets/NearbyMultiDevicesClientsSnippets.apk" />
-            <option name="push" value="NearbyFastPairSeekerDataProvider.apk->/system/app/NearbyFastPairSeekerDataProvider/NearbyFastPairSeekerDataProvider.apk" />
-        </target_preparer>
-        <target_preparer class="com.android.tradefed.targetprep.RebootTargetPreparer" />
-        <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-            <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
-            <option name="run-command" value="wm dismiss-keyguard" />
-        </target_preparer>
-        <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
-            <option name="force-skip-system-props" value="true" /> <!-- avoid restarting device -->
-            <option name="screen-always-on" value="on" />
-            <!-- List permissions requested by the APK: aapt d permissions <PATH_TO_YOUR_APK> -->
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADMIN" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADVERTISE" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_CONNECT" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_PRIVILEGED" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_SCAN" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.INTERNET" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.GET_ACCOUNTS" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.WRITE_SECURE_SETTINGS" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.REORDER_TASKS" />
-        </target_preparer>
-    </device>
-    <device name="device2">
-        <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
-        <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
-            <option name="remount-system" value="true" />
-            <option name="push" value="NearbyMultiDevicesClientsSnippets.apk->/system/app/NearbyMultiDevicesClientsSnippets/NearbyMultiDevicesClientsSnippets.apk" />
-            <option name="push" value="NearbyFastPairSeekerDataProvider.apk->/system/app/NearbyFastPairSeekerDataProvider/NearbyFastPairSeekerDataProvider.apk" />
-        </target_preparer>
-        <target_preparer class="com.android.tradefed.targetprep.RebootTargetPreparer" />
-        <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-            <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
-            <option name="run-command" value="wm dismiss-keyguard" />
-        </target_preparer>
-        <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
-            <option name="force-skip-system-props" value="true" /> <!-- avoid restarting device -->
-            <option name="screen-always-on" value="on" />
-            <!-- List permissions requested by the APK: aapt d permissions <PATH_TO_YOUR_APK> -->
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADMIN" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADVERTISE" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_CONNECT" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_PRIVILEGED" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_SCAN" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.INTERNET" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.GET_ACCOUNTS" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.WRITE_SECURE_SETTINGS" />
-            <option
-                name="run-command"
-                value="pm grant android.nearby.multidevices android.permission.REORDER_TASKS" />
-        </target_preparer>
-    </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="NearbyMultiDevicesTestSuite" />
-      <!-- Timeout limit in milliseconds for all test cases of the python binary -->
-      <option name="mobly-test-timeout" value="60000" />
-    </test>
-</configuration>
-
diff --git a/nearby/tests/multidevices/host/initial_pairing_test.py b/nearby/tests/multidevices/host/initial_pairing_test.py
deleted file mode 100644
index 1a49045..0000000
--- a/nearby/tests/multidevices/host/initial_pairing_test.py
+++ /dev/null
@@ -1,62 +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.
-
-"""CTS-V Nearby Mainline Fast Pair end-to-end test case: initial pairing test."""
-
-from test_helper import constants
-from test_helper import fast_pair_base_test
-
-# The model ID to simulate on provider side.
-PROVIDER_SIMULATOR_MODEL_ID = constants.DEFAULT_MODEL_ID
-# The public key to simulate as registered headsets.
-PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY = constants.DEFAULT_ANTI_SPOOFING_KEY
-# The anti-spoof key device metadata JSON file for data provider at seeker side.
-PROVIDER_SIMULATOR_KDM_JSON_FILE = constants.DEFAULT_KDM_JSON_FILE
-
-# Time in seconds for events waiting.
-SETUP_TIMEOUT_SEC = constants.SETUP_TIMEOUT_SEC
-BECOME_DISCOVERABLE_TIMEOUT_SEC = constants.BECOME_DISCOVERABLE_TIMEOUT_SEC
-START_ADVERTISING_TIMEOUT_SEC = constants.START_ADVERTISING_TIMEOUT_SEC
-HALF_SHEET_POPUP_TIMEOUT_SEC = constants.HALF_SHEET_POPUP_TIMEOUT_SEC
-MANAGE_ACCOUNT_DEVICE_TIMEOUT_SEC = constants.AVERAGE_PAIRING_TIMEOUT_SEC * 2
-
-
-class InitialPairingTest(fast_pair_base_test.FastPairBaseTest):
-    """Fast Pair initial pairing test."""
-
-    def setup_test(self) -> None:
-        super().setup_test()
-        self._provider.start_model_id_advertising(PROVIDER_SIMULATOR_MODEL_ID,
-                                                  PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY)
-        self._provider.wait_for_discoverable_mode(BECOME_DISCOVERABLE_TIMEOUT_SEC)
-        self._provider.wait_for_advertising_start(START_ADVERTISING_TIMEOUT_SEC)
-        self._seeker.put_anti_spoof_key_device_metadata(PROVIDER_SIMULATOR_MODEL_ID,
-                                                        PROVIDER_SIMULATOR_KDM_JSON_FILE)
-        self._seeker.set_fast_pair_scan_enabled(True)
-
-    # TODO(b/214015364): Remove Bluetooth bound on both sides ("Forget device").
-    def teardown_test(self) -> None:
-        self._seeker.set_fast_pair_scan_enabled(False)
-        self._provider.teardown_provider_simulator()
-        self._seeker.dismiss_halfsheet()
-        super().teardown_test()
-
-    def test_seeker_initial_pair_provider(self) -> None:
-        self._seeker.wait_and_assert_halfsheet_showed(
-            timeout_seconds=HALF_SHEET_POPUP_TIMEOUT_SEC,
-            expected_model_id=PROVIDER_SIMULATOR_MODEL_ID)
-        self._seeker.start_pairing()
-        self._seeker.wait_and_assert_account_device(
-            get_account_key_from_provider=self._provider.get_latest_received_account_key,
-            timeout_seconds=MANAGE_ACCOUNT_DEVICE_TIMEOUT_SEC)
diff --git a/nearby/tests/multidevices/host/seeker_discover_provider_test.py b/nearby/tests/multidevices/host/seeker_discover_provider_test.py
deleted file mode 100644
index 6356595..0000000
--- a/nearby/tests/multidevices/host/seeker_discover_provider_test.py
+++ /dev/null
@@ -1,52 +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.
-
-"""CTS-V Nearby Mainline Fast Pair end-to-end test case: seeker can discover the provider."""
-
-from test_helper import constants
-from test_helper import fast_pair_base_test
-
-# The model ID to simulate on provider side.
-PROVIDER_SIMULATOR_MODEL_ID = constants.DEFAULT_MODEL_ID
-# The public key to simulate as registered headsets.
-PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY = constants.DEFAULT_ANTI_SPOOFING_KEY
-
-# Time in seconds for events waiting.
-BECOME_DISCOVERABLE_TIMEOUT_SEC = constants.BECOME_DISCOVERABLE_TIMEOUT_SEC
-START_ADVERTISING_TIMEOUT_SEC = constants.START_ADVERTISING_TIMEOUT_SEC
-SCAN_TIMEOUT_SEC = constants.SCAN_TIMEOUT_SEC
-
-
-class SeekerDiscoverProviderTest(fast_pair_base_test.FastPairBaseTest):
-    """Fast Pair seeker discover provider test."""
-
-    def setup_test(self) -> None:
-        super().setup_test()
-        self._provider.start_model_id_advertising(
-            PROVIDER_SIMULATOR_MODEL_ID, PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY)
-        self._provider.wait_for_discoverable_mode(BECOME_DISCOVERABLE_TIMEOUT_SEC)
-        self._provider.wait_for_advertising_start(START_ADVERTISING_TIMEOUT_SEC)
-        self._seeker.start_scan()
-
-    def teardown_test(self) -> None:
-        self._seeker.stop_scan()
-        self._provider.teardown_provider_simulator()
-        super().teardown_test()
-
-    def test_seeker_start_scanning_find_provider(self) -> None:
-        provider_ble_mac_address = self._provider.get_ble_mac_address()
-        self._seeker.wait_and_assert_provider_found(
-            timeout_seconds=SCAN_TIMEOUT_SEC,
-            expected_model_id=PROVIDER_SIMULATOR_MODEL_ID,
-            expected_ble_mac_address=provider_ble_mac_address)
diff --git a/nearby/tests/multidevices/host/seeker_show_halfsheet_test.py b/nearby/tests/multidevices/host/seeker_show_halfsheet_test.py
deleted file mode 100644
index f6561e5..0000000
--- a/nearby/tests/multidevices/host/seeker_show_halfsheet_test.py
+++ /dev/null
@@ -1,56 +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.
-
-"""CTS-V Nearby Mainline Fast Pair end-to-end test case: seeker show half sheet UI."""
-
-from test_helper import constants
-from test_helper import fast_pair_base_test
-
-# The model ID to simulate on provider side.
-PROVIDER_SIMULATOR_MODEL_ID = constants.DEFAULT_MODEL_ID
-# The public key to simulate as registered headsets.
-PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY = constants.DEFAULT_ANTI_SPOOFING_KEY
-# The anti-spoof key device metadata JSON file for data provider at seeker side.
-PROVIDER_SIMULATOR_KDM_JSON_FILE = constants.DEFAULT_KDM_JSON_FILE
-
-# Time in seconds for events waiting.
-SETUP_TIMEOUT_SEC = constants.SETUP_TIMEOUT_SEC
-BECOME_DISCOVERABLE_TIMEOUT_SEC = constants.BECOME_DISCOVERABLE_TIMEOUT_SEC
-START_ADVERTISING_TIMEOUT_SEC = constants.START_ADVERTISING_TIMEOUT_SEC
-HALF_SHEET_POPUP_TIMEOUT_SEC = constants.HALF_SHEET_POPUP_TIMEOUT_SEC
-
-
-class SeekerShowHalfSheetTest(fast_pair_base_test.FastPairBaseTest):
-    """Fast Pair seeker show half sheet UI test."""
-
-    def setup_test(self) -> None:
-        super().setup_test()
-        self._provider.start_model_id_advertising(PROVIDER_SIMULATOR_MODEL_ID,
-                                                  PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY)
-        self._provider.wait_for_discoverable_mode(BECOME_DISCOVERABLE_TIMEOUT_SEC)
-        self._provider.wait_for_advertising_start(START_ADVERTISING_TIMEOUT_SEC)
-        self._seeker.put_anti_spoof_key_device_metadata(PROVIDER_SIMULATOR_MODEL_ID,
-                                                        PROVIDER_SIMULATOR_KDM_JSON_FILE)
-        self._seeker.set_fast_pair_scan_enabled(True)
-
-    def teardown_test(self) -> None:
-        self._seeker.set_fast_pair_scan_enabled(False)
-        self._provider.teardown_provider_simulator()
-        self._seeker.dismiss_halfsheet()
-        super().teardown_test()
-
-    def test_seeker_show_half_sheet(self) -> None:
-        self._seeker.wait_and_assert_halfsheet_showed(
-            timeout_seconds=HALF_SHEET_POPUP_TIMEOUT_SEC,
-            expected_model_id=PROVIDER_SIMULATOR_MODEL_ID)
diff --git a/nearby/tests/multidevices/host/suite_main.py b/nearby/tests/multidevices/host/suite_main.py
deleted file mode 100644
index 9a580fb..0000000
--- a/nearby/tests/multidevices/host/suite_main.py
+++ /dev/null
@@ -1,39 +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.
-
-"""The entry point for Nearby Mainline multi devices end-to-end test suite."""
-
-import logging
-import sys
-
-from mobly import suite_runner
-
-import initial_pairing_test
-import seeker_discover_provider_test
-import seeker_show_halfsheet_test
-
-_BOOTSTRAP_LOGGING_FILENAME = '/tmp/nearby_multi_devices_test_suite_log.txt'
-_TEST_CLASSES_LIST = [
-    seeker_discover_provider_test.SeekerDiscoverProviderTest,
-    seeker_show_halfsheet_test.SeekerShowHalfSheetTest,
-    initial_pairing_test.InitialPairingTest,
-]
-
-
-if __name__ == '__main__':
-    logging.basicConfig(filename=_BOOTSTRAP_LOGGING_FILENAME, level=logging.INFO)
-    if '--' in sys.argv:
-        index = sys.argv.index('--')
-        sys.argv = sys.argv[:1] + sys.argv[index + 1:]
-    suite_runner.run_suite(test_classes=_TEST_CLASSES_LIST)
diff --git a/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_account_devicemeta_json.txt b/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_account_devicemeta_json.txt
deleted file mode 100644
index d3deb40..0000000
--- a/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_account_devicemeta_json.txt
+++ /dev/null
@@ -1,47 +0,0 @@
-[
-  {
-    "account_key": "BPy5AaSyMfrFvMNgr6f7GA==",
-    "sha256_account_key_public_address": "jNGRz+Ni6ZuLd8hVF3lmGoJnF5byXBUyVi9CmnrF1so=",
-    "fast_pair_device_metadata": {
-      "image_url": "https:\/\/lh3.googleusercontent.com\/2PffmZiopo2AjT8sshX0Se3jV-91cp4yOCIay2bBvZqKoKGVy5B4uyzdHsde6UrUSGaoCqV-h4edd5ZljA4oSGc",
-      "intent_uri": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms\/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.apps.wearables.maestro.companion;end",
-      "ble_tx_power": 0,
-      "trigger_distance": 0,
-      "device_type": 0,
-      "left_bud_url": "",
-      "right_bud_url": "",
-      "case_url": "",
-      "initial_notification_description": "Tap to pair. Earbuds will be tied to %s",
-      "initial_notification_description_no_account": "Tap to pair with this device",
-      "initial_pairing_description": "Pixel Buds A-Series will appear on devices linked with ericth.nearby.dogfood@gmail.com",
-      "connect_success_companion_app_installed": "Your device is ready to be set up",
-      "connect_success_companion_app_not_installed": "Download the device app on Google Play to see all available features",
-      "subsequent_pairing_description": "Connect %s to this phone",
-      "retroactive_pairing_description": "Save device to %s for faster pairing to your other devices",
-      "wait_launch_companion_app_description": "This will take a few moments",
-      "fail_connect_go_to_settings_description": "Try manually pairing to the device by going to Settings"
-    },
-    "fast_pair_discovery_item": {
-      "id": "",
-      "mac_address": "",
-      "action_url": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms\/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.apps.wearables.maestro.companion;end",
-      "device_name": "",
-      "title": "Pixel Buds A-Series",
-      "description": "Tap to pair with this device",
-      "display_url": "",
-      "last_observation_timestamp_millis": 0,
-      "first_observation_timestamp_millis": 0,
-      "state": 1,
-      "action_url_type": 2,
-      "rssi": 0,
-      "pending_app_install_timestamp_millis": 0,
-      "tx_power": 0,
-      "app_name": "",
-      "package_name": "",
-      "trigger_id": "",
-      "icon_png": "",
-      "icon_fife_url": "https:\/\/lh3.googleusercontent.com\/2PffmZiopo2AjT8sshX0Se3jV-91cp4yOCIay2bBvZqKoKGVy5B4uyzdHsde6UrUSGaoCqV-h4edd5ZljA4oSGc",
-      "authentication_public_key_secp256r1": "z+grhW8lWVA34JUQhXOxMrk1WqVy+VpEDd2K+01ZJvS6KdV0OUg7FRMzq+ITuOqKO\/2TIRKEAEfMKdyk2Ob1Vw=="
-    }
-  }
-]
\ No newline at end of file
diff --git a/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_antispoofkey_devicemeta_json.txt b/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_antispoofkey_devicemeta_json.txt
deleted file mode 100644
index 3611b03..0000000
--- a/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_antispoofkey_devicemeta_json.txt
+++ /dev/null
@@ -1,28 +0,0 @@
-{
-  "anti_spoofing_public_key_str": "z+grhW8lWVA34JUQhXOxMrk1WqVy+VpEDd2K+01ZJvS6KdV0OUg7FRMzq+ITuOqKO\/2TIRKEAEfMKdyk2Ob1Vw==",
-  "fast_pair_device_metadata": {
-    "image_url": "https:\/\/lh3.googleusercontent.com\/2PffmZiopo2AjT8sshX0Se3jV-91cp4yOCIay2bBvZqKoKGVy5B4uyzdHsde6UrUSGaoCqV-h4edd5ZljA4oSGc",
-    "intent_uri": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms\/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.apps.wearables.maestro.companion;end",
-    "ble_tx_power": -11,
-    "trigger_distance": 0.6000000238418579,
-    "device_type": 7,
-    "name": "Pixel Buds A-Series",
-    "left_bud_url": "https:\/\/lh3.googleusercontent.com\/O8SVJ5E7CXUkpkym7ibZbp6wypuO7HaTFcslT_FjmEzJX4KHoIY_kzLTdK2kwJXiDBgg8cC__sG-JJ5aVnQtFjQ",
-    "right_bud_url": "https:\/\/lh3.googleusercontent.com\/X_FsRmEKH_fgKzvopyrlyWJAdczRel42Tih7p9-e-U48gBTaggGVQx70K27TzlqIaqYVuaNpTnGoUsKIgiy4WA",
-    "case_url": "https:\/\/lh3.googleusercontent.com\/mNZ7CGplQSpZhoY79jXDQU4B65eY2f0SndnYZLk1PSm8zKTYeRU7REmrLL_pptD6HpVI2F_oQ6xhhtZKOvB8EQ",
-    "initial_notification_description": "Tap to pair. Earbuds will be tied to %s",
-    "initial_notification_description_no_account": "Tap to pair with this device",
-    "open_companion_app_description": "Tap to finish setup",
-    "update_companion_app_description": "Tap to update device settings and finish setup",
-    "download_companion_app_description": "Tap to download device app on Google Play and see all features",
-    "unable_to_connect_title": "Unable to connect",
-    "unable_to_connect_description": "Try manually pairing to the device",
-    "initial_pairing_description": "%s will appear on devices linked with %s",
-    "connect_success_companion_app_installed": "Your device is ready to be set up",
-    "connect_success_companion_app_not_installed": "Download the device app on Google Play to see all available features",
-    "subsequent_pairing_description": "Connect %s to this phone",
-    "retroactive_pairing_description": "Save device to %s for faster pairing to your other devices",
-    "wait_launch_companion_app_description": "This will take a few moments",
-    "fail_connect_go_to_settings_description": "Try manually pairing to the device by going to Settings"
-  }
-}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/host/test_data/fastpair/simulator_account_devicemeta_json.txt b/nearby/tests/multidevices/host/test_data/fastpair/simulator_account_devicemeta_json.txt
deleted file mode 100644
index ed60860..0000000
--- a/nearby/tests/multidevices/host/test_data/fastpair/simulator_account_devicemeta_json.txt
+++ /dev/null
@@ -1,47 +0,0 @@
-[
-  {
-    "account_key": "BPy5AaSyMfrFvMNgr6f7GA==",
-    "sha256_account_key_public_address": "jNGRz+Ni6ZuLd8hVF3lmGoJnF5byXBUyVi9CmnrF1so=",
-    "fast_pair_device_metadata": {
-      "ble_tx_power": 0,
-      "case_url": "",
-      "connect_success_companion_app_installed": "Your device is ready to be set up",
-      "connect_success_companion_app_not_installed": "Download the device app on Google Play to see all available features",
-      "device_type": 0,
-      "fail_connect_go_to_settings_description": "Try manually pairing to the device by going to Settings",
-      "image_url": "https://lh3.googleusercontent.com/2PffmZiopo2AjT8sshX0Se3jV-91cp4yOCIay2bBvZqKoKGVy5B4uyzdHsde6UrUSGaoCqV-h4edd5ZljA4oSGc",
-      "initial_notification_description": "Tap to pair. Earbuds will be tied to %s",
-      "initial_notification_description_no_account": "Tap to pair with this device",
-      "initial_pairing_description": "Pixel Buds A-Series will appear on devices linked with ericth.nearby.dogfood@gmail.com",
-      "intent_uri": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.apps.wearables.maestro.companion;end",
-      "left_bud_url": "",
-      "retroactive_pairing_description": "Save device to %s for faster pairing to your other devices",
-      "right_bud_url": "",
-      "subsequent_pairing_description": "Connect %s to this phone",
-      "trigger_distance": 0,
-      "wait_launch_companion_app_description": "This will take a few moments"
-    },
-    "fast_pair_discovery_item": {
-      "action_url": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.apps.wearables.maestro.companion;end",
-      "action_url_type": 2,
-      "app_name": "",
-      "authentication_public_key_secp256r1": "z+grhW8lWVA34JUQhXOxMrk1WqVy+VpEDd2K+01ZJvS6KdV0OUg7FRMzq+ITuOqKO/2TIRKEAEfMKdyk2Ob1Vw==",
-      "description": "Tap to pair with this device",
-      "device_name": "",
-      "display_url": "",
-      "first_observation_timestamp_millis": 0,
-      "icon_fife_url": "https://lh3.googleusercontent.com/2PffmZiopo2AjT8sshX0Se3jV-91cp4yOCIay2bBvZqKoKGVy5B4uyzdHsde6UrUSGaoCqV-h4edd5ZljA4oSGc",
-      "icon_png": "",
-      "id": "",
-      "last_observation_timestamp_millis": 0,
-      "mac_address": "",
-      "package_name": "",
-      "pending_app_install_timestamp_millis": 0,
-      "rssi": 0,
-      "state": 1,
-      "title": "Pixel Buds A-Series",
-      "trigger_id": "",
-      "tx_power": 0
-    }
-  }
-]
\ No newline at end of file
diff --git a/nearby/tests/multidevices/host/test_data/fastpair/simulator_antispoofkey_devicemeta_json.txt b/nearby/tests/multidevices/host/test_data/fastpair/simulator_antispoofkey_devicemeta_json.txt
deleted file mode 100644
index fc9706a..0000000
--- a/nearby/tests/multidevices/host/test_data/fastpair/simulator_antispoofkey_devicemeta_json.txt
+++ /dev/null
@@ -1,28 +0,0 @@
-{
-  "anti_spoofing_public_key_str": "sjp\/AOS7+VnTCaueeWorjdeJ8Nc32EOmpe\/QRhzY9+cMNELU1QA3jzgvUXdWW73nl6+EN01eXtLBu2Fw9CGmfA==",
-  "fast_pair_device_metadata": {
-    "ble_tx_power": -10,
-    "case_url": "https://lh3.googleusercontent.com/mNZ7CGplQSpZhoY79jXDQU4B65eY2f0SndnYZLk1PSm8zKTYeRU7REmrLL_pptD6HpVI2F_oQ6xhhtZKOvB8EQ",
-    "connect_success_companion_app_installed": "Your device is ready to be set up",
-    "connect_success_companion_app_not_installed": "Download the device app on Google Play to see all available features",
-    "device_type": 7,
-    "download_companion_app_description": "Tap to download device app on Google Play and see all features",
-    "fail_connect_go_to_settings_description": "Try manually pairing to the device by going to Settings",
-    "image_url": "https://lh3.googleusercontent.com/THpAzISZGa5F86cMsBcTPhRWefBPc5dorBxWdOPCGvbFg6ZMHUjFuE-4kbLuoLoIMHf3Fd8jUvvcxnjp_Q",
-    "initial_notification_description": "Tap to pair. Earbuds will be tied to %s",
-    "initial_notification_description_no_account": "Tap to pair with this device",
-    "initial_pairing_description": "%s will appear on devices linked with %s",
-    "intent_uri": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.testapp;end",
-    "left_bud_url": "https://lh3.googleusercontent.com/O8SVJ5E7CXUkpkym7ibZbp6wypuO7HaTFcslT_FjmEzJX4KHoIY_kzLTdK2kwJXiDBgg8cC__sG-JJ5aVnQtFjQ",
-    "name": "Fast Pair Provider Simulator",
-    "open_companion_app_description": "Tap to finish setup",
-    "retroactive_pairing_description": "Save device to %s for faster pairing to your other devices",
-    "right_bud_url": "https://lh3.googleusercontent.com/X_FsRmEKH_fgKzvopyrlyWJAdczRel42Tih7p9-e-U48gBTaggGVQx70K27TzlqIaqYVuaNpTnGoUsKIgiy4WA",
-    "subsequent_pairing_description": "Connect %s to this phone",
-    "trigger_distance": 0.6000000238418579,
-    "unable_to_connect_description": "Try manually pairing to the device",
-    "unable_to_connect_title": "Unable to connect",
-    "update_companion_app_description": "Tap to update device settings and finish setup",
-    "wait_launch_companion_app_description": "This will take a few moments"
-  }
-}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/host/test_helper/__init__.py b/nearby/tests/multidevices/host/test_helper/__init__.py
deleted file mode 100644
index b0cae91..0000000
--- a/nearby/tests/multidevices/host/test_helper/__init__.py
+++ /dev/null
@@ -1,13 +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.
diff --git a/nearby/tests/multidevices/host/test_helper/constants.py b/nearby/tests/multidevices/host/test_helper/constants.py
deleted file mode 100644
index 342be8f..0000000
--- a/nearby/tests/multidevices/host/test_helper/constants.py
+++ /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.
-
-# Default model ID to simulate on provider side.
-DEFAULT_MODEL_ID = '00000c'
-
-# Default public key to simulate as registered headsets.
-DEFAULT_ANTI_SPOOFING_KEY = 'Cbj9eCJrTdDgSYxLkqtfADQi86vIaMvxJsQ298sZYWE='
-
-# Default anti-spoof Key Device Metadata JSON file for data provider at seeker side.
-DEFAULT_KDM_JSON_FILE = 'simulator_antispoofkey_devicemeta_json.txt'
-
-# Time in seconds for events waiting according to Fast Pair certification guidelines:
-# https://developers.google.com/nearby/fast-pair/certification-guideline
-SETUP_TIMEOUT_SEC = 5
-BECOME_DISCOVERABLE_TIMEOUT_SEC = 10
-START_ADVERTISING_TIMEOUT_SEC = 5
-SCAN_TIMEOUT_SEC = 5
-HALF_SHEET_POPUP_TIMEOUT_SEC = 5
-AVERAGE_PAIRING_TIMEOUT_SEC = 12
-
-# The phone to simulate Fast Pair provider (like headphone) needs changes in Android system:
-# 1. System permission check removal
-# 2. Adjusts Bluetooth profile configurations
-# The build fingerprint of the custom ROM for Fast Pair provider simulator.
-FAST_PAIR_PROVIDER_SIMULATOR_BUILD_FINGERPRINT = (
-    'google/bramble/bramble:Tiramisu/MASTER/eng.hylo.20211019.091550:userdebug/dev-keys')
diff --git a/nearby/tests/multidevices/host/test_helper/event_helper.py b/nearby/tests/multidevices/host/test_helper/event_helper.py
deleted file mode 100644
index 8abf05c..0000000
--- a/nearby/tests/multidevices/host/test_helper/event_helper.py
+++ /dev/null
@@ -1,69 +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.
-
-"""This is a shared library to help handling Mobly event waiting logic."""
-
-import time
-from typing import Callable
-
-from mobly.controllers.android_device_lib import callback_handler
-from mobly.controllers.android_device_lib import snippet_event
-
-# Abbreviations for common use type
-CallbackHandler = callback_handler.CallbackHandler
-SnippetEvent = snippet_event.SnippetEvent
-
-# Type definition for the callback functions to make code formatted nicely
-OnReceivedCallback = Callable[[SnippetEvent, int], bool]
-OnWaitingCallback = Callable[[int], None]
-OnMissedCallback = Callable[[], None]
-
-
-def wait_callback_event(callback_event_handler: CallbackHandler,
-                        event_name: str, timeout_seconds: int,
-                        on_received: OnReceivedCallback,
-                        on_waiting: OnWaitingCallback,
-                        on_missed: OnMissedCallback) -> None:
-    """Waits until the matched event has been received or timeout.
-
-    Here we keep waitAndGet for event callback from EventSnippet.
-    We loop until over timeout_seconds instead of directly
-    waitAndGet(timeout=teardown_timeout_seconds). Because there is
-    MAX_TIMEOUT limitation in callback_handler of Mobly.
-
-    Args:
-      callback_event_handler: Mobly callback events handler.
-      event_name: the specific name of the event to wait.
-      timeout_seconds: the number of seconds to wait before giving up.
-      on_received: calls when event received, return false to keep waiting.
-      on_waiting: calls when waitAndGet timeout.
-      on_missed: calls when giving up.
-    """
-    start_time = time.perf_counter()
-    deadline = start_time + timeout_seconds
-    while time.perf_counter() < deadline:
-        remaining_time_sec = min(callback_handler.DEFAULT_TIMEOUT,
-                                 deadline - time.perf_counter())
-        try:
-            event = callback_event_handler.waitAndGet(
-                event_name, timeout=remaining_time_sec)
-        except callback_handler.TimeoutError:
-            elapsed_time = int(time.perf_counter() - start_time)
-            on_waiting(elapsed_time)
-        else:
-            elapsed_time = int(time.perf_counter() - start_time)
-            if on_received(event, elapsed_time):
-                break
-    else:
-        on_missed()
diff --git a/nearby/tests/multidevices/host/test_helper/fast_pair_base_test.py b/nearby/tests/multidevices/host/test_helper/fast_pair_base_test.py
deleted file mode 100644
index 8b84839..0000000
--- a/nearby/tests/multidevices/host/test_helper/fast_pair_base_test.py
+++ /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.
-
-"""Base for all Nearby Mainline Fast Pair end-to-end test cases."""
-
-from typing import List, Tuple
-
-from mobly import base_test
-from mobly import signals
-from mobly.controllers import android_device
-
-from test_helper import constants
-from test_helper import fast_pair_provider_simulator
-from test_helper import fast_pair_seeker
-
-# Abbreviations for common use type.
-AndroidDevice = android_device.AndroidDevice
-FastPairProviderSimulator = fast_pair_provider_simulator.FastPairProviderSimulator
-FastPairSeeker = fast_pair_seeker.FastPairSeeker
-REQUIRED_BUILD_FINGERPRINT = constants.FAST_PAIR_PROVIDER_SIMULATOR_BUILD_FINGERPRINT
-
-
-class FastPairBaseTest(base_test.BaseTestClass):
-    """Base class for all Nearby Mainline Fast Pair end-to-end classes to inherit."""
-
-    _duts: List[AndroidDevice]
-    _provider: FastPairProviderSimulator
-    _seeker: FastPairSeeker
-
-    def setup_class(self) -> None:
-        super().setup_class()
-        self._duts = self.register_controller(android_device, min_number=2)
-
-        provider_ad, seeker_ad = self._check_devices_supported()
-        self._provider = FastPairProviderSimulator(provider_ad)
-        self._seeker = FastPairSeeker(seeker_ad)
-        self._provider.load_snippet()
-        self._seeker.load_snippet()
-
-    def setup_test(self) -> None:
-        super().setup_test()
-        self._provider.setup_provider_simulator(constants.SETUP_TIMEOUT_SEC)
-
-    def teardown_test(self) -> None:
-        super().teardown_test()
-        # Create per-test excepts of logcat.
-        for dut in self._duts:
-            dut.services.create_output_excerpts_all(self.current_test_info)
-
-    def _check_devices_supported(self) -> Tuple[AndroidDevice, AndroidDevice]:
-        # Assume the 1st phone is provider, the 2nd one is seeker.
-        provider_ad, seeker_ad = self._duts[:2]
-
-        for ad in self._duts:
-            if ad.build_info['build_fingerprint'] == REQUIRED_BUILD_FINGERPRINT:
-                if ad != provider_ad:
-                    provider_ad, seeker_ad = seeker_ad, provider_ad
-                break
-        else:
-            raise signals.TestAbortClass(
-                f'None of phones has custom ROM ({REQUIRED_BUILD_FINGERPRINT}) for Fast Pair '
-                f'provider simulator. Skip all the test cases!')
-
-        return provider_ad, seeker_ad
diff --git a/nearby/tests/multidevices/host/test_helper/fast_pair_provider_simulator.py b/nearby/tests/multidevices/host/test_helper/fast_pair_provider_simulator.py
deleted file mode 100644
index 592c4f1..0000000
--- a/nearby/tests/multidevices/host/test_helper/fast_pair_provider_simulator.py
+++ /dev/null
@@ -1,195 +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.
-
-"""Fast Pair provider simulator role."""
-
-import time
-
-from mobly import asserts
-from mobly.controllers import android_device
-from mobly.controllers.android_device_lib import jsonrpc_client_base
-from mobly.controllers.android_device_lib import snippet_event
-from typing import Optional
-
-from test_helper import event_helper
-
-# The package name of the provider simulator snippet.
-FP_PROVIDER_SIMULATOR_SNIPPETS_PACKAGE = 'android.nearby.multidevices'
-
-# Events reported from the provider simulator snippet.
-ON_A2DP_SINK_PROFILE_CONNECT_EVENT = 'onA2DPSinkProfileConnected'
-ON_SCAN_MODE_CHANGE_EVENT = 'onScanModeChange'
-ON_ADVERTISING_CHANGE_EVENT = 'onAdvertisingChange'
-
-# Target scan mode.
-DISCOVERABLE_MODE = 'DISCOVERABLE'
-
-# Abbreviations for common use type.
-AndroidDevice = android_device.AndroidDevice
-SnippetEvent = snippet_event.SnippetEvent
-wait_for_event = event_helper.wait_callback_event
-
-
-class FastPairProviderSimulator:
-    """A proxy for provider simulator snippet on the device."""
-
-    def __init__(self, ad: AndroidDevice) -> None:
-        self._ad = ad
-        self._ad.debug_tag = 'FastPairProviderSimulator'
-        self._provider_status_callback = None
-
-    def load_snippet(self) -> None:
-        """Starts the provider simulator snippet and connects.
-
-        Raises:
-          SnippetError: Illegal load operations are attempted.
-        """
-        self._ad.load_snippet(
-            name='fp', package=FP_PROVIDER_SIMULATOR_SNIPPETS_PACKAGE)
-
-    def setup_provider_simulator(self, timeout_seconds: int) -> None:
-        """Sets up the Fast Pair provider simulator.
-
-        Args:
-          timeout_seconds: The number of seconds to wait before giving up.
-        """
-        setup_status_callback = self._ad.fp.setupProviderSimulator()
-
-        def _on_a2dp_sink_profile_connect_event_received(_, elapsed_time: int) -> bool:
-            self._ad.log.info('Provider simulator connected to A2DP sink in %d seconds.',
-                              elapsed_time)
-            return True
-
-        def _on_a2dp_sink_profile_connect_event_waiting(elapsed_time: int) -> None:
-            self._ad.log.info(
-                'Still waiting "%s" event callback from provider side '
-                'after %d seconds...', ON_A2DP_SINK_PROFILE_CONNECT_EVENT, elapsed_time)
-
-        def _on_a2dp_sink_profile_connect_event_missed() -> None:
-            asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
-                         f'the specific "{ON_A2DP_SINK_PROFILE_CONNECT_EVENT}" event.')
-
-        wait_for_event(
-            callback_event_handler=setup_status_callback,
-            event_name=ON_A2DP_SINK_PROFILE_CONNECT_EVENT,
-            timeout_seconds=timeout_seconds,
-            on_received=_on_a2dp_sink_profile_connect_event_received,
-            on_waiting=_on_a2dp_sink_profile_connect_event_waiting,
-            on_missed=_on_a2dp_sink_profile_connect_event_missed)
-
-    def start_model_id_advertising(self, model_id: str, anti_spoofing_key: str) -> None:
-        """Starts model id advertising for scanning and initial pairing.
-
-        Args:
-          model_id: A 3-byte hex string for seeker side to recognize the device (ex:
-            0x00000C).
-          anti_spoofing_key: A public key for registered headsets.
-        """
-        self._ad.log.info(
-            'Provider simulator starts advertising as model id "%s" with anti-spoofing key "%s".',
-            model_id, anti_spoofing_key)
-        self._provider_status_callback = (
-            self._ad.fp.startModelIdAdvertising(model_id, anti_spoofing_key))
-
-    def teardown_provider_simulator(self) -> None:
-        """Tears down the Fast Pair provider simulator."""
-        self._ad.fp.teardownProviderSimulator()
-
-    def get_ble_mac_address(self) -> str:
-        """Gets Bluetooth low energy mac address of the provider simulator.
-
-        The BLE mac address will be set by the AdvertisingSet.getOwnAddress()
-        callback. This is the callback flow in the custom Android build. It takes
-        a while after advertising started so we use retry here to wait it.
-
-        Returns:
-          The BLE mac address of the Fast Pair provider simulator.
-        """
-        for _ in range(3):
-            try:
-                return self._ad.fp.getBluetoothLeAddress()
-            except jsonrpc_client_base.ApiError:
-                time.sleep(1)
-
-    def wait_for_discoverable_mode(self, timeout_seconds: int) -> None:
-        """Waits onScanModeChange event to ensure provider is discoverable.
-
-        Args:
-          timeout_seconds: The number of seconds to wait before giving up.
-        """
-
-        def _on_scan_mode_change_event_received(
-                scan_mode_change_event: SnippetEvent, elapsed_time: int) -> bool:
-            scan_mode = scan_mode_change_event.data['mode']
-            self._ad.log.info(
-                'Provider simulator changed the scan mode to %s in %d seconds.',
-                scan_mode, elapsed_time)
-            return scan_mode == DISCOVERABLE_MODE
-
-        def _on_scan_mode_change_event_waiting(elapsed_time: int) -> None:
-            self._ad.log.info(
-                'Still waiting "%s" event callback from provider side '
-                'after %d seconds...', ON_SCAN_MODE_CHANGE_EVENT, elapsed_time)
-
-        def _on_scan_mode_change_event_missed() -> None:
-            asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
-                         f'the specific "{ON_SCAN_MODE_CHANGE_EVENT}" event.')
-
-        wait_for_event(
-            callback_event_handler=self._provider_status_callback,
-            event_name=ON_SCAN_MODE_CHANGE_EVENT,
-            timeout_seconds=timeout_seconds,
-            on_received=_on_scan_mode_change_event_received,
-            on_waiting=_on_scan_mode_change_event_waiting,
-            on_missed=_on_scan_mode_change_event_missed)
-
-    def wait_for_advertising_start(self, timeout_seconds: int) -> None:
-        """Waits onAdvertisingChange event to ensure provider is advertising.
-
-        Args:
-          timeout_seconds: The number of seconds to wait before giving up.
-        """
-
-        def _on_advertising_mode_change_event_received(
-                scan_mode_change_event: SnippetEvent, elapsed_time: int) -> bool:
-            advertising_mode = scan_mode_change_event.data['isAdvertising']
-            self._ad.log.info(
-                'Provider simulator changed the advertising mode to %s in %d seconds.',
-                advertising_mode, elapsed_time)
-            return advertising_mode
-
-        def _on_advertising_mode_change_event_waiting(elapsed_time: int) -> None:
-            self._ad.log.info(
-                'Still waiting "%s" event callback from provider side '
-                'after %d seconds...', ON_ADVERTISING_CHANGE_EVENT, elapsed_time)
-
-        def _on_advertising_mode_change_event_missed() -> None:
-            asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
-                         f'the specific "{ON_ADVERTISING_CHANGE_EVENT}" event.')
-
-        wait_for_event(
-            callback_event_handler=self._provider_status_callback,
-            event_name=ON_ADVERTISING_CHANGE_EVENT,
-            timeout_seconds=timeout_seconds,
-            on_received=_on_advertising_mode_change_event_received,
-            on_waiting=_on_advertising_mode_change_event_waiting,
-            on_missed=_on_advertising_mode_change_event_missed)
-
-    def get_latest_received_account_key(self) -> Optional[str]:
-        """Gets the latest account key received on the provider side.
-
-        Returns:
-          The account key received at provider side.
-        """
-        return self._ad.fp.getLatestReceivedAccountKey()
diff --git a/nearby/tests/multidevices/host/test_helper/fast_pair_seeker.py b/nearby/tests/multidevices/host/test_helper/fast_pair_seeker.py
deleted file mode 100644
index 64fc2f2..0000000
--- a/nearby/tests/multidevices/host/test_helper/fast_pair_seeker.py
+++ /dev/null
@@ -1,186 +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.
-
-"""Fast Pair seeker role."""
-
-import json
-from typing import Callable, Optional
-
-from mobly import asserts
-from mobly.controllers import android_device
-from mobly.controllers.android_device_lib import snippet_event
-
-from test_helper import event_helper
-from test_helper import utils
-
-# The package name of the Nearby Mainline Fast Pair seeker Mobly snippet.
-FP_SEEKER_SNIPPETS_PACKAGE = 'android.nearby.multidevices'
-
-# Events reported from the seeker snippet.
-ON_PROVIDER_FOUND_EVENT = 'onDiscovered'
-ON_MANAGE_ACCOUNT_DEVICE_EVENT = 'onManageAccountDevice'
-
-# Abbreviations for common use type.
-AndroidDevice = android_device.AndroidDevice
-JsonObject = utils.JsonObject
-ProviderAccountKeyCallable = Callable[[], Optional[str]]
-SnippetEvent = snippet_event.SnippetEvent
-wait_for_event = event_helper.wait_callback_event
-
-
-class FastPairSeeker:
-    """A proxy for seeker snippet on the device."""
-
-    def __init__(self, ad: AndroidDevice) -> None:
-        self._ad = ad
-        self._ad.debug_tag = 'MainlineFastPairSeeker'
-        self._scan_result_callback = None
-        self._pairing_result_callback = None
-
-    def load_snippet(self) -> None:
-        """Starts the seeker snippet and connects.
-
-        Raises:
-          SnippetError: Illegal load operations are attempted.
-        """
-        self._ad.load_snippet(name='fp', package=FP_SEEKER_SNIPPETS_PACKAGE)
-
-    def start_scan(self) -> None:
-        """Starts scanning to find Fast Pair provider devices."""
-        self._scan_result_callback = self._ad.fp.startScan()
-
-    def stop_scan(self) -> None:
-        """Stops the Fast Pair seeker scanning."""
-        self._ad.fp.stopScan()
-
-    def wait_and_assert_provider_found(self, timeout_seconds: int,
-                                       expected_model_id: str,
-                                       expected_ble_mac_address: str) -> None:
-        """Waits and asserts any onDiscovered event from the seeker.
-
-        Args:
-          timeout_seconds: The number of seconds to wait before giving up.
-          expected_model_id: The expected model ID of the remote Fast Pair provider
-            device.
-          expected_ble_mac_address: The expected BLE MAC address of the remote Fast
-            Pair provider device.
-        """
-
-        def _on_provider_found_event_received(provider_found_event: SnippetEvent,
-                                              elapsed_time: int) -> bool:
-            nearby_device_str = provider_found_event.data['device']
-            self._ad.log.info('Seeker discovered first provider(%s) in %d seconds.',
-                              nearby_device_str, elapsed_time)
-            return expected_ble_mac_address in nearby_device_str
-
-        def _on_provider_found_event_waiting(elapsed_time: int) -> None:
-            self._ad.log.info(
-                'Still waiting "%s" event callback from seeker side '
-                'after %d seconds...', ON_PROVIDER_FOUND_EVENT, elapsed_time)
-
-        def _on_provider_found_event_missed() -> None:
-            asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
-                         f'the specific "{ON_PROVIDER_FOUND_EVENT}" event.')
-
-        wait_for_event(
-            callback_event_handler=self._scan_result_callback,
-            event_name=ON_PROVIDER_FOUND_EVENT,
-            timeout_seconds=timeout_seconds,
-            on_received=_on_provider_found_event_received,
-            on_waiting=_on_provider_found_event_waiting,
-            on_missed=_on_provider_found_event_missed)
-
-    def put_anti_spoof_key_device_metadata(self, model_id: str, kdm_json_file_name: str) -> None:
-        """Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into test data cache.
-
-        Args:
-          model_id: A string of model id to be associated with.
-          kdm_json_file_name: The FastPairAntispoofKeyDeviceMetadata JSON object.
-        """
-        self._ad.log.info('Puts FastPairAntispoofKeyDeviceMetadata into test data cache for '
-                          'model id "%s".', model_id)
-        kdm_json_object = utils.load_json_fast_pair_test_data(kdm_json_file_name)
-        self._ad.fp.putAntispoofKeyDeviceMetadata(
-            model_id,
-            utils.serialize_as_simplified_json_str(kdm_json_object))
-
-    def set_fast_pair_scan_enabled(self, enable: bool) -> None:
-        """Writes into Settings whether Fast Pair scan is enabled.
-
-        Args:
-          enable: whether the Fast Pair scan should be enabled.
-        """
-        self._ad.log.info('%s Fast Pair scan in Android settings.',
-                          'Enables' if enable else 'Disables')
-        self._ad.fp.setFastPairScanEnabled(enable)
-
-    def wait_and_assert_halfsheet_showed(self, timeout_seconds: int,
-                                         expected_model_id: str) -> None:
-        """Waits and asserts the onHalfSheetShowed event from the seeker.
-
-        Args:
-          timeout_seconds: The number of seconds to wait before giving up.
-          expected_model_id: A 3-byte hex string for seeker side to recognize
-            the remote provider device (ex: 0x00000c).
-        """
-        self._ad.log.info('Waits and asserts the half sheet showed for model id "%s".',
-                          expected_model_id)
-        self._ad.fp.waitAndAssertHalfSheetShowed(expected_model_id, timeout_seconds)
-
-    def dismiss_halfsheet(self) -> None:
-        """Dismisses the half sheet UI if showed."""
-        self._ad.fp.dismissHalfSheet()
-
-    def start_pairing(self) -> None:
-        """Starts pairing the provider via "Connect" button on half sheet UI."""
-        self._pairing_result_callback = self._ad.fp.startPairing()
-
-    def wait_and_assert_account_device(
-            self, timeout_seconds: int,
-            get_account_key_from_provider: ProviderAccountKeyCallable) -> None:
-        """Waits and asserts the onHalfSheetShowed event from the seeker.
-
-        Args:
-          timeout_seconds: The number of seconds to wait before giving up.
-          get_account_key_from_provider: The callable to get expected account key from the provider
-            side.
-        """
-
-        def _on_manage_account_device_event_received(manage_account_device_event: SnippetEvent,
-                                                     elapsed_time: int) -> bool:
-            account_key_json_str = manage_account_device_event.data['accountDeviceJsonString']
-            account_key_from_seeker = json.loads(account_key_json_str)['account_key']
-            account_key_from_provider = get_account_key_from_provider()
-            self._ad.log.info('Seeker add an account device with account key "%s" in %d seconds.',
-                              account_key_from_seeker, elapsed_time)
-            self._ad.log.info('The latest provider side account key is "%s".',
-                              account_key_from_provider)
-            return account_key_from_seeker == account_key_from_provider
-
-        def _on_manage_account_device_event_waiting(elapsed_time: int) -> None:
-            self._ad.log.info(
-                'Still waiting "%s" event callback from seeker side '
-                'after %d seconds...', ON_MANAGE_ACCOUNT_DEVICE_EVENT, elapsed_time)
-
-        def _on_manage_account_device_event_missed() -> None:
-            asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
-                         f'the specific "{ON_MANAGE_ACCOUNT_DEVICE_EVENT}" event.')
-
-        wait_for_event(
-            callback_event_handler=self._pairing_result_callback,
-            event_name=ON_MANAGE_ACCOUNT_DEVICE_EVENT,
-            timeout_seconds=timeout_seconds,
-            on_received=_on_manage_account_device_event_received,
-            on_waiting=_on_manage_account_device_event_waiting,
-            on_missed=_on_manage_account_device_event_missed)
diff --git a/nearby/tests/multidevices/host/test_helper/utils.py b/nearby/tests/multidevices/host/test_helper/utils.py
deleted file mode 100644
index a0acb57..0000000
--- a/nearby/tests/multidevices/host/test_helper/utils.py
+++ /dev/null
@@ -1,42 +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.
-
-import json
-import pathlib
-import sys
-from typing import Any, Dict
-
-# Type definition
-JsonObject = Dict[str, Any]
-
-
-def load_json_fast_pair_test_data(json_file_name: str) -> JsonObject:
-    """Loads a JSON text file from test data directory into a Json object.
-
-    Args:
-      json_file_name: The name of the JSON file.
-    """
-    return json.loads(
-        pathlib.Path(sys.argv[0]).parent.joinpath(
-            'test_data', 'fastpair', json_file_name).read_text()
-    )
-
-
-def serialize_as_simplified_json_str(json_data: JsonObject) -> str:
-    """Serializes a JSON object into a string without empty space.
-
-    Args:
-      json_data: The JSON object to be serialized.
-    """
-    return json.dumps(json_data, separators=(',', ':'))
diff --git a/nearby/tests/multidevices/host/tool/fast_pair_data_provider_shell.sh b/nearby/tests/multidevices/host/tool/fast_pair_data_provider_shell.sh
deleted file mode 100755
index a74c1a9..0000000
--- a/nearby/tests/multidevices/host/tool/fast_pair_data_provider_shell.sh
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/bin/bash
-
-#
-# 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.
-
-# A script to interactively manage FastPairTestDataCache of FastPairTestDataProviderService.
-#
-# FastPairTestDataProviderService (../../clients/test_service/fastpair_seeker_data_provider/) is a
-# run-Time configurable FastPairDataProviderService. It has a FastPairTestDataManager to receive
-# Intent broadcast to add/clear the FastPairTestDataCache. This cache provides the data to return to
-# the Nearby Mainline module for onXXX calls (ex: onLoadFastPairAntispoofKeyDeviceMetadata).
-#
-# To use this tool, make sure you:
-# 1. Flash the ROM your built to the device
-# 2. Build and install NearbyFastPairSeekerDataProvider to the device
-# m NearbyFastPairSeekerDataProvider
-# adb install -r -g ${ANDROID_PRODUCT_OUT}/system/app/NearbyFastPairSeekerDataProvider/NearbyFastPairSeekerDataProvider.apk
-# 3. Check FastPairService can connect to the FastPairTestDataProviderService.
-# adb logcat ServiceMonitor:* *:S
-# (ex: ServiceMonitor: [FAST_PAIR_DATA_PROVIDER] connected to {
-# android.nearby.fastpair.seeker.dataprovider/android.nearby.fastpair.seeker.dataprovider.FastPairTestDataProviderService})
-#
-# Sample Usages:
-# 1. Send FastPairAntispoofKeyDeviceMetadata for PixelBuds-A to FastPairTestDataCache
-# ./fast_pair_data_provider_shell.sh -m=718c17  -a=../test_data/fastpair/pixelbuds-a_antispoofkey_devicemeta_json.txt
-# 2. Send FastPairAccountDevicesMetadata for PixelBuds-A to FastPairTestDataCache
-# ./fast_pair_data_provider_shell.sh -d=../test_data/fastpair/pixelbuds-a_account_devicemeta_json.txt
-# 3. Send FastPairAntispoofKeyDeviceMetadata for Provider Simulator to FastPairTestDataCache
-# ./fast_pair_data_provider_shell.sh -m=00000c -a=../test_data/fastpair/simulator_antispoofkey_devicemeta_json.txt
-# 4. Send FastPairAccountDevicesMetadata for Provider Simulator to FastPairTestDataCache
-# ./fast_pair_data_provider_shell.sh -d=../test_data/fastpair/simulator_account_devicemeta_json.txt
-# 5. Clear FastPairTestDataCache
-# ./fast_pair_data_provider_shell.sh -c
-#
-# Check logcat:
-# adb logcat FastPairTestDataManager:* FastPairTestDataProviderService:* *:S
-
-for i in "$@"; do
-  case $i in
-    -a=*|--ask=*)
-      ASK_FILE="${i#*=}"
-      shift # past argument=value
-      ;;
-    -m=*|--model=*)
-      MODEL_ID="${i#*=}"
-      shift # past argument=value
-      ;;
-    -d=*|--adm=*)
-      ADM_FILE="${i#*=}"
-      shift # past argument=value
-      ;;
-    -c)
-      CLEAR="true"
-      shift # past argument
-      ;;
-    -*|--*)
-      echo "Unknown option $i"
-      exit 1
-      ;;
-    *)
-      ;;
-  esac
-done
-
-readonly ACTION_BASE="android.nearby.fastpair.seeker.action"
-readonly ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA="$ACTION_BASE.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA"
-readonly ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA="$ACTION_BASE.ACCOUNT_KEY_DEVICE_METADATA"
-readonly ACTION_RESET_TEST_DATA_CACHE="$ACTION_BASE.RESET"
-readonly DATA_JSON_STRING_KEY="json"
-readonly DATA_MODEL_ID_STRING_KEY="modelId"
-
-if [[ -n "${ASK_FILE}" ]] && [[ -n "${MODEL_ID}" ]]; then
-  echo "Sending AntispoofKeyDeviceMetadata for model ${MODEL_ID} to the FastPairTestDataCache..."
-  ASK_JSON_TEXT=$(tr -d '\n' < "$ASK_FILE")
-  CMD="am broadcast -a $ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA "
-  CMD+="-e $DATA_MODEL_ID_STRING_KEY '$MODEL_ID' "
-  CMD+="-e $DATA_JSON_STRING_KEY '\"'$ASK_JSON_TEXT'\"'"
-  CMD="adb shell \"$CMD\""
-  echo "$CMD" && eval "$CMD"
-fi
-
-if [ -n "${ADM_FILE}" ]; then
-  echo "Sending AccountKeyDeviceMetadata to the FastPairTestDataCache..."
-  ADM_JSON_TEXT=$(tr -d '\n' < "$ADM_FILE")
-  CMD="am broadcast -a $ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA "
-  CMD+="-e $DATA_JSON_STRING_KEY '\"'$ADM_JSON_TEXT'\"'"
-  CMD="adb shell \"$CMD\""
-  echo "$CMD" && eval "$CMD"
-fi
-
-if [ -n "${CLEAR}" ]; then
-  echo "Cleaning FastPairTestDataCache..."
-  CMD="adb shell am broadcast -a $ACTION_RESET_TEST_DATA_CACHE"
-  echo "$CMD" && eval "$CMD"
-fi
diff --git a/nearby/tests/robotests/Android.bp b/nearby/tests/robotests/Android.bp
deleted file mode 100644
index 70fa0c3..0000000
--- a/nearby/tests/robotests/Android.bp
+++ /dev/null
@@ -1,55 +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.
-
-//############################################
-// Nearby Robolectric test target. #
-//############################################
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_robolectric_test {
-    name: "NearbyRoboTests",
-    srcs: ["src/**/*.java"],
-    instrumentation_for: "NearbyFakeTestApp",
-    java_resource_dirs: ["config"],
-
-    libs: [
-        "android-support-annotations",
-        "services.core",
-    ],
-
-    static_libs: [
-        "androidx.test.core",
-        "androidx.core_core",
-        "androidx.annotation_annotation",
-        "androidx.legacy_legacy-support-v4",
-        "androidx.recyclerview_recyclerview",
-        "androidx.preference_preference",
-        "androidx.appcompat_appcompat",
-        "androidx.lifecycle_lifecycle-runtime",
-        "androidx.mediarouter_mediarouter-nodeps",
-        "error_prone_annotations",
-        "service-nearby-pre-jarjar",
-        "truth-prebuilt",
-        "robolectric_android-all-stub",
-    ],
-
-    test_options: {
-        // timeout in seconds.
-        timeout: 36000,
-    },
-    upstream: true,
-}
diff --git a/nearby/tests/robotests/AndroidManifest.xml b/nearby/tests/robotests/AndroidManifest.xml
deleted file mode 100644
index 25376cf..0000000
--- a/nearby/tests/robotests/AndroidManifest.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-    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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.nearby.common.bluetooth.fastpair.test">
-</manifest>
diff --git a/nearby/tests/robotests/config/robolectric.properties b/nearby/tests/robotests/config/robolectric.properties
deleted file mode 100644
index 932de7d..0000000
--- a/nearby/tests/robotests/config/robolectric.properties
+++ /dev/null
@@ -1,16 +0,0 @@
-#
-# Copyright (C) 2021 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-sdk=NEWEST_SDK
\ No newline at end of file
diff --git a/nearby/tests/robotests/fake_app/Android.bp b/nearby/tests/robotests/fake_app/Android.bp
deleted file mode 100644
index 707b38f..0000000
--- a/nearby/tests/robotests/fake_app/Android.bp
+++ /dev/null
@@ -1,26 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_app {
-    name: "NearbyFakeTestApp",
-    srcs: ["*.java"],
-    platform_apis: true,
-    optimize: {
-        enabled: false,
-    },
-}
diff --git a/nearby/tests/robotests/fake_app/AndroidManifest.xml b/nearby/tests/robotests/fake_app/AndroidManifest.xml
deleted file mode 100644
index fdb5390..0000000
--- a/nearby/tests/robotests/fake_app/AndroidManifest.xml
+++ /dev/null
@@ -1,19 +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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.nearby" />
diff --git a/nearby/tests/robotests/fake_app/Empty.java b/nearby/tests/robotests/fake_app/Empty.java
deleted file mode 100644
index 96619d5..0000000
--- a/nearby/tests/robotests/fake_app/Empty.java
+++ /dev/null
@@ -1,16 +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.
- */
-
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Bluelet.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Bluelet.java
deleted file mode 100644
index 182fde7..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Bluelet.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower;
-
-import android.os.ParcelUuid;
-
-/**
- * User interface for mocking and simulation of a Bluetooth device.
- */
-public interface Bluelet {
-
-    /**
-     * See {@link #setCreateBondOutcome}.
-     */
-    enum CreateBondOutcome {
-        SUCCESS,
-        FAILURE,
-        TIMEOUT
-    }
-
-    /**
-     * See {@link #setIoCapabilities}. Note that Bluetooth specifies a few more choices, but this is
-     * all DeviceShadower currently supports.
-     */
-    enum IoCapabilities {
-        NO_INPUT_NO_OUTPUT,
-        DISPLAY_YES_NO,
-        KEYBOARD_ONLY
-    }
-
-    /**
-     * See {@link #setFetchUuidsTiming}.
-     */
-    enum FetchUuidsTiming {
-        BEFORE_BONDING,
-        AFTER_BONDING,
-        NEVER
-    }
-
-    /**
-     * Set the initial state of the local Bluetooth adapter at the beginning of the test.
-     * <p>This method is not associated with broadcast event and is intended to be called at the
-     * beginning of the test. Allowed states:
-     *
-     * @see android.bluetooth.BluetoothAdapter#STATE_OFF
-     * @see android.bluetooth.BluetoothAdapter#STATE_ON
-     * </p>
-     */
-    Bluelet setAdapterInitialState(int state) throws IllegalArgumentException;
-
-    /**
-     * Set the bluetooth class of the local Bluetooth device at the beginning of the test.
-     * <p>
-     *
-     * @see android.bluetooth.BluetoothClass.Device
-     * @see android.bluetooth.BluetoothClass.Service
-     */
-    Bluelet setBluetoothClass(int bluetoothClass);
-
-    /**
-     * Set the scan mode of the local Bluetooth device at the beginning of the test.
-     */
-    Bluelet setScanMode(int scanMode);
-
-    /**
-     * Set the Bluetooth profiles supported by this device (e.g. A2DP Sink).
-     */
-    Bluelet setProfileUuids(ParcelUuid... profileUuids);
-
-    /**
-     * Makes bond attempts with this device succeed or fail.
-     *
-     * @param failureReason Ignored unless outcome is {@link CreateBondOutcome#FAILURE}. This is
-     * delivered in the intent that indicates bond state has changed to BOND_NONE. Values:
-     * https://cs.corp.google.com/android/frameworks/base/core/java/android/bluetooth/BluetoothDevice.java?rcl=38d9ee4cd661c10e012f71051d23644c65607eed&l=472
-     */
-    Bluelet setCreateBondOutcome(CreateBondOutcome outcome, int failureReason);
-
-    /**
-     * Sets the IO capabilities of this device. When bonding, a device states its IO capabilities in
-     * the pairing request. The pairing variant used depends on the IO capabilities of both devices
-     * (e.g. Just Works is the only available option for a NoInputNoOutput device, while Numeric
-     * Comparison aka Passkey Confirmation is used if both devices have a display and the ability to
-     * confirm/deny).
-     *
-     * @see <a href="https://blog.bluetooth.com/bluetooth-pairing-part-4">Bluetooth blog</a>
-     */
-    Bluelet setIoCapabilities(IoCapabilities ioCapabilities);
-
-    /**
-     * Make the device refuse connections. By default, connections are accepted.
-     *
-     * @param refuse Connections are refused if True.
-     */
-    Bluelet setRefuseConnections(boolean refuse);
-
-    /**
-     * Make the device refuse GATT connections. By default. connections are accepted.
-     *
-     * @param refuse GATT connections are refused if true.
-     */
-    Bluelet setRefuseGattConnections(boolean refuse);
-
-    /**
-     * When to send the ACTION_UUID broadcast. This can be {@link FetchUuidsTiming#BEFORE_BONDING},
-     * {@link FetchUuidsTiming#AFTER_BONDING}, or {@link FetchUuidsTiming#NEVER}. The default is
-     * {@link FetchUuidsTiming#AFTER_BONDING}.
-     */
-    Bluelet setFetchUuidsTiming(FetchUuidsTiming fetchUuidsTiming);
-
-    /**
-     * Adds a bonded device to the BluetoothAdapter.
-     */
-    Bluelet addBondedDevice(String address);
-
-    /**
-     * Enables the CVE-2019-2225 represents that the pairing variant will switch from Just Works to
-     * Consent when local device's io capability is Display Yes/No and remote is NoInputNoOutput.
-     *
-     * @see <a href="https://source.android.com/security/bulletin/2019-12-01#system">the security
-     * bulletin at 2019-12-01</a>
-     */
-    Bluelet enableCVE20192225(boolean value);
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironment.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironment.java
deleted file mode 100644
index 513d649..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironment.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower;
-
-import com.android.libraries.testing.deviceshadower.Enums.Distance;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-
-/**
- * Environment to setup and config Bluetooth unit test.
- */
-public class DeviceShadowEnvironment {
-
-    private static final String TAG = "DeviceShadowEnvironment";
-    private static final long RESET_TIMEOUT_MILLIS = 3000;
-
-    private static boolean sIsInitialized = false;
-
-    private DeviceShadowEnvironment() {
-    }
-
-    public static void init() {
-        sIsInitialized = true;
-        DeviceShadowEnvironmentImpl.reset();
-    }
-
-    public static void reset() {
-        sIsInitialized = false;
-
-        // Order matters because each steps check and manipulate internal objects in order.
-        // Wait Scheduler and executors complete, and shut down executors.
-        DeviceShadowEnvironmentImpl.await(RESET_TIMEOUT_MILLIS);
-
-        // Throw RuntimeException if there is any internal exceptions.
-        DeviceShadowEnvironmentImpl.checkInternalExceptions();
-
-        // Clear internal exceptions, and devicelets.
-        DeviceShadowEnvironmentImpl.reset();
-    }
-
-    public static boolean await(long timeoutMillis) {
-        return DeviceShadowEnvironmentImpl.await(timeoutMillis);
-    }
-
-    public static Devicelet addDevice(final String address) {
-        return DeviceShadowEnvironmentImpl.addDevice(address);
-    }
-
-    public static void removeDevice(String address) {
-        DeviceShadowEnvironmentImpl.removeDevice(address);
-    }
-
-    public static void setLocalDevice(final String address) {
-        DeviceShadowEnvironmentImpl.setLocalDevice(address);
-    }
-
-    public static void putNear(String address1, String address2) {
-        DeviceShadowEnvironmentImpl.setDistance(address1, address2, Distance.NEAR);
-    }
-
-    public static void setDistance(String address1, String address2, Distance distance) {
-        DeviceShadowEnvironmentImpl.setDistance(address1, address2, distance);
-    }
-
-    public static Future<Void> run(final String address, final Runnable snippet) {
-        return run(
-                address,
-                () -> {
-                    snippet.run();
-                    return null;
-                });
-    }
-
-    public static <T> Future<T> run(final String address, final Callable<T> snippet) {
-        return DeviceShadowEnvironmentImpl.run(address, snippet);
-    }
-
-    /* package */
-    static boolean isInitialized() {
-        return sIsInitialized;
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironmentInternal.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironmentInternal.java
deleted file mode 100644
index a5f8e6d..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironmentInternal.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower;
-
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.sms.SmsContentProvider;
-
-/**
- * Internal interface for device shadower.
- */
-public class DeviceShadowEnvironmentInternal {
-
-    /**
-     * Set an interruptible point to tested code.
-     * <p>
-     * This should only make changes when DeviceShadowEnvironment initialized, which means only in
-     * test cases.
-     */
-    public static void setInterruptibleBluetooth(int identifier) {
-        if (DeviceShadowEnvironment.isInitialized()) {
-            assert identifier > 0;
-            DeviceShadowEnvironmentImpl.setInterruptibleBluetooth(identifier);
-        }
-    }
-
-    /**
-     * Mark all bluetooth operation broken after identifier in tested code.
-     */
-    public static void interruptBluetooth(String address, int identifier) {
-        DeviceShadowEnvironmentImpl.interruptBluetooth(address, identifier);
-    }
-
-    /**
-     * Return SMS content provider to be registered by robolectric context.
-     */
-    public static Class<SmsContentProvider> getSmsContentProviderClass() {
-        return SmsContentProvider.class;
-    }
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Devicelet.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Devicelet.java
deleted file mode 100644
index bf31ead..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Devicelet.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower;
-
-/**
- * Devicelet is the handler to operate shadowed device objects in DeviceShadower.
- */
-public interface Devicelet {
-
-    Bluelet bluetooth();
-
-    Nfclet nfc();
-
-    Smslet sms();
-
-    String getAddress();
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Enums.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Enums.java
deleted file mode 100644
index 9eb3514..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Enums.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower;
-
-/**
- * Contains Enums used by DeviceShadower in interface and internally.
- */
-public interface Enums {
-
-    /**
-     * Represents vague distance between two devicelets.
-     */
-    enum Distance {
-        NEAR,
-        MID,
-        FAR,
-        AWAY,
-    }
-
-    /**
-     * Abstract base interface for operations.
-     */
-    interface Operation {
-
-    }
-
-    /**
-     * NFC operations.
-     */
-    enum NfcOperation implements Operation {
-        GET_ADAPTER,
-        ENABLE,
-        DISABLE,
-    }
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Nfclet.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Nfclet.java
deleted file mode 100644
index 4b00f24..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Nfclet.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower;
-
-/**
- * Interface of Nfclet
- */
-public interface Nfclet {
-
-    Nfclet setInitialState(int state);
-
-    Nfclet setInterruptOperation(Enums.NfcOperation operation);
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Smslet.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Smslet.java
deleted file mode 100644
index 483fab6..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Smslet.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower;
-
-import android.net.Uri;
-
-/**
- * Interface of Smslet
- */
-public interface Smslet {
-
-    Smslet addSms(Uri uri, String body);
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetooth.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetooth.java
deleted file mode 100644
index be8390e..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetooth.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 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 android.bluetooth;
-
-import android.content.AttributionSource;
-import android.os.ParcelFileDescriptor;
-import android.os.ParcelUuid;
-
-/**
- * Fake interface replacement for hidden IBluetooth class
- */
-public interface IBluetooth {
-
-    // Bluetooth settings.
-    String getAddress();
-
-    String getName();
-
-    boolean setName(String name);
-
-    // Remote device properties.
-    int getRemoteClass(BluetoothDevice device);
-
-    String getRemoteName(BluetoothDevice device);
-
-    int getRemoteType(BluetoothDevice device, AttributionSource attributionSource);
-
-    ParcelUuid[] getRemoteUuids(BluetoothDevice device);
-
-    boolean fetchRemoteUuids(BluetoothDevice device);
-
-    // Bluetooth discovery.
-    int getScanMode();
-
-    boolean setScanMode(int mode, int duration);
-
-    int getDiscoverableTimeout();
-
-    boolean setDiscoverableTimeout(int timeout);
-
-    boolean startDiscovery();
-
-    boolean cancelDiscovery();
-
-    boolean isDiscovering();
-
-    // Adapter state.
-    boolean isEnabled();
-
-    int getState();
-
-    boolean enable();
-
-    boolean disable();
-
-    // Rfcomm sockets.
-    ParcelFileDescriptor connectSocket(BluetoothDevice device, int type, ParcelUuid uuid,
-            int port, int flag);
-
-    ParcelFileDescriptor createSocketChannel(int type, String serviceName, ParcelUuid uuid,
-            int port, int flag);
-
-    // BLE settings.
-    /* SINCE SDK 21 */ boolean isMultiAdvertisementSupported();
-
-    /* SINCE SDK 22 */ boolean isPeripheralModeSupported();
-
-    /* SINCE SDK 21 */  boolean isOffloadedFilteringSupported();
-
-    // Bonding (pairing).
-    int getBondState(BluetoothDevice device, AttributionSource attributionSource);
-
-    boolean createBond(BluetoothDevice device, int transport, OobData remoteP192Data,
-            OobData remoteP256Data, AttributionSource attributionSource);
-
-    boolean setPairingConfirmation(BluetoothDevice device, boolean accept,
-            AttributionSource attributionSource);
-
-    boolean setPasskey(BluetoothDevice device, int passkey);
-
-    boolean cancelBondProcess(BluetoothDevice device);
-
-    boolean removeBond(BluetoothDevice device);
-
-    BluetoothDevice[] getBondedDevices();
-
-    // Connecting to profiles.
-    int getAdapterConnectionState();
-
-    int getProfileConnectionState(int profile);
-
-    // Access permissions
-    int getPhonebookAccessPermission(BluetoothDevice device);
-
-    boolean setPhonebookAccessPermission(BluetoothDevice device, int value);
-
-    int getMessageAccessPermission(BluetoothDevice device);
-
-    boolean setMessageAccessPermission(BluetoothDevice device, int value);
-
-    int getSimAccessPermission(BluetoothDevice device);
-
-    boolean setSimAccessPermission(BluetoothDevice device, int value);
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGatt.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGatt.java
deleted file mode 100644
index 16e4f01..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGatt.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright 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 android.bluetooth;
-
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanSettings;
-import android.os.ParcelUuid;
-
-import java.util.List;
-
-/**
- * Fake interface replacement for IBluetoothGatt
- * TODO(b/200231384): include >=N interface.
- */
-public interface IBluetoothGatt {
-
-    /* ONLY SDK 23 */
-    void startScan(int appIf, boolean isServer, ScanSettings settings,
-            List<ScanFilter> filters, List<?> scanStorages, String callPackage);
-
-    /* ONLY SDK 21 */
-    void startScan(int appIf, boolean isServer, ScanSettings settings,
-            List<ScanFilter> filters, List<?> scanStorages);
-
-    /* SINCE SDK 21 */
-    void stopScan(int appIf, boolean isServer);
-
-    /* SINCE SDK 21 */
-    void startMultiAdvertising(
-            int appIf, AdvertiseData advertiseData, AdvertiseData scanResponse,
-            AdvertiseSettings settings);
-
-    /* SINCE SDK 21 */
-    void stopMultiAdvertising(int appIf);
-
-    /* SINCE SDK 21 */
-    void registerClient(ParcelUuid appId, IBluetoothGattCallback callback);
-
-    /* SINCE SDK 21 */
-    void unregisterClient(int clientIf);
-
-    /* SINCE SDK 21 */
-    void clientConnect(int clientIf, String address, boolean isDirect, int transport);
-
-    /* SINCE SDK 21 */
-    void clientDisconnect(int clientIf, String address);
-
-    /* SINCE SDK 21 */
-    void discoverServices(int clientIf, String address);
-
-    /* SINCE SDK 21 */
-    void readCharacteristic(int clientIf, String address, int srvcType,
-            int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
-            int authReq);
-
-    /* SINCE SDK 21 */
-    void writeCharacteristic(int clientIf, String address, int srvcType,
-            int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
-            int writeType, int authReq, byte[] value);
-
-    /* SINCE SDK 21 */
-    void readDescriptor(int clientIf, String address, int srvcType,
-            int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
-            int descrInstanceId, ParcelUuid descrUuid, int authReq);
-
-    /* SINCE SDK 21 */
-    void writeDescriptor(int clientIf, String address, int srvcType,
-            int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
-            int descrInstanceId, ParcelUuid descrId, int writeType, int authReq, byte[] value);
-
-    /* SINCE SDK 21 */
-    void registerForNotification(int clientIf, String address, int srvcType,
-            int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
-            boolean enable);
-
-    /* SINCE SDK 21 */
-    void registerServer(ParcelUuid appId, IBluetoothGattServerCallback callback);
-
-    /* SINCE SDK 21 */
-    void unregisterServer(int serverIf);
-
-    /* SINCE SDK 21 */
-    void serverConnect(int servertIf, String address, boolean isDirect, int transport);
-
-    /* SINCE SDK 21 */
-    void serverDisconnect(int serverIf, String address);
-
-    /* SINCE SDK 21 */
-    void beginServiceDeclaration(int serverIf, int srvcType, int srvcInstanceId, int minHandles,
-            ParcelUuid srvcId, boolean advertisePreferred);
-
-    /* SINCE SDK 21 */
-    void addIncludedService(int serverIf, int srvcType, int srvcInstanceId, ParcelUuid srvcId);
-
-    /* SINCE SDK 21 */
-    void addCharacteristic(int serverIf, ParcelUuid charId, int properties, int permissions);
-
-    /* SINCE SDK 21 */
-    void addDescriptor(int serverIf, ParcelUuid descId, int permissions);
-
-    /* SINCE SDK 21 */
-    void endServiceDeclaration(int serverIf);
-
-    /* SINCE SDK 21 */
-    void removeService(int serverIf, int srvcType, int srvcInstanceId, ParcelUuid srvcId);
-
-    /* SINCE SDK 21 */
-    void clearServices(int serverIf);
-
-    /* SINCE SDK 21 */
-    void sendResponse(int serverIf, String address, int requestId,
-            int status, int offset, byte[] value);
-
-    /* SINCE SDK 21 */
-    void sendNotification(int serverIf, String address, int srvcType,
-            int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
-            boolean confirm, byte[] value);
-
-    /* SINCE SDK 21 */
-    void configureMTU(int clientIf, String address, int mtu);
-
-    /* SINCE SDK 21 */
-    void connectionParameterUpdate(int clientIf, String address, int connectionPriority);
-
-    void disconnectAll();
-
-    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states);
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattCallback.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattCallback.java
deleted file mode 100644
index b29369b..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattCallback.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright 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 android.bluetooth;
-
-import android.bluetooth.le.AdvertiseSettings;
-import android.bluetooth.le.ScanResult;
-import android.os.ParcelUuid;
-
-/**
- * Fake interface replacement for IBluetoothGattCallback
- * TODO(b/200231384): include >=N interface.
- */
-public interface IBluetoothGattCallback {
-
-    /* SINCE SDK 21 */
-    void onClientRegistered(int status, int clientIf);
-
-    /* SINCE SDK 21 */
-    void onClientConnectionState(int status, int clientIf, boolean connected, String address);
-
-    /* ONLY SDK 19 */
-    void onScanResult(String address, int rssi, byte[] advData);
-
-    /* SINCE SDK 21 */
-    void onScanResult(ScanResult scanResult);
-
-    /* SINCE SDK 21 */
-    void onGetService(String address, int srvcType, int srvcInstId, ParcelUuid srvcUuid);
-
-    /* SINCE SDK 21 */
-    void onGetIncludedService(String address, int srvcType, int srvcInstId,
-            ParcelUuid srvcUuid, int inclSrvcType,
-            int inclSrvcInstId, ParcelUuid inclSrvcUuid);
-
-    /* SINCE SDK 21 */
-    void onGetCharacteristic(String address, int srvcType,
-            int srvcInstId, ParcelUuid srvcUuid,
-            int charInstId, ParcelUuid charUuid,
-            int charProps);
-
-    /* SINCE SDK 21 */
-    void onGetDescriptor(String address, int srvcType,
-            int srvcInstId, ParcelUuid srvcUuid,
-            int charInstId, ParcelUuid charUuid,
-            int descrInstId, ParcelUuid descrUuid);
-
-    /* SINCE SDK 21 */
-    void onSearchComplete(String address, int status);
-
-    /* SINCE SDK 21 */
-    void onCharacteristicRead(String address, int status, int srvcType,
-            int srvcInstId, ParcelUuid srvcUuid,
-            int charInstId, ParcelUuid charUuid,
-            byte[] value);
-
-    /* SINCE SDK 21 */
-    void onCharacteristicWrite(String address, int status, int srvcType,
-            int srvcInstId, ParcelUuid srvcUuid,
-            int charInstId, ParcelUuid charUuid);
-
-    /* SINCE SDK 21 */
-    void onExecuteWrite(String address, int status);
-
-    /* SINCE SDK 21 */
-    void onDescriptorRead(String address, int status, int srvcType,
-            int srvcInstId, ParcelUuid srvcUuid,
-            int charInstId, ParcelUuid charUuid,
-            int descrInstId, ParcelUuid descrUuid,
-            byte[] value);
-
-    /* SINCE SDK 21 */
-    void onDescriptorWrite(String address, int status, int srvcType,
-            int srvcInstId, ParcelUuid srvcUuid,
-            int charInstId, ParcelUuid charUuid,
-            int descrInstId, ParcelUuid descrUuid);
-
-    /* SINCE SDK 21 */
-    void onNotify(String address, int srvcType,
-            int srvcInstId, ParcelUuid srvcUuid,
-            int charInstId, ParcelUuid charUuid,
-            byte[] value);
-
-    /* SINCE SDK 21 */
-    void onReadRemoteRssi(String address, int rssi, int status);
-
-    /* SDK 21 */
-    void onMultiAdvertiseCallback(int status, boolean isStart,
-            AdvertiseSettings advertiseSettings);
-
-    /* SDK 21 */
-    void onConfigureMTU(String address, int mtu, int status);
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattServerCallback.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattServerCallback.java
deleted file mode 100644
index 10b91bb..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattServerCallback.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 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 android.bluetooth;
-
-import android.os.ParcelUuid;
-
-/**
- * Fake interface of internal IBluetoothGattServerCallback.
- */
-public interface IBluetoothGattServerCallback {
-
-    /* SINCE SDK 21 */
-    void onServerRegistered(int status, int serverIf);
-
-    /* SINCE SDK 21 */
-    void onScanResult(String address, int rssi, byte[] advData);
-
-    /* SINCE SDK 21 */
-    void onServerConnectionState(int status, int serverIf, boolean connected, String address);
-
-    /* SINCE SDK 21 */
-    void onServiceAdded(int status, int srvcType, int srvcInstId, ParcelUuid srvcId);
-
-    /* SINCE SDK 21 */
-    void onCharacteristicReadRequest(String address, int transId, int offset, boolean isLong,
-            int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId, ParcelUuid charId);
-
-    /* SINCE SDK 21 */
-    void onDescriptorReadRequest(String address, int transId, int offset, boolean isLong,
-            int srvcType, int srvcInstId, ParcelUuid srvcId,
-            int charInstId, ParcelUuid charId, ParcelUuid descrId);
-
-    /* SINCE SDK 21 */
-    void onCharacteristicWriteRequest(String address, int transId, int offset, int length,
-            boolean isPrep, boolean needRsp, int srvcType, int srvcInstId, ParcelUuid srvcId,
-            int charInstId, ParcelUuid charId, byte[] value);
-
-    /* SINCE SDK 21 */
-    void onDescriptorWriteRequest(String address, int transId, int offset, int length,
-            boolean isPrep, boolean needRsp, int srvcType, int srvcInstId, ParcelUuid srvcId,
-            int charInstId, ParcelUuid charId, ParcelUuid descrId, byte[] value);
-
-    /* SINCE SDK 21 */
-    void onExecuteWrite(String address, int transId, boolean execWrite);
-
-    /* SINCE SDK 21 */
-    void onNotificationSent(String address, int status);
-
-    /* SINCE SDK 22 */
-    void onMtuChanged(String address, int mtu);
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManager.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManager.java
deleted file mode 100644
index 6bb2209..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManager.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 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.
- */
-
-/**
- * Intentionally in package android.bluetooth to fake existing interface in Android.
- */
-package android.bluetooth;
-
-/**
- * Fake interface for IBluetoothManager.
- */
-public interface IBluetoothManager {
-
-    boolean enable();
-
-    boolean disable(boolean persist);
-
-    String getAddress();
-
-    String getName();
-
-    IBluetooth registerAdapter(IBluetoothManagerCallback callback);
-
-    IBluetoothGatt getBluetoothGatt();
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManagerCallback.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManagerCallback.java
deleted file mode 100644
index f39b82f..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManagerCallback.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 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 android.bluetooth;
-
-/**
- * Fake interface replacement for hidden IBluetoothManagerCallback class
- */
-public interface IBluetoothManagerCallback {
-
-}
-
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/BeamShareData.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/BeamShareData.java
deleted file mode 100644
index 5357a9b..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/BeamShareData.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 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 android.nfc;
-
-import android.net.Uri;
-import android.os.UserHandle;
-
-/**
- * Fake BeamShareData.
- */
-public class BeamShareData {
-
-    public NdefMessage ndefMessage;
-    public Uri[] uris;
-    public UserHandle userHandle;
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/IAppCallback.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/IAppCallback.java
deleted file mode 100644
index 7b62f19..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/IAppCallback.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 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 android.nfc;
-
-/**
- * Fake interface for nfc service.
- */
-public interface IAppCallback {
-
-    /* M */ void onNdefPushComplete(byte peerLlcpVersion);
-
-    /* M */ BeamShareData createBeamShareData(byte peerLlcpVersion);
-
-    /* L */ void onNdefPushComplete();
-
-    /* L */ BeamShareData createBeamShareData();
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/INfcAdapter.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/INfcAdapter.java
deleted file mode 100644
index 08acdbc..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/INfcAdapter.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 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 android.nfc;
-
-/**
- * Fake interface of INfcAdapter
- */
-public interface INfcAdapter {
-
-    void setAppCallback(IAppCallback callback);
-
-    boolean enable();
-
-    boolean disable(boolean saveState);
-
-    int getState();
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleAdvertiser.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleAdvertiser.java
deleted file mode 100644
index f3328c8..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleAdvertiser.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.helpers.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.bluetooth.le.BluetoothLeAdvertiser;
-import android.util.Log;
-
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
-
-import com.google.common.base.Preconditions;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Future;
-
-/**
- * Helper class to operate a device as BLE advertiser.
- */
-public class BleAdvertiser {
-
-    private static final String TAG = "BleAdvertiser";
-
-    private static final int DEFAULT_MODE = AdvertiseSettings.ADVERTISE_MODE_BALANCED;
-    private static final int DEFAULT_TX_POWER_LEVEL = AdvertiseSettings.ADVERTISE_TX_POWER_HIGH;
-    private static final boolean DEFAULT_CONNECTABLE = true;
-    private static final int DEFAULT_TIMEOUT = 0;
-
-
-    /**
-     * Callback of {@link BleAdvertiser}.
-     */
-    public interface Callback {
-
-        void onStartFailure(String address, int errorCode);
-
-        void onStartSuccess(String address, AdvertiseSettings settingsInEffect);
-    }
-
-    /**
-     * Builder class of {@link BleAdvertiser}.
-     */
-    public static final class Builder {
-
-        private final String mAddress;
-        private final Callback mCallback;
-        private AdvertiseSettings mSettings = defaultSettings();
-        private AdvertiseData mData;
-        private AdvertiseData mResponse;
-
-        public Builder(String address, Callback callback) {
-            this.mAddress = Preconditions.checkNotNull(address);
-            this.mCallback = Preconditions.checkNotNull(callback);
-        }
-
-        public Builder setAdvertiseSettings(AdvertiseSettings settings) {
-            this.mSettings = settings;
-            return this;
-        }
-
-        public Builder setAdvertiseData(AdvertiseData data) {
-            this.mData = data;
-            return this;
-        }
-
-        public Builder setResponseData(AdvertiseData response) {
-            this.mResponse = response;
-            return this;
-        }
-
-        public BleAdvertiser build() {
-            return new BleAdvertiser(mAddress, mCallback, mSettings, mData, mResponse);
-        }
-    }
-
-    private static AdvertiseSettings defaultSettings() {
-        return new AdvertiseSettings.Builder()
-                .setAdvertiseMode(DEFAULT_MODE)
-                .setConnectable(DEFAULT_CONNECTABLE)
-                .setTimeout(DEFAULT_TIMEOUT)
-                .setTxPowerLevel(DEFAULT_TX_POWER_LEVEL).build();
-    }
-
-    private final String mAddress;
-    private final Callback mCallback;
-    private final AdvertiseSettings mSettings;
-    private final AdvertiseData mData;
-    private final AdvertiseData mResponse;
-    private final CountDownLatch mStartAdvertiseLatch;
-    private BluetoothLeAdvertiser mAdvertiser;
-
-    private BleAdvertiser(String address, Callback callback, AdvertiseSettings settings,
-            AdvertiseData data, AdvertiseData response) {
-        this.mAddress = address;
-        this.mCallback = callback;
-        this.mSettings = settings;
-        this.mData = data;
-        this.mResponse = response;
-        mStartAdvertiseLatch = new CountDownLatch(1);
-        DeviceShadowEnvironment.addDevice(address).bluetooth()
-                .setAdapterInitialState(BluetoothAdapter.STATE_ON);
-    }
-
-    /**
-     * Starts advertising.
-     */
-    public Future<Void> start() {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
-                mAdvertiser.startAdvertising(mSettings, mData, mResponse, mAdvertiseCallback);
-            }
-        });
-    }
-
-    /**
-     * Stops advertising.
-     */
-    public Future<Void> stop() {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mAdvertiser.stopAdvertising(mAdvertiseCallback);
-            }
-        });
-    }
-
-    public void waitTillAdvertiseCompleted() {
-        try {
-            mStartAdvertiseLatch.await();
-        } catch (InterruptedException e) {
-            Log.w(TAG, mAddress + " fails to wait till advertise completed: ", e);
-        }
-    }
-
-    private final AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
-        @Override
-        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
-            Log.v(TAG,
-                    String.format("onStartSuccess(settingsInEffect: %s) on %s ", settingsInEffect,
-                            mAddress));
-            mCallback.onStartSuccess(mAddress, settingsInEffect);
-            mStartAdvertiseLatch.countDown();
-        }
-
-        @Override
-        public void onStartFailure(int errorCode) {
-            Log.v(TAG, String.format("onStartFailure(errorCode: %d) on %s", errorCode, mAddress));
-            mCallback.onStartFailure(mAddress, errorCode);
-            mStartAdvertiseLatch.countDown();
-        }
-    };
-}
-
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleScanner.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleScanner.java
deleted file mode 100644
index 6a44c2b..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleScanner.java
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.helpers.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.util.Log;
-
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
-
-import com.google.common.base.Preconditions;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Helper class to operate a device as BLE scanner.
- */
-public class BleScanner {
-
-    private static final String TAG = "BleScanner";
-
-    private static final int DEFAULT_MODE = ScanSettings.SCAN_MODE_LOW_LATENCY;
-    private static final int DEFAULT_CALLBACK_TYPE = ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
-    private static final long DEFAULT_DELAY = 0L;
-
-    /**
-     * Callback of {@link BleScanner}.
-     */
-    public interface Callback {
-
-        void onScanResult(String address, int callbackType, ScanResult result);
-
-        void onBatchScanResults(String address, List<ScanResult> results);
-
-        void onScanFailed(String address, int errorCode);
-    }
-
-    /**
-     * Builder class of {@link BleScanner}.
-     */
-    public static final class Builder {
-
-        private final String mAddress;
-        private final Callback mCallback;
-        private ScanSettings mSettings = defaultSettings();
-        private List<ScanFilter> mFilters;
-        private int mNumOfExpectedScanCallbacks = 1;
-
-        public Builder(String address, Callback callback) {
-            this.mAddress = Preconditions.checkNotNull(address);
-            this.mCallback = Preconditions.checkNotNull(callback);
-        }
-
-        public Builder setScanSettings(ScanSettings settings) {
-            this.mSettings = settings;
-            return this;
-        }
-
-        public Builder addScanFilter(ScanFilter... filterArgs) {
-            if (this.mFilters == null) {
-                this.mFilters = new ArrayList<>();
-            }
-            for (ScanFilter filter : filterArgs) {
-                this.mFilters.add(filter);
-            }
-            return this;
-        }
-
-        /**
-         * Sets number of expected scan result callback.
-         *
-         * @param num Number of expected scan result callback, default to 1.
-         */
-        public Builder setNumOfExpectedScanCallbacks(int num) {
-            mNumOfExpectedScanCallbacks = num;
-            return this;
-        }
-
-        public BleScanner build() {
-            return new BleScanner(
-                    mAddress, mCallback, mSettings, mFilters, mNumOfExpectedScanCallbacks);
-        }
-    }
-
-    private static ScanSettings defaultSettings() {
-        return new ScanSettings.Builder()
-                .setScanMode(DEFAULT_MODE)
-                .setCallbackType(DEFAULT_CALLBACK_TYPE)
-                .setReportDelay(DEFAULT_DELAY).build();
-    }
-
-    private final String mAddress;
-    private final Callback mCallback;
-    private final ScanSettings mSettings;
-    private final List<ScanFilter> mFilters;
-    private final BlockingQueue<Integer> mScanResultCounts;
-    private int mNumOfExpectedScanCallbacks;
-    private int mNumOfReceivedScanCallbacks;
-    private BluetoothLeScanner mScanner;
-
-    private BleScanner(String address, Callback callback, ScanSettings settings,
-            List<ScanFilter> filters, int numOfExpectedScanResult) {
-        this.mAddress = address;
-        this.mCallback = callback;
-        this.mSettings = settings;
-        this.mFilters = filters;
-        this.mNumOfExpectedScanCallbacks = numOfExpectedScanResult;
-        this.mNumOfReceivedScanCallbacks = 0;
-        this.mScanResultCounts = new LinkedBlockingQueue<>(numOfExpectedScanResult);
-        DeviceShadowEnvironment.addDevice(address).bluetooth()
-                .setAdapterInitialState(BluetoothAdapter.STATE_ON);
-    }
-
-    public Future<Void> start() {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
-                mScanner.startScan(mFilters, mSettings, mScanCallback);
-            }
-        });
-    }
-
-    public void waitTillNextScanResult(long timeoutMillis) {
-        Integer result = null;
-        if (mNumOfReceivedScanCallbacks >= mNumOfExpectedScanCallbacks) {
-            return;
-        }
-        try {
-            if (timeoutMillis < 0) {
-                result = mScanResultCounts.take();
-            } else {
-                result = mScanResultCounts.poll(timeoutMillis, TimeUnit.MILLISECONDS);
-            }
-            if (result != null && result >= 0) {
-                mNumOfReceivedScanCallbacks++;
-            }
-            Log.v(TAG, "Scan results: " + result);
-        } catch (InterruptedException e) {
-            Log.w(TAG, mAddress + " fails to wait till next scan result: ", e);
-        }
-    }
-
-    public void waitTillNextScanResult() {
-        waitTillNextScanResult(-1);
-    }
-
-    public void waitTillAllScanResults() {
-        while (mNumOfReceivedScanCallbacks < mNumOfExpectedScanCallbacks) {
-            try {
-                if (mScanResultCounts.take() >= 0) {
-                    mNumOfReceivedScanCallbacks++;
-                }
-            } catch (InterruptedException e) {
-                Log.w(TAG, String.format("%s fails to wait scan result", mAddress), e);
-                return;
-            }
-        }
-    }
-
-    public Future<Void> stop() {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
-                mScanner.stopScan(mScanCallback);
-            }
-        });
-    }
-
-    private final ScanCallback mScanCallback = new ScanCallback() {
-        @Override
-        public void onScanResult(int callbackType, ScanResult result) {
-            Log.v(TAG, String.format("onScanResult(callbackType: %d, result: %s) on %s",
-                    callbackType, result, mAddress));
-            mCallback.onScanResult(mAddress, callbackType, result);
-            try {
-                mScanResultCounts.put(1);
-            } catch (InterruptedException e) {
-                // no-op.
-            }
-        }
-
-        @Override
-        public void onBatchScanResults(List<ScanResult> results) {
-            /**** Not supported yet.
-             Log.v(TAG, String.format("onBatchScanResults(results: %s) on %s",
-             Arrays.toString(results.toArray()), address));
-             callback.onBatchScanResults(address, results);
-             try {
-             scanResultCounts.put(results.size());
-             } catch (InterruptedException e) {
-             // no-op.
-             }
-             */
-        }
-
-        @Override
-        public void onScanFailed(int errorCode) {
-            /**** Not supported yet.
-             Log.v(TAG, String.format("onScanFailed(errorCode: %d) on %s", errorCode, address));
-             callback.onScanFailed(address, errorCode);
-             try {
-             scanResultCounts.put(-1);
-             } catch (InterruptedException e) {
-             // no-op.
-             }
-             */
-        }
-    };
-}
-
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattClient.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattClient.java
deleted file mode 100644
index 69e77af..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattClient.java
+++ /dev/null
@@ -1,324 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.helpers.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCallback;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.content.Context;
-import android.util.Log;
-
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Helper class to operate a device as gatt client.
- */
-public class BluetoothGattClient {
-
-    private static final String TAG = "BluetoothGattClient";
-    private static final int LATCH_TIMEOUT_MILLIS = 1000;
-
-    /**
-     * Callback of BluetoothGattClient.
-     */
-    public interface Callback {
-
-        void onConnectionStateChange(String address, int status, int newState);
-
-        void onCharacteristicChanged(String address, UUID uuid, byte[] value);
-
-        void onCharacteristicRead(String address, UUID uuid, byte[] value, int status);
-
-        void onCharacteristicWrite(String address, UUID uuid, byte[] value, int status);
-
-        void onDescriptorRead(String address, UUID uuid, byte[] value, int status);
-
-        void onDescriptorWrite(String address, UUID uuid, byte[] value, int status);
-
-        void onServicesDiscovered(
-                UUID[] serviceUuid, UUID[] characteristicUuid, UUID[] descriptorUuid, int status);
-
-        void onConfigureMTU(String address, int mtu, int status);
-    }
-
-    private final String mAddress;
-    private final Callback mCallback;
-    private final Context mContext;
-    private final Map<UUID, BluetoothGattCharacteristic> mCharacteristics = new HashMap<>();
-    private final Map<UUID, BluetoothGattDescriptor> mDescriptors = new HashMap<>();
-    private BluetoothGatt mGatt;
-    private CountDownLatch mConnectionLatch;
-    private CountDownLatch mServiceDiscoverLatch;
-
-    public BluetoothGattClient(String address, Callback callback, Context context) {
-        this.mAddress = address;
-        this.mCallback = callback;
-        this.mContext = context;
-        DeviceShadowEnvironment.addDevice(address).bluetooth()
-                .setAdapterInitialState(BluetoothAdapter.STATE_ON);
-    }
-
-    public Future<Void> connect(final String remoteAddress) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mConnectionLatch = new CountDownLatch(1);
-                mGatt = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(remoteAddress)
-                        .connectGatt(mContext, false /* auto connect */, mGattCallback);
-                try {
-                    mConnectionLatch.await(LATCH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
-                } catch (InterruptedException e) {
-                    // no-op.
-                }
-
-                mServiceDiscoverLatch = new CountDownLatch(1);
-                mGatt.discoverServices();
-                try {
-                    mServiceDiscoverLatch.await(LATCH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
-                } catch (InterruptedException e) {
-                    // no-op.
-                }
-            }
-        });
-    }
-
-    public Future<Void> close() {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mGatt.disconnect();
-                mGatt.close();
-            }
-        });
-    }
-
-    public Future<Void> readCharacteristic(final UUID uuid) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mGatt.readCharacteristic(mCharacteristics.get(uuid));
-            }
-        });
-    }
-
-    public Future<Void> setNotification(final UUID uuid) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mGatt.setCharacteristicNotification(mCharacteristics.get(uuid), true);
-            }
-        });
-    }
-
-    public Future<Void> writeCharacteristic(final UUID uuid, final byte[] value) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                BluetoothGattCharacteristic characteristic = mCharacteristics.get(uuid);
-                characteristic.setValue(value);
-                mGatt.writeCharacteristic(characteristic);
-            }
-        });
-    }
-
-    /**
-     * Reads the value of a descriptor with given UUID.
-     *
-     * <p>If different characteristics on the service have the same descriptor, use {@link
-     * BluetoothGattClient#readDescriptor(UUID, UUID)} instead.
-     */
-    public Future<Void> readDescriptor(final UUID uuid) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mGatt.readDescriptor(mDescriptors.get(uuid));
-            }
-        });
-    }
-
-    /**
-     * Reads the descriptor value of the specified characteristic.
-     */
-    public Future<Void> readDescriptor(final UUID descriptorUuid, final UUID characteristicUuid) {
-        return DeviceShadowEnvironment.run(
-                mAddress,
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        mGatt.readDescriptor(
-                                mCharacteristics.get(characteristicUuid)
-                                        .getDescriptor(descriptorUuid));
-                    }
-                });
-    }
-
-    /**
-     * Writes to the descriptor with given UUID.
-     *
-     * <p>If different characteristics on the service have the same descriptor, use {@link
-     * BluetoothGattClient#writeDescriptor(UUID, UUID, byte[])} instead.
-     */
-    public Future<Void> writeDescriptor(final UUID uuid, final byte[] value) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                BluetoothGattDescriptor descriptor = mDescriptors.get(uuid);
-                descriptor.setValue(value);
-                mGatt.writeDescriptor(descriptor);
-            }
-        });
-    }
-
-    /**
-     * Writes to the descriptor of the specified characteristic.
-     */
-    public Future<Void> writeDescriptor(
-            final UUID descriptorUuid, final UUID characteristicUuid, final byte[] value) {
-        return DeviceShadowEnvironment.run(
-                mAddress,
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        BluetoothGattDescriptor descriptor =
-                                mCharacteristics.get(characteristicUuid)
-                                        .getDescriptor(descriptorUuid);
-                        descriptor.setValue(value);
-                        mGatt.writeDescriptor(descriptor);
-                    }
-                });
-    }
-
-    public Future<Void> requestMtu(int mtu) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mGatt.requestMtu(mtu);
-            }
-        });
-    }
-
-    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
-        @Override
-        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
-            Log.v(TAG, String.format("onConnectionStateChange(status: %s, newState: %s)",
-                    status, newState));
-            if (mConnectionLatch != null) {
-                mConnectionLatch.countDown();
-            }
-            mCallback.onConnectionStateChange(gatt.getDevice().getAddress(), status, newState);
-        }
-
-        @Override
-        public void onCharacteristicChanged(
-                BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
-            Log.v(TAG, String.format("onCharacteristicChanged(characteristic: %s, value: %s)",
-                    characteristic.getUuid(), Arrays.toString(characteristic.getValue())));
-            mCallback.onCharacteristicChanged(
-                    gatt.getDevice().getAddress(), characteristic.getUuid(),
-                    characteristic.getValue());
-        }
-
-        @Override
-        public void onCharacteristicRead(
-                BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
-            Log.v(TAG, String.format("onCharacteristicRead(descriptor: %s, status: %s)",
-                    characteristic.getUuid(), status));
-            mCallback.onCharacteristicRead(
-                    gatt.getDevice().getAddress(), characteristic.getUuid(),
-                    characteristic.getValue(),
-                    status);
-        }
-
-        @Override
-        public void onCharacteristicWrite(
-                BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
-            Log.v(TAG, String.format("onCharacteristicWrite(descriptor: %s, status: %s)",
-                    characteristic.getUuid(), status));
-            mCallback.onCharacteristicWrite(gatt.getDevice().getAddress(),
-                    characteristic.getUuid(), characteristic.getValue(), status);
-        }
-
-        @Override
-        public void onDescriptorRead(
-                BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
-            Log.v(TAG, String.format("onDescriptorRead(descriptor: %s, status: %s)",
-                    descriptor.getUuid(), status));
-            mCallback.onDescriptorRead(
-                    gatt.getDevice().getAddress(), descriptor.getUuid(), descriptor.getValue(),
-                    status);
-        }
-
-        @Override
-        public void onDescriptorWrite(
-                BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
-            Log.v(TAG, String.format("onDescriptorWrite(descriptor: %s, status: %s)",
-                    descriptor.getUuid(), status));
-            mCallback.onDescriptorWrite(
-                    gatt.getDevice().getAddress(), descriptor.getUuid(), descriptor.getValue(),
-                    status);
-        }
-
-        @Override
-        public synchronized void onServicesDiscovered(BluetoothGatt gatt, int status) {
-            Log.v(TAG, "Discovered service: " + gatt.getServices());
-            List<UUID> serviceUuid = new ArrayList<>();
-            List<UUID> characteristicUuid = new ArrayList<>();
-            List<UUID> descriptorUuid = new ArrayList<>();
-            for (BluetoothGattService service : gatt.getServices()) {
-                serviceUuid.add(service.getUuid());
-                for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
-                    mCharacteristics.put(characteristic.getUuid(), characteristic);
-                    characteristicUuid.add(characteristic.getUuid());
-                    for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) {
-                        mDescriptors.put(descriptor.getUuid(), descriptor);
-                        descriptorUuid.add(descriptor.getUuid());
-                    }
-                }
-            }
-
-            Collections.sort(serviceUuid);
-            Collections.sort(characteristicUuid);
-            Collections.sort(descriptorUuid);
-
-            mCallback.onServicesDiscovered(serviceUuid.toArray(new UUID[serviceUuid.size()]),
-                    characteristicUuid.toArray(new UUID[characteristicUuid.size()]),
-                    descriptorUuid.toArray(new UUID[descriptorUuid.size()]),
-                    status);
-            mServiceDiscoverLatch.countDown();
-        }
-
-        @Override
-        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
-            Log.v(TAG, String.format("onMtuChanged(mtu: %s, status: %s)", mtu, status));
-            mCallback.onConfigureMTU(gatt.getDevice().getAddress(), mtu, status);
-        }
-    };
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattMaster.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattMaster.java
deleted file mode 100644
index e9f364a..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattMaster.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.helpers.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattServer;
-import android.bluetooth.BluetoothGattServerCallback;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothManager;
-import android.content.Context;
-import android.util.Log;
-
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.Future;
-
-/**
- * Helper class to operate a device as gatt server.
- */
-public class BluetoothGattMaster {
-
-    private static final String TAG = "BluetoothGattMaster";
-
-    /**
-     * Callback of BluetoothGattMaster.
-     */
-    public interface Callback {
-
-        void onConnectionStateChange(String address, int status, int newState);
-
-        void onCharacteristicReadRequest(String address, UUID uuid);
-
-        void onCharacteristicWriteRequest(String address, UUID uuid, byte[] value,
-                boolean preparedWrite, boolean responseNeeded);
-
-        void onDescriptorReadRequest(String address, UUID uuid);
-
-        void onDescriptorWriteRequest(String address, UUID uuid, byte[] value,
-                boolean preparedWrite, boolean responseNeeded);
-
-        void onNotificationSent(String address, int status);
-
-        void onExecuteWrite(String address, boolean execute);
-
-        void onServiceAdded(UUID uuid, int status);
-
-        void onMtuChanged(String address, int mtu);
-    }
-
-    private final String mAddress;
-    private final Callback mCallback;
-    private final Context mContext;
-    private BluetoothGattServer mGattServer;
-    private final Map<UUID, BluetoothGattCharacteristic> mCharacteristics = new HashMap<>();
-
-    public BluetoothGattMaster(String address, Callback callback, Context context) {
-        this.mAddress = address;
-        this.mCallback = callback;
-        this.mContext = context;
-        DeviceShadowEnvironment.addDevice(address).bluetooth()
-                .setAdapterInitialState(BluetoothAdapter.STATE_ON);
-    }
-
-    public Future<Void> start(final BluetoothGattService service) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
-                mGattServer = manager.openGattServer(mContext, mGattServerCallback);
-                mGattServer.addService(service);
-            }
-        });
-    }
-
-    public Future<Void> stop() {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mGattServer.close();
-            }
-        });
-    }
-
-    public Future<Void> notifyCharacteristic(
-            final String remoteAddress, final UUID uuid, final byte[] value,
-            final boolean confirm) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                BluetoothGattCharacteristic characteristic = mCharacteristics.get(uuid);
-                characteristic.setValue(value);
-                mGattServer.notifyCharacteristicChanged(
-                        BluetoothAdapter.getDefaultAdapter().getRemoteDevice(remoteAddress),
-                        characteristic, confirm);
-            }
-        });
-    }
-
-    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
-        @Override
-        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
-            String address = device.getAddress();
-            Log.v(TAG, String.format(
-                    "BluetoothGattServerManager.onConnectionStateChange on %s: status %d,"
-                            + " newState %d", address, status, newState));
-            mCallback.onConnectionStateChange(address, status, newState);
-        }
-
-        @Override
-        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
-                BluetoothGattCharacteristic characteristic) {
-            String address = device.getAddress();
-            UUID uuid = characteristic.getUuid();
-            Log.v(TAG,
-                    String.format("BluetoothGattServerManager.onCharacteristicReadRequest on %s: "
-                                    + "characteristic %s, request %d, offset %d",
-                            address, uuid, requestId, offset));
-            mCallback.onCharacteristicReadRequest(address, uuid);
-            mGattServer.sendResponse(
-                    device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
-                    characteristic.getValue());
-        }
-
-        @Override
-        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
-                BluetoothGattCharacteristic characteristic, boolean preparedWrite,
-                boolean responseNeeded,
-                int offset, byte[] value) {
-            String address = device.getAddress();
-            UUID uuid = characteristic.getUuid();
-            Log.v(TAG,
-                    String.format("BluetoothGattServerManager.onCharacteristicWriteRequest on %s: "
-                                    + "characteristic %s, request %d, offset %d, preparedWrite %b, "
-                                    + "responseNeeded %b",
-                            address, uuid, requestId, offset, preparedWrite, responseNeeded));
-            mCallback.onCharacteristicWriteRequest(address, uuid, value, preparedWrite,
-                    responseNeeded);
-
-            if (responseNeeded) {
-                mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
-                        null);
-            }
-        }
-
-        @Override
-        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
-                BluetoothGattDescriptor descriptor) {
-            String address = device.getAddress();
-            UUID uuid = descriptor.getUuid();
-            Log.v(TAG, String.format("BluetoothGattServerManager.onDescriptorReadRequest on %s: "
-                            + " descriptor %s, requestId %d, offset %d",
-                    address, uuid, requestId, offset));
-            mCallback.onDescriptorReadRequest(address, uuid);
-            mGattServer.sendResponse(
-                    device, requestId, BluetoothGatt.GATT_SUCCESS, offset, descriptor.getValue());
-        }
-
-        @Override
-        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
-                BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded,
-                int offset, byte[] value) {
-            String address = device.getAddress();
-            UUID uuid = descriptor.getUuid();
-            Log.v(TAG, String.format("BluetoothGattServerManager.onDescriptorWriteRequest on %s: "
-                            + "descriptor %s, requestId %d, offset %d, preparedWrite %b, "
-                            + "responseNeeded %b",
-                    address, uuid, requestId, offset, preparedWrite, responseNeeded));
-            mCallback.onDescriptorWriteRequest(address, uuid, value, preparedWrite, responseNeeded);
-
-            if (responseNeeded) {
-                mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
-                        null);
-            }
-        }
-
-        @Override
-        public void onNotificationSent(BluetoothDevice device, int status) {
-            String address = device.getAddress();
-            Log.v(TAG,
-                    String.format("BluetoothGattServerManager.onNotificationSent on %s: status %d",
-                            address, status));
-            mCallback.onNotificationSent(address, status);
-        }
-
-        @Override
-        public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
-            /*** Not implemented yet
-             String address = device.getAddress();
-             Log.v(TAG, String.format(
-             "BluetoothGattServerManager.onExecuteWrite on %s: requestId %d, execute %b",
-             address, requestId, execute));
-             callback.onExecuteWrite(address, execute);
-             */
-        }
-
-        @Override
-        public void onServiceAdded(int status, BluetoothGattService service) {
-            UUID uuid = service.getUuid();
-            Log.v(TAG, String.format(
-                    "BluetoothGattServerManager.onServiceAdded: service %s, status %d",
-                    uuid, status));
-            mCallback.onServiceAdded(uuid, status);
-
-            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
-                mCharacteristics.put(characteristic.getUuid(), characteristic);
-            }
-        }
-
-        @Override
-        public void onMtuChanged(BluetoothDevice device, int mtu) {
-            Log.v(TAG, String.format("onMtuChanged(mtu: %s)", mtu));
-            mCallback.onMtuChanged(device.getAddress(), mtu);
-        }
-    };
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommAcceptor.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommAcceptor.java
deleted file mode 100644
index 5204c2a..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommAcceptor.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.helpers.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothServerSocket;
-import android.bluetooth.BluetoothSocket;
-import android.util.Log;
-
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironmentInternal;
-import com.android.libraries.testing.deviceshadower.helpers.utils.IOUtils;
-
-import java.io.IOException;
-import java.util.Queue;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Helper class to operate a device with basic functionality to accept BluetoothRfcommConnection.
- *
- * <p>
- * Usage: // Create a virtual device to accept incoming connection. BluetoothRfcommAcceptor acceptor
- * = new BluetoothRfcommAcceptor(address, uuid, callback); // Start accepting incoming connection,
- * with given uuid. acceptor.start(); // Connector needs to wait till acceptor started to make sure
- * there is a server socket created. acceptor.waitTillServerSocketStarted();
- *
- * // Connector can initiate connection.
- *
- * // A blocking call to wait for connection. acceptor.waitTillConnected();
- *
- * // Acceptor sends a message acceptor.send("Hello".getBytes());
- *
- * // Cancel acceptor to release all blocking calls. acceptor.cancel();
- */
-public class BluetoothRfcommAcceptor {
-
-    private static final String TAG = "BluetoothRfcommAcceptor";
-
-    /**
-     * Identifiers to control Bluetooth operation.
-     */
-    public static final int PRE_START = 4;
-    public static final int PRE_ACCEPT = 1;
-    public static final int PRE_WRITE = 3;
-    public static final int PRE_READ = 2;
-
-    private final String mAddress;
-    private final UUID mUuid;
-    private BluetoothSocket mSocket;
-    private BluetoothServerSocket mServerSocket;
-
-    private final AtomicBoolean mCancelled;
-    private final Callback mCallback;
-    private final CountDownLatch mStartLatch = new CountDownLatch(1);
-    private final CountDownLatch mConnectLatch = new CountDownLatch(1);
-    private final Queue<CountDownLatch> mReadLatches = new ConcurrentLinkedQueue<>();
-
-    /**
-     * Callback of BluetoothRfcommAcceptor.
-     */
-    public interface Callback {
-
-        void onSocketAccepted(BluetoothSocket socket);
-
-        void onDataReceived(byte[] data);
-
-        void onDataWritten(byte[] data);
-
-        void onError(Exception exception);
-    }
-
-    public BluetoothRfcommAcceptor(String address, UUID uuid, Callback callback) {
-        this.mAddress = address;
-        this.mUuid = uuid;
-        this.mCallback = callback;
-        this.mCancelled = new AtomicBoolean(false);
-        DeviceShadowEnvironment.addDevice(address).bluetooth()
-                .setAdapterInitialState(BluetoothAdapter.STATE_ON);
-    }
-
-    /**
-     * Start bluetooth server socket, accept incoming connection, and receive incoming data once
-     * connected.
-     */
-    public Future<Void> start() {
-        return DeviceShadowEnvironment.run(mAddress, mCode);
-    }
-
-    /**
-     * Blocking call to wait bluetooth server socket started.
-     */
-    public void waitTillServerSocketStarted() {
-        try {
-            mStartLatch.await();
-        } catch (InterruptedException e) {
-            Log.w(TAG, mAddress + " fail to wait till started: ", e);
-        }
-    }
-
-    public void waitTillConnected() {
-        try {
-            mConnectLatch.await();
-        } catch (InterruptedException e) {
-            Log.w(TAG, mAddress + " fail to wait till started: ", e);
-        }
-    }
-
-    public void waitTillDataReceived() {
-        try {
-            if (mReadLatches.size() > 0) {
-                mReadLatches.poll().await();
-            }
-        } catch (InterruptedException e) {
-            // no-op
-        }
-    }
-
-    /**
-     * Stop receiving data by closing socket.
-     */
-    public Future<Void> cancel() {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mCancelled.set(true);
-                try {
-                    mSocket.close();
-                } catch (IOException e) {
-                    Log.w(TAG, mAddress + " fail to close server socket", e);
-                }
-            }
-        });
-    }
-
-    /**
-     * Send data to connected device.
-     */
-    public Future<Void> send(final byte[] data) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                if (mSocket != null) {
-                    try {
-                        DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_WRITE);
-                        IOUtils.write(mSocket.getOutputStream(), data);
-                        Log.d(TAG, mAddress + " write: " + new String(data));
-                        mCallback.onDataWritten(data);
-                    } catch (IOException e) {
-                        Log.w(TAG, mAddress + " fail to write: ", e);
-                        mCallback.onError(new IOException("Fail to write", e));
-                    }
-                }
-            }
-        });
-    }
-
-    private Runnable mCode = new Runnable() {
-        @Override
-        public void run() {
-            try {
-                DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_START);
-                mServerSocket = BluetoothAdapter.getDefaultAdapter()
-                        .listenUsingInsecureRfcommWithServiceRecord("AA", mUuid);
-            } catch (IOException e) {
-                Log.w(TAG, mAddress + " fail to start server socket: ", e);
-                mCallback.onError(new IOException("Fail to start server socket", e));
-                return;
-            } finally {
-                mStartLatch.countDown();
-            }
-
-            try {
-                DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_ACCEPT);
-                mSocket = mServerSocket.accept();
-                Log.d(TAG, mAddress + " accept: " + mSocket.getRemoteDevice().getAddress());
-                mCallback.onSocketAccepted(mSocket);
-                mServerSocket.close();
-            } catch (IOException e) {
-                Log.w(TAG, mAddress + " fail to connect: ", e);
-                mCallback.onError(new IOException("Fail to connect", e));
-                return;
-            } finally {
-                mConnectLatch.countDown();
-            }
-
-            do {
-                try {
-                    CountDownLatch latch = new CountDownLatch(1);
-                    mReadLatches.add(latch);
-                    DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_READ);
-                    byte[] data = IOUtils.read(mSocket.getInputStream());
-                    Log.d(TAG, mAddress + " read: " + new String(data));
-                    mCallback.onDataReceived(data);
-                    latch.countDown();
-                } catch (IOException e) {
-                    Log.w(TAG, mAddress + " fail to read: ", e);
-                    mCallback.onError(new IOException("Fail to read", e));
-                    return;
-                }
-            } while (!mCancelled.get());
-
-            Log.d(TAG, mAddress + " stop receiving");
-        }
-    };
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommConnector.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommConnector.java
deleted file mode 100644
index e386d59..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommConnector.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.helpers.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothSocket;
-import android.util.Log;
-
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironmentInternal;
-import com.android.libraries.testing.deviceshadower.helpers.utils.IOUtils;
-
-import java.io.IOException;
-import java.util.Queue;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Helper class to operate a device with basic functionality to accept BluetoothRfcommConnection.
- *
- * <p>
- * Usage: // Create a virtual device to initiate connection. BluetoothRfcommConnector connector =
- * new BluetoothRfcommConnector(address, callback); // Start connection to a remote address with
- * given uuid. connector.start(remoteAddress, remoteUuid);
- *
- * // A blocking call to wait for connection. connector.waitTillConnected();
- *
- * // Connector sends a message connector.send("Hello".getBytes());
- *
- * // Cancel connector to release all blocking calls. connector.cancel();
- */
-public class BluetoothRfcommConnector {
-
-    private static final String TAG = "BluetoothRfcommConnector";
-
-    /**
-     * Identifiers to control Bluetooth operation.
-     */
-    public static final int PRE_CONNECT = 1;
-    public static final int PRE_READ = 2;
-    public static final int PRE_WRITE = 3;
-
-    private final String mAddress;
-    private String mRemoteAddress = null;
-    private final UUID mRemoteUuid;
-    private BluetoothSocket mSocket;
-
-    private final Callback mCallback;
-    private final AtomicBoolean mCancelled;
-    private final CountDownLatch mConnectLatch = new CountDownLatch(1);
-    private final Queue<CountDownLatch> mReadLatches = new ConcurrentLinkedQueue<>();
-
-    /**
-     * Callback of BluetoothRfcommConnector.
-     */
-    public interface Callback {
-
-        void onConnected(BluetoothSocket socket);
-
-        void onDataReceived(byte[] data);
-
-        void onDataWritten(byte[] data);
-
-        void onError(Exception exception);
-    }
-
-    public BluetoothRfcommConnector(String address, UUID uuid, Callback callback) {
-        this.mAddress = address;
-        this.mRemoteUuid = uuid;
-        this.mCallback = callback;
-        this.mCancelled = new AtomicBoolean(false);
-        DeviceShadowEnvironment.addDevice(address).bluetooth()
-                .setAdapterInitialState(BluetoothAdapter.STATE_ON);
-    }
-
-    /**
-     * Start connection to a remote address, and receive data once connected.
-     */
-    public Future<Void> start(String remoteAddress) {
-        this.mRemoteAddress = remoteAddress;
-        return DeviceShadowEnvironment.run(mAddress, mCode);
-    }
-
-    /**
-     * Stop receiving data.
-     */
-    public Future<Void> cancel() {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mCancelled.set(true);
-                try {
-                    mSocket.close();
-                } catch (IOException e) {
-                    Log.w(TAG, mAddress + " fail to close socket", e);
-                }
-            }
-        });
-    }
-
-    public void waitTillConnected() {
-        try {
-            mConnectLatch.await();
-        } catch (InterruptedException e) {
-            Log.w(TAG, mAddress + " fail to wait till started: ", e);
-        }
-    }
-
-    public void waitTillDataReceived() {
-        try {
-            if (mReadLatches.size() > 0) {
-                mReadLatches.poll().await();
-            }
-        } catch (InterruptedException e) {
-            // no-op.
-        }
-    }
-
-    /**
-     * Send data to conneceted device.
-     */
-    public Future<Void> send(final byte[] data) {
-        return DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                if (mSocket != null) {
-                    try {
-                        DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_WRITE);
-                        IOUtils.write(mSocket.getOutputStream(), data);
-                        Log.d(TAG, mAddress + " write: " + new String(data));
-                        mCallback.onDataWritten(data);
-                    } catch (IOException e) {
-                        Log.w(TAG, mAddress + " fail to write: ", e);
-                        mCallback.onError(new IOException("Fail to write", e));
-                    }
-                }
-            }
-        });
-    }
-
-    private Runnable mCode = new Runnable() {
-        @Override
-        public void run() {
-            try {
-                DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_CONNECT);
-                mSocket = BluetoothAdapter.getDefaultAdapter()
-                        .getRemoteDevice(mRemoteAddress)
-                        .createInsecureRfcommSocketToServiceRecord(mRemoteUuid);
-                mSocket.connect();
-                Log.d(TAG, mAddress + " accept: " + mSocket.getRemoteDevice().getAddress());
-                mCallback.onConnected(mSocket);
-            } catch (IOException e) {
-                Log.w(TAG, mAddress + " fail to connect: ", e);
-                mCallback.onError(new IOException("Fail to connect", e));
-            } finally {
-                mConnectLatch.countDown();
-            }
-
-            try {
-                do {
-                    CountDownLatch latch = new CountDownLatch(1);
-                    mReadLatches.add(latch);
-                    DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_READ);
-                    byte[] data = IOUtils.read(mSocket.getInputStream());
-                    Log.d(TAG, mAddress + " read: " + new String(data));
-                    mCallback.onDataReceived(data);
-                    latch.countDown();
-                } while (!mCancelled.get());
-            } catch (IOException e) {
-                Log.w(TAG, mAddress + " fail to read: ", e);
-                mCallback.onError(new IOException("Fail to read", e));
-            }
-            Log.d(TAG, mAddress + " stop receiving");
-        }
-    };
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcActivity.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcActivity.java
deleted file mode 100644
index 8ae4435..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcActivity.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.helpers.nfc;
-
-import android.app.Activity;
-
-/**
- * Activity that triggers or receives NFC events.
- */
-public class NfcActivity extends Activity {
-
-    private NfcReceiver.Callback mCallback;
-
-    public void setCallback(NfcReceiver.Callback callback) {
-        this.mCallback = callback;
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        NfcReceiver.processIntent(mCallback, getIntent());
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcReceiver.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcReceiver.java
deleted file mode 100644
index b85a124..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcReceiver.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.helpers.nfc;
-
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.nfc.NdefMessage;
-import android.nfc.NfcAdapter;
-import android.os.Parcelable;
-import android.util.Log;
-
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Helper class to receive NFC events.
- */
-public class NfcReceiver {
-
-    private static final String TAG = "NfcReceiver";
-
-    /**
-     * Callback to receive message.
-     */
-    public interface Callback {
-
-        void onReceive(String message);
-    }
-
-    private final String mAddress;
-    private final Activity mActivity;
-    private CountDownLatch mReceiveLatch;
-
-    private final BroadcastReceiver mReceiver;
-    private final IntentFilter mFilter;
-
-    public NfcReceiver(String address, Activity activity, final Callback callback) {
-        this(address, activity, new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
-                    processIntent(callback, intent);
-                }
-            }
-        });
-        DeviceShadowEnvironment.addDevice(address);
-    }
-
-    public NfcReceiver(
-            final String address, Activity activity, final BroadcastReceiver clientReceiver) {
-        this.mAddress = address;
-        this.mActivity = activity;
-
-        this.mFilter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
-        this.mReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                Log.v(TAG, "Receive broadcast on device " + address);
-                clientReceiver.onReceive(context, intent);
-                mReceiveLatch.countDown();
-            }
-        };
-        DeviceShadowEnvironment.addDevice(address);
-    }
-
-    public void startReceive() throws InterruptedException, ExecutionException {
-        mReceiveLatch = new CountDownLatch(1);
-
-        DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mActivity.getApplication().registerReceiver(mReceiver, mFilter);
-            }
-        }).get();
-    }
-
-    public void waitUntilReceive(long timeoutMillis) throws InterruptedException {
-        mReceiveLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
-    }
-
-    public void stopReceive() throws InterruptedException, ExecutionException {
-        DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                mActivity.getApplication().unregisterReceiver(mReceiver);
-            }
-        }).get();
-    }
-
-    static void processIntent(Callback callback, Intent intent) {
-        Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
-        if (rawMsgs != null && rawMsgs.length > 0) {
-            // only one message sent during the beam
-            NdefMessage msg = (NdefMessage) rawMsgs[0];
-            if (callback != null) {
-                callback.onReceive(new String(msg.getRecords()[0].getPayload()));
-            }
-        }
-    }
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcSender.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcSender.java
deleted file mode 100644
index dbbb5fa..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcSender.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.helpers.nfc;
-
-import android.app.Activity;
-import android.nfc.NdefMessage;
-import android.nfc.NdefRecord;
-import android.nfc.NfcAdapter;
-import android.nfc.NfcAdapter.CreateNdefMessageCallback;
-import android.nfc.NfcAdapter.OnNdefPushCompleteCallback;
-import android.nfc.NfcEvent;
-
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
-
-import java.util.concurrent.ExecutionException;
-
-/**
- * Helper class to send NFC events.
- */
-public class NfcSender {
-
-    private static final String NFC_PACKAGE = "DS_PKG";
-    private static final String NFC_TAG = "DS_TAG";
-
-    /**
-     * Callback to update sender status.
-     */
-    public interface Callback {
-
-        void onSend(String message);
-    }
-
-    private final String mAddress;
-    private final Activity mActivity;
-    private final Callback mCallback;
-    private final SenderCallback mSenderCallback;
-    private String mSessage;
-
-    public NfcSender(String address, Activity activity, Callback callback) {
-        this.mCallback = callback;
-        this.mAddress = address;
-        this.mActivity = activity;
-        DeviceShadowEnvironment.addDevice(address);
-        this.mSenderCallback = new SenderCallback();
-    }
-
-    public void startSend(String message) throws InterruptedException, ExecutionException {
-        this.mSessage = message;
-        DeviceShadowEnvironment.run(mAddress, new Runnable() {
-            @Override
-            public void run() {
-                NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(mActivity);
-                nfcAdapter.setNdefPushMessageCallback(mSenderCallback, mActivity);
-                nfcAdapter.setOnNdefPushCompleteCallback(mSenderCallback, mActivity);
-            }
-        }).get();
-    }
-
-    class SenderCallback implements CreateNdefMessageCallback, OnNdefPushCompleteCallback {
-
-        @Override
-        public NdefMessage createNdefMessage(NfcEvent event) {
-            NdefMessage msg = new NdefMessage(new NdefRecord[]{
-                    NdefRecord.createExternal(NFC_PACKAGE, NFC_TAG, mSessage.getBytes())
-            });
-            return msg;
-        }
-
-        @Override
-        public void onNdefPushComplete(NfcEvent event) {
-            mCallback.onSend(mSessage);
-        }
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/utils/IOUtils.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/utils/IOUtils.java
deleted file mode 100644
index d89754b..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/utils/IOUtils.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.helpers.utils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-
-/**
- * Utils for IO methods.
- */
-public class IOUtils {
-
-    /**
-     * Write num of bytes to be sent and payload through OutputStream.
-     */
-    public static void write(OutputStream os, byte[] data) throws IOException {
-        ByteBuffer buffer = ByteBuffer.allocate(4 + data.length).putInt(data.length).put(data);
-        os.write(buffer.array());
-    }
-
-    /**
-     * Read num of bytes to be read, and payload through InputStream.
-     *
-     * @return payload received.
-     */
-    public static byte[] read(InputStream is) throws IOException {
-        byte[] size = new byte[4];
-        is.read(size, 0, 4 /* bytes of int type */);
-
-        byte[] data = new byte[ByteBuffer.wrap(size).getInt()];
-        is.read(data);
-        return data;
-    }
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowEnvironmentImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowEnvironmentImpl.java
deleted file mode 100644
index 6a06ce4..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowEnvironmentImpl.java
+++ /dev/null
@@ -1,353 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal;
-
-import android.content.ContentProvider;
-import android.os.Looper;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.Enums.Distance;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
-import com.android.libraries.testing.deviceshadower.internal.common.NamedRunnable;
-import com.android.libraries.testing.deviceshadower.internal.common.Scheduler;
-import com.android.libraries.testing.deviceshadower.internal.nfc.NfcletImpl;
-import com.android.libraries.testing.deviceshadower.internal.sms.SmsContentProvider;
-import com.android.libraries.testing.deviceshadower.internal.sms.SmsletImpl;
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import com.google.common.collect.ImmutableList;
-
-import org.robolectric.Shadows;
-import org.robolectric.shadows.ShadowLooper;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Proxy to manage internal data models, and help shadows to exchange data.
- */
-public class DeviceShadowEnvironmentImpl {
-
-    private static final Logger LOGGER = Logger.create("DeviceShadowEnvironmentImpl");
-    private static final long SCHEDULER_WAIT_TIMEOUT_MILLIS = 5000L;
-
-    // ThreadLocal to store local address for each device.
-    private static InheritableThreadLocal<DeviceletImpl> sLocalDeviceletImpl =
-            new InheritableThreadLocal<>();
-
-    // Devicelets contains all registered devicelet to simulate a device.
-    private static final Map<String, DeviceletImpl> DEVICELETS = new ConcurrentHashMap<>();
-
-    @VisibleForTesting
-    static final Map<String, ExecutorService> EXECUTORS = new ConcurrentHashMap<>();
-
-    private static final List<DeviceShadowException> INTERNAL_EXCEPTIONS =
-            Collections.synchronizedList(new ArrayList<DeviceShadowException>());
-
-    private static final ContentProvider smsContentProvider = new SmsContentProvider();
-
-    public static DeviceletImpl getDeviceletImpl(String address) {
-        return DEVICELETS.get(address);
-    }
-
-    public static void checkInternalExceptions() {
-        if (INTERNAL_EXCEPTIONS.size() > 0) {
-            for (DeviceShadowException exception : INTERNAL_EXCEPTIONS) {
-                LOGGER.e("Internal exception", exception);
-            }
-            INTERNAL_EXCEPTIONS.clear();
-            throw new RuntimeException("DeviceShadower has internal exceptions");
-        }
-    }
-
-    public static void reset() {
-        // reset local devicelet for single device testing
-        sLocalDeviceletImpl.remove();
-        DEVICELETS.clear();
-        BlueletImpl.reset();
-        INTERNAL_EXCEPTIONS.clear();
-    }
-
-    public static boolean await(long timeoutMillis) {
-        boolean schedulerDone = false;
-        try {
-            schedulerDone = Scheduler.await(timeoutMillis);
-        } catch (InterruptedException e) {
-            // no-op.
-        } finally {
-            if (!schedulerDone) {
-                catchInternalException(new DeviceShadowException("Scheduler not complete"));
-                for (DeviceletImpl devicelet : DEVICELETS.values()) {
-                    LOGGER.e(
-                            String.format(
-                                    "Device %s\n\tUI: %s\n\tService: %s",
-                                    devicelet.getAddress(),
-                                    devicelet.getUiScheduler(),
-                                    devicelet.getServiceScheduler()));
-                }
-                Scheduler.clear();
-            }
-        }
-        for (ExecutorService executor : EXECUTORS.values()) {
-            executor.shutdownNow();
-        }
-        boolean terminateSuccess = true;
-        for (ExecutorService executor : EXECUTORS.values()) {
-            try {
-                executor.awaitTermination(timeoutMillis, TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {
-                terminateSuccess = false;
-            }
-            if (!executor.isTerminated()) {
-                LOGGER.e("Failed to terminate executor.");
-                terminateSuccess = false;
-            }
-        }
-        EXECUTORS.clear();
-        return schedulerDone && terminateSuccess;
-    }
-
-    public static boolean hasLocalDeviceletImpl() {
-        return sLocalDeviceletImpl.get() != null;
-    }
-
-    public static DeviceletImpl getLocalDeviceletImpl() {
-        return sLocalDeviceletImpl.get();
-    }
-
-    public static List<DeviceletImpl> getDeviceletImpls() {
-        return ImmutableList.copyOf(DEVICELETS.values());
-    }
-
-    public static BlueletImpl getLocalBlueletImpl() {
-        return sLocalDeviceletImpl.get().blueletImpl();
-    }
-
-    public static BlueletImpl getBlueletImpl(String address) {
-        DeviceletImpl devicelet = getDeviceletImpl(address);
-        return devicelet == null ? null : devicelet.blueletImpl();
-    }
-
-    public static NfcletImpl getLocalNfcletImpl() {
-        return sLocalDeviceletImpl.get().nfcletImpl();
-    }
-
-    public static NfcletImpl getNfcletImpl(String address) {
-        DeviceletImpl devicelet = getDeviceletImpl(address);
-        return devicelet == null ? null : devicelet.nfcletImpl();
-    }
-
-    public static SmsletImpl getLocalSmsletImpl() {
-        return sLocalDeviceletImpl.get().smsletImpl();
-    }
-
-    public static ContentProvider getSmsContentProvider() {
-        return smsContentProvider;
-    }
-
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public static DeviceletImpl addDevice(String address) {
-        EXECUTORS.put(address, Executors.newCachedThreadPool());
-
-        // DeviceShadower keeps track of the "local" device based on the current thread. It uses an
-        // InheritableThreadLocal, so threads created by the current thread also get the same
-        // thread-local value. Add the device on its own thread, to set the thread local for that
-        // thread and its children.
-        try {
-            EXECUTORS
-                    .get(address)
-                    .submit(
-                            () -> {
-                                DeviceletImpl devicelet = new DeviceletImpl(address);
-                                DEVICELETS.put(address, devicelet);
-                                setLocalDevice(address);
-                                // Ensure these threads are actually created, by posting one empty
-                                // runnable.
-                                devicelet.getServiceScheduler()
-                                        .post(NamedRunnable.create("Init", () -> {
-                                        }));
-                                devicelet.getUiScheduler().post(NamedRunnable.create("Init", () -> {
-                                }));
-                            })
-                    .get();
-        } catch (InterruptedException | ExecutionException e) {
-            throw new IllegalStateException(e);
-        }
-
-        return DEVICELETS.get(address);
-    }
-
-    public static void removeDevice(String address) {
-        DEVICELETS.remove(address);
-        EXECUTORS.remove(address);
-    }
-
-    public static void setInterruptibleBluetooth(int identifier) {
-        getLocalBlueletImpl().setInterruptible(identifier);
-    }
-
-    public static void interruptBluetooth(String address, int identifier) {
-        getBlueletImpl(address).interrupt(identifier);
-    }
-
-    public static void setDistance(String address1, String address2, final Distance distance) {
-        final DeviceletImpl device1 = getDeviceletImpl(address1);
-        final DeviceletImpl device2 = getDeviceletImpl(address2);
-
-        Future<Void> result1 = null;
-        Future<Void> result2 = null;
-        if (device1.updateDistance(address2, distance)) {
-            result1 =
-                    run(
-                            address1,
-                            () -> {
-                                device1.onDistanceChange(device2, distance);
-                                return null;
-                            });
-        }
-
-        if (device2.updateDistance(address1, distance)) {
-            result2 =
-                    run(
-                            address2,
-                            () -> {
-                                device2.onDistanceChange(device1, distance);
-                                return null;
-                            });
-        }
-
-        try {
-            if (result1 != null) {
-                result1.get();
-            }
-            if (result2 != null) {
-                result2.get();
-            }
-        } catch (InterruptedException | ExecutionException e) {
-            catchInternalException(new DeviceShadowException(e));
-        }
-    }
-
-    /**
-     * Set local Bluelet for current thread.
-     *
-     * <p>This can be used to convert current running thread to hold a bluelet object, so that unit
-     * test does not have to call BluetoothEnvironment.run() to run code.
-     */
-    @VisibleForTesting
-    public static void setLocalDevice(String address) {
-        DeviceletImpl local = DEVICELETS.get(address);
-        if (local == null) {
-            throw new RuntimeException(address + " is not initialized by BluetoothEnvironment");
-        }
-        sLocalDeviceletImpl.set(local);
-    }
-
-    public static <T> Future<T> run(final String address, final Callable<T> snippet) {
-        return EXECUTORS
-                .get(address)
-                .submit(
-                        () -> {
-                            DeviceShadowEnvironmentImpl.setLocalDevice(address);
-                            ShadowLooper mainLooper = Shadows.shadowOf(Looper.getMainLooper());
-                            try {
-                                T result = snippet.call();
-
-                                // Avoid idling the main looper in paused mode since doing so is
-                                // only allowed from the main thread.
-                                if (!mainLooper.isPaused()) {
-                                    // In Robolectric, runnable doesn't run when posting thread
-                                    // differs from looper thread, idle main looper explicitly to
-                                    // execute posted Runnables.
-                                    ShadowLooper.idleMainLooper();
-                                }
-
-                                // Wait all scheduled runnables complete.
-                                Scheduler.await(SCHEDULER_WAIT_TIMEOUT_MILLIS);
-                                return result;
-                            } catch (Exception e) {
-                                LOGGER.e("Fail to call code on device: " + address, e);
-                                if (!mainLooper.isPaused()) {
-                                    // reset() is not supported in paused mode.
-                                    mainLooper.reset();
-                                }
-                                throw new RuntimeException(e);
-                            }
-                        });
-    }
-
-    // @CanIgnoreReturnValue
-    // Return value can be ignored because {@link Scheduler} will call
-    // {@link catchInternalException} to catch exceptions, and throw when test completes.
-    public static Future<?> runOnUi(String address, NamedRunnable snippet) {
-        Scheduler scheduler = DeviceShadowEnvironmentImpl.getDeviceletImpl(address)
-                .getUiScheduler();
-        return run(scheduler, address, snippet);
-    }
-
-    // @CanIgnoreReturnValue
-    // Return value can be ignored because {@link Scheduler} will call
-    // {@link catchInternalException} to catch exceptions, and throw when test completes.
-    public static Future<?> runOnService(String address, NamedRunnable snippet) {
-        Scheduler scheduler =
-                DeviceShadowEnvironmentImpl.getDeviceletImpl(address).getServiceScheduler();
-        return run(scheduler, address, snippet);
-    }
-
-    // @CanIgnoreReturnValue
-    // Return value can be ignored because {@link Scheduler} will call
-    // {@link catchInternalException} to catch exceptions, and throw when test completes.
-    private static Future<?> run(
-            Scheduler scheduler, final String address, final NamedRunnable snippet) {
-        return scheduler.post(
-                NamedRunnable.create(
-                        snippet.toString(),
-                        () -> {
-                            DeviceShadowEnvironmentImpl.setLocalDevice(address);
-                            snippet.run();
-                        }));
-    }
-
-    public static void catchInternalException(Exception exception) {
-        INTERNAL_EXCEPTIONS.add(new DeviceShadowException(exception));
-    }
-
-    // This is used to test Device Shadower internal.
-    @VisibleForTesting
-    public static void setDeviceletForTest(String address, DeviceletImpl devicelet) {
-        DEVICELETS.put(address, devicelet);
-    }
-
-    @VisibleForTesting
-    public static void setExecutorForTest(String address) {
-        setExecutorForTest(address, Executors.newCachedThreadPool());
-    }
-
-    @VisibleForTesting
-    public static void setExecutorForTest(String address, ExecutorService executor) {
-        EXECUTORS.put(address, executor);
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowException.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowException.java
deleted file mode 100644
index 77d358f..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowException.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal;
-
-/**
- * Internal exception to indicate error from DeviceShadower framework.
- */
-public class DeviceShadowException extends Exception {
-
-    public DeviceShadowException(Throwable e) {
-        super(e);
-    }
-
-    public DeviceShadowException(String msg) {
-        super(msg);
-    }
-
-    public DeviceShadowException(String msg, Throwable e) {
-        super(msg, e);
-    }
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceletImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceletImpl.java
deleted file mode 100644
index 9aea065..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceletImpl.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal;
-
-import com.android.libraries.testing.deviceshadower.Bluelet;
-import com.android.libraries.testing.deviceshadower.Devicelet;
-import com.android.libraries.testing.deviceshadower.Enums.Distance;
-import com.android.libraries.testing.deviceshadower.Nfclet;
-import com.android.libraries.testing.deviceshadower.Smslet;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
-import com.android.libraries.testing.deviceshadower.internal.common.BroadcastManager;
-import com.android.libraries.testing.deviceshadower.internal.common.Scheduler;
-import com.android.libraries.testing.deviceshadower.internal.nfc.NfcletImpl;
-import com.android.libraries.testing.deviceshadower.internal.sms.SmsletImpl;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * DeviceletImpl is the implementation to hold different medium-let in DeviceShadowEnvironment.
- */
-public class DeviceletImpl implements Devicelet {
-
-    private final BlueletImpl mBluelet;
-    private final NfcletImpl mNfclet;
-    private final SmsletImpl mSmslet;
-    private final BroadcastManager mBroadcastManager;
-    private final String mAddress;
-    private final Map<String, Distance> mDistanceMap = new HashMap<>();
-    private final Scheduler mServiceScheduler;
-    private final Scheduler mUiScheduler;
-
-    public DeviceletImpl(String address) {
-        this.mAddress = address;
-        this.mServiceScheduler = new Scheduler(address + "-service");
-        this.mUiScheduler = new Scheduler(address + "-main");
-        this.mBroadcastManager = new BroadcastManager(mUiScheduler);
-        this.mBluelet = new BlueletImpl(address, mBroadcastManager);
-        this.mNfclet = new NfcletImpl();
-        this.mSmslet = new SmsletImpl();
-    }
-
-    @Override
-    public Bluelet bluetooth() {
-        return mBluelet;
-    }
-
-    public BlueletImpl blueletImpl() {
-        return mBluelet;
-    }
-
-    @Override
-    public Nfclet nfc() {
-        return mNfclet;
-    }
-
-    public NfcletImpl nfcletImpl() {
-        return mNfclet;
-    }
-
-    @Override
-    public Smslet sms() {
-        return mSmslet;
-    }
-
-    public SmsletImpl smsletImpl() {
-        return mSmslet;
-    }
-
-    public BroadcastManager getBroadcastManager() {
-        return mBroadcastManager;
-    }
-
-    @Override
-    public String getAddress() {
-        return mAddress;
-    }
-
-    Scheduler getServiceScheduler() {
-        return mServiceScheduler;
-    }
-
-    Scheduler getUiScheduler() {
-        return mUiScheduler;
-    }
-
-    /**
-     * Update distance to remote device.
-     *
-     * @return true if distance updated.
-     */
-    /*package*/ boolean updateDistance(String remoteAddress, Distance distance) {
-        Distance currentDistance = mDistanceMap.get(remoteAddress);
-        if (currentDistance == null || !distance.equals(currentDistance)) {
-            mDistanceMap.put(remoteAddress, distance);
-            return true;
-        }
-        return false;
-    }
-
-    /*package*/ void onDistanceChange(DeviceletImpl remote, Distance distance) {
-        if (distance == Distance.NEAR) {
-            mNfclet.onNear(remote.mNfclet);
-        }
-    }
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/AdapterDelegate.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/AdapterDelegate.java
deleted file mode 100644
index b5227b7..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/AdapterDelegate.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothClass.Device;
-import android.os.Build.VERSION;
-
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.DeviceletImpl;
-import com.android.libraries.testing.deviceshadower.internal.common.NamedRunnable;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import javax.annotation.concurrent.GuardedBy;
-
-/**
- * Class handling Bluetooth Adapter State change. Currently async event processing is not supported,
- * and there is no deferred operation when adapter is in a pending state.
- */
-class AdapterDelegate {
-
-    /**
-     * Callback for adapter
-     */
-    public interface Callback {
-
-        void onAdapterStateChange(State prevState, State newState);
-
-        void onBleStateChange(State prevState, State newState);
-
-        void onDiscoveryStarted();
-
-        void onDiscoveryFinished();
-
-        void onDeviceFound(String address, int bluetoothClass, String name);
-    }
-
-    @GuardedBy("this")
-    private State mCurrentState;
-
-    private final String mAddress;
-    private final Callback mCallback;
-    private AtomicBoolean mIsDiscovering = new AtomicBoolean(false);
-    private final AtomicInteger mScanMode = new AtomicInteger(BluetoothAdapter.SCAN_MODE_NONE);
-    private int mBluetoothClass = Device.PHONE_SMART;
-
-    AdapterDelegate(String address, Callback callback) {
-        this.mAddress = address;
-        this.mCurrentState = State.OFF;
-        this.mCallback = callback;
-    }
-
-    synchronized void processEvent(Event event) {
-        State newState = TRANSITION[mCurrentState.ordinal()][event.ordinal()];
-        if (newState == null) {
-            return;
-        }
-        State prevState = mCurrentState;
-        mCurrentState = newState;
-        handleStateChange(prevState, newState);
-    }
-
-    private void handleStateChange(State prevState, State newState) {
-        // TODO(b/200231384): fake service bind/unbind on state change
-        if (prevState.equals(newState)) {
-            return;
-        }
-        if (VERSION.SDK_INT < 23) {
-            mCallback.onAdapterStateChange(prevState, newState);
-        } else {
-            mCallback.onBleStateChange(prevState, newState);
-            if (newState.equals(State.BLE_TURNING_ON)
-                    || newState.equals(State.BLE_TURNING_OFF)
-                    || newState.equals(State.OFF)
-                    || (newState.equals(State.BLE_ON) && prevState.equals(State.BLE_TURNING_ON))) {
-                return;
-            }
-            if (newState.equals(State.BLE_ON)) {
-                newState = State.OFF;
-            } else if (prevState.equals(State.BLE_ON)) {
-                prevState = State.OFF;
-            }
-            mCallback.onAdapterStateChange(prevState, newState);
-        }
-    }
-
-    synchronized State getState() {
-        return mCurrentState;
-    }
-
-    synchronized void setState(State state) {
-        mCurrentState = state;
-    }
-
-    void setBluetoothClass(int bluetoothClass) {
-        this.mBluetoothClass = bluetoothClass;
-    }
-
-    int getBluetoothClass() {
-        return mBluetoothClass;
-    }
-
-    @SuppressWarnings("FutureReturnValueIgnored")
-    void startDiscovery() {
-        synchronized (this) {
-            if (mIsDiscovering.get()) {
-                return;
-            }
-            mIsDiscovering.set(true);
-        }
-
-        mCallback.onDiscoveryStarted();
-
-        NamedRunnable onDeviceFound =
-                NamedRunnable.create(
-                        "BluetoothAdapter.onDeviceFound",
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                List<DeviceletImpl> devices =
-                                        DeviceShadowEnvironmentImpl.getDeviceletImpls();
-                                for (DeviceletImpl devicelet : devices) {
-                                    BlueletImpl bluelet = devicelet.blueletImpl();
-                                    if (mAddress.equals(devicelet.getAddress())
-                                            || bluelet.getAdapterDelegate().mScanMode.get()
-                                                    != BluetoothAdapter
-                                            .SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
-                                        continue;
-                                    }
-                                    mCallback.onDeviceFound(
-                                            bluelet.address,
-                                            bluelet.getAdapterDelegate().mBluetoothClass,
-                                            bluelet.mName);
-                                }
-                                finishDiscovery();
-                            }
-                        });
-
-        DeviceShadowEnvironmentImpl.runOnUi(mAddress, onDeviceFound);
-    }
-
-    void cancelDiscovery() {
-        finishDiscovery();
-    }
-
-    boolean isDiscovering() {
-        return mIsDiscovering.get();
-    }
-
-    void setScanMode(int scanMode) {
-        // TODO(b/200231384): broadcast scan mode change.
-        this.mScanMode.set(scanMode);
-    }
-
-    int getScanMode() {
-        return mScanMode.get();
-    }
-
-    private void finishDiscovery() {
-        synchronized (this) {
-            if (!mIsDiscovering.get()) {
-                return;
-            }
-            mIsDiscovering.set(false);
-        }
-        mCallback.onDiscoveryFinished();
-    }
-
-    enum State {
-        OFF(BluetoothAdapter.STATE_OFF),
-        TURNING_ON(BluetoothAdapter.STATE_TURNING_ON),
-        ON(BluetoothAdapter.STATE_ON),
-        TURNING_OFF(BluetoothAdapter.STATE_TURNING_OFF),
-        // States for API23+
-        BLE_TURNING_ON(BluetoothConstants.STATE_BLE_TURNING_ON),
-        BLE_ON(BluetoothConstants.STATE_BLE_ON),
-        BLE_TURNING_OFF(BluetoothConstants.STATE_BLE_TURNING_OFF);
-
-        private static final Map<Integer, State> LOOKUP = new HashMap<>();
-
-        static {
-            for (State state : State.values()) {
-                LOOKUP.put(state.getValue(), state);
-            }
-        }
-
-        static State lookup(int value) {
-            return LOOKUP.get(value);
-        }
-
-        private final int mValue;
-
-        State(int value) {
-            this.mValue = value;
-        }
-
-        int getValue() {
-            return mValue;
-        }
-    }
-
-    /*
-     * Represents Bluetooth events which can trigger adapter state change.
-     */
-    enum Event {
-        USER_TURN_ON,
-        USER_TURN_OFF,
-        BREDR_STARTED,
-        BREDR_STOPPED,
-        // Events for API23+
-        BLE_TURN_ON,
-        BLE_TURN_OFF,
-        BLE_STARTED,
-        BLE_STOPPED
-    }
-
-    private static final State[][] TRANSITION =
-            new State[State.values().length][Event.values().length];
-
-    static {
-        if (VERSION.SDK_INT < 23) {
-            // transition table before API23
-            TRANSITION[State.OFF.ordinal()][Event.USER_TURN_ON.ordinal()] = State.TURNING_ON;
-            TRANSITION[State.TURNING_ON.ordinal()][Event.BREDR_STARTED.ordinal()] = State.ON;
-            TRANSITION[State.ON.ordinal()][Event.USER_TURN_OFF.ordinal()] = State.TURNING_OFF;
-            TRANSITION[State.TURNING_OFF.ordinal()][Event.BREDR_STOPPED.ordinal()] = State.OFF;
-        } else {
-            // transition table starting from API23
-            TRANSITION[State.OFF.ordinal()][Event.BLE_TURN_ON.ordinal()] = State.BLE_TURNING_ON;
-            TRANSITION[State.BLE_TURNING_ON.ordinal()][Event.BLE_STARTED.ordinal()] = State.BLE_ON;
-            TRANSITION[State.BLE_ON.ordinal()][Event.USER_TURN_ON.ordinal()] = State.TURNING_ON;
-            TRANSITION[State.TURNING_ON.ordinal()][Event.BREDR_STARTED.ordinal()] = State.ON;
-            TRANSITION[State.ON.ordinal()][Event.BLE_TURN_OFF.ordinal()] = State.TURNING_OFF;
-            TRANSITION[State.TURNING_OFF.ordinal()][Event.BREDR_STOPPED.ordinal()] = State.BLE_ON;
-            TRANSITION[State.BLE_ON.ordinal()][Event.USER_TURN_OFF.ordinal()] =
-                    State.BLE_TURNING_OFF;
-            TRANSITION[State.BLE_TURNING_OFF.ordinal()][Event.BLE_STOPPED.ordinal()] = State.OFF;
-        }
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BlueletImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BlueletImpl.java
deleted file mode 100644
index 4e534e3..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BlueletImpl.java
+++ /dev/null
@@ -1,495 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth;
-
-import static org.robolectric.util.ReflectionHelpers.callConstructor;
-
-import android.Manifest.permission;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothClass;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.IBluetoothManager;
-import android.content.AttributionSource;
-import android.content.Intent;
-import android.os.Build.VERSION;
-import android.os.ParcelUuid;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.Bluelet;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.Event;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.State;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.RfcommDelegate;
-import com.android.libraries.testing.deviceshadower.internal.common.BroadcastManager;
-import com.android.libraries.testing.deviceshadower.internal.common.Interrupter;
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableMap;
-
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A container class of a real-world Bluetooth device.
- */
-public class BlueletImpl implements Bluelet {
-
-    enum PairingConfirmation {
-        UNKNOWN,
-        CONFIRMED,
-        DENIED
-    }
-
-    /**
-     * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}.
-     */
-    static final int REASON_SUCCESS = 0;
-    /**
-     * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}.
-     */
-    static final int UNBOND_REASON_AUTH_FAILED = 1;
-    /**
-     * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}.
-     */
-    static final int UNBOND_REASON_AUTH_CANCELED = 3;
-
-    /**
-     * Hidden in {@link BluetoothDevice}.
-     */
-    private static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON";
-
-    private static final Logger LOGGER = Logger.create("BlueletImpl");
-
-    private static final ImmutableMap<Integer, Integer> PROFILE_STATE_TO_ADAPTER_STATE =
-            ImmutableMap.<Integer, Integer>builder()
-                    .put(BluetoothProfile.STATE_CONNECTED, BluetoothAdapter.STATE_CONNECTED)
-                    .put(BluetoothProfile.STATE_CONNECTING, BluetoothAdapter.STATE_CONNECTING)
-                    .put(BluetoothProfile.STATE_DISCONNECTING, BluetoothAdapter.STATE_DISCONNECTING)
-                    .put(BluetoothProfile.STATE_DISCONNECTED, BluetoothAdapter.STATE_DISCONNECTED)
-                    .build();
-
-    public static void reset() {
-        RfcommDelegate.reset();
-    }
-
-    public final String address;
-    String mName;
-    ParcelUuid[] mProfileUuids = new ParcelUuid[0];
-    int mPhonebookAccessPermission;
-    int mMessageAccessPermission;
-    int mSimAccessPermission;
-    final BluetoothAdapter mAdapter;
-    int mPassKey;
-
-    private CreateBondOutcome mCreateBondOutcome = CreateBondOutcome.SUCCESS;
-    private int mCreateBondFailureReason;
-    private IoCapabilities mIoCapabilities = IoCapabilities.NO_INPUT_NO_OUTPUT;
-    private boolean mRefuseConnections;
-    private FetchUuidsTiming mFetchUuidsTiming = FetchUuidsTiming.AFTER_BONDING;
-    private boolean mEnableCVE20192225;
-
-    private final Interrupter mInterrupter;
-    private final AdapterDelegate mAdapterDelegate;
-    private final RfcommDelegate mRfcommDelegate;
-    private final GattDelegate mGattDelegate;
-    private final BluetoothBroadcastHandler mBluetoothBroadcastHandler;
-    private final Map<String, Integer> mRemoteAddressToBondState = new HashMap<>();
-    private final Map<String, PairingConfirmation> mRemoteAddressToPairingConfirmation =
-            new HashMap<>();
-    private final Map<Integer, Integer> mProfileTypeToConnectionState = new HashMap<>();
-    private final Set<BluetoothDevice> mBondedDevices = new HashSet<>();
-
-    public BlueletImpl(String address, BroadcastManager broadcastManager) {
-        this.address = address;
-        this.mName = address;
-        this.mAdapter = callConstructor(BluetoothAdapter.class,
-                ClassParameter.from(IBluetoothManager.class, new IBluetoothManagerImpl()),
-                ClassParameter.from(AttributionSource.class,
-                        AttributionSource.myAttributionSource()));
-        mBluetoothBroadcastHandler = new BluetoothBroadcastHandler(broadcastManager);
-        mInterrupter = new Interrupter();
-        mAdapterDelegate = new AdapterDelegate(address, mBluetoothBroadcastHandler);
-        mRfcommDelegate = new RfcommDelegate(address, mBluetoothBroadcastHandler, mInterrupter);
-        mGattDelegate = new GattDelegate(address);
-    }
-
-    @Override
-    public Bluelet setAdapterInitialState(int state) throws IllegalArgumentException {
-        LOGGER.d(String.format("Address: %s, setAdapterInitialState(%d)", address, state));
-        Preconditions.checkArgument(
-                state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_ON,
-                "State must be BluetoothAdapter.STATE_ON or BluetoothAdapter.STATE_OFF.");
-        mAdapterDelegate.setState(State.lookup(state));
-        return this;
-    }
-
-    @Override
-    public Bluelet setBluetoothClass(int bluetoothClass) {
-        mAdapterDelegate.setBluetoothClass(bluetoothClass);
-        return this;
-    }
-
-    @Override
-    public Bluelet setScanMode(int scanMode) {
-        mAdapterDelegate.setScanMode(scanMode);
-        return this;
-    }
-
-    @Override
-    public Bluelet setProfileUuids(ParcelUuid... profileUuids) {
-        this.mProfileUuids = profileUuids;
-        return this;
-    }
-
-    @Override
-    public Bluelet setIoCapabilities(IoCapabilities ioCapabilities) {
-        this.mIoCapabilities = ioCapabilities;
-        return this;
-    }
-
-    @Override
-    public Bluelet setCreateBondOutcome(CreateBondOutcome outcome, int failureReason) {
-        mCreateBondOutcome = outcome;
-        mCreateBondFailureReason = failureReason;
-        return this;
-    }
-
-    @Override
-    public Bluelet setRefuseConnections(boolean refuse) {
-        mRefuseConnections = refuse;
-        return this;
-    }
-
-    @Override
-    public Bluelet setRefuseGattConnections(boolean refuse) {
-        getGattDelegate().setRefuseConnections(refuse);
-        return this;
-    }
-
-    @Override
-    public Bluelet setFetchUuidsTiming(FetchUuidsTiming fetchUuidsTiming) {
-        this.mFetchUuidsTiming = fetchUuidsTiming;
-        return this;
-    }
-
-    @Override
-    public Bluelet addBondedDevice(String address) {
-        this.mBondedDevices.add(mAdapter.getRemoteDevice(address));
-        return this;
-    }
-
-    @Override
-    public Bluelet enableCVE20192225(boolean value) {
-        this.mEnableCVE20192225 = value;
-        return this;
-    }
-
-    IoCapabilities getIoCapabilities() {
-        return mIoCapabilities;
-    }
-
-    CreateBondOutcome getCreateBondOutcome() {
-        return mCreateBondOutcome;
-    }
-
-    int getCreateBondFailureReason() {
-        return mCreateBondFailureReason;
-    }
-
-    public boolean getRefuseConnections() {
-        return mRefuseConnections;
-    }
-
-    public FetchUuidsTiming getFetchUuidsTiming() {
-        return mFetchUuidsTiming;
-    }
-
-    BluetoothDevice[] getBondedDevices() {
-        return mBondedDevices.toArray(new BluetoothDevice[0]);
-    }
-
-    public boolean getEnableCVE20192225() {
-        return mEnableCVE20192225;
-    }
-
-    public void enableAdapter() {
-        LOGGER.d(String.format("Address: %s, enableAdapter()", address));
-        // TODO(b/200231384): async enabling, configurable delay, failure path
-        if (VERSION.SDK_INT < 23) {
-            mAdapterDelegate.processEvent(Event.USER_TURN_ON);
-            mAdapterDelegate.processEvent(Event.BREDR_STARTED);
-        } else {
-            mAdapterDelegate.processEvent(Event.BLE_TURN_ON);
-            mAdapterDelegate.processEvent(Event.BLE_STARTED);
-            mAdapterDelegate.processEvent(Event.USER_TURN_ON);
-            mAdapterDelegate.processEvent(Event.BREDR_STARTED);
-        }
-    }
-
-    public void disableAdapter() {
-        LOGGER.d(String.format("Address: %s, disableAdapter()", address));
-        // TODO(b/200231384): async disabling, configurable delay, failure path
-        if (VERSION.SDK_INT < 23) {
-            mAdapterDelegate.processEvent(Event.USER_TURN_OFF);
-            mAdapterDelegate.processEvent(Event.BREDR_STOPPED);
-        } else {
-            mAdapterDelegate.processEvent(Event.BLE_TURN_OFF);
-            mAdapterDelegate.processEvent(Event.BREDR_STOPPED);
-            mAdapterDelegate.processEvent(Event.USER_TURN_OFF);
-            mAdapterDelegate.processEvent(Event.BLE_STOPPED);
-        }
-    }
-
-    public AdapterDelegate getAdapterDelegate() {
-        return mAdapterDelegate;
-    }
-
-    public RfcommDelegate getRfcommDelegate() {
-        return mRfcommDelegate;
-    }
-
-    public GattDelegate getGattDelegate() {
-        return mGattDelegate;
-    }
-
-    public BluetoothAdapter getAdapter() {
-        return mAdapter;
-    }
-
-    public void setInterruptible(int identifier) {
-        LOGGER.d(String.format("Address: %s, setInterruptible(%d)", address, identifier));
-        mInterrupter.setInterruptible(identifier);
-    }
-
-    public void interrupt(int identifier) {
-        LOGGER.d(String.format("Address: %s, interrupt(%d)", address, identifier));
-        mInterrupter.interrupt(identifier);
-    }
-
-    @VisibleForTesting
-    public void setAdapterState(int state) throws IllegalArgumentException {
-        State s = State.lookup(state);
-        if (s == null) {
-            throw new IllegalArgumentException();
-        }
-        mAdapterDelegate.setState(s);
-    }
-
-    public int getBondState(String remoteAddress) {
-        return mRemoteAddressToBondState.containsKey(remoteAddress)
-                ? mRemoteAddressToBondState.get(remoteAddress)
-                : BluetoothDevice.BOND_NONE;
-    }
-
-    public void setBondState(String remoteAddress, int bondState, int failureReason) {
-        Intent intent =
-                newDeviceIntent(BluetoothDevice.ACTION_BOND_STATE_CHANGED, remoteAddress)
-                        .putExtra(BluetoothDevice.EXTRA_BOND_STATE, bondState)
-                        .putExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE,
-                                getBondState(remoteAddress));
-
-        if (failureReason != REASON_SUCCESS) {
-            intent.putExtra(EXTRA_REASON, failureReason);
-        }
-
-        LOGGER.d(
-                String.format(
-                        "Address: %s, Bluetooth Bond State Change Intent: remote=%s, %s -> %s "
-                                + "(reason=%s)",
-                        address, remoteAddress, getBondState(remoteAddress), bondState,
-                        failureReason));
-        mRemoteAddressToBondState.put(remoteAddress, bondState);
-        mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(
-                intent, android.Manifest.permission.BLUETOOTH);
-    }
-
-    public void onPairingRequest(String remoteAddress, int variant, int key) {
-        Intent intent =
-                newDeviceIntent(BluetoothDevice.ACTION_PAIRING_REQUEST, remoteAddress)
-                        .putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, variant)
-                        .putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, key);
-
-        LOGGER.d(
-                String.format(
-                        "Address: %s, Bluetooth Pairing Request Intent: remote=%s, variant=%s, "
-                                + "key=%s", address, remoteAddress, variant, key));
-        mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(intent, permission.BLUETOOTH);
-    }
-
-    public PairingConfirmation getPairingConfirmation(String remoteAddress) {
-        PairingConfirmation confirmation = mRemoteAddressToPairingConfirmation.get(remoteAddress);
-        return confirmation == null ? PairingConfirmation.UNKNOWN : confirmation;
-    }
-
-    public void setPairingConfirmation(String remoteAddress, PairingConfirmation confirmation) {
-        mRemoteAddressToPairingConfirmation.put(remoteAddress, confirmation);
-    }
-
-    public void onFetchedUuids(String remoteAddress, ParcelUuid[] profileUuids) {
-        Intent intent =
-                newDeviceIntent(BluetoothDevice.ACTION_UUID, remoteAddress)
-                        .putExtra(BluetoothDevice.EXTRA_UUID, profileUuids);
-
-        LOGGER.d(
-                String.format(
-                        "Address: %s, Bluetooth Found UUIDs Intent: remoteAddress=%s, uuids=%s",
-                        address, remoteAddress, Arrays.toString(profileUuids)));
-        mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(
-                intent, android.Manifest.permission.BLUETOOTH);
-    }
-
-    private static int maxProfileState(int a, int b) {
-        // Prefer connected > connecting > disconnecting > disconnected.
-        switch (a) {
-            case BluetoothProfile.STATE_CONNECTED:
-                return a;
-            case BluetoothProfile.STATE_CONNECTING:
-                return b == BluetoothProfile.STATE_CONNECTED ? b : a;
-            case BluetoothProfile.STATE_DISCONNECTING:
-                return b == BluetoothProfile.STATE_CONNECTED
-                        || b == BluetoothProfile.STATE_CONNECTING
-                        ? b
-                        : a;
-            case BluetoothProfile.STATE_DISCONNECTED:
-            default:
-                return b;
-        }
-    }
-
-    public int getAdapterConnectionState() {
-        int maxState = BluetoothProfile.STATE_DISCONNECTED;
-        for (int state : mProfileTypeToConnectionState.values()) {
-            maxState = maxProfileState(maxState, state);
-        }
-        return PROFILE_STATE_TO_ADAPTER_STATE.get(maxState);
-    }
-
-    public int getProfileConnectionState(int profileType) {
-        return mProfileTypeToConnectionState.containsKey(profileType)
-                ? mProfileTypeToConnectionState.get(profileType)
-                : BluetoothProfile.STATE_DISCONNECTED;
-    }
-
-    public void setProfileConnectionState(int profileType, int state, String remoteAddress) {
-        int previousAdapterState = getAdapterConnectionState();
-        mProfileTypeToConnectionState.put(profileType, state);
-        int adapterState = getAdapterConnectionState();
-        if (previousAdapterState != adapterState) {
-            Intent intent =
-                    newDeviceIntent(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED, remoteAddress)
-                            .putExtra(BluetoothAdapter.EXTRA_PREVIOUS_CONNECTION_STATE,
-                                    previousAdapterState)
-                            .putExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, adapterState);
-
-            LOGGER.d(
-                    "Adapter Connection State Changed Intent: "
-                            + previousAdapterState
-                            + " -> "
-                            + adapterState);
-            mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(
-                    intent, android.Manifest.permission.BLUETOOTH);
-        }
-    }
-
-    static class BluetoothBroadcastHandler implements AdapterDelegate.Callback,
-            RfcommDelegate.Callback {
-
-        private final BroadcastManager mBroadcastManager;
-
-        BluetoothBroadcastHandler(BroadcastManager broadcastManager) {
-            this.mBroadcastManager = broadcastManager;
-        }
-
-        @Override
-        public void onAdapterStateChange(State prevState, State newState) {
-            int prev = prevState.getValue();
-            int cur = newState.getValue();
-            LOGGER.d("Bluetooth State Change Intent: " + State.lookup(prev) + " -> " + State.lookup(
-                    cur));
-            Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED);
-            intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prev);
-            intent.putExtra(BluetoothAdapter.EXTRA_STATE, cur);
-            mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
-        }
-
-        @Override
-        public void onBleStateChange(State prevState, State newState) {
-            int prev = prevState.getValue();
-            int cur = newState.getValue();
-            LOGGER.d("BLE State Change Intent: " + State.lookup(prev) + " -> " + State.lookup(cur));
-            Intent intent = new Intent(BluetoothConstants.ACTION_BLE_STATE_CHANGED);
-            intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prev);
-            intent.putExtra(BluetoothAdapter.EXTRA_STATE, cur);
-            mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
-        }
-
-        @Override
-        public void onConnectionStateChange(String remoteAddress, boolean isConnected) {
-            LOGGER.d("Bluetooth Connection State Change Intent, isConnected: " + isConnected);
-            Intent intent =
-                    isConnected
-                            ? newDeviceIntent(BluetoothDevice.ACTION_ACL_CONNECTED, remoteAddress)
-                            : newDeviceIntent(BluetoothDevice.ACTION_ACL_DISCONNECTED,
-                                    remoteAddress);
-            mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
-        }
-
-        @Override
-        public void onDiscoveryStarted() {
-            LOGGER.d("Bluetooth discovery started.");
-            Intent intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
-            mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
-        }
-
-        @Override
-        public void onDiscoveryFinished() {
-            LOGGER.d("Bluetooth discovery finished.");
-            Intent intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
-            mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
-        }
-
-        @Override
-        public void onDeviceFound(String address, int bluetoothClass, String name) {
-            LOGGER.d("Bluetooth device found, address: " + address);
-            Intent intent =
-                    newDeviceIntent(BluetoothDevice.ACTION_FOUND, address)
-                            .putExtra(
-                                    BluetoothDevice.EXTRA_CLASS,
-                                    callConstructor(
-                                            BluetoothClass.class,
-                                            ClassParameter.from(int.class, bluetoothClass)))
-                            .putExtra(BluetoothDevice.EXTRA_NAME, name);
-            // TODO(b/200231384): support rssi
-            // TODO(b/200231384): send broadcast with additional ACCESS_COARSE_LOCATION permission
-            // once broadcast permission is implemented.
-            mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
-        }
-    }
-
-    private static Intent newDeviceIntent(String action, String address) {
-        return new Intent(action)
-                .putExtra(
-                        BluetoothDevice.EXTRA_DEVICE,
-                        BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address));
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BluetoothConstants.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BluetoothConstants.java
deleted file mode 100644
index fa519da..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BluetoothConstants.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth;
-
-/**
- * A class to hold Bluetooth constants.
- */
-public class BluetoothConstants {
-
-    /*** Bluetooth Adapter State ***/
-    // Must be identical to BluetoothAdapter hidden field STATE_BLE_TURNING_ON
-    public static final int STATE_BLE_TURNING_ON = 14;
-
-    // Must be identical to BluetoothAdapter hidden field STATE_BLE_ON
-    public static final int STATE_BLE_ON = 15;
-
-    // Must be identical to BluetoothAdapter hidden field STATE_BLE_TURNING_OFF
-    public static final int STATE_BLE_TURNING_OFF = 16;
-
-    // Must be identical to BluetoothAdapter hidden field ACTION_BLE_STATE_CHANGED
-    public static final String ACTION_BLE_STATE_CHANGED =
-            "android.bluetooth.adapter.action.BLE_STATE_CHANGED";
-
-    /*** Rfcomm Socket ***/
-    // Must be identical to BluetoothSocket field TYPE_RFCOMM.
-    // The field was package-private before M.
-    public static final int TYPE_RFCOMM = 1;
-
-    public static final int SOCKET_CLOSE = -10000;
-
-    // Android Bluetooth use -1 as port when creating server socket with uuid
-    public static final int SERVER_SOCKET_CHANNEL_AUTO_ASSIGN = -1;
-
-    // Android Bluetooth use -1 as port when creating socket with a uuid
-    public static final int SOCKET_CHANNEL_CONNECT_WITH_UUID = -1;
-
-    /*** BLE Advertise/Scan ***/
-    // Must be identical to AdvertiseCallback hidden field ADVERTISE_SUCCESS.
-    public static final int ADVERTISE_SUCCESS = 0;
-
-    // Must be identical to ScanRecord field DATA_TYPE_FLAGS.
-    public static final int DATA_TYPE_FLAGS = 0x01;
-
-    // Must be identical to ScanRecord field DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE.
-    public static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07;
-
-    // Must be identical to ScanRecord field DATA_TYPE_LOCAL_NAME_COMPLETE.
-    public static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09;
-
-    // Must be identical to ScanRecord field DATA_TYPE_TX_POWER_LEVEL.
-    public static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A;
-
-    // Must be identical to ScanRecord field DATA_TYPE_SERVICE_DATA.
-    public static final int DATA_TYPE_SERVICE_DATA = 0x16;
-
-    // Must be identical to ScanRecord field DATA_TYPE_MANUFACTURER_SPECIFIC_DATA.
-    public static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
-
-    /**
-     * @see #DATA_TYPE_FLAGS
-     */
-    public interface Flags {
-
-        byte LE_LIMITED_DISCOVERABLE_MODE = 1;
-        byte LE_GENERAL_DISCOVERABLE_MODE = 1 << 1;
-        byte BR_EDR_NOT_SUPPORTED = 1 << 2;
-        byte SIMULTANEOUS_LE_AND_BR_EDR_CONTROLLER = 1 << 3;
-        byte SIMULTANEOUS_LE_AND_BR_EDR_HOST = 1 << 4;
-    }
-
-    /**
-     * Observed that Android sets this for {@link #DATA_TYPE_FLAGS} when a packet is connectable (on
-     * a Nexus 6P running 7.1.2).
-     */
-    public static final byte FLAGS_IN_CONNECTABLE_PACKETS =
-            Flags.BR_EDR_NOT_SUPPORTED
-                    | Flags.LE_GENERAL_DISCOVERABLE_MODE
-                    | Flags.SIMULTANEOUS_LE_AND_BR_EDR_CONTROLLER
-                    | Flags.SIMULTANEOUS_LE_AND_BR_EDR_HOST;
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/GattDelegate.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/GattDelegate.java
deleted file mode 100644
index 4618561..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/GattDelegate.java
+++ /dev/null
@@ -1,609 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth;
-
-import android.annotation.Nullable;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.IBluetoothGattCallback;
-import android.bluetooth.IBluetoothGattServerCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanRecord;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.os.Build;
-import android.os.Build.VERSION;
-import android.os.ParcelUuid;
-import android.os.SystemClock;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.DeviceletImpl;
-import com.android.libraries.testing.deviceshadower.internal.utils.GattHelper;
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import com.google.common.base.Preconditions;
-import com.google.common.primitives.Bytes;
-
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Delegate to operate gatt operations.
- */
-public class GattDelegate {
-
-    private static final int DEFAULT_RSSI = -50;
-    private static final Logger LOGGER = Logger.create("GattDelegate");
-
-    // chipset properties
-    // use 2 as API 21 requires multi-advertisement support to use Le Advertising.
-    private final int mMaxAdvertiseInstances = 2;
-    private final AtomicBoolean mIsOffloadedFilteringSupported = new AtomicBoolean(false);
-    private final String mAddress;
-    private final AtomicInteger mCurrentClientIf = new AtomicInteger(0);
-    private final AtomicInteger mCurrentServerIf = new AtomicInteger(0);
-    private final AtomicBoolean mCurrentConnectionState = new AtomicBoolean(false);
-    private final Map<ParcelUuid, Service> mServices = new HashMap<>();
-    private final Map<Integer, IBluetoothGattCallback> mClientCallbacks;
-    private final Map<Integer, IBluetoothGattServerCallback> mServerCallbacks;
-    private final Map<Integer, Advertiser> mAdvertisers;
-    private final Map<Integer, Scanner> mScanners;
-    @Nullable
-    private Request mLastRequest;
-    private boolean mConnectable = true;
-
-    /**
-     * The parameters of a request, e.g. readCharacteristic(). Subclass for each request.
-     *
-     * @see #getLastRequest()
-     */
-    abstract static class Request {
-
-        final int mSrvcType;
-        final int mSrvcInstId;
-        final ParcelUuid mSrvcId;
-        final int mCharInstId;
-        final ParcelUuid mCharId;
-
-        Request(int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId,
-                ParcelUuid charId) {
-            this.mSrvcType = srvcType;
-            this.mSrvcInstId = srvcInstId;
-            this.mSrvcId = srvcId;
-            this.mCharInstId = charInstId;
-            this.mCharId = charId;
-        }
-    }
-
-    /**
-     * Corresponds to {@link android.bluetooth.IBluetoothGatt#readCharacteristic}.
-     */
-    static class ReadCharacteristicRequest extends Request {
-
-        ReadCharacteristicRequest(
-                int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId,
-                ParcelUuid charId) {
-            super(srvcType, srvcInstId, srvcId, charInstId, charId);
-        }
-    }
-
-    /**
-     * Corresponds to {@link android.bluetooth.IBluetoothGatt#readDescriptor}.
-     */
-    static class ReadDescriptorRequest extends Request {
-
-        final int mDescrInstId;
-        final ParcelUuid mDescrId;
-
-        ReadDescriptorRequest(
-                int srvcType,
-                int srvcInstId,
-                ParcelUuid srvcId,
-                int charInstId,
-                ParcelUuid charId,
-                int descrInstId,
-                ParcelUuid descrId) {
-            super(srvcType, srvcInstId, srvcId, charInstId, charId);
-            this.mDescrInstId = descrInstId;
-            this.mDescrId = descrId;
-        }
-    }
-
-    GattDelegate(String address) {
-        this(
-                address,
-                new HashMap<>(),
-                new HashMap<>(),
-                new ConcurrentHashMap<>(),
-                new ConcurrentHashMap<>());
-    }
-
-    @VisibleForTesting
-    GattDelegate(
-            String address,
-            Map<Integer, IBluetoothGattCallback> clientCallbacks,
-            Map<Integer, IBluetoothGattServerCallback> serverCallbacks,
-            Map<Integer, Advertiser> advertisers,
-            Map<Integer, Scanner> scanners) {
-        this.mAddress = address;
-        this.mClientCallbacks = clientCallbacks;
-        this.mServerCallbacks = serverCallbacks;
-        this.mAdvertisers = advertisers;
-        this.mScanners = scanners;
-    }
-
-    public void setRefuseConnections(boolean refuse) {
-        this.mConnectable = !refuse;
-    }
-
-    /**
-     * Used to maintain state between the request (e.g. readCharacteristic()) and sendResponse().
-     */
-    @Nullable
-    Request getLastRequest() {
-        return mLastRequest;
-    }
-
-    /**
-     * @see #getLastRequest()
-     */
-    void setLastRequest(@Nullable Request params) {
-        mLastRequest = params;
-    }
-
-    public int getClientIf() {
-        // TODO(b/200231384): support multiple client if.
-        return mCurrentClientIf.get();
-    }
-
-    public int getServerIf() {
-        // TODO(b/200231384): support multiple server if.
-        return mCurrentServerIf.get();
-    }
-
-    public IBluetoothGattServerCallback getServerCallback(int serverIf) {
-        return mServerCallbacks.get(serverIf);
-    }
-
-    public IBluetoothGattCallback getClientCallback(int clientIf) {
-        return mClientCallbacks.get(clientIf);
-    }
-
-    public int registerServer(IBluetoothGattServerCallback callback) {
-        mServerCallbacks.put(mCurrentServerIf.incrementAndGet(), callback);
-        return getServerIf();
-    }
-
-    public int registerClient(IBluetoothGattCallback callback) {
-        mClientCallbacks.put(mCurrentClientIf.incrementAndGet(), callback);
-        LOGGER.d(String.format("Client registered on %s, clientIf: %d", mAddress, getClientIf()));
-        return getClientIf();
-    }
-
-    public void unregisterClient(int clientIf) {
-        mClientCallbacks.remove(clientIf);
-        LOGGER.d(String.format("Client unregistered on %s, clientIf: %d", mAddress, clientIf));
-    }
-
-    public void unregisterServer(int serverIf) {
-        mServerCallbacks.remove(serverIf);
-    }
-
-    public int getMaxAdvertiseInstances() {
-        return mMaxAdvertiseInstances;
-    }
-
-    public boolean isOffloadedFilteringSupported() {
-        return mIsOffloadedFilteringSupported.get();
-    }
-
-    public boolean connect(String address) {
-        return mConnectable;
-    }
-
-    public boolean disconnect(String address) {
-        return true;
-    }
-
-    public void clientConnectionStateChange(
-            int state, int clientIf, boolean connected, String address) {
-        if (connected != mCurrentConnectionState.get()) {
-            mCurrentConnectionState.set(connected);
-            IBluetoothGattCallback callback = getClientCallback(clientIf);
-            if (callback != null) {
-                callback.onClientConnectionState(state, clientIf, connected, address);
-            }
-        }
-    }
-
-    public void serverConnectionStateChange(
-            int state, int serverIf, boolean connected, String address) {
-        if (connected != mCurrentConnectionState.get()) {
-            mCurrentConnectionState.set(connected);
-            IBluetoothGattServerCallback callback = getServerCallback(serverIf);
-            if (callback != null) {
-                callback.onServerConnectionState(state, serverIf, connected, address);
-            }
-        }
-    }
-
-    public Service addService(ParcelUuid uuid) {
-        Service srvc = new Service(uuid);
-        mServices.put(uuid, srvc);
-        return srvc;
-    }
-
-    public Collection<Service> getServices() {
-        return mServices.values();
-    }
-
-    public Service getService(ParcelUuid uuid) {
-        return mServices.get(uuid);
-    }
-
-    public void clientSetMtu(int clientIf, int mtu, String serverAddress) {
-        IBluetoothGattCallback callback = getClientCallback(clientIf);
-        if (callback != null && Build.VERSION.SDK_INT >= 21) {
-            callback.onConfigureMTU(serverAddress, mtu, BluetoothGatt.GATT_SUCCESS);
-        }
-    }
-
-    public void serverSetMtu(int serverIf, int mtu, String clientAddress) {
-        IBluetoothGattServerCallback callback = getServerCallback(serverIf);
-        if (callback != null && Build.VERSION.SDK_INT >= 22) {
-            callback.onMtuChanged(clientAddress, mtu);
-        }
-    }
-
-    public void startMultiAdvertising(
-            int appIf,
-            AdvertiseData advertiseData,
-            AdvertiseData scanResponse,
-            final AdvertiseSettings settings) {
-        LOGGER.d(String.format("startMultiAdvertising(%d) on %s", appIf, mAddress));
-        final Advertiser advertiser =
-                new Advertiser(
-                        appIf,
-                        mAddress,
-                        DeviceShadowEnvironmentImpl.getLocalBlueletImpl().mName,
-                        txPowerFromFlag(settings.getTxPowerLevel()),
-                        advertiseData,
-                        scanResponse,
-                        settings);
-        mAdvertisers.put(appIf, advertiser);
-        final IBluetoothGattCallback callback = mClientCallbacks.get(appIf);
-        @SuppressWarnings("unused") // go/futurereturn-lsc
-        Future<?> possiblyIgnoredError =
-                DeviceShadowEnvironmentImpl.run(
-                        mAddress,
-                        () -> {
-                            callback.onMultiAdvertiseCallback(
-                                    BluetoothConstants.ADVERTISE_SUCCESS, true /* isStart */,
-                                    settings);
-                            return null;
-                        });
-    }
-
-    /**
-     * Returns TxPower in dBm as measured at the source.
-     *
-     * <p>Note that this will vary by device and the values are only roughly accurate. The
-     * measurements were taken with a Nexus 6. Copied from the TxEddystone-UID app:
-     * {https://github.com/google/eddystone/blob/master/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/java/com/google/sample/txeddystone_uid/MainActivity.java}
-     */
-    private static byte txPowerFromFlag(int txPowerFlag) {
-        switch (txPowerFlag) {
-            case AdvertiseSettings.ADVERTISE_TX_POWER_HIGH:
-                return (byte) -16;
-            case AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM:
-                return (byte) -26;
-            case AdvertiseSettings.ADVERTISE_TX_POWER_LOW:
-                return (byte) -35;
-            case AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW:
-                return (byte) -59;
-            default:
-                throw new IllegalStateException("Unknown TxPower level=" + txPowerFlag);
-        }
-    }
-
-    public void stopMultiAdvertising(int appIf) {
-        LOGGER.d(String.format("stopAdvertising(%d) on %s", appIf, mAddress));
-        Advertiser advertiser = mAdvertisers.get(appIf);
-        if (advertiser == null) {
-            LOGGER.d(String.format("Advertising already stopped on %s, clientIf: %d", mAddress,
-                    appIf));
-            return;
-        }
-        mAdvertisers.remove(appIf);
-        final IBluetoothGattCallback callback = mClientCallbacks.get(appIf);
-        @SuppressWarnings("unused") // go/futurereturn-lsc
-        Future<?> possiblyIgnoredError =
-                DeviceShadowEnvironmentImpl.run(
-                        mAddress,
-                        () -> {
-                            callback.onMultiAdvertiseCallback(
-                                    BluetoothConstants.ADVERTISE_SUCCESS, false /* isStart */,
-                                    null /* setting */);
-                            return null;
-                        });
-    }
-
-    public void startScan(final int appIf, ScanSettings settings, List<ScanFilter> filters) {
-        LOGGER.d(String.format("startScan(%d) on %s", appIf, mAddress));
-        if (filters == null) {
-            filters = new ArrayList<>();
-        }
-        final Scanner scanner = new Scanner(appIf, settings, filters);
-        mScanners.put(appIf, scanner);
-        @SuppressWarnings("unused") // go/futurereturn-lsc
-        Future<?> possiblyIgnoredError =
-                DeviceShadowEnvironmentImpl.run(
-                        mAddress,
-                        () -> {
-                            try {
-                                scan(scanner);
-                            } catch (InterruptedException e) {
-                                LOGGER.e(
-                                        String.format("Failed to scan on %s, clientIf: %d.",
-                                                mAddress, scanner.mClientIf),
-                                        e);
-                            }
-                            return null;
-                        });
-    }
-
-    // TODO(b/200231384): support periodic scan with interval and scan window.
-    private void scan(Scanner scanner) throws InterruptedException {
-        // fetch existing advertisements
-        List<DeviceletImpl> devicelets = DeviceShadowEnvironmentImpl.getDeviceletImpls();
-        for (DeviceletImpl devicelet : devicelets) {
-            BlueletImpl bluelet = devicelet.blueletImpl();
-            if (bluelet.address.equals(mAddress)) {
-                continue;
-            }
-            for (Advertiser advertiser : bluelet.getGattDelegate().mAdvertisers.values()) {
-                if (VERSION.SDK_INT < 21) {
-                    throw new UnsupportedOperationException(
-                            String.format("API %d is not supported.", VERSION.SDK_INT));
-                }
-
-                byte[] advertiseData =
-                        GattHelper.convertAdvertiseData(
-                                advertiser.mAdvertiseData,
-                                advertiser.mTxPowerLevel,
-                                advertiser.mName,
-                                advertiser.mSettings.isConnectable());
-                byte[] scanResponse =
-                        GattHelper.convertAdvertiseData(
-                                advertiser.mScanResponse,
-                                advertiser.mTxPowerLevel,
-                                advertiser.mName,
-                                advertiser.mSettings.isConnectable());
-
-                ScanRecord scanRecord =
-                        ReflectionHelpers.callStaticMethod(
-                                ScanRecord.class,
-                                "parseFromBytes",
-                                ClassParameter.from(byte[].class,
-                                        Bytes.concat(advertiseData, scanResponse)));
-                ScanResult scanResult =
-                        new ScanResult(
-                                BluetoothAdapter.getDefaultAdapter()
-                                        .getRemoteDevice(advertiser.mAddress),
-                                scanRecord,
-                                DEFAULT_RSSI,
-                                SystemClock.elapsedRealtimeNanos());
-
-                if (!matchFilters(scanResult, scanner.mFilters)) {
-                    continue;
-                }
-
-                IBluetoothGattCallback callback = mClientCallbacks.get(scanner.mClientIf);
-                if (callback == null) {
-                    LOGGER.e(
-                            String.format("Callback is null on %s, clientIf: %d", mAddress,
-                                    scanner.mClientIf));
-                    return;
-                }
-                callback.onScanResult(scanResult);
-            }
-        }
-    }
-
-    private boolean matchFilters(ScanResult scanResult, List<ScanFilter> filters) {
-        for (ScanFilter filter : filters) {
-            if (!filter.matches(scanResult)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    public void stopScan(int appIf) {
-        LOGGER.d(String.format("stopScan(%d) on %s", appIf, mAddress));
-        Scanner scanner = mScanners.get(appIf);
-        if (scanner == null) {
-            LOGGER.d(
-                    String.format("Scanning already stopped on %s, clientIf: %d", mAddress, appIf));
-            return;
-        }
-        mScanners.remove(appIf);
-    }
-
-    static class Service {
-
-        private Map<ParcelUuid, Characteristic> mCharacteristics = new HashMap<>();
-        private ParcelUuid mUuid;
-
-        Service(ParcelUuid uuid) {
-            this.mUuid = uuid;
-        }
-
-        Characteristic getCharacteristic(ParcelUuid uuid) {
-            return mCharacteristics.get(uuid);
-        }
-
-        Characteristic addCharacteristic(ParcelUuid uuid, int properties, int permissions) {
-            Characteristic ch = new Characteristic(uuid, properties, permissions);
-            mCharacteristics.put(uuid, ch);
-            return ch;
-        }
-
-        Collection<Characteristic> getCharacteristics() {
-            return mCharacteristics.values();
-        }
-
-        ParcelUuid getUuid() {
-            return this.mUuid;
-        }
-    }
-
-    static class Characteristic {
-
-        private int mProperties;
-        private ParcelUuid mUuid;
-        private Map<ParcelUuid, Descriptor> mDescriptors = new HashMap<>();
-        private Set<String> mNotifyClients = new HashSet<>();
-        private byte[] mValue;
-
-        Characteristic(ParcelUuid uuid, int properties, int permissions) {
-            this.mProperties = properties;
-            this.mUuid = uuid;
-        }
-
-        Descriptor getDescriptor(ParcelUuid uuid) {
-            return mDescriptors.get(uuid);
-        }
-
-        Descriptor addDescriptor(ParcelUuid uuid, int permissions) {
-            Descriptor desc = new Descriptor(uuid, permissions);
-            mDescriptors.put(uuid, desc);
-            return desc;
-        }
-
-        Collection<Descriptor> getDescriptors() {
-            return mDescriptors.values();
-        }
-
-        void setValue(byte[] value) {
-            this.mValue = value;
-        }
-
-        byte[] getValue() {
-            return mValue;
-        }
-
-        ParcelUuid getUuid() {
-            return mUuid;
-        }
-
-        int getProperties() {
-            return mProperties;
-        }
-
-        void registerNotification(String client, int clientIf) {
-            mNotifyClients.add(client);
-        }
-
-        Set<String> getNotifyClients() {
-            return mNotifyClients;
-        }
-    }
-
-    static class Descriptor {
-
-        int mPermissions;
-        ParcelUuid mUuid;
-        byte[] mValue;
-
-        Descriptor(ParcelUuid uuid, int permissions) {
-            this.mUuid = uuid;
-            this.mPermissions = permissions;
-        }
-
-        void setValue(byte[] value) {
-            this.mValue = value;
-        }
-
-        byte[] getValue() {
-            return mValue;
-        }
-
-        ParcelUuid getUuid() {
-            return mUuid;
-        }
-    }
-
-    @VisibleForTesting
-    static class Advertiser {
-
-        final int mClientIf;
-        final String mAddress;
-        final String mName;
-        final int mTxPowerLevel;
-        final AdvertiseData mAdvertiseData;
-        @Nullable
-        final AdvertiseData mScanResponse;
-        final AdvertiseSettings mSettings;
-
-        Advertiser(
-                int clientIf,
-                String address,
-                String name,
-                int txPowerLevel,
-                AdvertiseData advertiseData,
-                AdvertiseData scanResponse,
-                AdvertiseSettings settings) {
-            this.mClientIf = clientIf;
-            this.mAddress = Preconditions.checkNotNull(address);
-            this.mName = name;
-            this.mTxPowerLevel = txPowerLevel;
-            this.mAdvertiseData = Preconditions.checkNotNull(advertiseData);
-            this.mScanResponse = scanResponse;
-            this.mSettings = Preconditions.checkNotNull(settings);
-        }
-    }
-
-    @VisibleForTesting
-    static class Scanner {
-
-        final int mClientIf;
-        final ScanSettings mSettings;
-        final List<ScanFilter> mFilters;
-
-        Scanner(int clientIf, ScanSettings settings, List<ScanFilter> filters) {
-            this.mClientIf = clientIf;
-            this.mSettings = Preconditions.checkNotNull(settings);
-            this.mFilters = Preconditions.checkNotNull(filters);
-        }
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothGattImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothGattImpl.java
deleted file mode 100644
index 0ac287d..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothGattImpl.java
+++ /dev/null
@@ -1,707 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.IBluetoothGatt;
-import android.bluetooth.IBluetoothGattCallback;
-import android.bluetooth.IBluetoothGattServerCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanSettings;
-import android.os.ParcelUuid;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.GattDelegate.ReadCharacteristicRequest;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.GattDelegate.ReadDescriptorRequest;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.GattDelegate.Request;
-import com.android.libraries.testing.deviceshadower.internal.common.NamedRunnable;
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Implementation of IBluetoothGatt.
- */
-public class IBluetoothGattImpl implements IBluetoothGatt {
-
-    private static final Logger LOGGER = Logger.create("IBluetoothGattImpl");
-    private GattDelegate.Service mCurrentService;
-    private GattDelegate.Characteristic mCurrentCharacteristic;
-
-    @Override
-    public void startScan(
-            int appIf,
-            boolean isServer,
-            ScanSettings settings,
-            List<ScanFilter> filters,
-            List<?> scanStorages,
-            String callingPackage) {
-        localGattDelegate().startScan(appIf, settings, filters);
-    }
-
-    @Override
-    public void startScan(
-            int appIf,
-            boolean isServer,
-            ScanSettings settings,
-            List<ScanFilter> filters,
-            List<?> scanStorages) {
-        startScan(appIf, isServer, settings, filters, scanStorages, "" /* callingPackage */);
-    }
-
-    @Override
-    public void stopScan(int appIf, boolean isServer) {
-        localGattDelegate().stopScan(appIf);
-    }
-
-    @Override
-    public void startMultiAdvertising(
-            int appIf,
-            AdvertiseData advertiseData,
-            AdvertiseData scanResponse,
-            AdvertiseSettings settings) {
-        localGattDelegate().startMultiAdvertising(appIf, advertiseData, scanResponse, settings);
-    }
-
-    @Override
-    public void stopMultiAdvertising(int appIf) {
-        localGattDelegate().stopMultiAdvertising(appIf);
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void registerClient(ParcelUuid appId, final IBluetoothGattCallback callback) {
-        final int clientIf = localGattDelegate().registerClient(callback);
-        NamedRunnable onClientRegistered =
-                NamedRunnable.create(
-                        "ClientGatt.onClientRegistered=" + clientIf,
-                        () -> {
-                            callback.onClientRegistered(BluetoothGatt.GATT_SUCCESS, clientIf);
-                        });
-
-        DeviceShadowEnvironmentImpl.runOnService(localAddress(), onClientRegistered);
-    }
-
-    @Override
-    public void unregisterClient(int clientIf) {
-        localGattDelegate().unregisterClient(clientIf);
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void clientConnect(
-            final int clientIf, final String serverAddress, boolean isDirect, int transport) {
-        // TODO(b/200231384): implement auto connect.
-        String clientAddress = localAddress();
-        int serverIf = remoteGattDelegate(serverAddress).getServerIf();
-        boolean success = remoteGattDelegate(serverAddress).connect(clientAddress);
-        if (!success) {
-            LOGGER.i(String.format("clientConnect failed: %s connect %s", serverAddress,
-                    clientAddress));
-            return;
-        }
-
-        DeviceShadowEnvironmentImpl.runOnService(
-                clientAddress,
-                newClientConnectionStateChangeRunnable(clientIf, true, serverAddress));
-
-        DeviceShadowEnvironmentImpl.runOnService(
-                serverAddress,
-                newServerConnectionStateChangeRunnable(serverIf, true, clientAddress));
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void clientDisconnect(final int clientIf, final String serverAddress) {
-        final String clientAddress = localAddress();
-        remoteGattDelegate(serverAddress).disconnect(clientAddress);
-        int serverIf = remoteGattDelegate(serverAddress).getServerIf();
-
-
-        DeviceShadowEnvironmentImpl.runOnService(
-                clientAddress,
-                newClientConnectionStateChangeRunnable(clientIf, false, serverAddress));
-
-        DeviceShadowEnvironmentImpl.runOnService(
-                serverAddress,
-                newServerConnectionStateChangeRunnable(serverIf, false, clientAddress));
-    }
-
-    @Override
-    public void discoverServices(int clientIf, String serverAddress) {
-        final IBluetoothGattCallback callback = localGattDelegate().getClientCallback(clientIf);
-        if (callback == null) {
-            return;
-        }
-        for (GattDelegate.Service service : remoteGattDelegate(serverAddress).getServices()) {
-            callback.onGetService(serverAddress, 0 /*srvcType*/, 0 /*srvcInstId*/,
-                    service.getUuid());
-
-            for (GattDelegate.Characteristic characteristic : service.getCharacteristics()) {
-                callback.onGetCharacteristic(
-                        serverAddress,
-                        0 /*srvcType*/,
-                        0 /*srvcInstId*/,
-                        service.getUuid(),
-                        0 /*charInstId*/,
-                        characteristic.getUuid(),
-                        characteristic.getProperties());
-                for (GattDelegate.Descriptor descriptor : characteristic.getDescriptors()) {
-                    callback.onGetDescriptor(
-                            serverAddress,
-                            0 /*srvcType*/,
-                            0 /*srvcInstId*/,
-                            service.getUuid(),
-                            0 /*charInstId*/,
-                            characteristic.getUuid(),
-                            0 /*descrInstId*/,
-                            descriptor.getUuid());
-                }
-            }
-        }
-
-        callback.onSearchComplete(serverAddress, BluetoothGatt.GATT_SUCCESS);
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void readCharacteristic(
-            final int clientIf,
-            final String serverAddress,
-            final int srvcType,
-            final int srvcInstId,
-            final ParcelUuid srvcId,
-            final int charInstId,
-            final ParcelUuid charId,
-            final int authReq) {
-        // TODO(b/200231384): implement authReq.
-        final String clientAddress = localAddress();
-        localGattDelegate()
-                .setLastRequest(
-                        new ReadCharacteristicRequest(srvcType, srvcInstId, srvcId, charInstId,
-                                charId));
-
-        NamedRunnable serverOnCharacteristicReadRequest =
-                NamedRunnable.create(
-                        "ServerGatt.onCharacteristicReadRequest",
-                        () -> {
-                            int serverIf = localGattDelegate().getServerIf();
-                            IBluetoothGattServerCallback callback =
-                                    localGattDelegate().getServerCallback(serverIf);
-                            if (callback != null) {
-                                callback.onCharacteristicReadRequest(
-                                        clientAddress,
-                                        0 /*transId*/,
-                                        0 /*offset*/,
-                                        false /*isLong*/,
-                                        0 /*srvcType*/,
-                                        srvcInstId,
-                                        srvcId,
-                                        charInstId,
-                                        charId);
-                            }
-                        });
-
-        DeviceShadowEnvironmentImpl.runOnService(serverAddress, serverOnCharacteristicReadRequest);
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void writeCharacteristic(
-            final int clientIf,
-            final String serverAddress,
-            final int srvcType,
-            final int srvcInstId,
-            final ParcelUuid srvcId,
-            final int charInstId,
-            final ParcelUuid charId,
-            final int writeType,
-            final int authReq,
-            final byte[] value) {
-        // TODO(b/200231384): implement write with response needed.
-        remoteGattDelegate(serverAddress).getService(srvcId).getCharacteristic(charId)
-                .setValue(value);
-        final String clientAddress = localAddress();
-
-        NamedRunnable clientOnCharacteristicWrite =
-                NamedRunnable.create(
-                        "ClientGatt.onCharacteristicWrite",
-                        () -> {
-                            IBluetoothGattCallback callback = localGattDelegate().getClientCallback(
-                                    clientIf);
-                            if (callback != null) {
-                                callback.onCharacteristicWrite(
-                                        serverAddress,
-                                        BluetoothGatt.GATT_SUCCESS,
-                                        0 /*srvcType*/,
-                                        srvcInstId,
-                                        srvcId,
-                                        charInstId,
-                                        charId);
-                            }
-                        });
-
-        NamedRunnable onCharacteristicWriteRequest =
-                NamedRunnable.create(
-                        "ServerGatt.onCharacteristicWriteRequest",
-                        () -> {
-                            int serverIf = localGattDelegate().getServerIf();
-                            IBluetoothGattServerCallback callback =
-                                    localGattDelegate().getServerCallback(serverIf);
-                            if (callback != null) {
-                                callback.onCharacteristicWriteRequest(
-                                        clientAddress,
-                                        0 /*transId*/,
-                                        0 /*offset*/,
-                                        value.length,
-                                        false /*isPrep*/,
-                                        false /*needRsp*/,
-                                        0 /*srvcType*/,
-                                        srvcInstId,
-                                        srvcId,
-                                        charInstId,
-                                        charId,
-                                        value);
-                            }
-                        });
-
-        DeviceShadowEnvironmentImpl.runOnService(clientAddress, clientOnCharacteristicWrite);
-
-        DeviceShadowEnvironmentImpl.runOnService(serverAddress, onCharacteristicWriteRequest);
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void readDescriptor(
-            final int clientIf,
-            final String serverAddress,
-            final int srvcType,
-            final int srvcInstId,
-            final ParcelUuid srvcId,
-            final int charInstId,
-            final ParcelUuid charId,
-            final int descrInstId,
-            final ParcelUuid descrId,
-            final int authReq) {
-        final String clientAddress = localAddress();
-        localGattDelegate()
-                .setLastRequest(
-                        new ReadDescriptorRequest(
-                                srvcType, srvcInstId, srvcId, charInstId, charId, descrInstId,
-                                descrId));
-
-        NamedRunnable serverOnDescriptorReadRequest =
-                NamedRunnable.create(
-                        "ServerGatt.onDescriptorReadRequest",
-                        () -> {
-                            int serverIf = localGattDelegate().getServerIf();
-                            IBluetoothGattServerCallback callback =
-                                    localGattDelegate().getServerCallback(serverIf);
-                            if (callback != null) {
-                                callback.onDescriptorReadRequest(
-                                        clientAddress,
-                                        0 /*transId*/,
-                                        0 /*offset*/,
-                                        false /*isLong*/,
-                                        0 /*srvcType*/,
-                                        srvcInstId,
-                                        srvcId,
-                                        charInstId,
-                                        charId,
-                                        descrId);
-                            }
-                        });
-
-        DeviceShadowEnvironmentImpl.runOnService(serverAddress, serverOnDescriptorReadRequest);
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void writeDescriptor(
-            final int clientIf,
-            final String serverAddress,
-            final int srvcType,
-            final int srvcInstId,
-            final ParcelUuid srvcId,
-            final int charInstId,
-            final ParcelUuid charId,
-            final int descrInstId,
-            final ParcelUuid descrId,
-            final int writeType,
-            final int authReq,
-            final byte[] value) {
-        // TODO(b/200231384): implement write with response needed.
-        remoteGattDelegate(serverAddress)
-                .getService(srvcId)
-                .getCharacteristic(charId)
-                .getDescriptor(descrId)
-                .setValue(value);
-        final String clientAddress = localAddress();
-
-        NamedRunnable serverOnDescriptorWriteRequest =
-                NamedRunnable.create(
-                        "ServerGatt.onDescriptorWriteRequest",
-                        () -> {
-                            int serverIf = localGattDelegate().getServerIf();
-                            IBluetoothGattServerCallback callback =
-                                    localGattDelegate().getServerCallback(serverIf);
-                            if (callback != null) {
-                                callback.onDescriptorWriteRequest(
-                                        clientAddress,
-                                        0 /*transId*/,
-                                        0 /*offset*/,
-                                        value.length,
-                                        false /*isPrep*/,
-                                        false /*needRsp*/,
-                                        0 /*srvcType*/,
-                                        srvcInstId,
-                                        srvcId,
-                                        charInstId,
-                                        charId,
-                                        descrId,
-                                        value);
-                            }
-                        });
-
-        NamedRunnable clientOnDescriptorWrite =
-                NamedRunnable.create(
-                        "ClientGatt.onDescriptorWrite",
-                        () -> {
-                            IBluetoothGattCallback callback = localGattDelegate().getClientCallback(
-                                    clientIf);
-                            if (callback != null) {
-                                callback.onDescriptorWrite(
-                                        serverAddress,
-                                        BluetoothGatt.GATT_SUCCESS,
-                                        0 /*srvcType*/,
-                                        srvcInstId,
-                                        srvcId,
-                                        charInstId,
-                                        charId,
-                                        descrInstId,
-                                        descrId);
-                            }
-                        });
-
-        DeviceShadowEnvironmentImpl.runOnService(serverAddress, serverOnDescriptorWriteRequest);
-
-        DeviceShadowEnvironmentImpl.runOnService(clientAddress, clientOnDescriptorWrite);
-    }
-
-    @Override
-    public void registerForNotification(
-            int clientIf,
-            String remoteAddress,
-            int srvcType,
-            int srvcInstId,
-            ParcelUuid srvcId,
-            int charInstId,
-            ParcelUuid charId,
-            boolean enable) {
-        remoteGattDelegate(remoteAddress)
-                .getService(srvcId)
-                .getCharacteristic(charId)
-                .registerNotification(localAddress(), clientIf);
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void registerServer(ParcelUuid appId, final IBluetoothGattServerCallback callback) {
-        // TODO(b/200231384): support multiple serverIf.
-        final int serverIf = localGattDelegate().registerServer(callback);
-        NamedRunnable serverOnRegistered =
-                NamedRunnable.create(
-                        "ServerGatt.onServerRegistered",
-                        () -> {
-                            callback.onServerRegistered(BluetoothGatt.GATT_SUCCESS, serverIf);
-                        });
-
-        DeviceShadowEnvironmentImpl.runOnService(localAddress(), serverOnRegistered);
-    }
-
-    @Override
-    public void unregisterServer(int serverIf) {
-        localGattDelegate().unregisterServer(serverIf);
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void serverConnect(
-            final int serverIf, final String clientAddress, boolean isDirect, int transport) {
-        // TODO(b/200231384): implement isDirect and transport.
-        boolean success = localGattDelegate().connect(clientAddress);
-        final String serverAddress = localAddress();
-        if (!success) {
-            return;
-        }
-        int clientIf = remoteGattDelegate(clientAddress).getClientIf();
-
-        DeviceShadowEnvironmentImpl.runOnService(
-                serverAddress,
-                newServerConnectionStateChangeRunnable(serverIf, true, clientAddress));
-
-        DeviceShadowEnvironmentImpl.runOnService(
-                clientAddress,
-                newClientConnectionStateChangeRunnable(clientIf, true, serverAddress));
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void serverDisconnect(final int serverIf, final String clientAddress) {
-        localGattDelegate().disconnect(clientAddress);
-        String serverAddress = localAddress();
-        int clientIf = remoteGattDelegate(clientAddress).getClientIf();
-
-        DeviceShadowEnvironmentImpl.runOnService(
-                serverAddress,
-                newServerConnectionStateChangeRunnable(serverIf, false, clientAddress));
-
-        DeviceShadowEnvironmentImpl.runOnService(
-                clientAddress,
-                newClientConnectionStateChangeRunnable(clientIf, false, serverAddress));
-    }
-
-    @Override
-    public void beginServiceDeclaration(
-            int serverIf,
-            int srvcType,
-            int srvcInstId,
-            int minHandles,
-            ParcelUuid srvcId,
-            boolean advertisePreferred) {
-        // TODO(b/200231384): support different service type, instanceId, advertisePreferred.
-        mCurrentService = localGattDelegate().addService(srvcId);
-    }
-
-    @Override
-    public void addIncludedService(int serverIf, int srvcType, int srvcInstId, ParcelUuid srvcId) {
-        // TODO(b/200231384): implement this.
-    }
-
-    @Override
-    public void addCharacteristic(int serverIf, ParcelUuid charId, int properties,
-            int permissions) {
-        mCurrentCharacteristic = mCurrentService.addCharacteristic(charId, properties, permissions);
-    }
-
-    @Override
-    public void addDescriptor(int serverIf, ParcelUuid descId, int permissions) {
-        mCurrentCharacteristic.addDescriptor(descId, permissions);
-    }
-
-    @Override
-    public void endServiceDeclaration(int serverIf) {
-        // TODO(b/200231384): choose correct srvc type and inst id.
-        IBluetoothGattServerCallback callback = localGattDelegate().getServerCallback(serverIf);
-        if (callback != null) {
-            callback.onServiceAdded(
-                    BluetoothGatt.GATT_SUCCESS, 0 /*srvcType*/, 0 /*srvcInstId*/,
-                    mCurrentService.getUuid());
-        }
-        mCurrentService = null;
-    }
-
-    @Override
-    public void removeService(int serverIf, int srvcType, int srvcInstId, ParcelUuid srvcId) {
-        // TODO(b/200231384): implement remove service.
-        // localGattDelegate().removeService(srvcId);
-    }
-
-    @Override
-    public void clearServices(int serverIf) {
-        // TODO(b/200231384): support multiple serverIf.
-        // localGattDelegate().clearService();
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void sendResponse(
-            int serverIf, String clientAddress, int requestId, int status, int offset,
-            byte[] value) {
-        // TODO(b/200231384): implement more operations.
-        String serverAddress = localAddress();
-
-        DeviceShadowEnvironmentImpl.runOnService(
-                clientAddress,
-                NamedRunnable.create(
-                        "ClientGatt.receiveResponse",
-                        () -> {
-                            IBluetoothGattCallback callback =
-                                    localGattDelegate().getClientCallback(
-                                            localGattDelegate().getClientIf());
-                            if (callback != null) {
-                                Request request = localGattDelegate().getLastRequest();
-                                localGattDelegate().setLastRequest(null);
-                                if (request != null) {
-                                    if (request instanceof ReadCharacteristicRequest) {
-                                        callback.onCharacteristicRead(
-                                                serverAddress,
-                                                status,
-                                                request.mSrvcType,
-                                                request.mSrvcInstId,
-                                                request.mSrvcId,
-                                                request.mCharInstId,
-                                                request.mCharId,
-                                                value);
-                                    } else if (request instanceof ReadDescriptorRequest) {
-                                        ReadDescriptorRequest readDescriptorRequest =
-                                                (ReadDescriptorRequest) request;
-                                        callback.onDescriptorRead(
-                                                serverAddress,
-                                                status,
-                                                readDescriptorRequest.mSrvcType,
-                                                readDescriptorRequest.mSrvcInstId,
-                                                readDescriptorRequest.mSrvcId,
-                                                readDescriptorRequest.mCharInstId,
-                                                readDescriptorRequest.mCharId,
-                                                readDescriptorRequest.mDescrInstId,
-                                                readDescriptorRequest.mDescrId,
-                                                value);
-                                    }
-                                }
-                            }
-                        }));
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void sendNotification(
-            final int serverIf,
-            final String address,
-            final int srvcType,
-            final int srvcInstId,
-            final ParcelUuid srvcId,
-            final int charInstId,
-            final ParcelUuid charId,
-            boolean confirm,
-            final byte[] value) {
-        GattDelegate.Characteristic characteristic =
-                localGattDelegate().getService(srvcId).getCharacteristic(charId);
-        characteristic.setValue(value);
-        final String serverAddress = localAddress();
-        for (final String clientAddress : characteristic.getNotifyClients()) {
-            NamedRunnable clientOnNotify =
-                    NamedRunnable.create(
-                            "ClientGatt.onNotify",
-                            () -> {
-                                int clientIf = localGattDelegate().getClientIf();
-                                IBluetoothGattCallback callback =
-                                        localGattDelegate().getClientCallback(clientIf);
-                                if (callback != null) {
-                                    callback.onNotify(
-                                            serverAddress, srvcType, srvcInstId, srvcId, charInstId,
-                                            charId, value);
-                                }
-                            });
-
-            DeviceShadowEnvironmentImpl.runOnService(clientAddress, clientOnNotify);
-        }
-
-        NamedRunnable serverOnNotificationSent =
-                NamedRunnable.create(
-                        "ServerGatt.onNotificationSent",
-                        () -> {
-                            IBluetoothGattServerCallback callback =
-                                    localGattDelegate().getServerCallback(serverIf);
-                            if (callback != null) {
-                                callback.onNotificationSent(address, BluetoothGatt.GATT_SUCCESS);
-                            }
-                        });
-
-        DeviceShadowEnvironmentImpl.runOnService(serverAddress, serverOnNotificationSent);
-    }
-
-    @Override
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void configureMTU(int clientIf, String address, int mtu) {
-        final String clientAddress = localAddress();
-
-        NamedRunnable clientSetMtu =
-                NamedRunnable.create(
-                        "ClientGatt.setMtu",
-                        () -> {
-                            localGattDelegate().clientSetMtu(clientIf, mtu, address);
-                        });
-        NamedRunnable serverSetMtu =
-                NamedRunnable.create(
-                        "ServerGatt.setMtu",
-                        () -> {
-                            int serverIf = localGattDelegate().getServerIf();
-                            localGattDelegate().serverSetMtu(serverIf, mtu, clientAddress);
-                        });
-
-        DeviceShadowEnvironmentImpl.runOnService(clientAddress, clientSetMtu);
-
-        DeviceShadowEnvironmentImpl.runOnService(address, serverSetMtu);
-    }
-
-    @Override
-    public void connectionParameterUpdate(int clientIf, String address, int connectionPriority) {
-        // TODO(b/200231384): Implement.
-    }
-
-    @Override
-    public void disconnectAll() {
-    }
-
-    @Override
-    public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
-        return new ArrayList<>();
-    }
-
-    @VisibleForTesting
-    static GattDelegate remoteGattDelegate(String address) {
-        return DeviceShadowEnvironmentImpl.getBlueletImpl(address).getGattDelegate();
-    }
-
-    private static GattDelegate localGattDelegate() {
-        return DeviceShadowEnvironmentImpl.getLocalBlueletImpl().getGattDelegate();
-    }
-
-    private static String localAddress() {
-        return DeviceShadowEnvironmentImpl.getLocalBlueletImpl().address;
-    }
-
-    private static NamedRunnable newClientConnectionStateChangeRunnable(
-            final int clientIf, final boolean isConnected, final String serverAddress) {
-        return NamedRunnable.create(
-                "ClientGatt.clientConnectionStateChange",
-                () -> {
-                    localGattDelegate()
-                            .clientConnectionStateChange(
-                                    BluetoothGatt.GATT_SUCCESS, clientIf, isConnected,
-                                    serverAddress);
-                });
-    }
-
-    private static NamedRunnable newServerConnectionStateChangeRunnable(
-            final int serverIf, final boolean isConnected, final String clientAddress) {
-        return NamedRunnable.create(
-                "ServerGatt.serverConnectionStateChange",
-                () -> {
-                    localGattDelegate()
-                            .serverConnectionStateChange(
-                                    BluetoothGatt.GATT_SUCCESS, serverIf, isConnected,
-                                    clientAddress);
-                });
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothImpl.java
deleted file mode 100644
index ccf0ac3..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothImpl.java
+++ /dev/null
@@ -1,428 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.IBluetooth;
-import android.bluetooth.OobData;
-import android.content.AttributionSource;
-import android.os.ParcelFileDescriptor;
-import android.os.ParcelUuid;
-
-import com.android.libraries.testing.deviceshadower.Bluelet.CreateBondOutcome;
-import com.android.libraries.testing.deviceshadower.Bluelet.FetchUuidsTiming;
-import com.android.libraries.testing.deviceshadower.Bluelet.IoCapabilities;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.State;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl.PairingConfirmation;
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import com.google.common.base.Preconditions;
-
-import java.util.Random;
-
-/**
- * Implementation of IBluetooth interface.
- */
-public class IBluetoothImpl implements IBluetooth {
-
-    private static final Logger LOGGER = Logger.create("BlueletImpl");
-
-    private enum PairingVariant {
-        JUST_WORKS,
-        /**
-         * AKA Passkey Confirmation.
-         */
-        NUMERIC_COMPARISON,
-        PASSKEY_INPUT,
-        CONSENT
-    }
-
-    /**
-     * User will be prompted to accept or deny the incoming pairing request.
-     */
-    private static final int PAIRING_VARIANT_CONSENT = 3;
-
-    /**
-     * User will be prompted to enter the passkey displayed on remote device. This is used for
-     * Bluetooth 2.1 pairing.
-     */
-    private static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4;
-
-    public IBluetoothImpl() {
-    }
-
-    @Override
-    public String getAddress() {
-        return localBlueletImpl().address;
-    }
-
-    @Override
-    public String getName() {
-        return localBlueletImpl().mName;
-    }
-
-    @Override
-    public boolean setName(String name) {
-        localBlueletImpl().mName = name;
-        return true;
-    }
-
-    @Override
-    public int getRemoteClass(BluetoothDevice device) {
-        return remoteBlueletImpl(device.getAddress()).getAdapterDelegate().getBluetoothClass();
-    }
-
-    @Override
-    public String getRemoteName(BluetoothDevice device) {
-        return remoteBlueletImpl(device.getAddress()).mName;
-    }
-
-    @Override
-    public int getRemoteType(BluetoothDevice device, AttributionSource attributionSource) {
-        return BluetoothDevice.DEVICE_TYPE_LE;
-    }
-
-    @Override
-    public ParcelUuid[] getRemoteUuids(BluetoothDevice device) {
-        return remoteBlueletImpl(device.getAddress()).mProfileUuids;
-    }
-
-    @Override
-    public boolean fetchRemoteUuids(BluetoothDevice device) {
-        localBlueletImpl().onFetchedUuids(device.getAddress(), getRemoteUuids(device));
-        return true;
-    }
-
-    @Override
-    public int getBondState(BluetoothDevice device, AttributionSource attributionSource) {
-        return localBlueletImpl().getBondState(device.getAddress());
-    }
-
-    @Override
-    public boolean createBond(BluetoothDevice device, int transport, OobData remoteP192Data,
-            OobData remoteP256Data, AttributionSource attributionSource) {
-        setBondState(device.getAddress(), BluetoothDevice.BOND_BONDING, BlueletImpl.REASON_SUCCESS);
-
-        BlueletImpl remoteBluelet = remoteBlueletImpl(device.getAddress());
-        BlueletImpl localBluelet = localBlueletImpl();
-
-        // Like the real Bluetooth stack, choose a pairing variant based on IO Capabilities.
-        // https://blog.bluetooth.com/bluetooth-pairing-part-2-key-generation-methods
-        PairingVariant variant = PairingVariant.JUST_WORKS;
-        if (localBluelet.getIoCapabilities() == IoCapabilities.DISPLAY_YES_NO) {
-            if (remoteBluelet.getIoCapabilities() == IoCapabilities.DISPLAY_YES_NO) {
-                variant = PairingVariant.NUMERIC_COMPARISON;
-            } else if (remoteBluelet.getIoCapabilities() == IoCapabilities.KEYBOARD_ONLY) {
-                variant = PairingVariant.PASSKEY_INPUT;
-            } else if (remoteBluelet.getIoCapabilities() == IoCapabilities.NO_INPUT_NO_OUTPUT
-                    && localBluelet.getEnableCVE20192225()) {
-                // After CVE-2019-2225, Bluetooth decides to ask consent instead of JustWorks.
-                variant = PairingVariant.CONSENT;
-            }
-        }
-
-        // Bonding doesn't complete until the passkey is confirmed on both devices. The passkey is a
-        // positive 6-digit integer, generated by the Bluetooth stack.
-        int passkey = new Random().nextInt(999999) + 1;
-        switch (variant) {
-            case NUMERIC_COMPARISON:
-                localBluelet.onPairingRequest(
-                        remoteBluelet.address, BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION,
-                        passkey);
-                remoteBluelet.onPairingRequest(
-                        localBluelet.address, BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION,
-                        passkey);
-                break;
-            case JUST_WORKS:
-                // Bonding completes immediately, with no PAIRING_REQUEST broadcast.
-                finishBonding(device);
-                break;
-            case PASSKEY_INPUT:
-                localBluelet.onPairingRequest(
-                        remoteBluelet.address, PAIRING_VARIANT_DISPLAY_PASSKEY, passkey);
-                localBluelet.mPassKey = passkey;
-                remoteBluelet.onPairingRequest(
-                        localBluelet.address, PAIRING_VARIANT_DISPLAY_PASSKEY, passkey);
-                break;
-            case CONSENT:
-                localBluelet.onPairingRequest(remoteBluelet.address,
-                        PAIRING_VARIANT_CONSENT, /* key= */ 0);
-                if (remoteBluelet.getIoCapabilities() == IoCapabilities.NO_INPUT_NO_OUTPUT) {
-                    remoteBluelet.setPairingConfirmation(localBluelet.address,
-                            PairingConfirmation.CONFIRMED);
-                } else {
-                    remoteBluelet.onPairingRequest(
-                            localBluelet.address, PAIRING_VARIANT_CONSENT, /* key= */ 0);
-                }
-                break;
-        }
-        return true;
-    }
-
-    private void finishBonding(BluetoothDevice device) {
-        BlueletImpl remoteBluelet = remoteBlueletImpl(device.getAddress());
-        finishBonding(
-                device, remoteBluelet.getCreateBondOutcome(),
-                remoteBluelet.getCreateBondFailureReason());
-    }
-
-    private void finishBonding(BluetoothDevice device, CreateBondOutcome outcome,
-            int failureReason) {
-        switch (outcome) {
-            case SUCCESS:
-                setBondState(device.getAddress(), BluetoothDevice.BOND_BONDED,
-                        BlueletImpl.REASON_SUCCESS);
-                break;
-            case FAILURE:
-                setBondState(device.getAddress(), BluetoothDevice.BOND_NONE, failureReason);
-                break;
-            case TIMEOUT:
-                // Send nothing.
-                break;
-        }
-    }
-
-    @Override
-    public boolean setPairingConfirmation(BluetoothDevice device, boolean confirmed,
-            AttributionSource attributionSource) {
-        localBlueletImpl()
-                .setPairingConfirmation(
-                        device.getAddress(),
-                        confirmed ? PairingConfirmation.CONFIRMED : PairingConfirmation.DENIED);
-
-        PairingConfirmation remoteConfirmation =
-                remoteBlueletImpl(device.getAddress()).getPairingConfirmation(
-                        localBlueletImpl().address);
-        if (confirmed && remoteConfirmation == PairingConfirmation.CONFIRMED) {
-            LOGGER.d(String.format("CONFIRMED"));
-            finishBonding(device);
-        } else if (!confirmed || remoteConfirmation == PairingConfirmation.DENIED) {
-            LOGGER.d(String.format("NOT CONFIRMED"));
-            finishBonding(device, CreateBondOutcome.FAILURE, BlueletImpl.UNBOND_REASON_AUTH_FAILED);
-        }
-        return true;
-    }
-
-    @Override
-    public boolean setPasskey(BluetoothDevice device, int passkey) {
-        BlueletImpl remoteBluelet = remoteBlueletImpl(device.getAddress());
-        if (passkey == remoteBluelet.mPassKey) {
-            finishBonding(device);
-        } else {
-            finishBonding(device, CreateBondOutcome.FAILURE, BlueletImpl.UNBOND_REASON_AUTH_FAILED);
-        }
-        return true;
-    }
-
-    @Override
-    public boolean cancelBondProcess(BluetoothDevice device) {
-        finishBonding(device, CreateBondOutcome.FAILURE, BlueletImpl.UNBOND_REASON_AUTH_CANCELED);
-        return true;
-    }
-
-    @Override
-    public boolean removeBond(BluetoothDevice device) {
-        setBondState(device.getAddress(), BluetoothDevice.BOND_NONE, BlueletImpl.REASON_SUCCESS);
-        return true;
-    }
-
-    @Override
-    public BluetoothDevice[] getBondedDevices() {
-        return localBlueletImpl().getBondedDevices();
-    }
-
-    @Override
-    public int getAdapterConnectionState() {
-        return localBlueletImpl().getAdapterConnectionState();
-    }
-
-    @Override
-    public int getProfileConnectionState(int profile) {
-        return localBlueletImpl().getProfileConnectionState(profile);
-    }
-
-    @Override
-    public int getPhonebookAccessPermission(BluetoothDevice device) {
-        return remoteBlueletImpl(device.getAddress()).mPhonebookAccessPermission;
-    }
-
-    @Override
-    public boolean setPhonebookAccessPermission(BluetoothDevice device, int value) {
-        remoteBlueletImpl(device.getAddress()).mPhonebookAccessPermission = value;
-        return true;
-    }
-
-    @Override
-    public int getMessageAccessPermission(BluetoothDevice device) {
-        return remoteBlueletImpl(device.getAddress()).mMessageAccessPermission;
-    }
-
-    @Override
-    public boolean setMessageAccessPermission(BluetoothDevice device, int value) {
-        remoteBlueletImpl(device.getAddress()).mMessageAccessPermission = value;
-        return true;
-    }
-
-    @Override
-    public int getSimAccessPermission(BluetoothDevice device) {
-        return remoteBlueletImpl(device.getAddress()).mSimAccessPermission;
-    }
-
-    @Override
-    public boolean setSimAccessPermission(BluetoothDevice device, int value) {
-        remoteBlueletImpl(device.getAddress()).mSimAccessPermission = value;
-        return true;
-    }
-
-    private static void setBondState(String remoteAddress, int state, int failureReason) {
-        BlueletImpl remoteBluelet = remoteBlueletImpl(remoteAddress);
-
-        if (remoteBluelet.getFetchUuidsTiming() == FetchUuidsTiming.BEFORE_BONDING) {
-            fetchUuidsOnBondedState(remoteAddress, state);
-        }
-
-        remoteBluelet.setBondState(localBlueletImpl().address, state, failureReason);
-        localBlueletImpl().setBondState(remoteAddress, state, failureReason);
-
-        if (remoteBluelet.getFetchUuidsTiming() == FetchUuidsTiming.AFTER_BONDING) {
-            fetchUuidsOnBondedState(remoteAddress, state);
-        }
-    }
-
-    private static void fetchUuidsOnBondedState(String remoteAddress, int state) {
-        if (state == BluetoothDevice.BOND_BONDED) {
-            remoteBlueletImpl(remoteAddress)
-                    .onFetchedUuids(localBlueletImpl().address, localBlueletImpl().mProfileUuids);
-            localBlueletImpl()
-                    .onFetchedUuids(remoteAddress, remoteBlueletImpl(remoteAddress).mProfileUuids);
-        }
-    }
-
-    @Override
-    public int getScanMode() {
-        return localBlueletImpl().getAdapterDelegate().getScanMode();
-    }
-
-    @Override
-    public boolean setScanMode(int mode, int duration) {
-        localBlueletImpl().getAdapterDelegate().setScanMode(mode);
-        return true;
-    }
-
-    @Override
-    public int getDiscoverableTimeout() {
-        return -1;
-    }
-
-    @Override
-    public boolean setDiscoverableTimeout(int timeout) {
-        return true;
-    }
-
-    @Override
-    public boolean startDiscovery() {
-        localBlueletImpl().getAdapterDelegate().startDiscovery();
-        return true;
-    }
-
-    @Override
-    public boolean cancelDiscovery() {
-        localBlueletImpl().getAdapterDelegate().cancelDiscovery();
-        return true;
-    }
-
-    @Override
-    public boolean isDiscovering() {
-        return localBlueletImpl().getAdapterDelegate().isDiscovering();
-
-    }
-
-    @Override
-    public boolean isEnabled() {
-        return localBlueletImpl().getAdapterDelegate().getState().equals(State.ON);
-    }
-
-    @Override
-    public int getState() {
-        return localBlueletImpl().getAdapterDelegate().getState().getValue();
-    }
-
-    @Override
-    public boolean enable() {
-        localBlueletImpl().enableAdapter();
-        return true;
-    }
-
-    @Override
-    public boolean disable() {
-        localBlueletImpl().disableAdapter();
-        return true;
-    }
-
-    @Override
-    public ParcelFileDescriptor connectSocket(BluetoothDevice device, int type, ParcelUuid uuid,
-            int port, int flag) {
-        Preconditions.checkArgument(
-                port == BluetoothConstants.SOCKET_CHANNEL_CONNECT_WITH_UUID,
-                "Connect to port is not supported.");
-        Preconditions.checkArgument(
-                type == BluetoothConstants.TYPE_RFCOMM,
-                "Only Rfcomm socket is supported.");
-        return localBlueletImpl().getRfcommDelegate()
-                .connectSocket(device.getAddress(), uuid.getUuid());
-    }
-
-    @Override
-    public ParcelFileDescriptor createSocketChannel(int type, String serviceName, ParcelUuid uuid,
-            int port, int flag) {
-        Preconditions.checkArgument(
-                port == BluetoothConstants.SERVER_SOCKET_CHANNEL_AUTO_ASSIGN,
-                "Listen on port is not supported.");
-        Preconditions.checkArgument(
-                type == BluetoothConstants.TYPE_RFCOMM,
-                "Only Rfcomm socket is supported.");
-        return localBlueletImpl().getRfcommDelegate().createSocketChannel(serviceName, uuid);
-    }
-
-    @Override
-    public boolean isMultiAdvertisementSupported() {
-        return maxAdvertiseInstances() > 1;
-    }
-
-    @Override
-    public boolean isPeripheralModeSupported() {
-        return maxAdvertiseInstances() > 0;
-    }
-
-    private int maxAdvertiseInstances() {
-        return localBlueletImpl().getGattDelegate().getMaxAdvertiseInstances();
-    }
-
-    @Override
-    public boolean isOffloadedFilteringSupported() {
-        return localBlueletImpl().getGattDelegate().isOffloadedFilteringSupported();
-    }
-
-    private static BlueletImpl localBlueletImpl() {
-        return DeviceShadowEnvironmentImpl.getLocalBlueletImpl();
-    }
-
-    private static BlueletImpl remoteBlueletImpl(String address) {
-        return DeviceShadowEnvironmentImpl.getBlueletImpl(address);
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothManagerImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothManagerImpl.java
deleted file mode 100644
index cb38a41..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothManagerImpl.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth;
-
-import android.bluetooth.IBluetooth;
-import android.bluetooth.IBluetoothGatt;
-import android.bluetooth.IBluetoothManager;
-import android.bluetooth.IBluetoothManagerCallback;
-
-/**
- * Implementation of IBluetoothManager interface
- */
-public class IBluetoothManagerImpl implements IBluetoothManager {
-
-    private final IBluetooth mFakeBluetoothService = new IBluetoothImpl();
-    private final IBluetoothGatt mFakeGattService = new IBluetoothGattImpl();
-
-    @Override
-    public String getAddress() {
-        return mFakeBluetoothService.getAddress();
-    }
-
-    @Override
-    public String getName() {
-        return mFakeBluetoothService.getName();
-    }
-
-    @Override
-    public IBluetooth registerAdapter(IBluetoothManagerCallback callback) {
-        return mFakeBluetoothService;
-    }
-
-    @Override
-    public IBluetoothGatt getBluetoothGatt() {
-        return mFakeGattService;
-    }
-
-    @Override
-    public boolean enable() {
-        mFakeBluetoothService.enable();
-        return true;
-    }
-
-    @Override
-    public boolean disable(boolean persist) {
-        mFakeBluetoothService.disable();
-        return true;
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/FileDescriptorFactory.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/FileDescriptorFactory.java
deleted file mode 100644
index 12fa587..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/FileDescriptorFactory.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth.connection;
-
-import java.io.FileDescriptor;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Factory which creates {@link FileDescriptor} given an MAC address. Each MAC address can have many
- * FileDescriptor but each FileDescriptor only maps to one MAC address.
- */
-public class FileDescriptorFactory {
-
-    private static FileDescriptorFactory sInstance = null;
-
-    public static synchronized FileDescriptorFactory getInstance() {
-        if (sInstance == null) {
-            sInstance = new FileDescriptorFactory();
-        }
-        return sInstance;
-    }
-
-    public static synchronized void reset() {
-        sInstance = null;
-    }
-
-    private final Map<FileDescriptor, String> mAddressMap;
-
-    private FileDescriptorFactory() {
-        mAddressMap = new ConcurrentHashMap<>();
-    }
-
-    public FileDescriptor createFileDescriptor(String address) {
-        FileDescriptor fd = new FileDescriptor();
-        mAddressMap.put(fd, address);
-        return fd;
-    }
-
-    public String getAddress(FileDescriptor fd) {
-        return mAddressMap.get(fd);
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PageScanHandler.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PageScanHandler.java
deleted file mode 100644
index 82b97ff..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PageScanHandler.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth.connection;
-
-import android.os.Build.VERSION;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.internal.utils.MacAddressGenerator;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.Map;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-
-/**
- * Encapsulate page scan operations -- handle connection establishment between Bluetooth devices.
- */
-public class PageScanHandler {
-
-    private static final ConnectionRequest REQUEST_SERVER_SOCKET_CLOSE = new ConnectionRequest();
-
-    private static PageScanHandler sInstance = null;
-
-    public static synchronized PageScanHandler getInstance() {
-        if (sInstance == null) {
-            sInstance = new PageScanHandler();
-        }
-        return sInstance;
-    }
-
-    public static synchronized void reset() {
-        sInstance = null;
-    }
-
-    // use FileDescriptor to identify incoming data before socket is connected.
-    private final Map<FileDescriptor, BlockingQueue<Integer>> mIncomingDataMap;
-    // map a server socket fd to a connection request queue
-    private final Map<FileDescriptor, BlockingQueue<ConnectionRequest>> mConnectionRequests;
-    // map a fd on client side to a fd of BluetoothSocket(not BluetoothServerSocket) on server side
-    private final Map<FileDescriptor, FileDescriptor> mClientServerFdMap;
-    // map a client fd to a connection request so the client socket can finish the pending
-    // connection
-    private final Map<FileDescriptor, ConnectionRequest> mPendingConnections;
-
-    private PageScanHandler() {
-        mIncomingDataMap = new ConcurrentHashMap<>();
-        mConnectionRequests = new ConcurrentHashMap<>();
-        mClientServerFdMap = new ConcurrentHashMap<>();
-        mPendingConnections = new ConcurrentHashMap<>();
-    }
-
-    public void postConnectionRequest(FileDescriptor serverSocketFd, ConnectionRequest request)
-            throws InterruptedException {
-        // used by the returning socket on server-side
-        FileDescriptor fd = FileDescriptorFactory.getInstance()
-                .createFileDescriptor(request.mServerAddress);
-        mClientServerFdMap.put(request.mClientFd, fd);
-        BlockingQueue<ConnectionRequest> requests = mConnectionRequests.get(serverSocketFd);
-        requests.put(request);
-        mPendingConnections.put(request.mClientFd, request);
-    }
-
-    public void addServerSocket(FileDescriptor serverSocketFd) {
-        mConnectionRequests.put(serverSocketFd, new LinkedBlockingQueue<ConnectionRequest>());
-    }
-
-    public FileDescriptor getServerFd(FileDescriptor clientFd) {
-        return mClientServerFdMap.get(clientFd);
-    }
-
-    // TODO(b/79994182): see go/objecttostring-lsc
-    @SuppressWarnings("ObjectToString")
-    public FileDescriptor processNextConnectionRequest(FileDescriptor serverSocketFd)
-            throws IOException, InterruptedException {
-        ConnectionRequest request = mConnectionRequests.get(serverSocketFd).take();
-        if (request == REQUEST_SERVER_SOCKET_CLOSE) {
-            // TODO(b/79994182): FileDescriptor does not implement toString() in serverSocketFd
-            throw new IOException("Server socket is closed. fd: " + serverSocketFd);
-        }
-        writeInitialConnectionInfo(serverSocketFd, request.mClientAddress, request.mPort);
-        return request.mClientFd;
-    }
-
-    public void waitForConnectionEstablished(FileDescriptor clientFd) throws InterruptedException {
-        ConnectionRequest request = mPendingConnections.get(clientFd);
-        if (request != null) {
-            request.mCountDownLatch.await();
-        }
-    }
-
-    public void finishPendingConnection(FileDescriptor clientFd) {
-        ConnectionRequest request = mPendingConnections.get(clientFd);
-        if (request != null) {
-            request.mCountDownLatch.countDown();
-        }
-    }
-
-    public void cancelServerSocket(FileDescriptor serverSocketFd) throws InterruptedException {
-        mConnectionRequests.get(serverSocketFd).put(REQUEST_SERVER_SOCKET_CLOSE);
-    }
-
-    public void writeInitialConnectionInfo(FileDescriptor fd, String address, int port)
-            throws InterruptedException {
-        for (byte b : initialConnectionInfo(address, port)) {
-            write(fd, Integer.valueOf(b));
-        }
-    }
-
-    public void writePort(FileDescriptor fd, int port) throws InterruptedException {
-        byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(port).array();
-        for (byte b : bytes) {
-            write(fd, Integer.valueOf(b));
-        }
-    }
-
-    public void write(FileDescriptor fd, int data) throws InterruptedException {
-        BlockingQueue<Integer> incomingData = mIncomingDataMap.get(fd);
-        if (incomingData == null) {
-            synchronized (mIncomingDataMap) {
-                incomingData = mIncomingDataMap.get(fd);
-                if (incomingData == null) {
-                    incomingData = new LinkedBlockingQueue<Integer>();
-                    mIncomingDataMap.put(fd, incomingData);
-                }
-            }
-        }
-        incomingData.put(data);
-    }
-
-    public int read(FileDescriptor fd) throws InterruptedException {
-        return mIncomingDataMap.get(fd).take();
-    }
-
-    /**
-     * A connection request from a {@link android.bluetooth.BluetoothSocket}.
-     */
-    @VisibleForTesting
-    public static class ConnectionRequest {
-
-        final FileDescriptor mClientFd;
-        final String mClientAddress;
-        final String mServerAddress;
-        final int mPort;
-        final CountDownLatch mCountDownLatch; // block server socket until connection established
-
-        public ConnectionRequest(FileDescriptor fd, String clientAddress, String serverAddress,
-                int port) {
-            mClientFd = fd;
-            this.mClientAddress = clientAddress;
-            this.mServerAddress = serverAddress;
-            this.mPort = port;
-            mCountDownLatch = new CountDownLatch(1);
-        }
-
-        private ConnectionRequest() {
-            mClientFd = null;
-            mClientAddress = null;
-            mServerAddress = null;
-            mPort = -1;
-            mCountDownLatch = new CountDownLatch(0);
-        }
-    }
-
-    private static byte[] initialConnectionInfo(String addr, int port) {
-        byte[] mac = MacAddressGenerator.convertStringMacAddress(addr);
-        int channel = port;
-        int status = 0;
-
-        if (VERSION.SDK_INT < 23) {
-            byte[] signal = new byte[16];
-            short signalSize = 16;
-            ByteBuffer buffer = ByteBuffer.wrap(signal);
-            buffer.order(ByteOrder.LITTLE_ENDIAN)
-                    .putShort(signalSize)
-                    .put(mac)
-                    .putInt(channel)
-                    .putInt(status);
-            return buffer.array();
-        } else {
-            byte[] signal = new byte[20];
-            short signalSize = 20;
-            short maxTxPacketSize = 10000;
-            short maxRxPacketSize = 10000;
-            ByteBuffer buffer = ByteBuffer.wrap(signal);
-            buffer.order(ByteOrder.LITTLE_ENDIAN)
-                    .putShort(signalSize)
-                    .put(mac)
-                    .putInt(channel)
-                    .putInt(status)
-                    .putShort(maxTxPacketSize)
-                    .putShort(maxRxPacketSize);
-            return buffer.array();
-        }
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PhysicalLink.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PhysicalLink.java
deleted file mode 100644
index e474c69..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PhysicalLink.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth.connection;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import com.google.common.collect.Sets;
-
-import java.io.FileDescriptor;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A class represents a physical link for communications between two Bluetooth devices.
- */
-public class PhysicalLink {
-
-    // Intended to use RfcommDelegate
-    private static final Logger LOGGER = Logger.create("RfcommDelegate");
-
-    private final Object mLock;
-    // Every socket has unique FileDescriptor, so use it as socket identifier during communication
-    private final Map<FileDescriptor, RfcommSocketConnection> mConnectionLookup;
-    // Map fd of a socket to the fd of the other socket it connects to
-    private final Map<FileDescriptor, FileDescriptor> mFdMap;
-    private final Set<RfcommSocketConnection> mConnections;
-    private final AtomicBoolean mIsEncrypted;
-    private final Map<String, RfcommDelegate.Callback> mCallbacks = new HashMap<>();
-
-    public PhysicalLink(String address1, String address2) {
-        this(address1,
-                DeviceShadowEnvironmentImpl.getBlueletImpl(address1).getRfcommDelegate().mCallback,
-                address2,
-                DeviceShadowEnvironmentImpl.getBlueletImpl(address2).getRfcommDelegate().mCallback,
-                new ConcurrentHashMap<FileDescriptor, RfcommSocketConnection>(),
-                new ConcurrentHashMap<FileDescriptor, FileDescriptor>(),
-                Sets.<RfcommSocketConnection>newConcurrentHashSet());
-    }
-
-    @VisibleForTesting
-    PhysicalLink(String address1, RfcommDelegate.Callback callback1,
-            String address2, RfcommDelegate.Callback callback2,
-            Map<FileDescriptor, RfcommSocketConnection> connectionLookup,
-            Map<FileDescriptor, FileDescriptor> fdMap,
-            Set<RfcommSocketConnection> connections) {
-        mLock = new Object();
-        mCallbacks.put(address1, callback1);
-        mCallbacks.put(address2, callback2);
-        this.mConnectionLookup = connectionLookup;
-        this.mFdMap = fdMap;
-        this.mConnections = connections;
-        mIsEncrypted = new AtomicBoolean(false);
-    }
-
-    public void addConnection(FileDescriptor fd1, FileDescriptor fd2) {
-        synchronized (mLock) {
-            int oldSize = mConnections.size();
-            RfcommSocketConnection connection = new RfcommSocketConnection(
-                    FileDescriptorFactory.getInstance().getAddress(fd1),
-                    FileDescriptorFactory.getInstance().getAddress(fd2)
-            );
-            mConnections.add(connection);
-            mConnectionLookup.put(fd1, connection);
-            mConnectionLookup.put(fd2, connection);
-            mFdMap.put(fd1, fd2);
-            mFdMap.put(fd2, fd1);
-            if (oldSize == 0) {
-                onConnectionStateChange(true);
-            }
-        }
-    }
-
-    // TODO(b/79994182): see go/objecttostring-lsc
-    @SuppressWarnings("ObjectToString")
-    public void closeConnection(FileDescriptor fd) {
-        // check for early return without locking
-        if (!mConnectionLookup.containsKey(fd)) {
-            // TODO(b/79994182): FileDescriptor does not implement toString() in fd
-            LOGGER.d("Connection doesn't exist, FileDescriptor: " + fd);
-            return;
-        }
-        synchronized (mLock) {
-            RfcommSocketConnection connection = mConnectionLookup.get(fd);
-            if (connection == null) {
-                // TODO(b/79994182): FileDescriptor does not implement toString() in fd
-                LOGGER.d("Connection doesn't exist, FileDescriptor: " + fd);
-                return;
-            }
-            int oldSize = mConnections.size();
-            FileDescriptor connectingFd = mFdMap.get(fd);
-            mConnectionLookup.remove(fd);
-            mConnectionLookup.remove(connectingFd);
-            mFdMap.remove(fd);
-            mFdMap.remove(connectingFd);
-            mConnections.remove(connection);
-            if (oldSize == 1) {
-                onConnectionStateChange(false);
-            }
-        }
-    }
-
-    public RfcommSocketConnection getConnection(FileDescriptor fd) {
-        return mConnectionLookup.get(fd);
-    }
-
-    public void encrypt() {
-        mIsEncrypted.set(true);
-    }
-
-    public boolean isEncrypted() {
-        return mIsEncrypted.get();
-    }
-
-    public boolean isConnected() {
-        return !mConnections.isEmpty();
-    }
-
-    private void onConnectionStateChange(boolean isConnected) {
-        for (Entry<String, RfcommDelegate.Callback> entry : mCallbacks.entrySet()) {
-            RfcommDelegate.Callback callback = entry.getValue();
-            String localAddress = entry.getKey();
-            callback.onConnectionStateChange(getRemoteAddress(localAddress), isConnected);
-        }
-    }
-
-    private String getRemoteAddress(String address) {
-        String remoteAddress = null;
-        for (String addr : mCallbacks.keySet()) {
-            if (!addr.equals(address)) {
-                remoteAddress = addr;
-                break;
-            }
-        }
-        return remoteAddress;
-    }
-
-    /**
-     * Represents a Rfcomm socket connection between two {@link android.bluetooth.BluetoothSocket}.
-     */
-    public static class RfcommSocketConnection {
-
-        final Map<String, BlockingQueue<Integer>> mIncomingDataMap; // address : incomingData
-
-        public RfcommSocketConnection(String address1, String address2) {
-            mIncomingDataMap = new ConcurrentHashMap<>();
-            mIncomingDataMap.put(address1, new LinkedBlockingQueue<Integer>());
-            mIncomingDataMap.put(address2, new LinkedBlockingQueue<Integer>());
-        }
-
-        public void write(String address, int b) throws InterruptedException {
-            mIncomingDataMap.get(address).put(b);
-        }
-
-        public int read(String address) throws InterruptedException {
-            return mIncomingDataMap.get(address).take();
-        }
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/RfcommDelegate.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/RfcommDelegate.java
deleted file mode 100644
index 3a4fdf6..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/RfcommDelegate.java
+++ /dev/null
@@ -1,367 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth.connection;
-
-import android.os.ParcelFileDescriptor;
-import android.os.ParcelUuid;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.BluetoothConstants;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.PageScanHandler.ConnectionRequest;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.PhysicalLink.RfcommSocketConnection;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.SdpHandler.ServiceRecord;
-import com.android.libraries.testing.deviceshadower.internal.common.Interrupter;
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import com.google.errorprone.annotations.FormatMethod;
-
-import org.robolectric.util.ReflectionHelpers;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Delegate for Bluetooth Rfcommon operations, including creating service record, establishing
- * connection, and data communications.
- * <p>Socket connection with uuid is supported. Listen on port and connect to port are not
- * supported.</p>
- */
-public class RfcommDelegate {
-
-    private static final Logger LOGGER = Logger.create("RfcommDelegate");
-    private static final Object LOCK = new Object();
-
-    /**
-     * Callback for Rfcomm operations
-     */
-    public interface Callback {
-
-        void onConnectionStateChange(String remoteAddress, boolean isConnected);
-    }
-
-    public static void reset() {
-        PageScanHandler.reset();
-        FileDescriptorFactory.reset();
-    }
-
-    final Callback mCallback;
-    private final String mAddress;
-    private final Interrupter mInterrupter;
-    private final SdpHandler mSdpHandler;
-    private final PageScanHandler mPageScanHandler;
-    private final Map<String, PhysicalLink> mConnectionMap; // remoteAddress : physicalLink
-
-    public RfcommDelegate(String address, Callback callback, Interrupter interrupter) {
-        this.mAddress = address;
-        this.mCallback = callback;
-        this.mInterrupter = interrupter;
-        mSdpHandler = new SdpHandler(address);
-        mPageScanHandler = PageScanHandler.getInstance();
-        mConnectionMap = new ConcurrentHashMap<>();
-    }
-
-    @SuppressWarnings("ObjectToString")
-    public ParcelFileDescriptor createSocketChannel(String serviceName, ParcelUuid uuid) {
-        ServiceRecord record = mSdpHandler.createServiceRecord(uuid.getUuid(), serviceName);
-        if (record == null) {
-            LOGGER.e(
-                    String.format("Address %s: failed to create socket channel, uuid: %s", mAddress,
-                            uuid));
-            return null;
-        }
-        try {
-            mPageScanHandler.writePort(record.mServerSocketFd, record.mPort);
-        } catch (InterruptedException e) {
-            LOGGER.e(String.format("Address %s: failed to write port to incoming data, fd: %s",
-                    mAddress,
-                    record.mServerSocketFd), e);
-            return null;
-        }
-        return parcelFileDescriptor(record.mServerSocketFd);
-    }
-
-    @SuppressWarnings("ObjectToString")
-    public ParcelFileDescriptor connectSocket(String remoteAddress, UUID uuid) {
-        BlueletImpl remote = DeviceShadowEnvironmentImpl.getBlueletImpl(remoteAddress);
-        if (remote == null) {
-            LOGGER.e(String.format("Device %s is not defined.", remoteAddress));
-            return null;
-        }
-        ServiceRecord record = remote.getRfcommDelegate().mSdpHandler.lookupChannel(uuid);
-        if (record == null) {
-            LOGGER.e(String.format("Address %s: failed to connect socket, uuid: %s", mAddress,
-                    uuid));
-            return null;
-        }
-        FileDescriptor fd = FileDescriptorFactory.getInstance().createFileDescriptor(mAddress);
-        try {
-            mPageScanHandler.writePort(fd, record.mPort);
-        } catch (InterruptedException e) {
-            LOGGER.e(String.format("Address %s: failed to write port to incoming data, fd: %s",
-                    mAddress,
-                    fd), e);
-            return null;
-        }
-
-        // establish connection
-        try {
-            initiateConnectToServer(fd, record, remoteAddress);
-        } catch (IOException e) {
-            LOGGER.e(
-                    String.format("Address %s: fail to initiate connection to server, clientFd: %s",
-                            mAddress, fd), e);
-            return null;
-        }
-        return parcelFileDescriptor(fd);
-    }
-
-    /**
-     * Creates connection and unblocks server socket.
-     * <p>ShadowBluetoothSocket calls the method at the end of connect().</p>
-     */
-    public void finishPendingConnection(
-            String serverAddress, FileDescriptor clientFd, boolean isEncrypted) {
-        // update states
-        PhysicalLink physicalChannel = mConnectionMap.get(serverAddress);
-        if (physicalChannel == null) {
-            // use class level lock to ensure two RfcommDelegate hold reference to the same Physical
-            // Link
-            synchronized (LOCK) {
-                physicalChannel = mConnectionMap.get(serverAddress);
-                if (physicalChannel == null) {
-                    physicalChannel = new PhysicalLink(
-                            serverAddress,
-                            FileDescriptorFactory.getInstance().getAddress(clientFd));
-                    addPhysicalChannel(serverAddress, physicalChannel);
-                    BlueletImpl remote = DeviceShadowEnvironmentImpl.getBlueletImpl(serverAddress);
-                    remote.getRfcommDelegate().addPhysicalChannel(mAddress, physicalChannel);
-                }
-            }
-        }
-        physicalChannel.addConnection(clientFd, mPageScanHandler.getServerFd(clientFd));
-
-        if (isEncrypted) {
-            physicalChannel.encrypt();
-        }
-        mPageScanHandler.finishPendingConnection(clientFd);
-    }
-
-    /**
-     * Process the next {@link ConnectionRequest} to {@link android.bluetooth.BluetoothServerSocket}
-     * identified by serverSocketFd. This call will block until next connection request is
-     * available.
-     */
-    @SuppressWarnings("ObjectToString")
-    public FileDescriptor processNextConnectionRequest(FileDescriptor serverSocketFd)
-            throws IOException {
-        try {
-            return mPageScanHandler.processNextConnectionRequest(serverSocketFd);
-        } catch (InterruptedException e) {
-            throw new IOException(
-                    logError(e, "failed to process next connection request, serverSocketFd: %s",
-                            serverSocketFd),
-                    e);
-        }
-    }
-
-    /**
-     * Waits for a connection established.
-     * <p>ShadowBluetoothServerSocket calls the method at the end of accept(). Ensure that a
-     * connection is established when accept() returns.</p>
-     */
-    @SuppressWarnings("ObjectToString")
-    public void waitForConnectionEstablished(FileDescriptor clientFd) throws IOException {
-        try {
-            mPageScanHandler.waitForConnectionEstablished(clientFd);
-        } catch (InterruptedException e) {
-            throw new IOException(
-                    logError(e, "failed to wait for connection established. clientFd: %s",
-                            clientFd), e);
-        }
-    }
-
-    @SuppressWarnings("ObjectToString")
-    public void write(String remoteAddress, FileDescriptor localFd, int b)
-            throws IOException {
-        checkInterrupt();
-        RfcommSocketConnection connection =
-                mConnectionMap.get(remoteAddress).getConnection(localFd);
-        if (connection == null) {
-            throw new IOException("closed");
-        }
-        try {
-            connection.write(remoteAddress, b);
-        } catch (InterruptedException e) {
-            throw new IOException(
-                    logError(e, "failed to write to target %s, fd: %s", remoteAddress,
-                            localFd), e);
-        }
-    }
-
-    @SuppressWarnings("ObjectToString")
-    public int read(String remoteAddress, FileDescriptor localFd) throws IOException {
-        checkInterrupt();
-        // remoteAddress is null: 1. server socket, 2. client socket before connected
-        try {
-            if (remoteAddress == null) {
-                return mPageScanHandler.read(localFd);
-            }
-        } catch (InterruptedException e) {
-            throw new IOException(logError(e, "failed to read, fd: %s", localFd), e);
-        }
-
-        RfcommSocketConnection connection =
-                mConnectionMap.get(remoteAddress).getConnection(localFd);
-        if (connection == null) {
-            throw new IOException("closed");
-        }
-        try {
-            return connection.read(mAddress);
-        } catch (InterruptedException e) {
-            throw new IOException(logError(e, "failed to read, fd: %s", localFd), e);
-        }
-    }
-
-    @SuppressWarnings("ObjectToString")
-    public void shutdownInput(String remoteAddress, FileDescriptor localFd)
-            throws IOException {
-        // remoteAddress is null: 1. server socket, 2. client socket before connected
-        try {
-            if (remoteAddress == null) {
-                mPageScanHandler.write(localFd, BluetoothConstants.SOCKET_CLOSE);
-                return;
-            }
-        } catch (InterruptedException e) {
-            throw new IOException(logError(e, "failed to shutdown input. fd: %s", localFd), e);
-        }
-
-        RfcommSocketConnection connection =
-                mConnectionMap.get(remoteAddress).getConnection(localFd);
-        if (connection == null) {
-            LOGGER.d(String.format("Address %s: Connection already closed. fd: %s.", mAddress,
-                    localFd));
-            return;
-        }
-        try {
-            connection.write(mAddress, BluetoothConstants.SOCKET_CLOSE);
-        } catch (InterruptedException e) {
-            throw new IOException(logError(e, "failed to shutdown input. fd: %s", localFd), e);
-        }
-    }
-
-    @SuppressWarnings("ObjectToString")
-    public void shutdownOutput(String remoteAddress, FileDescriptor localFd)
-            throws IOException {
-        RfcommSocketConnection connection =
-                mConnectionMap.get(remoteAddress).getConnection(localFd);
-        if (connection == null) {
-            LOGGER.d(String.format("Address %s: Connection already closed. fd: %s.", mAddress,
-                    localFd));
-            return;
-        }
-        try {
-            connection.write(remoteAddress, BluetoothConstants.SOCKET_CLOSE);
-        } catch (InterruptedException e) {
-            throw new IOException(logError(e, "failed to shutdown output. fd: %s", localFd), e);
-        }
-    }
-
-    @SuppressWarnings("ObjectToString")
-    public void closeServerSocket(FileDescriptor serverSocketFd) throws IOException {
-        // remove service record
-        UUID uuid = mSdpHandler.getUuid(serverSocketFd);
-        mSdpHandler.removeServiceRecord(uuid);
-        // unblock accept()
-        try {
-            mPageScanHandler.cancelServerSocket(serverSocketFd);
-        } catch (InterruptedException e) {
-            throw new IOException(
-                    logError(e, "failed to cancel server socket, serverSocketFd: %s",
-                            serverSocketFd),
-                    e);
-        }
-    }
-
-    public FileDescriptor getServerFd(FileDescriptor clientFd) {
-        return mPageScanHandler.getServerFd(clientFd);
-    }
-
-    @VisibleForTesting
-    public void addPhysicalChannel(String remoteAddress, PhysicalLink channel) {
-        mConnectionMap.put(remoteAddress, channel);
-    }
-
-    @SuppressWarnings("ObjectToString")
-    public void initiateConnectToClient(FileDescriptor clientFd, int port)
-            throws IOException {
-        checkInterrupt();
-        String clientAddress = FileDescriptorFactory.getInstance().getAddress(clientFd);
-        LOGGER.d(String.format("Address %s: init connection to %s, clientFd: %s",
-                mAddress, clientAddress, clientFd));
-        try {
-            mPageScanHandler.writeInitialConnectionInfo(clientFd, mAddress, port);
-        } catch (InterruptedException e) {
-            throw new IOException(
-                    logError(e,
-                            "failed to write initial connection info to %s, clientFd: %s",
-                            clientAddress, clientFd),
-                    e);
-        }
-    }
-
-    @SuppressWarnings("ObjectToString")
-    private void initiateConnectToServer(FileDescriptor clientFd, ServiceRecord serviceRecord,
-            String serverAddress) throws IOException {
-        checkInterrupt();
-        LOGGER.d(
-                String.format("Address %s: init connection to %s, serverSocketFd: %s, clientFd: %s",
-                        mAddress, serverAddress, serviceRecord.mServerSocketFd, clientFd));
-        try {
-            ConnectionRequest request = new ConnectionRequest(clientFd, mAddress, serverAddress,
-                    serviceRecord.mPort);
-            mPageScanHandler.postConnectionRequest(serviceRecord.mServerSocketFd, request);
-        } catch (InterruptedException e) {
-            throw new IOException(
-                    logError(e,
-                            "failed to post connection request, serverSocketFd: %s, "
-                                    + "clientFd: %s",
-                            serviceRecord.mServerSocketFd, clientFd),
-                    e);
-        }
-    }
-
-    public void checkInterrupt() throws IOException {
-        mInterrupter.checkInterrupt();
-    }
-
-    private ParcelFileDescriptor parcelFileDescriptor(FileDescriptor fd) {
-        return ReflectionHelpers.callConstructor(ParcelFileDescriptor.class,
-                ReflectionHelpers.ClassParameter.from(FileDescriptor.class, fd));
-    }
-
-    @FormatMethod
-    private String logError(Exception e, String msgTmpl, Object... args) {
-        String errMsg = String.format("Address %s: ", mAddress) + String.format(msgTmpl, args);
-        LOGGER.e(errMsg, e);
-        return errMsg;
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/SdpHandler.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/SdpHandler.java
deleted file mode 100644
index dbe8651..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/SdpHandler.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.bluetooth.connection;
-
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import com.google.common.base.Preconditions;
-import com.google.common.collect.Sets;
-
-import java.io.FileDescriptor;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Encapsulates SDP operations including creating service record and allocating channel.
- * <p>Listen on port and connect on port are not supported. </p>
- */
-public class SdpHandler {
-
-    // intended to use "RfcommDelegate"
-    private static final Logger LOGGER = Logger.create("RfcommDelegate");
-
-    private final Object mLock;
-    private final String mAddress;
-    private final Map<UUID, ServiceRecord> mServiceRecords;
-    private final Map<FileDescriptor, UUID> mFdUuidMap;
-    private final Set<Integer> mAvailablePortPool;
-    private final Set<Integer> mInUsePortPool;
-
-    public SdpHandler(String address) {
-        mLock = new Object();
-        this.mAddress = address;
-        mServiceRecords = new ConcurrentHashMap<>();
-        mFdUuidMap = new ConcurrentHashMap<>();
-        mAvailablePortPool = Sets.newConcurrentHashSet();
-        mInUsePortPool = Sets.newConcurrentHashSet();
-        // 1 to 30 are valid RFCOMM port
-        for (int i = 1; i <= 30; i++) {
-            mAvailablePortPool.add(i);
-        }
-    }
-
-    public ServiceRecord createServiceRecord(UUID uuid, String serviceName) {
-        Preconditions.checkNotNull(uuid);
-        LOGGER.d(String.format("Address %s: createServiceRecord with uuid %s", mAddress, uuid));
-        if (isUuidRegistered(uuid) || !checkChannelAvailability()) {
-            return null;
-        }
-        synchronized (mLock) {
-            // ensure uuid is not registered and there's available channel
-            if (isUuidRegistered(uuid) || !checkChannelAvailability()) {
-                return null;
-            }
-            Iterator<Integer> available = mAvailablePortPool.iterator();
-            int port = available.next();
-            mAvailablePortPool.remove(port);
-            mInUsePortPool.add(port);
-            ServiceRecord record = new ServiceRecord(mAddress, serviceName, port);
-            mServiceRecords.put(uuid, record);
-            mFdUuidMap.put(record.mServerSocketFd, uuid);
-            PageScanHandler.getInstance().addServerSocket(record.mServerSocketFd);
-            return record;
-        }
-    }
-
-    public void removeServiceRecord(UUID uuid) {
-        Preconditions.checkNotNull(uuid);
-        LOGGER.d(String.format("Address %s: removeServiceRecord with uuid %s", mAddress, uuid));
-        if (!isUuidRegistered(uuid)) {
-            return;
-        }
-        synchronized (mLock) {
-            if (!isUuidRegistered(uuid)) {
-                return;
-            }
-            ServiceRecord record = mServiceRecords.get(uuid);
-            mServiceRecords.remove(uuid);
-            mInUsePortPool.remove(record.mPort);
-            mAvailablePortPool.add(record.mPort);
-            mFdUuidMap.remove(record.mServerSocketFd);
-        }
-    }
-
-    public ServiceRecord lookupChannel(UUID uuid) {
-        ServiceRecord record = mServiceRecords.get(uuid);
-        if (record == null) {
-            LOGGER.e(String.format("Address %s: uuid %s not registered.", mAddress, uuid));
-        }
-        return record;
-    }
-
-    public UUID getUuid(FileDescriptor serverSocketFd) {
-        return mFdUuidMap.get(serverSocketFd);
-    }
-
-    private boolean isUuidRegistered(UUID uuid) {
-        if (mServiceRecords.containsKey(uuid)) {
-            LOGGER.d(String.format("Address %s: Uuid %s in use.", mAddress, uuid));
-            return true;
-        }
-        LOGGER.d(String.format("Address %s: Uuid %s not registered.", mAddress, uuid));
-        return false;
-    }
-
-    private boolean checkChannelAvailability() {
-        if (mAvailablePortPool.isEmpty()) {
-            LOGGER.e(String.format("Address %s: No available channel.", mAddress));
-            return false;
-        }
-        return true;
-    }
-
-    static class ServiceRecord {
-
-        final FileDescriptor mServerSocketFd;
-        final String mServiceName;
-        final int mPort;
-
-        ServiceRecord(String address, String serviceName, int port) {
-            mServerSocketFd = FileDescriptorFactory.getInstance().createFileDescriptor(address);
-            this.mServiceName = serviceName;
-            this.mPort = port;
-        }
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/BroadcastManager.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/BroadcastManager.java
deleted file mode 100644
index 0b309ae..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/BroadcastManager.java
+++ /dev/null
@@ -1,526 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.common;
-
-import android.content.BroadcastReceiver;
-import android.content.BroadcastReceiver.PendingResult;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Build.VERSION;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import com.google.common.base.Function;
-import com.google.common.base.Preconditions;
-import com.google.common.util.concurrent.AsyncFunction;
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.MoreExecutors;
-
-import org.robolectric.Shadows;
-import org.robolectric.shadows.ShadowApplication;
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.GuardedBy;
-
-/**
- * Manager for broadcasting of one virtual Device Shadower device.
- *
- * <p>Inspired by {@link ShadowApplication} and {@link LocalBroadcastManager}.
- * <li>Broadcast permission is not supported until manifest is supported.
- * <li>Send Broadcast is asynchronous.
- */
-public class BroadcastManager {
-
-    private static final Logger LOGGER = Logger.create("BroadcastManager");
-
-    private static final Comparator<ReceiverRecord> RECEIVER_RECORD_COMPARATOR =
-            new Comparator<ReceiverRecord>() {
-                @Override
-                public int compare(ReceiverRecord o1, ReceiverRecord o2) {
-                    return o2.mIntentFilter.getPriority() - o1.mIntentFilter.getPriority();
-                }
-            };
-
-    private final Scheduler mScheduler;
-    private final Map<String, Intent> mStickyIntents;
-
-    @GuardedBy("mRegisteredReceivers")
-    private final Map<BroadcastReceiver, Set<String>> mRegisteredReceivers;
-
-    @GuardedBy("mRegisteredReceivers")
-    private final Map<String, List<ReceiverRecord>> mActions;
-
-    public BroadcastManager(Scheduler scheduler) {
-        this(
-                scheduler,
-                new HashMap<String, Intent>(),
-                new HashMap<BroadcastReceiver, Set<String>>(),
-                new HashMap<String, List<ReceiverRecord>>());
-    }
-
-    @VisibleForTesting
-    BroadcastManager(
-            Scheduler scheduler,
-            Map<String, Intent> stickyIntents,
-            Map<BroadcastReceiver, Set<String>> registeredReceivers,
-            Map<String, List<ReceiverRecord>> actions) {
-        this.mScheduler = scheduler;
-        this.mStickyIntents = stickyIntents;
-        this.mRegisteredReceivers = registeredReceivers;
-        this.mActions = actions;
-    }
-
-    /**
-     * Registers a {@link BroadcastReceiver} with given {@link Context}.
-     *
-     * @see Context#registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)
-     */
-    @Nullable
-    public Intent registerReceiver(
-            @Nullable BroadcastReceiver receiver,
-            IntentFilter filter,
-            @Nullable String broadcastPermission,
-            @Nullable Handler handler,
-            Context context) {
-        // Ignore broadcastPermission before fully supporting manifest
-        Preconditions.checkNotNull(filter);
-        Preconditions.checkNotNull(context);
-        if (receiver != null) {
-            synchronized (mRegisteredReceivers) {
-                ReceiverRecord receiverRecord = new ReceiverRecord(receiver, filter, context,
-                        handler);
-                Set<String> actionSet = mRegisteredReceivers.get(receiver);
-                if (actionSet == null) {
-                    actionSet = new HashSet<>();
-                    mRegisteredReceivers.put(receiver, actionSet);
-                }
-                for (int i = 0; i < filter.countActions(); i++) {
-                    String action = filter.getAction(i);
-                    actionSet.add(action);
-                    List<ReceiverRecord> receiverRecords = mActions.get(action);
-                    if (receiverRecords == null) {
-                        receiverRecords = new ArrayList<>();
-                        mActions.put(action, receiverRecords);
-                    }
-                    receiverRecords.add(receiverRecord);
-                }
-            }
-        }
-        return processStickyIntents(receiver, filter, context);
-    }
-
-    // Broadcast all sticky intents matching the given IntentFilter.
-    @SuppressWarnings("FutureReturnValueIgnored")
-    @Nullable
-    private Intent processStickyIntents(
-            @Nullable final BroadcastReceiver receiver,
-            IntentFilter intentFilter,
-            final Context context) {
-        Intent result = null;
-        final List<Intent> matchedIntents = new ArrayList<>();
-        for (Intent intent : mStickyIntents.values()) {
-            if (match(intentFilter, intent)) {
-                if (result == null) {
-                    result = intent;
-                }
-                if (receiver == null) {
-                    return result;
-                }
-                matchedIntents.add(intent);
-            }
-        }
-        if (!matchedIntents.isEmpty()) {
-            mScheduler.post(
-                    NamedRunnable.create(
-                            "Broadcast.processStickyIntents",
-                            () -> {
-                                for (Intent intent : matchedIntents) {
-                                    receiver.onReceive(context, intent);
-                                }
-                            }));
-        }
-        return result;
-    }
-
-    /**
-     * Unregisters a {@link BroadcastReceiver}.
-     *
-     * @see Context#unregisterReceiver(BroadcastReceiver)
-     */
-    public void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
-        synchronized (mRegisteredReceivers) {
-            if (!mRegisteredReceivers.containsKey(broadcastReceiver)) {
-                LOGGER.w("Receiver not registered: " + broadcastReceiver);
-                return;
-            }
-            Set<String> actionSet = mRegisteredReceivers.remove(broadcastReceiver);
-            for (String action : actionSet) {
-                List<ReceiverRecord> receiverRecords = mActions.get(action);
-                Iterator<ReceiverRecord> iterator = receiverRecords.iterator();
-                while (iterator.hasNext()) {
-                    if (iterator.next().mBroadcastReceiver == broadcastReceiver) {
-                        iterator.remove();
-                    }
-                }
-                if (receiverRecords.isEmpty()) {
-                    mActions.remove(action);
-                }
-            }
-        }
-    }
-
-    /**
-     * Sends sticky broadcast with given {@link Intent}. This call is asynchronous.
-     *
-     * @see Context#sendStickyBroadcast(Intent)
-     */
-    public void sendStickyBroadcast(Intent intent) {
-        mStickyIntents.put(intent.getAction(), intent);
-        sendBroadcast(intent, null /* broadcastPermission */);
-    }
-
-    /**
-     * Sends broadcast with given {@link Intent}. Receiver permission is not supported. This call is
-     * asynchronous.
-     *
-     * @see Context#sendBroadcast(Intent, String)
-     */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void sendBroadcast(final Intent intent, @Nullable String receiverPermission) {
-        // Ignore permission matching before fully supporting manifest
-        final List<ReceiverRecord> receivers =
-                getMatchingReceivers(intent, false /* isOrdered */);
-        if (receivers.isEmpty()) {
-            return;
-        }
-        mScheduler.post(
-                NamedRunnable.create(
-                        "Broadcast.sendBroadcast",
-                        () -> {
-                            for (ReceiverRecord receiverRecord : receivers) {
-                                // Hacky: Call the shadow method, otherwise abort() NPEs after
-                                // calling onReceive().
-                                // TODO(b/200231384): Sending these, via context.sendBroadcast(),
-                                //  won't NPE...but it may not be possible on each simulated
-                                //  "device"'s main thread. Check if possible.
-                                BroadcastReceiver broadcastReceiver =
-                                        receiverRecord.mBroadcastReceiver;
-                                Shadows.shadowOf(broadcastReceiver)
-                                        .onReceive(receiverRecord.mContext, intent, /*abort=*/
-                                                new AtomicBoolean(false));
-                            }
-                        }));
-    }
-
-    /**
-     * Sends ordered broadcast with given {@link Intent}. Receiver permission is not supported. This
-     * call is asynchronous.
-     *
-     * @see Context#sendOrderedBroadcast(Intent, String)
-     */
-    public void sendOrderedBroadcast(Intent intent, @Nullable String receiverPermission) {
-        sendOrderedBroadcast(
-                intent,
-                receiverPermission,
-                null /* resultReceiver */,
-                null /* handler */,
-                0 /* initialCode */,
-                null /* initialData */,
-                null /* initialExtras */,
-                null /* context */);
-    }
-
-    /**
-     * Sends ordered broadcast with given {@link Intent} and result {@link BroadcastReceiver}.
-     * Receiver permission is not supported. This call is asynchronous.
-     *
-     * @see Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String,
-     * Bundle)
-     */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void sendOrderedBroadcast(
-            final Intent intent,
-            @Nullable String receiverPermission,
-            @Nullable BroadcastReceiver resultReceiver,
-            @Nullable Handler handler,
-            int initialCode,
-            @Nullable String initialData,
-            @Nullable Bundle initialExtras,
-            @Nullable Context context) {
-        // Ignore permission matching before fully supporting manifest
-        final List<ReceiverRecord> receivers =
-                getMatchingReceivers(intent, true /* isOrdered */);
-        if (receivers.isEmpty()) {
-            return;
-        }
-        if (resultReceiver != null) {
-            receivers.add(
-                    new ReceiverRecord(
-                            resultReceiver, null /* intentFilter */, context, handler));
-        }
-        mScheduler.post(
-                NamedRunnable.create(
-                        "Broadcast.sendOrderedBroadcast",
-                        () -> {
-                            postOrderedIntent(
-                                    receivers,
-                                    intent,
-                                    0 /* initialCode */,
-                                    null /* initialData */,
-                                    null /* initialExtras */);
-                        }));
-    }
-
-    @VisibleForTesting
-    void postOrderedIntent(
-            List<ReceiverRecord> receivers,
-            final Intent intent,
-            int initialCode,
-            @Nullable String initialData,
-            @Nullable Bundle initialExtras) {
-        final AtomicBoolean abort = new AtomicBoolean(false);
-        ListenableFuture<BroadcastResult> resultFuture =
-                Futures.immediateFuture(
-                        new BroadcastResult(initialCode, initialData, initialExtras));
-
-        for (ReceiverRecord receiverRecord : receivers) {
-            final BroadcastReceiver receiver = receiverRecord.mBroadcastReceiver;
-            final Context context = receiverRecord.mContext;
-            resultFuture =
-                    Futures.transformAsync(
-                            resultFuture,
-                            new AsyncFunction<BroadcastResult, BroadcastResult>() {
-                                @Override
-                                public ListenableFuture<BroadcastResult> apply(
-                                        BroadcastResult input) {
-                                    PendingResult result = newPendingResult(
-                                            input.mCode, input.mData, input.mExtras,
-                                            true /* isOrdered */);
-                                    ReflectionHelpers.callInstanceMethod(
-                                            receiver, "setPendingResult",
-                                            ClassParameter.from(PendingResult.class, result));
-                                    Shadows.shadowOf(receiver).onReceive(context, intent, abort);
-                                    return BroadcastResult.transform(result);
-                                }
-                            },
-                            MoreExecutors.directExecutor());
-        }
-        Futures.addCallback(
-                resultFuture,
-                new FutureCallback<BroadcastResult>() {
-                    @Override
-                    public void onSuccess(BroadcastResult result) {
-                        return;
-                    }
-
-                    @Override
-                    public void onFailure(Throwable t) {
-                        throw new RuntimeException(t);
-                    }
-                },
-                MoreExecutors.directExecutor());
-    }
-
-    private List<ReceiverRecord> getMatchingReceivers(Intent intent, boolean isOrdered) {
-        synchronized (mRegisteredReceivers) {
-            List<ReceiverRecord> result = new ArrayList<>();
-            if (!mActions.containsKey(intent.getAction())) {
-                return result;
-            }
-            Iterator<ReceiverRecord> iterator = mActions.get(intent.getAction()).iterator();
-            while (iterator.hasNext()) {
-                ReceiverRecord next = iterator.next();
-                if (match(next.mIntentFilter, intent)) {
-                    result.add(next);
-                }
-            }
-            if (isOrdered) {
-                Collections.sort(result, RECEIVER_RECORD_COMPARATOR);
-            }
-            return result;
-        }
-    }
-
-    private boolean match(IntentFilter intentFilter, Intent intent) {
-        // Action test
-        if (!intentFilter.matchAction(intent.getAction())) {
-            return false;
-        }
-        // Category test
-        if (intentFilter.matchCategories(intent.getCategories()) != null) {
-            return false;
-        }
-        // Data test
-        int matchResult =
-                intentFilter.matchData(intent.getType(), intent.getScheme(), intent.getData());
-        return matchResult != IntentFilter.NO_MATCH_TYPE
-                && matchResult != IntentFilter.NO_MATCH_DATA;
-    }
-
-    private static PendingResult newPendingResult(
-            int resultCode, String resultData, Bundle resultExtras, boolean isOrdered) {
-        ClassParameter<?>[] parameters;
-        // PendingResult constructor takes different parameters in different SDK levels.
-        if (VERSION.SDK_INT < 17) {
-            parameters =
-                    ClassParameter.fromComponentLists(
-                            new Class<?>[]{
-                                    int.class,
-                                    String.class,
-                                    Bundle.class,
-                                    int.class,
-                                    boolean.class,
-                                    boolean.class,
-                                    IBinder.class
-                            },
-                            new Object[]{
-                                    resultCode,
-                                    resultData,
-                                    resultExtras,
-                                    0 /* type */,
-                                    isOrdered,
-                                    false /* sticky */,
-                                    null /* IBinder */
-                            });
-        } else if (VERSION.SDK_INT < 23) {
-            parameters =
-                    ClassParameter.fromComponentLists(
-                            new Class<?>[]{
-                                    int.class,
-                                    String.class,
-                                    Bundle.class,
-                                    int.class,
-                                    boolean.class,
-                                    boolean.class,
-                                    IBinder.class,
-                                    int.class
-                            },
-                            new Object[]{
-                                    resultCode,
-                                    resultData,
-                                    resultExtras,
-                                    0 /* type */,
-                                    isOrdered,
-                                    false /* sticky */,
-                                    null /* IBinder */,
-                                    0 /* userId */
-                            });
-        } else {
-            parameters =
-                    ClassParameter.fromComponentLists(
-                            new Class<?>[]{
-                                    int.class,
-                                    String.class,
-                                    Bundle.class,
-                                    int.class,
-                                    boolean.class,
-                                    boolean.class,
-                                    IBinder.class,
-                                    int.class,
-                                    int.class
-                            },
-                            new Object[]{
-                                    resultCode,
-                                    resultData,
-                                    resultExtras,
-                                    0 /* type */,
-                                    isOrdered,
-                                    false /* sticky */,
-                                    null /* IBinder */,
-                                    0 /* userId */,
-                                    0 /* flags */
-                            });
-        }
-        return ReflectionHelpers.callConstructor(PendingResult.class, parameters);
-    }
-
-    /**
-     * Holder of broadcast result from previous receiver.
-     */
-    private static final class BroadcastResult {
-
-        private final int mCode;
-        private final String mData;
-        private final Bundle mExtras;
-
-        BroadcastResult(int code, String data, Bundle extras) {
-            this.mCode = code;
-            this.mData = data;
-            this.mExtras = extras;
-        }
-
-        private static ListenableFuture<BroadcastResult> transform(PendingResult result) {
-            return Futures.transform(
-                    Shadows.shadowOf(result).getFuture(),
-                    new Function<PendingResult, BroadcastResult>() {
-                        @Override
-                        public BroadcastResult apply(PendingResult input) {
-                            return new BroadcastResult(
-                                    input.getResultCode(), input.getResultData(),
-                                    input.getResultExtras(false));
-                        }
-                    },
-                    MoreExecutors.directExecutor());
-        }
-    }
-
-    /**
-     * Information of a registered BroadcastReceiver.
-     */
-    @VisibleForTesting
-    static final class ReceiverRecord {
-
-        final BroadcastReceiver mBroadcastReceiver;
-        final IntentFilter mIntentFilter;
-        final Context mContext;
-        final Handler mHandler;
-
-        @VisibleForTesting
-        ReceiverRecord(
-                BroadcastReceiver broadcastReceiver,
-                IntentFilter intentFilter,
-                Context context,
-                Handler handler) {
-            this.mBroadcastReceiver = broadcastReceiver;
-            this.mIntentFilter = intentFilter;
-            this.mContext = context;
-            this.mHandler = handler;
-        }
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/ContentDatabase.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/ContentDatabase.java
deleted file mode 100644
index 1f4d778..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/ContentDatabase.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.common;
-
-import android.database.Cursor;
-
-import org.robolectric.fakes.RoboCursor;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Simulate Sqlite database for Android content provider.
- */
-public class ContentDatabase {
-
-    private final List<String> mColumnNames;
-    private final List<List<Object>> mData;
-
-    public ContentDatabase(String... names) {
-        mColumnNames = Arrays.asList(names);
-        mData = new ArrayList<>();
-    }
-
-    public void addData(Object... items) {
-        mData.add(Arrays.asList(items));
-    }
-
-    public Cursor getCursor() {
-        RoboCursor cursor = new RoboCursor();
-        cursor.setColumnNames(mColumnNames);
-        Object[][] dataArr = new Object[mData.size()][mColumnNames.size()];
-        for (int i = 0; i < mData.size(); i++) {
-            dataArr[i] = new Object[mColumnNames.size()];
-            mData.get(i).toArray(dataArr[i]);
-        }
-        cursor.setResults(dataArr);
-        return cursor;
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Interrupter.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Interrupter.java
deleted file mode 100644
index 66e9cb0..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Interrupter.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.common;
-
-import com.android.libraries.testing.deviceshadower.Enums;
-
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Interrupter sets and checks interruptible point, and interrupt operation by throwing
- * IOException.
- */
-public class Interrupter {
-
-    private final InheritableThreadLocal<Integer> mCurrentIdentifier;
-    private int mInterruptIdentifier;
-
-    private final Set<Enums.Operation> mInterruptOperations = new HashSet<>();
-
-    public Interrupter() {
-        mCurrentIdentifier = new InheritableThreadLocal<Integer>() {
-            @Override
-            protected Integer initialValue() {
-                return -1;
-            }
-        };
-    }
-
-    public void checkInterrupt() throws IOException {
-        if (mCurrentIdentifier.get() == mInterruptIdentifier) {
-            throw new IOException(
-                    "Bluetooth interrupted at identifier: " + mCurrentIdentifier.get());
-        }
-    }
-
-    public void setInterruptible(int identifier) {
-        mCurrentIdentifier.set(identifier);
-    }
-
-    public void interrupt(int identifier) {
-        mInterruptIdentifier = identifier;
-    }
-
-    public void addInterruptOperation(Enums.Operation operation) {
-        mInterruptOperations.add(operation);
-    }
-
-    public boolean shouldInterrupt(Enums.Operation operation) {
-        return mInterruptOperations.contains(operation);
-    }
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/NamedRunnable.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/NamedRunnable.java
deleted file mode 100644
index 4e84d71..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/NamedRunnable.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.common;
-
-/**
- * Runnable with a name defined.
- */
-public abstract class NamedRunnable implements Runnable {
-
-    private final String mName;
-
-    private NamedRunnable(String name) {
-        this.mName = name;
-    }
-
-    public static NamedRunnable create(String name, Runnable runnable) {
-        return new NamedRunnable(name) {
-            @Override
-            public void run() {
-                runnable.run();
-            }
-        };
-    }
-
-    @Override
-    public String toString() {
-        return mName;
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Scheduler.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Scheduler.java
deleted file mode 100644
index 96e9b15..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Scheduler.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.common;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.concurrent.GuardedBy;
-
-/**
- * Scheduler to post runnables to a single thread.
- */
-public class Scheduler {
-
-    private static final Logger LOGGER = Logger.create("Scheduler");
-
-    @GuardedBy("Scheduler.class")
-    private static int sTotalRunnables = 0;
-
-    private static CountDownLatch sCompleteLatch;
-
-    public Scheduler() {
-        this(null);
-    }
-
-    public Scheduler(String name) {
-        mExecutor =
-                Executors.newSingleThreadExecutor(
-                        r -> {
-                            Thread thread = Executors.defaultThreadFactory().newThread(r);
-                            if (name != null) {
-                                thread.setName(name);
-                            }
-                            return thread;
-                        });
-    }
-
-    public static boolean await(long timeoutMillis) throws InterruptedException {
-
-        synchronized (Scheduler.class) {
-            if (isComplete()) {
-                return true;
-            }
-            if (sCompleteLatch == null) {
-                sCompleteLatch = new CountDownLatch(1);
-            }
-        }
-
-        // TODO(b/200231384): solve potential NPE caused by race condition.
-        boolean result = sCompleteLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
-        synchronized (Scheduler.class) {
-            sCompleteLatch = null;
-        }
-        return result;
-    }
-
-    private final ExecutorService mExecutor;
-
-    @GuardedBy("this")
-    private final List<ScheduledRunnable> mRunnables = new ArrayList<>();
-
-    @GuardedBy("this")
-    private long mCurrentTimeMillis = 0;
-
-    @GuardedBy("this")
-    private List<ScheduledRunnable> mRunningRunnables = new ArrayList<>();
-
-    /**
-     * Post a {@link NamedRunnable} to scheduler.
-     *
-     * <p>Return value can be ignored because exception will be handled by {@link
-     * DeviceShadowEnvironmentImpl#catchInternalException}.
-     */
-    // @CanIgnoreReturnValue
-    public synchronized Future<?> post(NamedRunnable r) {
-        synchronized (Scheduler.class) {
-            sTotalRunnables++;
-        }
-        advance(0);
-        return mExecutor.submit(new ScheduledRunnable(r, mCurrentTimeMillis).mRunnable);
-    }
-
-    public synchronized void post(NamedRunnable r, long delayMillis) {
-        synchronized (Scheduler.class) {
-            sTotalRunnables++;
-        }
-        addRunnables(new ScheduledRunnable(r, mCurrentTimeMillis + delayMillis));
-        advance(0);
-    }
-
-    public synchronized void shutdown() {
-        mExecutor.shutdown();
-    }
-
-    @VisibleForTesting
-    synchronized void advance(long durationMillis) {
-        mCurrentTimeMillis += durationMillis;
-        while (mRunnables.size() > 0) {
-            ScheduledRunnable r = mRunnables.get(0);
-            if (r.mTimeMillis <= mCurrentTimeMillis) {
-                mRunnables.remove(0);
-                mExecutor.execute(r.mRunnable);
-            } else {
-                break;
-            }
-        }
-    }
-
-    private synchronized void addRunnables(ScheduledRunnable r) {
-        int index = 0;
-        while (index < mRunnables.size() && mRunnables.get(index).mTimeMillis <= r.mTimeMillis) {
-            index++;
-        }
-        mRunnables.add(index, r);
-    }
-
-    @VisibleForTesting
-    static synchronized boolean isComplete() {
-        return sTotalRunnables == 0;
-    }
-
-    // Can only be called by DeviceShadowEnvironmentImpl when reset.
-    public static synchronized void clear() {
-        sTotalRunnables = 0;
-    }
-
-    class ScheduledRunnable {
-
-        final NamedRunnable mRunnable;
-        final long mTimeMillis;
-
-        ScheduledRunnable(final NamedRunnable r, long timeMillis) {
-            this.mTimeMillis = timeMillis;
-            this.mRunnable =
-                    NamedRunnable.create(
-                            r.toString(),
-                            () -> {
-                                synchronized (Scheduler.this) {
-                                    Scheduler.this.mRunningRunnables.add(ScheduledRunnable.this);
-                                }
-
-                                try {
-                                    r.run();
-                                } catch (Exception e) {
-                                    LOGGER.e("Error in scheduler runnable " + r, e);
-                                    DeviceShadowEnvironmentImpl.catchInternalException(e);
-                                }
-
-                                synchronized (Scheduler.this) {
-                                    // Remove the last one.
-                                    Scheduler.this.mRunningRunnables.remove(
-                                            Scheduler.this.mRunningRunnables.size() - 1);
-                                }
-
-                                // If this is last runnable,
-                                // When this section runs before await:
-                                //   totalRunnable will be 0, await will return directly.
-                                // When this section runs after await:
-                                //   latch will not be null, count down will terminate await.
-
-                                // TODO(b/200231384): when there are two threads running at same
-                                // time, there will be a case when totalRunnable is 0, but another
-                                // thread pending to acquire Scheduler.class lock to post a
-                                // runnable. Hence, await here might not be correct in this case.
-                                synchronized (Scheduler.class) {
-                                    sTotalRunnables--;
-                                    if (isComplete()) {
-                                        if (sCompleteLatch != null) {
-                                            sCompleteLatch.countDown();
-                                        }
-                                    }
-                                }
-                            });
-        }
-
-        @Override
-        public String toString() {
-            return mRunnable.toString();
-        }
-    }
-
-    @Override
-    public synchronized String toString() {
-        return String.format(
-                "\t%d scheduled runnables %s\n\t%d still running or aborted %s",
-                mRunnables.size(), mRunnables, mRunningRunnables.size(), mRunningRunnables);
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/INfcAdapterImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/INfcAdapterImpl.java
deleted file mode 100644
index 01dcac2..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/INfcAdapterImpl.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.nfc;
-
-import android.nfc.IAppCallback;
-import android.nfc.INfcAdapter;
-
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-
-/**
- * Implementation of INfcAdapter
- */
-public class INfcAdapterImpl implements INfcAdapter {
-
-    public INfcAdapterImpl() {
-    }
-
-    @Override
-    public void setAppCallback(IAppCallback callback) {
-        DeviceShadowEnvironmentImpl.getLocalNfcletImpl().mAppCallback = callback;
-    }
-
-    @Override
-    public boolean enable() {
-        return DeviceShadowEnvironmentImpl.getLocalNfcletImpl().enable();
-    }
-
-    @Override
-    public boolean disable(boolean saveState) {
-        // We do not need to save state because test only run once.
-        return DeviceShadowEnvironmentImpl.getLocalNfcletImpl().disable();
-    }
-
-    @Override
-    public int getState() {
-        return DeviceShadowEnvironmentImpl.getLocalNfcletImpl().getState();
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/NfcletImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/NfcletImpl.java
deleted file mode 100644
index 137f6b8..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/NfcletImpl.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.nfc;
-
-import android.content.Intent;
-import android.nfc.BeamShareData;
-import android.nfc.IAppCallback;
-import android.nfc.NdefMessage;
-import android.nfc.NfcAdapter;
-
-import com.android.libraries.testing.deviceshadower.Enums.NfcOperation;
-import com.android.libraries.testing.deviceshadower.Nfclet;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.common.Interrupter;
-import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
-
-import javax.annotation.concurrent.GuardedBy;
-
-/**
- * Implementation of Nfclet.
- */
-public class NfcletImpl implements Nfclet {
-
-    private static final Logger LOGGER = Logger.create("NfcletImpl");
-
-    IAppCallback mAppCallback;
-    private final Interrupter mInterrupter;
-
-    @GuardedBy("this")
-    private int mCurrentState;
-
-    public NfcletImpl() {
-        mInterrupter = new Interrupter();
-        mCurrentState = NfcAdapter.STATE_OFF;
-    }
-
-    public void onNear(NfcletImpl remote) {
-        if (remote.mAppCallback != null) {
-            LOGGER.v("NFC receiver get beam share data from remote");
-            BeamShareData data = remote.mAppCallback.createBeamShareData();
-            DeviceShadowEnvironmentImpl.getLocalDeviceletImpl().getBroadcastManager()
-                    .sendBroadcast(createNdefDiscoveredIntent(data), null);
-        }
-        if (mAppCallback != null) {
-            LOGGER.v("NFC sender onNdefPushComplete");
-            mAppCallback.onNdefPushComplete();
-        }
-    }
-
-    public synchronized int getState() {
-        return mCurrentState;
-    }
-
-    public boolean enable() {
-        if (shouldInterrupt(NfcOperation.ENABLE)) {
-            return false;
-        }
-        LOGGER.v("Enable NFC Adapter");
-        updateState(NfcAdapter.STATE_TURNING_ON);
-        updateState(NfcAdapter.STATE_ON);
-        return true;
-    }
-
-    public boolean disable() {
-        if (shouldInterrupt(NfcOperation.DISABLE)) {
-            return false;
-        }
-        LOGGER.v("Disable NFC Adapter");
-        updateState(NfcAdapter.STATE_TURNING_OFF);
-        updateState(NfcAdapter.STATE_OFF);
-        return true;
-    }
-
-    @Override
-    public synchronized Nfclet setInitialState(int state) {
-        mCurrentState = state;
-        return this;
-    }
-
-    @Override
-    public Nfclet setInterruptOperation(NfcOperation operation) {
-        mInterrupter.addInterruptOperation(operation);
-        return this;
-    }
-
-    public boolean shouldInterrupt(NfcOperation operation) {
-        return mInterrupter.shouldInterrupt(operation);
-    }
-
-    private synchronized void updateState(int state) {
-        if (mCurrentState != state) {
-            mCurrentState = state;
-            DeviceShadowEnvironmentImpl.getLocalDeviceletImpl().getBroadcastManager()
-                    .sendBroadcast(createAdapterStateChangedIntent(state), null);
-        }
-    }
-
-    private Intent createAdapterStateChangedIntent(int state) {
-        Intent intent = new Intent(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED);
-        intent.putExtra(NfcAdapter.EXTRA_ADAPTER_STATE, state);
-        return intent;
-    }
-
-    private Intent createNdefDiscoveredIntent(BeamShareData data) {
-        Intent intent = new Intent();
-        intent.setAction(NfcAdapter.ACTION_NDEF_DISCOVERED);
-        intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[]{data.ndefMessage});
-        // TODO(b/200231384): uncomment when uri and mime type implemented.
-        // ndefUri = message.getRecords()[0].toUri();
-        // ndefMimeType = message.getRecords()[0].toMimeType();
-        return intent;
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsContentProvider.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsContentProvider.java
deleted file mode 100644
index 6bc535b..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsContentProvider.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.sms;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.net.Uri;
-
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-
-/**
- * Content provider for SMS query.
- */
-public class SmsContentProvider extends ContentProvider {
-
-    public SmsContentProvider() {
-    }
-
-    @Override
-    public String getType(Uri uri) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public boolean onCreate() {
-        return true;
-    }
-
-    @Override
-    public Cursor query(
-            Uri uri, String[] projection, String selection, String[] selectionArgs,
-            String sortOrder) {
-        return DeviceShadowEnvironmentImpl.getLocalSmsletImpl().getCursor(uri);
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        throw new UnsupportedOperationException();
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsletImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsletImpl.java
deleted file mode 100644
index 00a581e..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsletImpl.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.sms;
-
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.Telephony;
-
-import com.android.libraries.testing.deviceshadower.Smslet;
-import com.android.libraries.testing.deviceshadower.internal.common.ContentDatabase;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Implementation of SMS functionality.
- */
-public class SmsletImpl implements Smslet {
-
-    private final Map<Uri, ContentDatabase> mUriToDataMap;
-
-    public SmsletImpl() {
-        mUriToDataMap = new HashMap<>();
-        mUriToDataMap.put(
-                Telephony.Sms.Inbox.CONTENT_URI, new ContentDatabase(Telephony.Sms.Inbox.BODY));
-        mUriToDataMap.put(Telephony.Sms.Sent.CONTENT_URI,
-                new ContentDatabase(Telephony.Sms.Inbox.BODY));
-        // TODO(b/200231384): implement Outbox, Intents, Conversations.
-    }
-
-    @Override
-    public Smslet addSms(Uri contentUri, String body) {
-        mUriToDataMap.get(contentUri).addData(body);
-        return this;
-    }
-
-    public Cursor getCursor(Uri uri) {
-        return mUriToDataMap.get(uri).getCursor();
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/GattHelper.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/GattHelper.java
deleted file mode 100644
index f45b125..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/GattHelper.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.utils;
-
-import android.bluetooth.le.AdvertiseData;
-import android.os.ParcelUuid;
-import android.util.SparseArray;
-
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.BluetoothConstants;
-
-import com.google.common.io.ByteArrayDataOutput;
-import com.google.common.io.ByteStreams;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.Charset;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.UUID;
-
-/**
- * Helper class for Gatt functionality.
- */
-public class GattHelper {
-
-    public static byte[] convertAdvertiseData(
-            AdvertiseData data, int txPowerLevel, String localName, boolean isConnectable) {
-        if (data == null) {
-            return new byte[0];
-        }
-        ByteArrayDataOutput result = ByteStreams.newDataOutput();
-        if (isConnectable) {
-            writeDataUnit(
-                    result,
-                    BluetoothConstants.DATA_TYPE_FLAGS,
-                    new byte[]{BluetoothConstants.FLAGS_IN_CONNECTABLE_PACKETS});
-        }
-        // tx power level is signed 8-bit int, range -100 to 20.
-        if (data.getIncludeTxPowerLevel()) {
-            writeDataUnit(
-                    result,
-                    BluetoothConstants.DATA_TYPE_TX_POWER_LEVEL,
-                    new byte[]{(byte) txPowerLevel});
-        }
-        // Local name
-        if (data.getIncludeDeviceName()) {
-            writeDataUnit(
-                    result,
-                    BluetoothConstants.DATA_TYPE_LOCAL_NAME_COMPLETE,
-                    localName.getBytes(Charset.defaultCharset()));
-        }
-        // Manufacturer data
-        SparseArray<byte[]> manufacturerData = data.getManufacturerSpecificData();
-        for (int i = 0; i < manufacturerData.size(); i++) {
-            int manufacturerId = manufacturerData.keyAt(i);
-            writeDataUnit(
-                    result,
-                    BluetoothConstants.DATA_TYPE_MANUFACTURER_SPECIFIC_DATA,
-                    parseManufacturerData(manufacturerId, manufacturerData.get(manufacturerId))
-            );
-        }
-        // Service data
-        Map<ParcelUuid, byte[]> serviceData = data.getServiceData();
-        for (Entry<ParcelUuid, byte[]> entry : serviceData.entrySet()) {
-            writeDataUnit(
-                    result,
-                    BluetoothConstants.DATA_TYPE_SERVICE_DATA,
-                    parseServiceData(entry.getKey().getUuid(), entry.getValue())
-            );
-        }
-        // Service UUID, 128-bit UUID in little endian
-        if (data.getServiceUuids() != null && !data.getServiceUuids().isEmpty()) {
-            ByteBuffer uuidBytes =
-                    ByteBuffer.allocate(data.getServiceUuids().size() * 16)
-                            .order(ByteOrder.LITTLE_ENDIAN);
-            for (ParcelUuid parcelUuid : data.getServiceUuids()) {
-                UUID uuid = parcelUuid.getUuid();
-                uuidBytes.putLong(uuid.getLeastSignificantBits())
-                        .putLong(uuid.getMostSignificantBits());
-            }
-            writeDataUnit(
-                    result,
-                    BluetoothConstants.DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE,
-                    uuidBytes.array()
-            );
-        }
-        return result.toByteArray();
-    }
-
-    private static byte[] parseServiceData(UUID uuid, byte[] serviceData) {
-        // First two bytes of the data are data UUID in little endian
-        int length = 2 + serviceData.length;
-        byte[] result = new byte[length];
-        // extract 16-bit UUID value
-        int uuidValue = (int) ((uuid.getMostSignificantBits() & 0x0000FFFF00000000L) >>> 32);
-        result[0] = (byte) (uuidValue & 0xFF);
-        result[1] = (byte) ((uuidValue >> 8) & 0xFF);
-        System.arraycopy(serviceData, 0, result, 2, serviceData.length);
-        return result;
-
-    }
-
-    private static byte[] parseManufacturerData(int manufacturerId, byte[] manufacturerData) {
-        // First two bytes are manufacturer id in little endian.
-        int length = 2 + manufacturerData.length;
-        byte[] result = new byte[length];
-        result[0] = (byte) (manufacturerId & 0xFF);
-        result[1] = (byte) ((manufacturerId >> 8) & 0xFF);
-        System.arraycopy(manufacturerData, 0, result, 2, manufacturerData.length);
-        return result;
-    }
-
-    private static void writeDataUnit(ByteArrayDataOutput output, int type, byte[] data) {
-        // Length includes the length of the field type, which is 1 byte.
-        int length = 1 + data.length;
-        // Length and type are unsigned 8-bit int. Assume the values are valid.
-        output.write(length);
-        output.write(type);
-        output.write(data);
-    }
-
-    private GattHelper() {
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/Logger.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/Logger.java
deleted file mode 100644
index 31f7202..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/Logger.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.utils;
-
-import android.text.TextUtils;
-import android.util.Log;
-
-/**
- * Logger class to provide formatted log for Device Shadower.
- *
- * <p>Log is formatted as "[TAG] [Keyword1, Keyword2 ...] Log Message Body".</p>
- */
-public class Logger {
-
-    private static final String TAG = "DeviceShadower";
-
-    private final String mTag;
-    private final String mPrefix;
-
-    public Logger(String tag, String... keywords) {
-        mTag = tag;
-        mPrefix = buildPrefix(keywords);
-    }
-
-    public static Logger create(String... keywords) {
-        return new Logger(TAG, keywords);
-    }
-
-    private static String buildPrefix(String... keywords) {
-        if (keywords.length == 0) {
-            return "";
-        }
-        return String.format(" [%s] ", TextUtils.join(", ", keywords));
-    }
-
-    /**
-     * @see Log#e(String, String)
-     */
-    public void e(String msg) {
-        Log.e(mTag, format(msg));
-    }
-
-    /**
-     * @see Log#e(String, String, Throwable)
-     */
-    public void e(String msg, Throwable throwable) {
-        Log.e(mTag, format(msg), throwable);
-    }
-
-    /**
-     * @see Log#d(String, String)
-     */
-    public void d(String msg) {
-        Log.d(mTag, format(msg));
-    }
-
-    /**
-     * @see Log#d(String, String, Throwable)
-     */
-    public void d(String msg, Throwable throwable) {
-        Log.d(mTag, format(msg), throwable);
-    }
-
-    /**
-     * @see Log#i(String, String)
-     */
-    public void i(String msg) {
-        Log.i(mTag, format(msg));
-    }
-
-    /**
-     * @see Log#i(String, String, Throwable)
-     */
-    public void i(String msg, Throwable throwable) {
-        Log.i(mTag, format(msg), throwable);
-    }
-
-    /**
-     * @see Log#v(String, String)
-     */
-    public void v(String msg) {
-        Log.v(mTag, format(msg));
-    }
-
-    /**
-     * @see Log#v(String, String, Throwable)
-     */
-    public void v(String msg, Throwable throwable) {
-        Log.v(mTag, format(msg), throwable);
-    }
-
-    /**
-     * @see Log#w(String, String)
-     */
-    public void w(String msg) {
-        Log.w(mTag, format(msg));
-    }
-
-    /**
-     * @see Log#w(String, Throwable)
-     */
-    public void w(Throwable throwable) {
-        Log.w(mTag, null, throwable);
-    }
-
-    /**
-     * @see Log#w(String, String, Throwable)
-     */
-    public void w(String msg, Throwable throwable) {
-        Log.w(mTag, format(msg), throwable);
-    }
-
-    /**
-     * @see Log#wtf(String, String)
-     */
-    public void wtf(String msg) {
-        Log.wtf(mTag, format(msg));
-    }
-
-    /**
-     * @see Log#wtf(String, String, Throwable)
-     */
-    public void wtf(String msg, Throwable throwable) {
-        Log.wtf(mTag, format(msg), throwable);
-    }
-
-    /**
-     * @see Log#isLoggable(String, int)
-     */
-    public boolean isLoggable(int level) {
-        return Log.isLoggable(mTag, level);
-    }
-
-    /**
-     * @see Log#println(int, String, String)
-     */
-    public int println(int priority, String msg) {
-        return Log.println(priority, mTag, format(msg));
-    }
-
-    private String format(String msg) {
-        return mPrefix + msg;
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/MacAddressGenerator.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/MacAddressGenerator.java
deleted file mode 100644
index f8d3193..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/MacAddressGenerator.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.internal.utils;
-
-import android.bluetooth.BluetoothAdapter;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.Locale;
-
-import javax.annotation.concurrent.GuardedBy;
-
-/**
- * A class which generates and converts valid Bluetooth MAC addresses.
- */
-public class MacAddressGenerator {
-
-    @GuardedBy("MacAddressGenerator.class")
-    private static MacAddressGenerator sInstance = new MacAddressGenerator();
-
-    @VisibleForTesting
-    public static synchronized void setInstanceForTest(MacAddressGenerator generator) {
-        sInstance = generator;
-    }
-
-    public static synchronized MacAddressGenerator get() {
-        return sInstance;
-    }
-
-    private long mLastAddress = 0x0L;
-
-    private MacAddressGenerator() {
-    }
-
-    public String generateMacAddress() {
-        byte[] bytes = generateMacAddressBytes();
-        return convertByteMacAddress(bytes);
-    }
-
-    public byte[] generateMacAddressBytes() {
-        long addr = mLastAddress++;
-        byte[] bytes = new byte[6];
-        for (int i = 5; i >= 0; i--) {
-            bytes[i] = (byte) (addr & 0xFF);
-            addr = addr >> 8;
-        }
-        return bytes;
-    }
-
-    public static byte[] convertStringMacAddress(String address) {
-        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
-            throw new IllegalArgumentException("Not a valid bluetooth mac hex string: " + address);
-        }
-        byte[] bytes = new byte[6];
-        String[] macValues = address.split(":");
-        for (int i = 0; i < bytes.length; i++) {
-            bytes[i] = Integer.decode("0x" + macValues[i]).byteValue();
-        }
-        return bytes;
-    }
-
-    public static String convertByteMacAddress(byte[] address) {
-        if (address == null || address.length != 6) {
-            throw new IllegalArgumentException("Bluetooth address must have 6 bytes");
-        }
-        return String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X",
-                address[0], address[1], address[2], address[3], address[4], address[5]);
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothA2dp.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothA2dp.java
deleted file mode 100644
index 344103b..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothA2dp.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.shadows.bluetooth;
-
-import static com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl.getBlueletImpl;
-import static com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl.getLocalBlueletImpl;
-
-import android.bluetooth.BluetoothA2dp;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.BluetoothProfile.ServiceListener;
-import android.content.Context;
-import android.content.Intent;
-
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Shadow of the Bluetooth A2DP service.
- */
-@Implements(BluetoothA2dp.class)
-public class ShadowBluetoothA2dp {
-
-    /**
-     * Hidden in {@link BluetoothProfile}.
-     */
-    public static final int A2DP_SINK = 11;
-
-    private final Map<BluetoothDevice, Integer> mDeviceToConnectionState = new HashMap<>();
-    private Context mContext;
-    @RealObject
-    private BluetoothA2dp mRealObject;
-
-    public void __constructor__(Context context, ServiceListener l) {
-        this.mContext = context;
-        l.onServiceConnected(BluetoothProfile.A2DP, mRealObject);
-    }
-
-    @Implementation
-    public List<BluetoothDevice> getConnectedDevices() {
-        List<BluetoothDevice> result = new ArrayList<>();
-        for (BluetoothDevice device : mDeviceToConnectionState.keySet()) {
-            if (getConnectionState(device) == BluetoothProfile.STATE_CONNECTED) {
-                result.add(device);
-            }
-        }
-        return result;
-    }
-
-    @Implementation
-    public int getConnectionState(BluetoothDevice device) {
-        return mDeviceToConnectionState.containsKey(device)
-                ? mDeviceToConnectionState.get(device)
-                : BluetoothProfile.STATE_DISCONNECTED;
-    }
-
-    @Implementation
-    public boolean connect(BluetoothDevice device) {
-        setConnectionState(BluetoothProfile.STATE_CONNECTING, device);
-        // Only successfully connect if the device is in the environment (i.e. nearby) and accepts
-        // connections.
-        BlueletImpl blueLet = getBlueletImpl(device.getAddress());
-        if (blueLet != null && !blueLet.getRefuseConnections()) {
-            setConnectionState(BluetoothProfile.STATE_CONNECTED, device);
-        } else {
-            // If the device isn't in the environment, still return true (no immediate failure, i.e.
-            // we're trying to connect) but send CONNECTING -> DISCONNECTED (like the OS does).
-            setConnectionState(BluetoothProfile.STATE_DISCONNECTED, device);
-        }
-        return true;
-    }
-
-    @Implementation
-    public void close() {
-    }
-
-    private void setConnectionState(int state, BluetoothDevice device) {
-        int previousState = getConnectionState(device);
-        mDeviceToConnectionState.put(device, state);
-
-        getLocalBlueletImpl()
-                .setProfileConnectionState(BluetoothProfile.A2DP, state, device.getAddress());
-        BlueletImpl remoteDevice = getBlueletImpl(device.getAddress());
-        if (remoteDevice != null) {
-            remoteDevice.setProfileConnectionState(A2DP_SINK, state, getLocalBlueletImpl().address);
-        }
-
-        mContext.sendBroadcast(
-                new Intent(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)
-                        .putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, previousState)
-                        .putExtra(BluetoothProfile.EXTRA_STATE, state)
-                        .putExtra(BluetoothDevice.EXTRA_DEVICE, device));
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothAdapter.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothAdapter.java
deleted file mode 100644
index 394afbc..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothAdapter.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.shadows.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.content.AttributionSource;
-
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
-import com.android.libraries.testing.deviceshadower.internal.utils.MacAddressGenerator;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-
-/**
- * Shadow of {@link BluetoothAdapter} to be used with Device Shadower in Robolectric test.
- */
-@Implements(BluetoothAdapter.class)
-public class ShadowBluetoothAdapter {
-
-    @RealObject
-    BluetoothAdapter mRealAdapter;
-
-    public ShadowBluetoothAdapter() {
-    }
-
-    @Implementation
-    public static synchronized BluetoothAdapter getDefaultAdapter() {
-        // Add a device and set local devicelet in case no local bluelet set
-        if (!DeviceShadowEnvironmentImpl.hasLocalDeviceletImpl()) {
-            String address = MacAddressGenerator.get().generateMacAddress();
-            DeviceShadowEnvironmentImpl.addDevice(address);
-            DeviceShadowEnvironmentImpl.setLocalDevice(address);
-        }
-        BlueletImpl localBluelet = DeviceShadowEnvironmentImpl.getLocalBlueletImpl();
-        return localBluelet.getAdapter();
-    }
-
-    @Implementation
-    public static BluetoothAdapter createAdapter(AttributionSource attributionSource) {
-        // Add a device and set local devicelet in case no local bluelet set
-        if (!DeviceShadowEnvironmentImpl.hasLocalDeviceletImpl()) {
-            String address = MacAddressGenerator.get().generateMacAddress();
-            DeviceShadowEnvironmentImpl.addDevice(address);
-            DeviceShadowEnvironmentImpl.setLocalDevice(address);
-        }
-        BlueletImpl localBluelet = DeviceShadowEnvironmentImpl.getLocalBlueletImpl();
-        return localBluelet.getAdapter();
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothDevice.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothDevice.java
deleted file mode 100644
index 247f46e..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothDevice.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.shadows.bluetooth;
-
-import android.bluetooth.BluetoothDevice;
-
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.IBluetoothImpl;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Placeholder for BluetoothDevice improvements
- */
-@Implements(BluetoothDevice.class)
-public class ShadowBluetoothDevice {
-
-    @RealObject
-    private BluetoothDevice mBluetoothDevice;
-    private static final Map<String, Integer> sBondTransport = new HashMap<>();
-    private static Map<String, Boolean> sPairingConfirmation = new HashMap<>();
-
-    public ShadowBluetoothDevice() {
-    }
-
-    @Implementation
-    public boolean setPasskey(int passkey) {
-        return new IBluetoothImpl().setPasskey(mBluetoothDevice, passkey);
-    }
-
-    @Implementation
-    public boolean createBond(int transport) {
-        sBondTransport.put(mBluetoothDevice.getAddress(), transport);
-        return Shadow.directlyOn(
-                mBluetoothDevice,
-                BluetoothDevice.class,
-                "createBond",
-                ClassParameter.from(int.class, transport));
-    }
-
-    public static int getBondTransport(String address) {
-        return sBondTransport.containsKey(address)
-                ? sBondTransport.get(address)
-                : BluetoothDevice.TRANSPORT_AUTO;
-    }
-
-    @Implementation
-    public boolean setPairingConfirmation(boolean confirm) {
-        sPairingConfirmation.put(mBluetoothDevice.getAddress(), confirm);
-        return Shadow.directlyOn(
-                mBluetoothDevice,
-                BluetoothDevice.class,
-                "setPairingConfirmation",
-                ClassParameter.from(boolean.class, confirm));
-    }
-
-    /**
-     * Gets the confirmation value previously set with a call to {@link
-     * BluetoothDevice#setPairingConfirmation(boolean)}. Default is false.
-     */
-    public static boolean getPairingConfirmation(String address) {
-        return sPairingConfirmation.containsKey(address) && sPairingConfirmation.get(address);
-    }
-
-    /**
-     * Resets the confirmation values.
-     */
-    public static void resetPairingConfirmation() {
-        sPairingConfirmation = new HashMap<>();
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothLeScanner.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothLeScanner.java
deleted file mode 100644
index 1f7da14..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothLeScanner.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.shadows.bluetooth;
-
-import android.bluetooth.le.BluetoothLeScanner;
-
-import org.robolectric.annotation.Implements;
-
-/**
- * Shadow of {@link BluetoothLeScanner} to be used with Device Shadower in Robolectric test.
- */
-@Implements(BluetoothLeScanner.class)
-public class ShadowBluetoothLeScanner {
-
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothServerSocket.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothServerSocket.java
deleted file mode 100644
index bffcf32..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothServerSocket.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.shadows.bluetooth;
-
-import android.bluetooth.BluetoothServerSocket;
-import android.bluetooth.BluetoothSocket;
-import android.net.LocalSocket;
-import android.os.ParcelFileDescriptor;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.RfcommDelegate;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-
-/**
- * Placeholder for BluetoothServerSocket updates
- */
-@Implements(BluetoothServerSocket.class)
-public class ShadowBluetoothServerSocket {
-
-    @RealObject
-    BluetoothServerSocket mRealServerSocket;
-
-    public ShadowBluetoothServerSocket() {
-    }
-
-    @Implementation
-    public BluetoothSocket accept(int timeout) throws IOException {
-        FileDescriptor serverSocketFd = getServerSocketFileDescriptor();
-        if (serverSocketFd == null) {
-            throw new IOException("socket is closed.");
-        }
-        RfcommDelegate local = getLocalRfcommDelegate();
-        local.checkInterrupt();
-        FileDescriptor clientFd = local.processNextConnectionRequest(serverSocketFd);
-        // configure the LocalSocket of the BluetoothServerSocket
-        BluetoothSocket internalSocket = ReflectionHelpers.getField(mRealServerSocket, "mSocket");
-        ShadowLocalSocket internalLocalSocket = getLocalSocketShadow(internalSocket);
-        internalLocalSocket.setAncillaryFd(local.getServerFd(clientFd));
-
-        // call original method
-        BluetoothSocket socket = Shadow.directlyOn(mRealServerSocket, BluetoothServerSocket.class,
-                "accept", ClassParameter.from(int.class, timeout));
-
-        // setup local socket of the returned BluetoothSocket
-        String remoteAddress = socket.getRemoteDevice().getAddress();
-        ShadowLocalSocket shadowLocalSocket = getLocalSocketShadow(socket);
-        shadowLocalSocket.setRemoteAddress(remoteAddress);
-        // init connection to client
-        local.initiateConnectToClient(clientFd, getPort());
-        local.waitForConnectionEstablished(clientFd);
-        return socket;
-    }
-
-    @Implementation
-    public void close() throws IOException {
-        getLocalRfcommDelegate().closeServerSocket(getServerSocketFileDescriptor());
-        Shadow.directlyOn(mRealServerSocket, BluetoothServerSocket.class, "close");
-    }
-
-    @VisibleForTesting
-    FileDescriptor getServerSocketFileDescriptor() {
-        BluetoothSocket socket = ReflectionHelpers.getField(mRealServerSocket, "mSocket");
-        ParcelFileDescriptor pfd = ReflectionHelpers.getField(socket, "mPfd");
-        if (pfd == null) {
-            return null;
-        }
-        return pfd.getFileDescriptor();
-    }
-
-    @VisibleForTesting
-    int getPort() {
-        BluetoothSocket socket = ReflectionHelpers.getField(mRealServerSocket, "mSocket");
-        return ReflectionHelpers.getField(socket, "mPort");
-    }
-
-    private ShadowLocalSocket getLocalSocketShadow(BluetoothSocket socket) {
-        LocalSocket localSocket = ReflectionHelpers.getField(socket, "mSocket");
-        return (ShadowLocalSocket) Shadow.extract(localSocket);
-    }
-
-    private RfcommDelegate getLocalRfcommDelegate() {
-        return DeviceShadowEnvironmentImpl.getLocalBlueletImpl().getRfcommDelegate();
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothSocket.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothSocket.java
deleted file mode 100644
index 5d417cf..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothSocket.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.shadows.bluetooth;
-
-import android.bluetooth.BluetoothSocket;
-import android.os.ParcelFileDescriptor;
-
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.RfcommDelegate;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Shadow implementation of a Bluetooth Socket
- */
-@Implements(BluetoothSocket.class)
-public class ShadowBluetoothSocket {
-
-    @RealObject
-    BluetoothSocket mRealSocket;
-
-    public ShadowBluetoothSocket() {
-    }
-
-    @Implementation
-    public void connect() throws IOException {
-        Shadow.directlyOn(mRealSocket, BluetoothSocket.class, "connect");
-
-        boolean isEncrypted = ReflectionHelpers.getField(mRealSocket, "mEncrypt");
-        FileDescriptor localFd =
-                ((ParcelFileDescriptor) ReflectionHelpers.getField(mRealSocket,
-                        "mPfd")).getFileDescriptor();
-        RfcommDelegate local = DeviceShadowEnvironmentImpl.getLocalBlueletImpl()
-                .getRfcommDelegate();
-        String remoteAddress = mRealSocket.getRemoteDevice().getAddress();
-        local.finishPendingConnection(remoteAddress, localFd, isEncrypted);
-
-        ShadowLocalSocket shadowLocalSocket = getLocalSocketShadow();
-        shadowLocalSocket.setRemoteAddress(remoteAddress);
-    }
-
-    @Implementation
-    public InputStream getInputStream() throws IOException {
-        ShadowLocalSocket socket = getLocalSocketShadow();
-        return socket.getInputStream();
-    }
-
-    @Implementation
-    public OutputStream getOutputStream() throws IOException {
-        ShadowLocalSocket socket = getLocalSocketShadow();
-        return socket.getOutputStream();
-    }
-
-    private ShadowLocalSocket getLocalSocketShadow() throws IOException {
-        try {
-            return (ShadowLocalSocket) Shadow.extract(
-                    ReflectionHelpers.getField(mRealSocket, "mSocket"));
-        } catch (NullPointerException e) {
-            throw new IOException(e);
-        }
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowLocalSocket.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowLocalSocket.java
deleted file mode 100644
index 5189330..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowLocalSocket.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.shadows.bluetooth;
-
-import android.net.LocalSocket;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.BluetoothConstants;
-import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.RfcommDelegate;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Shadow implementation of a LocalSocket to make bluetooth connections function.
- */
-@Implements(LocalSocket.class)
-public class ShadowLocalSocket {
-
-    private String mRemoteAddress;
-    private FileDescriptor mFd;
-    private FileDescriptor mAncillaryFd;
-
-    public ShadowLocalSocket() {
-    }
-
-    public void __constructor__(FileDescriptor fd) {
-        this.mFd = fd;
-    }
-
-    @Implementation
-    public FileDescriptor[] getAncillaryFileDescriptors() throws IOException {
-        return new FileDescriptor[]{mAncillaryFd};
-    }
-
-    @Implementation
-    @SuppressWarnings("InputStreamSlowMultibyteRead")
-    public InputStream getInputStream() throws IOException {
-        final RfcommDelegate local = getLocalRfcommDelegate();
-        return new InputStream() {
-            @Override
-            public int read() throws IOException {
-                int res = local.read(mRemoteAddress, mFd);
-                if (res == BluetoothConstants.SOCKET_CLOSE) {
-                    throw new IOException("closed");
-                }
-                return res & 0xFF;
-            }
-        };
-    }
-
-    @Implementation
-    public OutputStream getOutputStream() throws IOException {
-        final RfcommDelegate local = getLocalRfcommDelegate();
-        return new OutputStream() {
-            @Override
-            public void write(int b) throws IOException {
-                local.write(mRemoteAddress, mFd, b);
-            }
-        };
-    }
-
-    @Implementation
-    public void setSoTimeout(int n) throws IOException {
-        // Nothing
-    }
-
-    @Implementation
-    public void shutdownInput() throws IOException {
-        getLocalRfcommDelegate().shutdownInput(mRemoteAddress, mFd);
-    }
-
-    @Implementation
-    public void shutdownOutput() throws IOException {
-        if (mRemoteAddress == null) {
-            return;
-        }
-        getLocalRfcommDelegate().shutdownOutput(mRemoteAddress, mFd);
-    }
-
-    void setAncillaryFd(FileDescriptor fd) {
-        mAncillaryFd = fd;
-    }
-
-    void setRemoteAddress(String address) {
-        mRemoteAddress = address;
-    }
-
-    @VisibleForTesting
-    void setFileDescriptorForTest(FileDescriptor fd) {
-        this.mFd = fd;
-    }
-
-    private RfcommDelegate getLocalRfcommDelegate() {
-        return DeviceShadowEnvironmentImpl.getLocalBlueletImpl().getRfcommDelegate();
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowParcelFileDescriptor.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowParcelFileDescriptor.java
deleted file mode 100644
index 585939b..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowParcelFileDescriptor.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.shadows.bluetooth;
-
-import android.os.ParcelFileDescriptor;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-
-/**
- * Inert implementation of a ParcelFileDescriptor to make bluetooth connections function.
- */
-@Implements(ParcelFileDescriptor.class)
-public class ShadowParcelFileDescriptor {
-
-    private FileDescriptor mFd;
-
-    public ShadowParcelFileDescriptor() {
-    }
-
-    public void __constructor__(FileDescriptor fd) {
-        this.mFd = fd;
-    }
-
-    @Implementation
-    public FileDescriptor getFileDescriptor() {
-        return mFd;
-    }
-
-    @Implementation
-    public void close() throws IOException {
-        // Nothing
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/common/DeviceShadowContextImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/common/DeviceShadowContextImpl.java
deleted file mode 100644
index 9bbcee7..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/common/DeviceShadowContextImpl.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.shadows.common;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.os.Handler;
-import android.util.Log;
-
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.DeviceletImpl;
-import com.android.libraries.testing.deviceshadower.internal.common.BroadcastManager;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-import org.robolectric.shadows.ShadowContextImpl;
-
-import javax.annotation.Nullable;
-
-/**
- * Extends {@link ShadowContextImpl} to achieve automatic method redirection to correct virtual
- * device.
- *
- * <p>Supports:
- * <li>Broadcasting</li>
- * Includes send regular, regular sticky, ordered broadcast, and register/unregister receiver.
- * </p>
- */
-@Implements(className = "android.app.ContextImpl")
-public class DeviceShadowContextImpl extends ShadowContextImpl {
-
-    private static final String TAG = "DeviceShadowContextImpl";
-
-    @RealObject
-    private Context mContextImpl;
-
-    @Override
-    @Implementation
-    @Nullable
-    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
-        if (receiver == null) {
-            return null;
-        }
-        BroadcastManager manager = getLocalBroadcastManager();
-        if (manager == null) {
-            Log.w(TAG, "Receiver registered before any devices added: " + receiver);
-            return null;
-        }
-        return manager.registerReceiver(
-                receiver, filter, null /* permission */, null /* handler */, mContextImpl);
-    }
-
-    @Override
-    @Implementation
-    @Nullable
-    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
-            @Nullable String broadcastPermission, @Nullable Handler scheduler) {
-        return getLocalBroadcastManager().registerReceiver(
-                receiver, filter, broadcastPermission, scheduler, mContextImpl);
-    }
-
-    @Override
-    @Implementation
-    public void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
-        getLocalBroadcastManager().unregisterReceiver(broadcastReceiver);
-    }
-
-    @Override
-    @Implementation
-    public void sendBroadcast(Intent intent) {
-        getLocalBroadcastManager().sendBroadcast(intent, null /* permission */);
-    }
-
-    @Override
-    @Implementation
-    public void sendBroadcast(Intent intent, @Nullable String receiverPermission) {
-        getLocalBroadcastManager().sendBroadcast(intent, receiverPermission);
-    }
-
-    @Override
-    @Implementation
-    public void sendOrderedBroadcast(Intent intent, @Nullable String receiverPermission) {
-        getLocalBroadcastManager().sendOrderedBroadcast(intent, receiverPermission);
-    }
-
-    @Override
-    @Implementation
-    public void sendOrderedBroadcast(Intent intent, @Nullable String receiverPermission,
-            @Nullable BroadcastReceiver resultReceiver, @Nullable Handler scheduler,
-            int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras) {
-        getLocalBroadcastManager().sendOrderedBroadcast(intent, receiverPermission, resultReceiver,
-                scheduler, initialCode, initialData, initialExtras, mContextImpl);
-    }
-
-    @Override
-    @Implementation
-    public void sendStickyBroadcast(Intent intent) {
-        getLocalBroadcastManager().sendStickyBroadcast(intent);
-    }
-
-    private BroadcastManager getLocalBroadcastManager() {
-        DeviceletImpl devicelet = DeviceShadowEnvironmentImpl.getLocalDeviceletImpl();
-        if (devicelet == null) {
-            return null;
-        }
-        return devicelet.getBroadcastManager();
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/nfc/ShadowNfcAdapter.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/nfc/ShadowNfcAdapter.java
deleted file mode 100644
index e7112fb..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/nfc/ShadowNfcAdapter.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.shadows.nfc;
-
-import static org.robolectric.util.ReflectionHelpers.callConstructor;
-
-import android.content.Context;
-import android.nfc.NfcAdapter;
-
-import com.android.libraries.testing.deviceshadower.Enums.NfcOperation;
-import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
-import com.android.libraries.testing.deviceshadower.internal.nfc.INfcAdapterImpl;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
-
-/**
- * Shadow implementation of Nfc Adapter.
- */
-@Implements(NfcAdapter.class)
-public class ShadowNfcAdapter {
-
-    @Implementation
-    public static NfcAdapter getDefaultAdapter(Context context) {
-        if (DeviceShadowEnvironmentImpl.getLocalNfcletImpl()
-                .shouldInterrupt(NfcOperation.GET_ADAPTER)) {
-            return null;
-        }
-        ReflectionHelpers.setStaticField(NfcAdapter.class, "sService", new INfcAdapterImpl());
-        return callConstructor(NfcAdapter.class, ClassParameter.from(Context.class, context));
-    }
-
-    // TODO(b/200231384): support state change.
-    public ShadowNfcAdapter() {
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BaseTestCase.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BaseTestCase.java
deleted file mode 100644
index 8a3c0e7..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BaseTestCase.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.testcases;
-
-import android.app.Application;
-
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
-import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowLocalSocket;
-import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowParcelFileDescriptor;
-import com.android.libraries.testing.deviceshadower.shadows.common.DeviceShadowContextImpl;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.internal.AssumptionViolatedException;
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-/**
- * Base class for all DeviceShadower client.
- */
-@Config(
-        // sdk = 21,
-        shadows = {
-                DeviceShadowContextImpl.class,
-                ShadowParcelFileDescriptor.class,
-                ShadowLocalSocket.class
-        })
-public class BaseTestCase {
-
-    protected Application mContext = RuntimeEnvironment.application;
-
-    /**
-     * Test Watcher which logs test starting and finishing so log messages are easier to read.
-     */
-    @Rule
-    public TestWatcher watcher = new TestWatcher() {
-        @Override
-        protected void succeeded(Description description) {
-            super.succeeded(description);
-            logMessage(
-                    String.format("Test %s finished successfully.", description.getDisplayName()));
-        }
-
-        @Override
-        protected void failed(Throwable e, Description description) {
-            super.failed(e, description);
-            logMessage(String.format("Test %s failed.", description.getDisplayName()));
-        }
-
-        @Override
-        protected void skipped(AssumptionViolatedException e, Description description) {
-            super.skipped(e, description);
-            logMessage(String.format("Test %s is skipped.", description.getDisplayName()));
-        }
-
-        @Override
-        protected void starting(Description description) {
-            super.starting(description);
-            logMessage(String.format("Test %s started.", description.getDisplayName()));
-        }
-
-        @Override
-        protected void finished(Description description) {
-            super.finished(description);
-        }
-
-        private void logMessage(String message) {
-            System.out.println("\n*** " + message);
-        }
-    };
-
-    @Before
-    public void setUp() throws Exception {
-        DeviceShadowEnvironment.init();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        DeviceShadowEnvironment.reset();
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BluetoothTestCase.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BluetoothTestCase.java
deleted file mode 100644
index cddc6fe..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BluetoothTestCase.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.testcases;
-
-import static org.robolectric.Shadows.shadowOf;
-import static org.robolectric.util.ReflectionHelpers.callConstructor;
-
-import android.bluetooth.BluetoothManager;
-import android.content.Context;
-
-import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothA2dp;
-import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothAdapter;
-import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothDevice;
-import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothLeScanner;
-import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothServerSocket;
-import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothSocket;
-
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
-
-/**
- * Base class for Bluetooth Test
- */
-@Config(
-        shadows = {
-                ShadowBluetoothAdapter.class,
-                ShadowBluetoothDevice.class,
-                ShadowBluetoothLeScanner.class,
-                ShadowBluetoothSocket.class,
-                ShadowBluetoothServerSocket.class,
-                ShadowBluetoothA2dp.class
-        })
-public class BluetoothTestCase extends BaseTestCase {
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        // TODO(b/28087747): Get bluetooth Manager from robolectric framework.
-        shadowOf(RuntimeEnvironment.application)
-                .setSystemService(
-                        Context.BLUETOOTH_SERVICE,
-                        callConstructor(BluetoothManager.class,
-                                ClassParameter.from(Context.class, mContext)));
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/Matchers.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/Matchers.java
deleted file mode 100644
index 3bfe43b..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/Matchers.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.testcases;
-
-import static org.mockito.ArgumentMatchers.argThat;
-
-import android.bluetooth.BluetoothSocket;
-
-import org.mockito.ArgumentMatcher;
-
-/**
- * Convenient methods to create mockito matchers.
- */
-public class Matchers {
-
-    private Matchers() {
-    }
-
-    public static <T extends Exception> T exception(final Class<T> clazz, final String... msgs) {
-        return argThat(
-                new ArgumentMatcher<T>() {
-                    @Override
-                    public boolean matches(T obj) {
-                        if (!clazz.isInstance(obj)) {
-                            return false;
-                        }
-                        Throwable exception = clazz.cast(obj);
-                        for (String msg : msgs) {
-                            if (exception == null || !exception.getMessage().contains(msg)) {
-                                return false;
-                            }
-                            exception = exception.getCause();
-                        }
-                        return true;
-                    }
-                });
-    }
-
-    public static BluetoothSocket socket(final String addr) {
-        return argThat(
-                new ArgumentMatcher<BluetoothSocket>() {
-                    @Override
-                    public boolean matches(BluetoothSocket obj) {
-                        return ((BluetoothSocket) obj)
-                                .getRemoteDevice()
-                                .getAddress()
-                                .toUpperCase()
-                                .equals(addr.toUpperCase());
-                    }
-                });
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/NfcTestCase.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/NfcTestCase.java
deleted file mode 100644
index a80164b..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/NfcTestCase.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.testcases;
-
-import com.android.libraries.testing.deviceshadower.shadows.nfc.ShadowNfcAdapter;
-
-import org.robolectric.annotation.Config;
-
-/**
- * Base class for NFC Test
- */
-@Config(shadows = {ShadowNfcAdapter.class})
-public class NfcTestCase extends BaseTestCase {
-
-}
-
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/SmsTestCase.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/SmsTestCase.java
deleted file mode 100644
index edfcc6d..0000000
--- a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/SmsTestCase.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 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.libraries.testing.deviceshadower.testcases;
-
-import android.content.pm.ProviderInfo;
-import android.provider.Telephony;
-
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironmentInternal;
-
-import org.robolectric.Robolectric;
-
-/**
- * Base class for SMS Test
- */
-public class SmsTestCase extends BaseTestCase {
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        ProviderInfo info = new ProviderInfo();
-        info.authority = Telephony.Sms.CONTENT_URI.getAuthority();
-        Robolectric.buildContentProvider(
-                        DeviceShadowEnvironmentInternal.getSmsContentProviderClass())
-                .create(info);
-    }
-}
diff --git a/nearby/tests/robotests/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java b/nearby/tests/robotests/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java
deleted file mode 100644
index 1ac2aaf..0000000
--- a/nearby/tests/robotests/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Copyright 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.server.nearby.common.bluetooth.fastpair;
-
-import static org.robolectric.Shadows.shadowOf;
-
-import android.Manifest.permission;
-import android.bluetooth.BluetoothAdapter;
-
-import com.android.libraries.testing.deviceshadower.Bluelet.IoCapabilities;
-import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
-import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothDevice;
-import com.android.libraries.testing.deviceshadower.testcases.BluetoothTestCase;
-
-import com.google.common.base.VerifyException;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.util.concurrent.ExecutionException;
-
-/**
- * Tests for {@link BluetoothClassicPairer}.
- */
-@RunWith(RobolectricTestRunner.class)
-public class BluetoothClassicPairerTest extends BluetoothTestCase {
-
-    private static final String LOCAL_DEVICE_ADDRESS = "AA:AA:AA:AA:AA:01";
-
-    /**
-     * The remote device's Bluetooth Classic address.
-     */
-    private static final String REMOTE_DEVICE_PUBLIC_ADDRESS = "BB:BB:BB:BB:BB:0C";
-
-    private Preferences.Builder mPrefsBuilder;
-
-    @Before
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        mPrefsBuilder = Preferences.builder().setCreateBondTimeoutSeconds(10);
-
-        ShadowBluetoothDevice.resetPairingConfirmation();
-        shadowOf(mContext)
-                .grantPermissions(
-                        permission.BLUETOOTH, permission.BLUETOOTH_ADMIN,
-                        permission.BLUETOOTH_PRIVILEGED);
-
-        DeviceShadowEnvironment.addDevice(LOCAL_DEVICE_ADDRESS)
-                .bluetooth()
-                .setAdapterInitialState(BluetoothAdapter.STATE_ON)
-                .setIoCapabilities(IoCapabilities.DISPLAY_YES_NO);
-        DeviceShadowEnvironment.addDevice(REMOTE_DEVICE_PUBLIC_ADDRESS)
-                .bluetooth()
-                .setAdapterInitialState(BluetoothAdapter.STATE_ON)
-                .setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
-                .setIoCapabilities(IoCapabilities.DISPLAY_YES_NO);
-
-        // By default, code runs as if it's on this virtual "device".
-        DeviceShadowEnvironment.setLocalDevice(LOCAL_DEVICE_ADDRESS);
-    }
-
-    @Test
-    public void pair_setPairingConfirmationTrue_deviceBonded() throws Exception {
-    // TODO(b/217195327): replace deviceshadower with injector.
-    /*
-        AtomicReference<BluetoothDevice> targetRemoteDevice = new AtomicReference<>();
-        BluetoothClassicPairer bluetoothClassicPairer =
-                new BluetoothClassicPairer(
-                        mContext,
-                        BluetoothAdapter.getDefaultAdapter()
-                                .getRemoteDevice(REMOTE_DEVICE_PUBLIC_ADDRESS),
-                        mPrefsBuilder.build(),
-                        (BluetoothDevice remoteDevice, int key) -> {
-                            targetRemoteDevice.set(remoteDevice);
-                            // Confirms at remote device to pair with local one.
-                            setPairingConfirmationAtRemoteDevice(true);
-
-                            // Confirms to pair with remote device.
-                            remoteDevice.setPairingConfirmation(true);
-                        });
-
-        bluetoothClassicPairer.pair();
-
-        assertThat(targetRemoteDevice.get()).isNotNull();
-        assertThat(targetRemoteDevice.get().getAddress()).isEqualTo(REMOTE_DEVICE_PUBLIC_ADDRESS);
-        assertThat(targetRemoteDevice.get().getBondState()).isEqualTo(BluetoothDevice.BOND_BONDED);
-        assertThat(bluetoothClassicPairer.isPaired()).isTrue();
-    */
-    }
-
-    @Test
-    public void pair_setPairingConfirmationFalse_throwsExceptionDeviceNotBonded() throws Exception {
-    // TODO(b/217195327): replace deviceshadower with injector.
-    /*
-        AtomicReference<BluetoothDevice> targetRemoteDevice = new AtomicReference<>();
-        BluetoothClassicPairer bluetoothClassicPairer =
-                new BluetoothClassicPairer(
-                        mContext,
-                        BluetoothAdapter.getDefaultAdapter()
-                                .getRemoteDevice(REMOTE_DEVICE_PUBLIC_ADDRESS),
-                        mPrefsBuilder.build(),
-                        (BluetoothDevice remoteDevice, int key) -> {
-                            targetRemoteDevice.set(remoteDevice);
-                            // Confirms at remote device to pair with local one.
-                            setPairingConfirmationAtRemoteDevice(true);
-
-                            // Confirms NOT to pair with remote device.
-                            remoteDevice.setPairingConfirmation(false);
-                        });
-
-        assertThrows(PairingException.class, bluetoothClassicPairer::pair);
-
-        assertThat(targetRemoteDevice.get()).isNotNull();
-        assertThat(targetRemoteDevice.get().getAddress()).isEqualTo(REMOTE_DEVICE_PUBLIC_ADDRESS);
-        assertThat(targetRemoteDevice.get().getBondState()).isNotEqualTo(
-                BluetoothDevice.BOND_BONDED);
-        assertThat(bluetoothClassicPairer.isPaired()).isFalse();
-    */
-    }
-
-    @Test
-    public void pair_setPairingConfirmationIgnored_throwsExceptionDeviceNotBonded()
-            throws Exception {
-    // TODO(b/217195327): replace deviceshadower with injector.
-    /*
-        AtomicReference<BluetoothDevice> targetRemoteDevice = new AtomicReference<>();
-        BluetoothClassicPairer bluetoothClassicPairer =
-                new BluetoothClassicPairer(
-                        mContext,
-                        BluetoothAdapter.getDefaultAdapter()
-                                .getRemoteDevice(REMOTE_DEVICE_PUBLIC_ADDRESS),
-                        mPrefsBuilder.build(),
-                        (BluetoothDevice remoteDevice, int key) -> {
-                            targetRemoteDevice.set(remoteDevice);
-                            // Confirms at remote device to pair with local one.
-                            setPairingConfirmationAtRemoteDevice(true);
-
-                            // Ignores the setPairingConfirmation.
-                        });
-
-        assertThrows(PairingException.class, bluetoothClassicPairer::pair);
-        assertThat(targetRemoteDevice.get()).isNotNull();
-        assertThat(targetRemoteDevice.get().getAddress()).isEqualTo(REMOTE_DEVICE_PUBLIC_ADDRESS);
-        assertThat(targetRemoteDevice.get().getBondState()).isNotEqualTo(
-                BluetoothDevice.BOND_BONDED);
-        assertThat(bluetoothClassicPairer.isPaired()).isFalse();
-    */
-    }
-
-    private static void setPairingConfirmationAtRemoteDevice(boolean confirm) {
-        try {
-            DeviceShadowEnvironment.run(REMOTE_DEVICE_PUBLIC_ADDRESS,
-                    () -> BluetoothAdapter.getDefaultAdapter()
-                            .getRemoteDevice(LOCAL_DEVICE_ADDRESS)
-                            .setPairingConfirmation(confirm)).get();
-        } catch (InterruptedException | ExecutionException e) {
-            throw new VerifyException("failed to set pairing confirmation at remote device", e);
-        }
-    }
-}
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 8a8aeab..2950568 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -29,7 +30,6 @@
         "android.test.base",
         "android.test.mock",
         "android.test.runner",
-        "HalfSheetUX",
     ],
     compile_multilib: "both",
 
@@ -43,8 +43,7 @@
         "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
         "service-nearby-pre-jarjar",
-        "truth-prebuilt",
-        // "Robolectric_all-target",
+        "truth",
     ],
     // these are needed for Extended Mockito
     jni_libs: [
diff --git a/nearby/tests/unit/src/android/nearby/FastPairAntispoofKeyDeviceMetadataTest.java b/nearby/tests/unit/src/android/nearby/FastPairAntispoofKeyDeviceMetadataTest.java
deleted file mode 100644
index d095529..0000000
--- a/nearby/tests/unit/src/android/nearby/FastPairAntispoofKeyDeviceMetadataTest.java
+++ /dev/null
@@ -1,177 +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.nearby;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class FastPairAntispoofKeyDeviceMetadataTest {
-
-    private static final int BLE_TX_POWER  = 5;
-    private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
-    private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
-    private static final float DELTA = 0.001f;
-    private static final int DEVICE_TYPE = 7;
-    private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
-            "DOWNLOAD_COMPANION_APP_DESCRIPTION";
-    private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
-            "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
-    private static final byte[] IMAGE = new byte[] {7, 9};
-    private static final String IMAGE_URL = "IMAGE_URL";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION =
-            "INITIAL_NOTIFICATION_DESCRIPTION";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
-            "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
-    private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
-    private static final String INTENT_URI = "INTENT_URI";
-    private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
-    private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
-            "RETRO_ACTIVE_PAIRING_DESCRIPTION";
-    private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
-    private static final float TRIGGER_DISTANCE = 111;
-    private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
-    private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
-    private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
-    private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
-    private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
-    private static final String UPDATE_COMPANION_APP_DESCRIPTION =
-            "UPDATE_COMPANION_APP_DESCRIPTION";
-    private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
-            "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
-    private static final byte[] ANTI_SPOOFING_KEY = new byte[] {4, 5, 6};
-    private static final String NAME = "NAME";
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetGetFastPairAntispoofKeyDeviceMetadataNotNull() {
-        FastPairDeviceMetadata fastPairDeviceMetadata = genFastPairDeviceMetadata();
-        FastPairAntispoofKeyDeviceMetadata fastPairAntispoofKeyDeviceMetadata =
-                genFastPairAntispoofKeyDeviceMetadata(ANTI_SPOOFING_KEY, fastPairDeviceMetadata);
-
-        assertThat(fastPairAntispoofKeyDeviceMetadata.getAntispoofPublicKey()).isEqualTo(
-                ANTI_SPOOFING_KEY);
-        ensureFastPairDeviceMetadataAsExpected(
-                fastPairAntispoofKeyDeviceMetadata.getFastPairDeviceMetadata());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetGetFastPairAntispoofKeyDeviceMetadataNull() {
-        FastPairAntispoofKeyDeviceMetadata fastPairAntispoofKeyDeviceMetadata =
-                genFastPairAntispoofKeyDeviceMetadata(null, null);
-        assertThat(fastPairAntispoofKeyDeviceMetadata.getAntispoofPublicKey()).isEqualTo(
-                null);
-        assertThat(fastPairAntispoofKeyDeviceMetadata.getFastPairDeviceMetadata()).isEqualTo(
-                null);
-    }
-
-    /* Verifies DeviceMetadata. */
-    private static void ensureFastPairDeviceMetadataAsExpected(FastPairDeviceMetadata metadata) {
-        assertThat(metadata.getBleTxPower()).isEqualTo(BLE_TX_POWER);
-        assertThat(metadata.getConnectSuccessCompanionAppInstalled())
-                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        assertThat(metadata.getConnectSuccessCompanionAppNotInstalled())
-                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-        assertThat(metadata.getDeviceType()).isEqualTo(DEVICE_TYPE);
-        assertThat(metadata.getDownloadCompanionAppDescription())
-                .isEqualTo(DOWNLOAD_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getFailConnectGoToSettingsDescription())
-                .isEqualTo(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-        assertThat(metadata.getImage()).isEqualTo(IMAGE);
-        assertThat(metadata.getImageUrl()).isEqualTo(IMAGE_URL);
-        assertThat(metadata.getInitialNotificationDescription())
-                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION);
-        assertThat(metadata.getInitialNotificationDescriptionNoAccount())
-                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        assertThat(metadata.getInitialPairingDescription()).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
-        assertThat(metadata.getIntentUri()).isEqualTo(INTENT_URI);
-        assertThat(metadata.getName()).isEqualTo(NAME);
-        assertThat(metadata.getOpenCompanionAppDescription())
-                .isEqualTo(OPEN_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getRetroactivePairingDescription())
-                .isEqualTo(RETRO_ACTIVE_PAIRING_DESCRIPTION);
-        assertThat(metadata.getSubsequentPairingDescription())
-                .isEqualTo(SUBSEQUENT_PAIRING_DESCRIPTION);
-        assertThat(metadata.getTriggerDistance()).isWithin(DELTA).of(TRIGGER_DISTANCE);
-        assertThat(metadata.getTrueWirelessImageUrlCase()).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
-        assertThat(metadata.getTrueWirelessImageUrlLeftBud())
-                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        assertThat(metadata.getTrueWirelessImageUrlRightBud())
-                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        assertThat(metadata.getUnableToConnectDescription())
-                .isEqualTo(UNABLE_TO_CONNECT_DESCRIPTION);
-        assertThat(metadata.getUnableToConnectTitle()).isEqualTo(UNABLE_TO_CONNECT_TITLE);
-        assertThat(metadata.getUpdateCompanionAppDescription())
-                .isEqualTo(UPDATE_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getWaitLaunchCompanionAppDescription())
-                .isEqualTo(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-    }
-
-    /* Generates FastPairAntispoofKeyDeviceMetadata. */
-    private static FastPairAntispoofKeyDeviceMetadata genFastPairAntispoofKeyDeviceMetadata(
-            byte[] antispoofPublicKey, FastPairDeviceMetadata deviceMetadata) {
-        FastPairAntispoofKeyDeviceMetadata.Builder builder =
-                new FastPairAntispoofKeyDeviceMetadata.Builder();
-        builder.setAntispoofPublicKey(antispoofPublicKey);
-        builder.setFastPairDeviceMetadata(deviceMetadata);
-
-        return builder.build();
-    }
-
-    /* Generates FastPairDeviceMetadata. */
-    private static FastPairDeviceMetadata genFastPairDeviceMetadata() {
-        FastPairDeviceMetadata.Builder builder = new FastPairDeviceMetadata.Builder();
-        builder.setBleTxPower(BLE_TX_POWER);
-        builder.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        builder.setConnectSuccessCompanionAppNotInstalled(
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-        builder.setDeviceType(DEVICE_TYPE);
-        builder.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION);
-        builder.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-        builder.setImage(IMAGE);
-        builder.setImageUrl(IMAGE_URL);
-        builder.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION);
-        builder.setInitialNotificationDescriptionNoAccount(
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        builder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
-        builder.setIntentUri(INTENT_URI);
-        builder.setName(NAME);
-        builder.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION);
-        builder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
-        builder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
-        builder.setTriggerDistance(TRIGGER_DISTANCE);
-        builder.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE);
-        builder.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        builder.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        builder.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION);
-        builder.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE);
-        builder.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION);
-        builder.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-
-        return builder.build();
-    }
-}
diff --git a/nearby/tests/unit/src/android/nearby/FastPairDataProviderServiceTest.java b/nearby/tests/unit/src/android/nearby/FastPairDataProviderServiceTest.java
deleted file mode 100644
index b3f2442..0000000
--- a/nearby/tests/unit/src/android/nearby/FastPairDataProviderServiceTest.java
+++ /dev/null
@@ -1,966 +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.nearby;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.verify;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import android.accounts.Account;
-import android.content.Intent;
-import android.nearby.aidl.ByteArrayParcel;
-import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
-import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
-import android.nearby.aidl.FastPairDeviceMetadataParcel;
-import android.nearby.aidl.FastPairDiscoveryItemParcel;
-import android.nearby.aidl.FastPairEligibleAccountParcel;
-import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
-import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
-import android.nearby.aidl.FastPairManageAccountRequestParcel;
-import android.nearby.aidl.IFastPairAccountDevicesMetadataCallback;
-import android.nearby.aidl.IFastPairAntispoofKeyDeviceMetadataCallback;
-import android.nearby.aidl.IFastPairDataProvider;
-import android.nearby.aidl.IFastPairEligibleAccountsCallback;
-import android.nearby.aidl.IFastPairManageAccountCallback;
-import android.nearby.aidl.IFastPairManageAccountDeviceCallback;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-
-import com.google.common.collect.ImmutableList;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-
-@RunWith(AndroidJUnit4.class)
-public class FastPairDataProviderServiceTest {
-
-    private static final String TAG = "FastPairDataProviderServiceTest";
-
-    private static final int BLE_TX_POWER  = 5;
-    private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
-    private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
-    private static final float DELTA = 0.001f;
-    private static final int DEVICE_TYPE = 7;
-    private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
-            "DOWNLOAD_COMPANION_APP_DESCRIPTION";
-    private static final Account ELIGIBLE_ACCOUNT_1 = new Account("abc@google.com", "type1");
-    private static final boolean ELIGIBLE_ACCOUNT_1_OPT_IN = true;
-    private static final Account ELIGIBLE_ACCOUNT_2 = new Account("def@gmail.com", "type2");
-    private static final boolean ELIGIBLE_ACCOUNT_2_OPT_IN = false;
-    private static final Account MANAGE_ACCOUNT = new Account("ghi@gmail.com", "type3");
-    private static final Account ACCOUNTDEVICES_METADATA_ACCOUNT =
-            new Account("jk@gmail.com", "type4");
-    private static final int NUM_ACCOUNT_DEVICES = 2;
-
-    private static final int ERROR_CODE_BAD_REQUEST =
-            FastPairDataProviderService.ERROR_CODE_BAD_REQUEST;
-    private static final int MANAGE_ACCOUNT_REQUEST_TYPE =
-            FastPairDataProviderService.MANAGE_REQUEST_ADD;
-    private static final String ERROR_STRING = "ERROR_STRING";
-    private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
-            "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
-    private static final byte[] IMAGE = new byte[] {7, 9};
-    private static final String IMAGE_URL = "IMAGE_URL";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION =
-            "INITIAL_NOTIFICATION_DESCRIPTION";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
-            "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
-    private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
-    private static final String INTENT_URI = "INTENT_URI";
-    private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
-    private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
-            "RETRO_ACTIVE_PAIRING_DESCRIPTION";
-    private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
-    private static final float TRIGGER_DISTANCE = 111;
-    private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
-    private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
-    private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
-    private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
-    private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
-    private static final String UPDATE_COMPANION_APP_DESCRIPTION =
-            "UPDATE_COMPANION_APP_DESCRIPTION";
-    private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
-            "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
-    private static final byte[] ACCOUNT_KEY = new byte[] {3};
-    private static final byte[] ACCOUNT_KEY_2 = new byte[] {9, 3};
-    private static final byte[] SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS = new byte[] {2, 8};
-    private static final byte[] REQUEST_MODEL_ID = new byte[] {1, 2, 3};
-    private static final byte[] ANTI_SPOOFING_KEY = new byte[] {4, 5, 6};
-    private static final String ACTION_URL = "ACTION_URL";
-    private static final int ACTION_URL_TYPE = 5;
-    private static final String APP_NAME = "APP_NAME";
-    private static final byte[] AUTHENTICATION_PUBLIC_KEY_SEC_P256R1 = new byte[] {5, 7};
-    private static final String DESCRIPTION = "DESCRIPTION";
-    private static final String DEVICE_NAME = "DEVICE_NAME";
-    private static final String DISPLAY_URL = "DISPLAY_URL";
-    private static final long FIRST_OBSERVATION_TIMESTAMP_MILLIS = 8393L;
-    private static final String  ICON_FIFE_URL = "ICON_FIFE_URL";
-    private static final byte[]  ICON_PNG = new byte[]{2, 5};
-    private static final String ID = "ID";
-    private static final long LAST_OBSERVATION_TIMESTAMP_MILLIS = 934234L;
-    private static final String MAC_ADDRESS = "MAC_ADDRESS";
-    private static final String NAME = "NAME";
-    private static final String PACKAGE_NAME = "PACKAGE_NAME";
-    private static final long PENDING_APP_INSTALL_TIMESTAMP_MILLIS = 832393L;
-    private static final int RSSI = 9;
-    private static final int STATE = 63;
-    private static final String TITLE = "TITLE";
-    private static final String TRIGGER_ID = "TRIGGER_ID";
-    private static final int TX_POWER = 62;
-
-    private static final int ELIGIBLE_ACCOUNTS_NUM = 2;
-    private static final ImmutableList<FastPairEligibleAccount> ELIGIBLE_ACCOUNTS =
-            ImmutableList.of(
-                    genHappyPathFastPairEligibleAccount(ELIGIBLE_ACCOUNT_1,
-                            ELIGIBLE_ACCOUNT_1_OPT_IN),
-                    genHappyPathFastPairEligibleAccount(ELIGIBLE_ACCOUNT_2,
-                            ELIGIBLE_ACCOUNT_2_OPT_IN));
-    private static final int ACCOUNTKEY_DEVICE_NUM = 2;
-    private static final ImmutableList<FastPairAccountKeyDeviceMetadata>
-            FAST_PAIR_ACCOUNT_DEVICES_METADATA =
-            ImmutableList.of(
-                    genHappyPathFastPairAccountkeyDeviceMetadata(),
-                    genHappyPathFastPairAccountkeyDeviceMetadata());
-
-    private static final FastPairAntispoofKeyDeviceMetadataRequestParcel
-            FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL =
-            genFastPairAntispoofKeyDeviceMetadataRequestParcel();
-    private static final FastPairAccountDevicesMetadataRequestParcel
-            FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL =
-            genFastPairAccountDevicesMetadataRequestParcel();
-    private static final FastPairEligibleAccountsRequestParcel
-            FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL =
-            genFastPairEligibleAccountsRequestParcel();
-    private static final FastPairManageAccountRequestParcel
-            FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL =
-            genFastPairManageAccountRequestParcel();
-    private static final FastPairManageAccountDeviceRequestParcel
-            FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL =
-            genFastPairManageAccountDeviceRequestParcel();
-    private static final FastPairAntispoofKeyDeviceMetadata
-            HAPPY_PATH_FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA =
-            genHappyPathFastPairAntispoofKeyDeviceMetadata();
-
-    @Captor private ArgumentCaptor<FastPairEligibleAccountParcel[]>
-            mFastPairEligibleAccountParcelsArgumentCaptor;
-    @Captor private ArgumentCaptor<FastPairAccountKeyDeviceMetadataParcel[]>
-            mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor;
-
-    @Mock private FastPairDataProviderService mMockFastPairDataProviderService;
-    @Mock private IFastPairAntispoofKeyDeviceMetadataCallback.Stub
-            mAntispoofKeyDeviceMetadataCallback;
-    @Mock private IFastPairAccountDevicesMetadataCallback.Stub mAccountDevicesMetadataCallback;
-    @Mock private IFastPairEligibleAccountsCallback.Stub mEligibleAccountsCallback;
-    @Mock private IFastPairManageAccountCallback.Stub mManageAccountCallback;
-    @Mock private IFastPairManageAccountDeviceCallback.Stub mManageAccountDeviceCallback;
-
-    private MyHappyPathProvider mHappyPathFastPairDataProvider;
-    private MyErrorPathProvider mErrorPathFastPairDataProvider;
-
-    @Before
-    public void setUp() throws Exception {
-        initMocks(this);
-
-        mHappyPathFastPairDataProvider =
-                new MyHappyPathProvider(TAG, mMockFastPairDataProviderService);
-        mErrorPathFastPairDataProvider =
-                new MyErrorPathProvider(TAG, mMockFastPairDataProviderService);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathLoadFastPairAntispoofKeyDeviceMetadata() throws Exception {
-        // AOSP sends calls to OEM via Parcelable.
-        mHappyPathFastPairDataProvider.asProvider().loadFastPairAntispoofKeyDeviceMetadata(
-                FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL,
-                mAntispoofKeyDeviceMetadataCallback);
-
-        // OEM receives request and verifies that it is as expected.
-        final ArgumentCaptor<FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest>
-                fastPairAntispoofKeyDeviceMetadataRequestCaptor =
-                ArgumentCaptor.forClass(
-                        FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest.class
-                );
-        verify(mMockFastPairDataProviderService).onLoadFastPairAntispoofKeyDeviceMetadata(
-                fastPairAntispoofKeyDeviceMetadataRequestCaptor.capture(),
-                any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback.class));
-        ensureHappyPathAsExpected(fastPairAntispoofKeyDeviceMetadataRequestCaptor.getValue());
-
-        // AOSP receives responses and verifies that it is as expected.
-        final ArgumentCaptor<FastPairAntispoofKeyDeviceMetadataParcel>
-                fastPairAntispoofKeyDeviceMetadataParcelCaptor =
-                ArgumentCaptor.forClass(FastPairAntispoofKeyDeviceMetadataParcel.class);
-        verify(mAntispoofKeyDeviceMetadataCallback).onFastPairAntispoofKeyDeviceMetadataReceived(
-                fastPairAntispoofKeyDeviceMetadataParcelCaptor.capture());
-        ensureHappyPathAsExpected(fastPairAntispoofKeyDeviceMetadataParcelCaptor.getValue());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathLoadFastPairAccountDevicesMetadata() throws Exception {
-        // AOSP sends calls to OEM via Parcelable.
-        mHappyPathFastPairDataProvider.asProvider().loadFastPairAccountDevicesMetadata(
-                FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL,
-                mAccountDevicesMetadataCallback);
-
-        // OEM receives request and verifies that it is as expected.
-        final ArgumentCaptor<FastPairDataProviderService.FastPairAccountDevicesMetadataRequest>
-                fastPairAccountDevicesMetadataRequestCaptor =
-                ArgumentCaptor.forClass(
-                        FastPairDataProviderService.FastPairAccountDevicesMetadataRequest.class);
-        verify(mMockFastPairDataProviderService).onLoadFastPairAccountDevicesMetadata(
-                fastPairAccountDevicesMetadataRequestCaptor.capture(),
-                any(FastPairDataProviderService.FastPairAccountDevicesMetadataCallback.class));
-        ensureHappyPathAsExpected(fastPairAccountDevicesMetadataRequestCaptor.getValue());
-
-        // AOSP receives responses and verifies that it is as expected.
-        verify(mAccountDevicesMetadataCallback).onFastPairAccountDevicesMetadataReceived(
-                mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor.capture());
-        ensureHappyPathAsExpected(
-                mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor.getValue());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathLoadFastPairEligibleAccounts() throws Exception {
-        // AOSP sends calls to OEM via Parcelable.
-        mHappyPathFastPairDataProvider.asProvider().loadFastPairEligibleAccounts(
-                FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL,
-                mEligibleAccountsCallback);
-
-        // OEM receives request and verifies that it is as expected.
-        final ArgumentCaptor<FastPairDataProviderService.FastPairEligibleAccountsRequest>
-                fastPairEligibleAccountsRequestCaptor =
-                ArgumentCaptor.forClass(
-                        FastPairDataProviderService.FastPairEligibleAccountsRequest.class);
-        verify(mMockFastPairDataProviderService).onLoadFastPairEligibleAccounts(
-                fastPairEligibleAccountsRequestCaptor.capture(),
-                any(FastPairDataProviderService.FastPairEligibleAccountsCallback.class));
-        ensureHappyPathAsExpected(fastPairEligibleAccountsRequestCaptor.getValue());
-
-        // AOSP receives responses and verifies that it is as expected.
-        verify(mEligibleAccountsCallback).onFastPairEligibleAccountsReceived(
-                mFastPairEligibleAccountParcelsArgumentCaptor.capture());
-        ensureHappyPathAsExpected(mFastPairEligibleAccountParcelsArgumentCaptor.getValue());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathManageFastPairAccount() throws Exception {
-        // AOSP sends calls to OEM via Parcelable.
-        mHappyPathFastPairDataProvider.asProvider().manageFastPairAccount(
-                FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL,
-                mManageAccountCallback);
-
-        // OEM receives request and verifies that it is as expected.
-        final ArgumentCaptor<FastPairDataProviderService.FastPairManageAccountRequest>
-                fastPairManageAccountRequestCaptor =
-                ArgumentCaptor.forClass(
-                        FastPairDataProviderService.FastPairManageAccountRequest.class);
-        verify(mMockFastPairDataProviderService).onManageFastPairAccount(
-                fastPairManageAccountRequestCaptor.capture(),
-                any(FastPairDataProviderService.FastPairManageActionCallback.class));
-        ensureHappyPathAsExpected(fastPairManageAccountRequestCaptor.getValue());
-
-        // AOSP receives SUCCESS response.
-        verify(mManageAccountCallback).onSuccess();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathManageFastPairAccountDevice() throws Exception {
-        // AOSP sends calls to OEM via Parcelable.
-        mHappyPathFastPairDataProvider.asProvider().manageFastPairAccountDevice(
-                FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL,
-                mManageAccountDeviceCallback);
-
-        // OEM receives request and verifies that it is as expected.
-        final ArgumentCaptor<FastPairDataProviderService.FastPairManageAccountDeviceRequest>
-                fastPairManageAccountDeviceRequestCaptor =
-                ArgumentCaptor.forClass(
-                        FastPairDataProviderService.FastPairManageAccountDeviceRequest.class);
-        verify(mMockFastPairDataProviderService).onManageFastPairAccountDevice(
-                fastPairManageAccountDeviceRequestCaptor.capture(),
-                any(FastPairDataProviderService.FastPairManageActionCallback.class));
-        ensureHappyPathAsExpected(fastPairManageAccountDeviceRequestCaptor.getValue());
-
-        // AOSP receives SUCCESS response.
-        verify(mManageAccountDeviceCallback).onSuccess();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testErrorPathLoadFastPairAntispoofKeyDeviceMetadata() throws Exception {
-        mErrorPathFastPairDataProvider.asProvider().loadFastPairAntispoofKeyDeviceMetadata(
-                FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL,
-                mAntispoofKeyDeviceMetadataCallback);
-        verify(mMockFastPairDataProviderService).onLoadFastPairAntispoofKeyDeviceMetadata(
-                any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest.class),
-                any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback.class));
-        verify(mAntispoofKeyDeviceMetadataCallback).onError(
-                eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testErrorPathLoadFastPairAccountDevicesMetadata() throws Exception {
-        mErrorPathFastPairDataProvider.asProvider().loadFastPairAccountDevicesMetadata(
-                FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL,
-                mAccountDevicesMetadataCallback);
-        verify(mMockFastPairDataProviderService).onLoadFastPairAccountDevicesMetadata(
-                any(FastPairDataProviderService.FastPairAccountDevicesMetadataRequest.class),
-                any(FastPairDataProviderService.FastPairAccountDevicesMetadataCallback.class));
-        verify(mAccountDevicesMetadataCallback).onError(
-                eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testErrorPathLoadFastPairEligibleAccounts() throws Exception {
-        mErrorPathFastPairDataProvider.asProvider().loadFastPairEligibleAccounts(
-                FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL,
-                mEligibleAccountsCallback);
-        verify(mMockFastPairDataProviderService).onLoadFastPairEligibleAccounts(
-                any(FastPairDataProviderService.FastPairEligibleAccountsRequest.class),
-                any(FastPairDataProviderService.FastPairEligibleAccountsCallback.class));
-        verify(mEligibleAccountsCallback).onError(
-                eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testErrorPathManageFastPairAccount() throws Exception {
-        mErrorPathFastPairDataProvider.asProvider().manageFastPairAccount(
-                FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL,
-                mManageAccountCallback);
-        verify(mMockFastPairDataProviderService).onManageFastPairAccount(
-                any(FastPairDataProviderService.FastPairManageAccountRequest.class),
-                any(FastPairDataProviderService.FastPairManageActionCallback.class));
-        verify(mManageAccountCallback).onError(eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testErrorPathManageFastPairAccountDevice() throws Exception {
-        mErrorPathFastPairDataProvider.asProvider().manageFastPairAccountDevice(
-                FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL,
-                mManageAccountDeviceCallback);
-        verify(mMockFastPairDataProviderService).onManageFastPairAccountDevice(
-                any(FastPairDataProviderService.FastPairManageAccountDeviceRequest.class),
-                any(FastPairDataProviderService.FastPairManageActionCallback.class));
-        verify(mManageAccountDeviceCallback).onError(eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
-    }
-
-    public static class MyHappyPathProvider extends FastPairDataProviderService {
-
-        private final FastPairDataProviderService mMockFastPairDataProviderService;
-
-        public MyHappyPathProvider(@NonNull String tag, FastPairDataProviderService mock) {
-            super(tag);
-            mMockFastPairDataProviderService = mock;
-        }
-
-        public IFastPairDataProvider asProvider() {
-            Intent intent = new Intent();
-            return IFastPairDataProvider.Stub.asInterface(onBind(intent));
-        }
-
-        @Override
-        public void onLoadFastPairAntispoofKeyDeviceMetadata(
-                @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
-                @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairAntispoofKeyDeviceMetadata(
-                    request, callback);
-            callback.onFastPairAntispoofKeyDeviceMetadataReceived(
-                    HAPPY_PATH_FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA);
-        }
-
-        @Override
-        public void onLoadFastPairAccountDevicesMetadata(
-                @NonNull FastPairAccountDevicesMetadataRequest request,
-                @NonNull FastPairAccountDevicesMetadataCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairAccountDevicesMetadata(
-                    request, callback);
-            callback.onFastPairAccountDevicesMetadataReceived(FAST_PAIR_ACCOUNT_DEVICES_METADATA);
-        }
-
-        @Override
-        public void onLoadFastPairEligibleAccounts(
-                @NonNull FastPairEligibleAccountsRequest request,
-                @NonNull FastPairEligibleAccountsCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairEligibleAccounts(
-                    request, callback);
-            callback.onFastPairEligibleAccountsReceived(ELIGIBLE_ACCOUNTS);
-        }
-
-        @Override
-        public void onManageFastPairAccount(
-                @NonNull FastPairManageAccountRequest request,
-                @NonNull FastPairManageActionCallback callback) {
-            mMockFastPairDataProviderService.onManageFastPairAccount(request, callback);
-            callback.onSuccess();
-        }
-
-        @Override
-        public void onManageFastPairAccountDevice(
-                @NonNull FastPairManageAccountDeviceRequest request,
-                @NonNull FastPairManageActionCallback callback) {
-            mMockFastPairDataProviderService.onManageFastPairAccountDevice(request, callback);
-            callback.onSuccess();
-        }
-    }
-
-    public static class MyErrorPathProvider extends FastPairDataProviderService {
-
-        private final FastPairDataProviderService mMockFastPairDataProviderService;
-
-        public MyErrorPathProvider(@NonNull String tag, FastPairDataProviderService mock) {
-            super(tag);
-            mMockFastPairDataProviderService = mock;
-        }
-
-        public IFastPairDataProvider asProvider() {
-            Intent intent = new Intent();
-            return IFastPairDataProvider.Stub.asInterface(onBind(intent));
-        }
-
-        @Override
-        public void onLoadFastPairAntispoofKeyDeviceMetadata(
-                @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
-                @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairAntispoofKeyDeviceMetadata(
-                    request, callback);
-            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
-        }
-
-        @Override
-        public void onLoadFastPairAccountDevicesMetadata(
-                @NonNull FastPairAccountDevicesMetadataRequest request,
-                @NonNull FastPairAccountDevicesMetadataCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairAccountDevicesMetadata(
-                    request, callback);
-            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
-        }
-
-        @Override
-        public void onLoadFastPairEligibleAccounts(
-                @NonNull FastPairEligibleAccountsRequest request,
-                @NonNull FastPairEligibleAccountsCallback callback) {
-            mMockFastPairDataProviderService.onLoadFastPairEligibleAccounts(request, callback);
-            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
-        }
-
-        @Override
-        public void onManageFastPairAccount(
-                @NonNull FastPairManageAccountRequest request,
-                @NonNull FastPairManageActionCallback callback) {
-            mMockFastPairDataProviderService.onManageFastPairAccount(request, callback);
-            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
-        }
-
-        @Override
-        public void onManageFastPairAccountDevice(
-                @NonNull FastPairManageAccountDeviceRequest request,
-                @NonNull FastPairManageActionCallback callback) {
-            mMockFastPairDataProviderService.onManageFastPairAccountDevice(request, callback);
-            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
-        }
-    }
-
-    /* Generates AntispoofKeyDeviceMetadataRequestParcel. */
-    private static FastPairAntispoofKeyDeviceMetadataRequestParcel
-            genFastPairAntispoofKeyDeviceMetadataRequestParcel() {
-        FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel =
-                new FastPairAntispoofKeyDeviceMetadataRequestParcel();
-        requestParcel.modelId = REQUEST_MODEL_ID;
-
-        return requestParcel;
-    }
-
-    /* Generates AccountDevicesMetadataRequestParcel. */
-    private static FastPairAccountDevicesMetadataRequestParcel
-            genFastPairAccountDevicesMetadataRequestParcel() {
-        FastPairAccountDevicesMetadataRequestParcel requestParcel =
-                new FastPairAccountDevicesMetadataRequestParcel();
-
-        requestParcel.account = ACCOUNTDEVICES_METADATA_ACCOUNT;
-        requestParcel.deviceAccountKeys = new ByteArrayParcel[NUM_ACCOUNT_DEVICES];
-        requestParcel.deviceAccountKeys[0] = new ByteArrayParcel();
-        requestParcel.deviceAccountKeys[1] = new ByteArrayParcel();
-        requestParcel.deviceAccountKeys[0].byteArray = ACCOUNT_KEY;
-        requestParcel.deviceAccountKeys[1].byteArray = ACCOUNT_KEY_2;
-
-        return requestParcel;
-    }
-
-    /* Generates FastPairEligibleAccountsRequestParcel. */
-    private static FastPairEligibleAccountsRequestParcel
-            genFastPairEligibleAccountsRequestParcel() {
-        FastPairEligibleAccountsRequestParcel requestParcel =
-                new FastPairEligibleAccountsRequestParcel();
-        // No fields since FastPairEligibleAccountsRequestParcel is just a place holder now.
-        return requestParcel;
-    }
-
-    /* Generates FastPairManageAccountRequestParcel. */
-    private static FastPairManageAccountRequestParcel
-            genFastPairManageAccountRequestParcel() {
-        FastPairManageAccountRequestParcel requestParcel =
-                new FastPairManageAccountRequestParcel();
-        requestParcel.account = MANAGE_ACCOUNT;
-        requestParcel.requestType = MANAGE_ACCOUNT_REQUEST_TYPE;
-
-        return requestParcel;
-    }
-
-    /* Generates FastPairManageAccountDeviceRequestParcel. */
-    private static FastPairManageAccountDeviceRequestParcel
-            genFastPairManageAccountDeviceRequestParcel() {
-        FastPairManageAccountDeviceRequestParcel requestParcel =
-                new FastPairManageAccountDeviceRequestParcel();
-        requestParcel.account = MANAGE_ACCOUNT;
-        requestParcel.requestType = MANAGE_ACCOUNT_REQUEST_TYPE;
-        requestParcel.accountKeyDeviceMetadata =
-                genHappyPathFastPairAccountkeyDeviceMetadataParcel();
-
-        return requestParcel;
-    }
-
-    /* Generates Happy Path AntispoofKeyDeviceMetadata. */
-    private static FastPairAntispoofKeyDeviceMetadata
-            genHappyPathFastPairAntispoofKeyDeviceMetadata() {
-        FastPairAntispoofKeyDeviceMetadata.Builder builder =
-                new FastPairAntispoofKeyDeviceMetadata.Builder();
-        builder.setAntispoofPublicKey(ANTI_SPOOFING_KEY);
-        builder.setFastPairDeviceMetadata(genHappyPathFastPairDeviceMetadata());
-
-        return builder.build();
-    }
-
-    /* Generates Happy Path FastPairAccountKeyDeviceMetadata. */
-    private static FastPairAccountKeyDeviceMetadata
-            genHappyPathFastPairAccountkeyDeviceMetadata() {
-        FastPairAccountKeyDeviceMetadata.Builder builder =
-                new FastPairAccountKeyDeviceMetadata.Builder();
-        builder.setDeviceAccountKey(ACCOUNT_KEY);
-        builder.setFastPairDeviceMetadata(genHappyPathFastPairDeviceMetadata());
-        builder.setSha256DeviceAccountKeyPublicAddress(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
-        builder.setFastPairDiscoveryItem(genHappyPathFastPairDiscoveryItem());
-
-        return builder.build();
-    }
-
-    /* Generates Happy Path FastPairAccountKeyDeviceMetadataParcel. */
-    private static FastPairAccountKeyDeviceMetadataParcel
-            genHappyPathFastPairAccountkeyDeviceMetadataParcel() {
-        FastPairAccountKeyDeviceMetadataParcel parcel =
-                new FastPairAccountKeyDeviceMetadataParcel();
-        parcel.deviceAccountKey = ACCOUNT_KEY;
-        parcel.metadata = genHappyPathFastPairDeviceMetadataParcel();
-        parcel.sha256DeviceAccountKeyPublicAddress = SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS;
-        parcel.discoveryItem = genHappyPathFastPairDiscoveryItemParcel();
-
-        return parcel;
-    }
-
-    /* Generates Happy Path DiscoveryItem. */
-    private static FastPairDiscoveryItem genHappyPathFastPairDiscoveryItem() {
-        FastPairDiscoveryItem.Builder builder = new FastPairDiscoveryItem.Builder();
-
-        builder.setActionUrl(ACTION_URL);
-        builder.setActionUrlType(ACTION_URL_TYPE);
-        builder.setAppName(APP_NAME);
-        builder.setAuthenticationPublicKeySecp256r1(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
-        builder.setDescription(DESCRIPTION);
-        builder.setDeviceName(DEVICE_NAME);
-        builder.setDisplayUrl(DISPLAY_URL);
-        builder.setFirstObservationTimestampMillis(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
-        builder.setIconFfeUrl(ICON_FIFE_URL);
-        builder.setIconPng(ICON_PNG);
-        builder.setId(ID);
-        builder.setLastObservationTimestampMillis(LAST_OBSERVATION_TIMESTAMP_MILLIS);
-        builder.setMacAddress(MAC_ADDRESS);
-        builder.setPackageName(PACKAGE_NAME);
-        builder.setPendingAppInstallTimestampMillis(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
-        builder.setRssi(RSSI);
-        builder.setState(STATE);
-        builder.setTitle(TITLE);
-        builder.setTriggerId(TRIGGER_ID);
-        builder.setTxPower(TX_POWER);
-
-        return builder.build();
-    }
-
-    /* Generates Happy Path DiscoveryItemParcel. */
-    private static FastPairDiscoveryItemParcel genHappyPathFastPairDiscoveryItemParcel() {
-        FastPairDiscoveryItemParcel parcel = new FastPairDiscoveryItemParcel();
-
-        parcel.actionUrl = ACTION_URL;
-        parcel.actionUrlType = ACTION_URL_TYPE;
-        parcel.appName = APP_NAME;
-        parcel.authenticationPublicKeySecp256r1 = AUTHENTICATION_PUBLIC_KEY_SEC_P256R1;
-        parcel.description = DESCRIPTION;
-        parcel.deviceName = DEVICE_NAME;
-        parcel.displayUrl = DISPLAY_URL;
-        parcel.firstObservationTimestampMillis = FIRST_OBSERVATION_TIMESTAMP_MILLIS;
-        parcel.iconFifeUrl = ICON_FIFE_URL;
-        parcel.iconPng = ICON_PNG;
-        parcel.id = ID;
-        parcel.lastObservationTimestampMillis = LAST_OBSERVATION_TIMESTAMP_MILLIS;
-        parcel.macAddress = MAC_ADDRESS;
-        parcel.packageName = PACKAGE_NAME;
-        parcel.pendingAppInstallTimestampMillis = PENDING_APP_INSTALL_TIMESTAMP_MILLIS;
-        parcel.rssi = RSSI;
-        parcel.state = STATE;
-        parcel.title = TITLE;
-        parcel.triggerId = TRIGGER_ID;
-        parcel.txPower = TX_POWER;
-
-        return parcel;
-    }
-
-    /* Generates Happy Path DeviceMetadata. */
-    private static FastPairDeviceMetadata genHappyPathFastPairDeviceMetadata() {
-        FastPairDeviceMetadata.Builder builder = new FastPairDeviceMetadata.Builder();
-        builder.setBleTxPower(BLE_TX_POWER);
-        builder.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        builder.setConnectSuccessCompanionAppNotInstalled(
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-        builder.setDeviceType(DEVICE_TYPE);
-        builder.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION);
-        builder.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-        builder.setImage(IMAGE);
-        builder.setImageUrl(IMAGE_URL);
-        builder.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION);
-        builder.setInitialNotificationDescriptionNoAccount(
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        builder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
-        builder.setIntentUri(INTENT_URI);
-        builder.setName(NAME);
-        builder.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION);
-        builder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
-        builder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
-        builder.setTriggerDistance(TRIGGER_DISTANCE);
-        builder.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE);
-        builder.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        builder.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        builder.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION);
-        builder.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE);
-        builder.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION);
-        builder.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-
-        return builder.build();
-    }
-
-    /* Generates Happy Path DeviceMetadataParcel. */
-    private static FastPairDeviceMetadataParcel genHappyPathFastPairDeviceMetadataParcel() {
-        FastPairDeviceMetadataParcel parcel = new FastPairDeviceMetadataParcel();
-
-        parcel.bleTxPower = BLE_TX_POWER;
-        parcel.connectSuccessCompanionAppInstalled = CONNECT_SUCCESS_COMPANION_APP_INSTALLED;
-        parcel.connectSuccessCompanionAppNotInstalled =
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED;
-        parcel.deviceType = DEVICE_TYPE;
-        parcel.downloadCompanionAppDescription = DOWNLOAD_COMPANION_APP_DESCRIPTION;
-        parcel.failConnectGoToSettingsDescription = FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION;
-        parcel.image = IMAGE;
-        parcel.imageUrl = IMAGE_URL;
-        parcel.initialNotificationDescription = INITIAL_NOTIFICATION_DESCRIPTION;
-        parcel.initialNotificationDescriptionNoAccount =
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT;
-        parcel.initialPairingDescription = INITIAL_PAIRING_DESCRIPTION;
-        parcel.intentUri = INTENT_URI;
-        parcel.name = NAME;
-        parcel.openCompanionAppDescription = OPEN_COMPANION_APP_DESCRIPTION;
-        parcel.retroactivePairingDescription = RETRO_ACTIVE_PAIRING_DESCRIPTION;
-        parcel.subsequentPairingDescription = SUBSEQUENT_PAIRING_DESCRIPTION;
-        parcel.triggerDistance = TRIGGER_DISTANCE;
-        parcel.trueWirelessImageUrlCase = TRUE_WIRELESS_IMAGE_URL_CASE;
-        parcel.trueWirelessImageUrlLeftBud = TRUE_WIRELESS_IMAGE_URL_LEFT_BUD;
-        parcel.trueWirelessImageUrlRightBud = TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD;
-        parcel.unableToConnectDescription = UNABLE_TO_CONNECT_DESCRIPTION;
-        parcel.unableToConnectTitle = UNABLE_TO_CONNECT_TITLE;
-        parcel.updateCompanionAppDescription = UPDATE_COMPANION_APP_DESCRIPTION;
-        parcel.waitLaunchCompanionAppDescription = WAIT_LAUNCH_COMPANION_APP_DESCRIPTION;
-
-        return parcel;
-    }
-
-    /* Generates Happy Path FastPairEligibleAccount. */
-    private static FastPairEligibleAccount genHappyPathFastPairEligibleAccount(
-            Account account, boolean optIn) {
-        FastPairEligibleAccount.Builder builder = new FastPairEligibleAccount.Builder();
-        builder.setAccount(account);
-        builder.setOptIn(optIn);
-
-        return builder.build();
-    }
-
-    /* Verifies Happy Path AntispoofKeyDeviceMetadataRequest. */
-    private static void ensureHappyPathAsExpected(
-            FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest request) {
-        assertThat(request.getModelId()).isEqualTo(REQUEST_MODEL_ID);
-    }
-
-    /* Verifies Happy Path AccountDevicesMetadataRequest. */
-    private static void ensureHappyPathAsExpected(
-            FastPairDataProviderService.FastPairAccountDevicesMetadataRequest request) {
-        assertThat(request.getAccount()).isEqualTo(ACCOUNTDEVICES_METADATA_ACCOUNT);
-        assertThat(request.getDeviceAccountKeys().size()).isEqualTo(ACCOUNTKEY_DEVICE_NUM);
-        assertThat(request.getDeviceAccountKeys()).contains(ACCOUNT_KEY);
-        assertThat(request.getDeviceAccountKeys()).contains(ACCOUNT_KEY_2);
-    }
-
-    /* Verifies Happy Path FastPairEligibleAccountsRequest. */
-    @SuppressWarnings("UnusedVariable")
-    private static void ensureHappyPathAsExpected(
-            FastPairDataProviderService.FastPairEligibleAccountsRequest request) {
-        // No fields since FastPairEligibleAccountsRequest is just a place holder now.
-    }
-
-    /* Verifies Happy Path FastPairManageAccountRequest. */
-    private static void ensureHappyPathAsExpected(
-            FastPairDataProviderService.FastPairManageAccountRequest request) {
-        assertThat(request.getAccount()).isEqualTo(MANAGE_ACCOUNT);
-        assertThat(request.getRequestType()).isEqualTo(MANAGE_ACCOUNT_REQUEST_TYPE);
-    }
-
-    /* Verifies Happy Path FastPairManageAccountDeviceRequest. */
-    private static void ensureHappyPathAsExpected(
-            FastPairDataProviderService.FastPairManageAccountDeviceRequest request) {
-        assertThat(request.getAccount()).isEqualTo(MANAGE_ACCOUNT);
-        assertThat(request.getRequestType()).isEqualTo(MANAGE_ACCOUNT_REQUEST_TYPE);
-        ensureHappyPathAsExpected(request.getAccountKeyDeviceMetadata());
-    }
-
-    /* Verifies Happy Path AntispoofKeyDeviceMetadataParcel. */
-    private static void ensureHappyPathAsExpected(
-            FastPairAntispoofKeyDeviceMetadataParcel metadataParcel) {
-        assertThat(metadataParcel).isNotNull();
-        assertThat(metadataParcel.antispoofPublicKey).isEqualTo(ANTI_SPOOFING_KEY);
-        ensureHappyPathAsExpected(metadataParcel.deviceMetadata);
-    }
-
-    /* Verifies Happy Path FastPairAccountKeyDeviceMetadataParcel[]. */
-    private static void ensureHappyPathAsExpected(
-            FastPairAccountKeyDeviceMetadataParcel[] metadataParcels) {
-        assertThat(metadataParcels).isNotNull();
-        assertThat(metadataParcels).hasLength(ACCOUNTKEY_DEVICE_NUM);
-        for (FastPairAccountKeyDeviceMetadataParcel parcel: metadataParcels) {
-            ensureHappyPathAsExpected(parcel);
-        }
-    }
-
-    /* Verifies Happy Path FastPairAccountKeyDeviceMetadataParcel. */
-    private static void ensureHappyPathAsExpected(
-            FastPairAccountKeyDeviceMetadataParcel metadataParcel) {
-        assertThat(metadataParcel).isNotNull();
-        assertThat(metadataParcel.deviceAccountKey).isEqualTo(ACCOUNT_KEY);
-        assertThat(metadataParcel.sha256DeviceAccountKeyPublicAddress)
-                .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
-        ensureHappyPathAsExpected(metadataParcel.metadata);
-        ensureHappyPathAsExpected(metadataParcel.discoveryItem);
-    }
-
-    /* Verifies Happy Path FastPairAccountKeyDeviceMetadata. */
-    private static void ensureHappyPathAsExpected(
-            FastPairAccountKeyDeviceMetadata metadata) {
-        assertThat(metadata.getDeviceAccountKey()).isEqualTo(ACCOUNT_KEY);
-        assertThat(metadata.getSha256DeviceAccountKeyPublicAddress())
-                .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
-        ensureHappyPathAsExpected(metadata.getFastPairDeviceMetadata());
-        ensureHappyPathAsExpected(metadata.getFastPairDiscoveryItem());
-    }
-
-    /* Verifies Happy Path DeviceMetadataParcel. */
-    private static void ensureHappyPathAsExpected(FastPairDeviceMetadataParcel metadataParcel) {
-        assertThat(metadataParcel).isNotNull();
-        assertThat(metadataParcel.bleTxPower).isEqualTo(BLE_TX_POWER);
-
-        assertThat(metadataParcel.connectSuccessCompanionAppInstalled).isEqualTo(
-                CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        assertThat(metadataParcel.connectSuccessCompanionAppNotInstalled).isEqualTo(
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-
-        assertThat(metadataParcel.deviceType).isEqualTo(DEVICE_TYPE);
-        assertThat(metadataParcel.downloadCompanionAppDescription).isEqualTo(
-                DOWNLOAD_COMPANION_APP_DESCRIPTION);
-
-        assertThat(metadataParcel.failConnectGoToSettingsDescription).isEqualTo(
-                FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-
-        assertThat(metadataParcel.image).isEqualTo(IMAGE);
-        assertThat(metadataParcel.imageUrl).isEqualTo(IMAGE_URL);
-        assertThat(metadataParcel.initialNotificationDescription).isEqualTo(
-                INITIAL_NOTIFICATION_DESCRIPTION);
-        assertThat(metadataParcel.initialNotificationDescriptionNoAccount).isEqualTo(
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        assertThat(metadataParcel.initialPairingDescription).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
-        assertThat(metadataParcel.intentUri).isEqualTo(INTENT_URI);
-
-        assertThat(metadataParcel.name).isEqualTo(NAME);
-
-        assertThat(metadataParcel.openCompanionAppDescription).isEqualTo(
-                OPEN_COMPANION_APP_DESCRIPTION);
-
-        assertThat(metadataParcel.retroactivePairingDescription).isEqualTo(
-                RETRO_ACTIVE_PAIRING_DESCRIPTION);
-
-        assertThat(metadataParcel.subsequentPairingDescription).isEqualTo(
-                SUBSEQUENT_PAIRING_DESCRIPTION);
-
-        assertThat(metadataParcel.triggerDistance).isWithin(DELTA).of(TRIGGER_DISTANCE);
-        assertThat(metadataParcel.trueWirelessImageUrlCase).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
-        assertThat(metadataParcel.trueWirelessImageUrlLeftBud).isEqualTo(
-                TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        assertThat(metadataParcel.trueWirelessImageUrlRightBud).isEqualTo(
-                TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-
-        assertThat(metadataParcel.unableToConnectDescription).isEqualTo(
-                UNABLE_TO_CONNECT_DESCRIPTION);
-        assertThat(metadataParcel.unableToConnectTitle).isEqualTo(UNABLE_TO_CONNECT_TITLE);
-        assertThat(metadataParcel.updateCompanionAppDescription).isEqualTo(
-                UPDATE_COMPANION_APP_DESCRIPTION);
-
-        assertThat(metadataParcel.waitLaunchCompanionAppDescription).isEqualTo(
-                WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-    }
-
-    /* Verifies Happy Path DeviceMetadata. */
-    private static void ensureHappyPathAsExpected(FastPairDeviceMetadata metadata) {
-        assertThat(metadata.getBleTxPower()).isEqualTo(BLE_TX_POWER);
-        assertThat(metadata.getConnectSuccessCompanionAppInstalled())
-                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        assertThat(metadata.getConnectSuccessCompanionAppNotInstalled())
-                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-        assertThat(metadata.getDeviceType()).isEqualTo(DEVICE_TYPE);
-        assertThat(metadata.getDownloadCompanionAppDescription())
-                .isEqualTo(DOWNLOAD_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getFailConnectGoToSettingsDescription())
-                .isEqualTo(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-        assertThat(metadata.getImage()).isEqualTo(IMAGE);
-        assertThat(metadata.getImageUrl()).isEqualTo(IMAGE_URL);
-        assertThat(metadata.getInitialNotificationDescription())
-                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION);
-        assertThat(metadata.getInitialNotificationDescriptionNoAccount())
-                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        assertThat(metadata.getInitialPairingDescription()).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
-        assertThat(metadata.getIntentUri()).isEqualTo(INTENT_URI);
-        assertThat(metadata.getName()).isEqualTo(NAME);
-        assertThat(metadata.getOpenCompanionAppDescription())
-                .isEqualTo(OPEN_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getRetroactivePairingDescription())
-                .isEqualTo(RETRO_ACTIVE_PAIRING_DESCRIPTION);
-        assertThat(metadata.getSubsequentPairingDescription())
-                .isEqualTo(SUBSEQUENT_PAIRING_DESCRIPTION);
-        assertThat(metadata.getTriggerDistance()).isWithin(DELTA).of(TRIGGER_DISTANCE);
-        assertThat(metadata.getTrueWirelessImageUrlCase()).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
-        assertThat(metadata.getTrueWirelessImageUrlLeftBud())
-                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        assertThat(metadata.getTrueWirelessImageUrlRightBud())
-                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        assertThat(metadata.getUnableToConnectDescription())
-                .isEqualTo(UNABLE_TO_CONNECT_DESCRIPTION);
-        assertThat(metadata.getUnableToConnectTitle()).isEqualTo(UNABLE_TO_CONNECT_TITLE);
-        assertThat(metadata.getUpdateCompanionAppDescription())
-                .isEqualTo(UPDATE_COMPANION_APP_DESCRIPTION);
-        assertThat(metadata.getWaitLaunchCompanionAppDescription())
-                .isEqualTo(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-    }
-
-    /* Verifies Happy Path FastPairDiscoveryItemParcel. */
-    private static void ensureHappyPathAsExpected(FastPairDiscoveryItemParcel itemParcel) {
-        assertThat(itemParcel.actionUrl).isEqualTo(ACTION_URL);
-        assertThat(itemParcel.actionUrlType).isEqualTo(ACTION_URL_TYPE);
-        assertThat(itemParcel.appName).isEqualTo(APP_NAME);
-        assertThat(itemParcel.authenticationPublicKeySecp256r1)
-                .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
-        assertThat(itemParcel.description).isEqualTo(DESCRIPTION);
-        assertThat(itemParcel.deviceName).isEqualTo(DEVICE_NAME);
-        assertThat(itemParcel.displayUrl).isEqualTo(DISPLAY_URL);
-        assertThat(itemParcel.firstObservationTimestampMillis)
-                .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
-        assertThat(itemParcel.iconFifeUrl).isEqualTo(ICON_FIFE_URL);
-        assertThat(itemParcel.iconPng).isEqualTo(ICON_PNG);
-        assertThat(itemParcel.id).isEqualTo(ID);
-        assertThat(itemParcel.lastObservationTimestampMillis)
-                .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS);
-        assertThat(itemParcel.macAddress).isEqualTo(MAC_ADDRESS);
-        assertThat(itemParcel.packageName).isEqualTo(PACKAGE_NAME);
-        assertThat(itemParcel.pendingAppInstallTimestampMillis)
-                .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
-        assertThat(itemParcel.rssi).isEqualTo(RSSI);
-        assertThat(itemParcel.state).isEqualTo(STATE);
-        assertThat(itemParcel.title).isEqualTo(TITLE);
-        assertThat(itemParcel.triggerId).isEqualTo(TRIGGER_ID);
-        assertThat(itemParcel.txPower).isEqualTo(TX_POWER);
-    }
-
-    /* Verifies Happy Path FastPairDiscoveryItem. */
-    private static void ensureHappyPathAsExpected(FastPairDiscoveryItem item) {
-        assertThat(item.getActionUrl()).isEqualTo(ACTION_URL);
-        assertThat(item.getActionUrlType()).isEqualTo(ACTION_URL_TYPE);
-        assertThat(item.getAppName()).isEqualTo(APP_NAME);
-        assertThat(item.getAuthenticationPublicKeySecp256r1())
-                .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
-        assertThat(item.getDescription()).isEqualTo(DESCRIPTION);
-        assertThat(item.getDeviceName()).isEqualTo(DEVICE_NAME);
-        assertThat(item.getDisplayUrl()).isEqualTo(DISPLAY_URL);
-        assertThat(item.getFirstObservationTimestampMillis())
-                .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
-        assertThat(item.getIconFfeUrl()).isEqualTo(ICON_FIFE_URL);
-        assertThat(item.getIconPng()).isEqualTo(ICON_PNG);
-        assertThat(item.getId()).isEqualTo(ID);
-        assertThat(item.getLastObservationTimestampMillis())
-                .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS);
-        assertThat(item.getMacAddress()).isEqualTo(MAC_ADDRESS);
-        assertThat(item.getPackageName()).isEqualTo(PACKAGE_NAME);
-        assertThat(item.getPendingAppInstallTimestampMillis())
-                .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
-        assertThat(item.getRssi()).isEqualTo(RSSI);
-        assertThat(item.getState()).isEqualTo(STATE);
-        assertThat(item.getTitle()).isEqualTo(TITLE);
-        assertThat(item.getTriggerId()).isEqualTo(TRIGGER_ID);
-        assertThat(item.getTxPower()).isEqualTo(TX_POWER);
-    }
-
-    /* Verifies Happy Path EligibleAccountParcel[]. */
-    private static void ensureHappyPathAsExpected(FastPairEligibleAccountParcel[] accountsParcel) {
-        assertThat(accountsParcel).hasLength(ELIGIBLE_ACCOUNTS_NUM);
-
-        assertThat(accountsParcel[0].account).isEqualTo(ELIGIBLE_ACCOUNT_1);
-        assertThat(accountsParcel[0].optIn).isEqualTo(ELIGIBLE_ACCOUNT_1_OPT_IN);
-
-        assertThat(accountsParcel[1].account).isEqualTo(ELIGIBLE_ACCOUNT_2);
-        assertThat(accountsParcel[1].optIn).isEqualTo(ELIGIBLE_ACCOUNT_2_OPT_IN);
-    }
-}
diff --git a/nearby/tests/unit/src/android/nearby/FastPairEligibleAccountTest.java b/nearby/tests/unit/src/android/nearby/FastPairEligibleAccountTest.java
deleted file mode 100644
index da5a518..0000000
--- a/nearby/tests/unit/src/android/nearby/FastPairEligibleAccountTest.java
+++ /dev/null
@@ -1,66 +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.nearby;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.accounts.Account;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class FastPairEligibleAccountTest {
-
-    private static final Account ACCOUNT = new Account("abc@google.com", "type1");
-    private static final Account ACCOUNT_NULL = null;
-
-    private static final boolean OPT_IN_TRUE = true;
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetGetFastPairEligibleAccountNotNull() {
-        FastPairEligibleAccount eligibleAccount =
-                genFastPairEligibleAccount(ACCOUNT, OPT_IN_TRUE);
-
-        assertThat(eligibleAccount.getAccount()).isEqualTo(ACCOUNT);
-        assertThat(eligibleAccount.isOptIn()).isEqualTo(OPT_IN_TRUE);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetGetFastPairEligibleAccountNull() {
-        FastPairEligibleAccount eligibleAccount =
-                genFastPairEligibleAccount(ACCOUNT_NULL, OPT_IN_TRUE);
-
-        assertThat(eligibleAccount.getAccount()).isEqualTo(ACCOUNT_NULL);
-        assertThat(eligibleAccount.isOptIn()).isEqualTo(OPT_IN_TRUE);
-    }
-
-    /* Generates FastPairEligibleAccount. */
-    private static FastPairEligibleAccount genFastPairEligibleAccount(
-            Account account, boolean optIn) {
-        FastPairEligibleAccount.Builder builder = new FastPairEligibleAccount.Builder();
-        builder.setAccount(account);
-        builder.setOptIn(optIn);
-
-        return builder.build();
-    }
-}
diff --git a/nearby/tests/unit/src/android/nearby/PairStatusMetadataTest.java b/nearby/tests/unit/src/android/nearby/PairStatusMetadataTest.java
deleted file mode 100644
index 7bc6519..0000000
--- a/nearby/tests/unit/src/android/nearby/PairStatusMetadataTest.java
+++ /dev/null
@@ -1,105 +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.nearby;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.Parcel;
-
-import org.junit.Test;
-
-public class PairStatusMetadataTest {
-    private static final int UNKNOWN = 1000;
-    private static final int SUCCESS = 1001;
-    private static final int FAIL = 1002;
-    private static final int DISMISS = 1003;
-
-    @Test
-    public void statusToString() {
-        assertThat(PairStatusMetadata.statusToString(UNKNOWN)).isEqualTo("UNKNOWN");
-        assertThat(PairStatusMetadata.statusToString(SUCCESS)).isEqualTo("SUCCESS");
-        assertThat(PairStatusMetadata.statusToString(FAIL)).isEqualTo("FAIL");
-        assertThat(PairStatusMetadata.statusToString(DISMISS)).isEqualTo("DISMISS");
-    }
-
-    @Test
-    public void getStatus() {
-        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
-        assertThat(pairStatusMetadata.getStatus()).isEqualTo(1001);
-        pairStatusMetadata = new PairStatusMetadata(FAIL);
-        assertThat(pairStatusMetadata.getStatus()).isEqualTo(1002);
-        pairStatusMetadata = new PairStatusMetadata(DISMISS);
-        assertThat(pairStatusMetadata.getStatus()).isEqualTo(1003);
-        pairStatusMetadata = new PairStatusMetadata(UNKNOWN);
-        assertThat(pairStatusMetadata.getStatus()).isEqualTo(1000);
-    }
-
-    @Test
-    public void testToString() {
-        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
-        assertThat(pairStatusMetadata.toString())
-                .isEqualTo("PairStatusMetadata[ status=SUCCESS]");
-        pairStatusMetadata = new PairStatusMetadata(FAIL);
-        assertThat(pairStatusMetadata.toString())
-                .isEqualTo("PairStatusMetadata[ status=FAIL]");
-        pairStatusMetadata = new PairStatusMetadata(DISMISS);
-        assertThat(pairStatusMetadata.toString())
-                .isEqualTo("PairStatusMetadata[ status=DISMISS]");
-        pairStatusMetadata = new PairStatusMetadata(UNKNOWN);
-        assertThat(pairStatusMetadata.toString())
-                .isEqualTo("PairStatusMetadata[ status=UNKNOWN]");
-    }
-
-    @Test
-    public void testEquals() {
-        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
-        PairStatusMetadata pairStatusMetadata1 = new PairStatusMetadata(SUCCESS);
-        PairStatusMetadata pairStatusMetadata2 = new PairStatusMetadata(UNKNOWN);
-        assertThat(pairStatusMetadata.equals(pairStatusMetadata1)).isTrue();
-        assertThat(pairStatusMetadata.equals(pairStatusMetadata2)).isFalse();
-        assertThat(pairStatusMetadata.hashCode()).isEqualTo(pairStatusMetadata1.hashCode());
-    }
-
-    @Test
-    public void testParcelable() {
-        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
-        Parcel dest = Parcel.obtain();
-        pairStatusMetadata.writeToParcel(dest, 0);
-        dest.setDataPosition(0);
-        PairStatusMetadata comparStatusMetadata =
-                PairStatusMetadata.CREATOR.createFromParcel(dest);
-        assertThat(pairStatusMetadata.equals(comparStatusMetadata)).isTrue();
-    }
-
-    @Test
-    public void testCreatorNewArray() {
-        PairStatusMetadata[] pairStatusMetadatas = PairStatusMetadata.CREATOR.newArray(2);
-        assertThat(pairStatusMetadatas.length).isEqualTo(2);
-    }
-
-    @Test
-    public void describeContents() {
-        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
-        assertThat(pairStatusMetadata.describeContents()).isEqualTo(0);
-    }
-
-    @Test
-    public void  getStability() {
-        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
-        assertThat(pairStatusMetadata.getStability()).isEqualTo(0);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
index 5ddfed3..644e178 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
@@ -25,6 +25,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.os.Build;
 import android.provider.DeviceConfig;
 
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -68,7 +69,12 @@
                 "3", false);
         Thread.sleep(500);
 
-        assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+        // TestAppSupported Flag can only be set to true in user-debug devices.
+        if (Build.isDebuggable()) {
+            assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+        } else {
+            assertThat(mNearbyConfiguration.isTestAppSupported()).isFalse();
+        }
         assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isTrue();
         assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(3);
     }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
deleted file mode 100644
index c4a9729..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
+++ /dev/null
@@ -1,891 +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.server.nearby.common.ble;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.fail;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothAssignedNumbers;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.le.ScanFilter;
-import android.os.Parcel;
-import android.os.ParcelUuid;
-import android.util.SparseArray;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.server.nearby.common.ble.testing.FastPairTestData;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-@RunWith(AndroidJUnit4.class)
-public class BleFilterTest {
-
-
-    public static final ParcelUuid EDDYSTONE_SERVICE_DATA_PARCELUUID =
-            ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
-
-    private final BleFilter mEddystoneFilter = createEddystoneFilter();
-    private final BleFilter mEddystoneUidFilter = createEddystoneUidFilter();
-    private final BleFilter mEddystoneUrlFilter = createEddystoneUrlFilter();
-    private final BleFilter mEddystoneEidFilter = createEddystoneEidFilter();
-    private final BleFilter mIBeaconWithoutUuidFilter = createIBeaconWithoutUuidFilter();
-    private final BleFilter mIBeaconWithUuidFilter = createIBeaconWithUuidFilter();
-    private final BleFilter mChromecastFilter =
-            new BleFilter.Builder().setServiceUuid(
-                    new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")))
-                    .build();
-    private final BleFilter mEddystoneWithDeviceNameFilter =
-            new BleFilter.Builder()
-                    .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
-                    .setDeviceName("BERT")
-                    .build();
-    private final BleFilter mEddystoneWithDeviceAddressFilter =
-            new BleFilter.Builder()
-                    .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
-                    .setDeviceAddress("00:11:22:33:AA:BB")
-                    .build();
-    private final BleFilter mServiceUuidWithMaskFilter1 =
-            new BleFilter.Builder()
-                    .setServiceUuid(
-                            new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
-                            new ParcelUuid(UUID.fromString("0000000-0000-000-FFFF-FFFFFFFFFFFF")))
-                    .build();
-    private final BleFilter mServiceUuidWithMaskFilter2 =
-            new BleFilter.Builder()
-                    .setServiceUuid(
-                            new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
-                            new ParcelUuid(UUID.fromString("FFFFFFF-FFFF-FFF-FFFF-FFFFFFFFFFFF")))
-                    .build();
-
-    private final BleFilter mSmartSetupFilter =
-            new BleFilter.Builder()
-                    .setManufacturerData(
-                            BluetoothAssignedNumbers.GOOGLE,
-                            new byte[] {0x00, 0x10},
-                            new byte[] {0x00, (byte) 0xFF})
-                    .build();
-    private final BleFilter mWearFilter =
-            new BleFilter.Builder()
-                    .setManufacturerData(
-                            BluetoothAssignedNumbers.GOOGLE,
-                            new byte[] {0x00, 0x00, 0x00},
-                            new byte[] {0x00, 0x00, (byte) 0xFF})
-                    .build();
-    private final BleFilter mFakeSmartSetupSubsetFilter =
-            new BleFilter.Builder()
-                    .setManufacturerData(
-                            BluetoothAssignedNumbers.GOOGLE,
-                            new byte[] {0x00, 0x10, 0x50},
-                            new byte[] {0x00, (byte) 0xFF, (byte) 0xFF})
-                    .build();
-    private final BleFilter mFakeSmartSetupNotSubsetFilter =
-            new BleFilter.Builder()
-                    .setManufacturerData(
-                            BluetoothAssignedNumbers.GOOGLE,
-                            new byte[] {0x00, 0x10, 0x50},
-                            new byte[] {0x00, (byte) 0x00, (byte) 0xFF})
-                    .build();
-
-    private final BleFilter mFakeFilter1 =
-            new BleFilter.Builder()
-                    .setServiceData(
-                            ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
-                            new byte[] {0x51, 0x64},
-                            new byte[] {0x00, (byte) 0xFF})
-                    .build();
-    private final BleFilter mFakeFilter2 =
-            new BleFilter.Builder()
-                    .setServiceData(
-                            ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
-                            new byte[] {0x51, 0x64, 0x34},
-                            new byte[] {0x00, (byte) 0xFF, (byte) 0xFF})
-                    .build();
-
-    private ParcelUuid mServiceDataUuid;
-    private BleSighting mBleSighting;
-    private BleFilter.Builder mFilterBuilder;
-
-    @Before
-    public void setUp() throws Exception {
-        // This is the service data UUID in TestData.sd1.
-        // Can't be static because of Robolectric.
-        mServiceDataUuid = ParcelUuid.fromString("000000E0-0000-1000-8000-00805F9B34FB");
-
-        byte[] bleRecordBytes =
-                new byte[]{
-                        0x02,
-                        0x01,
-                        0x1a, // advertising flags
-                        0x05,
-                        0x02,
-                        0x0b,
-                        0x11,
-                        0x0a,
-                        0x11, // 16 bit service uuids
-                        0x04,
-                        0x09,
-                        0x50,
-                        0x65,
-                        0x64, // setName
-                        0x02,
-                        0x0A,
-                        (byte) 0xec, // tx power level
-                        0x05,
-                        0x16,
-                        0x0b,
-                        0x11,
-                        0x50,
-                        0x64, // service data
-                        0x05,
-                        (byte) 0xff,
-                        (byte) 0xe0,
-                        0x00,
-                        0x02,
-                        0x15, // manufacturer specific data
-                        0x03,
-                        0x50,
-                        0x01,
-                        0x02, // an unknown data type won't cause trouble
-                };
-
-        mBleSighting = new BleSighting(null /* device */, bleRecordBytes,
-                -10, 1397545200000000L);
-        mFilterBuilder = new BleFilter.Builder();
-    }
-
-    @Test
-    public void setNameFilter() {
-        BleFilter filter = mFilterBuilder.setDeviceName("Ped").build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-
-        filter = mFilterBuilder.setDeviceName("Pem").build();
-        assertThat(filter.matches(mBleSighting)).isFalse();
-    }
-
-    @Test
-    public void setServiceUuidFilter() {
-        BleFilter filter =
-                mFilterBuilder.setServiceUuid(
-                        ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB"))
-                        .build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-
-        filter =
-                mFilterBuilder.setServiceUuid(
-                        ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"))
-                        .build();
-        assertThat(filter.matches(mBleSighting)).isFalse();
-
-        filter =
-                mFilterBuilder
-                        .setServiceUuid(
-                                ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"),
-                                ParcelUuid.fromString("FFFFFFF0-FFFF-FFFF-FFFF-FFFFFFFFFFFF"))
-                        .build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-    }
-
-    @Test
-    public void setServiceDataFilter() {
-        byte[] setServiceData = new byte[]{0x50, 0x64};
-        ParcelUuid serviceDataUuid = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
-        BleFilter filter = mFilterBuilder.setServiceData(serviceDataUuid, setServiceData).build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-
-        byte[] emptyData = new byte[0];
-        filter = mFilterBuilder.setServiceData(serviceDataUuid, emptyData).build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-
-        byte[] prefixData = new byte[]{0x50};
-        filter = mFilterBuilder.setServiceData(serviceDataUuid, prefixData).build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-
-        byte[] nonMatchData = new byte[]{0x51, 0x64};
-        byte[] mask = new byte[]{(byte) 0x00, (byte) 0xFF};
-        filter = mFilterBuilder.setServiceData(serviceDataUuid, nonMatchData, mask).build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-
-        filter = mFilterBuilder.setServiceData(serviceDataUuid, nonMatchData).build();
-        assertThat(filter.matches(mBleSighting)).isFalse();
-    }
-
-    @Test
-    public void manufacturerSpecificData() {
-        byte[] setManufacturerData = new byte[]{0x02, 0x15};
-        int manufacturerId = 0xE0;
-        BleFilter filter =
-                mFilterBuilder.setManufacturerData(manufacturerId, setManufacturerData).build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-
-        byte[] emptyData = new byte[0];
-        filter = mFilterBuilder.setManufacturerData(manufacturerId, emptyData).build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-
-        byte[] prefixData = new byte[]{0x02};
-        filter = mFilterBuilder.setManufacturerData(manufacturerId, prefixData).build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-
-        // Data and mask are nullable. Check that we still match when they're null.
-        filter = mFilterBuilder.setManufacturerData(manufacturerId,
-                null /* data */).build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-        filter = mFilterBuilder.setManufacturerData(manufacturerId,
-                null /* data */, null /* mask */).build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-
-        // Test data mask
-        byte[] nonMatchData = new byte[]{0x02, 0x14};
-        filter = mFilterBuilder.setManufacturerData(manufacturerId, nonMatchData).build();
-        assertThat(filter.matches(mBleSighting)).isFalse();
-        byte[] mask = new byte[]{(byte) 0xFF, (byte) 0x00};
-        filter = mFilterBuilder.setManufacturerData(manufacturerId, nonMatchData, mask).build();
-        assertThat(filter.matches(mBleSighting)).isTrue();
-    }
-
-    @Test
-    public void manufacturerDataNotInBleRecord() {
-        byte[] bleRecord = FastPairTestData.adv_2;
-        // Verify manufacturer with no data
-        byte[] data = {(byte) 0xe0, (byte) 0x00};
-        BleFilter filter = mFilterBuilder.setManufacturerData(0x00e0, data).build();
-        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
-    }
-
-    @Test
-    public void manufacturerDataMaskNotInBleRecord() {
-        byte[] bleRecord = FastPairTestData.adv_2;
-
-        // Verify matching partial manufacturer with data and mask
-        byte[] data = {(byte) 0x15};
-        byte[] mask = {(byte) 0xff};
-
-        BleFilter filter = mFilterBuilder
-                .setManufacturerData(0x00e0, data, mask).build();
-        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
-    }
-
-
-    @Test
-    public void serviceData() throws Exception {
-        byte[] bleRecord = FastPairTestData.sd1;
-        byte[] serviceData = {(byte) 0x15};
-
-        // Verify manufacturer 2-byte UUID with no data
-        BleFilter filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData).build();
-        assertMatches(filter, null, 0, bleRecord);
-    }
-
-    @Test
-    public void serviceDataNoMatch() {
-        byte[] bleRecord = FastPairTestData.sd1;
-        byte[] serviceData = {(byte) 0xe1, (byte) 0x00};
-
-        // Verify manufacturer 2-byte UUID with no data
-        BleFilter filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData).build();
-        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
-    }
-
-    @Test
-    public void serviceDataUuidNotInBleRecord() {
-        byte[] bleRecord = FastPairTestData.eir_1;
-        byte[] serviceData = {(byte) 0xe0, (byte) 0x00};
-
-        // Verify Service Data with 2-byte UUID, no data, and NOT in scan record
-        BleFilter filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData).build();
-        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
-    }
-
-    @Test
-    public void serviceDataMask() {
-        byte[] bleRecord = FastPairTestData.sd1;
-        BleFilter filter;
-
-        // Verify matching partial manufacturer with data and mask
-        byte[] serviceData1 = {(byte) 0x15};
-        byte[] mask1 = {(byte) 0xff};
-        filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData1, mask1).build();
-        assertMatches(filter, null, 0, bleRecord);
-    }
-
-    @Test
-    public void serviceDataMaskNoMatch() {
-        byte[] bleRecord = FastPairTestData.sd1;
-        BleFilter filter;
-
-        // Verify non-matching partial manufacturer with data and mask
-        byte[] serviceData2 = {(byte) 0xe0, (byte) 0x00, (byte) 0x10};
-        byte[] mask2 = {(byte) 0xff, (byte) 0xff, (byte) 0xff};
-        filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData2, mask2).build();
-        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void serviceDataMaskWithDifferentLength() {
-        // Different lengths for data and mask.
-        byte[] serviceData = {(byte) 0xe0, (byte) 0x00, (byte) 0x10};
-        byte[] mask = {(byte) 0xff, (byte) 0xff};
-
-        //expected.expect(IllegalArgumentException.class);
-
-        mFilterBuilder.setServiceData(mServiceDataUuid, serviceData, mask).build();
-    }
-
-    @Test
-    public void serviceDataMaskNotInBleRecord() {
-        byte[] bleRecord = FastPairTestData.eir_1;
-        BleFilter filter;
-
-        // Verify matching partial manufacturer with data and mask
-        byte[] serviceData1 = {(byte) 0xe0, (byte) 0x00, (byte) 0x15};
-        byte[] mask1 = {(byte) 0xff, (byte) 0xff, (byte) 0xff};
-        filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData1, mask1).build();
-        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
-    }
-
-
-    @Test
-    public void deviceNameTest() {
-        // Verify the name filter matches
-        byte[] bleRecord = FastPairTestData.adv_1;
-        BleFilter filter = mFilterBuilder.setDeviceName("Pedometer").build();
-        assertMatches(filter, null, 0, bleRecord);
-    }
-
-    @Test
-    public void deviceNameNoMatch() {
-        // Verify the name filter does not match
-        byte[] bleRecord = FastPairTestData.adv_1;
-        BleFilter filter = mFilterBuilder.setDeviceName("Foo").build();
-        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
-    }
-
-    @Test
-    public void deviceNameNotInBleRecord() {
-        // Verify the name filter does not match
-        byte[] bleRecord = FastPairTestData.eir_1;
-        BleFilter filter = mFilterBuilder.setDeviceName("Pedometer").build();
-        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
-    }
-
-    @Test
-    public void serviceUuid() {
-        byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
-        ParcelUuid uuid = ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
-
-        BleFilter filter = mFilterBuilder.setServiceUuid(uuid).build();
-        assertMatches(filter, null, 0, bleRecord);
-    }
-
-    @Test
-    public void serviceUuidNoMatch() {
-        // Verify the name filter does not match
-        byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
-        ParcelUuid uuid = ParcelUuid.fromString("00001804-0000-1000-8000-000000000000");
-
-        BleFilter filter = mFilterBuilder.setServiceUuid(uuid).build();
-        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
-    }
-
-    @Test
-    public void serviceUuidNotInBleRecord() {
-        // Verify the name filter does not match
-        byte[] bleRecord = FastPairTestData.eir_1;
-        ParcelUuid uuid = ParcelUuid.fromString("00001804-0000-1000-8000-000000000000");
-
-        BleFilter filter = mFilterBuilder.setServiceUuid(uuid).build();
-        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
-    }
-
-    @Test
-    public void serviceUuidMask() {
-        byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
-        ParcelUuid uuid = ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
-        ParcelUuid mask = ParcelUuid.fromString("00000000-0000-0000-0000-FFFFFFFFFFFF");
-        BleFilter filter = mFilterBuilder.setServiceUuid(uuid, mask).build();
-        assertMatches(filter, null, 0, bleRecord);
-    }
-
-
-    @Test
-    public void macAddress() {
-        byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
-        String macAddress = "00:11:22:33:AA:BB";
-        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-
-        BluetoothDevice device = adapter.getRemoteDevice(macAddress);
-        BleFilter filter = mFilterBuilder.setDeviceAddress(macAddress).build();
-        assertMatches(filter, device, 0, bleRecord);
-    }
-
-    @Test
-    public void macAddressNoMatch() {
-        byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
-        String macAddress = "00:11:22:33:AA:00";
-        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-
-        BluetoothDevice device = adapter.getRemoteDevice("00:11:22:33:AA:BB");
-        BleFilter filter = mFilterBuilder.setDeviceAddress(macAddress).build();
-        assertThat(matches(filter, device, 0, bleRecord)).isFalse();
-    }
-
-    @Test
-    public void eddystoneIsSuperset() {
-        // Verify eddystone subtypes pass.
-        assertThat(mEddystoneFilter.isSuperset(mEddystoneFilter)).isTrue();
-        assertThat(mEddystoneUidFilter.isSuperset(mEddystoneUidFilter)).isTrue();
-        assertThat(mEddystoneFilter.isSuperset(mEddystoneUidFilter)).isTrue();
-        assertThat(mEddystoneFilter.isSuperset(mEddystoneEidFilter)).isTrue();
-        assertThat(mEddystoneFilter.isSuperset(mEddystoneUrlFilter)).isTrue();
-
-        // Non-eddystone beacon filters should never be supersets.
-        assertThat(mEddystoneFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
-        assertThat(mEddystoneFilter.isSuperset(mWearFilter)).isFalse();
-        assertThat(mEddystoneFilter.isSuperset(mSmartSetupFilter)).isFalse();
-        assertThat(mEddystoneFilter.isSuperset(mChromecastFilter)).isFalse();
-        assertThat(mEddystoneFilter.isSuperset(mFakeFilter1)).isFalse();
-        assertThat(mEddystoneFilter.isSuperset(mFakeFilter2)).isFalse();
-
-        assertThat(mEddystoneUidFilter.isSuperset(mWearFilter)).isFalse();
-        assertThat(mEddystoneUidFilter.isSuperset(mSmartSetupFilter)).isFalse();
-        assertThat(mEddystoneUidFilter.isSuperset(mChromecastFilter)).isFalse();
-        assertThat(mEddystoneUidFilter.isSuperset(mFakeFilter1)).isFalse();
-        assertThat(mEddystoneUidFilter.isSuperset(mFakeFilter2)).isFalse();
-    }
-
-    @Test
-    public void iBeaconIsSuperset() {
-        // Verify that an iBeacon filter is a superset of itself and any filters that specify UUIDs.
-        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mIBeaconWithoutUuidFilter)).isTrue();
-        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mIBeaconWithUuidFilter)).isTrue();
-
-        // Non-iBeacon filters should never be supersets.
-        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mEddystoneEidFilter)).isFalse();
-        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
-        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mEddystoneUidFilter)).isFalse();
-        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mWearFilter)).isFalse();
-        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mSmartSetupFilter)).isFalse();
-        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mChromecastFilter)).isFalse();
-        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mFakeFilter1)).isFalse();
-        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mFakeFilter2)).isFalse();
-    }
-
-    @Test
-    public void mixedFilterIsSuperset() {
-        // Compare service data vs manufacturer data filters to verify we detect supersets
-        // correctly in filters that aren't for iBeacon and Eddystone.
-        assertThat(mWearFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
-        assertThat(mSmartSetupFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
-        assertThat(mChromecastFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
-
-        assertThat(mWearFilter.isSuperset(mEddystoneFilter)).isFalse();
-        assertThat(mSmartSetupFilter.isSuperset(mEddystoneFilter)).isFalse();
-        assertThat(mChromecastFilter.isSuperset(mEddystoneFilter)).isFalse();
-
-        assertThat(mWearFilter.isSuperset(mEddystoneUidFilter)).isFalse();
-        assertThat(mSmartSetupFilter.isSuperset(mEddystoneUidFilter)).isFalse();
-        assertThat(mChromecastFilter.isSuperset(mEddystoneUidFilter)).isFalse();
-
-        assertThat(mWearFilter.isSuperset(mEddystoneEidFilter)).isFalse();
-        assertThat(mSmartSetupFilter.isSuperset(mEddystoneEidFilter)).isFalse();
-        assertThat(mChromecastFilter.isSuperset(mEddystoneEidFilter)).isFalse();
-
-        assertThat(mWearFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
-        assertThat(mSmartSetupFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
-        assertThat(mChromecastFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
-
-        assertThat(mWearFilter.isSuperset(mIBeaconWithUuidFilter)).isFalse();
-        assertThat(mSmartSetupFilter.isSuperset(mIBeaconWithUuidFilter)).isFalse();
-        assertThat(mChromecastFilter.isSuperset(mIBeaconWithUuidFilter)).isFalse();
-
-        assertThat(mWearFilter.isSuperset(mChromecastFilter)).isFalse();
-        assertThat(mSmartSetupFilter.isSuperset(mChromecastFilter)).isFalse();
-        assertThat(mSmartSetupFilter.isSuperset(mWearFilter)).isFalse();
-        assertThat(mChromecastFilter.isSuperset(mWearFilter)).isFalse();
-
-        assertThat(mFakeFilter1.isSuperset(mFakeFilter2)).isTrue();
-        assertThat(mFakeFilter2.isSuperset(mFakeFilter1)).isFalse();
-        assertThat(mSmartSetupFilter.isSuperset(mFakeSmartSetupSubsetFilter)).isTrue();
-        assertThat(mSmartSetupFilter.isSuperset(mFakeSmartSetupNotSubsetFilter)).isFalse();
-
-        assertThat(mEddystoneFilter.isSuperset(mEddystoneWithDeviceNameFilter)).isTrue();
-        assertThat(mEddystoneFilter.isSuperset(mEddystoneWithDeviceAddressFilter)).isTrue();
-        assertThat(mEddystoneWithDeviceAddressFilter.isSuperset(mEddystoneFilter)).isFalse();
-
-        assertThat(mChromecastFilter.isSuperset(mServiceUuidWithMaskFilter1)).isTrue();
-        assertThat(mServiceUuidWithMaskFilter2.isSuperset(mServiceUuidWithMaskFilter1)).isFalse();
-        assertThat(mServiceUuidWithMaskFilter1.isSuperset(mServiceUuidWithMaskFilter2)).isTrue();
-        assertThat(mEddystoneFilter.isSuperset(mServiceUuidWithMaskFilter1)).isFalse();
-    }
-
-    @Test
-    public void toOsFilter_getTheSameFilterParameter() {
-        BleFilter nearbyFilter = createTestFilter();
-        ScanFilter osFilter = nearbyFilter.toOsFilter();
-        assertFilterValuesEqual(nearbyFilter, osFilter);
-    }
-
-    @Test
-    public void describeContents() {
-        BleFilter nearbyFilter = createTestFilter();
-        assertThat(nearbyFilter.describeContents()).isEqualTo(0);
-    }
-
-    @Test
-    public void testHashCode() {
-        BleFilter nearbyFilter = createTestFilter();
-        BleFilter compareFilter = new BleFilter("BERT", "00:11:22:33:AA:BB",
-                new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
-                new ParcelUuid(UUID.fromString("FFFFFFF-FFFF-FFF-FFFF-FFFFFFFFFFFF")),
-                ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
-                new byte[] {0x51, 0x64}, new byte[] {0x00, (byte) 0xFF},
-                BluetoothAssignedNumbers.GOOGLE, new byte[] {0x00, 0x10},
-                new byte[] {0x00, (byte) 0xFF});
-        assertThat(nearbyFilter.hashCode()).isEqualTo(compareFilter.hashCode());
-    }
-
-    @Test
-    public void testToString() {
-        BleFilter nearbyFilter = createTestFilter();
-        assertThat(nearbyFilter.toString()).isEqualTo("BleFilter [deviceName=BERT,"
-                + " deviceAddress=00:11:22:33:AA:BB, uuid=0000fea0-0000-1000-8000-00805f9b34fb,"
-                + " uuidMask=0fffffff-ffff-0fff-ffff-ffffffffffff,"
-                + " serviceDataUuid=0000110b-0000-1000-8000-00805f9b34fb,"
-                + " serviceData=[81, 100], serviceDataMask=[0, -1],"
-                + " manufacturerId=224, manufacturerData=[0, 16], manufacturerDataMask=[0, -1]]");
-    }
-
-    @Test
-    public void testParcel() {
-        BleFilter nearbyFilter = createTestFilter();
-        Parcel parcel = Parcel.obtain();
-        nearbyFilter.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        BleFilter compareFilter = BleFilter.CREATOR.createFromParcel(
-                parcel);
-        parcel.recycle();
-        assertThat(compareFilter.getDeviceName()).isEqualTo("BERT");
-    }
-
-    @Test
-    public void testCreatorNewArray() {
-        BleFilter[] nearbyFilters  = BleFilter.CREATOR.newArray(2);
-        assertThat(nearbyFilters.length).isEqualTo(2);
-    }
-
-    private static boolean matches(
-            BleFilter filter, BluetoothDevice device, int rssi, byte[] bleRecord) {
-        return filter.matches(new BleSighting(device,
-                bleRecord, rssi, 0 /* timestampNanos */));
-    }
-
-    private static void assertFilterValuesEqual(BleFilter nearbyFilter, ScanFilter osFilter) {
-        assertThat(osFilter.getDeviceAddress()).isEqualTo(nearbyFilter.getDeviceAddress());
-        assertThat(osFilter.getDeviceName()).isEqualTo(nearbyFilter.getDeviceName());
-
-        assertThat(osFilter.getManufacturerData()).isEqualTo(nearbyFilter.getManufacturerData());
-        assertThat(osFilter.getManufacturerDataMask())
-                .isEqualTo(nearbyFilter.getManufacturerDataMask());
-        assertThat(osFilter.getManufacturerId()).isEqualTo(nearbyFilter.getManufacturerId());
-
-        assertThat(osFilter.getServiceData()).isEqualTo(nearbyFilter.getServiceData());
-        assertThat(osFilter.getServiceDataMask()).isEqualTo(nearbyFilter.getServiceDataMask());
-        assertThat(osFilter.getServiceDataUuid()).isEqualTo(nearbyFilter.getServiceDataUuid());
-
-        assertThat(osFilter.getServiceUuid()).isEqualTo(nearbyFilter.getServiceUuid());
-        assertThat(osFilter.getServiceUuidMask()).isEqualTo(nearbyFilter.getServiceUuidMask());
-    }
-
-    private static void assertMatches(
-            BleFilter filter, BluetoothDevice device, int rssi, byte[] bleRecordBytes) {
-
-        // Device match.
-        if (filter.getDeviceAddress() != null
-                && (device == null || !filter.getDeviceAddress().equals(device.getAddress()))) {
-            fail("Filter specified a device address ("
-                    + filter.getDeviceAddress()
-                    + ") which doesn't match the actual value: ["
-                    + (device == null ? "null device" : device.getAddress())
-                    + "]");
-        }
-
-        // BLE record is null but there exist filters on it.
-        BleRecord bleRecord = BleRecord.parseFromBytes(bleRecordBytes);
-        if (bleRecord == null
-                && (filter.getDeviceName() != null
-                || filter.getServiceUuid() != null
-                || filter.getManufacturerData() != null
-                || filter.getServiceData() != null)) {
-            fail(
-                    "The bleRecordBytes given parsed to a null bleRecord, but the filter"
-                            + "has a non-null field which depends on the scan record");
-        }
-
-        // Local name match.
-        if (filter.getDeviceName() != null
-                && !filter.getDeviceName().equals(bleRecord.getDeviceName())) {
-            fail(
-                    "The filter's device name ("
-                            + filter.getDeviceName()
-                            + ") doesn't match the scan record device name ("
-                            + bleRecord.getDeviceName()
-                            + ")");
-        }
-
-        // UUID match.
-        if (filter.getServiceUuid() != null
-                && !matchesServiceUuids(filter.getServiceUuid(),
-                filter.getServiceUuidMask(), bleRecord.getServiceUuids())) {
-            fail("The filter specifies a service UUID "
-                    + "but it doesn't match what's in the scan record");
-        }
-
-        // Service data match
-        if (filter.getServiceDataUuid() != null
-                && !BleFilter.matchesPartialData(
-                filter.getServiceData(),
-                filter.getServiceDataMask(),
-                bleRecord.getServiceData(filter.getServiceDataUuid()))) {
-            fail(
-                    "The filter's service data doesn't match what's in the scan record.\n"
-                            + "Service data: "
-                            + byteString(filter.getServiceData())
-                            + "\n"
-                            + "Service data UUID: "
-                            + filter.getServiceDataUuid().toString()
-                            + "\n"
-                            + "Service data mask: "
-                            + byteString(filter.getServiceDataMask())
-                            + "\n"
-                            + "Scan record service data: "
-                            + byteString(bleRecord.getServiceData(filter.getServiceDataUuid()))
-                            + "\n"
-                            + "Scan record data map:\n"
-                            + byteString(bleRecord.getServiceData()));
-        }
-
-        // Manufacturer data match.
-        if (filter.getManufacturerId() >= 0
-                && !BleFilter.matchesPartialData(
-                filter.getManufacturerData(),
-                filter.getManufacturerDataMask(),
-                bleRecord.getManufacturerSpecificData(filter.getManufacturerId()))) {
-            fail(
-                    "The filter's manufacturer data doesn't match what's in the scan record.\n"
-                            + "Manufacturer ID: "
-                            + filter.getManufacturerId()
-                            + "\n"
-                            + "Manufacturer data: "
-                            + byteString(filter.getManufacturerData())
-                            + "\n"
-                            + "Manufacturer data mask: "
-                            + byteString(filter.getManufacturerDataMask())
-                            + "\n"
-                            + "Scan record manufacturer-specific data: "
-                            + byteString(bleRecord.getManufacturerSpecificData(
-                            filter.getManufacturerId()))
-                            + "\n"
-                            + "Manufacturer data array:\n"
-                            + byteString(bleRecord.getManufacturerSpecificData()));
-        }
-
-        // All filters match.
-        assertThat(
-                matches(filter, device, rssi, bleRecordBytes)).isTrue();
-    }
-
-
-    private static String byteString(byte[] bytes) {
-        if (bytes == null) {
-            return "[null]";
-        } else {
-            final char[] hexArray = "0123456789ABCDEF".toCharArray();
-            char[] hexChars = new char[bytes.length * 2];
-            for (int i = 0; i < bytes.length; i++) {
-                int v = bytes[i] & 0xFF;
-                hexChars[i * 2] = hexArray[v >>> 4];
-                hexChars[i * 2 + 1] = hexArray[v & 0x0F];
-            }
-            return new String(hexChars);
-        }
-    }
-
-    private static String byteString(Map<ParcelUuid, byte[]> bytesMap) {
-        StringBuilder builder = new StringBuilder();
-        for (Map.Entry<ParcelUuid, byte[]> entry : bytesMap.entrySet()) {
-            builder.append(builder.toString().isEmpty() ? "  " : "\n  ");
-            builder.append(entry.getKey().toString());
-            builder.append(" --> ");
-            builder.append(byteString(entry.getValue()));
-        }
-        return builder.toString();
-    }
-
-    private static String byteString(SparseArray<byte[]> bytesArray) {
-        StringBuilder builder = new StringBuilder();
-        for (int i = 0; i < bytesArray.size(); i++) {
-            builder.append(builder.toString().isEmpty() ? "  " : "\n  ");
-            builder.append(byteString(bytesArray.valueAt(i)));
-        }
-        return builder.toString();
-    }
-
-    private static BleFilter createTestFilter() {
-        BleFilter.Builder builder = new BleFilter.Builder();
-        builder
-                .setServiceUuid(
-                        new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
-                        new ParcelUuid(UUID.fromString("FFFFFFF-FFFF-FFF-FFFF-FFFFFFFFFFFF")))
-                .setDeviceAddress("00:11:22:33:AA:BB")
-                .setDeviceName("BERT")
-                .setManufacturerData(
-                        BluetoothAssignedNumbers.GOOGLE,
-                        new byte[] {0x00, 0x10},
-                        new byte[] {0x00, (byte) 0xFF})
-                .setServiceData(
-                        ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
-                        new byte[] {0x51, 0x64},
-                        new byte[] {0x00, (byte) 0xFF});
-        return builder.build();
-    }
-
-    // ref to beacon.decode.BeaconFilterBuilder.eddystoneFilter()
-    private static BleFilter createEddystoneFilter() {
-        return new BleFilter.Builder().setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID).build();
-    }
-    // ref to beacon.decode.BeaconFilterBuilder.eddystoneUidFilter()
-    private static BleFilter createEddystoneUidFilter() {
-        return new BleFilter.Builder()
-                .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
-                .setServiceData(
-                        EDDYSTONE_SERVICE_DATA_PARCELUUID, new byte[] {(short) 0x00},
-                        new byte[] {(byte) 0xf0})
-                .build();
-    }
-
-    // ref to beacon.decode.BeaconFilterBuilder.eddystoneUrlFilter()
-    private static BleFilter createEddystoneUrlFilter() {
-        return new BleFilter.Builder()
-                .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
-                .setServiceData(
-                        EDDYSTONE_SERVICE_DATA_PARCELUUID,
-                        new byte[] {(short) 0x10}, new byte[] {(byte) 0xf0})
-                .build();
-    }
-
-    // ref to beacon.decode.BeaconFilterBuilder.eddystoneEidFilter()
-    private static BleFilter createEddystoneEidFilter() {
-        return new BleFilter.Builder()
-                .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
-                .setServiceData(
-                        EDDYSTONE_SERVICE_DATA_PARCELUUID,
-                        new byte[] {(short) 0x30}, new byte[] {(byte) 0xf0})
-                .build();
-    }
-
-    // ref to beacon.decode.BeaconFilterBuilder.iBeaconWithoutUuidFilter()
-    private static BleFilter createIBeaconWithoutUuidFilter() {
-        byte[] data = {(byte) 0x02, (byte) 0x15};
-        byte[] mask = {(byte) 0xff, (byte) 0xff};
-
-        return new BleFilter.Builder().setManufacturerData((short) 0x004C, data, mask).build();
-    }
-
-    // ref to beacon.decode.BeaconFilterBuilder.iBeaconWithUuidFilter()
-    private static BleFilter createIBeaconWithUuidFilter() {
-        byte[] data = getFilterData(ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB"));
-        byte[] mask = getFilterMask(ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB"));
-
-        return new BleFilter.Builder().setManufacturerData((short) 0x004C, data, mask).build();
-    }
-
-    // Ref to beacon.decode.AppleBeaconDecoder.getFilterData
-    private static byte[] getFilterData(ParcelUuid uuid) {
-        byte[] data = new byte[18];
-        data[0] = (byte) 0x02;
-        data[1] = (byte) 0x15;
-        // Check if UUID is needed in data
-        if (uuid != null) {
-            // Convert UUID to array in big endian order
-            byte[] uuidBytes = uuidToByteArray(uuid);
-            for (int i = 0; i < 16; i++) {
-                // Adding uuid bytes in big-endian order to match iBeacon format
-                data[i + 2] = uuidBytes[i];
-            }
-        }
-        return data;
-    }
-
-    // Ref to beacon.decode.AppleBeaconDecoder.getFilterMask
-    private static byte[] getFilterMask(ParcelUuid uuid) {
-        byte[] mask = new byte[18];
-        mask[0] = (byte) 0xff;
-        mask[1] = (byte) 0xff;
-        // Check if UUID is needed in data
-        if (uuid != null) {
-            for (int i = 0; i < 16; i++) {
-                mask[i + 2] = (byte) 0xff;
-            }
-        }
-        return mask;
-    }
-
-    // Ref to beacon.decode.AppleBeaconDecoder.uuidToByteArray
-    private static byte[] uuidToByteArray(ParcelUuid uuid) {
-        ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
-        bb.putLong(uuid.getUuid().getMostSignificantBits());
-        bb.putLong(uuid.getUuid().getLeastSignificantBits());
-        return bb.array();
-    }
-
-    private static boolean matchesServiceUuids(
-            ParcelUuid uuid, ParcelUuid parcelUuidMask, List<ParcelUuid> uuids) {
-        if (uuid == null) {
-            return true;
-        }
-
-        for (ParcelUuid parcelUuid : uuids) {
-            UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
-            if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    // Check if the uuid pattern matches the particular service uuid.
-    private static boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) {
-        if (mask == null) {
-            return uuid.equals(data);
-        }
-        if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits())
-                != (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) {
-            return false;
-        }
-        return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits())
-                == (data.getMostSignificantBits() & mask.getMostSignificantBits()));
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
deleted file mode 100644
index 3f9a259..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
+++ /dev/null
@@ -1,295 +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.
- */
-
-/*
- * 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.server.nearby.common.ble;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.ParcelUuid;
-import android.util.SparseArray;
-
-import androidx.test.filters.SdkSuppress;
-
-import org.junit.Test;
-
-/** Test for Bluetooth LE {@link BleRecord}. */
-public class BleRecordTest {
-
-    // iBeacon (Apple) Packet 1
-    private static final byte[] BEACON = {
-            // Flags
-            (byte) 0x02,
-            (byte) 0x01,
-            (byte) 0x06,
-            // Manufacturer-specific data header
-            (byte) 0x1a,
-            (byte) 0xff,
-            (byte) 0x4c,
-            (byte) 0x00,
-            // iBeacon Type
-            (byte) 0x02,
-            // Frame length
-            (byte) 0x15,
-            // iBeacon Proximity UUID
-            (byte) 0xf7,
-            (byte) 0x82,
-            (byte) 0x6d,
-            (byte) 0xa6,
-            (byte) 0x4f,
-            (byte) 0xa2,
-            (byte) 0x4e,
-            (byte) 0x98,
-            (byte) 0x80,
-            (byte) 0x24,
-            (byte) 0xbc,
-            (byte) 0x5b,
-            (byte) 0x71,
-            (byte) 0xe0,
-            (byte) 0x89,
-            (byte) 0x3e,
-            // iBeacon Instance ID (Major/Minor)
-            (byte) 0x44,
-            (byte) 0xd0,
-            (byte) 0x25,
-            (byte) 0x22,
-            // Tx Power
-            (byte) 0xb3,
-            // RSP
-            (byte) 0x08,
-            (byte) 0x09,
-            (byte) 0x4b,
-            (byte) 0x6f,
-            (byte) 0x6e,
-            (byte) 0x74,
-            (byte) 0x61,
-            (byte) 0x6b,
-            (byte) 0x74,
-            (byte) 0x02,
-            (byte) 0x0a,
-            (byte) 0xf4,
-            (byte) 0x0a,
-            (byte) 0x16,
-            (byte) 0x0d,
-            (byte) 0xd0,
-            (byte) 0x74,
-            (byte) 0x6d,
-            (byte) 0x4d,
-            (byte) 0x6b,
-            (byte) 0x32,
-            (byte) 0x36,
-            (byte) 0x64,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00
-    };
-
-    // iBeacon (Apple) Packet 1
-    private static final byte[] SAME_BEACON = {
-            // Flags
-            (byte) 0x02,
-            (byte) 0x01,
-            (byte) 0x06,
-            // Manufacturer-specific data header
-            (byte) 0x1a,
-            (byte) 0xff,
-            (byte) 0x4c,
-            (byte) 0x00,
-            // iBeacon Type
-            (byte) 0x02,
-            // Frame length
-            (byte) 0x15,
-            // iBeacon Proximity UUID
-            (byte) 0xf7,
-            (byte) 0x82,
-            (byte) 0x6d,
-            (byte) 0xa6,
-            (byte) 0x4f,
-            (byte) 0xa2,
-            (byte) 0x4e,
-            (byte) 0x98,
-            (byte) 0x80,
-            (byte) 0x24,
-            (byte) 0xbc,
-            (byte) 0x5b,
-            (byte) 0x71,
-            (byte) 0xe0,
-            (byte) 0x89,
-            (byte) 0x3e,
-            // iBeacon Instance ID (Major/Minor)
-            (byte) 0x44,
-            (byte) 0xd0,
-            (byte) 0x25,
-            (byte) 0x22,
-            // Tx Power
-            (byte) 0xb3,
-            // RSP
-            (byte) 0x08,
-            (byte) 0x09,
-            (byte) 0x4b,
-            (byte) 0x6f,
-            (byte) 0x6e,
-            (byte) 0x74,
-            (byte) 0x61,
-            (byte) 0x6b,
-            (byte) 0x74,
-            (byte) 0x02,
-            (byte) 0x0a,
-            (byte) 0xf4,
-            (byte) 0x0a,
-            (byte) 0x16,
-            (byte) 0x0d,
-            (byte) 0xd0,
-            (byte) 0x74,
-            (byte) 0x6d,
-            (byte) 0x4d,
-            (byte) 0x6b,
-            (byte) 0x32,
-            (byte) 0x36,
-            (byte) 0x64,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00
-    };
-
-    // iBeacon (Apple) Packet 1 with a modified second field.
-    private static final byte[] OTHER_BEACON = {
-            (byte) 0x02, // Length of this Data
-            (byte) 0x02, // <<Flags>>
-            (byte) 0x04, // BR/EDR Not Supported.
-            // Apple Specific Data
-            26, // length of data that follows
-            (byte) 0xff, // <<Manufacturer Specific Data>>
-            // Company Identifier Code = Apple
-            (byte) 0x4c, // LSB
-            (byte) 0x00, // MSB
-            // iBeacon Header
-            0x02,
-            // iBeacon Length
-            0x15,
-            // UUID = PROXIMITY_NOW
-            // IEEE 128-bit UUID represented as UUID[15]: msb To UUID[0]: lsb
-            (byte) 0x14,
-            (byte) 0xe4,
-            (byte) 0xfd,
-            (byte) 0x9f, // UUID[15] - UUID[12]
-            (byte) 0x66,
-            (byte) 0x67,
-            (byte) 0x4c,
-            (byte) 0xcb, // UUID[11] - UUID[08]
-            (byte) 0xa6,
-            (byte) 0x1b,
-            (byte) 0x24,
-            (byte) 0xd0, // UUID[07] - UUID[04]
-            (byte) 0x9a,
-            (byte) 0xb1,
-            (byte) 0x7e,
-            (byte) 0x93, // UUID[03] - UUID[00]
-            // ID as an int (decimal) = 1297482358
-            (byte) 0x76, // Major H
-            (byte) 0x02, // Major L
-            (byte) 0x56, // Minor H
-            (byte) 0x4d, // Minor L
-            // Normalized Tx Power of -77dbm
-            (byte) 0xb3,
-            0x00, // Zero padding for testing
-    };
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEquals() {
-        BleRecord record = BleRecord.parseFromBytes(BEACON);
-        BleRecord record2 = BleRecord.parseFromBytes(SAME_BEACON);
-
-        assertThat(record).isEqualTo(record2);
-
-        // Different items.
-        record2 = BleRecord.parseFromBytes(OTHER_BEACON);
-        assertThat(record).isNotEqualTo(record2);
-        assertThat(record.hashCode()).isNotEqualTo(record2.hashCode());
-    }
-
-    @Test
-    public void testFields() {
-        BleRecord record = BleRecord.parseFromBytes(BEACON);
-        assertThat(byteString(record.getManufacturerSpecificData()))
-                .isEqualTo("  0215F7826DA64FA24E988024BC5B71E0893E44D02522B3");
-        assertThat(
-                byteString(record.getServiceData(
-                        ParcelUuid.fromString("000000E0-0000-1000-8000-00805F9B34FB"))))
-                .isEqualTo("[null]");
-        assertThat(record.getTxPowerLevel()).isEqualTo(-12);
-        assertThat(record.toString()).isEqualTo(
-                "BleRecord [advertiseFlags=6, serviceUuids=[], "
-                        + "manufacturerSpecificData={76=[2, 21, -9, -126, 109, -90, 79, -94, 78,"
-                        + " -104, -128, 36, -68, 91, 113, -32, -119, 62, 68, -48, 37, 34, -77]},"
-                        + " serviceData={0000d00d-0000-1000-8000-00805f9b34fb"
-                        + "=[116, 109, 77, 107, 50, 54, 100]},"
-                        + " txPowerLevel=-12, deviceName=Kontakt]");
-    }
-
-    private static String byteString(SparseArray<byte[]> bytesArray) {
-        StringBuilder builder = new StringBuilder();
-        for (int i = 0; i < bytesArray.size(); i++) {
-            builder.append(builder.toString().isEmpty() ? "  " : "\n  ");
-            builder.append(byteString(bytesArray.valueAt(i)));
-        }
-        return builder.toString();
-    }
-
-    private static String byteString(byte[] bytes) {
-        if (bytes == null) {
-            return "[null]";
-        } else {
-            final char[] hexArray = "0123456789ABCDEF".toCharArray();
-            char[] hexChars = new char[bytes.length * 2];
-            for (int i = 0; i < bytes.length; i++) {
-                int v = bytes[i] & 0xFF;
-                hexChars[i * 2] = hexArray[v >>> 4];
-                hexChars[i * 2 + 1] = hexArray[v & 0x0F];
-            }
-            return new String(hexChars);
-        }
-    }
-}
-
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
deleted file mode 100644
index 3443b53..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
+++ /dev/null
@@ -1,155 +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.server.nearby.common.ble;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.os.Parcel;
-
-import org.junit.Test;
-
-import java.util.concurrent.TimeUnit;
-
-/** Test for Bluetooth LE {@link BleSighting}. */
-public class BleSightingTest {
-    private static final String DEVICE_NAME = "device1";
-    private static final String OTHER_DEVICE_NAME = "device2";
-    private static final long TIME_EPOCH_MILLIS = 123456;
-    private static final long OTHER_TIME_EPOCH_MILLIS = 456789;
-    private static final int RSSI = 1;
-    private static final int OTHER_RSSI = 2;
-
-    private final BluetoothDevice mBluetoothDevice1 =
-            BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
-    private final BluetoothDevice mBluetoothDevice2 =
-            BluetoothAdapter.getDefaultAdapter().getRemoteDevice("AA:BB:CC:DD:EE:FF");
-
-
-    @Test
-    public void testEquals() {
-        BleSighting sighting =
-                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
-        BleSighting sighting2 =
-                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
-        assertThat(sighting.equals(sighting2)).isTrue();
-        assertThat(sighting2.equals(sighting)).isTrue();
-        assertThat(sighting.hashCode()).isEqualTo(sighting2.hashCode());
-
-        // Transitive property.
-        BleSighting sighting3 =
-                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
-        assertThat(sighting2.equals(sighting3)).isTrue();
-        assertThat(sighting.equals(sighting3)).isTrue();
-
-        // Set different values for each field, one at a time.
-        sighting2 = buildBleSighting(mBluetoothDevice2, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
-        assertSightingsNotEquals(sighting, sighting2);
-
-        sighting2 = buildBleSighting(mBluetoothDevice1, OTHER_DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
-        assertSightingsNotEquals(sighting, sighting2);
-
-        sighting2 = buildBleSighting(mBluetoothDevice1, DEVICE_NAME, OTHER_TIME_EPOCH_MILLIS, RSSI);
-        assertSightingsNotEquals(sighting, sighting2);
-
-        sighting2 = buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, OTHER_RSSI);
-        assertSightingsNotEquals(sighting, sighting2);
-    }
-
-    @Test
-    public void getNormalizedRSSI_usingNearbyRssiOffset_getCorrectValue() {
-        BleSighting sighting =
-                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
-
-        int defaultRssiOffset = 3;
-        assertThat(sighting.getNormalizedRSSI()).isEqualTo(RSSI + defaultRssiOffset);
-    }
-
-    @Test
-    public void testFields() {
-        BleSighting sighting =
-                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
-        assertThat(byteString(sighting.getBleRecordBytes()))
-                .isEqualTo("080964657669636531");
-        assertThat(sighting.getRssi()).isEqualTo(RSSI);
-        assertThat(sighting.getTimestampMillis()).isEqualTo(TIME_EPOCH_MILLIS);
-        assertThat(sighting.getTimestampNanos())
-                .isEqualTo(TimeUnit.MILLISECONDS.toNanos(TIME_EPOCH_MILLIS));
-        assertThat(sighting.toString()).isEqualTo(
-                "BleSighting{device=" + mBluetoothDevice1 + ","
-                        + " bleRecord=BleRecord [advertiseFlags=-1,"
-                        + " serviceUuids=[],"
-                        + " manufacturerSpecificData={}, serviceData={},"
-                        + " txPowerLevel=-2147483648,"
-                        + " deviceName=device1],"
-                        + " rssi=1,"
-                        + " timestampNanos=123456000000}");
-    }
-
-    @Test
-    public void testParcelable() {
-        BleSighting sighting =
-                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
-        Parcel dest = Parcel.obtain();
-        sighting.writeToParcel(dest, 0);
-        dest.setDataPosition(0);
-        assertThat(sighting.getRssi()).isEqualTo(RSSI);
-    }
-
-    @Test
-    public void testCreatorNewArray() {
-        BleSighting[]  sightings =
-                BleSighting.CREATOR.newArray(2);
-        assertThat(sightings.length).isEqualTo(2);
-    }
-
-    private static String byteString(byte[] bytes) {
-        if (bytes == null) {
-            return "[null]";
-        } else {
-            final char[] hexArray = "0123456789ABCDEF".toCharArray();
-            char[] hexChars = new char[bytes.length * 2];
-            for (int i = 0; i < bytes.length; i++) {
-                int v = bytes[i] & 0xFF;
-                hexChars[i * 2] = hexArray[v >>> 4];
-                hexChars[i * 2 + 1] = hexArray[v & 0x0F];
-            }
-            return new String(hexChars);
-        }
-    }
-
-        /** Builds a BleSighting instance which will correctly match filters by device name. */
-    private static BleSighting buildBleSighting(
-            BluetoothDevice bluetoothDevice, String deviceName, long timeEpochMillis, int rssi) {
-        byte[] nameBytes = deviceName.getBytes(UTF_8);
-        byte[] bleRecordBytes = new byte[nameBytes.length + 2];
-        bleRecordBytes[0] = (byte) (nameBytes.length + 1);
-        bleRecordBytes[1] = 0x09; // Value of private BleRecord.DATA_TYPE_LOCAL_NAME_COMPLETE;
-        System.arraycopy(nameBytes, 0, bleRecordBytes, 2, nameBytes.length);
-
-        return new BleSighting(bluetoothDevice, bleRecordBytes,
-                rssi, TimeUnit.MILLISECONDS.toNanos(timeEpochMillis));
-    }
-
-    private static void assertSightingsNotEquals(BleSighting sighting1, BleSighting sighting2) {
-        assertThat(sighting1.equals(sighting2)).isFalse();
-        assertThat(sighting1.hashCode()).isNotEqualTo(sighting2.hashCode());
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java
deleted file mode 100644
index 9a9181d..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java
+++ /dev/null
@@ -1,37 +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.server.nearby.common.ble.decode;
-
-import static com.android.server.nearby.common.ble.BleRecord.parseFromBytes;
-import static com.android.server.nearby.common.ble.testing.FastPairTestData.getFastPairRecord;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-
-public class BeaconDecoderTest {
-    private BeaconDecoder mBeaconDecoder = new FastPairDecoder();;
-
-    @Test
-    public void testFields() {
-        assertThat(mBeaconDecoder.getTelemetry(parseFromBytes(getFastPairRecord()))).isNull();
-        assertThat(mBeaconDecoder.getUrl(parseFromBytes(getFastPairRecord()))).isNull();
-        assertThat(mBeaconDecoder.supportsBeaconIdAndTxPower(parseFromBytes(getFastPairRecord())))
-                .isTrue();
-        assertThat(mBeaconDecoder.supportsTxPower()).isTrue();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
deleted file mode 100644
index 6552699..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
+++ /dev/null
@@ -1,549 +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.server.nearby.common.ble.decode;
-
-import static com.android.server.nearby.common.ble.BleRecord.parseFromBytes;
-import static com.android.server.nearby.common.ble.testing.FastPairTestData.DEVICE_ADDRESS;
-import static com.android.server.nearby.common.ble.testing.FastPairTestData.FAST_PAIR_MODEL_ID;
-import static com.android.server.nearby.common.ble.testing.FastPairTestData.FAST_PAIR_SHARED_ACCOUNT_KEY_RECORD;
-import static com.android.server.nearby.common.ble.testing.FastPairTestData.RSSI;
-import static com.android.server.nearby.common.ble.testing.FastPairTestData.getFastPairRecord;
-import static com.android.server.nearby.common.ble.testing.FastPairTestData.newFastPairRecord;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.server.nearby.common.ble.BleRecord;
-import com.android.server.nearby.common.ble.BleSighting;
-import com.android.server.nearby.common.ble.testing.FastPairTestData;
-import com.android.server.nearby.util.Hex;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class FastPairDecoderTest {
-    private static final String LONG_MODEL_ID = "1122334455667788";
-    // Bits 3-6 are model ID length bits = 0b1000 = 8
-    private static final byte LONG_MODEL_ID_HEADER = 0b00010000;
-    private static final String PADDED_LONG_MODEL_ID = "00001111";
-    // Bits 3-6 are model ID length bits = 0b0100 = 4
-    private static final byte PADDED_LONG_MODEL_ID_HEADER = 0b00001000;
-    private static final String TRIMMED_LONG_MODEL_ID = "001111";
-    private static final byte MODEL_ID_HEADER = 0b00000110;
-    private static final String MODEL_ID = "112233";
-    private static final byte BLOOM_FILTER_HEADER = 0b01100000;
-    private static final byte BLOOM_FILTER_NO_NOTIFICATION_HEADER = 0b01100010;
-    private static final String BLOOM_FILTER = "112233445566";
-    private static final byte LONG_BLOOM_FILTER_HEADER = (byte) 0b10100000;
-    private static final String LONG_BLOOM_FILTER = "00112233445566778899";
-    private static final byte BLOOM_FILTER_SALT_HEADER = 0b00010001;
-    private static final String BLOOM_FILTER_SALT = "01";
-    private static final byte BATTERY_HEADER = 0b00110011;
-    private static final byte BATTERY_NO_NOTIFICATION_HEADER = 0b00110100;
-    private static final String BATTERY = "01048F";
-    private static final byte RANDOM_RESOLVABLE_DATA_HEADER = 0b01000110;
-    private static final String RANDOM_RESOLVABLE_DATA = "11223344";
-
-    private final FastPairDecoder mDecoder = new FastPairDecoder();
-    private final BluetoothDevice mBluetoothDevice =
-            BluetoothAdapter.getDefaultAdapter().getRemoteDevice(DEVICE_ADDRESS);
-
-    @Test
-    public void filter() {
-        assertThat(FastPairDecoder.FILTER.matches(bleSighting(getFastPairRecord()))).isTrue();
-        assertThat(FastPairDecoder.FILTER.matches(bleSighting(FAST_PAIR_SHARED_ACCOUNT_KEY_RECORD)))
-                .isTrue();
-
-        // Any ID is a valid frame.
-        assertThat(FastPairDecoder.FILTER.matches(
-                bleSighting(newFastPairRecord(Hex.stringToBytes("000001"))))).isTrue();
-        assertThat(FastPairDecoder.FILTER.matches(
-                bleSighting(newFastPairRecord(Hex.stringToBytes("098FEC"))))).isTrue();
-        assertThat(FastPairDecoder.FILTER.matches(
-                bleSighting(FastPairTestData.newFastPairRecord(
-                        LONG_MODEL_ID_HEADER, Hex.stringToBytes(LONG_MODEL_ID))))).isTrue();
-    }
-
-    @Test
-    public void getModelId() {
-        assertThat(mDecoder.getBeaconIdBytes(parseFromBytes(getFastPairRecord())))
-                .isEqualTo(FAST_PAIR_MODEL_ID);
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
-        assertThat(
-                mDecoder.getBeaconIdBytes(
-                        newBleRecord(fastPairServiceData.createServiceData())))
-                .isEqualTo(Hex.stringToBytes(LONG_MODEL_ID));
-
-        FastPairServiceData fastPairServiceData1 =
-                new FastPairServiceData(PADDED_LONG_MODEL_ID_HEADER, PADDED_LONG_MODEL_ID);
-        assertThat(
-                mDecoder.getBeaconIdBytes(
-                        newBleRecord(fastPairServiceData1.createServiceData())))
-                .isEqualTo(Hex.stringToBytes(TRIMMED_LONG_MODEL_ID));
-    }
-
-    @Test
-    public void getBeaconIdType() {
-        assertThat(mDecoder.getBeaconIdType()).isEqualTo(1);
-    }
-
-    @Test
-    public void getCalibratedBeaconTxPower() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
-        assertThat(
-                mDecoder.getCalibratedBeaconTxPower(
-                        newBleRecord(fastPairServiceData.createServiceData())))
-                .isNull();
-    }
-
-    @Test
-    public void getServiceDataArray() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
-        assertThat(
-                mDecoder.getServiceDataArray(
-                        newBleRecord(fastPairServiceData.createServiceData())))
-                .isEqualTo(Hex.stringToBytes("101122334455667788"));
-    }
-
-    @Test
-    public void hasBloomFilter() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
-        assertThat(
-                mDecoder.hasBloomFilter(
-                        newBleRecord(fastPairServiceData.createServiceData())))
-                .isFalse();
-    }
-
-    @Test
-    public void hasModelId_allCases() {
-        // One type of the format is just the 3-byte model ID. This format has no header byte (all 3
-        // service data bytes are the model ID in little endian).
-        assertThat(hasModelId("112233", mDecoder)).isTrue();
-
-        // If the model ID is shorter than 3 bytes, then return null.
-        assertThat(hasModelId("11", mDecoder)).isFalse();
-
-        // If the data is longer than 3 bytes,
-        // byte 0 must be 0bVVVLLLLR (version, ID length, reserved).
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData((byte) 0b00001000, "11223344");
-        assertThat(
-                FastPairDecoder.hasBeaconIdBytes(
-                        newBleRecord(fastPairServiceData.createServiceData()))).isTrue();
-
-        FastPairServiceData fastPairServiceData1 =
-                new FastPairServiceData((byte) 0b00001010, "1122334455");
-        assertThat(
-                FastPairDecoder.hasBeaconIdBytes(
-                        newBleRecord(fastPairServiceData1.createServiceData()))).isTrue();
-
-        // Length bits correct, but version bits != version 0 (only supported version).
-        FastPairServiceData fastPairServiceData2 =
-                new FastPairServiceData((byte) 0b00101000, "11223344");
-        assertThat(
-                FastPairDecoder.hasBeaconIdBytes(
-                        newBleRecord(fastPairServiceData2.createServiceData()))).isFalse();
-
-        // Version bits correct, but length bits incorrect (too big, too small).
-        FastPairServiceData fastPairServiceData3 =
-                new FastPairServiceData((byte) 0b00001010, "11223344");
-        assertThat(
-                FastPairDecoder.hasBeaconIdBytes(
-                        newBleRecord(fastPairServiceData3.createServiceData()))).isFalse();
-
-        FastPairServiceData fastPairServiceData4 =
-                new FastPairServiceData((byte) 0b00000010, "11223344");
-        assertThat(
-                FastPairDecoder.hasBeaconIdBytes(
-                        newBleRecord(fastPairServiceData4.createServiceData()))).isFalse();
-    }
-
-    @Test
-    public void getBatteryLevel() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_HEADER);
-        fastPairServiceData.mExtraFields.add(BATTERY);
-        assertThat(
-                FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BATTERY));
-    }
-
-    @Test
-    public void getBatteryLevel_notIncludedInPacket() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(
-                FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData())).isNull();
-    }
-
-    @Test
-    public void getBatteryLevel_noModelId() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData((byte) 0b00000000, null);
-        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_HEADER);
-        fastPairServiceData.mExtraFields.add(BATTERY);
-        assertThat(
-                FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BATTERY));
-    }
-
-    @Test
-    public void getBatteryLevel_multipelExtraField() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_HEADER);
-        fastPairServiceData.mExtraFields.add(BATTERY);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(
-                FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BATTERY));
-    }
-
-    @Test
-    public void getBatteryLevelNoNotification() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_NO_NOTIFICATION_HEADER);
-        fastPairServiceData.mExtraFields.add(BATTERY);
-        assertThat(
-                FastPairDecoder.getBatteryLevelNoNotification(
-                        fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BATTERY));
-    }
-
-    @Test
-    public void getBatteryLevelNoNotification_notIncludedInPacket() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(
-                FastPairDecoder.getBatteryLevelNoNotification(
-                        fastPairServiceData.createServiceData())).isNull();
-    }
-
-    @Test
-    public void getBatteryLevelNoNotification_noModelId() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData((byte) 0b00000000, null);
-        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_NO_NOTIFICATION_HEADER);
-        fastPairServiceData.mExtraFields.add(BATTERY);
-        assertThat(
-                FastPairDecoder.getBatteryLevelNoNotification(
-                        fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BATTERY));
-    }
-
-    @Test
-    public void getBatteryLevelNoNotification_multipleExtraField() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_NO_NOTIFICATION_HEADER);
-        fastPairServiceData.mExtraFields.add(BATTERY);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(
-                FastPairDecoder.getBatteryLevelNoNotification(
-                        fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BATTERY));
-    }
-
-    @Test
-    public void getBloomFilter() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
-    }
-
-    @Test
-    public void getBloomFilterNoNotification() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_NO_NOTIFICATION_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(
-                FastPairDecoder.getBloomFilterNoNotification(
-                        fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
-    }
-
-    @Test
-    public void getBloomFilter_smallModelId() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(null, MODEL_ID);
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isNull();
-    }
-
-    @Test
-    public void getBloomFilter_headerVersionBitsNotZero() {
-        // Header bits are defined as 0bVVVLLLLR (V=version, L=length, R=reserved), must be zero.
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData((byte) 0b00100000, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isNull();
-    }
-
-    @Test
-    public void getBloomFilter_noExtraFieldBytesIncluded() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(null);
-        fastPairServiceData.mExtraFields.add(null);
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isNull();
-    }
-
-    @Test
-    public void getBloomFilter_extraFieldLengthIsZero() {
-        // The extra field header is formatted as 0bLLLLTTTT (L=length, T=type).
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00000000);
-        fastPairServiceData.mExtraFields.add(null);
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .hasLength(0);
-    }
-
-    @Test
-    public void getBloomFilter_extraFieldLengthLongerThanPacket() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b11110000);
-        fastPairServiceData.mExtraFields.add("1122");
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isNull();
-    }
-
-    @Test
-    public void getBloomFilter_secondExtraFieldLengthLongerThanPacket() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00100000);
-        fastPairServiceData.mExtraFields.add("1122");
-        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b11110001);
-        fastPairServiceData.mExtraFields.add("3344");
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData())).isNull();
-    }
-
-    @Test
-    public void getBloomFilter_typeIsWrong() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b01100001);
-        fastPairServiceData.mExtraFields.add("112233445566");
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isNull();
-    }
-
-    @Test
-    public void getBloomFilter_noModelId() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData((byte) 0b00000000, null);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
-    }
-
-    @Test
-    public void getBloomFilter_noModelIdAndMultipleExtraFields() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData((byte) 0b00000000, null);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00010001);
-        fastPairServiceData.mExtraFields.add("00");
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
-    }
-
-    @Test
-    public void getBloomFilter_modelIdAndMultipleExtraFields() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_SALT_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER_SALT);
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
-    }
-
-    @Test
-    public void getBloomFilterSalt_modelIdAndMultipleExtraFields() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_SALT_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER_SALT);
-        assertThat(
-                FastPairDecoder.getBloomFilterSalt(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER_SALT));
-    }
-
-    @Test
-    public void getBloomFilter_modelIdAndMultipleExtraFieldsWithBloomFilterLast() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00010001);
-        fastPairServiceData.mExtraFields.add("1A");
-        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00100010);
-        fastPairServiceData.mExtraFields.add("2CFE");
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
-    }
-
-    @Test
-    public void getBloomFilter_modelIdAndMultipleExtraFieldsWithSameType() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add("000000000000");
-        assertThat(
-                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
-    }
-
-    @Test
-    public void getBloomFilter_longExtraField() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(LONG_BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(LONG_BLOOM_FILTER);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add("000000000000");
-        assertThat(
-                FastPairDecoder.getBloomFilter(
-                        fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(LONG_BLOOM_FILTER));
-    }
-
-    @Test
-    public void getRandomResolvableData_whenNoConnectionState() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        assertThat(
-                FastPairDecoder.getRandomResolvableData(
-                        fastPairServiceData.createServiceData()))
-                .isEqualTo(null);
-    }
-
-    @Test
-    public void getRandomResolvableData_whenContainConnectionState() {
-        FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(RANDOM_RESOLVABLE_DATA_HEADER);
-        fastPairServiceData.mExtraFields.add(RANDOM_RESOLVABLE_DATA);
-        assertThat(
-                FastPairDecoder.getRandomResolvableData(
-                        fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(RANDOM_RESOLVABLE_DATA));
-    }
-
-    private static BleRecord newBleRecord(byte[] serviceDataBytes) {
-        return parseFromBytes(newFastPairRecord(serviceDataBytes));
-    }
-
-    private static boolean hasModelId(String modelId, FastPairDecoder decoder) {
-        byte[] modelIdBytes = Hex.stringToBytes(modelId);
-        BleRecord bleRecord =
-                parseFromBytes(FastPairTestData.newFastPairRecord((byte) 0, modelIdBytes));
-        return FastPairDecoder.hasBeaconIdBytes(bleRecord)
-                && Arrays.equals(decoder.getBeaconIdBytes(bleRecord), modelIdBytes);
-    }
-
-    private BleSighting bleSighting(byte[] frame) {
-        return new BleSighting(mBluetoothDevice, frame, RSSI,
-                TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()));
-    }
-
-    static class FastPairServiceData {
-        private Byte mHeader;
-        private String mModelId;
-        List<Byte> mExtraFieldHeaders = new ArrayList<>();
-        List<String> mExtraFields = new ArrayList<>();
-
-        FastPairServiceData(Byte header, String modelId) {
-            this.mHeader = header;
-            this.mModelId = modelId;
-        }
-        private byte[] createServiceData() {
-            if (mExtraFieldHeaders.size() != mExtraFields.size()) {
-                throw new RuntimeException("Number of headers and extra fields must match.");
-            }
-            byte[] serviceData =
-                    Bytes.concat(
-                            mHeader == null ? new byte[0] : new byte[] {mHeader},
-                            mModelId == null ? new byte[0] : Hex.stringToBytes(mModelId));
-            for (int i = 0; i < mExtraFieldHeaders.size(); i++) {
-                serviceData =
-                        Bytes.concat(
-                                serviceData,
-                                mExtraFieldHeaders.get(i) != null
-                                        ? new byte[] {mExtraFieldHeaders.get(i)}
-                                        : new byte[0],
-                                mExtraFields.get(i) != null
-                                        ? Hex.stringToBytes(mExtraFields.get(i))
-                                        : new byte[0]);
-            }
-            return serviceData;
-        }
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java
deleted file mode 100644
index 29f8d4e..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java
+++ /dev/null
@@ -1,65 +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.server.nearby.common.ble.util;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class RangingUtilsTest {
-    // relative error to be used in comparing doubles
-    private static final double DELTA = 1e-5;
-
-    @Test
-    public void distanceFromRssi_getCorrectValue() {
-        // Distance expected to be 1.0 meters based on an RSSI/TxPower of -41dBm
-        // Using params: int rssi (dBm), int calibratedTxPower (dBm)
-        double distance = RangingUtils.distanceFromRssiAndTxPower(-82, -41);
-        assertThat(distance).isWithin(DELTA).of(1.0);
-
-        double distance2 = RangingUtils.distanceFromRssiAndTxPower(-111, -50);
-        assertThat(distance2).isWithin(DELTA).of(10.0);
-
-        //rssi txpower
-        double distance4 = RangingUtils.distanceFromRssiAndTxPower(-50, -29);
-        assertThat(distance4).isWithin(DELTA).of(0.1);
-    }
-
-    @Test
-    public void testRssiFromDistance() {
-        // RSSI expected at 1 meter based on the calibrated tx field of -41dBm
-        // Using params: distance (m), int calibratedTxPower (dBm),
-        int rssi = RangingUtils.rssiFromTargetDistance(1.0, -41);
-        int rssi1 = RangingUtils.rssiFromTargetDistance(0.0, -41);
-
-        assertThat(rssi).isEqualTo(-82);
-        assertThat(rssi1).isEqualTo(-41);
-    }
-
-    @Test
-    public void testOutOfRange() {
-        double distance = RangingUtils.distanceFromRssiAndTxPower(-200, -41);
-        assertThat(distance).isWithin(DELTA).of(177.82794);
-
-        distance = RangingUtils.distanceFromRssiAndTxPower(200, -41);
-        assertThat(distance).isWithin(DELTA).of(0);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/StringUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/StringUtilsTest.java
deleted file mode 100644
index 6b20fdd..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/StringUtilsTest.java
+++ /dev/null
@@ -1,122 +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.server.nearby.common.ble.util;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.ParcelUuid;
-import android.util.SparseArray;
-
-import com.android.server.nearby.common.ble.BleRecord;
-
-import org.junit.Test;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class StringUtilsTest {
-    // iBeacon (Apple) Packet 1
-    private static final byte[] BEACON = {
-            // Flags
-            (byte) 0x02,
-            (byte) 0x01,
-            (byte) 0x06,
-            // Manufacturer-specific data header
-            (byte) 0x1a,
-            (byte) 0xff,
-            (byte) 0x4c,
-            (byte) 0x00,
-            // iBeacon Type
-            (byte) 0x02,
-            // Frame length
-            (byte) 0x15,
-            // iBeacon Proximity UUID
-            (byte) 0xf7,
-            (byte) 0x82,
-            (byte) 0x6d,
-            (byte) 0xa6,
-            (byte) 0x4f,
-            (byte) 0xa2,
-            (byte) 0x4e,
-            (byte) 0x98,
-            (byte) 0x80,
-            (byte) 0x24,
-            (byte) 0xbc,
-            (byte) 0x5b,
-            (byte) 0x71,
-            (byte) 0xe0,
-            (byte) 0x89,
-            (byte) 0x3e,
-            // iBeacon Instance ID (Major/Minor)
-            (byte) 0x44,
-            (byte) 0xd0,
-            (byte) 0x25,
-            (byte) 0x22,
-            // Tx Power
-            (byte) 0xb3,
-            // RSP
-            (byte) 0x08,
-            (byte) 0x09,
-            (byte) 0x4b,
-            (byte) 0x6f,
-            (byte) 0x6e,
-            (byte) 0x74,
-            (byte) 0x61,
-            (byte) 0x6b,
-            (byte) 0x74,
-            (byte) 0x02,
-            (byte) 0x0a,
-            (byte) 0xf4,
-            (byte) 0x0a,
-            (byte) 0x16,
-            (byte) 0x0d,
-            (byte) 0xd0,
-            (byte) 0x74,
-            (byte) 0x6d,
-            (byte) 0x4d,
-            (byte) 0x6b,
-            (byte) 0x32,
-            (byte) 0x36,
-            (byte) 0x64,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00,
-            (byte) 0x00
-    };
-
-    @Test
-    public void testToString() {
-        BleRecord record = BleRecord.parseFromBytes(BEACON);
-        assertThat(StringUtils.toString((SparseArray<byte[]>) null)).isEqualTo("null");
-        assertThat(StringUtils.toString(new SparseArray<byte[]>())).isEqualTo("{}");
-        assertThat(StringUtils
-                .toString((Map<ParcelUuid, byte[]>) null)).isEqualTo("null");
-        assertThat(StringUtils
-                .toString(new HashMap<ParcelUuid, byte[]>())).isEqualTo("{}");
-        assertThat(StringUtils.toString(record.getManufacturerSpecificData()))
-                .isEqualTo("{76=[2, 21, -9, -126, 109, -90, 79, -94, 78, -104, -128,"
-                        + " 36, -68, 91, 113, -32, -119, 62, 68, -48, 37, 34, -77]}");
-        assertThat(StringUtils.toString(record.getServiceData()))
-                .isEqualTo("{0000d00d-0000-1000-8000-00805f9b34fb="
-                        + "[116, 109, 77, 107, 50, 54, 100]}");
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java
deleted file mode 100644
index 30df81f..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java
+++ /dev/null
@@ -1,307 +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.server.nearby.common.bloomfilter;
-
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.primitives.Bytes.concat;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-/**
- * Unit-tests for the {@link BloomFilter} class.
- */
-public class BloomFilterTest {
-    private static final int BYTE_ARRAY_LENGTH = 100;
-
-    private final BloomFilter mBloomFilter =
-            new BloomFilter(new byte[BYTE_ARRAY_LENGTH], newHasher());
-
-    public BloomFilter.Hasher newHasher() {
-        return new FastPairBloomFilterHasher();
-    }
-
-    @Test
-    public void emptyFilter_returnsEmptyArray() throws Exception {
-        assertThat(mBloomFilter.asBytes()).isEqualTo(new byte[BYTE_ARRAY_LENGTH]);
-    }
-
-    @Test
-    public void emptyFilter_neverContains() throws Exception {
-        assertThat(mBloomFilter.possiblyContains(element(1))).isFalse();
-        assertThat(mBloomFilter.possiblyContains(element(2))).isFalse();
-        assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
-    }
-
-    @Test
-    public void add() throws Exception {
-        assertThat(mBloomFilter.possiblyContains(element(1))).isFalse();
-        mBloomFilter.add(element(1));
-        assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
-    }
-
-    @Test
-    public void add_onlyGivenArgAdded() throws Exception {
-        mBloomFilter.add(element(1));
-        assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
-        assertThat(mBloomFilter.possiblyContains(element(2))).isFalse();
-        assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
-    }
-
-    @Test
-    public void add_multipleArgs() throws Exception {
-        mBloomFilter.add(element(1));
-        mBloomFilter.add(element(2));
-        assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
-        assertThat(mBloomFilter.possiblyContains(element(2))).isTrue();
-        assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
-    }
-
-    /**
-     * This test was added because of a bug where the BloomFilter doesn't utilize all bits given.
-     * Functionally, the filter still works, but we just have a much higher false positive rate. The
-     * bug was caused by confusing bit length and byte length, which made our BloomFilter only set
-     * bits on the first byteLength (bitLength / 8) bits rather than the whole bitLength bits.
-     *
-     * <p>Here, we're verifying that the bits set are somewhat scattered. So instead of something
-     * like [ 0, 1, 1, 0, 0, 0, 0, ..., 0 ], we should be getting something like
-     * [ 0, 1, 0, 0, 1, 1, 0, 0,0, 1, ..., 1, 0].
-     */
-    @Test
-    public void randomness_noEndBias() throws Exception {
-        // Add one element to our BloomFilter.
-        mBloomFilter.add(element(1));
-
-        // Record the amount of non-zero bytes and the longest streak of zero bytes in the resulting
-        // BloomFilter. This is an approximation of reasonable distribution since we're recording by
-        // bytes instead of bits.
-        int nonZeroCount = 0;
-        int longestZeroStreak = 0;
-        int currentZeroStreak = 0;
-        for (byte b : mBloomFilter.asBytes()) {
-            if (b == 0) {
-                currentZeroStreak++;
-            } else {
-                // Increment the number of non-zero bytes we've seen, update the longest zero
-                // streak, and then reset the current zero streak.
-                nonZeroCount++;
-                longestZeroStreak = Math.max(longestZeroStreak, currentZeroStreak);
-                currentZeroStreak = 0;
-            }
-        }
-        // Update the longest zero streak again for the tail case.
-        longestZeroStreak = Math.max(longestZeroStreak, currentZeroStreak);
-
-        // Since randomness is hard to measure within one unit test, we instead do a valid check.
-        // All non-zero bytes should not be packed into one end of the array.
-        //
-        // In this case, the size of one end is approximated to be:
-        //   BYTE_ARRAY_LENGTH / nonZeroCount.
-        // Therefore, the longest zero streak should be less than:
-        //   BYTE_ARRAY_LENGTH - one end of the array.
-        int longestAcceptableZeroStreak = BYTE_ARRAY_LENGTH - (BYTE_ARRAY_LENGTH / nonZeroCount);
-        assertThat(longestZeroStreak).isAtMost(longestAcceptableZeroStreak);
-    }
-
-    @Test
-    public void randomness_falsePositiveRate() throws Exception {
-        // Create a new BloomFilter with a length of only 10 bytes.
-        BloomFilter bloomFilter = new BloomFilter(new byte[10], newHasher());
-
-        // Add 5 distinct elements to the BloomFilter.
-        for (int i = 0; i < 5; i++) {
-            bloomFilter.add(element(i));
-        }
-
-        // Now test 100 other elements and record the number of false positives.
-        int falsePositives = 0;
-        for (int i = 5; i < 105; i++) {
-            falsePositives += bloomFilter.possiblyContains(element(i)) ? 1 : 0;
-        }
-
-        // We expect the false positive rate to be 3% with 5 elements in a 10 byte filter. Thus,
-        // we give a little leeway and verify that the false positive rate is no more than 5%.
-        assertWithMessage(
-                String.format(
-                        "False positive rate too large. Expected <= 5%%, but got %d%%.",
-                        falsePositives))
-                .that(falsePositives <= 5)
-                .isTrue();
-        System.out.printf("False positive rate: %d%%%n", falsePositives);
-    }
-
-
-    private String element(int index) {
-        return "ELEMENT_" + index;
-    }
-
-    @Test
-    public void specificBitPattern() throws Exception {
-        // Create a new BloomFilter along with a fixed set of elements
-        // and bit patterns to verify with.
-        BloomFilter bloomFilter = new BloomFilter(new byte[6], newHasher());
-        // Combine an account key and mac address.
-        byte[] element =
-                concat(
-                        base16().decode("11223344556677889900AABBCCDDEEFF"),
-                        base16().withSeparator(":", 2).decode("84:68:3E:00:02:11"));
-        byte[] expectedBitPattern = new byte[] {0x50, 0x00, 0x04, 0x15, 0x08, 0x01};
-
-        // Add the fixed elements to the filter.
-        bloomFilter.add(element);
-
-        // Verify that the resulting bytes match the expected one.
-        byte[] bloomFilterBytes = bloomFilter.asBytes();
-        assertWithMessage(
-                "Unexpected bit pattern. Expected %s, but got %s.",
-                base16().encode(expectedBitPattern), base16().encode(bloomFilterBytes))
-                .that(Arrays.equals(expectedBitPattern, bloomFilterBytes))
-                .isTrue();
-
-        // Verify that the expected bit pattern creates a BloomFilter containing all fixed elements.
-        bloomFilter = new BloomFilter(expectedBitPattern, newHasher());
-        assertThat(bloomFilter.possiblyContains(element)).isTrue();
-    }
-
-    // This test case has been on the spec,
-    // https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
-    // Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
-    // address) to clarify test results with partners.
-    @Test
-    public void specificBitPattern_hasOneAccountKey() {
-        BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
-        BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
-        byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
-        byte[] salt1 = base16().decode("C7");
-        byte[] salt2 = base16().decode("C7C8");
-
-        // Add the fixed elements to the filter.
-        bloomFilter1.add(concat(accountKey, salt1));
-        bloomFilter2.add(concat(accountKey, salt2));
-
-        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("0A428810"));
-        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("020C802A"));
-    }
-
-    // Adds this test case to spec. We can easily change the parameters (e.g. account key, ble
-    // address, battery data) to clarify test results with partners.
-    @Test
-    public void specificBitPattern_hasOneAccountKey_withBatteryData() {
-        BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
-        BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
-        byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
-        byte[] salt1 = base16().decode("C7");
-        byte[] salt2 = base16().decode("C7C8");
-        byte[] batteryData = {
-                0b00110011, // length = 3, show UI indication.
-                0b01000000, // Left bud: not charging, battery level = 64.
-                0b01000000, // Right bud: not charging, battery level = 64.
-                0b01000000 // Case: not charging, battery level = 64.
-        };
-
-        // Adds battery data to build bloom filter.
-        bloomFilter1.add(concat(accountKey, salt1, batteryData));
-        bloomFilter2.add(concat(accountKey, salt2, batteryData));
-
-        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("4A00F000"));
-        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("0101460A"));
-    }
-
-    // This test case has been on the spec,
-    // https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
-    // Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
-    // address) to clarify test results with partners.
-    @Test
-    public void specificBitPattern_hasTwoAccountKeys() {
-        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
-        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
-        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
-        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
-        byte[] salt1 = base16().decode("C7");
-        byte[] salt2 = base16().decode("C7C8");
-
-        // Adds the fixed elements to the filter.
-        bloomFilter1.add(concat(accountKey1, salt1));
-        bloomFilter1.add(concat(accountKey2, salt1));
-        bloomFilter2.add(concat(accountKey1, salt2));
-        bloomFilter2.add(concat(accountKey2, salt2));
-
-        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("2FBA064200"));
-        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("844A62208B"));
-    }
-
-    // Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
-    // address, battery data) to clarify test results with partners.
-    @Test
-    public void specificBitPattern_hasTwoAccountKeys_withBatteryData() {
-        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
-        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
-        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
-        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
-        byte[] salt1 = base16().decode("C7");
-        byte[] salt2 = base16().decode("C7C8");
-        byte[] batteryData = {
-                0b00110011, // length = 3, show UI indication.
-                0b01000000, // Left bud: not charging, battery level = 64.
-                0b01000000, // Right bud: not charging, battery level = 64.
-                0b01000000 // Case: not charging, battery level = 64.
-        };
-
-        // Adds battery data to build bloom filter.
-        bloomFilter1.add(concat(accountKey1, salt1, batteryData));
-        bloomFilter1.add(concat(accountKey2, salt1, batteryData));
-        bloomFilter2.add(concat(accountKey1, salt2, batteryData));
-        bloomFilter2.add(concat(accountKey2, salt2, batteryData));
-
-        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("102256C04D"));
-        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("461524D008"));
-    }
-
-    // Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
-    // address, battery data and battery remaining time) to clarify test results with partners.
-    @Test
-    public void specificBitPattern_hasTwoAccountKeys_withBatteryLevelAndRemainingTime() {
-        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
-        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
-        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
-        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
-        byte[] salt1 = base16().decode("C7");
-        byte[] salt2 = base16().decode("C7C8");
-        byte[] batteryData = {
-                0b00110011, // length = 3, show UI indication.
-                0b01000000, // Left bud: not charging, battery level = 64.
-                0b01000000, // Right bud: not charging, battery level = 64.
-                0b01000000 // Case: not charging, battery level = 64.
-        };
-        byte[] batteryRemainingTime = {
-                0b00010101, // length = 1, type = 0b0101 (remaining battery time).
-                0x1E, // remaining battery time (in minutes) =  30 minutes.
-        };
-
-        // Adds battery data to build bloom filter.
-        bloomFilter1.add(concat(accountKey1, salt1, batteryData, batteryRemainingTime));
-        bloomFilter1.add(concat(accountKey2, salt1, batteryData, batteryRemainingTime));
-        bloomFilter2.add(concat(accountKey1, salt2, batteryData, batteryRemainingTime));
-        bloomFilter2.add(concat(accountKey2, salt2, batteryData, batteryRemainingTime));
-
-        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("32A086B41A"));
-        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("C2A042043E"));
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java
deleted file mode 100644
index 92c3b1a..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java
+++ /dev/null
@@ -1,43 +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.server.nearby.common.bloomfilter;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import org.junit.Test;
-
-import java.nio.charset.Charset;
-
-public class FastPairBloomFilterHasherTest {
-    private static final Charset CHARSET = UTF_8;
-    private static FastPairBloomFilterHasher sFastPairBloomFilterHasher =
-            new FastPairBloomFilterHasher();
-    @Test
-    public void getHashes() {
-        int[] hashe1 = sFastPairBloomFilterHasher.getHashes(element(1).getBytes(CHARSET));
-        int[] hashe2 = sFastPairBloomFilterHasher.getHashes(element(1).getBytes(CHARSET));
-        int[] hashe3 = sFastPairBloomFilterHasher.getHashes(element(2).getBytes(CHARSET));
-        assertThat(hashe1).isEqualTo(hashe2);
-        assertThat(hashe1).isNotEqualTo(hashe3);
-    }
-
-    private String element(int index) {
-        return "ELEMENT_" + index;
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGeneratorTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGeneratorTest.java
deleted file mode 100644
index 35a45c0..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGeneratorTest.java
+++ /dev/null
@@ -1,49 +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.server.nearby.common.bluetooth.fastpair;
-
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.security.NoSuchAlgorithmException;
-
-/**
- * Unit tests for {@link AccountKeyGenerator}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AccountKeyGeneratorTest {
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void createAccountKey() throws NoSuchAlgorithmException {
-        byte[] accountKey = AccountKeyGenerator.createAccountKey();
-
-        assertThat(accountKey).hasLength(16);
-        assertThat(accountKey[0]).isEqualTo(AccountKeyCharacteristic.TYPE);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoderTest.java
deleted file mode 100644
index cf40fc6..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoderTest.java
+++ /dev/null
@@ -1,207 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AdditionalDataEncoder.MAX_LENGTH_OF_DATA;
-import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.KEY_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE;
-import static com.android.server.nearby.common.bluetooth.fastpair.NamingEncoder.EXTRACT_HMAC_SIZE;
-
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.security.GeneralSecurityException;
-
-/**
- * Unit tests for {@link AdditionalDataEncoder}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AdditionalDataEncoderTest {
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void decodeEncodedAdditionalDataPacket_mustGetSameRawData()
-            throws GeneralSecurityException {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] rawData = base16().decode("00112233445566778899AABBCCDDEEFF");
-
-        byte[] encodedAdditionalDataPacket =
-                AdditionalDataEncoder.encodeAdditionalDataPacket(secret, rawData);
-        byte[] additionalData =
-                AdditionalDataEncoder
-                        .decodeAdditionalDataPacket(secret, encodedAdditionalDataPacket);
-
-        assertThat(additionalData).isEqualTo(rawData);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputNullSecretKeyToEncode_mustThrowException() {
-        byte[] rawData = base16().decode("00112233445566778899AABBCCDDEEFF");
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> AdditionalDataEncoder.encodeAdditionalDataPacket(null, rawData));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Incorrect secret for encoding additional data packet");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputIncorrectKeySizeToEncode_mustThrowException() {
-        byte[] secret = new byte[KEY_LENGTH - 1];
-        byte[] rawData = base16().decode("00112233445566778899AABBCCDDEEFF");
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> AdditionalDataEncoder.encodeAdditionalDataPacket(secret, rawData));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Incorrect secret for encoding additional data packet");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputNullAdditionalDataToEncode_mustThrowException()
-            throws GeneralSecurityException {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> AdditionalDataEncoder.encodeAdditionalDataPacket(secret, null));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Invalid data for encoding additional data packet");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputInvalidAdditionalDataSizeToEncode_mustThrowException()
-            throws GeneralSecurityException {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] rawData = base16().decode("");
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> AdditionalDataEncoder.encodeAdditionalDataPacket(secret, rawData));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Invalid data for encoding additional data packet");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputTooLargeAdditionalDataSizeToEncode_mustThrowException()
-            throws GeneralSecurityException {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] rawData = new byte[MAX_LENGTH_OF_DATA + 1];
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> AdditionalDataEncoder.encodeAdditionalDataPacket(secret, rawData));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Invalid data for encoding additional data packet");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputIncorrectKeySizeToDecode_mustThrowException() {
-        byte[] secret = new byte[KEY_LENGTH - 1];
-        byte[] packet = base16().decode("01234567890123456789");
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> AdditionalDataEncoder.decodeAdditionalDataPacket(secret, packet));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Incorrect secret for decoding additional data packet");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputTooSmallPacketSize_mustThrowException() {
-        byte[] secret = new byte[KEY_LENGTH];
-        byte[] packet = new byte[EXTRACT_HMAC_SIZE - 1];
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> AdditionalDataEncoder.decodeAdditionalDataPacket(secret, packet));
-
-        assertThat(exception).hasMessageThat().contains("Additional data packet size is incorrect");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputTooLargePacketSize_mustThrowException() throws GeneralSecurityException {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] packet = new byte[MAX_LENGTH_OF_DATA + EXTRACT_HMAC_SIZE + NONCE_SIZE + 1];
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> AdditionalDataEncoder.decodeAdditionalDataPacket(secret, packet));
-
-        assertThat(exception).hasMessageThat().contains("Additional data packet size is incorrect");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputIncorrectHmacToDecode_mustThrowException() throws GeneralSecurityException {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] rawData = base16().decode("00112233445566778899AABBCCDDEEFF");
-
-        byte[] additionalDataPacket = AdditionalDataEncoder
-                .encodeAdditionalDataPacket(secret, rawData);
-        additionalDataPacket[0] = (byte) ~additionalDataPacket[0];
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> AdditionalDataEncoder
-                                .decodeAdditionalDataPacket(secret, additionalDataPacket));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Verify HMAC failed, could be incorrect key or packet.");
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryptionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryptionTest.java
deleted file mode 100644
index 7d86037..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryptionTest.java
+++ /dev/null
@@ -1,214 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE;
-import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.KEY_LENGTH;
-
-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 android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-
-/** Unit tests for {@link AesCtrMultpleBlockEncryption}. */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AesCtrMultipleBlockEncryptionTest {
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void decryptEncryptedData_nonBlockSizeAligned_mustEqualToPlaintext() throws Exception {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8); // The length is 31.
-
-        byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
-        byte[] decrypted = AesCtrMultipleBlockEncryption.decrypt(secret, encrypted);
-
-        assertThat(decrypted).isEqualTo(plaintext);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void decryptEncryptedData_blockSizeAligned_mustEqualToPlaintext() throws Exception {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] plaintext =
-                // The length is 32.
-                base16().decode("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF");
-
-        byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
-        byte[] decrypted = AesCtrMultipleBlockEncryption.decrypt(secret, encrypted);
-
-        assertThat(decrypted).isEqualTo(plaintext);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void generateNonceTwice_mustBeDifferent() {
-        byte[] nonce1 = AesCtrMultipleBlockEncryption.generateNonce();
-        byte[] nonce2 = AesCtrMultipleBlockEncryption.generateNonce();
-
-        assertThat(nonce1).isNotEqualTo(nonce2);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void encryptedSamePlaintext_mustBeDifferentEncryptedResult() throws Exception {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
-
-        byte[] encrypted1 = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
-        byte[] encrypted2 = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
-
-        assertThat(encrypted1).isNotEqualTo(encrypted2);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void encryptData_mustBeDifferentToUnencrypted() throws Exception {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
-
-        byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
-
-        assertThat(encrypted).isNotEqualTo(plaintext);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputIncorrectKeySizeToEncrypt_mustThrowException() {
-        byte[] secret = new byte[KEY_LENGTH + 1];
-        byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
-
-        IllegalArgumentException exception =
-                assertThrows(
-                        IllegalArgumentException.class,
-                        () -> AesCtrMultipleBlockEncryption.encrypt(secret, plaintext));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Incorrect key length for encryption, only supports 16-byte AES Key.");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputIncorrectKeySizeToDecrypt_mustThrowException() {
-        byte[] secret = new byte[KEY_LENGTH - 1];
-        byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
-
-        IllegalArgumentException exception =
-                assertThrows(
-                        IllegalArgumentException.class,
-                        () -> AesCtrMultipleBlockEncryption.decrypt(secret, plaintext));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Incorrect key length for encryption, only supports 16-byte AES Key.");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputIncorrectDataSizeToDecrypt_mustThrowException()
-            throws GeneralSecurityException {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
-
-        byte[] encryptedData = Arrays.copyOfRange(
-                AesCtrMultipleBlockEncryption.encrypt(secret, plaintext), /*from=*/ 0, NONCE_SIZE);
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> AesCtrMultipleBlockEncryption.decrypt(secret, encryptedData));
-
-        assertThat(exception).hasMessageThat().contains("Incorrect data length");
-    }
-
-    // Add some random tests that for a certain amount of random plaintext of random length to prove
-    // our encryption/decryption is correct. This is suggested by security team.
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void decryptEncryptedRandomDataForCertainAmount_mustEqualToOriginalData()
-            throws Exception {
-        SecureRandom random = new SecureRandom();
-        for (int i = 0; i < 1000; i++) {
-            byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-            int dataLength = random.nextInt(64) + 1;
-            byte[] data = new byte[dataLength];
-            random.nextBytes(data);
-
-            byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, data);
-            byte[] decrypted = AesCtrMultipleBlockEncryption.decrypt(secret, encrypted);
-
-            assertThat(decrypted).isEqualTo(data);
-        }
-    }
-
-    // Add some random tests that for a certain amount of random plaintext of random length to prove
-    // our encryption is correct. This is suggested by security team.
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void twoDistinctEncryptionOnSameRandomData_mustBeDifferentResult() throws Exception {
-        SecureRandom random = new SecureRandom();
-        for (int i = 0; i < 1000; i++) {
-            byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-            int dataLength = random.nextInt(64) + 1;
-            byte[] data = new byte[dataLength];
-            random.nextBytes(data);
-
-            byte[] encrypted1 = AesCtrMultipleBlockEncryption.encrypt(secret, data);
-            byte[] encrypted2 = AesCtrMultipleBlockEncryption.encrypt(secret, data);
-
-            assertThat(encrypted1).isNotEqualTo(encrypted2);
-        }
-    }
-
-    // Adds this test example on spec. Also we can easily change the parameters(e.g. secret, data,
-    // nonce) to clarify test results with partners.
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputTestExampleToEncrypt_getCorrectResult() throws GeneralSecurityException {
-        byte[] secret = base16().decode("0123456789ABCDEF0123456789ABCDEF");
-        byte[] nonce = base16().decode("0001020304050607");
-
-        // "Someone's Google Headphone".getBytes(UTF_8) is
-        // base16().decode("536F6D656F6E65277320476F6F676C65204865616470686F6E65");
-        byte[] encryptedData =
-                AesCtrMultipleBlockEncryption.doAesCtr(
-                        secret,
-                        "Someone's Google Headphone".getBytes(UTF_8),
-                        nonce);
-
-        assertThat(encryptedData)
-                .isEqualTo(base16().decode("EE4A2483738052E44E9B2A145E5DDFAA44B9E5536AF438E1E5C6"));
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryptionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryptionTest.java
deleted file mode 100644
index eccbd01..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryptionTest.java
+++ /dev/null
@@ -1,66 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Unit tests for {@link AesEcbSingleBlockEncryption}. */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AesEcbSingleBlockEncryptionTest {
-
-    private static final byte[] PLAINTEXT = base16().decode("F30F4E786C59A7BBF3873B5A49BA97EA");
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void encryptDecryptSuccessful() throws Exception {
-        byte[] secret = AesEcbSingleBlockEncryption.generateKey();
-        byte[] encrypted = AesEcbSingleBlockEncryption.encrypt(secret, PLAINTEXT);
-        assertThat(encrypted).isNotEqualTo(PLAINTEXT);
-        byte[] decrypted = AesEcbSingleBlockEncryption.decrypt(secret, encrypted);
-        assertThat(decrypted).isEqualTo(PLAINTEXT);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void encryptionSizeLimitationEnforced() throws Exception {
-        byte[] secret = AesEcbSingleBlockEncryption.generateKey();
-        byte[] largePacket = Bytes.concat(PLAINTEXT, PLAINTEXT);
-        AesEcbSingleBlockEncryption.encrypt(secret, largePacket);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void decryptionSizeLimitationEnforced() throws Exception {
-        byte[] secret = AesEcbSingleBlockEncryption.generateKey();
-        byte[] largePacket = Bytes.concat(PLAINTEXT, PLAINTEXT);
-        AesEcbSingleBlockEncryption.decrypt(secret, largePacket);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddressTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddressTest.java
deleted file mode 100644
index 6c95558..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddressTest.java
+++ /dev/null
@@ -1,71 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.bluetooth.BluetoothAdapter;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Unit tests for {@link BluetoothAddress}. */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothAddressTest {
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void maskBluetoothAddress_whenInputIsNull() {
-        assertThat(BluetoothAddress.maskBluetoothAddress(null)).isEqualTo("");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void maskBluetoothAddress_whenInputStringNotMatchFormat() {
-        assertThat(BluetoothAddress.maskBluetoothAddress("AA:BB:CC")).isEqualTo("AA:BB:CC");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void maskBluetoothAddress_whenInputStringMatchFormat() {
-        assertThat(BluetoothAddress.maskBluetoothAddress("AA:BB:CC:DD:EE:FF"))
-                .isEqualTo("XX:XX:XX:XX:EE:FF");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void maskBluetoothAddress_whenInputStringContainLowerCaseMatchFormat() {
-        assertThat(BluetoothAddress.maskBluetoothAddress("Aa:Bb:cC:dD:eE:Ff"))
-                .isEqualTo("XX:XX:XX:XX:EE:FF");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void maskBluetoothAddress_whenInputBluetoothDevice() {
-        assertThat(
-                BluetoothAddress.maskBluetoothAddress(
-                        BluetoothAdapter.getDefaultAdapter().getRemoteDevice("FF:EE:DD:CC:BB:AA")))
-                .isEqualTo("XX:XX:XX:XX:BB:AA");
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java
deleted file mode 100644
index 6871024..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java
+++ /dev/null
@@ -1,215 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.content.Context;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.google.common.collect.Iterables;
-
-import junit.framework.TestCase;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-import java.util.stream.Collectors;
-
-/** Unit tests for {@link BluetoothAudioPairer}. */
-@Presubmit
-@SmallTest
-public class BluetoothAudioPairerTest extends TestCase {
-
-    private static final byte[] SECRET = new byte[]{3, 0};
-    private static final boolean PRIVATE_INITIAL_PAIRING = false;
-    private static final String EVENT_NAME = "EVENT_NAME";
-    private static final BluetoothDevice BLUETOOTH_DEVICE = BluetoothAdapter.getDefaultAdapter()
-            .getRemoteDevice("11:22:33:44:55:66");
-    private static final int BOND_TIMEOUT_SECONDS = 1;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        initMocks(this);
-        BluetoothAudioPairer.enableTestMode();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testKeyBasedPairingInfoConstructor() {
-        assertThat(new BluetoothAudioPairer.KeyBasedPairingInfo(
-                SECRET,
-                null /* GattConnectionManager */,
-                PRIVATE_INITIAL_PAIRING)).isNotNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothAudioPairerConstructor() {
-        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
-        try {
-            assertThat(new BluetoothAudioPairer(
-                    context,
-                    BLUETOOTH_DEVICE,
-                    Preferences.builder().build(),
-                    new EventLoggerWrapper(new TestEventLogger()),
-                    null /* KeyBasePairingInfo */,
-                    null /*PasskeyConfirmationHandler */,
-                    new TimingLogger(EVENT_NAME, Preferences.builder().build()))).isNotNull();
-        } catch (PairingException e) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void getDevice() {
-        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
-        try {
-            assertThat(new BluetoothAudioPairer(
-                    context,
-                    BLUETOOTH_DEVICE,
-                    Preferences.builder().build(),
-                    new EventLoggerWrapper(new TestEventLogger()),
-                    null /* KeyBasePairingInfo */,
-                    null /*PasskeyConfirmationHandler */,
-                    new TimingLogger(EVENT_NAME, Preferences.builder().build())).getDevice())
-                    .isEqualTo(BLUETOOTH_DEVICE);
-        } catch (PairingException e) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothAudioPairerUnpairNoCrash() {
-        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
-        try {
-            new BluetoothAudioPairer(
-                    context,
-                    BLUETOOTH_DEVICE,
-                    Preferences.builder().build(),
-                    new EventLoggerWrapper(new TestEventLogger()),
-                    null /* KeyBasePairingInfo */,
-                    null /*PasskeyConfirmationHandler */,
-                    new TimingLogger(EVENT_NAME, Preferences.builder().build())).unpair();
-        } catch (PairingException | InterruptedException | ExecutionException
-                | TimeoutException e) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothAudioPairerPairNoCrash() {
-        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
-        try {
-            new BluetoothAudioPairer(
-                    context,
-                    BLUETOOTH_DEVICE,
-                    Preferences.builder().setCreateBondTimeoutSeconds(BOND_TIMEOUT_SECONDS).build(),
-                    new EventLoggerWrapper(new TestEventLogger()),
-                    null /* KeyBasePairingInfo */,
-                    null /*PasskeyConfirmationHandler */,
-                    new TimingLogger(EVENT_NAME, Preferences.builder().build())).pair();
-        } catch (PairingException | InterruptedException | ExecutionException
-                | TimeoutException e) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothAudioPairerConnectNoCrash() {
-        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
-        try {
-            new BluetoothAudioPairer(
-                    context,
-                    BLUETOOTH_DEVICE,
-                    Preferences.builder().setCreateBondTimeoutSeconds(BOND_TIMEOUT_SECONDS).build(),
-                    new EventLoggerWrapper(new TestEventLogger()),
-                    null /* KeyBasePairingInfo */,
-                    null /*PasskeyConfirmationHandler */,
-                    new TimingLogger(EVENT_NAME, Preferences.builder().build()))
-                    .connect(Constants.A2DP_SINK_SERVICE_UUID, true /* enable pairing behavior */);
-        } catch (PairingException | InterruptedException | ExecutionException
-                | TimeoutException | ReflectionException e) {
-        }
-    }
-
-    static class TestEventLogger implements EventLogger {
-
-        private List<Item> mLogs = new ArrayList<>();
-
-        @Override
-        public void logEventSucceeded(Event event) {
-            mLogs.add(new Item(event));
-        }
-
-        @Override
-        public void logEventFailed(Event event, Exception e) {
-            mLogs.add(new ItemFailed(event, e));
-        }
-
-        List<Item> getErrorLogs() {
-            return mLogs.stream().filter(item -> item instanceof ItemFailed)
-                    .collect(Collectors.toList());
-        }
-
-        List<Item> getLogs() {
-            return mLogs;
-        }
-
-        List<Item> getLast() {
-            return mLogs.subList(mLogs.size() - 1, mLogs.size());
-        }
-
-        BluetoothDevice getDevice() {
-            return Iterables.getLast(mLogs).mEvent.getBluetoothDevice();
-        }
-
-        public static class Item {
-
-            final Event mEvent;
-
-            Item(Event event) {
-                this.mEvent = event;
-            }
-
-            @Override
-            public String toString() {
-                return "Item{" + "event=" + mEvent + '}';
-            }
-        }
-
-        public static class ItemFailed extends Item {
-
-            final Exception mException;
-
-            ItemFailed(Event event, Exception e) {
-                super(event);
-                this.mException = e;
-            }
-
-            @Override
-            public String toString() {
-                return "ItemFailed{" + "event=" + mEvent + ", exception=" + mException + '}';
-            }
-        }
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java
deleted file mode 100644
index 48f4b9b..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java
+++ /dev/null
@@ -1,60 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-
-public class BluetoothClassicPairerTest {
-    @Mock
-    PasskeyConfirmationHandler mPasskeyConfirmationHandler;
-    private static final BluetoothDevice BLUETOOTH_DEVICE = BluetoothAdapter.getDefaultAdapter()
-            .getRemoteDevice("11:22:33:44:55:66");
-    private  BluetoothClassicPairer mBluetoothClassicPairer;
-
-    @Before
-    public void setUp() {
-        mBluetoothClassicPairer = new BluetoothClassicPairer(
-                ApplicationProvider.getApplicationContext(),
-                BLUETOOTH_DEVICE,
-                Preferences.builder().build(),
-                mPasskeyConfirmationHandler);
-    }
-
-    @Test
-    public void pair() throws PairingException {
-        PairingException exception =
-                assertThrows(
-                        PairingException.class,
-                        () -> mBluetoothClassicPairer.pair());
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("BluetoothClassicPairer, createBond");
-        assertThat(mBluetoothClassicPairer.isPaired()).isFalse();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuidsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuidsTest.java
deleted file mode 100644
index fa977ed..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuidsTest.java
+++ /dev/null
@@ -1,76 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.UUID;
-
-/** Unit tests for {@link BluetoothUuids}. */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothUuidsTest {
-
-    // According to {@code android.bluetooth.BluetoothUuid}
-    private static final short A2DP_SINK_SHORT_UUID = (short) 0x110B;
-    private static final UUID A2DP_SINK_CHARACTERISTICS =
-            UUID.fromString("0000110B-0000-1000-8000-00805F9B34FB");
-
-    // According to {go/fastpair-128bit-gatt}, the short uuid locates at the 3rd and 4th bytes based
-    // on the Fast Pair custom GATT characteristics 128-bit UUIDs base -
-    // "FE2C0000-8366-4814-8EB0-01DE32100BEA".
-    private static final short CUSTOM_SHORT_UUID = (short) 0x9487;
-    private static final UUID CUSTOM_CHARACTERISTICS =
-            UUID.fromString("FE2C9487-8366-4814-8EB0-01DE32100BEA");
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void get16BitUuid() {
-        assertThat(BluetoothUuids.get16BitUuid(A2DP_SINK_CHARACTERISTICS))
-                .isEqualTo(A2DP_SINK_SHORT_UUID);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void is16BitUuid() {
-        assertThat(BluetoothUuids.is16BitUuid(A2DP_SINK_CHARACTERISTICS)).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void to128BitUuid() {
-        assertThat(BluetoothUuids.to128BitUuid(A2DP_SINK_SHORT_UUID))
-                .isEqualTo(A2DP_SINK_CHARACTERISTICS);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void toFastPair128BitUuid() {
-        assertThat(BluetoothUuids.toFastPair128BitUuid(CUSTOM_SHORT_UUID))
-                .isEqualTo(CUSTOM_CHARACTERISTICS);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BytesTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BytesTest.java
deleted file mode 100644
index 9450d60..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BytesTest.java
+++ /dev/null
@@ -1,64 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.filters.SdkSuppress;
-
-import junit.framework.TestCase;
-
-import java.nio.ByteOrder;
-import java.util.Arrays;
-
-/**
- * Unit tests for {@link Bytes}.
- */
-public class BytesTest extends TestCase {
-
-    private static final Bytes.Value VALUE1 =
-            new Bytes.Value(new byte[]{1, 2}, ByteOrder.BIG_ENDIAN);
-    private static final Bytes.Value VALUE2 =
-            new Bytes.Value(new byte[]{1, 2}, ByteOrder.BIG_ENDIAN);
-    private static final Bytes.Value VALUE3 =
-            new Bytes.Value(new byte[]{1, 3}, ByteOrder.BIG_ENDIAN);
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEquals_asExpected()  {
-        assertThat(VALUE1.equals(VALUE2)).isTrue();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNotEquals_asExpected()  {
-        assertThat(VALUE1.equals(VALUE3)).isFalse();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetBytes_asExpected()  {
-        assertThat(Arrays.equals(VALUE1.getBytes(ByteOrder.BIG_ENDIAN), new byte[]{1, 2})).isTrue();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testToString()  {
-        assertThat(VALUE1.toString()).isEqualTo("0102");
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testReverse()  {
-        assertThat(VALUE1.reverse(new byte[]{1, 2})).isEqualTo(new byte[]{2, 1});
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java
deleted file mode 100644
index 6684fbc..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java
+++ /dev/null
@@ -1,143 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid;
-import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.toFastPair128BitUuid;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.when;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import android.bluetooth.BluetoothGattCharacteristic;
-
-import androidx.test.filters.SdkSuppress;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
-
-import com.google.common.collect.ImmutableList;
-
-import junit.framework.TestCase;
-
-import org.mockito.Mock;
-
-import java.util.UUID;
-
-/**
- * Unit tests for {@link Constants}.
- */
-public class ConstantsTest extends TestCase {
-
-    private static final int PASSKEY = 32689;
-
-    @Mock
-    private BluetoothGattConnection mMockGattConnection;
-
-    private static final UUID OLD_KEY_BASE_PAIRING_CHARACTERISTICS = to128BitUuid((short) 0x1234);
-
-    private static final UUID NEW_KEY_BASE_PAIRING_CHARACTERISTICS =
-            toFastPair128BitUuid((short) 0x1234);
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        initMocks(this);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getId_whenSupportNewCharacteristics() throws BluetoothException {
-        when(mMockGattConnection.getCharacteristic(any(UUID.class), any(UUID.class)))
-                .thenReturn(new BluetoothGattCharacteristic(NEW_KEY_BASE_PAIRING_CHARACTERISTICS, 0,
-                        0));
-
-        assertThat(KeyBasedPairingCharacteristic.getId(mMockGattConnection))
-                .isEqualTo(NEW_KEY_BASE_PAIRING_CHARACTERISTICS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getId_whenNotSupportNewCharacteristics() throws BluetoothException {
-        // {@link BluetoothGattConnection#getCharacteristic(UUID, UUID)} throws {@link
-        // BluetoothException} if the characteristic not found .
-        when(mMockGattConnection.getCharacteristic(any(UUID.class), any(UUID.class)))
-                .thenThrow(new BluetoothException(""));
-
-        assertThat(KeyBasedPairingCharacteristic.getId(mMockGattConnection))
-                .isEqualTo(OLD_KEY_BASE_PAIRING_CHARACTERISTICS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_accountKeyCharacteristic_notCrash() throws BluetoothException {
-        Constants.FastPairService.AccountKeyCharacteristic.getId(mMockGattConnection);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_additionalDataCharacteristic_notCrash() throws BluetoothException {
-        Constants.FastPairService.AdditionalDataCharacteristic.getId(mMockGattConnection);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_nameCharacteristic_notCrash() throws BluetoothException {
-        Constants.FastPairService.NameCharacteristic.getId(mMockGattConnection);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_passKeyCharacteristic_encryptDecryptSuccessfully()
-            throws java.security.GeneralSecurityException {
-        byte[] secret = AesEcbSingleBlockEncryption.generateKey();
-
-        Constants.FastPairService.PasskeyCharacteristic.getId(mMockGattConnection);
-        assertThat(
-                Constants.FastPairService.PasskeyCharacteristic.decrypt(
-                        Constants.FastPairService.PasskeyCharacteristic.Type.SEEKER,
-                        secret,
-                        Constants.FastPairService.PasskeyCharacteristic.encrypt(
-                                Constants.FastPairService.PasskeyCharacteristic.Type.SEEKER,
-                                secret,
-                                PASSKEY))
-        ).isEqualTo(PASSKEY);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_beaconActionsCharacteristic_notCrash() throws BluetoothException {
-        Constants.FastPairService.BeaconActionsCharacteristic.getId(mMockGattConnection);
-        for (byte b : ImmutableList.of(
-                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
-                        .READ_BEACON_PARAMETERS,
-                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
-                        .READ_PROVISIONING_STATE,
-                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
-                        .SET_EPHEMERAL_IDENTITY_KEY,
-                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
-                        .CLEAR_EPHEMERAL_IDENTITY_KEY,
-                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
-                        .READ_EPHEMERAL_IDENTITY_KEY,
-                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
-                        .RING,
-                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
-                        .READ_RINGING_STATE,
-                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
-                        .UNKNOWN
-        )) {
-            assertThat(Constants.FastPairService.BeaconActionsCharacteristic
-                    .valueOf(b)).isEqualTo(b);
-        }
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/CreateBondExceptionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/CreateBondExceptionTest.java
deleted file mode 100644
index 052e696..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/CreateBondExceptionTest.java
+++ /dev/null
@@ -1,44 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.filters.SdkSuppress;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.intdefs.FastPairEventIntDefs;
-
-import junit.framework.TestCase;
-
-/**
- * Unit tests for {@link CreateBondException}.
- */
-public class CreateBondExceptionTest extends TestCase {
-
-    private static final String FORMAT = "FORMAT";
-    private static final int REASON = 0;
-    private static final CreateBondException EXCEPTION = new CreateBondException(
-            FastPairEventIntDefs.CreateBondErrorCode.INCORRECT_VARIANT, REASON, FORMAT);
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getter_asExpected() throws BluetoothException {
-        assertThat(EXCEPTION.getErrorCode()).isEqualTo(
-                FastPairEventIntDefs.CreateBondErrorCode.INCORRECT_VARIANT);
-        assertThat(EXCEPTION.getReason()).isSameInstanceAs(REASON);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiverTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiverTest.java
deleted file mode 100644
index 94bf111..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiverTest.java
+++ /dev/null
@@ -1,58 +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.server.nearby.common.bluetooth.fastpair;
-
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import android.bluetooth.BluetoothDevice;
-import android.content.Context;
-import android.content.Intent;
-
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import junit.framework.TestCase;
-
-import org.mockito.Mock;
-
-/**
- * Unit tests for {@link DeviceIntentReceiver}.
- */
-public class DeviceIntentReceiverTest extends TestCase {
-    @Mock Preferences mPreferences;
-    @Mock BluetoothDevice mBluetoothDevice;
-
-    private DeviceIntentReceiver mDeviceIntentReceiver;
-    private Intent mIntent;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        initMocks(this);
-
-        Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        mDeviceIntentReceiver = DeviceIntentReceiver.oneShotReceiver(
-                context, mPreferences, mBluetoothDevice);
-
-        mIntent = new Intent().putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_onReceive_notCrash() throws Exception {
-        mDeviceIntentReceiver.onReceive(mIntent);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchangeTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchangeTest.java
deleted file mode 100644
index 3719783..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchangeTest.java
+++ /dev/null
@@ -1,89 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.io.BaseEncoding.base64;
-import static com.google.common.primitives.Bytes.concat;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link EllipticCurveDiffieHellmanExchange}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class EllipticCurveDiffieHellmanExchangeTest {
-
-    public static final byte[] ANTI_SPOOF_PUBLIC_KEY = base64().decode(
-            "d2JTfvfdS6u7LmGfMOmco3C7ra3lW1k17AOly0LrBydDZURacfTYIMmo5K1ejfD9e8b6qHs"
-                    + "DTNzselhifi10kQ==");
-    public static final byte[] ANTI_SPOOF_PRIVATE_KEY =
-            base64().decode("Rn9GbLRPQTFc2O7WFVGkydzcUS9Tuj7R9rLh6EpLtuU=");
-
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void generateCommonKey() throws Exception {
-        EllipticCurveDiffieHellmanExchange bob = EllipticCurveDiffieHellmanExchange.create();
-        EllipticCurveDiffieHellmanExchange alice = EllipticCurveDiffieHellmanExchange.create();
-
-        assertThat(bob.getPublicKey()).isNotEqualTo(alice.getPublicKey());
-        assertThat(bob.getPrivateKey()).isNotEqualTo(alice.getPrivateKey());
-
-        assertThat(bob.generateSecret(alice.getPublicKey()))
-                .isEqualTo(alice.generateSecret(bob.getPublicKey()));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void generateCommonKey_withExistingPrivateKey() throws Exception {
-        EllipticCurveDiffieHellmanExchange bob = EllipticCurveDiffieHellmanExchange.create();
-        EllipticCurveDiffieHellmanExchange alice =
-                EllipticCurveDiffieHellmanExchange.create(ANTI_SPOOF_PRIVATE_KEY);
-
-        assertThat(alice.generateSecret(bob.getPublicKey()))
-                .isEqualTo(bob.generateSecret(ANTI_SPOOF_PUBLIC_KEY));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void generateCommonKey_soundcoreAntiSpoofingKey_generatedTooShort() throws Exception {
-        // This soundcore device has a public key that was generated which starts with 0x0. This was
-        // stripped out in our database, but this test confirms that adding that byte back fixes the
-        // issue and allows the generated secrets to match each other.
-        byte[] soundCorePublicKey = concat(new byte[]{0}, base64().decode(
-                "EYapuIsyw/nwHAdMxr12FCtAi4gY3EtuW06JuKDg4SA76IoIDVeol2vsGKy0Ea2Z00"
-                        + "ArOTiBDsk0L+4Xo9AA"));
-        byte[] soundCorePrivateKey = base64()
-                .decode("lW5idsrfX7cBC8kO/kKn3w3GXirqt9KnJoqXUcOMhjM=");
-        EllipticCurveDiffieHellmanExchange bob = EllipticCurveDiffieHellmanExchange.create();
-        EllipticCurveDiffieHellmanExchange alice =
-                EllipticCurveDiffieHellmanExchange.create(soundCorePrivateKey);
-
-        assertThat(alice.generateSecret(bob.getPublicKey()))
-                .isEqualTo(bob.generateSecret(soundCorePublicKey));
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
deleted file mode 100644
index f10f66d..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
+++ /dev/null
@@ -1,84 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.os.Parcel;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link Event}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class EventTest {
-
-    private static final String EXCEPTION_MESSAGE = "Test exception";
-    private static final long TIMESTAMP = 1234L;
-    private static final @EventCode int EVENT_CODE = EventCode.CREATE_BOND;
-    private static final BluetoothDevice BLUETOOTH_DEVICE = BluetoothAdapter.getDefaultAdapter()
-            .getRemoteDevice("11:22:33:44:55:66");
-    private static final Short PROFILE = (short) 1;
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void createAndReadFromParcel() {
-        Event event =
-                Event.builder()
-                        .setException(new Exception(EXCEPTION_MESSAGE))
-                        .setTimestamp(TIMESTAMP)
-                        .setEventCode(EVENT_CODE)
-                        .setBluetoothDevice(BLUETOOTH_DEVICE)
-                        .setProfile(PROFILE)
-                        .build();
-        assertThat(event.hasBluetoothDevice()).isTrue();
-        assertThat(event.hasProfile()).isTrue();
-        assertThat(event.isFailure()).isTrue();
-        assertThat(event.toString()).isEqualTo(
-                "Event{eventCode=1120, timestamp=1234, profile=1, "
-                        + "bluetoothDevice=" + BLUETOOTH_DEVICE + ", "
-                        + "exception=java.lang.Exception: Test exception}");
-
-        Parcel parcel = Parcel.obtain();
-        event.writeToParcel(parcel, event.describeContents());
-        parcel.setDataPosition(0);
-        Event result = Event.CREATOR.createFromParcel(parcel);
-
-        assertThat(result.getException()).hasMessageThat()
-                .isEqualTo(event.getException().getMessage());
-        assertThat(result.getTimestamp()).isEqualTo(event.getTimestamp());
-        assertThat(result.getEventCode()).isEqualTo(event.getEventCode());
-        assertThat(result.getBluetoothDevice()).isEqualTo(event.getBluetoothDevice());
-        assertThat(result.getProfile()).isEqualTo(event.getProfile());
-        assertThat(result.equals(event)).isTrue();
-
-        assertThat(Event.CREATOR.newArray(10)).isNotEmpty();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java
deleted file mode 100644
index 63c39b2..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java
+++ /dev/null
@@ -1,397 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED;
-import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST;
-import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.GATT_ERROR_CODE_TIMEOUT;
-import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.appendMoreErrorCode;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyShort;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.protobuf.ByteString;
-
-import junit.framework.TestCase;
-
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-import java.util.stream.Collectors;
-
-/**
- * Unit tests for {@link FastPairDualConnection}.
- */
-@Presubmit
-@SmallTest
-public class FastPairDualConnectionTest extends TestCase {
-
-    private static final String BLE_ADDRESS = "00:11:22:33:FF:EE";
-    private static final String MASKED_BLE_ADDRESS = "MASKED_BLE_ADDRESS";
-    private static final short[] PROFILES = {Constants.A2DP_SINK_SERVICE_UUID};
-    private static final int NUM_CONNECTION_ATTEMPTS = 1;
-    private static final boolean ENABLE_PAIRING_BEHAVIOR = true;
-    private static final BluetoothDevice BLUETOOTH_DEVICE = BluetoothAdapter.getDefaultAdapter()
-            .getRemoteDevice("11:22:33:44:55:66");
-    private static final String DEVICE_NAME = "DEVICE_NAME";
-    private static final byte[] ACCOUNT_KEY = new byte[]{1, 3};
-    private static final byte[] HASH_VALUE = new byte[]{7};
-
-    private TestEventLogger mEventLogger;
-    @Mock private TimingLogger mTimingLogger;
-    @Mock private BluetoothAudioPairer mBluetoothAudioPairer;
-    @Mock private android.bluetooth.BluetoothAdapter mBluetoothAdapter;
-    @Mock FastPairDualConnection mFastPairDualConnection;
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-
-        BluetoothAudioPairer.enableTestMode();
-        FastPairDualConnection.enableTestMode();
-        MockitoAnnotations.initMocks(this);
-
-        doNothing().when(mBluetoothAudioPairer).connect(anyShort(), anyBoolean());
-        mEventLogger = new TestEventLogger();
-    }
-
-    private FastPairDualConnection newFastPairDualConnection(
-            String bleAddress, Preferences.Builder prefsBuilder) {
-        return new FastPairDualConnection(
-                ApplicationProvider.getApplicationContext(),
-                bleAddress,
-                prefsBuilder.build(),
-                mEventLogger,
-                mTimingLogger);
-    }
-
-    private FastPairDualConnection newFastPairDualConnection2(
-            String bleAddress, Preferences.Builder prefsBuilder) {
-        return new FastPairDualConnection(
-                ApplicationProvider.getApplicationContext(),
-                bleAddress,
-                prefsBuilder.build(),
-                mEventLogger);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testFastPairDualConnectionConstructor() {
-        assertThat(newFastPairDualConnection(BLE_ADDRESS, Preferences.builder())).isNotNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testFastPairDualConnectionConstructor2() {
-        assertThat(newFastPairDualConnection2(BLE_ADDRESS, Preferences.builder())).isNotNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testAttemptConnectProfiles() {
-        try {
-            new FastPairDualConnection(
-                    ApplicationProvider.getApplicationContext(),
-                    BLE_ADDRESS,
-                    Preferences.builder().build(),
-                    mEventLogger,
-                    mTimingLogger)
-                    .attemptConnectProfiles(
-                            mBluetoothAudioPairer,
-                            MASKED_BLE_ADDRESS,
-                            PROFILES,
-                            NUM_CONNECTION_ATTEMPTS,
-                            ENABLE_PAIRING_BEHAVIOR);
-        } catch (PairingException e) {
-            // Mocked pair doesn't throw Pairing Exception.
-        }
-    }
-
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testAppendMoreErrorCode_gattError() {
-        assertThat(
-                appendMoreErrorCode(
-                        GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED,
-                        new BluetoothGattException("Test", 133)))
-                .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED + 133);
-        assertThat(
-                appendMoreErrorCode(
-                        GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED,
-                        new BluetoothGattException("Test", 257)))
-                .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED + 257);
-        assertThat(
-                appendMoreErrorCode(
-                        GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED, new BluetoothException("Test")))
-                .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED);
-        assertThat(
-                appendMoreErrorCode(
-                        GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED,
-                        new BluetoothOperationTimeoutException("Test")))
-                .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED + GATT_ERROR_CODE_TIMEOUT);
-        assertThat(
-                appendMoreErrorCode(
-                        GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST,
-                        new BluetoothGattException("Test", 41)))
-                .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST + 41);
-        assertThat(
-                appendMoreErrorCode(
-                        GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST,
-                        new BluetoothGattException("Test", 788)))
-                .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST + 788);
-        assertThat(
-                appendMoreErrorCode(
-                        GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST, new BluetoothException("Test")))
-                .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST);
-        assertThat(
-                appendMoreErrorCode(
-                        GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST,
-                        new BluetoothOperationTimeoutException("Test")))
-                .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST + GATT_ERROR_CODE_TIMEOUT);
-        assertThat(appendMoreErrorCode(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST, /* cause= */ null))
-                .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testUnpairNotCrash() {
-        try {
-            new FastPairDualConnection(
-                    ApplicationProvider.getApplicationContext(),
-                    BLE_ADDRESS,
-                    Preferences.builder().build(),
-                    mEventLogger,
-                    mTimingLogger).unpair(BLUETOOTH_DEVICE);
-        } catch (ExecutionException | InterruptedException | ReflectionException
-                | TimeoutException | PairingException e) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetFastPairHistory() {
-        new FastPairDualConnection(
-                ApplicationProvider.getApplicationContext(),
-                BLE_ADDRESS,
-                Preferences.builder().build(),
-                mEventLogger,
-                mTimingLogger).setFastPairHistory(ImmutableList.of());
-    }
-
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetGetProviderDeviceName() {
-        FastPairDualConnection connection = new FastPairDualConnection(
-                ApplicationProvider.getApplicationContext(),
-                BLE_ADDRESS,
-                Preferences.builder().build(),
-                mEventLogger,
-                mTimingLogger);
-        connection.setProviderDeviceName(DEVICE_NAME);
-        connection.getProviderDeviceName();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetExistingAccountKey() {
-        FastPairDualConnection connection = new FastPairDualConnection(
-                ApplicationProvider.getApplicationContext(),
-                BLE_ADDRESS,
-                Preferences.builder().build(),
-                mEventLogger,
-                mTimingLogger);
-        connection.getExistingAccountKey();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testPair() {
-        FastPairDualConnection connection = new FastPairDualConnection(
-                ApplicationProvider.getApplicationContext(),
-                BLE_ADDRESS,
-                Preferences.builder().setNumSdpAttempts(0)
-                        .setLogPairWithCachedModelId(false).build(),
-                mEventLogger,
-                mTimingLogger);
-        try {
-            connection.pair();
-        } catch (BluetoothException | InterruptedException | ReflectionException
-                | ExecutionException | TimeoutException | PairingException e) {
-        }
-    }
-
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetPublicAddress() {
-        FastPairDualConnection connection = new FastPairDualConnection(
-                ApplicationProvider.getApplicationContext(),
-                BLE_ADDRESS,
-                Preferences.builder().setNumSdpAttempts(0)
-                        .setLogPairWithCachedModelId(false).build(),
-                mEventLogger,
-                mTimingLogger);
-        connection.getPublicAddress();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testShouldWriteAccountKeyForExistingCase() {
-        FastPairDualConnection connection = new FastPairDualConnection(
-                ApplicationProvider.getApplicationContext(),
-                BLE_ADDRESS,
-                Preferences.builder().setNumSdpAttempts(0)
-                        .setLogPairWithCachedModelId(false).build(),
-                mEventLogger,
-                mTimingLogger);
-        connection.shouldWriteAccountKeyForExistingCase(ACCOUNT_KEY);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testReadFirmwareVersion() {
-        FastPairDualConnection connection = new FastPairDualConnection(
-                ApplicationProvider.getApplicationContext(),
-                BLE_ADDRESS,
-                Preferences.builder().setNumSdpAttempts(0)
-                        .setLogPairWithCachedModelId(false).build(),
-                mEventLogger,
-                mTimingLogger);
-        try {
-            connection.readFirmwareVersion();
-        } catch (BluetoothException | InterruptedException | ExecutionException
-                | TimeoutException e) {
-        }
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testReflectionException()
-            throws BluetoothException, ReflectionException, ExecutionException,
-            InterruptedException, PairingException, TimeoutException {
-        when(mFastPairDualConnection.pair())
-                .thenThrow(new ReflectionException(
-                        new NoSuchMethodException("testReflectionException")));
-        ReflectionException exception =
-                assertThrows(
-                        ReflectionException.class,
-                        () -> mFastPairDualConnection.pair());
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("testReflectionException");
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHistoryItem() {
-        FastPairDualConnection connection = new FastPairDualConnection(
-                ApplicationProvider.getApplicationContext(),
-                BLE_ADDRESS,
-                Preferences.builder().setNumSdpAttempts(0)
-                        .setLogPairWithCachedModelId(false).build(),
-                mEventLogger,
-                mTimingLogger);
-        ImmutableList.Builder<FastPairHistoryItem> historyBuilder = ImmutableList.builder();
-        FastPairHistoryItem historyItem1 =
-                FastPairHistoryItem.create(
-                        ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(HASH_VALUE));
-        historyBuilder.add(historyItem1);
-
-        connection.setFastPairHistory(historyBuilder.build());
-        assertThat(connection.mPairedHistoryFinder.isInPairedHistory("11:22:33:44:55:88"))
-                .isFalse();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetLeState() throws ReflectionException {
-        FastPairDualConnection.getLeState(mBluetoothAdapter);
-    }
-
-    static class TestEventLogger implements EventLogger {
-
-        private List<Item> mLogs = new ArrayList<>();
-
-        @Override
-        public void logEventSucceeded(Event event) {
-            mLogs.add(new Item(event));
-        }
-
-        @Override
-        public void logEventFailed(Event event, Exception e) {
-            mLogs.add(new ItemFailed(event, e));
-        }
-
-        List<Item> getErrorLogs() {
-            return mLogs.stream().filter(item -> item instanceof ItemFailed)
-                    .collect(Collectors.toList());
-        }
-
-        List<Item> getLogs() {
-            return mLogs;
-        }
-
-        List<Item> getLast() {
-            return mLogs.subList(mLogs.size() - 1, mLogs.size());
-        }
-
-        BluetoothDevice getDevice() {
-            return Iterables.getLast(mLogs).mEvent.getBluetoothDevice();
-        }
-
-        public static class Item {
-
-            final Event mEvent;
-
-            Item(Event event) {
-                this.mEvent = event;
-            }
-
-            @Override
-            public String toString() {
-                return "Item{" + "event=" + mEvent + '}';
-            }
-        }
-
-        public static class ItemFailed extends Item {
-
-            final Exception mException;
-
-            ItemFailed(Event event, Exception e) {
-                super(event);
-                this.mException = e;
-            }
-
-            @Override
-            public String toString() {
-                return "ItemFailed{" + "event=" + mEvent + ", exception=" + mException + '}';
-            }
-        }
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItemTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItemTest.java
deleted file mode 100644
index b47fd89..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItemTest.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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.primitives.Bytes.concat;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.google.common.hash.Hashing;
-import com.google.protobuf.ByteString;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link FastPairHistoryItem}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class FastPairHistoryItemTest {
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputMatchedPublicAddress_isMatchedReturnTrue() {
-        final byte[] accountKey = base16().decode("0123456789ABCDEF");
-        final byte[] publicAddress = BluetoothAddress.decode("11:22:33:44:55:66");
-        final byte[] hashValue =
-                Hashing.sha256().hashBytes(concat(accountKey, publicAddress)).asBytes();
-
-        FastPairHistoryItem historyItem =
-                FastPairHistoryItem
-                        .create(ByteString.copyFrom(accountKey), ByteString.copyFrom(hashValue));
-
-        assertThat(historyItem.isMatched(publicAddress)).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputNotMatchedPublicAddress_isMatchedReturnFalse() {
-        final byte[] accountKey = base16().decode("0123456789ABCDEF");
-        final byte[] publicAddress1 = BluetoothAddress.decode("11:22:33:44:55:66");
-        final byte[] publicAddress2 = BluetoothAddress.decode("11:22:33:44:55:77");
-        final byte[] hashValue =
-                Hashing.sha256().hashBytes(concat(accountKey, publicAddress1)).asBytes();
-
-        FastPairHistoryItem historyItem =
-                FastPairHistoryItem
-                        .create(ByteString.copyFrom(accountKey), ByteString.copyFrom(hashValue));
-
-        assertThat(historyItem.isMatched(publicAddress2)).isFalse();
-    }
-}
-
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManagerTest.java
deleted file mode 100644
index 2f80a30..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManagerTest.java
+++ /dev/null
@@ -1,154 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.annotation.Nullable;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
-
-import com.google.common.collect.ImmutableSet;
-
-import junit.framework.TestCase;
-
-import java.time.Duration;
-import java.util.concurrent.ExecutionException;
-
-/**
- * Unit tests for {@link GattConnectionManager}.
- */
-@Presubmit
-@SmallTest
-public class GattConnectionManagerTest extends TestCase {
-
-    private static final String FAST_PAIR_ADDRESS = "BB:BB:BB:BB:BB:1E";
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-
-        GattConnectionManager.enableTestMode();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGattConnectionManagerConstructor() throws Exception {
-        GattConnectionManager manager = createManager(Preferences.builder());
-        try {
-            manager.getConnection();
-        } catch (ExecutionException e) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testIsNoRetryError() {
-        Preferences preferences =
-                Preferences.builder()
-                        .setGattConnectionAndSecretHandshakeNoRetryGattError(
-                                ImmutableSet.of(257, 999))
-                        .build();
-
-        assertThat(
-                GattConnectionManager.isNoRetryError(
-                        preferences, new BluetoothGattException("Test", 133)))
-                .isFalse();
-        assertThat(
-                GattConnectionManager.isNoRetryError(
-                        preferences, new BluetoothGattException("Test", 257)))
-                .isTrue();
-        assertThat(
-                GattConnectionManager.isNoRetryError(
-                        preferences, new BluetoothGattException("Test", 999)))
-                .isTrue();
-        assertThat(GattConnectionManager.isNoRetryError(
-                preferences, new BluetoothException("Test")))
-                .isFalse();
-        assertThat(
-                GattConnectionManager.isNoRetryError(
-                        preferences, new BluetoothOperationTimeoutException("Test")))
-                .isFalse();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetTimeoutNotOverShortRetryMaxSpentTimeGetShort() {
-        Preferences preferences = Preferences.builder().build();
-
-        assertThat(
-                createManager(Preferences.builder(), () -> {})
-                        .getTimeoutMs(
-                                preferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs() - 1))
-                .isEqualTo(preferences.getGattConnectShortTimeoutMs());
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetTimeoutOverShortRetryMaxSpentTimeGetLong() {
-        Preferences preferences = Preferences.builder().build();
-
-        assertThat(
-                createManager(Preferences.builder(), () -> {})
-                        .getTimeoutMs(
-                                preferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs() + 1))
-                .isEqualTo(preferences.getGattConnectLongTimeoutMs());
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetTimeoutRetryNotEnabledGetOrigin() {
-        Preferences preferences = Preferences.builder().build();
-
-        assertThat(
-                createManager(
-                        Preferences.builder().setRetryGattConnectionAndSecretHandshake(false),
-                        () -> {})
-                        .getTimeoutMs(0))
-                .isEqualTo(Duration.ofSeconds(
-                        preferences.getGattConnectionTimeoutSeconds()).toMillis());
-    }
-
-    private GattConnectionManager createManager(Preferences.Builder prefs) {
-        return createManager(prefs, () -> {});
-    }
-
-    private GattConnectionManager createManager(
-            Preferences.Builder prefs, ToggleBluetoothTask toggleBluetooth) {
-        return createManager(prefs, toggleBluetooth,
-                /* fastPairSignalChecker= */ null);
-    }
-
-    private GattConnectionManager createManager(
-            Preferences.Builder prefs,
-            ToggleBluetoothTask toggleBluetooth,
-            @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker) {
-        return new GattConnectionManager(
-                ApplicationProvider.getApplicationContext(),
-                prefs.build(),
-                new EventLoggerWrapper(null),
-                BluetoothAdapter.getDefaultAdapter(),
-                toggleBluetooth,
-                FAST_PAIR_ADDRESS,
-                new TimingLogger("GattConnectionManager", prefs.build()),
-                fastPairSignalChecker,
-                /* setMtu= */ false);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandlerTest.java
deleted file mode 100644
index e53e60f..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandlerTest.java
+++ /dev/null
@@ -1,524 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.BLUETOOTH_ADDRESS_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.DEVICE_ACTION;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DISCOVERABLE;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR;
-import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.ADDITIONAL_DATA_TYPE_INDEX;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.when;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.annotation.Nullable;
-import androidx.core.util.Consumer;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
-import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
-import com.android.server.nearby.intdefs.NearbyEventIntDefs;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.common.io.BaseEncoding;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.security.GeneralSecurityException;
-import java.time.Duration;
-import java.util.Arrays;
-
-/**
- * Unit tests for {@link HandshakeHandler}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class HandshakeHandlerTest {
-
-    public static final byte[] PUBLIC_KEY =
-            BaseEncoding.base64().decode(
-                    "d2JTfvfdS6u7LmGfMOmco3C7ra3lW1k17AOly0LrBydDZURacfTY"
-                            + "IMmo5K1ejfD9e8b6qHsDTNzselhifi10kQ==");
-    private static final String SEEKER_ADDRESS = "A1:A2:A3:A4:A5:A6";
-    private static final String PROVIDER_BLE_ADDRESS = "11:22:33:44:55:66";
-    /**
-     * The random-resolvable private address (RPA) is sometimes used when advertising over BLE, to
-     * hide the static public address (otherwise, the Fast Pair device would b
-     * identifiable/trackable whenever it's BLE advertising).
-     */
-    private static final String RANDOM_PRIVATE_ADDRESS = "BB:BB:BB:BB:BB:1E";
-    private static final byte[] SHARED_SECRET =
-            BaseEncoding.base16().decode("0123456789ABCDEF0123456789ABCDEF");
-
-    @Mock EventLoggerWrapper mEventLoggerWrapper;
-    @Mock BluetoothGattConnection mBluetoothGattConnection;
-    @Mock BluetoothGattConnection.ChangeObserver mChangeObserver;
-    @Mock private Consumer<Integer> mRescueFromError;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void handshakeGattError_noRetryError_failed() throws BluetoothException {
-        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
-                new HandshakeHandler.KeyBasedPairingRequest.Builder()
-                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
-                        .build();
-        BluetoothGattException exception =
-                new BluetoothGattException("Exception for no retry", 257);
-        when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
-        GattConnectionManager gattConnectionManager =
-                createGattConnectionManager(Preferences.builder(), () -> {});
-        gattConnectionManager.setGattConnection(mBluetoothGattConnection);
-        when(mBluetoothGattConnection.enableNotification(any(), any()))
-                .thenReturn(mChangeObserver);
-        InOrder inOrder = inOrder(mEventLoggerWrapper);
-
-        assertThrows(
-                BluetoothGattException.class,
-                () ->
-                        getHandshakeHandler(gattConnectionManager, address -> address)
-                                .doHandshakeWithRetryAndSignalLostCheck(
-                                        PUBLIC_KEY,
-                                        keyBasedPairingRequest,
-                                        mRescueFromError));
-
-        inOrder.verify(mEventLoggerWrapper).setCurrentEvent(
-                NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
-        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
-        inOrder.verifyNoMoreInteractions();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void handshakeGattError_retryAndNoCount_throwException() throws BluetoothException {
-        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
-                new HandshakeHandler.KeyBasedPairingRequest.Builder()
-                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
-                        .build();
-        BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
-        when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
-        GattConnectionManager gattConnectionManager =
-                createGattConnectionManager(Preferences.builder(), () -> {});
-        gattConnectionManager.setGattConnection(mBluetoothGattConnection);
-        when(mBluetoothGattConnection.enableNotification(any(), any()))
-                .thenReturn(mChangeObserver);
-        InOrder inOrder = inOrder(mEventLoggerWrapper);
-
-        HandshakeHandler.HandshakeException handshakeException =
-                assertThrows(
-                        HandshakeHandler.HandshakeException.class,
-                        () -> getHandshakeHandler(gattConnectionManager, address -> address)
-                                .doHandshakeWithRetryAndSignalLostCheck(
-                                        PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
-
-        inOrder.verify(mEventLoggerWrapper)
-                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
-        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
-        inOrder.verify(mEventLoggerWrapper)
-                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
-        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
-        inOrder.verify(mEventLoggerWrapper)
-                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
-        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
-        inOrder.verify(mEventLoggerWrapper)
-                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
-        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
-        inOrder.verifyNoMoreInteractions();
-        assertThat(handshakeException.getOriginalException()).isEqualTo(exception);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void handshakeGattError_noRetryOnTimeout_throwException() throws BluetoothException {
-        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
-                new HandshakeHandler.KeyBasedPairingRequest.Builder()
-                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
-                        .build();
-        BluetoothOperationExecutor.BluetoothOperationTimeoutException exception =
-                new BluetoothOperationExecutor.BluetoothOperationTimeoutException("Test timeout");
-        when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
-        GattConnectionManager gattConnectionManager =
-                createGattConnectionManager(Preferences.builder(), () -> {});
-        gattConnectionManager.setGattConnection(mBluetoothGattConnection);
-        when(mBluetoothGattConnection.enableNotification(any(), any()))
-                .thenReturn(mChangeObserver);
-        InOrder inOrder = inOrder(mEventLoggerWrapper);
-
-        assertThrows(
-                HandshakeHandler.HandshakeException.class,
-                () ->
-                        new HandshakeHandler(
-                                gattConnectionManager,
-                                PROVIDER_BLE_ADDRESS,
-                                Preferences.builder().setRetrySecretHandshakeTimeout(false).build(),
-                                mEventLoggerWrapper,
-                                address -> address)
-                                .doHandshakeWithRetryAndSignalLostCheck(
-                                        PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
-
-        inOrder.verify(mEventLoggerWrapper)
-                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
-        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
-        inOrder.verifyNoMoreInteractions();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void handshakeGattError_signalLost() throws BluetoothException {
-        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
-                new HandshakeHandler.KeyBasedPairingRequest.Builder()
-                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
-                        .build();
-        BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
-        when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
-        GattConnectionManager gattConnectionManager =
-                createGattConnectionManager(Preferences.builder(), () -> {});
-        gattConnectionManager.setGattConnection(mBluetoothGattConnection);
-        when(mBluetoothGattConnection.enableNotification(any(), any()))
-                .thenReturn(mChangeObserver);
-        InOrder inOrder = inOrder(mEventLoggerWrapper);
-
-        SignalLostException signalLostException =
-                assertThrows(
-                        SignalLostException.class,
-                        () -> getHandshakeHandler(gattConnectionManager, address -> null)
-                                .doHandshakeWithRetryAndSignalLostCheck(
-                                        PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
-
-        inOrder.verify(mEventLoggerWrapper)
-                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
-        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
-        assertThat(signalLostException).hasCauseThat().isEqualTo(exception);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void handshakeGattError_addressRotate() throws BluetoothException {
-        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
-                new HandshakeHandler.KeyBasedPairingRequest.Builder()
-                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
-                        .build();
-        BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
-        when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
-        GattConnectionManager gattConnectionManager =
-                createGattConnectionManager(Preferences.builder(), () -> {});
-        gattConnectionManager.setGattConnection(mBluetoothGattConnection);
-        when(mBluetoothGattConnection.enableNotification(any(), any()))
-                .thenReturn(mChangeObserver);
-        InOrder inOrder = inOrder(mEventLoggerWrapper);
-
-        SignalRotatedException signalRotatedException =
-                assertThrows(
-                        SignalRotatedException.class,
-                        () -> getHandshakeHandler(
-                                gattConnectionManager, address -> "AA:BB:CC:DD:EE:FF")
-                                .doHandshakeWithRetryAndSignalLostCheck(
-                                        PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
-
-        inOrder.verify(mEventLoggerWrapper).setCurrentEvent(
-                NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
-        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
-        assertThat(signalRotatedException.getNewAddress()).isEqualTo("AA:BB:CC:DD:EE:FF");
-        assertThat(signalRotatedException).hasCauseThat().isEqualTo(exception);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void constructBytes_setRetroactiveFlag_decodeCorrectly() throws
-            GeneralSecurityException {
-        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
-                new HandshakeHandler.KeyBasedPairingRequest.Builder()
-                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
-                        .addFlag(REQUEST_RETROACTIVE_PAIR)
-                        .setSeekerPublicAddress(BluetoothAddress.decode(SEEKER_ADDRESS))
-                        .build();
-
-        byte[] encryptedRawMessage =
-                AesEcbSingleBlockEncryption.encrypt(
-                        SHARED_SECRET, keyBasedPairingRequest.getBytes());
-        HandshakeRequest handshakeRequest =
-                new HandshakeRequest(SHARED_SECRET, encryptedRawMessage);
-
-        assertThat(handshakeRequest.getType())
-                .isEqualTo(HandshakeRequest.Type.KEY_BASED_PAIRING_REQUEST);
-        assertThat(handshakeRequest.requestRetroactivePair()).isTrue();
-        assertThat(handshakeRequest.getVerificationData())
-                .isEqualTo(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS));
-        assertThat(handshakeRequest.getSeekerPublicAddress())
-                .isEqualTo(BluetoothAddress.decode(SEEKER_ADDRESS));
-        assertThat(handshakeRequest.requestDeviceName()).isFalse();
-        assertThat(handshakeRequest.requestDiscoverable()).isFalse();
-        assertThat(handshakeRequest.requestProviderInitialBonding()).isFalse();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void getTimeout_notOverShortRetryMaxSpentTime_getShort() {
-        Preferences preferences = Preferences.builder().build();
-
-        assertThat(getHandshakeHandler(/* getEnable128BitCustomGattCharacteristicsId= */ true)
-                .getTimeoutMs(
-                        preferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
-                                - 1))
-                .isEqualTo(preferences.getSecretHandshakeShortTimeoutMs());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void getTimeout_overShortRetryMaxSpentTime_getLong() {
-        Preferences preferences = Preferences.builder().build();
-
-        assertThat(getHandshakeHandler(/* getEnable128BitCustomGattCharacteristicsId= */ true)
-                .getTimeoutMs(
-                        preferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
-                                + 1))
-                .isEqualTo(preferences.getSecretHandshakeLongTimeoutMs());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void getTimeout_retryNotEnabled_getOrigin() {
-        Preferences preferences = Preferences.builder().build();
-
-        assertThat(
-                new HandshakeHandler(
-                        createGattConnectionManager(Preferences.builder(), () -> {}),
-                        PROVIDER_BLE_ADDRESS,
-                        Preferences.builder()
-                                .setRetryGattConnectionAndSecretHandshake(false).build(),
-                        mEventLoggerWrapper,
-                        /* fastPairSignalChecker= */ null)
-                        .getTimeoutMs(0))
-                .isEqualTo(Duration.ofSeconds(
-                        preferences.getGattOperationTimeoutSeconds()).toMillis());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void triggersActionOverBle_notCrash() {
-        HandshakeHandler.ActionOverBle.Builder actionOverBleBuilder =
-                new HandshakeHandler.ActionOverBle.Builder()
-                        .addFlag(
-                                Constants.FastPairService.KeyBasedPairingCharacteristic
-                                        .ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC)
-                        .setVerificationData(BluetoothAddress.decode(RANDOM_PRIVATE_ADDRESS))
-                        .setAdditionalDataType(AdditionalDataType.PERSONALIZED_NAME)
-                        .setEvent(0, 0)
-                        .setEventAdditionalData(new byte[]{1})
-                        .getThis();
-        HandshakeHandler.ActionOverBle actionOverBle = actionOverBleBuilder.build();
-        assertThat(actionOverBle.getBytes().length).isEqualTo(16);
-        assertThat(
-                Arrays.equals(
-                        Arrays.copyOfRange(actionOverBle.getBytes(), 0, 12),
-                        new byte[]{
-                                (byte) 16, (byte)  -64, (byte) -69, (byte) -69,
-                                (byte) -69, (byte)  -69, (byte) -69, (byte) 30,
-                                (byte) 0, (byte) 0, (byte) 1, (byte) 1}))
-                .isTrue();
-    }
-
-    private GattConnectionManager createGattConnectionManager(
-            Preferences.Builder prefs, ToggleBluetoothTask toggleBluetooth) {
-        return new GattConnectionManager(
-                ApplicationProvider.getApplicationContext(),
-                prefs.build(),
-                new EventLoggerWrapper(null),
-                BluetoothAdapter.getDefaultAdapter(),
-                toggleBluetooth,
-                PROVIDER_BLE_ADDRESS,
-                new TimingLogger("GattConnectionManager", prefs.build()),
-                /* fastPairSignalChecker= */ null,
-                /* setMtu= */ false);
-    }
-
-    private HandshakeHandler getHandshakeHandler(
-            GattConnectionManager gattConnectionManager,
-            @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker) {
-        return new HandshakeHandler(
-                gattConnectionManager,
-                PROVIDER_BLE_ADDRESS,
-                Preferences.builder()
-                        .setGattConnectionAndSecretHandshakeNoRetryGattError(ImmutableSet.of(257))
-                        .setRetrySecretHandshakeTimeout(true)
-                        .build(),
-                mEventLoggerWrapper,
-                fastPairSignalChecker);
-    }
-
-    private HandshakeHandler getHandshakeHandler(
-            boolean getEnable128BitCustomGattCharacteristicsId) {
-        return new HandshakeHandler(
-                createGattConnectionManager(Preferences.builder(), () -> {}),
-                PROVIDER_BLE_ADDRESS,
-                Preferences.builder()
-                        .setGattOperationTimeoutSeconds(5)
-                        .setEnable128BitCustomGattCharacteristicsId(
-                                getEnable128BitCustomGattCharacteristicsId)
-                        .build(),
-                mEventLoggerWrapper,
-                /* fastPairSignalChecker= */ null);
-    }
-
-    private static class HandshakeRequest {
-
-        /**
-         * 16 bytes data: 1-byte for type, 1-byte for flags, 6-bytes for provider's BLE address, 8
-         * bytes optional data.
-         *
-         * @see {go/fast-pair-spec-handshake-message1}
-         */
-        private final byte[] mDecryptedMessage;
-
-        HandshakeRequest(byte[] key, byte[] encryptedPairingRequest)
-                throws GeneralSecurityException {
-            mDecryptedMessage = AesEcbSingleBlockEncryption.decrypt(key, encryptedPairingRequest);
-        }
-
-        /**
-         * Gets the type of this handshake request. Currently, we have 2 types: 0x00 for Key-based
-         * Pairing Request and 0x10 for Action Request.
-         */
-        public Type getType() {
-            return Type.valueOf(mDecryptedMessage[Request.TYPE_INDEX]);
-        }
-
-        /**
-         * Gets verification data of this handshake request.
-         * Currently, we use Provider's BLE address.
-         */
-        public byte[] getVerificationData() {
-            return Arrays.copyOfRange(
-                    mDecryptedMessage,
-                    Request.VERIFICATION_DATA_INDEX,
-                    Request.VERIFICATION_DATA_INDEX + Request.VERIFICATION_DATA_LENGTH);
-        }
-
-        /** Gets Seeker's public address of the handshake request. */
-        public byte[] getSeekerPublicAddress() {
-            return Arrays.copyOfRange(
-                    mDecryptedMessage,
-                    Request.SEEKER_PUBLIC_ADDRESS_INDEX,
-                    Request.SEEKER_PUBLIC_ADDRESS_INDEX + BLUETOOTH_ADDRESS_LENGTH);
-        }
-
-        /** Checks whether the Seeker request discoverability from flags byte. */
-        public boolean requestDiscoverable() {
-            return (getFlags() & REQUEST_DISCOVERABLE) != 0;
-        }
-
-        /**
-         * Checks whether the Seeker requests that the Provider shall initiate bonding from
-         * flags byte.
-         */
-        public boolean requestProviderInitialBonding() {
-            return (getFlags() & PROVIDER_INITIATES_BONDING) != 0;
-        }
-
-        /** Checks whether the Seeker requests that the Provider shall notify the existing name. */
-        public boolean requestDeviceName() {
-            return (getFlags() & REQUEST_DEVICE_NAME) != 0;
-        }
-
-        /** Checks whether this is for retroactively writing account key. */
-        public boolean requestRetroactivePair() {
-            return (getFlags() & REQUEST_RETROACTIVE_PAIR) != 0;
-        }
-
-        /** Gets the flags of this handshake request. */
-        private byte getFlags() {
-            return mDecryptedMessage[Request.FLAGS_INDEX];
-        }
-
-        /** Checks whether the Seeker requests a device action. */
-        public boolean requestDeviceAction() {
-            return (getFlags() & DEVICE_ACTION) != 0;
-        }
-
-        /**
-         * Checks whether the Seeker requests an action which will be followed by an additional
-         * data.
-         */
-        public boolean requestFollowedByAdditionalData() {
-            return (getFlags() & ADDITIONAL_DATA_CHARACTERISTIC) != 0;
-        }
-
-        /** Gets the {@link AdditionalDataType} of this handshake request. */
-        @AdditionalDataType
-        public int getAdditionalDataType() {
-            if (!requestFollowedByAdditionalData()
-                    || mDecryptedMessage.length <= ADDITIONAL_DATA_TYPE_INDEX) {
-                return AdditionalDataType.UNKNOWN;
-            }
-            return mDecryptedMessage[ADDITIONAL_DATA_TYPE_INDEX];
-        }
-
-        /** Enumerates the handshake message types. */
-        public enum Type {
-            KEY_BASED_PAIRING_REQUEST(Request.TYPE_KEY_BASED_PAIRING_REQUEST),
-            ACTION_OVER_BLE(Request.TYPE_ACTION_OVER_BLE),
-            UNKNOWN((byte) 0xFF);
-
-            private final byte mValue;
-
-            Type(byte type) {
-                mValue = type;
-            }
-
-            public static Type valueOf(byte value) {
-                for (Type type : Type.values()) {
-                    if (type.getValue() == value) {
-                        return type;
-                    }
-                }
-                return UNKNOWN;
-            }
-
-            public byte getValue() {
-                return mValue;
-            }
-        }
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPieceTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPieceTest.java
deleted file mode 100644
index 32e62a3..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPieceTest.java
+++ /dev/null
@@ -1,258 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.net.Uri;
-import android.os.Parcel;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link HeadsetPiece}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class HeadsetPieceTest {
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void parcelAndUnparcel() {
-        HeadsetPiece headsetPiece = createDefaultHeadset().build();
-        Parcel expectedParcel = Parcel.obtain();
-        headsetPiece.writeToParcel(expectedParcel, 0);
-        expectedParcel.setDataPosition(0);
-
-        HeadsetPiece fromParcel = HeadsetPiece.CREATOR.createFromParcel(expectedParcel);
-
-        assertThat(fromParcel).isEqualTo(headsetPiece);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void parcelAndUnparcel_nullImageContentUri() {
-        HeadsetPiece headsetPiece = createDefaultHeadset().setImageContentUri(null).build();
-        Parcel expectedParcel = Parcel.obtain();
-        headsetPiece.writeToParcel(expectedParcel, 0);
-        expectedParcel.setDataPosition(0);
-
-        HeadsetPiece fromParcel = HeadsetPiece.CREATOR.createFromParcel(expectedParcel);
-
-        assertThat(fromParcel).isEqualTo(headsetPiece);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void equals() {
-        HeadsetPiece headsetPiece = createDefaultHeadset().build();
-
-        HeadsetPiece compareTo = createDefaultHeadset().build();
-
-        assertThat(headsetPiece).isEqualTo(compareTo);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void equals_nullImageContentUri() {
-        HeadsetPiece headsetPiece = createDefaultHeadset().setImageContentUri(null).build();
-
-        HeadsetPiece compareTo = createDefaultHeadset().setImageContentUri(null).build();
-
-        assertThat(headsetPiece).isEqualTo(compareTo);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void notEquals_differentLowLevelThreshold() {
-        HeadsetPiece headsetPiece = createDefaultHeadset().build();
-
-        HeadsetPiece compareTo = createDefaultHeadset().setLowLevelThreshold(1).build();
-
-        assertThat(headsetPiece).isNotEqualTo(compareTo);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void notEquals_differentBatteryLevel() {
-        HeadsetPiece headsetPiece = createDefaultHeadset().build();
-
-        HeadsetPiece compareTo = createDefaultHeadset().setBatteryLevel(99).build();
-
-        assertThat(headsetPiece).isNotEqualTo(compareTo);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void notEquals_differentImageUrl() {
-        HeadsetPiece headsetPiece = createDefaultHeadset().build();
-
-        HeadsetPiece compareTo =
-                createDefaultHeadset().setImageUrl("http://fake.image.path/different.png").build();
-
-        assertThat(headsetPiece).isNotEqualTo(compareTo);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void notEquals_differentChargingState() {
-        HeadsetPiece headsetPiece = createDefaultHeadset().build();
-
-        HeadsetPiece compareTo = createDefaultHeadset().setCharging(false).build();
-
-        assertThat(headsetPiece).isNotEqualTo(compareTo);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void notEquals_differentImageContentUri() {
-        HeadsetPiece headsetPiece = createDefaultHeadset().build();
-
-        HeadsetPiece compareTo =
-                createDefaultHeadset().setImageContentUri(Uri.parse("content://different.png"))
-                        .build();
-
-        assertThat(headsetPiece).isNotEqualTo(compareTo);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void notEquals_nullImageContentUri() {
-        HeadsetPiece headsetPiece = createDefaultHeadset().build();
-
-        HeadsetPiece compareTo = createDefaultHeadset().setImageContentUri(null).build();
-
-        assertThat(headsetPiece).isNotEqualTo(compareTo);
-    }
-
-    private static HeadsetPiece.Builder createDefaultHeadset() {
-        return HeadsetPiece.builder()
-                .setLowLevelThreshold(30)
-                .setBatteryLevel(18)
-                .setImageUrl("http://fake.image.path/image.png")
-                .setImageContentUri(Uri.parse("content://image.png"))
-                .setCharging(true);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void isLowBattery() {
-        HeadsetPiece headsetPiece =
-                HeadsetPiece.builder()
-                        .setLowLevelThreshold(30)
-                        .setBatteryLevel(18)
-                        .setImageUrl("http://fake.image.path/image.png")
-                        .setCharging(false)
-                        .build();
-
-        assertThat(headsetPiece.isBatteryLow()).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void isNotLowBattery() {
-        HeadsetPiece headsetPiece =
-                HeadsetPiece.builder()
-                        .setLowLevelThreshold(30)
-                        .setBatteryLevel(31)
-                        .setImageUrl("http://fake.image.path/image.png")
-                        .setCharging(false)
-                        .build();
-
-        assertThat(headsetPiece.isBatteryLow()).isFalse();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void isNotLowBattery_whileCharging() {
-        HeadsetPiece headsetPiece =
-                HeadsetPiece.builder()
-                        .setLowLevelThreshold(30)
-                        .setBatteryLevel(18)
-                        .setImageUrl("http://fake.image.path/image.png")
-                        .setCharging(true)
-                        .build();
-
-        assertThat(headsetPiece.isBatteryLow()).isFalse();
-    }
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void describeContents() {
-        HeadsetPiece headsetPiece =
-                HeadsetPiece.builder()
-                        .setLowLevelThreshold(30)
-                        .setBatteryLevel(18)
-                        .setImageUrl("http://fake.image.path/image.png")
-                        .setCharging(true)
-                        .build();
-        assertThat(headsetPiece.describeContents()).isEqualTo(0);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void hashcode() {
-        HeadsetPiece headsetPiece =
-                HeadsetPiece.builder()
-                        .setLowLevelThreshold(30)
-                        .setBatteryLevel(18)
-                        .setImageUrl("http://fake.image.path/image.png")
-                        .setCharging(true)
-                        .build();
-        HeadsetPiece headsetPiece1 =
-                HeadsetPiece.builder()
-                        .setLowLevelThreshold(30)
-                        .setBatteryLevel(18)
-                        .setImageUrl("http://fake.image.path/image.png")
-                        .setCharging(true)
-                        .build();
-        assertThat(headsetPiece.hashCode()).isEqualTo(headsetPiece1.hashCode());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testToString() {
-        HeadsetPiece headsetPiece =
-                HeadsetPiece.builder()
-                        .setLowLevelThreshold(30)
-                        .setBatteryLevel(18)
-                        .setImageUrl("http://fake.image.path/image.png")
-                        .setCharging(true)
-                        .build();
-        assertThat(headsetPiece.toString())
-                .isEqualTo("HeadsetPiece{lowLevelThreshold=30,"
-                        + " batteryLevel=18,"
-                        + " imageUrl=http://fake.image.path/image.png,"
-                        + " charging=true,"
-                        + " imageContentUri=null}");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testCreatorNewArray() {
-        HeadsetPiece[]  headsetPieces =
-                HeadsetPiece.CREATOR.newArray(2);
-        assertThat(headsetPieces.length).isEqualTo(2);
-    }
-}
-
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256Test.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256Test.java
deleted file mode 100644
index 8db3b97..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256Test.java
+++ /dev/null
@@ -1,153 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.KEY_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.HmacSha256.HMAC_SHA256_BLOCK_SIZE;
-
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.primitives.Bytes.concat;
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.google.common.base.Preconditions;
-import com.google.common.hash.Hashing;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import java.util.Random;
-
-/**
- * Unit tests for {@link HmacSha256}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class HmacSha256Test {
-
-    private static final int EXTRACT_HMAC_SIZE = 8;
-    private static final byte OUTER_PADDING_BYTE = 0x5c;
-    private static final byte INNER_PADDING_BYTE = 0x36;
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void compareResultWithOurImplementation_mustBeIdentical()
-            throws GeneralSecurityException {
-        Random random = new Random(0xFE2C);
-
-        for (int i = 0; i < 1000; i++) {
-            byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-            // Avoid too small data size that may cause false alarm.
-            int dataLength = random.nextInt(64);
-            byte[] data = new byte[dataLength];
-            random.nextBytes(data);
-
-            assertThat(HmacSha256.build(secret, data)).isEqualTo(doHmacSha256(secret, data));
-        }
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputIncorrectKeySizeToDecrypt_mustThrowException() {
-        byte[] secret = new byte[KEY_LENGTH - 1];
-        byte[] data = base16().decode("1234567890ABCDEF1234567890ABCDEF1234567890ABCD");
-
-        GeneralSecurityException exception =
-                assertThrows(GeneralSecurityException.class, () -> HmacSha256.build(secret, data));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Incorrect key length, should be the AES-128 key.");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputTwoIdenticalArrays_compareTwoHmacMustReturnTrue() {
-        Random random = new Random(0x1237);
-        byte[] array1 = new byte[EXTRACT_HMAC_SIZE];
-        random.nextBytes(array1);
-        byte[] array2 = Arrays.copyOf(array1, array1.length);
-
-        assertThat(HmacSha256.compareTwoHMACs(array1, array2)).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputTwoRandomArrays_compareTwoHmacMustReturnFalse() {
-        Random random = new Random(0xff);
-        byte[] array1 = new byte[EXTRACT_HMAC_SIZE];
-        random.nextBytes(array1);
-        byte[] array2 = new byte[EXTRACT_HMAC_SIZE];
-        random.nextBytes(array2);
-
-        assertThat(HmacSha256.compareTwoHMACs(array1, array2)).isFalse();
-    }
-
-    // HMAC-SHA256 may not be previously defined on Bluetooth platforms, so we explicitly create
-    // the code on test case. This will allow us to easily detect where partner implementation might
-    // have gone wrong or where our spec isn't clear enough.
-    static byte[] doHmacSha256(byte[] key, byte[] data) {
-
-        Preconditions.checkArgument(
-                key != null && key.length == KEY_LENGTH && data != null,
-                "Parameters can't be null.");
-
-        // Performs SHA256(concat((key ^ opad),SHA256(concat((key ^ ipad), data)))), where
-        // key is the given secret extended to 64 bytes by concat(secret, ZEROS).
-        // opad is 64 bytes outer padding, consisting of repeated bytes valued 0x5c.
-        // ipad is 64 bytes inner padding, consisting of repeated bytes valued 0x36.
-        byte[] keyIpad = new byte[HMAC_SHA256_BLOCK_SIZE];
-        byte[] keyOpad = new byte[HMAC_SHA256_BLOCK_SIZE];
-
-        for (int i = 0; i < KEY_LENGTH; i++) {
-            keyIpad[i] = (byte) (key[i] ^ INNER_PADDING_BYTE);
-            keyOpad[i] = (byte) (key[i] ^ OUTER_PADDING_BYTE);
-        }
-        Arrays.fill(keyIpad, KEY_LENGTH, HMAC_SHA256_BLOCK_SIZE, INNER_PADDING_BYTE);
-        Arrays.fill(keyOpad, KEY_LENGTH, HMAC_SHA256_BLOCK_SIZE, OUTER_PADDING_BYTE);
-
-        byte[] innerSha256Result = Hashing.sha256().hashBytes(concat(keyIpad, data)).asBytes();
-        return Hashing.sha256().hashBytes(concat(keyOpad, innerSha256Result)).asBytes();
-    }
-
-    // Adds this test example on spec. Also we can easily change the parameters(e.g. secret, data)
-    // to clarify test results with partners.
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputTestExampleToHmacSha256_getCorrectResult() {
-        byte[] secret = base16().decode("0123456789ABCDEF0123456789ABCDEF");
-        byte[] data =
-                base16().decode(
-                        "0001020304050607EE4A2483738052E44E9B2A145E5DDFAA44B9E5536AF438E1E5C6");
-
-        byte[] hmacResult = doHmacSha256(secret, data);
-
-        assertThat(hmacResult)
-                .isEqualTo(base16().decode(
-                        "55EC5E6055AF6E92618B7D8710D4413709AB5DA27CA26A66F52E5AD4E8209052"));
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/LtvTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/LtvTest.java
deleted file mode 100644
index 81a5d92..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/LtvTest.java
+++ /dev/null
@@ -1,52 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link Ltv}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class LtvTest {
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testParseEmpty_throwsException() throws Ltv.ParseException {
-        assertThrows(Ltv.ParseException.class,
-                () -> Ltv.parse(new byte[]{0}));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testParse_finishesSuccessfully() throws Ltv.ParseException {
-        assertThat(Ltv.parse(new byte[]{3, 4, 5, 6})).isNotEmpty();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoderTest.java
deleted file mode 100644
index d4c3342..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoderTest.java
+++ /dev/null
@@ -1,131 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder.EXTRACT_HMAC_SIZE;
-import static com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder.SECTION_NONCE_LENGTH;
-
-import static com.google.common.primitives.Bytes.concat;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-
-/**
- * Unit tests for {@link MessageStreamHmacEncoder}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class MessageStreamHmacEncoderTest {
-
-    private static final int ACCOUNT_KEY_LENGTH = 16;
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void encodeMessagePacket() throws GeneralSecurityException {
-        int messageLength = 2;
-        SecureRandom secureRandom = new SecureRandom();
-        byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
-        secureRandom.nextBytes(accountKey);
-        byte[] data = new byte[messageLength];
-        secureRandom.nextBytes(data);
-        byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
-        secureRandom.nextBytes(sectionNonce);
-
-        byte[] result = MessageStreamHmacEncoder
-                .encodeMessagePacket(accountKey, sectionNonce, data);
-
-        assertThat(result).hasLength(messageLength + SECTION_NONCE_LENGTH + EXTRACT_HMAC_SIZE);
-        // First bytes are raw message bytes.
-        assertThat(Arrays.copyOf(result, messageLength)).isEqualTo(data);
-        // Following by message nonce.
-        byte[] messageNonce =
-                Arrays.copyOfRange(result, messageLength, messageLength + SECTION_NONCE_LENGTH);
-        byte[] extractedHmac =
-                Arrays.copyOf(
-                        HmacSha256.buildWith64BytesKey(accountKey,
-                                concat(sectionNonce, messageNonce, data)),
-                        EXTRACT_HMAC_SIZE);
-        // Finally hash mac.
-        assertThat(Arrays.copyOfRange(result, messageLength + SECTION_NONCE_LENGTH, result.length))
-                .isEqualTo(extractedHmac);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void verifyHmac() throws GeneralSecurityException {
-        int messageLength = 2;
-        SecureRandom secureRandom = new SecureRandom();
-        byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
-        secureRandom.nextBytes(accountKey);
-        byte[] data = new byte[messageLength];
-        secureRandom.nextBytes(data);
-        byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
-        secureRandom.nextBytes(sectionNonce);
-        byte[] result = MessageStreamHmacEncoder
-                .encodeMessagePacket(accountKey, sectionNonce, data);
-
-        assertThat(MessageStreamHmacEncoder.verifyHmac(accountKey, sectionNonce, result)).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void verifyHmac_failedByAccountKey() throws GeneralSecurityException {
-        int messageLength = 2;
-        SecureRandom secureRandom = new SecureRandom();
-        byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
-        secureRandom.nextBytes(accountKey);
-        byte[] data = new byte[messageLength];
-        secureRandom.nextBytes(data);
-        byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
-        secureRandom.nextBytes(sectionNonce);
-        byte[] result = MessageStreamHmacEncoder
-                .encodeMessagePacket(accountKey, sectionNonce, data);
-        secureRandom.nextBytes(accountKey);
-
-        assertThat(MessageStreamHmacEncoder.verifyHmac(accountKey, sectionNonce, result)).isFalse();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void verifyHmac_failedBySectionNonce() throws GeneralSecurityException {
-        int messageLength = 2;
-        SecureRandom secureRandom = new SecureRandom();
-        byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
-        secureRandom.nextBytes(accountKey);
-        byte[] data = new byte[messageLength];
-        secureRandom.nextBytes(data);
-        byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
-        secureRandom.nextBytes(sectionNonce);
-        byte[] result = MessageStreamHmacEncoder
-                .encodeMessagePacket(accountKey, sectionNonce, data);
-        secureRandom.nextBytes(sectionNonce);
-
-        assertThat(MessageStreamHmacEncoder.verifyHmac(accountKey, sectionNonce, result)).isFalse();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoderTest.java
deleted file mode 100644
index d66d209..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoderTest.java
+++ /dev/null
@@ -1,149 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.KEY_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE;
-import static com.android.server.nearby.common.bluetooth.fastpair.NamingEncoder.EXTRACT_HMAC_SIZE;
-import static com.android.server.nearby.common.bluetooth.fastpair.NamingEncoder.MAX_LENGTH_OF_NAME;
-
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.security.GeneralSecurityException;
-
-/**
- * Unit tests for {@link NamingEncoder}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class NamingEncoderTest {
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void decodeEncodedNamingPacket_mustGetSameName() throws GeneralSecurityException {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        String name = "Someone's Google Headphone";
-
-        byte[] encodedNamingPacket = NamingEncoder.encodeNamingPacket(secret, name);
-
-        assertThat(NamingEncoder.decodeNamingPacket(secret, encodedNamingPacket)).isEqualTo(name);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputIncorrectKeySizeToEncode_mustThrowException() {
-        byte[] secret = new byte[KEY_LENGTH - 1];
-        String data = "Someone's Google Headphone";
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> NamingEncoder.encodeNamingPacket(secret, data));
-
-        assertThat(exception).hasMessageThat()
-                .contains("Incorrect secret for encoding name packet");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputIncorrectKeySizeToDecode_mustThrowException() {
-        byte[] secret = new byte[KEY_LENGTH - 1];
-        byte[] data = new byte[50];
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> NamingEncoder.decodeNamingPacket(secret, data));
-
-        assertThat(exception).hasMessageThat()
-                .contains("Incorrect secret for decoding name packet");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputTooSmallPacketSize_mustThrowException() {
-        byte[] secret = new byte[KEY_LENGTH];
-        byte[] data = new byte[EXTRACT_HMAC_SIZE - 1];
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> NamingEncoder.decodeNamingPacket(secret, data));
-
-        assertThat(exception).hasMessageThat().contains("Naming packet size is incorrect");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputTooLargePacketSize_mustThrowException() throws GeneralSecurityException {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        byte[] namingPacket = new byte[MAX_LENGTH_OF_NAME + EXTRACT_HMAC_SIZE + NONCE_SIZE + 1];
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> NamingEncoder.decodeNamingPacket(secret, namingPacket));
-
-        assertThat(exception).hasMessageThat().contains("Naming packet size is incorrect");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void inputIncorrectHmacToDecode_mustThrowException() throws GeneralSecurityException {
-        byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
-        String name = "Someone's Google Headphone";
-
-        byte[] encodedNamingPacket = NamingEncoder.encodeNamingPacket(secret, name);
-        encodedNamingPacket[0] = (byte) ~encodedNamingPacket[0];
-
-        GeneralSecurityException exception =
-                assertThrows(
-                        GeneralSecurityException.class,
-                        () -> NamingEncoder.decodeNamingPacket(secret, encodedNamingPacket));
-
-        assertThat(exception)
-                .hasMessageThat()
-                .contains("Verify HMAC failed, could be incorrect key or naming packet.");
-    }
-
-    // Adds this test example on spec. Also we can easily change the parameters(e.g. secret, naming
-    // packet) to clarify test results with partners.
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void decodeTestNamingPacket_mustGetSameName() throws GeneralSecurityException {
-        byte[] secret = base16().decode("0123456789ABCDEF0123456789ABCDEF");
-        byte[] namingPacket = base16().decode(
-                "55EC5E6055AF6E920001020304050607EE4A2483738052E44E9B2A145E5DDFAA44B9E5536AF438"
-                        + "E1E5C6");
-
-        assertThat(NamingEncoder.decodeNamingPacket(secret, namingPacket))
-                .isEqualTo("Someone's Google Headphone");
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListenerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListenerTest.java
deleted file mode 100644
index a58a92d..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListenerTest.java
+++ /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 com.android.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-
-public class PairingProgressListenerTest {
-
-    @Test
-    public void testFromOrdinal() {
-        assertThat(PairingProgressListener.fromOrdinal(0)).isEqualTo(
-                PairingProgressListener.PairingEvent.START);
-        assertThat(PairingProgressListener.fromOrdinal(1)).isEqualTo(
-                PairingProgressListener.PairingEvent.SUCCESS);
-        assertThat(PairingProgressListener.fromOrdinal(2)).isEqualTo(
-                PairingProgressListener.PairingEvent.FAILED);
-        assertThat(PairingProgressListener.fromOrdinal(3)).isEqualTo(
-                PairingProgressListener.PairingEvent.UNKNOWN);
-        assertThat(PairingProgressListener.fromOrdinal(4)).isEqualTo(
-                PairingProgressListener.PairingEvent.UNKNOWN);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PreferencesTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PreferencesTest.java
deleted file mode 100644
index 378e3b9..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PreferencesTest.java
+++ /dev/null
@@ -1,1303 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link Preferences}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class PreferencesTest {
-
-    private static final int FIRST_INT = 1505;
-    private static final int SECOND_INT = 1506;
-    private static final boolean FIRST_BOOL = true;
-    private static final boolean SECOND_BOOL = false;
-    private static final short FIRST_SHORT = 32;
-    private static final short SECOND_SHORT = 73;
-    private static final long FIRST_LONG = 9838L;
-    private static final long SECOND_LONG = 93935L;
-    private static final String FIRST_STRING = "FIRST_STRING";
-    private static final String SECOND_STRING = "SECOND_STRING";
-    private static final byte[] FIRST_BYTES = new byte[] {7, 9};
-    private static final byte[] SECOND_BYTES = new byte[] {2};
-    private static final ImmutableSet<Integer> FIRST_INT_SETS = ImmutableSet.of(6, 8);
-    private static final ImmutableSet<Integer> SECOND_INT_SETS = ImmutableSet.of(6, 8);
-    private static final Preferences.ExtraLoggingInformation FIRST_EXTRA_LOGGING_INFO =
-            Preferences.ExtraLoggingInformation.builder().setModelId("000006").build();
-    private static final Preferences.ExtraLoggingInformation SECOND_EXTRA_LOGGING_INFO =
-            Preferences.ExtraLoggingInformation.builder().setModelId("000007").build();
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGattOperationTimeoutSeconds() {
-        Preferences prefs =
-                Preferences.builder().setGattOperationTimeoutSeconds(FIRST_INT).build();
-        assertThat(prefs.getGattOperationTimeoutSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getGattOperationTimeoutSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setGattOperationTimeoutSeconds(SECOND_INT).build();
-        assertThat(prefs2.getGattOperationTimeoutSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGattConnectionTimeoutSeconds() {
-        Preferences prefs =
-                Preferences.builder().setGattConnectionTimeoutSeconds(FIRST_INT).build();
-        assertThat(prefs.getGattConnectionTimeoutSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getGattConnectionTimeoutSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setGattConnectionTimeoutSeconds(SECOND_INT).build();
-        assertThat(prefs2.getGattConnectionTimeoutSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothToggleTimeoutSeconds() {
-        Preferences prefs =
-                Preferences.builder().setBluetoothToggleTimeoutSeconds(FIRST_INT).build();
-        assertThat(prefs.getBluetoothToggleTimeoutSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getBluetoothToggleTimeoutSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setBluetoothToggleTimeoutSeconds(SECOND_INT).build();
-        assertThat(prefs2.getBluetoothToggleTimeoutSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothToggleSleepSeconds() {
-        Preferences prefs =
-                Preferences.builder().setBluetoothToggleSleepSeconds(FIRST_INT).build();
-        assertThat(prefs.getBluetoothToggleSleepSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getBluetoothToggleSleepSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setBluetoothToggleSleepSeconds(SECOND_INT).build();
-        assertThat(prefs2.getBluetoothToggleSleepSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testClassicDiscoveryTimeoutSeconds() {
-        Preferences prefs =
-                Preferences.builder().setClassicDiscoveryTimeoutSeconds(FIRST_INT).build();
-        assertThat(prefs.getClassicDiscoveryTimeoutSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getClassicDiscoveryTimeoutSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setClassicDiscoveryTimeoutSeconds(SECOND_INT).build();
-        assertThat(prefs2.getClassicDiscoveryTimeoutSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNumDiscoverAttempts() {
-        Preferences prefs =
-                Preferences.builder().setNumDiscoverAttempts(FIRST_INT).build();
-        assertThat(prefs.getNumDiscoverAttempts()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getNumDiscoverAttempts())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setNumDiscoverAttempts(SECOND_INT).build();
-        assertThat(prefs2.getNumDiscoverAttempts()).isEqualTo(SECOND_INT);
-    }
-
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testDiscoveryRetrySleepSeconds() {
-        Preferences prefs =
-                Preferences.builder().setDiscoveryRetrySleepSeconds(FIRST_INT).build();
-        assertThat(prefs.getDiscoveryRetrySleepSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getDiscoveryRetrySleepSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setDiscoveryRetrySleepSeconds(SECOND_INT).build();
-        assertThat(prefs2.getDiscoveryRetrySleepSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSdpTimeoutSeconds() {
-        Preferences prefs =
-                Preferences.builder().setSdpTimeoutSeconds(FIRST_INT).build();
-        assertThat(prefs.getSdpTimeoutSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getSdpTimeoutSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setSdpTimeoutSeconds(SECOND_INT).build();
-        assertThat(prefs2.getSdpTimeoutSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNumSdpAttempts() {
-        Preferences prefs =
-                Preferences.builder().setNumSdpAttempts(FIRST_INT).build();
-        assertThat(prefs.getNumSdpAttempts()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getNumSdpAttempts())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setNumSdpAttempts(SECOND_INT).build();
-        assertThat(prefs2.getNumSdpAttempts()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNumCreateBondAttempts() {
-        Preferences prefs =
-                Preferences.builder().setNumCreateBondAttempts(FIRST_INT).build();
-        assertThat(prefs.getNumCreateBondAttempts()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getNumCreateBondAttempts())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setNumCreateBondAttempts(SECOND_INT).build();
-        assertThat(prefs2.getNumCreateBondAttempts()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNumConnectAttempts() {
-        Preferences prefs =
-                Preferences.builder().setNumConnectAttempts(FIRST_INT).build();
-        assertThat(prefs.getNumConnectAttempts()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getNumConnectAttempts())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setNumConnectAttempts(SECOND_INT).build();
-        assertThat(prefs2.getNumConnectAttempts()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNumWriteAccountKeyAttempts() {
-        Preferences prefs =
-                Preferences.builder().setNumWriteAccountKeyAttempts(FIRST_INT).build();
-        assertThat(prefs.getNumWriteAccountKeyAttempts()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getNumWriteAccountKeyAttempts())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setNumWriteAccountKeyAttempts(SECOND_INT).build();
-        assertThat(prefs2.getNumWriteAccountKeyAttempts()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothStatePollingMillis() {
-        Preferences prefs =
-                Preferences.builder().setBluetoothStatePollingMillis(FIRST_INT).build();
-        assertThat(prefs.getBluetoothStatePollingMillis()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getBluetoothStatePollingMillis())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setBluetoothStatePollingMillis(SECOND_INT).build();
-        assertThat(prefs2.getBluetoothStatePollingMillis()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNumAttempts() {
-        Preferences prefs =
-                Preferences.builder().setNumAttempts(FIRST_INT).build();
-        assertThat(prefs.getNumAttempts()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getNumAttempts())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setNumAttempts(SECOND_INT).build();
-        assertThat(prefs2.getNumAttempts()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testRemoveBondTimeoutSeconds() {
-        Preferences prefs =
-                Preferences.builder().setRemoveBondTimeoutSeconds(FIRST_INT).build();
-        assertThat(prefs.getRemoveBondTimeoutSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getRemoveBondTimeoutSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setRemoveBondTimeoutSeconds(SECOND_INT).build();
-        assertThat(prefs2.getRemoveBondTimeoutSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testRemoveBondSleepMillis() {
-        Preferences prefs =
-                Preferences.builder().setRemoveBondSleepMillis(FIRST_INT).build();
-        assertThat(prefs.getRemoveBondSleepMillis()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getRemoveBondSleepMillis())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setRemoveBondSleepMillis(SECOND_INT).build();
-        assertThat(prefs2.getRemoveBondSleepMillis()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testCreateBondTimeoutSeconds() {
-        Preferences prefs =
-                Preferences.builder().setCreateBondTimeoutSeconds(FIRST_INT).build();
-        assertThat(prefs.getCreateBondTimeoutSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getCreateBondTimeoutSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setCreateBondTimeoutSeconds(SECOND_INT).build();
-        assertThat(prefs2.getCreateBondTimeoutSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHidCreateBondTimeoutSeconds() {
-        Preferences prefs =
-                Preferences.builder().setHidCreateBondTimeoutSeconds(FIRST_INT).build();
-        assertThat(prefs.getHidCreateBondTimeoutSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getHidCreateBondTimeoutSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setHidCreateBondTimeoutSeconds(SECOND_INT).build();
-        assertThat(prefs2.getHidCreateBondTimeoutSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testProxyTimeoutSeconds() {
-        Preferences prefs =
-                Preferences.builder().setProxyTimeoutSeconds(FIRST_INT).build();
-        assertThat(prefs.getProxyTimeoutSeconds()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getProxyTimeoutSeconds())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setProxyTimeoutSeconds(SECOND_INT).build();
-        assertThat(prefs2.getProxyTimeoutSeconds()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWriteAccountKeySleepMillis() {
-        Preferences prefs =
-                Preferences.builder().setWriteAccountKeySleepMillis(FIRST_INT).build();
-        assertThat(prefs.getWriteAccountKeySleepMillis()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getWriteAccountKeySleepMillis())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setWriteAccountKeySleepMillis(SECOND_INT).build();
-        assertThat(prefs2.getWriteAccountKeySleepMillis()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testPairFailureCounts() {
-        Preferences prefs =
-                Preferences.builder().setPairFailureCounts(FIRST_INT).build();
-        assertThat(prefs.getPairFailureCounts()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getPairFailureCounts())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setPairFailureCounts(SECOND_INT).build();
-        assertThat(prefs2.getPairFailureCounts()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testCreateBondTransportType() {
-        Preferences prefs =
-                Preferences.builder().setCreateBondTransportType(FIRST_INT).build();
-        assertThat(prefs.getCreateBondTransportType()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getCreateBondTransportType())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setCreateBondTransportType(SECOND_INT).build();
-        assertThat(prefs2.getCreateBondTransportType()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGattConnectRetryTimeoutMillis() {
-        Preferences prefs =
-                Preferences.builder().setGattConnectRetryTimeoutMillis(FIRST_INT).build();
-        assertThat(prefs.getGattConnectRetryTimeoutMillis()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getGattConnectRetryTimeoutMillis())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setGattConnectRetryTimeoutMillis(SECOND_INT).build();
-        assertThat(prefs2.getGattConnectRetryTimeoutMillis()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNumSdpAttemptsAfterBonded() {
-        Preferences prefs =
-                Preferences.builder().setNumSdpAttemptsAfterBonded(FIRST_INT).build();
-        assertThat(prefs.getNumSdpAttemptsAfterBonded()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getNumSdpAttemptsAfterBonded())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setNumSdpAttemptsAfterBonded(SECOND_INT).build();
-        assertThat(prefs2.getNumSdpAttemptsAfterBonded()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSameModelIdPairedDeviceCount() {
-        Preferences prefs =
-                Preferences.builder().setSameModelIdPairedDeviceCount(FIRST_INT).build();
-        assertThat(prefs.getSameModelIdPairedDeviceCount()).isEqualTo(FIRST_INT);
-        assertThat(prefs.toBuilder().build().getSameModelIdPairedDeviceCount())
-                .isEqualTo(FIRST_INT);
-
-        Preferences prefs2 =
-                Preferences.builder().setSameModelIdPairedDeviceCount(SECOND_INT).build();
-        assertThat(prefs2.getSameModelIdPairedDeviceCount()).isEqualTo(SECOND_INT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testIgnoreDiscoveryError() {
-        Preferences prefs =
-                Preferences.builder().setIgnoreDiscoveryError(FIRST_BOOL).build();
-        assertThat(prefs.getIgnoreDiscoveryError()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getIgnoreDiscoveryError())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setIgnoreDiscoveryError(SECOND_BOOL).build();
-        assertThat(prefs2.getIgnoreDiscoveryError()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testToggleBluetoothOnFailure() {
-        Preferences prefs =
-                Preferences.builder().setToggleBluetoothOnFailure(FIRST_BOOL).build();
-        assertThat(prefs.getToggleBluetoothOnFailure()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getToggleBluetoothOnFailure())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setToggleBluetoothOnFailure(SECOND_BOOL).build();
-        assertThat(prefs2.getToggleBluetoothOnFailure()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothStateUsesPolling() {
-        Preferences prefs =
-                Preferences.builder().setBluetoothStateUsesPolling(FIRST_BOOL).build();
-        assertThat(prefs.getBluetoothStateUsesPolling()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getBluetoothStateUsesPolling())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setBluetoothStateUsesPolling(SECOND_BOOL).build();
-        assertThat(prefs2.getBluetoothStateUsesPolling()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEnableBrEdrHandover() {
-        Preferences prefs =
-                Preferences.builder().setEnableBrEdrHandover(FIRST_BOOL).build();
-        assertThat(prefs.getEnableBrEdrHandover()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getEnableBrEdrHandover())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setEnableBrEdrHandover(SECOND_BOOL).build();
-        assertThat(prefs2.getEnableBrEdrHandover()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWaitForUuidsAfterBonding() {
-        Preferences prefs =
-                Preferences.builder().setWaitForUuidsAfterBonding(FIRST_BOOL).build();
-        assertThat(prefs.getWaitForUuidsAfterBonding()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getWaitForUuidsAfterBonding())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setWaitForUuidsAfterBonding(SECOND_BOOL).build();
-        assertThat(prefs2.getWaitForUuidsAfterBonding()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testReceiveUuidsAndBondedEventBeforeClose() {
-        Preferences prefs =
-                Preferences.builder().setReceiveUuidsAndBondedEventBeforeClose(FIRST_BOOL).build();
-        assertThat(prefs.getReceiveUuidsAndBondedEventBeforeClose()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getReceiveUuidsAndBondedEventBeforeClose())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setReceiveUuidsAndBondedEventBeforeClose(SECOND_BOOL).build();
-        assertThat(prefs2.getReceiveUuidsAndBondedEventBeforeClose()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testRejectPhonebookAccess() {
-        Preferences prefs =
-                Preferences.builder().setRejectPhonebookAccess(FIRST_BOOL).build();
-        assertThat(prefs.getRejectPhonebookAccess()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getRejectPhonebookAccess())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setRejectPhonebookAccess(SECOND_BOOL).build();
-        assertThat(prefs2.getRejectPhonebookAccess()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testRejectMessageAccess() {
-        Preferences prefs =
-                Preferences.builder().setRejectMessageAccess(FIRST_BOOL).build();
-        assertThat(prefs.getRejectMessageAccess()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getRejectMessageAccess())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setRejectMessageAccess(SECOND_BOOL).build();
-        assertThat(prefs2.getRejectMessageAccess()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testRejectSimAccess() {
-        Preferences prefs =
-                Preferences.builder().setRejectSimAccess(FIRST_BOOL).build();
-        assertThat(prefs.getRejectSimAccess()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getRejectSimAccess())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setRejectSimAccess(SECOND_BOOL).build();
-        assertThat(prefs2.getRejectSimAccess()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSkipDisconnectingGattBeforeWritingAccountKey() {
-        Preferences prefs =
-                Preferences.builder().setSkipDisconnectingGattBeforeWritingAccountKey(FIRST_BOOL)
-                        .build();
-        assertThat(prefs.getSkipDisconnectingGattBeforeWritingAccountKey()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getSkipDisconnectingGattBeforeWritingAccountKey())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setSkipDisconnectingGattBeforeWritingAccountKey(SECOND_BOOL)
-                        .build();
-        assertThat(prefs2.getSkipDisconnectingGattBeforeWritingAccountKey()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testMoreEventLogForQuality() {
-        Preferences prefs =
-                Preferences.builder().setMoreEventLogForQuality(FIRST_BOOL).build();
-        assertThat(prefs.getMoreEventLogForQuality()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getMoreEventLogForQuality())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setMoreEventLogForQuality(SECOND_BOOL).build();
-        assertThat(prefs2.getMoreEventLogForQuality()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testRetryGattConnectionAndSecretHandshake() {
-        Preferences prefs =
-                Preferences.builder().setRetryGattConnectionAndSecretHandshake(FIRST_BOOL).build();
-        assertThat(prefs.getRetryGattConnectionAndSecretHandshake()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getRetryGattConnectionAndSecretHandshake())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setRetryGattConnectionAndSecretHandshake(SECOND_BOOL).build();
-        assertThat(prefs2.getRetryGattConnectionAndSecretHandshake()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testRetrySecretHandshakeTimeout() {
-        Preferences prefs =
-                Preferences.builder().setRetrySecretHandshakeTimeout(FIRST_BOOL).build();
-        assertThat(prefs.getRetrySecretHandshakeTimeout()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getRetrySecretHandshakeTimeout())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setRetrySecretHandshakeTimeout(SECOND_BOOL).build();
-        assertThat(prefs2.getRetrySecretHandshakeTimeout()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testLogUserManualRetry() {
-        Preferences prefs =
-                Preferences.builder().setLogUserManualRetry(FIRST_BOOL).build();
-        assertThat(prefs.getLogUserManualRetry()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getLogUserManualRetry())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setLogUserManualRetry(SECOND_BOOL).build();
-        assertThat(prefs2.getLogUserManualRetry()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testIsDeviceFinishCheckAddressFromCache() {
-        Preferences prefs =
-                Preferences.builder().setIsDeviceFinishCheckAddressFromCache(FIRST_BOOL).build();
-        assertThat(prefs.getIsDeviceFinishCheckAddressFromCache()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getIsDeviceFinishCheckAddressFromCache())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setIsDeviceFinishCheckAddressFromCache(SECOND_BOOL).build();
-        assertThat(prefs2.getIsDeviceFinishCheckAddressFromCache()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testLogPairWithCachedModelId() {
-        Preferences prefs =
-                Preferences.builder().setLogPairWithCachedModelId(FIRST_BOOL).build();
-        assertThat(prefs.getLogPairWithCachedModelId()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getLogPairWithCachedModelId())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setLogPairWithCachedModelId(SECOND_BOOL).build();
-        assertThat(prefs2.getLogPairWithCachedModelId()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testDirectConnectProfileIfModelIdInCache() {
-        Preferences prefs =
-                Preferences.builder().setDirectConnectProfileIfModelIdInCache(FIRST_BOOL).build();
-        assertThat(prefs.getDirectConnectProfileIfModelIdInCache()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getDirectConnectProfileIfModelIdInCache())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setDirectConnectProfileIfModelIdInCache(SECOND_BOOL).build();
-        assertThat(prefs2.getDirectConnectProfileIfModelIdInCache()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testAcceptPasskey() {
-        Preferences prefs =
-                Preferences.builder().setAcceptPasskey(FIRST_BOOL).build();
-        assertThat(prefs.getAcceptPasskey()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getAcceptPasskey())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setAcceptPasskey(SECOND_BOOL).build();
-        assertThat(prefs2.getAcceptPasskey()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testProviderInitiatesBondingIfSupported() {
-        Preferences prefs =
-                Preferences.builder().setProviderInitiatesBondingIfSupported(FIRST_BOOL).build();
-        assertThat(prefs.getProviderInitiatesBondingIfSupported()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getProviderInitiatesBondingIfSupported())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setProviderInitiatesBondingIfSupported(SECOND_BOOL).build();
-        assertThat(prefs2.getProviderInitiatesBondingIfSupported()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testAttemptDirectConnectionWhenPreviouslyBonded() {
-        Preferences prefs =
-                Preferences.builder()
-                        .setAttemptDirectConnectionWhenPreviouslyBonded(FIRST_BOOL).build();
-        assertThat(prefs.getAttemptDirectConnectionWhenPreviouslyBonded()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getAttemptDirectConnectionWhenPreviouslyBonded())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder()
-                        .setAttemptDirectConnectionWhenPreviouslyBonded(SECOND_BOOL).build();
-        assertThat(prefs2.getAttemptDirectConnectionWhenPreviouslyBonded()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testAutomaticallyReconnectGattWhenNeeded() {
-        Preferences prefs =
-                Preferences.builder().setAutomaticallyReconnectGattWhenNeeded(FIRST_BOOL).build();
-        assertThat(prefs.getAutomaticallyReconnectGattWhenNeeded()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getAutomaticallyReconnectGattWhenNeeded())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setAutomaticallyReconnectGattWhenNeeded(SECOND_BOOL).build();
-        assertThat(prefs2.getAutomaticallyReconnectGattWhenNeeded()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSkipConnectingProfiles() {
-        Preferences prefs =
-                Preferences.builder().setSkipConnectingProfiles(FIRST_BOOL).build();
-        assertThat(prefs.getSkipConnectingProfiles()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getSkipConnectingProfiles())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setSkipConnectingProfiles(SECOND_BOOL).build();
-        assertThat(prefs2.getSkipConnectingProfiles()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testIgnoreUuidTimeoutAfterBonded() {
-        Preferences prefs =
-                Preferences.builder().setIgnoreUuidTimeoutAfterBonded(FIRST_BOOL).build();
-        assertThat(prefs.getIgnoreUuidTimeoutAfterBonded()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getIgnoreUuidTimeoutAfterBonded())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setIgnoreUuidTimeoutAfterBonded(SECOND_BOOL).build();
-        assertThat(prefs2.getIgnoreUuidTimeoutAfterBonded()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSpecifyCreateBondTransportType() {
-        Preferences prefs =
-                Preferences.builder().setSpecifyCreateBondTransportType(FIRST_BOOL).build();
-        assertThat(prefs.getSpecifyCreateBondTransportType()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getSpecifyCreateBondTransportType())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setSpecifyCreateBondTransportType(SECOND_BOOL).build();
-        assertThat(prefs2.getSpecifyCreateBondTransportType()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testIncreaseIntentFilterPriority() {
-        Preferences prefs =
-                Preferences.builder().setIncreaseIntentFilterPriority(FIRST_BOOL).build();
-        assertThat(prefs.getIncreaseIntentFilterPriority()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getIncreaseIntentFilterPriority())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setIncreaseIntentFilterPriority(SECOND_BOOL).build();
-        assertThat(prefs2.getIncreaseIntentFilterPriority()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEvaluatePerformance() {
-        Preferences prefs =
-                Preferences.builder().setEvaluatePerformance(FIRST_BOOL).build();
-        assertThat(prefs.getEvaluatePerformance()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getEvaluatePerformance())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setEvaluatePerformance(SECOND_BOOL).build();
-        assertThat(prefs2.getEvaluatePerformance()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEnableNamingCharacteristic() {
-        Preferences prefs =
-                Preferences.builder().setEnableNamingCharacteristic(FIRST_BOOL).build();
-        assertThat(prefs.getEnableNamingCharacteristic()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getEnableNamingCharacteristic())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setEnableNamingCharacteristic(SECOND_BOOL).build();
-        assertThat(prefs2.getEnableNamingCharacteristic()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEnableFirmwareVersionCharacteristic() {
-        Preferences prefs =
-                Preferences.builder().setEnableFirmwareVersionCharacteristic(FIRST_BOOL).build();
-        assertThat(prefs.getEnableFirmwareVersionCharacteristic()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getEnableFirmwareVersionCharacteristic())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setEnableFirmwareVersionCharacteristic(SECOND_BOOL).build();
-        assertThat(prefs2.getEnableFirmwareVersionCharacteristic()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testKeepSameAccountKeyWrite() {
-        Preferences prefs =
-                Preferences.builder().setKeepSameAccountKeyWrite(FIRST_BOOL).build();
-        assertThat(prefs.getKeepSameAccountKeyWrite()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getKeepSameAccountKeyWrite())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setKeepSameAccountKeyWrite(SECOND_BOOL).build();
-        assertThat(prefs2.getKeepSameAccountKeyWrite()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testIsRetroactivePairing() {
-        Preferences prefs =
-                Preferences.builder().setIsRetroactivePairing(FIRST_BOOL).build();
-        assertThat(prefs.getIsRetroactivePairing()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getIsRetroactivePairing())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setIsRetroactivePairing(SECOND_BOOL).build();
-        assertThat(prefs2.getIsRetroactivePairing()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSupportHidDevice() {
-        Preferences prefs =
-                Preferences.builder().setSupportHidDevice(FIRST_BOOL).build();
-        assertThat(prefs.getSupportHidDevice()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getSupportHidDevice())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setSupportHidDevice(SECOND_BOOL).build();
-        assertThat(prefs2.getSupportHidDevice()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEnablePairingWhileDirectlyConnecting() {
-        Preferences prefs =
-                Preferences.builder().setEnablePairingWhileDirectlyConnecting(FIRST_BOOL).build();
-        assertThat(prefs.getEnablePairingWhileDirectlyConnecting()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getEnablePairingWhileDirectlyConnecting())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setEnablePairingWhileDirectlyConnecting(SECOND_BOOL).build();
-        assertThat(prefs2.getEnablePairingWhileDirectlyConnecting()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testAcceptConsentForFastPairOne() {
-        Preferences prefs =
-                Preferences.builder().setAcceptConsentForFastPairOne(FIRST_BOOL).build();
-        assertThat(prefs.getAcceptConsentForFastPairOne()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getAcceptConsentForFastPairOne())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setAcceptConsentForFastPairOne(SECOND_BOOL).build();
-        assertThat(prefs2.getAcceptConsentForFastPairOne()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEnable128BitCustomGattCharacteristicsId() {
-        Preferences prefs =
-                Preferences.builder().setEnable128BitCustomGattCharacteristicsId(FIRST_BOOL)
-                        .build();
-        assertThat(prefs.getEnable128BitCustomGattCharacteristicsId()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getEnable128BitCustomGattCharacteristicsId())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setEnable128BitCustomGattCharacteristicsId(SECOND_BOOL)
-                        .build();
-        assertThat(prefs2.getEnable128BitCustomGattCharacteristicsId()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEnableSendExceptionStepToValidator() {
-        Preferences prefs =
-                Preferences.builder().setEnableSendExceptionStepToValidator(FIRST_BOOL).build();
-        assertThat(prefs.getEnableSendExceptionStepToValidator()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getEnableSendExceptionStepToValidator())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setEnableSendExceptionStepToValidator(SECOND_BOOL).build();
-        assertThat(prefs2.getEnableSendExceptionStepToValidator()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEnableAdditionalDataTypeWhenActionOverBle() {
-        Preferences prefs =
-                Preferences.builder().setEnableAdditionalDataTypeWhenActionOverBle(FIRST_BOOL)
-                        .build();
-        assertThat(prefs.getEnableAdditionalDataTypeWhenActionOverBle()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getEnableAdditionalDataTypeWhenActionOverBle())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setEnableAdditionalDataTypeWhenActionOverBle(SECOND_BOOL)
-                        .build();
-        assertThat(prefs2.getEnableAdditionalDataTypeWhenActionOverBle()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testCheckBondStateWhenSkipConnectingProfiles() {
-        Preferences prefs =
-                Preferences.builder().setCheckBondStateWhenSkipConnectingProfiles(FIRST_BOOL)
-                        .build();
-        assertThat(prefs.getCheckBondStateWhenSkipConnectingProfiles()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getCheckBondStateWhenSkipConnectingProfiles())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setCheckBondStateWhenSkipConnectingProfiles(SECOND_BOOL)
-                        .build();
-        assertThat(prefs2.getCheckBondStateWhenSkipConnectingProfiles()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHandlePasskeyConfirmationByUi() {
-        Preferences prefs =
-                Preferences.builder().setHandlePasskeyConfirmationByUi(FIRST_BOOL).build();
-        assertThat(prefs.getHandlePasskeyConfirmationByUi()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getHandlePasskeyConfirmationByUi())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setHandlePasskeyConfirmationByUi(SECOND_BOOL).build();
-        assertThat(prefs2.getHandlePasskeyConfirmationByUi()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEnablePairFlowShowUiWithoutProfileConnection() {
-        Preferences prefs =
-                Preferences.builder().setEnablePairFlowShowUiWithoutProfileConnection(FIRST_BOOL)
-                        .build();
-        assertThat(prefs.getEnablePairFlowShowUiWithoutProfileConnection()).isEqualTo(FIRST_BOOL);
-        assertThat(prefs.toBuilder().build().getEnablePairFlowShowUiWithoutProfileConnection())
-                .isEqualTo(FIRST_BOOL);
-
-        Preferences prefs2 =
-                Preferences.builder().setEnablePairFlowShowUiWithoutProfileConnection(SECOND_BOOL)
-                        .build();
-        assertThat(prefs2.getEnablePairFlowShowUiWithoutProfileConnection()).isEqualTo(SECOND_BOOL);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBrHandoverDataCharacteristicId() {
-        Preferences prefs =
-                Preferences.builder().setBrHandoverDataCharacteristicId(FIRST_SHORT).build();
-        assertThat(prefs.getBrHandoverDataCharacteristicId()).isEqualTo(FIRST_SHORT);
-        assertThat(prefs.toBuilder().build().getBrHandoverDataCharacteristicId())
-                .isEqualTo(FIRST_SHORT);
-
-        Preferences prefs2 =
-                Preferences.builder().setBrHandoverDataCharacteristicId(SECOND_SHORT).build();
-        assertThat(prefs2.getBrHandoverDataCharacteristicId()).isEqualTo(SECOND_SHORT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothSigDataCharacteristicId() {
-        Preferences prefs =
-                Preferences.builder().setBluetoothSigDataCharacteristicId(FIRST_SHORT).build();
-        assertThat(prefs.getBluetoothSigDataCharacteristicId()).isEqualTo(FIRST_SHORT);
-        assertThat(prefs.toBuilder().build().getBluetoothSigDataCharacteristicId())
-                .isEqualTo(FIRST_SHORT);
-
-        Preferences prefs2 =
-                Preferences.builder().setBluetoothSigDataCharacteristicId(SECOND_SHORT).build();
-        assertThat(prefs2.getBluetoothSigDataCharacteristicId()).isEqualTo(SECOND_SHORT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testFirmwareVersionCharacteristicId() {
-        Preferences prefs =
-                Preferences.builder().setFirmwareVersionCharacteristicId(FIRST_SHORT).build();
-        assertThat(prefs.getFirmwareVersionCharacteristicId()).isEqualTo(FIRST_SHORT);
-        assertThat(prefs.toBuilder().build().getFirmwareVersionCharacteristicId())
-                .isEqualTo(FIRST_SHORT);
-
-        Preferences prefs2 =
-                Preferences.builder().setFirmwareVersionCharacteristicId(SECOND_SHORT).build();
-        assertThat(prefs2.getFirmwareVersionCharacteristicId()).isEqualTo(SECOND_SHORT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBrTransportBlockDataDescriptorId() {
-        Preferences prefs =
-                Preferences.builder().setBrTransportBlockDataDescriptorId(FIRST_SHORT).build();
-        assertThat(prefs.getBrTransportBlockDataDescriptorId()).isEqualTo(FIRST_SHORT);
-        assertThat(prefs.toBuilder().build().getBrTransportBlockDataDescriptorId())
-                .isEqualTo(FIRST_SHORT);
-
-        Preferences prefs2 =
-                Preferences.builder().setBrTransportBlockDataDescriptorId(SECOND_SHORT).build();
-        assertThat(prefs2.getBrTransportBlockDataDescriptorId()).isEqualTo(SECOND_SHORT);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGattConnectShortTimeoutMs() {
-        Preferences prefs =
-                Preferences.builder().setGattConnectShortTimeoutMs(FIRST_LONG).build();
-        assertThat(prefs.getGattConnectShortTimeoutMs()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getGattConnectShortTimeoutMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setGattConnectShortTimeoutMs(SECOND_LONG).build();
-        assertThat(prefs2.getGattConnectShortTimeoutMs()).isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGattConnectLongTimeoutMs() {
-        Preferences prefs =
-                Preferences.builder().setGattConnectLongTimeoutMs(FIRST_LONG).build();
-        assertThat(prefs.getGattConnectLongTimeoutMs()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getGattConnectLongTimeoutMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setGattConnectLongTimeoutMs(SECOND_LONG).build();
-        assertThat(prefs2.getGattConnectLongTimeoutMs()).isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGattConnectShortTimeoutRetryMaxSpentTimeMs() {
-        Preferences prefs =
-                Preferences.builder().setGattConnectShortTimeoutRetryMaxSpentTimeMs(FIRST_LONG)
-                        .build();
-        assertThat(prefs.getGattConnectShortTimeoutRetryMaxSpentTimeMs()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getGattConnectShortTimeoutRetryMaxSpentTimeMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setGattConnectShortTimeoutRetryMaxSpentTimeMs(SECOND_LONG)
-                        .build();
-        assertThat(prefs2.getGattConnectShortTimeoutRetryMaxSpentTimeMs()).isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testAddressRotateRetryMaxSpentTimeMs() {
-        Preferences prefs =
-                Preferences.builder().setAddressRotateRetryMaxSpentTimeMs(FIRST_LONG).build();
-        assertThat(prefs.getAddressRotateRetryMaxSpentTimeMs()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getAddressRotateRetryMaxSpentTimeMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setAddressRotateRetryMaxSpentTimeMs(SECOND_LONG).build();
-        assertThat(prefs2.getAddressRotateRetryMaxSpentTimeMs()).isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testPairingRetryDelayMs() {
-        Preferences prefs =
-                Preferences.builder().setPairingRetryDelayMs(FIRST_LONG).build();
-        assertThat(prefs.getPairingRetryDelayMs()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getPairingRetryDelayMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setPairingRetryDelayMs(SECOND_LONG).build();
-        assertThat(prefs2.getPairingRetryDelayMs()).isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSecretHandshakeShortTimeoutMs() {
-        Preferences prefs =
-                Preferences.builder().setSecretHandshakeShortTimeoutMs(FIRST_LONG).build();
-        assertThat(prefs.getSecretHandshakeShortTimeoutMs()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getSecretHandshakeShortTimeoutMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setSecretHandshakeShortTimeoutMs(SECOND_LONG).build();
-        assertThat(prefs2.getSecretHandshakeShortTimeoutMs()).isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSecretHandshakeLongTimeoutMs() {
-        Preferences prefs =
-                Preferences.builder().setSecretHandshakeLongTimeoutMs(FIRST_LONG).build();
-        assertThat(prefs.getSecretHandshakeLongTimeoutMs()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getSecretHandshakeLongTimeoutMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setSecretHandshakeLongTimeoutMs(SECOND_LONG).build();
-        assertThat(prefs2.getSecretHandshakeLongTimeoutMs()).isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSecretHandshakeShortTimeoutRetryMaxSpentTimeMs() {
-        Preferences prefs =
-                Preferences.builder().setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(FIRST_LONG)
-                        .build();
-        assertThat(prefs.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(SECOND_LONG)
-                        .build();
-        assertThat(prefs2.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs())
-                .isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSecretHandshakeLongTimeoutRetryMaxSpentTimeMs() {
-        Preferences prefs =
-                Preferences.builder().setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(FIRST_LONG)
-                        .build();
-        assertThat(prefs.getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(SECOND_LONG)
-                        .build();
-        assertThat(prefs2.getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs())
-                .isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSecretHandshakeRetryAttempts() {
-        Preferences prefs =
-                Preferences.builder().setSecretHandshakeRetryAttempts(FIRST_LONG).build();
-        assertThat(prefs.getSecretHandshakeRetryAttempts()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getSecretHandshakeRetryAttempts())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setSecretHandshakeRetryAttempts(SECOND_LONG).build();
-        assertThat(prefs2.getSecretHandshakeRetryAttempts()).isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSecretHandshakeRetryGattConnectionMaxSpentTimeMs() {
-        Preferences prefs =
-                Preferences.builder()
-                        .setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(FIRST_LONG).build();
-        assertThat(prefs.getSecretHandshakeRetryGattConnectionMaxSpentTimeMs())
-                .isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getSecretHandshakeRetryGattConnectionMaxSpentTimeMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(
-                        SECOND_LONG).build();
-        assertThat(prefs2.getSecretHandshakeRetryGattConnectionMaxSpentTimeMs())
-                .isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSignalLostRetryMaxSpentTimeMs() {
-        Preferences prefs =
-                Preferences.builder().setSignalLostRetryMaxSpentTimeMs(FIRST_LONG).build();
-        assertThat(prefs.getSignalLostRetryMaxSpentTimeMs()).isEqualTo(FIRST_LONG);
-        assertThat(prefs.toBuilder().build().getSignalLostRetryMaxSpentTimeMs())
-                .isEqualTo(FIRST_LONG);
-
-        Preferences prefs2 =
-                Preferences.builder().setSignalLostRetryMaxSpentTimeMs(SECOND_LONG).build();
-        assertThat(prefs2.getSignalLostRetryMaxSpentTimeMs()).isEqualTo(SECOND_LONG);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testCachedDeviceAddress() {
-        Preferences prefs =
-                Preferences.builder().setCachedDeviceAddress(FIRST_STRING).build();
-        assertThat(prefs.getCachedDeviceAddress()).isEqualTo(FIRST_STRING);
-        assertThat(prefs.toBuilder().build().getCachedDeviceAddress())
-                .isEqualTo(FIRST_STRING);
-
-        Preferences prefs2 =
-                Preferences.builder().setCachedDeviceAddress(SECOND_STRING).build();
-        assertThat(prefs2.getCachedDeviceAddress()).isEqualTo(SECOND_STRING);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testPossibleCachedDeviceAddress() {
-        Preferences prefs =
-                Preferences.builder().setPossibleCachedDeviceAddress(FIRST_STRING).build();
-        assertThat(prefs.getPossibleCachedDeviceAddress()).isEqualTo(FIRST_STRING);
-        assertThat(prefs.toBuilder().build().getPossibleCachedDeviceAddress())
-                .isEqualTo(FIRST_STRING);
-
-        Preferences prefs2 =
-                Preferences.builder().setPossibleCachedDeviceAddress(SECOND_STRING).build();
-        assertThat(prefs2.getPossibleCachedDeviceAddress()).isEqualTo(SECOND_STRING);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSupportedProfileUuids() {
-        Preferences prefs =
-                Preferences.builder().setSupportedProfileUuids(FIRST_BYTES).build();
-        assertThat(prefs.getSupportedProfileUuids()).isEqualTo(FIRST_BYTES);
-        assertThat(prefs.toBuilder().build().getSupportedProfileUuids())
-                .isEqualTo(FIRST_BYTES);
-
-        Preferences prefs2 =
-                Preferences.builder().setSupportedProfileUuids(SECOND_BYTES).build();
-        assertThat(prefs2.getSupportedProfileUuids()).isEqualTo(SECOND_BYTES);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGattConnectionAndSecretHandshakeNoRetryGattError() {
-        Preferences prefs =
-                Preferences.builder().setGattConnectionAndSecretHandshakeNoRetryGattError(
-                        FIRST_INT_SETS).build();
-        assertThat(prefs.getGattConnectionAndSecretHandshakeNoRetryGattError())
-                .isEqualTo(FIRST_INT_SETS);
-        assertThat(prefs.toBuilder().build().getGattConnectionAndSecretHandshakeNoRetryGattError())
-                .isEqualTo(FIRST_INT_SETS);
-
-        Preferences prefs2 =
-                Preferences.builder().setGattConnectionAndSecretHandshakeNoRetryGattError(
-                        SECOND_INT_SETS).build();
-        assertThat(prefs2.getGattConnectionAndSecretHandshakeNoRetryGattError())
-                .isEqualTo(SECOND_INT_SETS);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testExtraLoggingInformation() {
-        Preferences prefs =
-                Preferences.builder().setExtraLoggingInformation(FIRST_EXTRA_LOGGING_INFO).build();
-        Preferences prefs2 =
-                Preferences.builder().setExtraLoggingInformation(SECOND_EXTRA_LOGGING_INFO).build();
-        Preferences prefs3 =
-                Preferences.builder().setExtraLoggingInformation(FIRST_EXTRA_LOGGING_INFO).build();
-
-        assertThat(prefs.getExtraLoggingInformation()).isEqualTo(FIRST_EXTRA_LOGGING_INFO);
-        assertThat(prefs2.getExtraLoggingInformation()).isEqualTo(SECOND_EXTRA_LOGGING_INFO);
-        assertThat(prefs.toBuilder().build().getExtraLoggingInformation())
-                .isEqualTo(FIRST_EXTRA_LOGGING_INFO);
-        // Test equal()
-        assertThat(prefs.getExtraLoggingInformation().equals(null)).isFalse();
-        assertThat(prefs.getExtraLoggingInformation()
-                .equals(prefs2.getExtraLoggingInformation())).isFalse();
-        assertThat(prefs.getExtraLoggingInformation()
-                .equals(prefs3.getExtraLoggingInformation())).isTrue();
-        // Test getModelId()
-        assertThat(prefs.getExtraLoggingInformation().getModelId())
-                .isEqualTo(prefs3.getExtraLoggingInformation().getModelId());
-        assertThat(prefs.getExtraLoggingInformation().getModelId())
-                .isNotEqualTo(prefs2.getExtraLoggingInformation().getModelId());
-        // Test hashCode()
-        assertThat(prefs.getExtraLoggingInformation().hashCode())
-                .isEqualTo(prefs3.getExtraLoggingInformation().hashCode());
-        assertThat(prefs.getExtraLoggingInformation().hashCode())
-                .isNotEqualTo(prefs2.getExtraLoggingInformation().hashCode());
-        // Test toBuilder()
-
-        assertThat(prefs.getExtraLoggingInformation()
-                .toBuilder().setModelId("000007").build().getModelId())
-                .isEqualTo("000007");
-        // Test toString()
-        assertThat(prefs.getExtraLoggingInformation().toString())
-                .isEqualTo("ExtraLoggingInformation{modelId=000006}");
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TdsExceptionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TdsExceptionTest.java
deleted file mode 100644
index e8b9480..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TdsExceptionTest.java
+++ /dev/null
@@ -1,34 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.android.server.nearby.intdefs.FastPairEventIntDefs;
-
-import org.junit.Test;
-
-public class TdsExceptionTest {
-    @Test
-    public void testGetErrorCode() {
-        TdsException tdsException = new TdsException(
-                FastPairEventIntDefs.BrEdrHandoverErrorCode.BLUETOOTH_MAC_INVALID, "format");
-        assertThat(tdsException.getErrorCode())
-                .isEqualTo(FastPairEventIntDefs.BrEdrHandoverErrorCode.BLUETOOTH_MAC_INVALID);
-
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TimingLoggerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TimingLoggerTest.java
deleted file mode 100644
index 4672905..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TimingLoggerTest.java
+++ /dev/null
@@ -1,218 +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.server.nearby.common.bluetooth.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.SystemClock;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming;
-import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.Timing;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link TimingLogger}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TimingLoggerTest {
-
-    private final Preferences mPrefs = Preferences.builder().setEvaluatePerformance(true).build();
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void logPairedTiming() {
-        String label = "start";
-        TimingLogger timingLogger = new TimingLogger("paired", mPrefs);
-        timingLogger.start(label);
-        SystemClock.sleep(1000);
-        timingLogger.end();
-
-        assertThat(timingLogger.getTimings()).hasSize(2);
-
-        // Calculate execution time and only store result at "start" timing.
-        // Expected output:
-        // <pre>
-        //  I/FastPair: paired [Exclusive time] / [Total time]
-        //  I/FastPair:   start 1000ms
-        //  I/FastPair: paired end, 1000ms
-        // </pre>
-        timingLogger.dump();
-
-        assertPairedTiming(label, timingLogger.getTimings().get(0),
-                timingLogger.getTimings().get(1));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void logScopedTiming() {
-        String label = "scopedTiming";
-        TimingLogger timingLogger = new TimingLogger("scoped", mPrefs);
-        try (ScopedTiming scopedTiming = new ScopedTiming(timingLogger, label)) {
-            SystemClock.sleep(1000);
-        }
-
-        assertThat(timingLogger.getTimings()).hasSize(2);
-
-        // Calculate execution time and only store result at "start" timings.
-        // Expected output:
-        // <pre>
-        //  I/FastPair: scoped [Exclusive time] / [Total time]
-        //  I/FastPair:   scopedTiming 1000ms
-        //  I/FastPair: scoped end, 1000ms
-        // </pre>
-        timingLogger.dump();
-
-        assertPairedTiming(label, timingLogger.getTimings().get(0),
-                timingLogger.getTimings().get(1));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void logOrderedTiming() {
-        String label1 = "t1";
-        String label2 = "t2";
-        TimingLogger timingLogger = new TimingLogger("ordered", mPrefs);
-        try (ScopedTiming t1 = new ScopedTiming(timingLogger, label1)) {
-            SystemClock.sleep(1000);
-        }
-        try (ScopedTiming t2 = new ScopedTiming(timingLogger, label2)) {
-            SystemClock.sleep(1000);
-        }
-
-        assertThat(timingLogger.getTimings()).hasSize(4);
-
-        // Calculate execution time and only store result at "start" timings.
-        // Expected output:
-        // <pre>
-        //  I/FastPair: ordered [Exclusive time] / [Total time]
-        //  I/FastPair:   t1 1000ms
-        //  I/FastPair:   t2 1000ms
-        //  I/FastPair: ordered end, 2000ms
-        // </pre>
-        timingLogger.dump();
-
-        // We expect get timings in this order: t1 start, t1 end, t2 start, t2 end.
-        Timing start1 = timingLogger.getTimings().get(0);
-        Timing end1 = timingLogger.getTimings().get(1);
-        Timing start2 = timingLogger.getTimings().get(2);
-        Timing end2 = timingLogger.getTimings().get(3);
-
-        // Verify the paired timings.
-        assertPairedTiming(label1, start1, end1);
-        assertPairedTiming(label2, start2, end2);
-
-        // Verify the order and total time.
-        assertOrderedTiming(start1, start2);
-        assertThat(start1.getExclusiveTime() + start2.getExclusiveTime())
-                .isEqualTo(timingLogger.getTotalTime());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void logNestedTiming() {
-        String labelOuter = "outer";
-        String labelInner1 = "inner1";
-        String labelInner1Inner1 = "inner1inner1";
-        String labelInner2 = "inner2";
-        TimingLogger timingLogger = new TimingLogger("nested", mPrefs);
-        try (ScopedTiming outer = new ScopedTiming(timingLogger, labelOuter)) {
-            SystemClock.sleep(1000);
-            try (ScopedTiming inner1 = new ScopedTiming(timingLogger, labelInner1)) {
-                SystemClock.sleep(1000);
-                try (ScopedTiming inner1inner1 = new ScopedTiming(timingLogger,
-                        labelInner1Inner1)) {
-                    SystemClock.sleep(1000);
-                }
-            }
-            try (ScopedTiming inner2 = new ScopedTiming(timingLogger, labelInner2)) {
-                SystemClock.sleep(1000);
-            }
-        }
-
-        assertThat(timingLogger.getTimings()).hasSize(8);
-
-        // Calculate execution time and only store result at "start" timing.
-        // Expected output:
-        // <pre>
-        //  I/FastPair: nested [Exclusive time] / [Total time]
-        //  I/FastPair:   outer 1000ms / 4000ms
-        //  I/FastPair:     inner1 1000ms / 2000ms
-        //  I/FastPair:       inner1inner1 1000ms
-        //  I/FastPair:     inner2 1000ms
-        //  I/FastPair: nested end, 4000ms
-        // </pre>
-        timingLogger.dump();
-
-        // We expect get timings in this order: outer start, inner1 start, inner1inner1 start,
-        // inner1inner1 end, inner1 end, inner2 start, inner2 end, outer end.
-        Timing startOuter = timingLogger.getTimings().get(0);
-        Timing startInner1 = timingLogger.getTimings().get(1);
-        Timing startInner1Inner1 = timingLogger.getTimings().get(2);
-        Timing endInner1Inner1 = timingLogger.getTimings().get(3);
-        Timing endInner1 = timingLogger.getTimings().get(4);
-        Timing startInner2 = timingLogger.getTimings().get(5);
-        Timing endInner2 = timingLogger.getTimings().get(6);
-        Timing endOuter = timingLogger.getTimings().get(7);
-
-        // Verify the paired timings.
-        assertPairedTiming(labelOuter, startOuter, endOuter);
-        assertPairedTiming(labelInner1, startInner1, endInner1);
-        assertPairedTiming(labelInner1Inner1, startInner1Inner1, endInner1Inner1);
-        assertPairedTiming(labelInner2, startInner2, endInner2);
-
-        // Verify the order and total time.
-        assertOrderedTiming(startOuter, startInner1);
-        assertOrderedTiming(startInner1, startInner1Inner1);
-        assertOrderedTiming(startInner1Inner1, startInner2);
-        assertThat(
-                startOuter.getExclusiveTime() + startInner1.getTotalTime() + startInner2
-                        .getTotalTime())
-                .isEqualTo(timingLogger.getTotalTime());
-
-        // Verify the nested execution time.
-        assertThat(startInner1Inner1.getTotalTime()).isAtMost(startInner1.getTotalTime());
-        assertThat(startInner1.getTotalTime() + startInner2.getTotalTime())
-                .isAtMost(startOuter.getTotalTime());
-    }
-
-    private void assertPairedTiming(String label, Timing start, Timing end) {
-        assertThat(start.isStartTiming()).isTrue();
-        assertThat(start.getName()).isEqualTo(label);
-        assertThat(end.isEndTiming()).isTrue();
-        assertThat(end.getTimestamp()).isAtLeast(start.getTimestamp());
-
-        assertThat(start.getExclusiveTime() > 0).isTrue();
-        assertThat(start.getTotalTime()).isAtLeast(start.getExclusiveTime());
-        assertThat(end.getExclusiveTime() == 0).isTrue();
-        assertThat(end.getTotalTime() == 0).isTrue();
-    }
-
-    private void assertOrderedTiming(Timing t1, Timing t2) {
-        assertThat(t1.isStartTiming()).isTrue();
-        assertThat(t2.isStartTiming()).isTrue();
-        assertThat(t2.getTimestamp()).isAtLeast(t1.getTimestamp());
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnectionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnectionTest.java
deleted file mode 100644
index 80bde63..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnectionTest.java
+++ /dev/null
@@ -1,848 +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.server.nearby.common.bluetooth.gatt;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isA;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothStatusCodes;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-
-import androidx.test.filters.SdkSuppress;
-
-import com.android.server.nearby.common.bluetooth.BluetoothConsts;
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.ReservedUuids;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.OperationType;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattWrapper;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.SynchronousOperation;
-
-import junit.framework.TestCase;
-
-import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatchers;
-import org.mockito.Captor;
-import org.mockito.Mock;
-
-import java.util.Arrays;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Unit tests for {@link BluetoothGattConnection}.
- */
-public class BluetoothGattConnectionTest extends TestCase {
-
-    private static final UUID SERVICE_UUID = UUID.randomUUID();
-    private static final UUID CHARACTERISTIC_UUID = UUID.randomUUID();
-    private static final UUID DESCRIPTOR_UUID = UUID.randomUUID();
-    private static final byte[] DATA = "data".getBytes();
-    private static final int RSSI = -63;
-    private static final int CONNECTION_PRIORITY = 128;
-    private static final int MTU_REQUEST = 512;
-    private static final BluetoothGattHelper.ConnectionOptions CONNECTION_OPTIONS =
-            BluetoothGattHelper.ConnectionOptions.builder().build();
-
-    @Mock
-    private BluetoothGattWrapper mMockBluetoothGattWrapper;
-    @Mock
-    private BluetoothDevice mMockBluetoothDevice;
-    @Mock
-    private BluetoothOperationExecutor mMockBluetoothOperationExecutor;
-    @Mock
-    private BluetoothGattService mMockBluetoothGattService;
-    @Mock
-    private BluetoothGattService mMockBluetoothGattService2;
-    @Mock
-    private BluetoothGattCharacteristic mMockBluetoothGattCharacteristic;
-    @Mock
-    private BluetoothGattCharacteristic mMockBluetoothGattCharacteristic2;
-    @Mock
-    private BluetoothGattDescriptor mMockBluetoothGattDescriptor;
-    @Mock
-    private BluetoothGattConnection.CharacteristicChangeListener mMockCharChangeListener;
-    @Mock
-    private BluetoothGattConnection.ChangeObserver mMockChangeObserver;
-    @Mock
-    private BluetoothGattConnection.ConnectionCloseListener mMockConnectionCloseListener;
-
-    @Captor
-    private ArgumentCaptor<Operation<?>> mOperationCaptor;
-    @Captor
-    private ArgumentCaptor<SynchronousOperation<?>> mSynchronousOperationCaptor;
-    @Captor
-    private ArgumentCaptor<BluetoothGattCharacteristic> mCharacteristicCaptor;
-    @Captor
-    private ArgumentCaptor<BluetoothGattDescriptor> mDescriptorCaptor;
-
-    private BluetoothGattConnection mBluetoothGattConnection;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        initMocks(this);
-
-        mBluetoothGattConnection = new BluetoothGattConnection(
-                mMockBluetoothGattWrapper,
-                mMockBluetoothOperationExecutor,
-                CONNECTION_OPTIONS);
-        mBluetoothGattConnection.onConnected();
-
-        when(mMockBluetoothGattWrapper.getDevice()).thenReturn(mMockBluetoothDevice);
-        when(mMockBluetoothGattWrapper.discoverServices()).thenReturn(true);
-        when(mMockBluetoothGattWrapper.refresh()).thenReturn(true);
-        when(mMockBluetoothGattWrapper.readCharacteristic(mMockBluetoothGattCharacteristic))
-                .thenReturn(true);
-        when(mMockBluetoothGattWrapper
-                .writeCharacteristic(ArgumentMatchers.<BluetoothGattCharacteristic>any(), any(),
-                        anyInt()))
-                .thenReturn(BluetoothStatusCodes.SUCCESS);
-        when(mMockBluetoothGattWrapper.readDescriptor(mMockBluetoothGattDescriptor))
-                .thenReturn(true);
-        when(mMockBluetoothGattWrapper.writeDescriptor(
-                ArgumentMatchers.<BluetoothGattDescriptor>any(), any()))
-                .thenReturn(BluetoothStatusCodes.SUCCESS);
-        when(mMockBluetoothGattWrapper.readRemoteRssi()).thenReturn(true);
-        when(mMockBluetoothGattWrapper.requestConnectionPriority(CONNECTION_PRIORITY))
-                .thenReturn(true);
-        when(mMockBluetoothGattWrapper.requestMtu(MTU_REQUEST)).thenReturn(true);
-        when(mMockBluetoothGattWrapper.getServices())
-                .thenReturn(Arrays.asList(mMockBluetoothGattService));
-        when(mMockBluetoothGattService.getUuid()).thenReturn(SERVICE_UUID);
-        when(mMockBluetoothGattService.getCharacteristics())
-                .thenReturn(Arrays.asList(mMockBluetoothGattCharacteristic));
-        when(mMockBluetoothGattCharacteristic.getUuid()).thenReturn(CHARACTERISTIC_UUID);
-        when(mMockBluetoothGattCharacteristic.getProperties())
-                .thenReturn(
-                        BluetoothGattCharacteristic.PROPERTY_NOTIFY
-                                | BluetoothGattCharacteristic.PROPERTY_WRITE);
-        BluetoothGattDescriptor clientConfigDescriptor =
-                new BluetoothGattDescriptor(
-                        ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION,
-                        BluetoothGattDescriptor.PERMISSION_WRITE);
-        when(mMockBluetoothGattCharacteristic.getDescriptor(
-                ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION))
-                .thenReturn(clientConfigDescriptor);
-        when(mMockBluetoothGattCharacteristic.getDescriptors())
-                .thenReturn(Arrays.asList(mMockBluetoothGattDescriptor, clientConfigDescriptor));
-        when(mMockBluetoothGattDescriptor.getUuid()).thenReturn(DESCRIPTOR_UUID);
-        when(mMockBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getDevice() {
-        BluetoothDevice result = mBluetoothGattConnection.getDevice();
-
-        assertThat(result).isEqualTo(mMockBluetoothDevice);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getConnectionOptions() {
-        BluetoothGattHelper.ConnectionOptions result = mBluetoothGattConnection
-                .getConnectionOptions();
-
-        assertThat(result).isSameInstanceAs(CONNECTION_OPTIONS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_isConnected_false_beforeConnection() {
-        mBluetoothGattConnection = new BluetoothGattConnection(
-                mMockBluetoothGattWrapper,
-                mMockBluetoothOperationExecutor,
-                CONNECTION_OPTIONS);
-
-        boolean result = mBluetoothGattConnection.isConnected();
-
-        assertThat(result).isFalse();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_isConnected_true_afterConnection() {
-        boolean result = mBluetoothGattConnection.isConnected();
-
-        assertThat(result).isTrue();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_isConnected_false_afterDisconnection() {
-        mBluetoothGattConnection.onClosed();
-
-        boolean result = mBluetoothGattConnection.isConnected();
-
-        assertThat(result).isFalse();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getService_notDiscovered() throws Exception {
-        BluetoothGattService result = mBluetoothGattConnection.getService(SERVICE_UUID);
-        verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
-        mSynchronousOperationCaptor.getValue().call();
-        verify(mMockBluetoothOperationExecutor)
-                .execute(
-                        mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
-        mOperationCaptor.getValue().run();
-
-        assertThat(result).isEqualTo(mMockBluetoothGattService);
-        verify(mMockBluetoothGattWrapper).discoverServices();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getService_alreadyDiscovered() throws Exception {
-        mBluetoothGattConnection.getService(SERVICE_UUID);
-        verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
-        mSynchronousOperationCaptor.getValue().call();
-        reset(mMockBluetoothOperationExecutor);
-
-        BluetoothGattService result = mBluetoothGattConnection.getService(SERVICE_UUID);
-
-        assertThat(result).isEqualTo(mMockBluetoothGattService);
-        // Verify that service discovery has been done only once
-        verifyNoMoreInteractions(mMockBluetoothOperationExecutor);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getService_notFound() throws Exception {
-        when(mMockBluetoothGattWrapper.getServices()).thenReturn(
-                Arrays.<BluetoothGattService>asList());
-
-        try {
-            mBluetoothGattConnection.getService(SERVICE_UUID);
-            fail("Expected BluetoothException");
-        } catch (BluetoothException expected) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getService_moreThanOne() throws Exception {
-        when(mMockBluetoothGattWrapper.getServices())
-                .thenReturn(Arrays.asList(mMockBluetoothGattService, mMockBluetoothGattService));
-
-        try {
-            mBluetoothGattConnection.getService(SERVICE_UUID);
-            fail("Expected BluetoothException");
-        } catch (BluetoothException expected) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getCharacteristic() throws Exception {
-        BluetoothGattCharacteristic result =
-                mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
-
-        assertThat(result).isEqualTo(mMockBluetoothGattCharacteristic);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getCharacteristic_notFound() throws Exception {
-        when(mMockBluetoothGattService.getCharacteristics())
-                .thenReturn(Arrays.<BluetoothGattCharacteristic>asList());
-
-        try {
-            mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
-            fail("Expected BluetoothException");
-        } catch (BluetoothException expected) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getCharacteristic_moreThanOne() throws Exception {
-        when(mMockBluetoothGattService.getCharacteristics())
-                .thenReturn(
-                        Arrays.asList(mMockBluetoothGattCharacteristic,
-                                mMockBluetoothGattCharacteristic));
-
-        try {
-            mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
-            fail("Expected BluetoothException");
-        } catch (BluetoothException expected) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getCharacteristic_moreThanOneService() throws Exception {
-        // Add a new service with the same service UUID as our existing one, but add a different
-        // characteristic inside of it.
-        when(mMockBluetoothGattWrapper.getServices())
-                .thenReturn(Arrays.asList(mMockBluetoothGattService, mMockBluetoothGattService2));
-        when(mMockBluetoothGattService2.getUuid()).thenReturn(SERVICE_UUID);
-        when(mMockBluetoothGattService2.getCharacteristics())
-                .thenReturn(Arrays.asList(mMockBluetoothGattCharacteristic2));
-        when(mMockBluetoothGattCharacteristic2.getUuid())
-                .thenReturn(
-                        new UUID(
-                                CHARACTERISTIC_UUID.getMostSignificantBits(),
-                                CHARACTERISTIC_UUID.getLeastSignificantBits() + 1));
-        when(mMockBluetoothGattCharacteristic2.getProperties())
-                .thenReturn(
-                        BluetoothGattCharacteristic.PROPERTY_NOTIFY
-                                | BluetoothGattCharacteristic.PROPERTY_WRITE);
-
-        mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getDescriptor() throws Exception {
-        when(mMockBluetoothGattCharacteristic.getDescriptors())
-                .thenReturn(Arrays.asList(mMockBluetoothGattDescriptor));
-
-        BluetoothGattDescriptor result =
-                mBluetoothGattConnection
-                        .getDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
-
-        assertThat(result).isEqualTo(mMockBluetoothGattDescriptor);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getDescriptor_notFound() throws Exception {
-        when(mMockBluetoothGattCharacteristic.getDescriptors())
-                .thenReturn(Arrays.<BluetoothGattDescriptor>asList());
-
-        try {
-            mBluetoothGattConnection
-                    .getDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
-            fail("Expected BluetoothException");
-        } catch (BluetoothException expected) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getDescriptor_moreThanOne() throws Exception {
-        when(mMockBluetoothGattCharacteristic.getDescriptors())
-                .thenReturn(
-                        Arrays.asList(mMockBluetoothGattDescriptor, mMockBluetoothGattDescriptor));
-
-        try {
-            mBluetoothGattConnection
-                    .getDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
-            fail("Expected BluetoothException");
-        } catch (BluetoothException expected) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_discoverServices() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new SynchronousOperation<>(
-                        mMockBluetoothOperationExecutor, OperationType.NOTIFICATION_CHANGE)))
-                .thenReturn(mMockChangeObserver);
-
-        mBluetoothGattConnection.discoverServices();
-
-        verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
-        mSynchronousOperationCaptor.getValue().call();
-        verify(mMockBluetoothOperationExecutor)
-                .execute(
-                        mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).discoverServices();
-        verify(mMockBluetoothGattWrapper, never()).refresh();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_discoverServices_serviceChange() throws Exception {
-        when(mMockBluetoothGattWrapper.getService(ReservedUuids.Services.GENERIC_ATTRIBUTE))
-                .thenReturn(mMockBluetoothGattService);
-        when(mMockBluetoothGattService
-                .getCharacteristic(ReservedUuids.Characteristics.SERVICE_CHANGE))
-                .thenReturn(mMockBluetoothGattCharacteristic);
-
-        mBluetoothGattConnection.discoverServices();
-
-        verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
-        mSynchronousOperationCaptor.getValue().call();
-        verify(mMockBluetoothOperationExecutor, times(2))
-                .execute(
-                        mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
-        verify(mMockBluetoothGattWrapper).refresh();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_discoverServices_SelfDefinedServiceDynamic() throws Exception {
-        when(mMockBluetoothGattWrapper.getService(BluetoothConsts.SERVICE_DYNAMIC_SERVICE))
-                .thenReturn(mMockBluetoothGattService);
-        when(mMockBluetoothGattService
-                .getCharacteristic(BluetoothConsts.SERVICE_DYNAMIC_CHARACTERISTIC))
-                .thenReturn(mMockBluetoothGattCharacteristic);
-
-        mBluetoothGattConnection.discoverServices();
-
-        verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
-        mSynchronousOperationCaptor.getValue().call();
-        verify(mMockBluetoothOperationExecutor, times(2))
-                .execute(
-                        mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
-        verify(mMockBluetoothGattWrapper).refresh();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_discoverServices_refreshWithGattErrorOnMncAbove() throws Exception {
-        if (VERSION.SDK_INT <= VERSION_CODES.LOLLIPOP_MR1) {
-            return;
-        }
-        mBluetoothGattConnection.discoverServices();
-        verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
-
-        doThrow(new BluetoothGattException("fail", BluetoothGattConnection.GATT_ERROR))
-                .doReturn(null)
-                .when(mMockBluetoothOperationExecutor)
-                .execute(isA(Operation.class),
-                        eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
-        mSynchronousOperationCaptor.getValue().call();
-        verify(mMockBluetoothOperationExecutor, times(2))
-                .execute(
-                        mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
-        verify(mMockBluetoothGattWrapper).refresh();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_discoverServices_refreshWithGattInternalErrorOnMncAbove() throws Exception {
-        if (VERSION.SDK_INT <= VERSION_CODES.LOLLIPOP_MR1) {
-            return;
-        }
-        mBluetoothGattConnection.discoverServices();
-        verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
-
-        doThrow(new BluetoothGattException("fail", BluetoothGattConnection.GATT_INTERNAL_ERROR))
-                .doReturn(null)
-                .when(mMockBluetoothOperationExecutor)
-                .execute(isA(Operation.class),
-                        eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
-        mSynchronousOperationCaptor.getValue().call();
-        verify(mMockBluetoothOperationExecutor, times(2))
-                .execute(
-                        mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
-        verify(mMockBluetoothGattWrapper).refresh();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_discoverServices_dynamicServices_notBonded() throws Exception {
-        when(mMockBluetoothGattWrapper.getService(ReservedUuids.Services.GENERIC_ATTRIBUTE))
-                .thenReturn(mMockBluetoothGattService);
-        when(mMockBluetoothGattService
-                .getCharacteristic(ReservedUuids.Characteristics.SERVICE_CHANGE))
-                .thenReturn(mMockBluetoothGattCharacteristic);
-        when(mMockBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
-
-        mBluetoothGattConnection.discoverServices();
-
-        verify(mMockBluetoothGattWrapper, never()).refresh();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_readCharacteristic() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new Operation<byte[]>(
-                        OperationType.READ_CHARACTERISTIC,
-                        mMockBluetoothGattWrapper,
-                        mMockBluetoothGattCharacteristic),
-                BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
-                .thenReturn(DATA);
-
-        byte[] result = mBluetoothGattConnection
-                .readCharacteristic(mMockBluetoothGattCharacteristic);
-
-        assertThat(result).isEqualTo(DATA);
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mOperationCaptor.capture(), anyLong());
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).readCharacteristic(mMockBluetoothGattCharacteristic);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_readCharacteristic_by_uuid() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new Operation<byte[]>(
-                        OperationType.READ_CHARACTERISTIC,
-                        mMockBluetoothGattWrapper,
-                        mMockBluetoothGattCharacteristic),
-                BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
-                .thenReturn(DATA);
-
-        byte[] result = mBluetoothGattConnection
-                .readCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
-
-        assertThat(result).isEqualTo(DATA);
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mOperationCaptor.capture(), anyLong());
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).readCharacteristic(mMockBluetoothGattCharacteristic);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_writeCharacteristic() throws Exception {
-        BluetoothGattCharacteristic characteristic =
-                new BluetoothGattCharacteristic(
-                        CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE, 0);
-        mBluetoothGattConnection.writeCharacteristic(characteristic, DATA);
-
-        verify(mMockBluetoothOperationExecutor)
-                .execute(mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).writeCharacteristic(mCharacteristicCaptor.capture(),
-                eq(DATA), eq(characteristic.getWriteType()));
-        BluetoothGattCharacteristic writtenCharacteristic = mCharacteristicCaptor.getValue();
-        assertThat(writtenCharacteristic.getUuid()).isEqualTo(CHARACTERISTIC_UUID);
-        assertThat(writtenCharacteristic).isEqualTo(characteristic);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_writeCharacteristic_by_uuid() throws Exception {
-        mBluetoothGattConnection.writeCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID, DATA);
-
-        verify(mMockBluetoothOperationExecutor)
-                .execute(mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).writeCharacteristic(mCharacteristicCaptor.capture(),
-                eq(DATA), anyInt());
-        BluetoothGattCharacteristic writtenCharacteristic = mCharacteristicCaptor.getValue();
-        assertThat(writtenCharacteristic.getUuid()).isEqualTo(CHARACTERISTIC_UUID);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_readDescriptor() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new Operation<byte[]>(
-                        OperationType.READ_DESCRIPTOR, mMockBluetoothGattWrapper,
-                        mMockBluetoothGattDescriptor),
-                BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
-                .thenReturn(DATA);
-
-        byte[] result = mBluetoothGattConnection.readDescriptor(mMockBluetoothGattDescriptor);
-
-        assertThat(result).isEqualTo(DATA);
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mOperationCaptor.capture(), anyLong());
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).readDescriptor(mMockBluetoothGattDescriptor);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_readDescriptor_by_uuid() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new Operation<byte[]>(
-                        OperationType.READ_DESCRIPTOR, mMockBluetoothGattWrapper,
-                        mMockBluetoothGattDescriptor),
-                BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
-                .thenReturn(DATA);
-
-        byte[] result =
-                mBluetoothGattConnection
-                        .readDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
-
-        assertThat(result).isEqualTo(DATA);
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mOperationCaptor.capture(), anyLong());
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).readDescriptor(mMockBluetoothGattDescriptor);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_writeDescriptor() throws Exception {
-        BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(DESCRIPTOR_UUID, 0);
-        mBluetoothGattConnection.writeDescriptor(descriptor, DATA);
-
-        verify(mMockBluetoothOperationExecutor)
-                .execute(mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(), eq(DATA));
-        BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
-        assertThat(writtenDescriptor.getUuid()).isEqualTo(DESCRIPTOR_UUID);
-        assertThat(writtenDescriptor).isEqualTo(descriptor);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_writeDescriptor_by_uuid() throws Exception {
-        mBluetoothGattConnection.writeDescriptor(
-                SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID, DATA);
-
-        verify(mMockBluetoothOperationExecutor)
-                .execute(mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(), eq(DATA));
-        BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
-        assertThat(writtenDescriptor.getUuid()).isEqualTo(DESCRIPTOR_UUID);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_readRemoteRssi() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new Operation<Integer>(OperationType.READ_RSSI, mMockBluetoothGattWrapper),
-                BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
-                .thenReturn(RSSI);
-
-        int result = mBluetoothGattConnection.readRemoteRssi();
-
-        assertThat(result).isEqualTo(RSSI);
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(
-                        mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).readRemoteRssi();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getMaxDataPacketSize() throws Exception {
-        int result = mBluetoothGattConnection.getMaxDataPacketSize();
-
-        assertThat(result).isEqualTo(mBluetoothGattConnection.getMtu() - 3);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetNotificationEnabled_indication_enable() throws Exception {
-        when(mMockBluetoothGattCharacteristic.getProperties())
-                .thenReturn(BluetoothGattCharacteristic.PROPERTY_INDICATE);
-
-        mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, true);
-
-        verify(mMockBluetoothGattWrapper)
-                .setCharacteristicNotification(mMockBluetoothGattCharacteristic, true);
-        verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(),
-                eq(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE));
-        BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
-        assertThat(writtenDescriptor.getUuid())
-                .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_getNotificationEnabled_notification_enable() throws Exception {
-        mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, true);
-
-        verify(mMockBluetoothGattWrapper)
-                .setCharacteristicNotification(mMockBluetoothGattCharacteristic, true);
-        verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(),
-                eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE));
-        BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
-        assertThat(writtenDescriptor.getUuid())
-                .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_setNotificationEnabled_indication_disable() throws Exception {
-        when(mMockBluetoothGattCharacteristic.getProperties())
-                .thenReturn(BluetoothGattCharacteristic.PROPERTY_INDICATE);
-
-        mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, false);
-
-        verify(mMockBluetoothGattWrapper)
-                .setCharacteristicNotification(mMockBluetoothGattCharacteristic, false);
-        verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(),
-                eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
-        BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
-        assertThat(writtenDescriptor.getUuid())
-                .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_setNotificationEnabled_notification_disable() throws Exception {
-        mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, false);
-
-        verify(mMockBluetoothGattWrapper)
-                .setCharacteristicNotification(mMockBluetoothGattCharacteristic, false);
-        verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(),
-                eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
-        BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
-        assertThat(writtenDescriptor.getUuid())
-                .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_setNotificationEnabled_failure() throws Exception {
-        when(mMockBluetoothGattCharacteristic.getProperties())
-                .thenReturn(BluetoothGattCharacteristic.PROPERTY_READ);
-
-        try {
-            mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic,
-                    true);
-            fail("BluetoothException was expected");
-        } catch (BluetoothException expected) {
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_enableNotification_Uuid() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new SynchronousOperation<>(
-                        mMockBluetoothOperationExecutor,
-                        OperationType.NOTIFICATION_CHANGE,
-                        mMockBluetoothGattCharacteristic)))
-                .thenReturn(mMockChangeObserver);
-        mBluetoothGattConnection.enableNotification(SERVICE_UUID, CHARACTERISTIC_UUID);
-
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mSynchronousOperationCaptor.capture());
-        ((ChangeObserver) mSynchronousOperationCaptor.getValue().call())
-                .setListener(mMockCharChangeListener);
-        mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
-        verify(mMockCharChangeListener).onValueChange(DATA);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_enableNotification() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new SynchronousOperation<>(
-                        mMockBluetoothOperationExecutor,
-                        OperationType.NOTIFICATION_CHANGE,
-                        mMockBluetoothGattCharacteristic)))
-                .thenReturn(mMockChangeObserver);
-        mBluetoothGattConnection.enableNotification(mMockBluetoothGattCharacteristic);
-
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mSynchronousOperationCaptor.capture());
-        ((ChangeObserver) mSynchronousOperationCaptor.getValue().call())
-                .setListener(mMockCharChangeListener);
-
-        mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
-
-        verify(mMockCharChangeListener).onValueChange(DATA);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_enableNotification_observe() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new SynchronousOperation<>(
-                        mMockBluetoothOperationExecutor,
-                        OperationType.NOTIFICATION_CHANGE,
-                        mMockBluetoothGattCharacteristic)))
-                .thenReturn(mMockChangeObserver);
-        mBluetoothGattConnection.enableNotification(mMockBluetoothGattCharacteristic);
-
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mSynchronousOperationCaptor.capture());
-        ChangeObserver changeObserver = (ChangeObserver) mSynchronousOperationCaptor.getValue()
-                .call();
-        mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
-        assertThat(changeObserver.waitForUpdate(TimeUnit.SECONDS.toMillis(1))).isEqualTo(DATA);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_disableNotification_Uuid() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new SynchronousOperation<>(
-                        OperationType.NOTIFICATION_CHANGE, mMockBluetoothGattCharacteristic)))
-                .thenReturn(mMockChangeObserver);
-        mBluetoothGattConnection
-                .enableNotification(SERVICE_UUID, CHARACTERISTIC_UUID)
-                .setListener(mMockCharChangeListener);
-
-        mBluetoothGattConnection.disableNotification(SERVICE_UUID, CHARACTERISTIC_UUID);
-
-        mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
-        verify(mMockCharChangeListener, never()).onValueChange(DATA);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_disableNotification() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new SynchronousOperation<ChangeObserver>(
-                        OperationType.NOTIFICATION_CHANGE, mMockBluetoothGattCharacteristic)))
-                .thenReturn(mMockChangeObserver);
-        mBluetoothGattConnection
-                .enableNotification(mMockBluetoothGattCharacteristic)
-                .setListener(mMockCharChangeListener);
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mSynchronousOperationCaptor.capture());
-        mSynchronousOperationCaptor.getValue().call();
-
-        mBluetoothGattConnection.disableNotification(mMockBluetoothGattCharacteristic);
-        verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
-        mSynchronousOperationCaptor.getValue().call();
-
-        mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
-        verify(mMockCharChangeListener, never()).onValueChange(DATA);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_addCloseListener() throws Exception {
-        mBluetoothGattConnection.addCloseListener(mMockConnectionCloseListener);
-
-        mBluetoothGattConnection.onClosed();
-        verify(mMockConnectionCloseListener).onClose();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_removeCloseListener() throws Exception {
-        mBluetoothGattConnection.addCloseListener(mMockConnectionCloseListener);
-
-        mBluetoothGattConnection.removeCloseListener(mMockConnectionCloseListener);
-
-        mBluetoothGattConnection.onClosed();
-        verify(mMockConnectionCloseListener, never()).onClose();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_close() throws Exception {
-        mBluetoothGattConnection.close();
-
-        verify(mMockBluetoothOperationExecutor)
-                .execute(mOperationCaptor.capture(),
-                        eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothGattWrapper).disconnect();
-        verify(mMockBluetoothGattWrapper).close();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_onClosed() throws Exception {
-        mBluetoothGattConnection.onClosed();
-
-        verify(mMockBluetoothOperationExecutor, never())
-                .execute(mOperationCaptor.capture(), anyLong());
-        verify(mMockBluetoothGattWrapper).close();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelperTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelperTest.java
deleted file mode 100644
index 7c20be1..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelperTest.java
+++ /dev/null
@@ -1,675 +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.server.nearby.common.bluetooth.gatt;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isA;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.os.ParcelUuid;
-import android.test.mock.MockContext;
-
-import androidx.test.filters.SdkSuppress;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
-import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.OperationType;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattWrapper;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeScanner;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanCallback;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanResult;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
-
-import junit.framework.TestCase;
-
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-
-import java.util.Arrays;
-import java.util.UUID;
-
-/**
- * Unit tests for {@link BluetoothGattHelper}.
- */
-public class BluetoothGattHelperTest extends TestCase {
-
-    private static final UUID SERVICE_UUID = UUID.randomUUID();
-    private static final int GATT_STATUS = 1234;
-    private static final Operation<BluetoothDevice> SCANNING_OPERATION =
-            new Operation<BluetoothDevice>(OperationType.SCAN);
-    private static final byte[] CHARACTERISTIC_VALUE = "characteristic_value".getBytes();
-    private static final byte[] DESCRIPTOR_VALUE = "descriptor_value".getBytes();
-    private static final int RSSI = -63;
-    private static final int MTU = 50;
-    private static final long CONNECT_TIMEOUT_MILLIS = 5000;
-
-    private Context mMockApplicationContext = new MockContext();
-    @Mock
-    private BluetoothAdapter mMockBluetoothAdapter;
-    @Mock
-    private BluetoothLeScanner mMockBluetoothLeScanner;
-    @Mock
-    private BluetoothOperationExecutor mMockBluetoothOperationExecutor;
-    @Mock
-    private BluetoothDevice mMockBluetoothDevice;
-    @Mock
-    private BluetoothGattConnection mMockBluetoothGattConnection;
-    @Mock
-    private BluetoothGattWrapper mMockBluetoothGattWrapper;
-    @Mock
-    private BluetoothGattCharacteristic mMockBluetoothGattCharacteristic;
-    @Mock
-    private BluetoothGattDescriptor mMockBluetoothGattDescriptor;
-    @Mock
-    private ScanResult mMockScanResult;
-
-    @Captor
-    private ArgumentCaptor<Operation<?>> mOperationCaptor;
-    @Captor
-    private ArgumentCaptor<ScanSettings> mScanSettingsCaptor;
-
-    private BluetoothGattHelper mBluetoothGattHelper;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        initMocks(this);
-
-        mBluetoothGattHelper = new BluetoothGattHelper(
-                mMockApplicationContext,
-                mMockBluetoothAdapter,
-                mMockBluetoothOperationExecutor);
-
-        when(mMockBluetoothAdapter.getBluetoothLeScanner()).thenReturn(mMockBluetoothLeScanner);
-        when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION,
-                BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS)).thenReturn(mMockBluetoothDevice);
-        when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION)).thenReturn(
-                mMockBluetoothDevice);
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new Operation<BluetoothGattConnection>(OperationType.CONNECT, mMockBluetoothDevice),
-                CONNECT_TIMEOUT_MILLIS))
-                .thenReturn(mMockBluetoothGattConnection);
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new Operation<BluetoothGattConnection>(OperationType.CONNECT,
-                        mMockBluetoothDevice)))
-                .thenReturn(mMockBluetoothGattConnection);
-        when(mMockBluetoothGattCharacteristic.getValue()).thenReturn(CHARACTERISTIC_VALUE);
-        when(mMockBluetoothGattDescriptor.getValue()).thenReturn(DESCRIPTOR_VALUE);
-        when(mMockScanResult.getDevice()).thenReturn(mMockBluetoothDevice);
-        when(mMockBluetoothGattWrapper.getDevice()).thenReturn(mMockBluetoothDevice);
-        when(mMockBluetoothDevice.connectGatt(eq(mMockApplicationContext), anyBoolean(),
-                eq(mBluetoothGattHelper.mBluetoothGattCallback))).thenReturn(
-                mMockBluetoothGattWrapper);
-        when(mMockBluetoothDevice.connectGatt(eq(mMockApplicationContext), anyBoolean(),
-                eq(mBluetoothGattHelper.mBluetoothGattCallback), anyInt()))
-                .thenReturn(mMockBluetoothGattWrapper);
-        when(mMockBluetoothGattConnection.getConnectionOptions())
-                .thenReturn(ConnectionOptions.builder().build());
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_autoConnect_uuid_success_lowLatency() throws Exception {
-        BluetoothGattConnection result = mBluetoothGattHelper.autoConnect(SERVICE_UUID);
-
-        assertThat(result).isEqualTo(mMockBluetoothGattConnection);
-        verify(mMockBluetoothOperationExecutor, atLeastOnce())
-                .executeNonnull(mOperationCaptor.capture(),
-                        anyLong());
-        for (Operation<?> operation : mOperationCaptor.getAllValues()) {
-            operation.run();
-        }
-        verify(mMockBluetoothLeScanner).startScan(eq(Arrays.asList(
-                new ScanFilter.Builder().setServiceUuid(new ParcelUuid(SERVICE_UUID)).build())),
-                mScanSettingsCaptor.capture(), eq(mBluetoothGattHelper.mScanCallback));
-        assertThat(mScanSettingsCaptor.getValue().getScanMode()).isEqualTo(
-                ScanSettings.SCAN_MODE_LOW_LATENCY);
-        verify(mMockBluetoothLeScanner).stopScan(mBluetoothGattHelper.mScanCallback);
-        verifyNoMoreInteractions(mMockBluetoothLeScanner);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_autoConnect_uuid_success_lowPower() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION,
-                BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS)).thenThrow(
-                new BluetoothOperationTimeoutException("Timeout"));
-
-        BluetoothGattConnection result = mBluetoothGattHelper.autoConnect(SERVICE_UUID);
-
-        assertThat(result).isEqualTo(mMockBluetoothGattConnection);
-        verify(mMockBluetoothOperationExecutor).executeNonnull(mOperationCaptor.capture());
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothLeScanner).startScan(eq(Arrays.asList(
-                new ScanFilter.Builder().setServiceUuid(new ParcelUuid(SERVICE_UUID)).build())),
-                mScanSettingsCaptor.capture(), eq(mBluetoothGattHelper.mScanCallback));
-        assertThat(mScanSettingsCaptor.getValue().getScanMode()).isEqualTo(
-                ScanSettings.SCAN_MODE_LOW_POWER);
-        verify(mMockBluetoothLeScanner, times(2)).stopScan(mBluetoothGattHelper.mScanCallback);
-        verifyNoMoreInteractions(mMockBluetoothLeScanner);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_autoConnect_uuid_success_afterRetry() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new Operation<BluetoothGattConnection>(OperationType.CONNECT, mMockBluetoothDevice),
-                BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS))
-                .thenThrow(new BluetoothException("first attempt fails!"))
-                .thenReturn(mMockBluetoothGattConnection);
-
-        BluetoothGattConnection result = mBluetoothGattHelper.autoConnect(SERVICE_UUID);
-
-        assertThat(result).isEqualTo(mMockBluetoothGattConnection);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_autoConnect_uuid_failure_scanning() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION,
-                BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS)).thenThrow(
-                new BluetoothException("Scanning failed"));
-
-        try {
-            mBluetoothGattHelper.autoConnect(SERVICE_UUID);
-            fail("BluetoothException expected");
-        } catch (BluetoothException e) {
-            // expected
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_autoConnect_uuid_failure_connecting() throws Exception {
-        when(mMockBluetoothOperationExecutor.executeNonnull(
-                new Operation<BluetoothGattConnection>(OperationType.CONNECT, mMockBluetoothDevice),
-                CONNECT_TIMEOUT_MILLIS))
-                .thenThrow(new BluetoothException("Connect failed"));
-
-        try {
-            mBluetoothGattHelper.autoConnect(SERVICE_UUID);
-            fail("BluetoothException expected");
-        } catch (BluetoothException e) {
-            // expected
-        }
-        verify(mMockBluetoothOperationExecutor, times(3))
-                .executeNonnull(
-                        new Operation<BluetoothGattConnection>(OperationType.CONNECT,
-                                mMockBluetoothDevice),
-                        CONNECT_TIMEOUT_MILLIS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_autoConnect_uuid_failure_noBle() throws Exception {
-        when(mMockBluetoothAdapter.getBluetoothLeScanner()).thenReturn(null);
-
-        try {
-            mBluetoothGattHelper.autoConnect(SERVICE_UUID);
-            fail("BluetoothException expected");
-        } catch (BluetoothException e) {
-            // expected
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_connect() throws Exception {
-        BluetoothGattConnection result = mBluetoothGattHelper.connect(mMockBluetoothDevice);
-
-        assertThat(result).isEqualTo(mMockBluetoothGattConnection);
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mOperationCaptor.capture(), eq(CONNECT_TIMEOUT_MILLIS));
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothDevice).connectGatt(mMockApplicationContext, false,
-                mBluetoothGattHelper.mBluetoothGattCallback,
-                android.bluetooth.BluetoothDevice.TRANSPORT_LE);
-        assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper).getDevice())
-                .isEqualTo(mMockBluetoothDevice);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_connect_withOptionAutoConnect_success() throws Exception {
-        BluetoothGattConnection result = mBluetoothGattHelper
-                .connect(
-                        mMockBluetoothDevice,
-                        ConnectionOptions.builder()
-                                .setAutoConnect(true)
-                                .build());
-
-        assertThat(result).isEqualTo(mMockBluetoothGattConnection);
-        verify(mMockBluetoothOperationExecutor).executeNonnull(mOperationCaptor.capture());
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothDevice).connectGatt(mMockApplicationContext, true,
-                mBluetoothGattHelper.mBluetoothGattCallback,
-                android.bluetooth.BluetoothDevice.TRANSPORT_LE);
-        assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)
-                .getConnectionOptions())
-                .isEqualTo(ConnectionOptions.builder()
-                        .setAutoConnect(true)
-                        .build());
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_connect_withOptionAutoConnect_failure_nullResult() throws Exception {
-        when(mMockBluetoothDevice.connectGatt(eq(mMockApplicationContext), anyBoolean(),
-                eq(mBluetoothGattHelper.mBluetoothGattCallback),
-                eq(android.bluetooth.BluetoothDevice.TRANSPORT_LE))).thenReturn(null);
-
-        try {
-            mBluetoothGattHelper.connect(
-                    mMockBluetoothDevice,
-                    ConnectionOptions.builder()
-                            .setAutoConnect(true)
-                            .build());
-            verify(mMockBluetoothOperationExecutor).executeNonnull(mOperationCaptor.capture());
-            mOperationCaptor.getValue().run();
-            fail("BluetoothException expected");
-        } catch (BluetoothException e) {
-            // expected
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_connect_withOptionRequestConnectionPriority_success() throws Exception {
-        // Operation succeeds on the 3rd try.
-        when(mMockBluetoothGattWrapper
-                .requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH))
-                .thenReturn(false)
-                .thenReturn(false)
-                .thenReturn(true);
-
-        BluetoothGattConnection result = mBluetoothGattHelper
-                .connect(
-                        mMockBluetoothDevice,
-                        ConnectionOptions.builder()
-                                .setConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
-                                .build());
-
-        assertThat(result).isEqualTo(mMockBluetoothGattConnection);
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mOperationCaptor.capture(), eq(CONNECT_TIMEOUT_MILLIS));
-        mOperationCaptor.getValue().run();
-        verify(mMockBluetoothDevice).connectGatt(mMockApplicationContext, false,
-                mBluetoothGattHelper.mBluetoothGattCallback,
-                android.bluetooth.BluetoothDevice.TRANSPORT_LE);
-        assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)
-                .getConnectionOptions())
-                .isEqualTo(ConnectionOptions.builder()
-                        .setConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
-                        .build());
-        verify(mMockBluetoothGattWrapper, times(3))
-                .requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_connect_cancel() throws Exception {
-        mBluetoothGattHelper.connect(mMockBluetoothDevice);
-
-        verify(mMockBluetoothOperationExecutor)
-                .executeNonnull(mOperationCaptor.capture(), eq(CONNECT_TIMEOUT_MILLIS));
-        Operation<?> operation = mOperationCaptor.getValue();
-        operation.run();
-        operation.cancel();
-
-        verify(mMockBluetoothGattWrapper).disconnect();
-        verify(mMockBluetoothGattWrapper).close();
-        assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onConnectionStateChange_connected_success()
-            throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-
-        mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
-                mMockBluetoothGattWrapper,
-                BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(
-                new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
-                BluetoothGatt.GATT_SUCCESS,
-                mMockBluetoothGattConnection);
-        verify(mMockBluetoothGattConnection).onConnected();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onConnectionStateChange_connected_success_withMtuOption()
-            throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-        when(mMockBluetoothGattConnection.getConnectionOptions())
-                .thenReturn(BluetoothGattHelper.ConnectionOptions.builder()
-                        .setMtu(MTU)
-                        .build());
-        when(mMockBluetoothGattWrapper.requestMtu(MTU)).thenReturn(true);
-
-        mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
-                mMockBluetoothGattWrapper,
-                BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
-
-        verifyZeroInteractions(mMockBluetoothOperationExecutor);
-        verify(mMockBluetoothGattConnection, never()).onConnected();
-        verify(mMockBluetoothGattWrapper).requestMtu(MTU);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onConnectionStateChange_connected_success_failMtuOption()
-            throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-        when(mMockBluetoothGattConnection.getConnectionOptions())
-                .thenReturn(BluetoothGattHelper.ConnectionOptions.builder()
-                        .setMtu(MTU)
-                        .build());
-        when(mMockBluetoothGattWrapper.requestMtu(MTU)).thenReturn(false);
-
-        mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
-                mMockBluetoothGattWrapper,
-                BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
-
-        verify(mMockBluetoothOperationExecutor).notifyFailure(
-                eq(new Operation<>(OperationType.CONNECT, mMockBluetoothDevice)),
-                any(BluetoothException.class));
-        verify(mMockBluetoothGattConnection, never()).onConnected();
-        verify(mMockBluetoothGattWrapper).disconnect();
-        verify(mMockBluetoothGattWrapper).close();
-        assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onConnectionStateChange_connected_unexpectedSuccess()
-            throws Exception {
-        mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
-                mMockBluetoothGattWrapper,
-                BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
-
-        verifyZeroInteractions(mMockBluetoothOperationExecutor);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onConnectionStateChange_connected_failure()
-            throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-
-        mBluetoothGattHelper.mBluetoothGattCallback
-                .onConnectionStateChange(
-                        mMockBluetoothGattWrapper,
-                        BluetoothGatt.GATT_FAILURE,
-                        BluetoothGatt.STATE_CONNECTED);
-
-        verify(mMockBluetoothOperationExecutor)
-                .notifyCompletion(
-                        new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
-                        BluetoothGatt.GATT_FAILURE,
-                        null);
-        verify(mMockBluetoothGattWrapper).disconnect();
-        verify(mMockBluetoothGattWrapper).close();
-        assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_unexpectedSuccess()
-            throws Exception {
-        mBluetoothGattHelper.mBluetoothGattCallback
-                .onConnectionStateChange(
-                        mMockBluetoothGattWrapper,
-                        BluetoothGatt.GATT_SUCCESS,
-                        BluetoothGatt.STATE_DISCONNECTED);
-
-        verifyZeroInteractions(mMockBluetoothOperationExecutor);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_notConnected()
-            throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-        when(mMockBluetoothGattConnection.isConnected()).thenReturn(false);
-
-        mBluetoothGattHelper.mBluetoothGattCallback
-                .onConnectionStateChange(
-                        mMockBluetoothGattWrapper,
-                        GATT_STATUS,
-                        BluetoothGatt.STATE_DISCONNECTED);
-
-        verify(mMockBluetoothOperationExecutor)
-                .notifyCompletion(
-                        new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
-                        GATT_STATUS,
-                        null);
-        verify(mMockBluetoothGattWrapper).disconnect();
-        verify(mMockBluetoothGattWrapper).close();
-        assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_success()
-            throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-        when(mMockBluetoothGattConnection.isConnected()).thenReturn(true);
-
-        mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
-                mMockBluetoothGattWrapper,
-                BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_DISCONNECTED);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(
-                new Operation<>(OperationType.DISCONNECT, mMockBluetoothDevice),
-                BluetoothGatt.GATT_SUCCESS);
-        verify(mMockBluetoothGattConnection).onClosed();
-        assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_failure()
-            throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-        when(mMockBluetoothGattConnection.isConnected()).thenReturn(true);
-
-        mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
-                mMockBluetoothGattWrapper,
-                BluetoothGatt.GATT_FAILURE, BluetoothGatt.STATE_DISCONNECTED);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(
-                new Operation<>(OperationType.DISCONNECT, mMockBluetoothDevice),
-                BluetoothGatt.GATT_FAILURE);
-        verify(mMockBluetoothGattConnection).onClosed();
-        assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onServicesDiscovered() throws Exception {
-        mBluetoothGattHelper.mBluetoothGattCallback.onServicesDiscovered(mMockBluetoothGattWrapper,
-                GATT_STATUS);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(
-                new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL,
-                        mMockBluetoothGattWrapper),
-                GATT_STATUS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onCharacteristicRead() throws Exception {
-        mBluetoothGattHelper.mBluetoothGattCallback.onCharacteristicRead(mMockBluetoothGattWrapper,
-                mMockBluetoothGattCharacteristic, GATT_STATUS);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<byte[]>(
-                        OperationType.READ_CHARACTERISTIC, mMockBluetoothGattWrapper,
-                        mMockBluetoothGattCharacteristic),
-                GATT_STATUS, CHARACTERISTIC_VALUE);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onCharacteristicWrite() throws Exception {
-        mBluetoothGattHelper.mBluetoothGattCallback.onCharacteristicWrite(mMockBluetoothGattWrapper,
-                mMockBluetoothGattCharacteristic, GATT_STATUS);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<Void>(
-                        OperationType.WRITE_CHARACTERISTIC, mMockBluetoothGattWrapper,
-                        mMockBluetoothGattCharacteristic),
-                GATT_STATUS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onDescriptorRead() throws Exception {
-        mBluetoothGattHelper.mBluetoothGattCallback.onDescriptorRead(mMockBluetoothGattWrapper,
-                mMockBluetoothGattDescriptor, GATT_STATUS);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<byte[]>(
-                        OperationType.READ_DESCRIPTOR, mMockBluetoothGattWrapper,
-                        mMockBluetoothGattDescriptor),
-                GATT_STATUS,
-                DESCRIPTOR_VALUE);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onDescriptorWrite() throws Exception {
-        mBluetoothGattHelper.mBluetoothGattCallback.onDescriptorWrite(mMockBluetoothGattWrapper,
-                mMockBluetoothGattDescriptor, GATT_STATUS);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<Void>(
-                        OperationType.WRITE_DESCRIPTOR, mMockBluetoothGattWrapper,
-                        mMockBluetoothGattDescriptor),
-                GATT_STATUS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onReadRemoteRssi() throws Exception {
-        mBluetoothGattHelper.mBluetoothGattCallback.onReadRemoteRssi(mMockBluetoothGattWrapper,
-                RSSI, GATT_STATUS);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(
-                new Operation<Integer>(OperationType.READ_RSSI, mMockBluetoothGattWrapper),
-                GATT_STATUS, RSSI);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onReliableWriteCompleted() throws Exception {
-        mBluetoothGattHelper.mBluetoothGattCallback.onReliableWriteCompleted(
-                mMockBluetoothGattWrapper,
-                GATT_STATUS);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(
-                new Operation<Void>(OperationType.WRITE_RELIABLE, mMockBluetoothGattWrapper),
-                GATT_STATUS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onMtuChanged() throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-        when(mMockBluetoothGattConnection.isConnected()).thenReturn(true);
-
-        mBluetoothGattHelper.mBluetoothGattCallback
-                .onMtuChanged(mMockBluetoothGattWrapper, MTU, GATT_STATUS);
-
-        verify(mMockBluetoothOperationExecutor).notifyCompletion(
-                new Operation<>(OperationType.CHANGE_MTU, mMockBluetoothGattWrapper), GATT_STATUS,
-                MTU);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothGattCallback_onMtuChangedDuringConnection_success() throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-        when(mMockBluetoothGattConnection.isConnected()).thenReturn(false);
-
-        mBluetoothGattHelper.mBluetoothGattCallback.onMtuChanged(
-                mMockBluetoothGattWrapper, MTU, BluetoothGatt.GATT_SUCCESS);
-
-        verify(mMockBluetoothGattConnection).onConnected();
-        verify(mMockBluetoothOperationExecutor)
-                .notifyCompletion(
-                        new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
-                        BluetoothGatt.GATT_SUCCESS,
-                        mMockBluetoothGattConnection);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testBluetoothGattCallback_onMtuChangedDuringConnection_fail() throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-        when(mMockBluetoothGattConnection.isConnected()).thenReturn(false);
-
-        mBluetoothGattHelper.mBluetoothGattCallback
-                .onMtuChanged(mMockBluetoothGattWrapper, MTU, GATT_STATUS);
-
-        verify(mMockBluetoothGattConnection).onConnected();
-        verify(mMockBluetoothOperationExecutor)
-                .notifyCompletion(
-                        new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
-                        GATT_STATUS,
-                        mMockBluetoothGattConnection);
-        verify(mMockBluetoothGattWrapper).disconnect();
-        verify(mMockBluetoothGattWrapper).close();
-        assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_BluetoothGattCallback_onCharacteristicChanged() throws Exception {
-        mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
-                mMockBluetoothGattConnection);
-
-        mBluetoothGattHelper.mBluetoothGattCallback.onCharacteristicChanged(
-                mMockBluetoothGattWrapper,
-                mMockBluetoothGattCharacteristic);
-
-        verify(mMockBluetoothGattConnection).onCharacteristicChanged(
-                mMockBluetoothGattCharacteristic,
-                CHARACTERISTIC_VALUE);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_ScanCallback_onScanFailed() throws Exception {
-        mBluetoothGattHelper.mScanCallback.onScanFailed(ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
-
-        verify(mMockBluetoothOperationExecutor).notifyFailure(
-                eq(new Operation<BluetoothDevice>(OperationType.SCAN)),
-                isA(BluetoothException.class));
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_ScanCallback_onScanResult() throws Exception {
-        mBluetoothGattHelper.mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
-                mMockScanResult);
-
-        verify(mMockBluetoothOperationExecutor).notifySuccess(
-                new Operation<BluetoothDevice>(OperationType.SCAN), mMockBluetoothDevice);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapterTest.java
deleted file mode 100644
index a9ab7da..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapterTest.java
+++ /dev/null
@@ -1,148 +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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Unit tests for {@link BluetoothAdapter}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothAdapterTest {
-
-    private static final byte[] BYTES = new byte[]{0, 1, 2, 3, 4, 5};
-    private static final String ADDRESS = "00:11:22:33:AA:BB";
-
-    @Mock private android.bluetooth.BluetoothAdapter mBluetoothAdapter;
-    @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
-    @Mock private android.bluetooth.le.BluetoothLeAdvertiser mBluetoothLeAdvertiser;
-    @Mock private android.bluetooth.le.BluetoothLeScanner mBluetoothLeScanner;
-
-    BluetoothAdapter mTestabilityBluetoothAdapter;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mTestabilityBluetoothAdapter = BluetoothAdapter.wrap(mBluetoothAdapter);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWrapNullAdapter_isNull() {
-        assertThat(BluetoothAdapter.wrap(null)).isNull();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
-        assertThat(mTestabilityBluetoothAdapter).isNotNull();
-        assertThat(mTestabilityBluetoothAdapter.unwrap()).isSameInstanceAs(mBluetoothAdapter);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testDisable_callsWrapped() {
-        when(mBluetoothAdapter.disable()).thenReturn(true);
-        assertThat(mTestabilityBluetoothAdapter.disable()).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEnable_callsWrapped() {
-        when(mBluetoothAdapter.enable()).thenReturn(true);
-        assertThat(mTestabilityBluetoothAdapter.enable()).isTrue();
-        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
-        assertThat(mTestabilityBluetoothAdapter.isEnabled()).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetBluetoothLeAdvertiser_callsWrapped() {
-        when(mBluetoothAdapter.getBluetoothLeAdvertiser()).thenReturn(mBluetoothLeAdvertiser);
-        assertThat(mTestabilityBluetoothAdapter.getBluetoothLeAdvertiser().unwrap())
-                .isSameInstanceAs(mBluetoothLeAdvertiser);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetBluetoothLeScanner_callsWrapped() {
-        when(mBluetoothAdapter.getBluetoothLeScanner()).thenReturn(mBluetoothLeScanner);
-        assertThat(mTestabilityBluetoothAdapter.getBluetoothLeScanner().unwrap())
-                .isSameInstanceAs(mBluetoothLeScanner);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetBondedDevices_callsWrapped() {
-        when(mBluetoothAdapter.getBondedDevices()).thenReturn(null);
-        assertThat(mTestabilityBluetoothAdapter.getBondedDevices()).isNull();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testIsDiscovering_pcallsWrapped() {
-        when(mBluetoothAdapter.isDiscovering()).thenReturn(true);
-        assertThat(mTestabilityBluetoothAdapter.isDiscovering()).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testStartDiscovery_callsWrapped() {
-        when(mBluetoothAdapter.startDiscovery()).thenReturn(true);
-        assertThat(mTestabilityBluetoothAdapter.startDiscovery()).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testCancelDiscovery_callsWrapped() {
-        when(mBluetoothAdapter.cancelDiscovery()).thenReturn(true);
-        assertThat(mTestabilityBluetoothAdapter.cancelDiscovery()).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetRemoteDeviceBytes_callsWrapped() {
-        when(mBluetoothAdapter.getRemoteDevice(BYTES)).thenReturn(mBluetoothDevice);
-        assertThat(mTestabilityBluetoothAdapter.getRemoteDevice(BYTES).unwrap())
-                .isSameInstanceAs(mBluetoothDevice);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetRemoteDeviceString_callsWrapped() {
-        when(mBluetoothAdapter.getRemoteDevice(ADDRESS)).thenReturn(mBluetoothDevice);
-        assertThat(mTestabilityBluetoothAdapter.getRemoteDevice(ADDRESS).unwrap())
-                .isSameInstanceAs(mBluetoothDevice);
-
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDeviceTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDeviceTest.java
deleted file mode 100644
index d494024..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDeviceTest.java
+++ /dev/null
@@ -1,206 +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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.IOException;
-import java.util.UUID;
-
-/**
- * Unit tests for {@link BluetoothDevice}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothDeviceTest {
-    private static final UUID UUID_CONST = UUID.randomUUID();
-    private static final String ADDRESS = "ADDRESS";
-    private static final String STRING = "STRING";
-
-    @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
-    @Mock private android.bluetooth.BluetoothGatt mBluetoothGatt;
-    @Mock private android.bluetooth.BluetoothSocket mBluetoothSocket;
-    @Mock private android.bluetooth.BluetoothClass mBluetoothClass;
-
-    BluetoothDevice mTestabilityBluetoothDevice;
-    BluetoothGattCallback mTestBluetoothGattCallback;
-    Context mContext;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mTestabilityBluetoothDevice = BluetoothDevice.wrap(mBluetoothDevice);
-        mTestBluetoothGattCallback = new TestBluetoothGattCallback();
-        mContext = InstrumentationRegistry.getInstrumentation().getContext();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
-        assertThat(mTestabilityBluetoothDevice).isNotNull();
-        assertThat(mTestabilityBluetoothDevice.unwrap()).isSameInstanceAs(mBluetoothDevice);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEquality_asExpected() {
-        assertThat(mTestabilityBluetoothDevice.equals(null)).isFalse();
-        assertThat(mTestabilityBluetoothDevice.equals(mTestabilityBluetoothDevice)).isTrue();
-        assertThat(mTestabilityBluetoothDevice.equals(BluetoothDevice.wrap(mBluetoothDevice)))
-                .isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHashCode_asExpected() {
-        assertThat(mTestabilityBluetoothDevice.hashCode())
-                .isEqualTo(BluetoothDevice.wrap(mBluetoothDevice).hashCode());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConnectGattWithThreeParameters_callsWrapped() {
-        when(mBluetoothDevice
-                .connectGatt(mContext, true, mTestBluetoothGattCallback.unwrap()))
-                .thenReturn(mBluetoothGatt);
-        assertThat(mTestabilityBluetoothDevice
-                .connectGatt(mContext, true, mTestBluetoothGattCallback)
-                .unwrap())
-                .isSameInstanceAs(mBluetoothGatt);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConnectGattWithFourParameters_callsWrapped() {
-        when(mBluetoothDevice
-                .connectGatt(mContext, true, mTestBluetoothGattCallback.unwrap(), 1))
-                .thenReturn(mBluetoothGatt);
-        assertThat(mTestabilityBluetoothDevice
-                .connectGatt(mContext, true, mTestBluetoothGattCallback, 1)
-                .unwrap())
-                .isSameInstanceAs(mBluetoothGatt);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testCreateRfcommSocketToServiceRecord_callsWrapped() throws IOException {
-        when(mBluetoothDevice.createRfcommSocketToServiceRecord(UUID_CONST))
-                .thenReturn(mBluetoothSocket);
-        assertThat(mTestabilityBluetoothDevice.createRfcommSocketToServiceRecord(UUID_CONST))
-                .isSameInstanceAs(mBluetoothSocket);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testCreateInsecureRfcommSocketToServiceRecord_callsWrapped() throws IOException {
-        when(mBluetoothDevice.createInsecureRfcommSocketToServiceRecord(UUID_CONST))
-                .thenReturn(mBluetoothSocket);
-        assertThat(mTestabilityBluetoothDevice
-                .createInsecureRfcommSocketToServiceRecord(UUID_CONST))
-                .isSameInstanceAs(mBluetoothSocket);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetPairingConfirmation_callsWrapped() throws IOException {
-        when(mBluetoothDevice.setPairingConfirmation(true)).thenReturn(true);
-        assertThat(mTestabilityBluetoothDevice.setPairingConfirmation(true)).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testFetchUuidsWithSdp_callsWrapped() throws IOException {
-        when(mBluetoothDevice.fetchUuidsWithSdp()).thenReturn(true);
-        assertThat(mTestabilityBluetoothDevice.fetchUuidsWithSdp()).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testCreateBond_callsWrapped() throws IOException {
-        when(mBluetoothDevice.createBond()).thenReturn(true);
-        assertThat(mTestabilityBluetoothDevice.createBond()).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetUuids_callsWrapped() throws IOException {
-        when(mBluetoothDevice.getUuids()).thenReturn(null);
-        assertThat(mTestabilityBluetoothDevice.getUuids()).isNull();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetBondState_callsWrapped() throws IOException {
-        when(mBluetoothDevice.getBondState()).thenReturn(1);
-        assertThat(mTestabilityBluetoothDevice.getBondState()).isEqualTo(1);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetAddress_callsWrapped() throws IOException {
-        when(mBluetoothDevice.getAddress()).thenReturn(ADDRESS);
-        assertThat(mTestabilityBluetoothDevice.getAddress()).isSameInstanceAs(ADDRESS);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetBluetoothClass_callsWrapped() throws IOException {
-        when(mBluetoothDevice.getBluetoothClass()).thenReturn(mBluetoothClass);
-        assertThat(mTestabilityBluetoothDevice.getBluetoothClass())
-                .isSameInstanceAs(mBluetoothClass);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetType_callsWrapped() throws IOException {
-        when(mBluetoothDevice.getType()).thenReturn(1);
-        assertThat(mTestabilityBluetoothDevice.getType()).isEqualTo(1);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetName_callsWrapped() throws IOException {
-        when(mBluetoothDevice.getName()).thenReturn(STRING);
-        assertThat(mTestabilityBluetoothDevice.getName()).isSameInstanceAs(STRING);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testToString_callsWrapped() {
-        when(mBluetoothDevice.toString()).thenReturn(STRING);
-        assertThat(mTestabilityBluetoothDevice.toString()).isSameInstanceAs(STRING);
-    }
-
-    private static class TestBluetoothGattCallback extends BluetoothGattCallback {}
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallbackTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallbackTest.java
deleted file mode 100644
index 26ae6d7..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallbackTest.java
+++ /dev/null
@@ -1,98 +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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-
-/**
- * Unit tests for {@link BluetoothGattCallback}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothGattCallbackTest {
-    @Mock private android.bluetooth.BluetoothGatt mBluetoothGatt;
-    @Mock private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
-    @Mock private android.bluetooth.BluetoothGattDescriptor mBluetoothGattDescriptor;
-
-    TestBluetoothGattCallback mTestBluetoothGattCallback = new TestBluetoothGattCallback();
-
-    @Test
-    public void testOnConnectionStateChange_notCrash() {
-        mTestBluetoothGattCallback.unwrap()
-                .onConnectionStateChange(mBluetoothGatt, 1, 1);
-    }
-
-    @Test
-    public void testOnServiceDiscovered_notCrash() {
-        mTestBluetoothGattCallback.unwrap().onServicesDiscovered(mBluetoothGatt, 1);
-    }
-
-    @Test
-    public void testOnCharacteristicRead_notCrash() {
-        mTestBluetoothGattCallback.unwrap().onCharacteristicRead(mBluetoothGatt,
-                mBluetoothGattCharacteristic, 1);
-    }
-
-    @Test
-    public void testOnCharacteristicWrite_notCrash() {
-        mTestBluetoothGattCallback.unwrap().onCharacteristicWrite(mBluetoothGatt,
-                mBluetoothGattCharacteristic, 1);
-    }
-
-    @Test
-    public void testOnDescriptionRead_notCrash() {
-        mTestBluetoothGattCallback.unwrap().onDescriptorRead(mBluetoothGatt,
-                mBluetoothGattDescriptor, 1);
-    }
-
-    @Test
-    public void testOnDescriptionWrite_notCrash() {
-        mTestBluetoothGattCallback.unwrap().onDescriptorWrite(mBluetoothGatt,
-                mBluetoothGattDescriptor, 1);
-    }
-
-    @Test
-    public void testOnReadRemoteRssi_notCrash() {
-        mTestBluetoothGattCallback.unwrap().onReadRemoteRssi(mBluetoothGatt, 1, 1);
-    }
-
-    @Test
-    public void testOnReliableWriteCompleted_notCrash() {
-        mTestBluetoothGattCallback.unwrap().onReliableWriteCompleted(mBluetoothGatt, 1);
-    }
-
-    @Test
-    public void testOnMtuChanged_notCrash() {
-        mTestBluetoothGattCallback.unwrap().onMtuChanged(mBluetoothGatt, 1, 1);
-    }
-
-    @Test
-    public void testOnCharacteristicChanged_notCrash() {
-        mTestBluetoothGattCallback.unwrap()
-                .onCharacteristicChanged(mBluetoothGatt, mBluetoothGattCharacteristic);
-    }
-
-    private static class TestBluetoothGattCallback extends BluetoothGattCallback { }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallbackTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallbackTest.java
deleted file mode 100644
index fb99317..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallbackTest.java
+++ /dev/null
@@ -1,121 +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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-
-/**
- * Unit tests for {@link BluetoothGattServerCallback}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothGattServerCallbackTest {
-    @Mock
-    private android.bluetooth.BluetoothDevice mBluetoothDevice;
-    @Mock
-    private android.bluetooth.BluetoothGattService mBluetoothGattService;
-    @Mock
-    private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
-    @Mock
-    private android.bluetooth.BluetoothGattDescriptor mBluetoothGattDescriptor;
-
-    TestBluetoothGattServerCallback
-            mTestBluetoothGattServerCallback = new TestBluetoothGattServerCallback();
-
-    @Test
-    public void testOnCharacteristicReadRequest_notCrash() {
-        mTestBluetoothGattServerCallback.unwrap().onCharacteristicReadRequest(
-                mBluetoothDevice, 1, 1, mBluetoothGattCharacteristic);
-    }
-
-    @Test
-    public void testOnCharacteristicWriteRequest_notCrash() {
-        mTestBluetoothGattServerCallback.unwrap().onCharacteristicWriteRequest(
-                mBluetoothDevice,
-                1,
-                mBluetoothGattCharacteristic,
-                false,
-                true,
-                1,
-                new byte[]{1});
-    }
-
-    @Test
-    public void testOnConnectionStateChange_notCrash() {
-        mTestBluetoothGattServerCallback.unwrap().onConnectionStateChange(
-                mBluetoothDevice,
-                1,
-                2);
-    }
-
-    @Test
-    public void testOnDescriptorReadRequest_notCrash() {
-        mTestBluetoothGattServerCallback.unwrap().onDescriptorReadRequest(
-                mBluetoothDevice,
-                1,
-                2, mBluetoothGattDescriptor);
-    }
-
-    @Test
-    public void testOnDescriptorWriteRequest_notCrash() {
-        mTestBluetoothGattServerCallback.unwrap().onDescriptorWriteRequest(
-                mBluetoothDevice,
-                1,
-                mBluetoothGattDescriptor,
-                false,
-                true,
-                2,
-                new byte[]{1});
-    }
-
-    @Test
-    public void testOnExecuteWrite_notCrash() {
-        mTestBluetoothGattServerCallback.unwrap().onExecuteWrite(
-                mBluetoothDevice,
-                1,
-                false);
-    }
-
-    @Test
-    public void testOnMtuChanged_notCrash() {
-        mTestBluetoothGattServerCallback.unwrap().onMtuChanged(
-                mBluetoothDevice,
-                1);
-    }
-
-    @Test
-    public void testOnNotificationSent_notCrash() {
-        mTestBluetoothGattServerCallback.unwrap().onNotificationSent(
-                mBluetoothDevice,
-                1);
-    }
-
-    @Test
-    public void testOnServiceAdded_notCrash() {
-        mTestBluetoothGattServerCallback.unwrap().onServiceAdded(1, mBluetoothGattService);
-    }
-
-    private static class TestBluetoothGattServerCallback extends BluetoothGattServerCallback { }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerTest.java
deleted file mode 100644
index 48283d1..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerTest.java
+++ /dev/null
@@ -1,149 +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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.UUID;
-
-/**
- * Unit tests for {@link BluetoothGattServer}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothGattServerTest {
-    private static final UUID UUID_CONST = UUID.randomUUID();
-    private static final byte[] BYTES = new byte[]{1, 2, 3};
-
-    @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
-    @Mock private android.bluetooth.BluetoothGattServer mBluetoothGattServer;
-    @Mock private android.bluetooth.BluetoothGattService mBluetoothGattService;
-    @Mock private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
-
-    BluetoothGattServer mTestabilityBluetoothGattServer;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mTestabilityBluetoothGattServer = BluetoothGattServer.wrap(mBluetoothGattServer);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
-        assertThat(mTestabilityBluetoothGattServer).isNotNull();
-        assertThat(mTestabilityBluetoothGattServer.unwrap()).isSameInstanceAs(mBluetoothGattServer);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConnect_callsWrapped() {
-        when(mBluetoothGattServer
-                .connect(mBluetoothDevice, true))
-                .thenReturn(true);
-        assertThat(mTestabilityBluetoothGattServer
-                .connect(BluetoothDevice.wrap(mBluetoothDevice), true))
-                .isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testAddService_callsWrapped() {
-        when(mBluetoothGattServer
-                .addService(mBluetoothGattService))
-                .thenReturn(true);
-        assertThat(mTestabilityBluetoothGattServer
-                .addService(mBluetoothGattService))
-                .isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testClearServices_callsWrapped() {
-        doNothing().when(mBluetoothGattServer).clearServices();
-        mTestabilityBluetoothGattServer.clearServices();
-        verify(mBluetoothGattServer).clearServices();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testClose_callsWrapped() {
-        doNothing().when(mBluetoothGattServer).close();
-        mTestabilityBluetoothGattServer.close();
-        verify(mBluetoothGattServer).close();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNotifyCharacteristicChanged_callsWrapped() {
-        when(mBluetoothGattServer
-                .notifyCharacteristicChanged(
-                        mBluetoothDevice,
-                        mBluetoothGattCharacteristic,
-                        true))
-                .thenReturn(true);
-        assertThat(mTestabilityBluetoothGattServer
-                .notifyCharacteristicChanged(
-                        BluetoothDevice.wrap(mBluetoothDevice),
-                        mBluetoothGattCharacteristic,
-                        true))
-                .isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSendResponse_callsWrapped() {
-        when(mBluetoothGattServer.sendResponse(
-                mBluetoothDevice, 1, 1, 1, BYTES)).thenReturn(true);
-        mTestabilityBluetoothGattServer.sendResponse(
-                BluetoothDevice.wrap(mBluetoothDevice), 1, 1, 1, BYTES);
-        verify(mBluetoothGattServer).sendResponse(
-                mBluetoothDevice, 1, 1, 1, BYTES);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testCancelConnection_callsWrapped() {
-        doNothing().when(mBluetoothGattServer).cancelConnection(mBluetoothDevice);
-        mTestabilityBluetoothGattServer.cancelConnection(BluetoothDevice.wrap(mBluetoothDevice));
-        verify(mBluetoothGattServer).cancelConnection(mBluetoothDevice);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetService_callsWrapped() {
-        when(mBluetoothGattServer.getService(UUID_CONST)).thenReturn(null);
-        assertThat(mTestabilityBluetoothGattServer.getService(UUID_CONST)).isNull();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapperTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapperTest.java
deleted file mode 100644
index a03a255..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapperTest.java
+++ /dev/null
@@ -1,190 +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.server.nearby.common.bluetooth.testability.android.bluetooth;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.UUID;
-
-/**
- * Unit tests for {@link BluetoothGattWrapper}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothGattWrapperTest {
-    private static final UUID UUID_CONST = UUID.randomUUID();
-    private static final byte[] BYTES = new byte[]{1, 2, 3};
-
-    @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
-    @Mock private android.bluetooth.BluetoothGatt mBluetoothGatt;
-    @Mock private android.bluetooth.BluetoothGattService mBluetoothGattService;
-    @Mock private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
-    @Mock private android.bluetooth.BluetoothGattDescriptor mBluetoothGattDescriptor;
-
-    BluetoothGattWrapper mBluetoothGattWrapper;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mBluetoothGattWrapper = BluetoothGattWrapper.wrap(mBluetoothGatt);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
-        assertThat(mBluetoothGattWrapper).isNotNull();
-        assertThat(mBluetoothGattWrapper.unwrap()).isSameInstanceAs(mBluetoothGatt);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testEquality_asExpected() {
-        assertThat(mBluetoothGattWrapper.equals(null)).isFalse();
-        assertThat(mBluetoothGattWrapper.equals(mBluetoothGattWrapper)).isTrue();
-        assertThat(mBluetoothGattWrapper.equals(BluetoothGattWrapper.wrap(mBluetoothGatt)))
-                .isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetDevice_callsWrapped() {
-        when(mBluetoothGatt.getDevice()).thenReturn(mBluetoothDevice);
-        assertThat(mBluetoothGattWrapper.getDevice().unwrap()).isSameInstanceAs(mBluetoothDevice);
-    }
-
-    @Test
-    public void testHashCode_asExpected() {
-        assertThat(mBluetoothGattWrapper.hashCode())
-                .isEqualTo(BluetoothGattWrapper.wrap(mBluetoothGatt).hashCode());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetServices_callsWrapped() {
-        when(mBluetoothGatt.getServices()).thenReturn(null);
-        assertThat(mBluetoothGattWrapper.getServices()).isNull();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetService_callsWrapped() {
-        when(mBluetoothGatt.getService(UUID_CONST)).thenReturn(mBluetoothGattService);
-        assertThat(mBluetoothGattWrapper.getService(UUID_CONST))
-                .isSameInstanceAs(mBluetoothGattService);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testDiscoverServices_callsWrapped() {
-        when(mBluetoothGatt.discoverServices()).thenReturn(true);
-        assertThat(mBluetoothGattWrapper.discoverServices()).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testReadCharacteristic_callsWrapped() {
-        when(mBluetoothGatt.readCharacteristic(mBluetoothGattCharacteristic)).thenReturn(true);
-        assertThat(mBluetoothGattWrapper.readCharacteristic(mBluetoothGattCharacteristic)).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWriteCharacteristic_callsWrapped() {
-        when(mBluetoothGatt.writeCharacteristic(mBluetoothGattCharacteristic, BYTES, 1))
-                .thenReturn(1);
-        assertThat(mBluetoothGattWrapper.writeCharacteristic(
-                mBluetoothGattCharacteristic, BYTES, 1)).isEqualTo(1);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testReadDescriptor_callsWrapped() {
-        when(mBluetoothGatt.readDescriptor(mBluetoothGattDescriptor)).thenReturn(false);
-        assertThat(mBluetoothGattWrapper.readDescriptor(mBluetoothGattDescriptor)).isFalse();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWriteDescriptor_callsWrapped() {
-        when(mBluetoothGatt.writeDescriptor(mBluetoothGattDescriptor, BYTES)).thenReturn(5);
-        assertThat(mBluetoothGattWrapper.writeDescriptor(mBluetoothGattDescriptor, BYTES))
-                .isEqualTo(5);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testReadRemoteRssi_callsWrapped() {
-        when(mBluetoothGatt.readRemoteRssi()).thenReturn(false);
-        assertThat(mBluetoothGattWrapper.readRemoteRssi()).isFalse();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testRequestConnectionPriority_callsWrapped() {
-        when(mBluetoothGatt.requestConnectionPriority(5)).thenReturn(false);
-        assertThat(mBluetoothGattWrapper.requestConnectionPriority(5)).isFalse();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testRequestMtu_callsWrapped() {
-        when(mBluetoothGatt.requestMtu(5)).thenReturn(false);
-        assertThat(mBluetoothGattWrapper.requestMtu(5)).isFalse();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSetCharacteristicNotification_callsWrapped() {
-        when(mBluetoothGatt.setCharacteristicNotification(mBluetoothGattCharacteristic, true))
-                .thenReturn(false);
-        assertThat(mBluetoothGattWrapper
-                .setCharacteristicNotification(mBluetoothGattCharacteristic, true)).isFalse();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testDisconnect_callsWrapped() {
-        doNothing().when(mBluetoothGatt).disconnect();
-        mBluetoothGattWrapper.disconnect();
-        verify(mBluetoothGatt).disconnect();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testClose_callsWrapped() {
-        doNothing().when(mBluetoothGatt).close();
-        mBluetoothGattWrapper.close();
-        verify(mBluetoothGatt).close();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothAdvertiserTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothAdvertiserTest.java
deleted file mode 100644
index 8468ed1..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothAdvertiserTest.java
+++ /dev/null
@@ -1,101 +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.server.nearby.common.bluetooth.testability.android.bluetooth.le;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.verify;
-
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Unit tests for {@link BluetoothLeAdvertiser}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothAdvertiserTest {
-    @Mock android.bluetooth.le.BluetoothLeAdvertiser mWrappedBluetoothLeAdvertiser;
-    @Mock AdvertiseSettings mAdvertiseSettings;
-    @Mock AdvertiseData mAdvertiseData;
-    @Mock AdvertiseCallback mAdvertiseCallback;
-
-    BluetoothLeAdvertiser mBluetoothLeAdvertiser;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mBluetoothLeAdvertiser = BluetoothLeAdvertiser.wrap(mWrappedBluetoothLeAdvertiser);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWrapNullAdapter_isNull() {
-        assertThat(BluetoothLeAdvertiser.wrap(null)).isNull();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
-        assertThat(mWrappedBluetoothLeAdvertiser).isNotNull();
-        assertThat(mBluetoothLeAdvertiser.unwrap()).isSameInstanceAs(mWrappedBluetoothLeAdvertiser);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testStartAdvertisingThreeParameters_callsWrapped() {
-        doNothing().when(mWrappedBluetoothLeAdvertiser)
-                .startAdvertising(mAdvertiseSettings, mAdvertiseData, mAdvertiseCallback);
-        mBluetoothLeAdvertiser
-                .startAdvertising(mAdvertiseSettings, mAdvertiseData, mAdvertiseCallback);
-        verify(mWrappedBluetoothLeAdvertiser).startAdvertising(
-                mAdvertiseSettings, mAdvertiseData, mAdvertiseCallback);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testStartAdvertisingFourParameters_callsWrapped() {
-        doNothing().when(mWrappedBluetoothLeAdvertiser).startAdvertising(
-                mAdvertiseSettings, mAdvertiseData, mAdvertiseData, mAdvertiseCallback);
-        mBluetoothLeAdvertiser.startAdvertising(
-                mAdvertiseSettings, mAdvertiseData, mAdvertiseData, mAdvertiseCallback);
-        verify(mWrappedBluetoothLeAdvertiser).startAdvertising(
-                mAdvertiseSettings, mAdvertiseData, mAdvertiseData, mAdvertiseCallback);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testStopAdvertising_callsWrapped() {
-        doNothing().when(mWrappedBluetoothLeAdvertiser).stopAdvertising(mAdvertiseCallback);
-        mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
-        verify(mWrappedBluetoothLeAdvertiser).stopAdvertising(mAdvertiseCallback);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScannerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScannerTest.java
deleted file mode 100644
index 3fce54f..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScannerTest.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.server.nearby.common.bluetooth.testability.android.bluetooth.le;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.PendingIntent;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanSettings;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.google.common.collect.ImmutableList;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Unit tests for {@link BluetoothLeScanner}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothLeScannerTest {
-    @Mock android.bluetooth.le.BluetoothLeScanner mWrappedBluetoothLeScanner;
-    @Mock PendingIntent mPendingIntent;
-    @Mock ScanSettings mScanSettings;
-    @Mock ScanFilter mScanFilter;
-
-    TestScanCallback mTestScanCallback = new TestScanCallback();
-    BluetoothLeScanner mBluetoothLeScanner;
-    ImmutableList<ScanFilter> mImmutableScanFilterList;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mBluetoothLeScanner = BluetoothLeScanner.wrap(mWrappedBluetoothLeScanner);
-        mImmutableScanFilterList = ImmutableList.of(mScanFilter);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWrapNullAdapter_isNull() {
-        assertThat(BluetoothLeAdvertiser.wrap(null)).isNull();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
-        assertThat(mWrappedBluetoothLeScanner).isNotNull();
-        assertThat(mBluetoothLeScanner.unwrap()).isSameInstanceAs(mWrappedBluetoothLeScanner);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testStartScan_callsWrapped() {
-        doNothing().when(mWrappedBluetoothLeScanner).startScan(mTestScanCallback.unwrap());
-        mBluetoothLeScanner.startScan(mTestScanCallback);
-        verify(mWrappedBluetoothLeScanner).startScan(mTestScanCallback.unwrap());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testStartScanWithFiltersCallback_callsWrapped() {
-        doNothing().when(mWrappedBluetoothLeScanner)
-                .startScan(mImmutableScanFilterList, mScanSettings, mTestScanCallback.unwrap());
-        mBluetoothLeScanner.startScan(mImmutableScanFilterList, mScanSettings, mTestScanCallback);
-        verify(mWrappedBluetoothLeScanner)
-                .startScan(mImmutableScanFilterList, mScanSettings, mTestScanCallback.unwrap());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testStartScanWithFiltersCallbackIntent_callsWrapped() {
-        when(mWrappedBluetoothLeScanner.startScan(
-                mImmutableScanFilterList, mScanSettings, mPendingIntent)).thenReturn(1);
-        mBluetoothLeScanner.startScan(mImmutableScanFilterList, mScanSettings, mPendingIntent);
-        verify(mWrappedBluetoothLeScanner)
-                .startScan(mImmutableScanFilterList, mScanSettings, mPendingIntent);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testStopScan_callsWrapped() {
-        doNothing().when(mWrappedBluetoothLeScanner).stopScan(mTestScanCallback.unwrap());
-        mBluetoothLeScanner.stopScan(mTestScanCallback);
-        verify(mWrappedBluetoothLeScanner).stopScan(mTestScanCallback.unwrap());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testStopScanPendingIntent_callsWrapped() {
-        doNothing().when(mWrappedBluetoothLeScanner).stopScan(mPendingIntent);
-        mBluetoothLeScanner.stopScan(mPendingIntent);
-        verify(mWrappedBluetoothLeScanner).stopScan(mPendingIntent);
-    }
-
-    private static class TestScanCallback extends ScanCallback {};
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallbackTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallbackTest.java
deleted file mode 100644
index 6d68486..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallbackTest.java
+++ /dev/null
@@ -1,64 +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.server.nearby.common.bluetooth.testability.android.bluetooth.le;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.google.common.collect.ImmutableList;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Unit tests for {@link ScanCallback}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class ScanCallbackTest {
-    @Mock android.bluetooth.le.ScanResult mScanResult;
-
-    TestScanCallback mTestScanCallback = new TestScanCallback();
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Test
-    public void testOnScanFailed_notCrash() {
-        mTestScanCallback.unwrap().onScanFailed(1);
-    }
-
-    @Test
-    public void testOnScanResult_notCrash() {
-        mTestScanCallback.unwrap().onScanResult(1, mScanResult);
-    }
-
-    @Test
-    public void testOnBatchScanResult_notCrash() {
-        mTestScanCallback.unwrap().onBatchScanResults(ImmutableList.of(mScanResult));
-    }
-
-    private static class TestScanCallback extends ScanCallback { }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResultTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResultTest.java
deleted file mode 100644
index 255c178..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResultTest.java
+++ /dev/null
@@ -1,76 +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.server.nearby.common.bluetooth.testability.android.bluetooth.le;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Unit tests for {@link ScanResult}.
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class ScanResultTest {
-
-    @Mock android.bluetooth.le.ScanResult mWrappedScanResult;
-    @Mock android.bluetooth.le.ScanRecord mScanRecord;
-    @Mock android.bluetooth.BluetoothDevice mBluetoothDevice;
-    ScanResult mScanResult;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mScanResult = ScanResult.wrap(mWrappedScanResult);
-    }
-
-    @Test
-    public void testGetScanRecord_calledWrapped() {
-        when(mWrappedScanResult.getScanRecord()).thenReturn(mScanRecord);
-        assertThat(mScanResult.getScanRecord()).isSameInstanceAs(mScanRecord);
-    }
-
-    @Test
-    public void testGetRssi_calledWrapped() {
-        when(mWrappedScanResult.getRssi()).thenReturn(3);
-        assertThat(mScanResult.getRssi()).isEqualTo(3);
-    }
-
-    @Test
-    public void testGetTimestampNanos_calledWrapped() {
-        when(mWrappedScanResult.getTimestampNanos()).thenReturn(4L);
-        assertThat(mScanResult.getTimestampNanos()).isEqualTo(4L);
-    }
-
-    @Test
-    public void testGetDevice_calledWrapped() {
-        when(mWrappedScanResult.getDevice()).thenReturn(mBluetoothDevice);
-        assertThat(mScanResult.getDevice().unwrap()).isSameInstanceAs(mBluetoothDevice);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java
deleted file mode 100644
index 7535c06..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java
+++ /dev/null
@@ -1,63 +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.server.nearby.common.bluetooth.util;
-
-import static com.android.server.nearby.common.bluetooth.util.BluetoothGattUtils.getMessageForStatusCode;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.bluetooth.BluetoothGatt;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-
-/** Unit tests for {@link BluetoothGattUtils}. */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BluetoothGattUtilsTest {
-    private static final ImmutableSet<String> GATT_HIDDEN_CONSTANTS = ImmutableSet.of(
-            "GATT_WRITE_REQUEST_BUSY", "GATT_WRITE_REQUEST_FAIL", "GATT_WRITE_REQUEST_SUCCESS");
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetMessageForStatusCode() throws Exception {
-        Field[] publicFields = BluetoothGatt.class.getFields();
-        for (Field field : publicFields) {
-            if ((field.getModifiers() & Modifier.STATIC) == 0
-                    || field.getDeclaringClass() != BluetoothGatt.class) {
-                continue;
-            }
-            String fieldName = field.getName();
-            if (!fieldName.startsWith("GATT_") || GATT_HIDDEN_CONSTANTS.contains(fieldName)) {
-                continue;
-            }
-            int fieldValue = (Integer) field.get(null);
-            assertThat(getMessageForStatusCode(fieldValue)).isEqualTo(fieldName);
-        }
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutorTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutorTest.java
deleted file mode 100644
index 7b3ebab..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutorTest.java
+++ /dev/null
@@ -1,321 +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.server.nearby.common.bluetooth.util;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import android.bluetooth.BluetoothGatt;
-
-import androidx.test.filters.SdkSuppress;
-
-import com.android.server.nearby.common.bluetooth.BluetoothException;
-import com.android.server.nearby.common.bluetooth.testability.NonnullProvider;
-import com.android.server.nearby.common.bluetooth.testability.TimeProvider;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
-import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.SynchronousOperation;
-
-import junit.framework.TestCase;
-
-import org.mockito.Mock;
-
-import java.util.Arrays;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-
-/**
- * Unit tests for {@link BluetoothOperationExecutor}.
- */
-public class BluetoothOperationExecutorTest extends TestCase {
-
-    private static final String OPERATION_RESULT = "result";
-    private static final String EXCEPTION_REASON = "exception";
-    private static final long TIME = 1234;
-    private static final long TIMEOUT = 121212;
-
-    @Mock
-    private NonnullProvider<BlockingQueue<Object>> mMockBlockingQueueProvider;
-    @Mock
-    private TimeProvider mMockTimeProvider;
-    @Mock
-    private BlockingQueue<Object> mMockBlockingQueue;
-    @Mock
-    private Semaphore mMockSemaphore;
-    @Mock
-    private Operation<String> mMockStringOperation;
-    @Mock
-    private Operation<Void> mMockVoidOperation;
-    @Mock
-    private Future<Object> mMockFuture;
-    @Mock
-    private Future<Object> mMockFuture2;
-
-    private BluetoothOperationExecutor mBluetoothOperationExecutor;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        initMocks(this);
-
-        when(mMockBlockingQueueProvider.get()).thenReturn(mMockBlockingQueue);
-        when(mMockSemaphore.tryAcquire()).thenReturn(true);
-        when(mMockTimeProvider.getTimeMillis()).thenReturn(TIME);
-
-        mBluetoothOperationExecutor =
-                new BluetoothOperationExecutor(mMockSemaphore, mMockTimeProvider,
-                        mMockBlockingQueueProvider);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testExecute() throws Exception {
-        when(mMockBlockingQueue.take()).thenReturn(OPERATION_RESULT);
-
-        String result = mBluetoothOperationExecutor.execute(mMockStringOperation);
-
-        verify(mMockStringOperation).execute(mBluetoothOperationExecutor);
-        assertThat(result).isEqualTo(OPERATION_RESULT);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testExecuteWithTimeout() throws Exception {
-        when(mMockBlockingQueue.poll(TIMEOUT, TimeUnit.MILLISECONDS)).thenReturn(OPERATION_RESULT);
-
-        String result = mBluetoothOperationExecutor.execute(mMockStringOperation, TIMEOUT);
-
-        verify(mMockStringOperation).execute(mBluetoothOperationExecutor);
-        assertThat(result).isEqualTo(OPERATION_RESULT);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testSchedule() throws Exception {
-        when(mMockBlockingQueue.poll(TIMEOUT, TimeUnit.MILLISECONDS)).thenReturn(OPERATION_RESULT);
-
-        Future<String> result = mBluetoothOperationExecutor.schedule(mMockStringOperation);
-
-        verify(mMockStringOperation).execute(mBluetoothOperationExecutor);
-        assertThat(result.get(TIMEOUT, TimeUnit.MILLISECONDS)).isEqualTo(OPERATION_RESULT);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testScheduleOtherOperationInProgress() throws Exception {
-        when(mMockSemaphore.tryAcquire()).thenReturn(false);
-        when(mMockBlockingQueue.poll(TIMEOUT, TimeUnit.MILLISECONDS)).thenReturn(OPERATION_RESULT);
-
-        Future<String> result = mBluetoothOperationExecutor.schedule(mMockStringOperation);
-
-        verify(mMockStringOperation, never()).run();
-
-        when(mMockSemaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)).thenReturn(true);
-
-        assertThat(result.get(TIMEOUT, TimeUnit.MILLISECONDS)).isEqualTo(OPERATION_RESULT);
-        verify(mMockStringOperation).execute(mBluetoothOperationExecutor);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNotifySuccessWithResult() throws Exception {
-        when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
-        Future<String> future = mBluetoothOperationExecutor.schedule(mMockStringOperation);
-
-        mBluetoothOperationExecutor.notifySuccess(mMockStringOperation, OPERATION_RESULT);
-
-        assertThat(future.get(1, TimeUnit.MILLISECONDS)).isEqualTo(OPERATION_RESULT);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNotifySuccessTwice() throws Exception {
-        BlockingQueue<Object> resultQueue = new LinkedBlockingDeque<Object>();
-        when(mMockBlockingQueueProvider.get()).thenReturn(resultQueue);
-        Future<String> future = mBluetoothOperationExecutor.schedule(mMockStringOperation);
-
-        mBluetoothOperationExecutor.notifySuccess(mMockStringOperation, OPERATION_RESULT);
-
-        assertThat(future.get(1, TimeUnit.MILLISECONDS)).isEqualTo(OPERATION_RESULT);
-
-        // the second notification should be ignored
-        mBluetoothOperationExecutor.notifySuccess(mMockStringOperation, OPERATION_RESULT);
-        assertThat(resultQueue).isEmpty();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNotifySuccessWithNullResult() throws Exception {
-        when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
-        Future<String> future = mBluetoothOperationExecutor.schedule(mMockStringOperation);
-
-        mBluetoothOperationExecutor.notifySuccess(mMockStringOperation, null);
-
-        assertThat(future.get(1, TimeUnit.MILLISECONDS)).isNull();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNotifySuccess() throws Exception {
-        when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
-        Future<Void> future = mBluetoothOperationExecutor.schedule(mMockVoidOperation);
-
-        mBluetoothOperationExecutor.notifySuccess(mMockVoidOperation);
-
-        future.get(1, TimeUnit.MILLISECONDS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNotifyCompletionSuccess() throws Exception {
-        when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
-        Future<Void> future = mBluetoothOperationExecutor.schedule(mMockVoidOperation);
-
-        mBluetoothOperationExecutor
-                .notifyCompletion(mMockVoidOperation, BluetoothGatt.GATT_SUCCESS);
-
-        future.get(1, TimeUnit.MILLISECONDS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNotifyCompletionFailure() throws Exception {
-        when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
-        Future<Void> future = mBluetoothOperationExecutor.schedule(mMockVoidOperation);
-
-        mBluetoothOperationExecutor
-                .notifyCompletion(mMockVoidOperation, BluetoothGatt.GATT_FAILURE);
-
-        try {
-            BluetoothOperationExecutor.getResult(future, 1);
-            fail("Expected BluetoothException");
-        } catch (BluetoothException e) {
-            //expected
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testNotifyFailure() throws Exception {
-        when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
-        Future<Void> future = mBluetoothOperationExecutor.schedule(mMockVoidOperation);
-
-        mBluetoothOperationExecutor
-                .notifyFailure(mMockVoidOperation, new BluetoothException("test"));
-
-        try {
-            BluetoothOperationExecutor.getResult(future, 1);
-            fail("Expected BluetoothException");
-        } catch (BluetoothException e) {
-            //expected
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWaitFor() throws Exception {
-        mBluetoothOperationExecutor.waitFor(Arrays.asList(mMockFuture, mMockFuture2));
-
-        verify(mMockFuture).get();
-        verify(mMockFuture2).get();
-    }
-
-    @SuppressWarnings("unchecked")
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testWaitForWithTimeout() throws Exception {
-        mBluetoothOperationExecutor.waitFor(
-                Arrays.asList(mMockFuture, mMockFuture2),
-                TIMEOUT);
-
-        verify(mMockFuture).get(TIMEOUT, TimeUnit.MILLISECONDS);
-        verify(mMockFuture2).get(TIMEOUT, TimeUnit.MILLISECONDS);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetResult() throws Exception {
-        when(mMockFuture.get()).thenReturn(OPERATION_RESULT);
-
-        Object result = BluetoothOperationExecutor.getResult(mMockFuture);
-
-        assertThat(result).isEqualTo(OPERATION_RESULT);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testGetResultWithTimeout() throws Exception {
-        when(mMockFuture.get(TIMEOUT, TimeUnit.MILLISECONDS)).thenThrow(new TimeoutException());
-
-        try {
-            BluetoothOperationExecutor.getResult(mMockFuture, TIMEOUT);
-            fail("Expected BluetoothOperationTimeoutException");
-        } catch (BluetoothOperationTimeoutException e) {
-            //expected
-        }
-        verify(mMockFuture).cancel(true);
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_SynchronousOperation_execute() throws Exception {
-        when(mMockBlockingQueueProvider.get()).thenReturn(mMockBlockingQueue);
-        SynchronousOperation<String> synchronousOperation = new SynchronousOperation<String>() {
-            @Override
-            public String call() throws BluetoothException {
-                return OPERATION_RESULT;
-            }
-        };
-
-        @SuppressWarnings("unused") // future return.
-        Future<?> possiblyIgnoredError = mBluetoothOperationExecutor.schedule(synchronousOperation);
-
-        verify(mMockBlockingQueue).add(OPERATION_RESULT);
-        verify(mMockSemaphore).release();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_SynchronousOperation_exception() throws Exception {
-        final BluetoothException exception = new BluetoothException(EXCEPTION_REASON);
-        when(mMockBlockingQueueProvider.get()).thenReturn(mMockBlockingQueue);
-        SynchronousOperation<String> synchronousOperation = new SynchronousOperation<String>() {
-            @Override
-            public String call() throws BluetoothException {
-                throw exception;
-            }
-        };
-
-        @SuppressWarnings("unused") // future return.
-        Future<?> possiblyIgnoredError = mBluetoothOperationExecutor.schedule(synchronousOperation);
-
-        verify(mMockBlockingQueue).add(exception);
-        verify(mMockSemaphore).release();
-    }
-
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void test_AsynchronousOperation_exception() throws Exception {
-        final BluetoothException exception = new BluetoothException(EXCEPTION_REASON);
-        when(mMockBlockingQueueProvider.get()).thenReturn(mMockBlockingQueue);
-        Operation<String> operation = new Operation<String>() {
-            @Override
-            public void run() throws BluetoothException {
-                throw exception;
-            }
-        };
-
-        @SuppressWarnings("unused") // future return.
-        Future<?> possiblyIgnoredError = mBluetoothOperationExecutor.schedule(operation);
-
-        verify(mMockBlockingQueue).add(exception);
-        verify(mMockSemaphore).release();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
deleted file mode 100644
index bebb2f2..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
+++ /dev/null
@@ -1,88 +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.server.nearby.common.eventloop;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.filters.SdkSuppress;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-public class EventLoopTest {
-    private static final String TAG = "EventLoopTest";
-
-    private final EventLoop mEventLoop = EventLoop.newInstance(TAG);
-    private final List<Integer> mExecutedRunnables = new ArrayList<>();
-    @Rule
-    public ExpectedException thrown = ExpectedException.none();
-
-    @Test
-    public void remove() {
-        mEventLoop.postRunnable(new NumberedRunnable(0));
-        NumberedRunnable runnableToAddAndRemove = new NumberedRunnable(1);
-        mEventLoop.postRunnable(runnableToAddAndRemove);
-        mEventLoop.removeRunnable(runnableToAddAndRemove);
-        mEventLoop.postRunnable(new NumberedRunnable(2));
-        assertThat(mExecutedRunnables).doesNotContain(1);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void isPosted() {
-        NumberedRunnable runnable = new NumberedRunnable(0);
-        mEventLoop.postRunnableDelayed(runnable, 10 * 1000L);
-        assertThat(mEventLoop.isPosted(runnable)).isTrue();
-        mEventLoop.removeRunnable(runnable);
-        assertThat(mEventLoop.isPosted(runnable)).isFalse();
-
-        // Let a runnable execute, then verify that it's not posted.
-        mEventLoop.postRunnable(runnable);
-        assertThat(mEventLoop.isPosted(runnable)).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void postAndWaitAfterDestroy() throws InterruptedException {
-        mEventLoop.destroy();
-        mEventLoop.postAndWait(new NumberedRunnable(0));
-
-        assertThat(mExecutedRunnables).isEmpty();
-    }
-
-
-    private class NumberedRunnable extends NamedRunnable {
-        private final int mId;
-
-        private NumberedRunnable(int id) {
-            super("NumberedRunnable:" + id);
-            this.mId = id;
-        }
-
-        @Override
-        public void run() {
-            // Note: when running in robolectric, this is not actually executed on a different
-            // thread, it's executed in the same thread the test runs in, so this is safe.
-            mExecutedRunnables.add(mId);
-        }
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/HandlerEventLoopImplTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/HandlerEventLoopImplTest.java
deleted file mode 100644
index 4775456..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/HandlerEventLoopImplTest.java
+++ /dev/null
@@ -1,92 +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.server.nearby.common.eventloop;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.filters.SdkSuppress;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class HandlerEventLoopImplTest {
-    private static final String TAG = "HandlerEventLoopImplTest";
-    private final HandlerEventLoopImpl mHandlerEventLoopImpl =
-            new HandlerEventLoopImpl(TAG);
-    private final List<Integer> mExecutedRunnables = new ArrayList<>();
-    @Rule
-    public ExpectedException thrown = ExpectedException.none();
-
-    @Test
-    public void remove() {
-        mHandlerEventLoopImpl.postRunnable(new NumberedRunnable(0));
-        NumberedRunnable runnableToAddAndRemove = new NumberedRunnable(1);
-        mHandlerEventLoopImpl.postRunnable(runnableToAddAndRemove);
-        mHandlerEventLoopImpl.removeRunnable(runnableToAddAndRemove);
-        mHandlerEventLoopImpl.postRunnable(new NumberedRunnable(2));
-        assertThat(mExecutedRunnables).doesNotContain(1);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void isPosted() {
-        NumberedRunnable runnable = new HandlerEventLoopImplTest.NumberedRunnable(0);
-        mHandlerEventLoopImpl.postRunnableDelayed(runnable, 10 * 1000L);
-        assertThat(mHandlerEventLoopImpl.isPosted(runnable)).isTrue();
-        mHandlerEventLoopImpl.removeRunnable(runnable);
-        assertThat(mHandlerEventLoopImpl.isPosted(runnable)).isFalse();
-
-        // Let a runnable execute, then verify that it's not posted.
-        mHandlerEventLoopImpl.postRunnable(runnable);
-        assertThat(mHandlerEventLoopImpl.isPosted(runnable)).isTrue();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void postAndWaitAfterDestroy() throws InterruptedException {
-        mHandlerEventLoopImpl.destroy();
-        mHandlerEventLoopImpl.postAndWait(new HandlerEventLoopImplTest.NumberedRunnable(0));
-        assertThat(mExecutedRunnables).isEmpty();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void postEmptyQueueRunnable() {
-        mHandlerEventLoopImpl.postEmptyQueueRunnable(
-                new HandlerEventLoopImplTest.NumberedRunnable(0));
-    }
-
-    private class NumberedRunnable extends NamedRunnable {
-        private final int mId;
-
-        private NumberedRunnable(int id) {
-            super("NumberedRunnable:" + id);
-            this.mId = id;
-        }
-
-        @Override
-        public void run() {
-            // Note: when running in robolectric, this is not actually executed on a different
-            // thread, it's executed in the same thread the test runs in, so this is safe.
-            mExecutedRunnables.add(mId);
-        }
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/NamedRunnableTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/NamedRunnableTest.java
deleted file mode 100644
index 7005da1..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/NamedRunnableTest.java
+++ /dev/null
@@ -1,36 +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.server.nearby.common.eventloop;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-
-public class NamedRunnableTest {
-    private static final String TAG = "NamedRunnableTest";
-
-    @Test
-    public void testToString() {
-        assertThat(mNamedRunnable.toString()).isEqualTo("Runnable[" + TAG +  "]");
-    }
-
-    private final NamedRunnable mNamedRunnable = new NamedRunnable(TAG) {
-        @Override
-        public void run() {
-        }
-    };
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/fastpair/IconUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/fastpair/IconUtilsTest.java
deleted file mode 100644
index d39d9cc..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/fastpair/IconUtilsTest.java
+++ /dev/null
@@ -1,70 +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.server.nearby.common.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-
-import org.junit.Test;
-import org.mockito.Mock;
-
-public class IconUtilsTest {
-    private static final int MIN_ICON_SIZE = 16;
-    private static final int DESIRED_ICON_SIZE = 32;
-    @Mock
-    Context mContext;
-
-    @Test
-    public void isIconSizedCorrectly() {
-        // Null bitmap is not sized correctly
-        assertThat(IconUtils.isIconSizeCorrect(null)).isFalse();
-
-        int minIconSize = MIN_ICON_SIZE;
-        int desiredIconSize = DESIRED_ICON_SIZE;
-
-        // Bitmap that is 1x1 pixels is not sized correctly
-        Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
-        assertThat(IconUtils.isIconSizeCorrect(icon)).isFalse();
-
-        // Bitmap is categorized as small, and not regular
-        icon = Bitmap.createBitmap(minIconSize + 1,
-                minIconSize + 1, Bitmap.Config.ALPHA_8);
-        assertThat(IconUtils.isIconSizeCorrect(icon)).isTrue();
-        assertThat(IconUtils.isIconSizedSmall(icon)).isTrue();
-        assertThat(IconUtils.isIconSizedRegular(icon)).isFalse();
-
-        // Bitmap is categorized as regular, but not small
-        icon = Bitmap.createBitmap(desiredIconSize + 1,
-                desiredIconSize + 1, Bitmap.Config.ALPHA_8);
-        assertThat(IconUtils.isIconSizeCorrect(icon)).isTrue();
-        assertThat(IconUtils.isIconSizedSmall(icon)).isFalse();
-        assertThat(IconUtils.isIconSizedRegular(icon)).isTrue();
-    }
-
-    @Test
-    public void testAddWhiteCircleBackground() {
-        int minIconSize = MIN_ICON_SIZE;
-        Bitmap icon = Bitmap.createBitmap(minIconSize + 1, minIconSize + 1,
-                Bitmap.Config.ALPHA_8);
-
-        assertThat(
-                IconUtils.isIconSizeCorrect(IconUtils.addWhiteCircleBackground(mContext, icon)))
-                .isTrue();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/locator/LocatorTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/locator/LocatorTest.java
deleted file mode 100644
index c3a4e55..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/locator/LocatorTest.java
+++ /dev/null
@@ -1,91 +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.server.nearby.common.locator;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.server.nearby.common.eventloop.EventLoop;
-import com.android.server.nearby.fastpair.FastPairAdvHandler;
-import com.android.server.nearby.fastpair.FastPairModule;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.MockitoAnnotations;
-
-import java.time.Clock;
-
-import src.com.android.server.nearby.fastpair.testing.MockingLocator;
-
-public class LocatorTest {
-    private Locator mLocator;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mLocator = src.com.android.server.nearby.fastpair.testing.MockingLocator.withMocksOnly(
-                ApplicationProvider.getApplicationContext());
-        mLocator.bind(new FastPairModule());
-    }
-
-    @Test
-    public void genericConstructor() {
-        assertThat(mLocator.get(FastPairCacheManager.class)).isNotNull();
-        assertThat(mLocator.get(FootprintsDeviceManager.class)).isNotNull();
-        assertThat(mLocator.get(EventLoop.class)).isNotNull();
-        assertThat(mLocator.get(FastPairHalfSheetManager.class)).isNotNull();
-        assertThat(mLocator.get(FastPairAdvHandler.class)).isNotNull();
-        assertThat(mLocator.get(Clock.class)).isNotNull();
-    }
-
-    @Test
-    public void genericDestroy() {
-        mLocator.destroy();
-    }
-
-    @Test
-    public void getOptional() {
-        assertThat(mLocator.getOptional(FastPairModule.class)).isNotNull();
-        mLocator.removeBindingForTest(FastPairModule.class);
-        assertThat(mLocator.getOptional(FastPairModule.class)).isNull();
-    }
-
-    @Test
-    public void getParent() {
-        assertThat(mLocator.getParent()).isNotNull();
-    }
-
-    @Test
-    public void getUnboundErrorMessage() {
-        assertThat(mLocator.getUnboundErrorMessage(FastPairModule.class))
-                .isEqualTo(
-                        "Unbound type: com.android.server.nearby.fastpair.FastPairModule\n"
-                        + "Searched locators:\n" + "android.app.Application ->\n"
-                                + "android.app.Application ->\n" + "android.app.Application");
-    }
-
-    @Test
-    public void getContextForTest() {
-        src.com.android.server.nearby.fastpair.testing.MockingLocator  mockingLocator =
-                new MockingLocator(ApplicationProvider.getApplicationContext(), mLocator);
-        assertThat(mockingLocator.getContextForTest()).isNotNull();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/servicemonitor/PackageWatcherTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/servicemonitor/PackageWatcherTest.java
deleted file mode 100644
index eafc7db..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/common/servicemonitor/PackageWatcherTest.java
+++ /dev/null
@@ -1,45 +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.server.nearby.common.servicemonitor;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Intent;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import org.junit.Test;
-
-public class PackageWatcherTest {
-    private PackageWatcher mPackageWatcher = new PackageWatcher() {
-        @Override
-        public void onSomePackagesChanged() {
-        }
-    };
-
-    @Test
-    public void getPackageName() {
-        Intent intent = new Intent("Action", null);
-        assertThat(mPackageWatcher.getPackageName(intent)).isNull();
-    }
-
-    @Test
-    public void onReceive() {
-        Intent intent = new Intent(Intent.ACTION_PACKAGES_UNSUSPENDED, null);
-        mPackageWatcher.onReceive(ApplicationProvider.getApplicationContext(), intent);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
deleted file mode 100644
index 900b618..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
+++ /dev/null
@@ -1,280 +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.server.nearby.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.accounts.Account;
-import android.content.Context;
-import android.nearby.FastPairDevice;
-
-import com.android.server.nearby.common.bloomfilter.BloomFilter;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
-import com.android.server.nearby.provider.FastPairDataProvider;
-
-import com.google.common.collect.ImmutableList;
-import com.google.protobuf.ByteString;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.time.Clock;
-import java.util.List;
-
-import service.proto.Cache;
-import service.proto.Data;
-import service.proto.Rpcs;
-
-public class FastPairAdvHandlerTest {
-    @Mock
-    private Context mContext;
-    @Mock
-    private FastPairDataProvider mFastPairDataProvider;
-    @Mock
-    private FastPairHalfSheetManager mFastPairHalfSheetManager;
-    @Mock
-    private FastPairNotificationManager mFastPairNotificationManager;
-    @Mock
-    private FastPairCacheManager mFastPairCacheManager;
-    @Mock
-    private FastPairController mFastPairController;
-    @Mock
-    private Data.FastPairDeviceWithAccountKey mFastPairDeviceWithAccountKey;
-    @Mock
-    private BloomFilter mBloomFilter;
-    @Mock
-    Cache.StoredFastPairItem mStoredFastPairItem;
-    @Mock private Clock mClock;
-
-    private final Account mAccount = new Account("test1@gmail.com", "com.google");
-    private static final byte[] ACCOUNT_KEY =
-            new byte[] {4, 65, 90, -26, -5, -38, -128, 40, -103, 101, 95, 55, 8, -42, -120, 78};
-    private static final byte[] ACCOUNT_KEY_2 = new byte[] {0, 1, 2};
-    private static final String BLUETOOTH_ADDRESS = "AA:BB:CC:DD";
-    private static final String MODEL_ID = "MODEL_ID";
-    private static final int CLOSE_RSSI = -80;
-    private static final int FAR_AWAY_RSSI = -120;
-    private static final int TX_POWER = -70;
-    private static final byte[] INITIAL_BYTE_ARRAY = new byte[]{0x01, 0x02, 0x03};
-    private static final byte[] SUBSEQUENT_DATA_BYTES = new byte[]{
-            0, -112, -63, 32, 37, -20, 36, 0, -60, 0, -96, 17, -10, 51, -28, -28, 100};
-    private static final byte[] SUBSEQUENT_DATA_BYTES_INVALID = new byte[]{
-            0, -112, -63, 32, 37, -20, 48, 0, -60, 0, 90, 17, -10, 51, -28, -28, 100};
-    private static final byte[] SALT = new byte[]{0x01};
-    private static final Cache.StoredDiscoveryItem STORED_DISCOVERY_ITEM =
-            Cache.StoredDiscoveryItem.newBuilder()
-                    .setDeviceName("Device Name")
-                    .setTxPower(TX_POWER)
-                    .setMacAddress(BLUETOOTH_ADDRESS)
-                    .build();
-
-    LocatorContextWrapper mLocatorContextWrapper;
-    FastPairAdvHandler mFastPairAdvHandler;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-
-        mLocatorContextWrapper = new LocatorContextWrapper(mContext);
-        mLocatorContextWrapper.getLocator().overrideBindingForTest(
-                FastPairHalfSheetManager.class, mFastPairHalfSheetManager
-        );
-        mLocatorContextWrapper.getLocator().overrideBindingForTest(
-                FastPairNotificationManager.class, mFastPairNotificationManager
-        );
-        mLocatorContextWrapper.getLocator().overrideBindingForTest(
-                FastPairCacheManager.class, mFastPairCacheManager
-        );
-        mLocatorContextWrapper.getLocator().overrideBindingForTest(
-                FastPairController.class, mFastPairController);
-        mLocatorContextWrapper.getLocator().overrideBindingForTest(Clock.class, mClock);
-
-        when(mFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(any()))
-                .thenReturn(Rpcs.GetObservedDeviceResponse.getDefaultInstance());
-        when(mFastPairDataProvider.loadFastPairEligibleAccounts()).thenReturn(List.of(mAccount));
-        when(mFastPairDataProvider.loadFastPairDeviceWithAccountKey(mAccount))
-                .thenReturn(List.of(mFastPairDeviceWithAccountKey));
-        when(mFastPairDataProvider.loadFastPairDeviceWithAccountKey(eq(mAccount), any()))
-                .thenReturn(List.of(mFastPairDeviceWithAccountKey));
-        when(mFastPairDeviceWithAccountKey.getAccountKey())
-                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY));
-        when(mFastPairDeviceWithAccountKey.getDiscoveryItem())
-                .thenReturn(STORED_DISCOVERY_ITEM);
-        when(mStoredFastPairItem.getAccountKey())
-                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY_2), ByteString.copyFrom(ACCOUNT_KEY_2));
-        when(mFastPairCacheManager.getAllSavedStoredFastPairItem())
-                .thenReturn(List.of(mStoredFastPairItem));
-
-        mFastPairAdvHandler = new FastPairAdvHandler(mLocatorContextWrapper, mFastPairDataProvider);
-    }
-
-    @Test
-    public void testInitialBroadcast() {
-        FastPairDevice fastPairDevice = new FastPairDevice.Builder()
-                .setData(INITIAL_BYTE_ARRAY)
-                .setBluetoothAddress(BLUETOOTH_ADDRESS)
-                .setModelId(MODEL_ID)
-                .setRssi(CLOSE_RSSI)
-                .setTxPower(TX_POWER)
-                .build();
-
-        mFastPairAdvHandler.handleBroadcast(fastPairDevice);
-
-        verify(mFastPairHalfSheetManager).showHalfSheet(any());
-    }
-
-    @Test
-    public void testInitialBroadcast_farAway_notShowHalfSheet() {
-        FastPairDevice fastPairDevice = new FastPairDevice.Builder()
-                .setData(INITIAL_BYTE_ARRAY)
-                .setBluetoothAddress(BLUETOOTH_ADDRESS)
-                .setModelId(MODEL_ID)
-                .setRssi(FAR_AWAY_RSSI)
-                .setTxPower(TX_POWER)
-                .build();
-
-        mFastPairAdvHandler.handleBroadcast(fastPairDevice);
-
-        verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
-    }
-
-    @Test
-    public void testSubsequentBroadcast_showNotification() {
-        FastPairDevice fastPairDevice = new FastPairDevice.Builder()
-                .setData(SUBSEQUENT_DATA_BYTES)
-                .setBluetoothAddress(BLUETOOTH_ADDRESS)
-                .setModelId(MODEL_ID)
-                .setRssi(CLOSE_RSSI)
-                .setTxPower(TX_POWER)
-                .build();
-        mFastPairAdvHandler.handleBroadcast(fastPairDevice);
-
-        DiscoveryItem discoveryItem =
-                new DiscoveryItem(mLocatorContextWrapper, STORED_DISCOVERY_ITEM);
-        verify(mFastPairNotificationManager).showDiscoveryNotification(eq(discoveryItem),
-                eq(ACCOUNT_KEY));
-        verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
-    }
-
-    @Test
-    public void testSubsequentBroadcast_tooFar_notShowNotification() {
-        FastPairDevice fastPairDevice = new FastPairDevice.Builder()
-                .setData(SUBSEQUENT_DATA_BYTES)
-                .setBluetoothAddress(BLUETOOTH_ADDRESS)
-                .setModelId(MODEL_ID)
-                .setRssi(FAR_AWAY_RSSI)
-                .setTxPower(TX_POWER)
-                .build();
-        mFastPairAdvHandler.handleBroadcast(fastPairDevice);
-
-        verify(mFastPairController, never()).pair(any(), any(), any());
-        verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
-    }
-
-    @Test
-    public void testSubsequentBroadcast_notRecognize_notShowNotification() {
-        FastPairDevice fastPairDevice = new FastPairDevice.Builder()
-                .setData(SUBSEQUENT_DATA_BYTES_INVALID)
-                .setBluetoothAddress(BLUETOOTH_ADDRESS)
-                .setModelId(MODEL_ID)
-                .setRssi(FAR_AWAY_RSSI)
-                .setTxPower(TX_POWER)
-                .build();
-        mFastPairAdvHandler.handleBroadcast(fastPairDevice);
-
-        verify(mFastPairController, never()).pair(any(), any(), any());
-        verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
-    }
-
-    @Test
-    public void testSubsequentBroadcast_cached_notShowNotification() {
-        when(mStoredFastPairItem.getAccountKey())
-                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
-
-        FastPairDevice fastPairDevice = new FastPairDevice.Builder()
-                .setData(SUBSEQUENT_DATA_BYTES_INVALID)
-                .setBluetoothAddress(BLUETOOTH_ADDRESS)
-                .setModelId(MODEL_ID)
-                .setRssi(FAR_AWAY_RSSI)
-                .setTxPower(TX_POWER)
-                .build();
-        mFastPairAdvHandler.handleBroadcast(fastPairDevice);
-
-        verify(mFastPairController, never()).pair(any(), any(), any());
-        verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
-    }
-
-    @Test
-    public void testFindRecognizedDevice_bloomFilterNotContains_notFound() {
-        when(mFastPairDeviceWithAccountKey.getAccountKey())
-                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
-        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(false);
-
-        assertThat(FastPairAdvHandler.findRecognizedDevice(
-                ImmutableList.of(mFastPairDeviceWithAccountKey), mBloomFilter, SALT)).isNull();
-    }
-
-    @Test
-    public void testFindRecognizedDevice_bloomFilterContains_found() {
-        when(mFastPairDeviceWithAccountKey.getAccountKey())
-                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
-        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(true);
-
-        assertThat(FastPairAdvHandler.findRecognizedDevice(
-                ImmutableList.of(mFastPairDeviceWithAccountKey), mBloomFilter, SALT)).isNotNull();
-    }
-
-    @Test
-    public void testFindRecognizedDeviceFromCachedItem_bloomFilterNotContains_notFound() {
-        when(mStoredFastPairItem.getAccountKey())
-                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
-        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(false);
-
-        assertThat(FastPairAdvHandler.findRecognizedDeviceFromCachedItem(
-                ImmutableList.of(mStoredFastPairItem), mBloomFilter, SALT)).isNull();
-    }
-
-    @Test
-    public void testFindRecognizedDeviceFromCachedItem_bloomFilterContains_found() {
-        when(mStoredFastPairItem.getAccountKey())
-                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
-        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(true);
-
-        assertThat(FastPairAdvHandler.findRecognizedDeviceFromCachedItem(
-                ImmutableList.of(mStoredFastPairItem), mBloomFilter, SALT)).isNotNull();
-    }
-
-    @Test
-    public void testGenerateBatteryData_correct() {
-        byte[] data = new byte[]
-                {0, -112, 96, 5, -125, 45, 35, 98, 98, 81, 13, 17, 3, 51, -28, -28, -28};
-        assertThat(FastPairAdvHandler.generateBatteryData(data))
-                .isEqualTo(new byte[]{51, -28, -28, -28});
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairManagerTest.java
deleted file mode 100644
index 00df1b9..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairManagerTest.java
+++ /dev/null
@@ -1,68 +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.server.nearby.fastpair;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-
-public class FastPairManagerTest {
-    private FastPairManager mFastPairManager;
-    @Mock private Context mContext;
-    private LocatorContextWrapper mLocatorContextWrapper;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-
-        mLocatorContextWrapper = new LocatorContextWrapper(mContext);
-        mFastPairManager = new FastPairManager(mLocatorContextWrapper);
-        when(mContext.getContentResolver()).thenReturn(
-                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testFastPairInit() {
-        mFastPairManager.initiate();
-
-        verify(mContext, times(1)).registerReceiver(any(), any(), anyInt());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testFastPairCleanUp() {
-        mFastPairManager.cleanUp();
-
-        verify(mContext, times(1)).unregisterReceiver(any());
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java
deleted file mode 100644
index 9cf65f4..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java
+++ /dev/null
@@ -1,27 +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.server.nearby.fastpair;
-
-import org.junit.Test;
-
-public class FlagUtilsTest {
-
-    @Test
-    public void testGetPreferencesBuilder_notCrash() {
-        FlagUtils.getPreferencesBuilder().build();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/ModuleTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/ModuleTest.java
deleted file mode 100644
index bb4e3d0..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/ModuleTest.java
+++ /dev/null
@@ -1,67 +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 src.com.android.server.nearby.fastpair;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SdkSuppress;
-
-import com.android.server.nearby.common.eventloop.EventLoop;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.fastpair.FastPairAdvHandler;
-import com.android.server.nearby.fastpair.FastPairModule;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.MockitoAnnotations;
-
-import java.time.Clock;
-
-import src.com.android.server.nearby.fastpair.testing.MockingLocator;
-
-public class ModuleTest {
-    private Locator mLocator;
-
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mLocator = MockingLocator.withMocksOnly(ApplicationProvider.getApplicationContext());
-        mLocator.bind(new FastPairModule());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void genericConstructor() {
-        assertThat(mLocator.get(FastPairCacheManager.class)).isNotNull();
-        assertThat(mLocator.get(FootprintsDeviceManager.class)).isNotNull();
-        assertThat(mLocator.get(EventLoop.class)).isNotNull();
-        assertThat(mLocator.get(FastPairHalfSheetManager.class)).isNotNull();
-        assertThat(mLocator.get(FastPairAdvHandler.class)).isNotNull();
-        assertThat(mLocator.get(Clock.class)).isNotNull();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void genericDestroy() {
-        mLocator.destroy();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java
deleted file mode 100644
index 5d4ea22..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java
+++ /dev/null
@@ -1,234 +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.server.nearby.fastpair.cache;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.FastPairManager;
-import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import service.proto.Cache;
-
-/** Unit tests for {@link DiscoveryItem} */
-public class DiscoveryItemTest {
-    private static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
-    private static final String DEFAULT_DESCRIPITON = "description";
-    private static final long DEFAULT_TIMESTAMP = 1000000000L;
-    private static final String DEFAULT_TITLE = "title";
-    private static final String APP_NAME = "app_name";
-    private static final String ACTION_URL =
-            "intent:#Intent;action=com.android.server.nearby:ACTION_FAST_PAIR;"
-            + "package=com.google.android.gms;"
-            + "component=com.google.android.gms/"
-            + ".nearby.discovery.service.DiscoveryService;end";
-    private static final String DISPLAY_URL = "DISPLAY_URL";
-    private static final String TRIGGER_ID = "trigger.id";
-    private static final String FAST_PAIR_ID = "id";
-    private static final int RSSI = -80;
-    private static final int TX_POWER = -10;
-
-    @Mock private Context mContext;
-    private LocatorContextWrapper mLocatorContextWrapper;
-    private FastPairCacheManager mFastPairCacheManager;
-    private FastPairManager mFastPairManager;
-    private DiscoveryItem mDiscoveryItem;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mLocatorContextWrapper = new LocatorContextWrapper(mContext);
-        mFastPairManager = new FastPairManager(mLocatorContextWrapper);
-        mFastPairCacheManager = mLocatorContextWrapper.getLocator().get(FastPairCacheManager.class);
-        when(mContext.getContentResolver()).thenReturn(
-                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
-        mDiscoveryItem =
-                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
-    }
-
-    @Test
-    public void testMultipleFields() {
-        assertThat(mDiscoveryItem.getId()).isEqualTo(FAST_PAIR_ID);
-        assertThat(mDiscoveryItem.getDescription()).isEqualTo(DEFAULT_DESCRIPITON);
-        assertThat(mDiscoveryItem.getDisplayUrl()).isEqualTo(DISPLAY_URL);
-        assertThat(mDiscoveryItem.getTriggerId()).isEqualTo(TRIGGER_ID);
-        assertThat(mDiscoveryItem.getMacAddress()).isEqualTo(DEFAULT_MAC_ADDRESS);
-        assertThat(
-                mDiscoveryItem.getFirstObservationTimestampMillis()).isEqualTo(DEFAULT_TIMESTAMP);
-        assertThat(
-                mDiscoveryItem.getLastObservationTimestampMillis()).isEqualTo(DEFAULT_TIMESTAMP);
-        assertThat(mDiscoveryItem.getActionUrl()).isEqualTo(ACTION_URL);
-        assertThat(mDiscoveryItem.getAppName()).isEqualTo(APP_NAME);
-        assertThat(mDiscoveryItem.getRssi()).isEqualTo(RSSI);
-        assertThat(mDiscoveryItem.getTxPower()).isEqualTo(TX_POWER);
-        assertThat(mDiscoveryItem.getFastPairInformation()).isNull();
-        assertThat(mDiscoveryItem.getFastPairSecretKey()).isNull();
-        assertThat(mDiscoveryItem.getIcon()).isNull();
-        assertThat(mDiscoveryItem.getIconFifeUrl()).isNotNull();
-        assertThat(mDiscoveryItem.getState()).isNotNull();
-        assertThat(mDiscoveryItem.getTitle()).isNotNull();
-        assertThat(mDiscoveryItem.isApp()).isFalse();
-        assertThat(mDiscoveryItem.isDeletable(
-                100000L, 0L)).isTrue();
-        assertThat(mDiscoveryItem.isDeviceType(Cache.NearbyType.NEARBY_CHROMECAST)).isTrue();
-        assertThat(mDiscoveryItem.isExpired(
-                100000L, 0L)).isTrue();
-        assertThat(mDiscoveryItem.isFastPair()).isTrue();
-        assertThat(mDiscoveryItem.isPendingAppInstallValid(5)).isTrue();
-        assertThat(mDiscoveryItem.isPendingAppInstallValid(5,
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID,  null,
-                TRIGGER_ID,  DEFAULT_MAC_ADDRESS,  "", RSSI, TX_POWER))).isTrue();
-        assertThat(mDiscoveryItem.isTypeEnabled(Cache.NearbyType.NEARBY_CHROMECAST)).isTrue();
-        assertThat(mDiscoveryItem.toString()).isNotNull();
-    }
-
-    @Test
-    public void isMuted() {
-        assertThat(mDiscoveryItem.isMuted()).isFalse();
-    }
-
-    @Test
-    public void itemWithDefaultDescription_shouldShowUp() {
-        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
-
-        // Null description should not show up.
-        mDiscoveryItem.setStoredItemForTest(DiscoveryItem.newStoredDiscoveryItem());
-        mDiscoveryItem.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID,  null,
-                        TRIGGER_ID,  DEFAULT_MAC_ADDRESS,  "", RSSI, TX_POWER));
-        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
-
-        // Empty description should not show up.
-        mDiscoveryItem.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID,  "",
-                        TRIGGER_ID,  DEFAULT_MAC_ADDRESS, DEFAULT_TITLE, RSSI, TX_POWER));
-        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
-    }
-
-    @Test
-    public void itemWithEmptyTitle_shouldNotShowUp() {
-        // Null title should not show up.
-        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
-        // Empty title should not show up.
-        mDiscoveryItem.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
-                        TRIGGER_ID, DEFAULT_MAC_ADDRESS, "", RSSI, TX_POWER));
-        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
-
-        // Null title should not show up.
-        mDiscoveryItem.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
-                        TRIGGER_ID, DEFAULT_MAC_ADDRESS, null, RSSI, TX_POWER));
-        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
-    }
-
-    @Test
-    public void itemWithRssiAndTxPower_shouldHaveCorrectEstimatedDistance() {
-        assertThat(mDiscoveryItem.getEstimatedDistance()).isWithin(0.01).of(28.18);
-    }
-
-    @Test
-    public void itemWithoutRssiOrTxPower_shouldNotHaveEstimatedDistance() {
-        mDiscoveryItem.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
-                        TRIGGER_ID, DEFAULT_MAC_ADDRESS, "", 0, 0));
-        assertThat(mDiscoveryItem.getEstimatedDistance()).isWithin(0.01).of(0);
-    }
-
-    @Test
-    public void getUiHashCode_differentAddress_differentHash() {
-        mDiscoveryItem.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
-                        TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
-        DiscoveryItem compareTo =
-                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
-        compareTo.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
-                        TRIGGER_ID, "55:44:33:22:11:00", "", RSSI, TX_POWER));
-        assertThat(mDiscoveryItem.getUiHashCode()).isNotEqualTo(compareTo.getUiHashCode());
-    }
-
-    @Test
-    public void getUiHashCode_sameAddress_sameHash() {
-        mDiscoveryItem.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
-                        TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
-        DiscoveryItem compareTo =
-                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
-        compareTo.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
-                        TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
-        assertThat(mDiscoveryItem.getUiHashCode()).isEqualTo(compareTo.getUiHashCode());
-    }
-
-    @Test
-    public void isFastPair() {
-        DiscoveryItem fastPairItem =
-                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
-        assertThat(fastPairItem.isFastPair()).isTrue();
-    }
-
-    @Test
-    public void testEqual() {
-        DiscoveryItem fastPairItem =
-                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
-        assertThat(mDiscoveryItem.equals(fastPairItem)).isTrue();
-    }
-
-    @Test
-    public void testCompareTo() {
-        DiscoveryItem fastPairItem =
-                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
-        assertThat(mDiscoveryItem.compareTo(fastPairItem)).isEqualTo(0);
-    }
-
-
-    @Test
-    public void testCopyOfStoredItem() {
-        DiscoveryItem fastPairItem =
-                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
-        fastPairItem.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
-                        TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
-        assertThat(mDiscoveryItem.equals(fastPairItem)).isFalse();
-        fastPairItem.setStoredItemForTest(mDiscoveryItem.getCopyOfStoredItem());
-        assertThat(mDiscoveryItem.equals(fastPairItem)).isTrue();
-    }
-
-    @Test
-    public void testStoredItemForTest() {
-        DiscoveryItem fastPairItem =
-                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
-        fastPairItem.setStoredItemForTest(
-                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
-                        TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
-        assertThat(mDiscoveryItem.equals(fastPairItem)).isFalse();
-        fastPairItem.setStoredItemForTest(mDiscoveryItem.getStoredItemForTest());
-        assertThat(mDiscoveryItem.equals(fastPairItem)).isTrue();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
deleted file mode 100644
index 18f2cf6..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
+++ /dev/null
@@ -1,155 +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.server.nearby.fastpair.cache;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.le.ScanResult;
-import android.content.Context;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SdkSuppress;
-
-import com.google.protobuf.ByteString;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import service.proto.Cache;
-
-public class FastPairCacheManagerTest {
-
-    private static final String MODEL_ID = "001";
-    private static final String MODEL_ID2 = "002";
-    private static final String APP_NAME = "APP_NAME";
-    private static final String MAC_ADDRESS = "00:11:22:33";
-    private static final ByteString ACCOUNT_KEY = ByteString.copyFromUtf8("axgs");
-    private static final String MAC_ADDRESS_B = "00:11:22:44";
-    private static final ByteString ACCOUNT_KEY_B = ByteString.copyFromUtf8("axgb");
-
-    @Mock
-    DiscoveryItem mDiscoveryItem;
-    @Mock
-    DiscoveryItem mDiscoveryItem2;
-    @Mock
-    Cache.StoredFastPairItem mStoredFastPairItem;
-    @Mock
-    ScanResult mScanResult;
-
-    Context mContext;
-    Cache.StoredDiscoveryItem mStoredDiscoveryItem = Cache.StoredDiscoveryItem.newBuilder()
-            .setTriggerId(MODEL_ID)
-            .setAppName(APP_NAME).build();
-    Cache.StoredDiscoveryItem mStoredDiscoveryItem2 = Cache.StoredDiscoveryItem.newBuilder()
-            .setTriggerId(MODEL_ID2)
-            .setAppName(APP_NAME).build();
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mContext = ApplicationProvider.getApplicationContext();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void notSaveRetrieveInfo() {
-        when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
-        when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
-
-        FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
-
-        assertThat(fastPairCacheManager.getStoredDiscoveryItem(MODEL_ID).getAppName())
-                .isNotEqualTo(APP_NAME);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void saveRetrieveInfo() {
-        when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
-        when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
-        when(mDiscoveryItem2.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem2);
-        when(mDiscoveryItem2.getTriggerId()).thenReturn(MODEL_ID2);
-
-        FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
-        fastPairCacheManager.saveDiscoveryItem(mDiscoveryItem);
-        assertThat(fastPairCacheManager.getStoredDiscoveryItem(MODEL_ID).getAppName())
-                .isEqualTo(APP_NAME);
-        assertThat(fastPairCacheManager.getAllSavedStoreDiscoveryItem()).hasSize(1);
-
-        fastPairCacheManager.saveDiscoveryItem(mDiscoveryItem2);
-        assertThat(fastPairCacheManager.getStoredDiscoveryItem(MODEL_ID2).getAppName())
-                .isEqualTo(APP_NAME);
-        assertThat(fastPairCacheManager.getAllSavedStoreDiscoveryItem()).hasSize(2);
-        fastPairCacheManager.cleanUp();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void saveRetrieveInfoStoredFastPairItem() {
-        Cache.StoredFastPairItem storedFastPairItem = Cache.StoredFastPairItem.newBuilder()
-                .setMacAddress(MAC_ADDRESS)
-                .setAccountKey(ACCOUNT_KEY)
-                .build();
-
-
-        FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
-        fastPairCacheManager.putStoredFastPairItem(storedFastPairItem);
-
-        assertThat(fastPairCacheManager.getStoredFastPairItemFromMacAddress(
-                MAC_ADDRESS).getAccountKey())
-                .isEqualTo(ACCOUNT_KEY);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void checkGetAllFastPairItems() {
-        Cache.StoredFastPairItem storedFastPairItem = Cache.StoredFastPairItem.newBuilder()
-                .setMacAddress(MAC_ADDRESS)
-                .setAccountKey(ACCOUNT_KEY)
-                .build();
-        Cache.StoredFastPairItem storedFastPairItemB = Cache.StoredFastPairItem.newBuilder()
-                .setMacAddress(MAC_ADDRESS_B)
-                .setAccountKey(ACCOUNT_KEY_B)
-                .build();
-
-        FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
-        fastPairCacheManager.putStoredFastPairItem(storedFastPairItem);
-        fastPairCacheManager.putStoredFastPairItem(storedFastPairItemB);
-
-        assertThat(fastPairCacheManager.getAllSavedStoredFastPairItem().size())
-                .isEqualTo(2);
-
-        fastPairCacheManager.removeStoredFastPairItem(MAC_ADDRESS_B);
-
-        assertThat(fastPairCacheManager.getAllSavedStoredFastPairItem().size())
-                .isEqualTo(1);
-
-        fastPairCacheManager.cleanUp();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void getDeviceFromScanResult_notCrash() {
-        FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
-        fastPairCacheManager.getDeviceFromScanResult(mScanResult);
-
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java
deleted file mode 100644
index c5428f5..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java
+++ /dev/null
@@ -1,61 +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.server.nearby.fastpair.cache;
-
-import static org.junit.Assert.assertThrows;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteException;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.MockitoAnnotations;
-
-public class FastPairDbHelperTest {
-
-    Context mContext;
-    FastPairDbHelper mFastPairDbHelper;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mContext = InstrumentationRegistry.getInstrumentation().getContext();
-        mFastPairDbHelper = new FastPairDbHelper(mContext);
-    }
-
-    @After
-    public void teardown() {
-        mFastPairDbHelper.close();
-    }
-
-    @Test
-    public void testUpgrade_notCrash() {
-        mFastPairDbHelper
-                .onUpgrade(mFastPairDbHelper.getWritableDatabase(), 1, 2);
-    }
-
-    @Test
-    public void testDowngrade_throwsException()  {
-        assertThrows(
-                SQLiteException.class,
-                () -> mFastPairDbHelper.onDowngrade(
-                        mFastPairDbHelper.getWritableDatabase(), 2, 1));
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklistTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklistTest.java
deleted file mode 100644
index f3afbe7..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklistTest.java
+++ /dev/null
@@ -1,283 +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.server.nearby.fastpair.halfsheet;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import com.android.server.nearby.fastpair.blocklist.Blocklist;
-import com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState;
-import com.android.server.nearby.fastpair.blocklist.BlocklistElement;
-import com.android.server.nearby.util.DefaultClock;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-public class FastPairHalfSheetBlocklistTest {
-
-    @Mock
-    private DefaultClock mClock;
-    private FastPairHalfSheetBlocklist mFastPairHalfSheetBlocklist;
-    private static final int SIZE_OF_BLOCKLIST = 2;
-    private static final long CURRENT_TIME = 1000000L;
-    private static final long BLOCKLIST_CANCEL_TIMEOUT_MILLIS = 30000L;
-    private static final long SUPPRESS_ALL_DURATION_MILLIS = 60000L;
-    private static final long DURATION_RESURFACE_DISMISS_HALF_SHEET_MILLISECOND = 86400000;
-    private static final long STATE_EXPIRATION_MILLISECOND = 86400000;
-    private static final int HALFSHEET_ID = 1;
-    private static final long DURATION_MILLI_SECONDS_LONG = 86400000;
-    private static final int DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS = 1;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME);
-        mFastPairHalfSheetBlocklist = new FastPairHalfSheetBlocklist(SIZE_OF_BLOCKLIST, mClock);
-    }
-
-    @Test
-    public void testUpdateState() {
-        mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.ACTIVE, CURRENT_TIME));
-
-        boolean initiallyBlocklisted =
-                mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                        DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS);
-
-        mFastPairHalfSheetBlocklist.updateState(HALFSHEET_ID, Blocklist.BlocklistState.ACTIVE);
-        boolean isBlockListedWhenActive =
-                mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                        DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS);
-
-        mFastPairHalfSheetBlocklist.updateState(HALFSHEET_ID, Blocklist.BlocklistState.DISMISSED);
-        boolean isBlockListedAfterDismissed =
-                mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                        DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS);
-
-        mFastPairHalfSheetBlocklist.updateState(HALFSHEET_ID,
-                Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN);
-        boolean isBlockListedAfterDoNotShowAgain =
-                mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                        DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS);
-
-        mFastPairHalfSheetBlocklist.updateState(HALFSHEET_ID,
-                Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN_LONG);
-        boolean isBlockListedAfterDoNotShowAgainLong =
-                mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                        DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS);
-
-        assertThat(initiallyBlocklisted).isFalse();
-        assertThat(isBlockListedWhenActive).isFalse();
-        assertThat(isBlockListedAfterDismissed).isTrue();
-        assertThat(isBlockListedAfterDoNotShowAgain).isTrue();
-        assertThat(isBlockListedAfterDoNotShowAgainLong).isTrue();
-    }
-
-    @Test
-    public void testBlocklist_overflow() {
-        mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DISMISSED, CURRENT_TIME));
-        mFastPairHalfSheetBlocklist.put(HALFSHEET_ID + 1,
-                new BlocklistElement(Blocklist.BlocklistState.UNKNOWN, CURRENT_TIME));
-        mFastPairHalfSheetBlocklist.put(HALFSHEET_ID + 2,
-                new BlocklistElement(Blocklist.BlocklistState.UNKNOWN, CURRENT_TIME));
-
-        // blocklist should have evicted HALFSHEET_ID making it no longer blocklisted, this is
-        // because for the test we initialize the size of the blocklist cache to be max = 2
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
-    }
-
-    @Test
-    public void removeHalfSheetDismissState() {
-        mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DISMISSED, CURRENT_TIME));
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
-
-        mFastPairHalfSheetBlocklist.put(
-                HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
-        assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID)).isTrue();
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
-        assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID + 1)).isFalse();
-    }
-
-    @Test
-    public void removeHalfSheetBanState() {
-        mFastPairHalfSheetBlocklist.put(
-                HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
-        assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID)).isTrue();
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
-        assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID + 1)).isFalse();
-    }
-
-    @Test
-    public void testHalfSheetTimeOutReleaseBan() {
-        mFastPairHalfSheetBlocklist.put(
-                HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
-        when(mClock.elapsedRealtime())
-            .thenReturn(CURRENT_TIME + BLOCKLIST_CANCEL_TIMEOUT_MILLIS + 1);
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
-    }
-
-    @Test
-    public void testHalfSheetDoNotShowAgainLong() {
-        mFastPairHalfSheetBlocklist.put(
-                HALFSHEET_ID,
-                new BlocklistElement(
-                        Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN_LONG, CURRENT_TIME));
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
-        assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID)).isTrue();
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
-        assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID + 1)).isFalse();
-    }
-
-    @Test
-    public void testHalfSheetDoNotShowAgainLongTimeout() {
-        mFastPairHalfSheetBlocklist.put(
-                HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
-        when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME + DURATION_MILLI_SECONDS_LONG + 1);
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
-    }
-
-    @Test
-    public void banAllItem_blockHalfSheet() {
-        mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.ACTIVE, CURRENT_TIME));
-
-        mFastPairHalfSheetBlocklist.banAllItem(SUPPRESS_ALL_DURATION_MILLIS);
-        when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME + SUPPRESS_ALL_DURATION_MILLIS - 1);
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
-    }
-
-    @Test
-    public void banAllItem_invokeAgainWithShorterDurationTime_blockHalfSheet() {
-        mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.ACTIVE, CURRENT_TIME));
-
-        mFastPairHalfSheetBlocklist.banAllItem(SUPPRESS_ALL_DURATION_MILLIS);
-        // The 2nd invocation time is shorter than the original one so it's ignored.
-        mFastPairHalfSheetBlocklist.banAllItem(SUPPRESS_ALL_DURATION_MILLIS - 1);
-        when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME + SUPPRESS_ALL_DURATION_MILLIS - 1);
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
-    }
-
-    @Test
-    public void banAllItem_releaseHalfSheet() {
-        mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.ACTIVE, CURRENT_TIME));
-
-        mFastPairHalfSheetBlocklist.banAllItem(SUPPRESS_ALL_DURATION_MILLIS);
-        when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME + SUPPRESS_ALL_DURATION_MILLIS);
-
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
-    }
-
-    @Test
-    public void banAllItem_extendEndTime_blockHalfSheet() {
-        mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.ACTIVE, CURRENT_TIME));
-
-        mFastPairHalfSheetBlocklist.banAllItem(SUPPRESS_ALL_DURATION_MILLIS);
-        when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME + SUPPRESS_ALL_DURATION_MILLIS);
-        // Another banAllItem comes so the end time is extended.
-        mFastPairHalfSheetBlocklist.banAllItem(/* banDurationTimeMillis= */ 1);
-
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
-    }
-
-    @Test
-    public void testHalfSheetTimeOutFirstDismissWithInDuration() {
-        mFastPairHalfSheetBlocklist.put(
-                HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
-        when(mClock.elapsedRealtime())
-            .thenReturn(CURRENT_TIME + DURATION_RESURFACE_DISMISS_HALF_SHEET_MILLISECOND - 1);
-
-        assertThat(
-                mFastPairHalfSheetBlocklist.isBlocklisted(
-                        HALFSHEET_ID, (int) DURATION_RESURFACE_DISMISS_HALF_SHEET_MILLISECOND))
-                .isTrue();
-    }
-
-    @Test
-    public void testHalfSheetTimeOutFirstDismissOutOfDuration() {
-        mFastPairHalfSheetBlocklist.put(
-                HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
-        when(mClock.elapsedRealtime())
-            .thenReturn(CURRENT_TIME + DURATION_RESURFACE_DISMISS_HALF_SHEET_MILLISECOND + 1);
-
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
-    }
-
-    @Test
-    public void testHalfSheetReset() {
-        mFastPairHalfSheetBlocklist.put(
-                HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
-        mFastPairHalfSheetBlocklist.resetBlockState(HALFSHEET_ID);
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-                DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
-    }
-
-    @Test
-    public void testIsStateExpired() {
-        mFastPairHalfSheetBlocklist.put(
-                HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
-        when(mClock.elapsedRealtime())
-                .thenReturn(CURRENT_TIME + 1);
-        assertThat(mFastPairHalfSheetBlocklist.isStateExpired(HALFSHEET_ID)).isFalse();
-        when(mClock.elapsedRealtime())
-                .thenReturn(CURRENT_TIME +  STATE_EXPIRATION_MILLISECOND + 1);
-        assertThat(mFastPairHalfSheetBlocklist.isStateExpired(HALFSHEET_ID)).isTrue();
-    }
-
-    @Test
-    public void testForceUpdateState() {
-        mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
-                new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-            DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
-        mFastPairHalfSheetBlocklist.forceUpdateState(HALFSHEET_ID, BlocklistState.ACTIVE);
-        assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
-            DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
deleted file mode 100644
index 82b9070..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
+++ /dev/null
@@ -1,569 +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.server.nearby.fastpair.halfsheet;
-
-import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.ACTIVE;
-import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DISMISSED;
-import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN;
-import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN_LONG;
-import static com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager.DISMISS_HALFSHEET_RUNNABLE_NAME;
-import static com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager.SHOW_TOAST_RUNNABLE_NAME;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.nearby.FastPairStatusCallback;
-import android.nearby.PairStatusMetadata;
-import android.os.UserHandle;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.nearby.common.eventloop.EventLoop;
-import com.android.server.nearby.common.eventloop.NamedRunnable;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.FastPairController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import service.proto.Cache;
-
-public class FastPairHalfSheetManagerTest {
-    private static final String MODEL_ID = "model_id";
-    private static final String BLE_ADDRESS = "11:22:44:66";
-    private static final String MODEL_ID_1 = "model_id_1";
-    private static final String BLE_ADDRESS_1 = "99:99:99:99";
-    private static final String NAME = "device_name";
-    private static final int PASSKEY = 1234;
-    private static final int SUCCESS = 1001;
-    private static final int FAIL = 1002;
-    private static final String EXTRA_HALF_SHEET_CONTENT =
-            "com.android.nearby.halfsheet.HALF_SHEET_CONTENT";
-    private static final String RESULT_FAIL = "RESULT_FAIL";
-    private FastPairHalfSheetManager mFastPairHalfSheetManager;
-    private Cache.ScanFastPairStoreItem mScanFastPairStoreItem;
-    private ResolveInfo mResolveInfo;
-    private List<ResolveInfo> mResolveInfoList;
-    private ApplicationInfo mApplicationInfo;
-    @Mock private Context mContext;
-    @Mock
-    LocatorContextWrapper mContextWrapper;
-    @Mock
-    PackageManager mPackageManager;
-    @Mock
-    Locator mLocator;
-    @Mock
-    FastPairController mFastPairController;
-    @Mock
-    EventLoop mEventLoop;
-    @Mock
-    FastPairStatusCallback mFastPairStatusCallback;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mLocator.overrideBindingForTest(FastPairController.class, mFastPairController);
-        mLocator.overrideBindingForTest(EventLoop.class, mEventLoop);
-
-        mResolveInfo = new ResolveInfo();
-        mResolveInfoList = new ArrayList<>();
-        mResolveInfo.activityInfo = new ActivityInfo();
-        mApplicationInfo = new ApplicationInfo();
-        mPackageManager = mock(PackageManager.class);
-
-        when(mContext.getContentResolver()).thenReturn(
-                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
-        when(mContextWrapper.getPackageManager()).thenReturn(mPackageManager);
-        when(mContextWrapper.getLocator()).thenReturn(mLocator);
-        when(mLocator.get(EventLoop.class)).thenReturn(mEventLoop);
-        when(mPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(mResolveInfoList);
-        when(mPackageManager.canRequestPackageInstalls()).thenReturn(false);
-
-        mScanFastPairStoreItem = Cache.ScanFastPairStoreItem.newBuilder()
-                .setModelId(MODEL_ID)
-                .setAddress(BLE_ADDRESS)
-                .setDeviceName(NAME)
-                .build();
-    }
-
-    @Test
-    public void verifyFastPairHalfSheetManagerBehavior() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-
-        verify(mContextWrapper, atLeastOnce())
-                .startActivityAsUser(intentArgumentCaptor.capture(), eq(UserHandle.CURRENT));
-    }
-
-    @Test
-    public void verifyFastPairHalfSheetManagerHalfSheetApkNotValidBehavior() {
-        // application directory is wrong
-        mApplicationInfo.sourceDir = "/apex/com.android.nearby";
-        mApplicationInfo.packageName = "test.package";
-        mResolveInfo.activityInfo.applicationInfo = mApplicationInfo;
-        mResolveInfoList.add(mResolveInfo);
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-
-        verify(mContextWrapper, never())
-                .startActivityAsUser(intentArgumentCaptor.capture(), eq(UserHandle.CURRENT));
-    }
-
-    @Test
-    public void testHalfSheetForegroundState() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        assertThat(mFastPairHalfSheetManager.getHalfSheetForeground()).isTrue();
-        mFastPairHalfSheetManager.dismiss(MODEL_ID);
-        assertThat(mFastPairHalfSheetManager.getHalfSheetForeground()).isFalse();
-    }
-
-    @Test
-    public void testEmptyMethods() {
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        mFastPairHalfSheetManager.destroyBluetoothPairController();
-        mFastPairHalfSheetManager.notifyPairingProcessDone(true, BLE_ADDRESS, null);
-        mFastPairHalfSheetManager.showPairingFailed();
-        mFastPairHalfSheetManager.showPairingHalfSheet(null);
-        mFastPairHalfSheetManager.showPairingSuccessHalfSheet(BLE_ADDRESS);
-        mFastPairHalfSheetManager.showPasskeyConfirmation(null, PASSKEY);
-    }
-
-    @Test
-    public void showInitialPairingHalfSheetThenDismissOnce_stateDISMISSED() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        FastPairHalfSheetBlocklist mHalfSheetBlocklist =
-                mFastPairHalfSheetManager.getHalfSheetBlocklist();
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        mFastPairHalfSheetManager.dismiss(MODEL_ID);
-
-        Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
-
-        //First time dismiss -> state: DISMISSED
-        assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(DISMISSED);
-    }
-
-    @Test
-    public void showInitialPairingHalfSheetThenBan_stateDO_NOT_SHOW_AGAIN() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        FastPairHalfSheetBlocklist mHalfSheetBlocklist =
-                mFastPairHalfSheetManager.getHalfSheetBlocklist();
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        mFastPairHalfSheetManager.dismiss(MODEL_ID);
-        mFastPairHalfSheetManager.dismiss(MODEL_ID);
-
-        Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
-
-        //First time ban -> state: DO_NOT_SHOW_AGAIN
-        assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(DO_NOT_SHOW_AGAIN);
-    }
-
-    @Test
-    public void showInitialPairingHalfSheetThenBanTwice_stateDO_NOT_SHOW_AGAIN_LONG() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        FastPairHalfSheetBlocklist mHalfSheetBlocklist =
-                mFastPairHalfSheetManager.getHalfSheetBlocklist();
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        mFastPairHalfSheetManager.dismiss(MODEL_ID);
-        mFastPairHalfSheetManager.dismiss(MODEL_ID);
-        mFastPairHalfSheetManager.dismiss(MODEL_ID);
-
-        Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
-
-        //Second time ban -> state: DO_NOT_SHOW_AGAIN
-        assertThat(mHalfSheetBlocklist.get(halfSheetId).getState())
-                .isEqualTo(DO_NOT_SHOW_AGAIN_LONG);
-    }
-
-    @Test
-    public void testResetBanSate_resetDISMISSEDtoACTIVE() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        FastPairHalfSheetBlocklist mHalfSheetBlocklist =
-                mFastPairHalfSheetManager.getHalfSheetBlocklist();
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-
-        Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
-
-        mHalfSheetBlocklist.updateState(halfSheetId, DISMISSED);
-        mFastPairHalfSheetManager.resetBanState(MODEL_ID);
-
-        assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(ACTIVE);
-    }
-
-    @Test
-    public void testResetBanSate_resetDO_NOT_SHOW_AGAINtoACTIVE() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        FastPairHalfSheetBlocklist mHalfSheetBlocklist =
-                mFastPairHalfSheetManager.getHalfSheetBlocklist();
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-
-        Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
-
-        mHalfSheetBlocklist.updateState(halfSheetId, DO_NOT_SHOW_AGAIN);
-        mFastPairHalfSheetManager.resetBanState(MODEL_ID);
-
-        assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(ACTIVE);
-    }
-
-    @Test
-    public void testResetBanSate_resetDO_NOT_SHOW_AGAIN_LONGtoACTIVE() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        FastPairHalfSheetBlocklist mHalfSheetBlocklist =
-                mFastPairHalfSheetManager.getHalfSheetBlocklist();
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-
-        Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
-
-        mHalfSheetBlocklist.updateState(halfSheetId, DO_NOT_SHOW_AGAIN_LONG
-        );
-        mFastPairHalfSheetManager.resetBanState(MODEL_ID);
-
-        assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(ACTIVE);
-    }
-
-    @Test
-    public void testReportDonePairing() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-
-        assertThat(mFastPairHalfSheetManager.getHalfSheetBlocklist().size()).isEqualTo(1);
-
-        mFastPairHalfSheetManager
-                .reportDonePairing(mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID));
-
-        assertThat(mFastPairHalfSheetManager.getHalfSheetBlocklist().size()).isEqualTo(0);
-    }
-
-    @Test
-    public void showInitialPairingHalfSheet_AutoDismiss() throws InterruptedException {
-        configResolveInfoList();
-        mFastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-
-        verifyInitialPairingNameRunnablePostedTimes(1);
-    }
-
-    @Test
-    public void showInitialPairingHalfSheet_whenUiShownAndItemWithTheSameAddress() {
-        Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
-                .setModelId(MODEL_ID)
-                .setAddress(BLE_ADDRESS)
-                .setDeviceName(NAME)
-                .build();
-        configResolveInfoList();
-        mFastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-
-        verifyHalfSheetActivityIntent(1);
-        verifyInitialPairingNameRunnablePostedTimes(1);
-
-        mFastPairHalfSheetManager.showHalfSheet(testItem);
-        // When half sheet shown and receives broadcast from the same address,
-        // DO NOT request start-activity to avoid unnecessary memory usage,
-        // Just reset the auto dismiss timeout for the new request
-        verifyHalfSheetActivityIntent(1);
-        verifyInitialPairingNameRunnablePostedTimes(2);
-    }
-
-    @Test
-    public void showInitialPairingHalfSheet_whenUiShowAndItemWithDifferentAddressSameModelId() {
-        Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
-                .setModelId(MODEL_ID)
-                .setAddress(BLE_ADDRESS_1)
-                .setDeviceName(NAME)
-                .build();
-        configResolveInfoList();
-        mFastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-
-        verifyHalfSheetActivityIntent(1);
-        verifyInitialPairingNameRunnablePostedTimes(1);
-
-        mFastPairHalfSheetManager.showHalfSheet(testItem);
-        // When half sheet shown and receives broadcast from the same model id
-        // but with different address, DO NOT rest the auto dismiss timeout. No action is required.
-        verifyHalfSheetActivityIntent(1);
-        verifyInitialPairingNameRunnablePostedTimes(1);
-    }
-
-    @Test
-    public void showInitialPairingHalfSheet_whenUiShowAndItemWithDifferentModelId() {
-        Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
-                .setModelId(MODEL_ID_1)
-                .setAddress(BLE_ADDRESS_1)
-                .setDeviceName(NAME)
-                .build();
-        configResolveInfoList();
-        mFastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-
-        verifyInitialPairingNameRunnablePostedTimes(1);
-        verifyHalfSheetActivityIntent(1);
-
-        mFastPairHalfSheetManager.showHalfSheet(testItem);
-        // When half sheet shown and receives broadcast from a different model id,
-        // the new request should be ignored. No action is required.
-        verifyHalfSheetActivityIntent(1);
-        verifyInitialPairingNameRunnablePostedTimes(1);
-    }
-
-    @Test
-    public void showInitialPairingHalfSheet_whenUiNotShownAndIsPairingWithTheSameAddress() {
-        Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
-                .setModelId(MODEL_ID)
-                .setAddress(BLE_ADDRESS)
-                .setDeviceName(NAME)
-                .build();
-        configResolveInfoList();
-        mFastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        mFastPairHalfSheetManager.setHalfSheetForeground(/* state= */ false);
-        mFastPairHalfSheetManager.setIsActivePairing(true);
-        mFastPairHalfSheetManager.showHalfSheet(testItem);
-
-        // If the half sheet is not in foreground but the system is still pairing the same device,
-        // mark as duplicate request and skip.
-        verifyHalfSheetActivityIntent(1);
-        verifyInitialPairingNameRunnablePostedTimes(1);
-    }
-
-    @Test
-    public void showInitialPairingHalfSheet_whenUiNotShownAndIsPairingWithADifferentAddress() {
-        Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
-                .setModelId(MODEL_ID_1)
-                .setAddress(BLE_ADDRESS_1)
-                .setDeviceName(NAME)
-                .build();
-        configResolveInfoList();
-        mFastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        mFastPairHalfSheetManager.setHalfSheetForeground(/* state= */ false);
-        mFastPairHalfSheetManager.setIsActivePairing(true);
-        mFastPairHalfSheetManager.showHalfSheet(testItem);
-
-        // shouldShowHalfSheet
-        verifyHalfSheetActivityIntent(2);
-        verifyInitialPairingNameRunnablePostedTimes(2);
-    }
-
-    @Test
-    public void showInitialPairingHalfSheet_whenUiNotShownAndIsNotPairingWithTheSameAddress() {
-        Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
-                .setModelId(MODEL_ID)
-                .setAddress(BLE_ADDRESS)
-                .setDeviceName(NAME)
-                .build();
-        configResolveInfoList();
-        mFastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        mFastPairHalfSheetManager.setHalfSheetForeground(/* state= */ false);
-        mFastPairHalfSheetManager.setIsActivePairing(false);
-        mFastPairHalfSheetManager.showHalfSheet(testItem);
-
-        // shouldShowHalfSheet
-        verifyHalfSheetActivityIntent(2);
-        verifyInitialPairingNameRunnablePostedTimes(2);
-    }
-
-    @Test
-    public void testReportActivelyPairing() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-
-        assertThat(mFastPairHalfSheetManager.isActivePairing()).isFalse();
-
-        mFastPairHalfSheetManager.reportActivelyPairing();
-
-        assertThat(mFastPairHalfSheetManager.isActivePairing()).isTrue();
-    }
-
-    @Test
-    public void showPairingSuccessHalfSheetHalfSheetActivityActive_ChangeUIToShowSuccessInfo() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        mFastPairHalfSheetManager.mFastPairUiService
-                .setFastPairStatusCallback(mFastPairStatusCallback);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        mFastPairHalfSheetManager.showPairingSuccessHalfSheet(BLE_ADDRESS);
-
-        verifyFastPairStatusCallback(1, SUCCESS);
-        assertThat(mFastPairHalfSheetManager.isActivePairing()).isFalse();
-    }
-
-    @Test
-    public void showPairingSuccessHalfSheetHalfSheetActivityNotActive_showToast() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        mFastPairHalfSheetManager.setHalfSheetForeground(false);
-        mFastPairHalfSheetManager.showPairingSuccessHalfSheet(BLE_ADDRESS);
-
-        ArgumentCaptor<NamedRunnable> captor = ArgumentCaptor.forClass(NamedRunnable.class);
-
-        verify(mEventLoop).postRunnable(captor.capture());
-        assertThat(
-                captor.getAllValues().stream()
-                        .filter(r -> r.name.equals(SHOW_TOAST_RUNNABLE_NAME))
-                        .count())
-                .isEqualTo(1);
-        assertThat(mFastPairHalfSheetManager.isActivePairing()).isFalse();
-    }
-
-    @Test
-    public void showPairingFailedHalfSheetHalfSheetActivityActive_ChangeUIToShowFailedInfo() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        mFastPairHalfSheetManager.mFastPairUiService
-                .setFastPairStatusCallback(mFastPairStatusCallback);
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        mFastPairHalfSheetManager.showPairingFailed();
-
-        verifyFastPairStatusCallback(1, FAIL);
-        assertThat(mFastPairHalfSheetManager.isActivePairing()).isFalse();
-    }
-
-    @Test
-    public void showPairingFailedHalfSheetActivityNotActive_StartHalfSheetToShowFailedInfo() {
-        configResolveInfoList();
-        mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        FastPairHalfSheetBlocklist mHalfSheetBlocklist =
-                mFastPairHalfSheetManager.getHalfSheetBlocklist();
-
-        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
-        mFastPairHalfSheetManager.setHalfSheetForeground(false);
-        mFastPairHalfSheetManager.showPairingFailed();
-
-        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
-        Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
-
-        verify(mContextWrapper, times(2))
-                .startActivityAsUser(captor.capture(), eq(UserHandle.CURRENT));
-        assertThat(
-                captor.getAllValues().stream()
-                        .filter(r ->
-                            r.getStringExtra(EXTRA_HALF_SHEET_CONTENT) != null
-                                    && r.getStringExtra(EXTRA_HALF_SHEET_CONTENT)
-                                    .equals(RESULT_FAIL))
-
-                        .count())
-                .isEqualTo(1);
-        assertThat(mFastPairHalfSheetManager.isActivePairing()).isFalse();
-        assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(ACTIVE);
-    }
-
-    private void verifyInitialPairingNameRunnablePostedTimes(int times) {
-        ArgumentCaptor<NamedRunnable> captor = ArgumentCaptor.forClass(NamedRunnable.class);
-
-        verify(mEventLoop, times(times)).postRunnableDelayed(captor.capture(), anyLong());
-        assertThat(
-                captor.getAllValues().stream()
-                        .filter(r -> r.name.equals(DISMISS_HALFSHEET_RUNNABLE_NAME))
-                        .count())
-                .isEqualTo(times);
-    }
-
-    private void verifyHalfSheetActivityIntent(int times) {
-        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
-
-        verify(mContextWrapper, times(times))
-                .startActivityAsUser(captor.capture(), eq(UserHandle.CURRENT));
-        assertThat(
-                captor.getAllValues().stream()
-                        .filter(r -> r.getAction().equals("android.nearby.SHOW_HALFSHEET"))
-                        .count())
-                .isEqualTo(times);
-    }
-
-    private void verifyFastPairStatusCallback(int times, int status) {
-        ArgumentCaptor<PairStatusMetadata> captor =
-                ArgumentCaptor.forClass(PairStatusMetadata.class);
-        verify(mFastPairStatusCallback, times(times)).onPairUpdate(any(), captor.capture());
-        assertThat(
-                captor.getAllValues().stream()
-                        .filter(r -> r.getStatus() == status)
-                        .count())
-                .isEqualTo(times);
-    }
-
-    private void configResolveInfoList() {
-        mApplicationInfo.sourceDir = "/apex/com.android.tethering";
-        mApplicationInfo.packageName = "test.package";
-        mResolveInfo.activityInfo.applicationInfo = mApplicationInfo;
-        mResolveInfoList.add(mResolveInfo);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilderTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilderTest.java
deleted file mode 100644
index d995969..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilderTest.java
+++ /dev/null
@@ -1,107 +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.server.nearby.fastpair.notification;
-
-import static com.android.server.nearby.fastpair.notification.FastPairNotificationBuilder.NOTIFICATION_OVERRIDE_NAME_EXTRA;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.when;
-
-import android.app.Notification;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.nearby.halfsheet.R;
-import com.android.server.nearby.fastpair.HalfSheetResources;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class FastPairNotificationBuilderTest {
-
-    private static final String STRING_DEVICE = "Devices";
-    private static final String STRING_NEARBY = "Nearby";
-
-    @Mock private Context mContext;
-    @Mock private PackageManager mPackageManager;
-    @Mock private Resources mResources;
-
-    private ResolveInfo mResolveInfo;
-    private List<ResolveInfo> mResolveInfoList;
-    private ApplicationInfo mApplicationInfo;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        HalfSheetResources.setResourcesContextForTest(mContext);
-
-        mResolveInfo = new ResolveInfo();
-        mResolveInfoList = new ArrayList<>();
-        mResolveInfo.activityInfo = new ActivityInfo();
-        mApplicationInfo = new ApplicationInfo();
-
-        when(mContext.getResources()).thenReturn(mResources);
-        when(mContext.getApplicationInfo())
-                .thenReturn(InstrumentationRegistry
-                        .getInstrumentation().getContext().getApplicationInfo());
-        when(mContext.getContentResolver()).thenReturn(
-                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
-        when(mContext.getPackageManager()).thenReturn(mPackageManager);
-        when(mPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(mResolveInfoList);
-        when(mPackageManager.canRequestPackageInstalls()).thenReturn(false);
-        mApplicationInfo.sourceDir = "/apex/com.android.nearby";
-        mApplicationInfo.packageName = "test.package";
-        mResolveInfo.activityInfo.applicationInfo = mApplicationInfo;
-        mResolveInfoList.add(mResolveInfo);
-
-        when(mResources.getString(eq(R.string.common_devices))).thenReturn(STRING_DEVICE);
-        when(mResources.getString(eq(R.string.common_nearby_title))).thenReturn(STRING_NEARBY);
-    }
-
-    @Test
-    public void setIsDevice_true() {
-        Notification notification =
-                new FastPairNotificationBuilder(mContext, "channelId")
-                        .setIsDevice(true).build();
-        assertThat(notification.extras.getString(NOTIFICATION_OVERRIDE_NAME_EXTRA))
-                .isEqualTo(STRING_DEVICE);
-    }
-
-    @Test
-    public void setIsDevice_false() {
-        Notification notification =
-                new FastPairNotificationBuilder(mContext, "channelId")
-                        .setIsDevice(false).build();
-        assertThat(notification.extras.getString(NOTIFICATION_OVERRIDE_NAME_EXTRA))
-                .isEqualTo(STRING_NEARBY);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
deleted file mode 100644
index 9670a3f..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
+++ /dev/null
@@ -1,101 +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.server.nearby.fastpair.notification;
-
-import static org.mockito.Mockito.when;
-
-import android.app.NotificationManager;
-import android.content.Context;
-import android.content.res.Resources;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.HalfSheetResources;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import service.proto.Cache;
-
-public class FastPairNotificationManagerTest {
-
-    @Mock
-    private Context mContext;
-    @Mock
-    NotificationManager mNotificationManager;
-    @Mock
-    Resources mResources;
-    @Mock
-    private LocatorContextWrapper mLocatorContextWrapper;
-    @Mock
-    private Locator mLocator;
-
-    private static final int NOTIFICATION_ID = 1;
-    private static final int BATTERY_LEVEL = 1;
-    private static final String DEVICE_NAME = "deviceName";
-    private FastPairNotificationManager mFastPairNotificationManager;
-    private DiscoveryItem mItem;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        when(mContext.getContentResolver()).thenReturn(
-                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
-        when(mLocatorContextWrapper.getResources()).thenReturn(mResources);
-        when(mLocatorContextWrapper.getLocator()).thenReturn(mLocator);
-        HalfSheetResources.setResourcesContextForTest(mLocatorContextWrapper);
-        // Real context is needed
-        Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        FastPairNotificationManager fastPairNotificationManager =
-        mFastPairNotificationManager =
-                new FastPairNotificationManager(context, NOTIFICATION_ID, mNotificationManager,
-                        new HalfSheetResources(mLocatorContextWrapper));
-        mLocator.overrideBindingForTest(FastPairNotificationManager.class,
-                fastPairNotificationManager);
-
-        mItem = new DiscoveryItem(mLocatorContextWrapper,
-                Cache.StoredDiscoveryItem.newBuilder().setTitle("Device Name").build());
-    }
-
-    @Test
-    public void  notifyPairingProcessDone() {
-        mFastPairNotificationManager.notifyPairingProcessDone(true, true,
-                "privateAddress", "publicAddress");
-    }
-
-    @Test
-    public void  showConnectingNotification() {
-        mFastPairNotificationManager.showConnectingNotification(mItem);
-    }
-
-    @Test
-    public void   showPairingFailedNotification() {
-        mFastPairNotificationManager
-                .showPairingFailedNotification(mItem, new byte[]{1});
-    }
-
-    @Test
-    public void  showPairingSucceededNotification() {
-        mFastPairNotificationManager
-                .showPairingSucceededNotification(mItem, BATTERY_LEVEL, DEVICE_NAME);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationsTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationsTest.java
deleted file mode 100644
index cfebbde..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationsTest.java
+++ /dev/null
@@ -1,143 +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.server.nearby.fastpair.notification;
-
-import static com.android.server.nearby.fastpair.notification.FastPairNotificationManager.DEVICES_WITHIN_REACH_CHANNEL_ID;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.when;
-
-import android.app.Notification;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.nearby.halfsheet.R;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.HalfSheetResources;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import service.proto.Cache;
-
-public class FastPairNotificationsTest {
-    private static final Cache.StoredDiscoveryItem SCAN_FAST_PAIR_ITEM =
-            Cache.StoredDiscoveryItem.newBuilder()
-                    .setDeviceName("TestName")
-                    .build();
-    private static final String STRING_DEVICE = "Devices";
-    private static final String STRING_NEARBY = "Nearby";
-    private static final String STRING_YOUR_DEVICE = "Your saved device is available";
-    private static final String STRING_CONNECTING = "Connecting";
-    private static final String STRING_DEVICE_READY = "Device connected";
-
-    private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
-    @Mock
-    LocatorContextWrapper mContextWrapper;
-    @Mock
-    Locator mLocator;
-    @Mock
-    private Context mContext;
-    @Mock
-    private Resources mResources;
-    @Mock
-    private Drawable mDrawable;
-
-    private DiscoveryItem mItem;
-    private HalfSheetResources mHalfSheetResources;
-    private FastPairNotifications mFastPairNotifications;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        mHalfSheetResources = new HalfSheetResources(mContext);
-        Context realContext = InstrumentationRegistry.getInstrumentation().getContext();
-        mFastPairNotifications =
-                new FastPairNotifications(realContext, mHalfSheetResources);
-        HalfSheetResources.setResourcesContextForTest(mContext);
-
-        when(mContextWrapper.getLocator()).thenReturn(mLocator);
-        when(mContext.getResources()).thenReturn(mResources);
-
-        when(mResources.getString(eq(R.string.common_devices))).thenReturn(STRING_DEVICE);
-        when(mResources.getString(eq(R.string.common_nearby_title))).thenReturn(STRING_NEARBY);
-        when(mResources.getString(eq(R.string.fast_pair_your_device)))
-                .thenReturn(STRING_YOUR_DEVICE);
-        when(mResources.getString(eq(R.string.common_connecting))).thenReturn(STRING_CONNECTING);
-        when(mResources.getString(eq(R.string.fast_pair_device_ready)))
-                .thenReturn(STRING_DEVICE_READY);
-        when(mResources.getDrawable(eq(R.drawable.quantum_ic_devices_other_vd_theme_24), any()))
-                .thenReturn(mDrawable);
-
-        mItem = new DiscoveryItem(mContextWrapper, SCAN_FAST_PAIR_ITEM);
-    }
-
-    @Test
-    public void verify_progressNotification() {
-        Notification notification = mFastPairNotifications.progressNotification(mItem);
-
-        assertThat(notification.getChannelId()).isEqualTo(DEVICES_WITHIN_REACH_CHANNEL_ID);
-        assertThat(notification.getSmallIcon().getResId())
-                .isEqualTo(R.drawable.quantum_ic_devices_other_vd_theme_24);
-        assertThat(notification.category).isEqualTo(Notification.CATEGORY_PROGRESS);
-        assertThat(notification.tickerText.toString()).isEqualTo(STRING_CONNECTING);
-    }
-
-    @Test
-    public void verify_discoveryNotification() {
-        Notification notification =
-                mFastPairNotifications.discoveryNotification(mItem, ACCOUNT_KEY);
-
-        assertThat(notification.getChannelId()).isEqualTo(DEVICES_WITHIN_REACH_CHANNEL_ID);
-        assertThat(notification.getSmallIcon().getResId())
-                .isEqualTo(R.drawable.quantum_ic_devices_other_vd_theme_24);
-        assertThat(notification.category).isEqualTo(Notification.CATEGORY_RECOMMENDATION);
-    }
-
-    @Test
-    public void verify_succeededNotification() {
-        Notification notification = mFastPairNotifications
-                .pairingSucceededNotification(101, null, "model name", mItem);
-
-        assertThat(notification.getChannelId()).isEqualTo(DEVICES_WITHIN_REACH_CHANNEL_ID);
-        assertThat(notification.getSmallIcon().getResId())
-                .isEqualTo(R.drawable.quantum_ic_devices_other_vd_theme_24);
-        assertThat(notification.tickerText.toString()).isEqualTo(STRING_DEVICE_READY);
-        assertThat(notification.category).isEqualTo(Notification.CATEGORY_STATUS);
-    }
-
-    @Test
-    public void verify_failedNotification() {
-        Notification notification =
-                mFastPairNotifications.showPairingFailedNotification(mItem, ACCOUNT_KEY);
-
-        assertThat(notification.getChannelId()).isEqualTo(DEVICES_WITHIN_REACH_CHANNEL_ID);
-        assertThat(notification.getSmallIcon().getResId())
-                .isEqualTo(R.drawable.quantum_ic_devices_other_vd_theme_24);
-        assertThat(notification.category).isEqualTo(Notification.CATEGORY_ERROR);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
deleted file mode 100644
index 2d496fd..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
+++ /dev/null
@@ -1,140 +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.server.nearby.fastpair.pairinghandler;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-
-import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
-import com.android.server.nearby.common.eventloop.EventLoop;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
-
-import com.google.protobuf.ByteString;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.time.Clock;
-
-import service.proto.Cache;
-import service.proto.Rpcs;
-
-public class HalfSheetPairingProgressHandlerTest {
-    @Mock
-    Locator mLocator;
-    @Mock
-    LocatorContextWrapper mContextWrapper;
-    @Mock
-    Clock mClock;
-    @Mock
-    FastPairCacheManager mFastPairCacheManager;
-    @Mock
-    FastPairConnection mFastPairConnection;
-    @Mock
-    FootprintsDeviceManager mFootprintsDeviceManager;
-    @Mock
-    EventLoop mEventLoop;
-
-    private static final String MAC_ADDRESS = "00:11:22:33:44:55";
-    private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
-    private static final int SUBSEQUENT_PAIR_START = 1310;
-    private static final int SUBSEQUENT_PAIR_END = 1320;
-    private static final int PASSKEY = 1234;
-    private static HalfSheetPairingProgressHandler sHalfSheetPairingProgressHandler;
-    private static DiscoveryItem sDiscoveryItem;
-    private static BluetoothDevice sBluetoothDevice;
-    private static FastPairHalfSheetManager sFastPairHalfSheetManager;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        when(mContextWrapper.getLocator()).thenReturn(mLocator);
-        mLocator.overrideBindingForTest(FastPairCacheManager.class, mFastPairCacheManager);
-        mLocator.overrideBindingForTest(Clock.class, mClock);
-        sFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
-        mLocator.bind(FastPairHalfSheetManager.class, sFastPairHalfSheetManager);
-        when(mLocator.get(FastPairHalfSheetManager.class)).thenReturn(sFastPairHalfSheetManager);
-        when(mLocator.get(EventLoop.class)).thenReturn(mEventLoop);
-        sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        sDiscoveryItem.setStoredItemForTest(
-                sDiscoveryItem.getStoredItemForTest().toBuilder()
-                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
-                        .setMacAddress(MAC_ADDRESS)
-                        .setFastPairInformation(
-                                Cache.FastPairInformation.newBuilder()
-                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
-                        .build());
-        sHalfSheetPairingProgressHandler =
-                new HalfSheetPairingProgressHandler(mContextWrapper, sDiscoveryItem,
-                        sDiscoveryItem.getAppPackageName(), ACCOUNT_KEY);
-
-        sBluetoothDevice =
-                BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
-    }
-
-    @Test
-    public void getPairEndEventCode() {
-        assertThat(sHalfSheetPairingProgressHandler
-                .getPairEndEventCode()).isEqualTo(SUBSEQUENT_PAIR_END);
-    }
-
-    @Test
-    public void getPairStartEventCode() {
-        assertThat(sHalfSheetPairingProgressHandler
-                .getPairStartEventCode()).isEqualTo(SUBSEQUENT_PAIR_START);
-    }
-
-    @Test
-    public void testOnHandlePasskeyConfirmation() {
-        sHalfSheetPairingProgressHandler.onHandlePasskeyConfirmation(sBluetoothDevice, PASSKEY);
-    }
-
-    @Test
-    public void testOnPairedCallbackCalled() {
-        sHalfSheetPairingProgressHandler.onPairedCallbackCalled(mFastPairConnection, ACCOUNT_KEY,
-                mFootprintsDeviceManager, MAC_ADDRESS);
-    }
-
-    @Test
-    public void testonPairingFailed() {
-        Throwable e = new Throwable("onPairingFailed");
-        sHalfSheetPairingProgressHandler.onPairingFailed(e);
-    }
-
-    @Test
-    public void testonPairingStarted() {
-        sHalfSheetPairingProgressHandler.onPairingStarted();
-        assertThat(sFastPairHalfSheetManager.isActivePairing()).isTrue();
-    }
-
-    @Test
-    public void testonPairingSuccess() {
-        sHalfSheetPairingProgressHandler.onPairingSuccess(MAC_ADDRESS);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
deleted file mode 100644
index 5c61ddb..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
+++ /dev/null
@@ -1,161 +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.server.nearby.fastpair.pairinghandler;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.app.NotificationManager;
-import android.bluetooth.BluetoothManager;
-import android.content.Context;
-import android.content.res.Resources;
-
-import androidx.annotation.Nullable;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.HalfSheetResources;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
-import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
-
-import com.google.protobuf.ByteString;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.time.Clock;
-
-import service.proto.Cache;
-import service.proto.Rpcs;
-
-public class NotificationPairingProgressHandlerTest {
-
-    @Mock
-    Locator mLocator;
-    @Mock
-    LocatorContextWrapper mContextWrapper;
-    @Mock
-    Clock mClock;
-    @Mock
-    FastPairCacheManager mFastPairCacheManager;
-    @Mock
-    FastPairConnection mFastPairConnection;
-    @Mock
-    FootprintsDeviceManager mFootprintsDeviceManager;
-    @Mock
-    android.bluetooth.BluetoothManager mBluetoothManager;
-    @Mock
-    NotificationManager mNotificationManager;
-    @Mock
-    Resources mResources;
-
-    private static final String MAC_ADDRESS = "00:11:22:33:44:55";
-    private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
-    private static final int SUBSEQUENT_PAIR_START = 1310;
-    private static final int SUBSEQUENT_PAIR_END = 1320;
-    private static DiscoveryItem sDiscoveryItem;
-    private static  NotificationPairingProgressHandler sNotificationPairingProgressHandler;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        when(mContextWrapper.getSystemService(BluetoothManager.class))
-                .thenReturn(mBluetoothManager);
-        when(mContextWrapper.getLocator()).thenReturn(mLocator);
-        when(mContextWrapper.getResources()).thenReturn(mResources);
-        HalfSheetResources.setResourcesContextForTest(mContextWrapper);
-
-        mLocator.overrideBindingForTest(FastPairCacheManager.class,
-                mFastPairCacheManager);
-        mLocator.overrideBindingForTest(Clock.class, mClock);
-        sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        sDiscoveryItem.setStoredItemForTest(
-                sDiscoveryItem.getStoredItemForTest().toBuilder()
-                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
-                        .setFastPairInformation(
-                                Cache.FastPairInformation.newBuilder()
-                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
-                        .build());
-        sNotificationPairingProgressHandler = createProgressHandler(ACCOUNT_KEY, sDiscoveryItem);
-    }
-
-    @Test
-    public void getPairEndEventCode() {
-        assertThat(sNotificationPairingProgressHandler
-                .getPairEndEventCode()).isEqualTo(SUBSEQUENT_PAIR_END);
-    }
-
-    @Test
-    public void getPairStartEventCode() {
-        assertThat(sNotificationPairingProgressHandler
-                .getPairStartEventCode()).isEqualTo(SUBSEQUENT_PAIR_START);
-    }
-
-    @Test
-    public void onReadyToPair() {
-        sNotificationPairingProgressHandler.onReadyToPair();
-    }
-
-    @Test
-    public void onPairedCallbackCalled() {
-        sNotificationPairingProgressHandler.onPairedCallbackCalled(mFastPairConnection,
-                    ACCOUNT_KEY, mFootprintsDeviceManager, MAC_ADDRESS);
-    }
-
-    @Test
-    public void  onPairingFailed() {
-        Throwable e = new Throwable("Pairing Failed");
-        sNotificationPairingProgressHandler.onPairingFailed(e);
-    }
-
-    @Test
-    public void onPairingSuccess() {
-        sNotificationPairingProgressHandler.onPairingSuccess(sDiscoveryItem.getMacAddress());
-    }
-
-    private NotificationPairingProgressHandler createProgressHandler(
-            @Nullable byte[] accountKey, DiscoveryItem fastPairItem) {
-        FastPairHalfSheetManager fastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-        // Real context is needed
-        Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        FastPairNotificationManager fastPairNotificationManager =
-                new FastPairNotificationManager(context, 1234, mNotificationManager,
-                        new HalfSheetResources(mContextWrapper));
-        mLocator.overrideBindingForTest(FastPairHalfSheetManager.class, fastPairHalfSheetManager);
-        mLocator.overrideBindingForTest(FastPairNotificationManager.class,
-                fastPairNotificationManager);
-
-        NotificationPairingProgressHandler mNotificationPairingProgressHandler =
-                new NotificationPairingProgressHandler(
-                        mContextWrapper,
-                        fastPairItem,
-                        fastPairItem.getAppPackageName(),
-                        accountKey,
-                        fastPairNotificationManager);
-        return mNotificationPairingProgressHandler;
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
deleted file mode 100644
index 6d769df..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
+++ /dev/null
@@ -1,244 +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.server.nearby.fastpair.pairinghandler;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.app.NotificationManager;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothManager;
-import android.content.Context;
-import android.content.res.Resources;
-
-import androidx.annotation.Nullable;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
-import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.HalfSheetResources;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
-import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
-import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
-import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
-
-import com.google.protobuf.ByteString;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.time.Clock;
-
-import service.proto.Cache;
-import service.proto.Rpcs;
-public class PairingProgressHandlerBaseTest {
-
-    @Mock
-    Locator mLocator;
-    @Mock
-    LocatorContextWrapper mContextWrapper;
-    @Mock
-    Clock mClock;
-    @Mock
-    FastPairCacheManager mFastPairCacheManager;
-    @Mock
-    FootprintsDeviceManager mFootprintsDeviceManager;
-    @Mock
-    FastPairConnection mFastPairConnection;
-    @Mock
-    BluetoothManager mBluetoothManager;
-    @Mock
-    Resources mResources;
-    @Mock
-    NotificationManager mNotificationManager;
-
-    private static final String MAC_ADDRESS = "00:11:22:33:44:55";
-    private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
-    private static final int PASSKEY = 1234;
-    private static DiscoveryItem sDiscoveryItem;
-    private static PairingProgressHandlerBase sPairingProgressHandlerBase;
-    private static BluetoothDevice sBluetoothDevice;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        when(mContextWrapper.getSystemService(BluetoothManager.class))
-                .thenReturn(mBluetoothManager);
-        when(mContextWrapper.getLocator()).thenReturn(mLocator);
-        mLocator.overrideBindingForTest(FastPairCacheManager.class,
-                mFastPairCacheManager);
-        mLocator.overrideBindingForTest(Clock.class, mClock);
-        when(mContextWrapper.getResources()).thenReturn(mResources);
-        HalfSheetResources.setResourcesContextForTest(mContextWrapper);
-
-        sBluetoothDevice =
-                BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
-        sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        sDiscoveryItem.setStoredItemForTest(
-                sDiscoveryItem.getStoredItemForTest().toBuilder()
-                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
-                        .setFastPairInformation(
-                                Cache.FastPairInformation.newBuilder()
-                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
-                        .build());
-
-        sPairingProgressHandlerBase =
-                createProgressHandler(ACCOUNT_KEY, sDiscoveryItem, /* isRetroactivePair= */ false);
-    }
-
-    @Test
-    public void createHandler_halfSheetSubsequentPairing_notificationPairingHandlerCreated() {
-        sDiscoveryItem.setStoredItemForTest(
-                sDiscoveryItem.getStoredItemForTest().toBuilder()
-                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
-                        .setFastPairInformation(
-                                Cache.FastPairInformation.newBuilder()
-                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
-                        .build());
-
-        PairingProgressHandlerBase progressHandler =
-                createProgressHandler(ACCOUNT_KEY, sDiscoveryItem, /* isRetroactivePair= */ false);
-
-        assertThat(progressHandler).isInstanceOf(NotificationPairingProgressHandler.class);
-    }
-
-    @Test
-    public void createHandler_halfSheetInitialPairing_halfSheetPairingHandlerCreated() {
-        // No account key
-        sDiscoveryItem.setStoredItemForTest(
-                sDiscoveryItem.getStoredItemForTest().toBuilder()
-                        .setFastPairInformation(
-                                Cache.FastPairInformation.newBuilder()
-                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
-                        .build());
-
-        PairingProgressHandlerBase progressHandler =
-                createProgressHandler(null, sDiscoveryItem, /* isRetroactivePair= */ false);
-
-        assertThat(progressHandler).isInstanceOf(HalfSheetPairingProgressHandler.class);
-    }
-
-    @Test
-    public void onPairingStarted() {
-        sPairingProgressHandlerBase.onPairingStarted();
-    }
-
-    @Test
-    public void onWaitForScreenUnlock() {
-        sPairingProgressHandlerBase.onWaitForScreenUnlock();
-    }
-
-    @Test
-    public void  onScreenUnlocked() {
-        sPairingProgressHandlerBase.onScreenUnlocked();
-    }
-
-    @Test
-    public void onReadyToPair() {
-        sPairingProgressHandlerBase.onReadyToPair();
-    }
-
-    @Test
-    public void  onSetupPreferencesBuilder() {
-        Preferences.Builder prefsBuilder =
-                Preferences.builder()
-                        .setEnableBrEdrHandover(false)
-                        .setIgnoreDiscoveryError(true);
-        sPairingProgressHandlerBase.onSetupPreferencesBuilder(prefsBuilder);
-    }
-
-    @Test
-    public void  onPairingSetupCompleted() {
-        sPairingProgressHandlerBase.onPairingSetupCompleted();
-    }
-
-    @Test
-    public void onHandlePasskeyConfirmation() {
-        sPairingProgressHandlerBase.onHandlePasskeyConfirmation(sBluetoothDevice, PASSKEY);
-    }
-
-    @Test
-    public void getKeyForLocalCache() {
-        FastPairConnection.SharedSecret sharedSecret =
-                FastPairConnection.SharedSecret.create(ACCOUNT_KEY, sDiscoveryItem.getMacAddress());
-        sPairingProgressHandlerBase
-                .getKeyForLocalCache(ACCOUNT_KEY, mFastPairConnection, sharedSecret);
-    }
-
-    @Test
-    public void onPairedCallbackCalled() {
-        sPairingProgressHandlerBase.onPairedCallbackCalled(mFastPairConnection,
-                ACCOUNT_KEY, mFootprintsDeviceManager, MAC_ADDRESS);
-    }
-
-    @Test
-    public void onPairingFailed() {
-        Throwable e = new Throwable("Pairing Failed");
-        sPairingProgressHandlerBase.onPairingFailed(e);
-    }
-
-    @Test
-    public void onPairingSuccess() {
-        sPairingProgressHandlerBase.onPairingSuccess(sDiscoveryItem.getMacAddress());
-    }
-
-    @Test
-    public void  optInFootprintsForInitialPairing() {
-        sPairingProgressHandlerBase.optInFootprintsForInitialPairing(
-                mFootprintsDeviceManager, sDiscoveryItem, ACCOUNT_KEY, null);
-    }
-
-    @Test
-    public void skipWaitingScreenUnlock() {
-        assertThat(sPairingProgressHandlerBase.skipWaitingScreenUnlock()).isFalse();
-    }
-
-    private PairingProgressHandlerBase createProgressHandler(
-            @Nullable byte[] accountKey, DiscoveryItem fastPairItem, boolean isRetroactivePair) {
-
-        FastPairHalfSheetManager fastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-        // Real context is needed
-        Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        FastPairNotificationManager fastPairNotificationManager =
-                new FastPairNotificationManager(context, 1234, mNotificationManager,
-                        new HalfSheetResources(mContextWrapper));
-
-        mLocator.overrideBindingForTest(FastPairNotificationManager.class,
-                fastPairNotificationManager);
-        mLocator.overrideBindingForTest(FastPairHalfSheetManager.class, fastPairHalfSheetManager);
-        PairingProgressHandlerBase pairingProgressHandlerBase =
-                PairingProgressHandlerBase.create(
-                        mContextWrapper,
-                        fastPairItem,
-                        fastPairItem.getAppPackageName(),
-                        accountKey,
-                        mFootprintsDeviceManager,
-                        fastPairNotificationManager,
-                        fastPairHalfSheetManager,
-                        isRetroactivePair);
-        return pairingProgressHandlerBase;
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
deleted file mode 100644
index cdec04d..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
+++ /dev/null
@@ -1,88 +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.server.nearby.fastpair.testing;
-
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-
-import service.proto.Cache;
-
-public class FakeDiscoveryItems {
-    private static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
-    private static final long DEFAULT_TIMESTAMP = 1000000000L;
-    private static final String DEFAULT_DESCRIPITON = "description";
-    private static final String APP_NAME = "app_name";
-    private static final String ACTION_URL =
-            "intent:#Intent;action=com.android.server.nearby:ACTION_FAST_PAIR;"
-                    + "package=com.google.android.gms;"
-                    + "component=com.google.android.gms/"
-                    + ".nearby.discovery.service.DiscoveryService;end";
-    private static final String DISPLAY_URL = "DISPLAY_URL";
-    private static final String TRIGGER_ID = "trigger.id";
-    private static final String FAST_PAIR_ID = "id";
-    private static final int RSSI = -80;
-    private static final int TX_POWER = -10;
-    public static DiscoveryItem newFastPairDiscoveryItem(LocatorContextWrapper contextWrapper) {
-        return new DiscoveryItem(contextWrapper, newFastPairDeviceStoredItem());
-    }
-
-    public static Cache.StoredDiscoveryItem newFastPairDeviceStoredItem() {
-        return newFastPairDeviceStoredItem(TRIGGER_ID);
-    }
-
-    public static Cache.StoredDiscoveryItem newFastPairDeviceStoredItem(String triggerId) {
-        Cache.StoredDiscoveryItem.Builder item = Cache.StoredDiscoveryItem.newBuilder();
-        item.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
-        item.setId(FAST_PAIR_ID);
-        item.setDescription(DEFAULT_DESCRIPITON);
-        item.setTriggerId(triggerId);
-        item.setMacAddress(DEFAULT_MAC_ADDRESS);
-        item.setFirstObservationTimestampMillis(DEFAULT_TIMESTAMP);
-        item.setLastObservationTimestampMillis(DEFAULT_TIMESTAMP);
-        item.setActionUrl(ACTION_URL);
-        item.setAppName(APP_NAME);
-        item.setRssi(RSSI);
-        item.setTxPower(TX_POWER);
-        item.setDisplayUrl(DISPLAY_URL);
-        return item.build();
-    }
-
-    public static Cache.StoredDiscoveryItem newFastPairDeviceStoredItem(String id,
-            String description, String triggerId, String macAddress, String title,
-            int rssi, int txPower) {
-        Cache.StoredDiscoveryItem.Builder item = Cache.StoredDiscoveryItem.newBuilder();
-        item.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
-        if (id != null) {
-            item.setId(id);
-        }
-        if (description != null) {
-            item.setDescription(description);
-        }
-        if (triggerId != null) {
-            item.setTriggerId(triggerId);
-        }
-        if (macAddress != null) {
-            item.setMacAddress(macAddress);
-        }
-        if (title != null) {
-            item.setTitle(title);
-        }
-        item.setRssi(rssi);
-        item.setTxPower(txPower);
-        return item.build();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingLocator.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingLocator.java
deleted file mode 100644
index c9a4533..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingLocator.java
+++ /dev/null
@@ -1,53 +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 src.com.android.server.nearby.fastpair.testing;
-
-import android.content.Context;
-
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-
-/** A locator for tests that, by default, installs mocks for everything that's requested of it. */
-public class MockingLocator extends Locator {
-    private final LocatorContextWrapper mLocatorContextWrapper;
-
-    /**
-     * Creates a MockingLocator with the explicit bindings already configured on the given locator.
-     */
-    public static MockingLocator withBindings(Context context, Locator locator) {
-        Locator mockingLocator = new Locator(context);
-        mockingLocator.bind(new MockingModule());
-        locator.attachParent(mockingLocator);
-        return new MockingLocator(context, locator);
-    }
-
-    /** Creates a MockingLocator with no explicit bindings. */
-    public static MockingLocator withMocksOnly(Context context) {
-        return withBindings(context, new Locator(context));
-    }
-
-    @SuppressWarnings("nullness") // due to passing in this before initialized.
-    public MockingLocator(Context context, Locator locator) {
-        super(context, locator);
-        this.mLocatorContextWrapper = new LocatorContextWrapper(context, this);
-    }
-
-    /** Returns a LocatorContextWrapper with this Locator attached. */
-    public LocatorContextWrapper getContextForTest() {
-        return mLocatorContextWrapper;
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingModule.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingModule.java
deleted file mode 100644
index 7938c55..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingModule.java
+++ /dev/null
@@ -1,39 +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 src.com.android.server.nearby.fastpair.testing;
-
-import android.content.Context;
-
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.Module;
-
-
-import org.mockito.Mockito;
-
-/** Module for tests that just provides mocks for anything that's requested of it. */
-public class MockingModule extends Module {
-
-    @Override
-    public void configure(Context context, Class<?> type, Locator locator) {
-        configureMock(type, locator);
-    }
-
-    private <T> void configureMock(Class<T> type, Locator locator) {
-        T mock = Mockito.mock(type);
-        locator.bind(type, mock);
-    }
-}
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
new file mode 100644
index 0000000..32286e1
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.nearby.managers;
+
+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;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.bluetooth.finder.Eid;
+import android.hardware.bluetooth.finder.IBluetoothFinder;
+import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+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;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+public class BluetoothFinderManagerTest {
+    private BluetoothFinderManager mBluetoothFinderManager;
+    private boolean mGetServiceCalled = false;
+
+    @Mock private IBluetoothFinder mIBluetoothFinderMock;
+    @Mock private IBinder mServiceBinderMock;
+
+    private ArgumentCaptor<DeathRecipient> mDeathRecipientCaptor =
+            ArgumentCaptor.forClass(DeathRecipient.class);
+
+    private ArgumentCaptor<Eid[]> mEidArrayCaptor = ArgumentCaptor.forClass(Eid[].class);
+
+    private class BluetoothFinderManagerSpy extends BluetoothFinderManager {
+        @Override
+        protected IBluetoothFinder getServiceMockable() {
+            mGetServiceCalled = true;
+            return mIBluetoothFinderMock;
+        }
+
+        @Override
+        protected IBinder getServiceBinderMockable() {
+            return mServiceBinderMock;
+        }
+    }
+
+    @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();
+    }
+
+    @Test
+    public void testSendEids() throws Exception {
+        byte[] eidBytes1 = {
+                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+                (byte) 0xe1, (byte) 0xde
+        };
+        byte[] eidBytes2 = {
+                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+                (byte) 0xf2, (byte) 0xef
+        };
+        PoweredOffFindingEphemeralId ephemeralId1 = new PoweredOffFindingEphemeralId();
+        PoweredOffFindingEphemeralId ephemeralId2 = new PoweredOffFindingEphemeralId();
+        ephemeralId1.bytes = eidBytes1;
+        ephemeralId2.bytes = eidBytes2;
+
+        mBluetoothFinderManager.sendEids(List.of(ephemeralId1, ephemeralId2));
+
+        verify(mIBluetoothFinderMock).sendEids(mEidArrayCaptor.capture());
+        assertThat(mEidArrayCaptor.getValue()[0].bytes).isEqualTo(eidBytes1);
+        assertThat(mEidArrayCaptor.getValue()[1].bytes).isEqualTo(eidBytes2);
+    }
+
+    @Test
+    public void testSendEids_remoteException() throws Exception {
+        doThrow(new RemoteException())
+                .when(mIBluetoothFinderMock).sendEids(any());
+        mBluetoothFinderManager.sendEids(List.of());
+
+        // Verify that we get the service again following a RemoteException.
+        mGetServiceCalled = false;
+        mBluetoothFinderManager.sendEids(List.of());
+        assertThat(mGetServiceCalled).isTrue();
+    }
+
+    @Test
+    public void testSendEids_serviceSpecificException() throws Exception {
+        doThrow(new ServiceSpecificException(1))
+                .when(mIBluetoothFinderMock).sendEids(any());
+        mBluetoothFinderManager.sendEids(List.of());
+    }
+
+    @Test
+    public void testSetPoweredOffFinderMode() throws Exception {
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+        verify(mIBluetoothFinderMock).setPoweredOffFinderMode(true);
+
+        mBluetoothFinderManager.setPoweredOffFinderMode(false);
+        verify(mIBluetoothFinderMock).setPoweredOffFinderMode(false);
+    }
+
+    @Test
+    public void testSetPoweredOffFinderMode_remoteException() throws Exception {
+        doThrow(new RemoteException())
+                .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+
+        // Verify that we get the service again following a RemoteException.
+        mGetServiceCalled = false;
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+        assertThat(mGetServiceCalled).isTrue();
+    }
+
+    @Test
+    public void testSetPoweredOffFinderMode_serviceSpecificException() throws Exception {
+        doThrow(new ServiceSpecificException(1))
+                .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+    }
+
+    @Test
+    public void testGetPoweredOffFinderMode() throws Exception {
+        when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(true);
+        assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isTrue();
+
+        when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(false);
+        assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+    }
+
+    @Test
+    public void testGetPoweredOffFinderMode_remoteException() throws Exception {
+        when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenThrow(new RemoteException());
+        assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+
+        // Verify that we get the service again following a RemoteException.
+        mGetServiceCalled = false;
+        assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+        assertThat(mGetServiceCalled).isTrue();
+    }
+
+    @Test
+    public void testGetPoweredOffFinderMode_serviceSpecificException() throws Exception {
+        when(mIBluetoothFinderMock.getPoweredOffFinderMode())
+                .thenThrow(new ServiceSpecificException(1));
+        assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+    }
+
+    @Test
+    public void testDeathRecipient() throws Exception {
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+        verify(mServiceBinderMock).linkToDeath(mDeathRecipientCaptor.capture(), anyInt());
+        mDeathRecipientCaptor.getValue().binderDied();
+
+        // Verify that we get the service again following a binder death.
+        mGetServiceCalled = false;
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+        assertThat(mGetServiceCalled).isTrue();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
index bc38210..7ff7b13 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
@@ -24,7 +24,9 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.UiAutomation;
 import android.content.Context;
@@ -34,6 +36,7 @@
 import android.nearby.PresenceBroadcastRequest;
 import android.nearby.PresenceCredential;
 import android.nearby.PrivateCredential;
+import android.os.IBinder;
 import android.provider.DeviceConfig;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -74,6 +77,8 @@
     IBroadcastListener mBroadcastListener;
     @Mock
     BleBroadcastProvider mBleBroadcastProvider;
+    @Mock
+    IBinder mBinder;
     private Context mContext;
     private BroadcastProviderManager mBroadcastProviderManager;
     private BroadcastRequest mBroadcastRequest;
@@ -82,9 +87,12 @@
 
     @Before
     public void setUp() {
+        when(mBroadcastListener.asBinder()).thenReturn(mBinder);
         mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
         DeviceConfig.setProperty(
                 NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "true", false);
+        DeviceConfig.setProperty(
+                NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
 
         mContext = ApplicationProvider.getApplicationContext();
         mBroadcastProviderManager = new BroadcastProviderManager(
@@ -104,15 +112,28 @@
     }
 
     @Test
-    public void testStartAdvertising() {
+    public void testStartAdvertising() throws Exception {
         mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
         verify(mBleBroadcastProvider).start(eq(BroadcastRequest.PRESENCE_VERSION_V0),
                 any(byte[].class), any(BleBroadcastProvider.BroadcastListener.class));
+        verify(mBinder).linkToDeath(any(), eq(0));
     }
 
     @Test
     public void testStopAdvertising() {
+        mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
         mBroadcastProviderManager.stopBroadcast(mBroadcastListener);
+        verify(mBinder).unlinkToDeath(any(), eq(0));
+    }
+
+    @Test
+    public void testRegisterAdvertising_twoTimes_fail() throws Exception {
+        IBroadcastListener newListener = mock(IBroadcastListener.class);
+        IBinder newBinder = mock(IBinder.class);
+        when(newListener.asBinder()).thenReturn(newBinder);
+        mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
+        mBroadcastProviderManager.startBroadcast(mBroadcastRequest, newListener);
+        verify(newListener).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
     }
 
     @Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
index aa0dad3..0ca571a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.managers;
 
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
 
@@ -23,10 +24,12 @@
 
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -36,6 +39,8 @@
 import android.nearby.ScanRequest;
 import android.os.IBinder;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.provider.BleDiscoveryProvider;
 import com.android.server.nearby.provider.ChreCommunication;
@@ -86,6 +91,8 @@
     DiscoveryProviderManagerLegacy.ScanListenerDeathRecipient mScanListenerDeathRecipient;
     @Mock
     IBinder mIBinder;
+    @Mock
+    BluetoothAdapter mBluetoothAdapter;
     private DiscoveryProviderManagerLegacy mDiscoveryProviderManager;
     private Map<IBinder, DiscoveryProviderManagerLegacy.ScanListenerRecord>
             mScanTypeScanListenerRecordMap;
@@ -135,10 +142,15 @@
 
     @Before
     public void setup() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                READ_DEVICE_CONFIG);
         MockitoAnnotations.initMocks(this);
         when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+        when(mInjector.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
         when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
         when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
 
         mScanTypeScanListenerRecordMap = new HashMap<>();
         mDiscoveryProviderManager =
@@ -164,6 +176,13 @@
     }
 
     @Test
+    public void test_enableBleWhenBleOff() throws Exception {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        mDiscoveryProviderManager.init();
+        verify(mBluetoothAdapter, times(1)).enableBLE();
+    }
+
+    @Test
     public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
         when(mChreDiscoveryProvider.available()).thenReturn(true);
 
@@ -375,4 +394,62 @@
                 .isTrue();
         assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
     }
+
+    @Test
+    public void isBluetoothEnabledTest_bluetoothEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void isBluetoothEnabledTest_bleEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enableFailed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
+
+    @Test
+    public void enabledTest_scanIsOn() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_failed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
index 7ecf631..7cea34a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.managers;
 
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
 
@@ -24,10 +25,12 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
 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.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -37,6 +40,8 @@
 import android.nearby.ScanRequest;
 import android.os.IBinder;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.provider.BleDiscoveryProvider;
 import com.android.server.nearby.provider.ChreCommunication;
@@ -80,6 +85,8 @@
     CallerIdentity mCallerIdentity;
     @Mock
     IBinder mIBinder;
+    @Mock
+    BluetoothAdapter mBluetoothAdapter;
     private Executor mExecutor;
     private DiscoveryProviderManager mDiscoveryProviderManager;
 
@@ -128,12 +135,17 @@
 
     @Before
     public void setup() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                READ_DEVICE_CONFIG);
         MockitoAnnotations.initMocks(this);
         mExecutor = Executors.newSingleThreadExecutor();
         when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
         when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
         when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
         when(mScanListener.asBinder()).thenReturn(mIBinder);
+        when(mInjector.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
 
         mDiscoveryProviderManager =
                 new DiscoveryProviderManager(mContext, mExecutor, mInjector,
@@ -157,6 +169,13 @@
     }
 
     @Test
+    public void test_enableBleWhenBleOff() throws Exception {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        mDiscoveryProviderManager.init();
+        verify(mBluetoothAdapter, times(1)).enableBLE();
+    }
+
+    @Test
     public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
         reset(mBluetoothController);
         when(mChreDiscoveryProvider.available()).thenReturn(true);
@@ -336,4 +355,62 @@
                 .isTrue();
         assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
     }
+
+    @Test
+    public void isBluetoothEnabledTest_bluetoothEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void isBluetoothEnabledTest_bleEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enableFailed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
+
+    @Test
+    public void enabledTest_scanIsOn() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_failed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
new file mode 100644
index 0000000..6ec7c57
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.server.nearby.presence.EncryptionInfo.EncodingScheme;
+import com.android.server.nearby.util.ArrayUtils;
+
+import org.junit.Test;
+
+
+/**
+ * Unit test for {@link EncryptionInfo}.
+ */
+public class EncryptionInfoTest {
+    private static final byte[] SALT =
+            new byte[]{25, -21, 35, -108, -26, -126, 99, 60, 110, 45, -116, 34, 91, 126, -23, 127};
+
+    @Test
+    public void test_illegalLength() {
+        byte[] data = new byte[]{1, 2};
+        assertThrows(IllegalArgumentException.class, () -> new EncryptionInfo(data));
+    }
+
+    @Test
+    public void test_illegalEncodingScheme() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new EncryptionInfo(ArrayUtils.append((byte) 0b10110000, SALT)));
+        assertThrows(IllegalArgumentException.class,
+                () -> new EncryptionInfo(ArrayUtils.append((byte) 0b01101000, SALT)));
+    }
+
+    @Test
+    public void test_getMethods_signature() {
+        byte[] data = ArrayUtils.append((byte) 0b10001000, SALT);
+        EncryptionInfo info = new EncryptionInfo(data);
+        assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.SIGNATURE);
+        assertThat(info.getSalt()).isEqualTo(SALT);
+    }
+
+    @Test
+    public void test_getMethods_mic() {
+        byte[] data = ArrayUtils.append((byte) 0b10000000, SALT);
+        EncryptionInfo info = new EncryptionInfo(data);
+        assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.MIC);
+        assertThat(info.getSalt()).isEqualTo(SALT);
+    }
+    @Test
+    public void test_toBytes() {
+        byte[] data = EncryptionInfo.toByte(EncodingScheme.MIC, SALT);
+        EncryptionInfo info = new EncryptionInfo(data);
+        assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.MIC);
+        assertThat(info.getSalt()).isEqualTo(SALT);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
index 895df69..3f00a42 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.nearby.presence;
 
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID_BYTES;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.nearby.BroadcastRequest;
@@ -25,19 +27,22 @@
 import android.nearby.PrivateCredential;
 import android.nearby.PublicCredential;
 
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
-import com.android.server.nearby.util.encryption.CryptorImpV1;
+import com.android.server.nearby.util.ArrayUtils;
+import com.android.server.nearby.util.encryption.CryptorMicImp;
 
 import org.junit.Before;
 import org.junit.Test;
 
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
 public class ExtendedAdvertisementTest {
+    private static final int EXTENDED_ADVERTISEMENT_BYTE_LENGTH = 67;
     private static final int IDENTITY_TYPE = PresenceCredential.IDENTITY_TYPE_PRIVATE;
+    private static final int DATA_TYPE_ACTION = 6;
     private static final int DATA_TYPE_MODEL_ID = 7;
     private static final int DATA_TYPE_BLE_ADDRESS = 101;
     private static final int DATA_TYPE_PUBLIC_IDENTITY = 3;
@@ -49,18 +54,23 @@
     private static final DataElement BLE_ADDRESS_ELEMENT =
             new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS);
 
-    private static final byte[] IDENTITY =
-            new byte[]{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+    private static final byte[] METADATA_ENCRYPTION_KEY =
+            new byte[]{-39, -55, 115, 78, -57, 40, 115, 0, -112, 86, -86, 7, -42, 68, 11, 12};
     private static final int MEDIUM_TYPE_BLE = 0;
     private static final byte[] SALT = {2, 3};
+
     private static final int PRESENCE_ACTION_1 = 1;
     private static final int PRESENCE_ACTION_2 = 2;
+    private static final DataElement PRESENCE_ACTION_DE_1 =
+            new DataElement(DATA_TYPE_ACTION, new byte[]{PRESENCE_ACTION_1});
+    private static final DataElement PRESENCE_ACTION_DE_2 =
+            new DataElement(DATA_TYPE_ACTION, new byte[]{PRESENCE_ACTION_2});
 
     private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
     private static final byte[] AUTHENTICITY_KEY =
             new byte[]{-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
     private static final byte[] PUBLIC_KEY =
-            new byte[] {
+            new byte[]{
                     48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61,
                     66, 0, 4, -56, -39, -92, 69, 0, 52, 23, 67, 83, -14, 75, 52, -14, -5, -41, 48,
                     -83, 31, 42, -39, 102, -13, 22, -73, -73, 86, 30, -96, -84, -13, 4, 122, 104,
@@ -68,7 +78,7 @@
                     123, 41, -119, -25, 1, -112, 112
             };
     private static final byte[] ENCRYPTED_METADATA_BYTES =
-            new byte[] {
+            new byte[]{
                     -44, -25, -95, -124, -7, 90, 116, -8, 7, -120, -23, -22, -106, -44, -19, 61,
                     -18, 39, 29, 78, 108, -11, -39, 85, -30, 64, -99, 102, 65, 37, -42, 114, -37,
                     88, -112, 8, -75, -53, 23, -16, -104, 67, 49, 48, -53, 73, -109, 44, -23, -11,
@@ -78,23 +88,65 @@
                     -4, -46, -30, -85, -50, 100, 46, -66, -128, 7, 66, 9, 88, 95, 12, -13, 81, -91,
             };
     private static final byte[] METADATA_ENCRYPTION_KEY_TAG =
-            new byte[] {-126, -104, 1, -1, 26, -46, -68, -86};
+            new byte[]{-100, 102, -35, -99, 66, -85, -55, -58, -52, 11, -74, 102, 109, -89, 1, -34,
+                    45, 43, 107, -60, 99, -21, 28, 34, 31, -100, -96, 108, 108, -18, 107, 5};
+
+    private static final String ENCODED_ADVERTISEMENT_ENCRYPTION_INFO =
+            "2091911000DE2A89ED98474AF3E41E48487E8AEBDE90014C18BCB9F9AAC5C11A1BE00A10A5DCD2C49A74BE"
+                    + "BAF0FE72FD5053B9DF8B9976C80BE0DCE8FEE83F1BFA9A89EB176CA48EE4ED5D15C6CDAD6B9E"
+                    + "41187AA6316D7BFD8E454A53971AC00836F7AB0771FF0534050037D49C6AEB18CF9F8590E5CD"
+                    + "EE2FBC330FCDC640C63F0735B7E3F02FE61A0496EF976A158AD3455D";
+    private static final byte[] METADATA_ENCRYPTION_KEY_TAG_2 =
+            new byte[]{-54, -39, 41, 16, 61, 79, -116, 14, 94, 0, 84, 45, 26, -108, 66, -48, 124,
+                    -81, 61, 56, -98, -47, 14, -19, 116, 106, -27, 123, -81, 49, 83, -42};
+
     private static final String DEVICE_NAME = "test_device";
 
+    private static final byte[] SALT_16 =
+            ArrayUtils.stringToBytes("DE2A89ED98474AF3E41E48487E8AEBDE");
+    private static final byte[] AUTHENTICITY_KEY_2 =  ArrayUtils.stringToBytes(
+            "959D2F3CAB8EE4A2DEB0255C03762CF5D39EB919300420E75A089050FB025E20");
+    private static final byte[] METADATA_ENCRYPTION_KEY_2 =  ArrayUtils.stringToBytes(
+            "EF5E9A0867560E52AE1F05FCA7E48D29");
+
+    private static final DataElement DE1 = new DataElement(571, ArrayUtils.stringToBytes(
+            "537F96FD94E13BE589F0141145CFC0EEC4F86FBDB2"));
+    private static final DataElement DE2 = new DataElement(541, ArrayUtils.stringToBytes(
+            "D301FFB24B5B"));
+    private static final DataElement DE3 = new DataElement(51, ArrayUtils.stringToBytes(
+            "EA95F07C25B75C04E1B2B8731F6A55BA379FB141"));
+    private static final DataElement DE4 = new DataElement(729, ArrayUtils.stringToBytes(
+            "2EFD3101E2311BBB108F0A7503907EAF0C2EAAA60CDA8D33A294C4CEACE0"));
+    private static final DataElement DE5 = new DataElement(411, ArrayUtils.stringToBytes("B0"));
+
     private PresenceBroadcastRequest.Builder mBuilder;
+    private PresenceBroadcastRequest.Builder mBuilderCredentialInfo;
     private PrivateCredential mPrivateCredential;
+    private PrivateCredential mPrivateCredential2;
+
     private PublicCredential mPublicCredential;
+    private PublicCredential mPublicCredential2;
 
     @Before
     public void setUp() {
         mPrivateCredential =
-                new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+                new PrivateCredential.Builder(
+                        SECRET_ID, AUTHENTICITY_KEY, METADATA_ENCRYPTION_KEY, DEVICE_NAME)
+                        .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+                        .build();
+        mPrivateCredential2 =
+                new PrivateCredential.Builder(
+                        SECRET_ID, AUTHENTICITY_KEY_2, METADATA_ENCRYPTION_KEY_2, DEVICE_NAME)
                         .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
                         .build();
         mPublicCredential =
                 new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
                         ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG)
                         .build();
+        mPublicCredential2 =
+                new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY_2, PUBLIC_KEY,
+                        ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG_2)
+                        .build();
         mBuilder =
                 new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
                         SALT, mPrivateCredential)
@@ -103,6 +155,16 @@
                         .addAction(PRESENCE_ACTION_2)
                         .addExtendedProperty(new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS))
                         .addExtendedProperty(new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA));
+
+        mBuilderCredentialInfo =
+                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+                        SALT_16, mPrivateCredential2)
+                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+                        .addExtendedProperty(DE1)
+                        .addExtendedProperty(DE2)
+                        .addExtendedProperty(DE3)
+                        .addExtendedProperty(DE4)
+                        .addExtendedProperty(DE5);
     }
 
     @Test
@@ -112,36 +174,50 @@
 
         assertThat(originalAdvertisement.getActions())
                 .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
-        assertThat(originalAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(originalAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
         assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
-        assertThat(originalAdvertisement.getLength()).isEqualTo(66);
         assertThat(originalAdvertisement.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V1);
         assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
         assertThat(originalAdvertisement.getDataElements())
-                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+                .containsExactly(PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2,
+                        MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+        assertThat(originalAdvertisement.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
+    }
+
+    @Test
+    public void test_createFromRequest_credentialInfo() {
+        ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+                mBuilderCredentialInfo.build());
+
+        assertThat(originalAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY_2);
+        assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+        assertThat(originalAdvertisement.getVersion()).isEqualTo(
+                BroadcastRequest.PRESENCE_VERSION_V1);
+        assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT_16);
+        assertThat(originalAdvertisement.getDataElements())
+                .containsExactly(DE1, DE2, DE3, DE4, DE5);
     }
 
     @Test
     public void test_createFromRequest_encodeAndDecode() {
         ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
                 mBuilder.build());
-
         byte[] generatedBytes = originalAdvertisement.toBytes();
-
         ExtendedAdvertisement newAdvertisement =
                 ExtendedAdvertisement.fromBytes(generatedBytes, mPublicCredential);
 
         assertThat(newAdvertisement.getActions())
                 .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
-        assertThat(newAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(newAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
         assertThat(newAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
-        assertThat(newAdvertisement.getLength()).isEqualTo(66);
+        assertThat(newAdvertisement.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
         assertThat(newAdvertisement.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V1);
         assertThat(newAdvertisement.getSalt()).isEqualTo(SALT);
         assertThat(newAdvertisement.getDataElements())
-                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT,
+                        PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2);
     }
 
     @Test
@@ -170,45 +246,77 @@
                         .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
                         .addAction(PRESENCE_ACTION_1);
         assertThat(ExtendedAdvertisement.createFromRequest(builder2.build())).isNull();
-
-        // empty action
-        PresenceBroadcastRequest.Builder builder3 =
-                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
-                        SALT, mPrivateCredential)
-                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V1);
-        assertThat(ExtendedAdvertisement.createFromRequest(builder3.build())).isNull();
     }
 
     @Test
-    public void test_toBytes() {
+    public void test_toBytesSalt() throws Exception {
         ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
         assertThat(adv.toBytes()).isEqualTo(getExtendedAdvertisementByteArray());
     }
 
     @Test
-    public void test_fromBytes() {
+    public void test_fromBytesSalt() throws Exception {
         byte[] originalBytes = getExtendedAdvertisementByteArray();
         ExtendedAdvertisement adv =
                 ExtendedAdvertisement.fromBytes(originalBytes, mPublicCredential);
 
         assertThat(adv.getActions())
                 .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
-        assertThat(adv.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(adv.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
         assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
-        assertThat(adv.getLength()).isEqualTo(66);
+        assertThat(adv.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
         assertThat(adv.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V1);
         assertThat(adv.getSalt()).isEqualTo(SALT);
         assertThat(adv.getDataElements())
-                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT,
+                        PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2);
+    }
+
+    @Test
+    public void test_toBytesCredentialElement() {
+        ExtendedAdvertisement adv =
+                ExtendedAdvertisement.createFromRequest(mBuilderCredentialInfo.build());
+        assertThat(ArrayUtils.bytesToStringUppercase(adv.toBytes())).isEqualTo(
+                ENCODED_ADVERTISEMENT_ENCRYPTION_INFO);
+    }
+
+    @Test
+    public void test_fromBytesCredentialElement() {
+        ExtendedAdvertisement adv =
+                ExtendedAdvertisement.fromBytes(
+                        ArrayUtils.stringToBytes(ENCODED_ADVERTISEMENT_ENCRYPTION_INFO),
+                        mPublicCredential2);
+        assertThat(adv.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY_2);
+        assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+        assertThat(adv.getVersion()).isEqualTo(BroadcastRequest.PRESENCE_VERSION_V1);
+        assertThat(adv.getSalt()).isEqualTo(SALT_16);
+        assertThat(adv.getDataElements()).containsExactly(DE1, DE2, DE3, DE4, DE5);
+    }
+
+    @Test
+    public void test_fromBytes_metadataTagNotMatched_fail() throws Exception {
+        byte[] originalBytes = getExtendedAdvertisementByteArray();
+        PublicCredential credential =
+                new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+                        ENCRYPTED_METADATA_BYTES,
+                        new byte[]{113, 90, -55, 73, 25, -9, 55, -44, 102, 44, 81, -68, 101, 21, 32,
+                                92, -107, 3, 108, 90, 28, -73, 16, 49, -95, -121, 8, -45, -27, 16,
+                                6, 108})
+                        .build();
+        ExtendedAdvertisement adv =
+                ExtendedAdvertisement.fromBytes(originalBytes, credential);
+        assertThat(adv).isNull();
     }
 
     @Test
     public void test_toString() {
         ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
         assertThat(adv.toString()).isEqualTo("ExtendedAdvertisement:"
-                + "<VERSION: 1, length: 66, dataElementCount: 2, identityType: 1, "
-                + "identity: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], salt: [2, 3],"
+                + "<VERSION: 1, length: " + EXTENDED_ADVERTISEMENT_BYTE_LENGTH
+                + ", dataElementCount: 4, identityType: 1, "
+                + "identity: " + Arrays.toString(METADATA_ENCRYPTION_KEY)
+                + ", salt: [2, 3],"
                 + " actions: [1, 2]>");
     }
 
@@ -222,26 +330,22 @@
         assertThat(adv.getDataElements(DATA_TYPE_PUBLIC_IDENTITY)).isEmpty();
     }
 
-    private static byte[] getExtendedAdvertisementByteArray() {
-        ByteBuffer buffer = ByteBuffer.allocate(66);
+    private static byte[] getExtendedAdvertisementByteArray() throws Exception {
+        CryptorMicImp cryptor = CryptorMicImp.getInstance();
+        ByteBuffer buffer = ByteBuffer.allocate(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
         buffer.put((byte) 0b00100000); // Header V1
-        buffer.put((byte) 0b00100000); // Salt header: length 2, type 0
+        buffer.put(
+                (byte) (EXTENDED_ADVERTISEMENT_BYTE_LENGTH - 2)); // Section header (section length)
+
         // Salt data
-        buffer.put(SALT);
+        // Salt header: length 2, type 0
+        byte[] saltBytes = ArrayUtils.concatByteArrays(new byte[]{(byte) 0b00100000}, SALT);
+        buffer.put(saltBytes);
         // Identity header: length 16, type 1 (private identity)
-        buffer.put(new byte[]{(byte) 0b10010000, (byte) 0b00000001});
-        // Identity data
-        buffer.put(CryptorImpIdentityV1.getInstance().encrypt(IDENTITY, SALT, AUTHENTICITY_KEY));
+        byte[] identityHeader = new byte[]{(byte) 0b10010000, (byte) 0b00000001};
+        buffer.put(identityHeader);
 
         ByteBuffer deBuffer = ByteBuffer.allocate(28);
-        // Action1 header: length 1, type 6
-        deBuffer.put(new byte[]{(byte) 0b00010110});
-        // Action1 data
-        deBuffer.put((byte) PRESENCE_ACTION_1);
-        // Action2 header: length 1, type 6
-        deBuffer.put(new byte[]{(byte) 0b00010110});
-        // Action2 data
-        deBuffer.put((byte) PRESENCE_ACTION_2);
         // Ble address header: length 7, type 102
         deBuffer.put(new byte[]{(byte) 0b10000111, (byte) 0b01100101});
         // Ble address data
@@ -250,11 +354,30 @@
         deBuffer.put(new byte[]{(byte) 0b10001101, (byte) 0b00000111});
         // model id data
         deBuffer.put(MODE_ID_DATA);
+        // Action1 header: length 1, type 6
+        deBuffer.put(new byte[]{(byte) 0b00010110});
+        // Action1 data
+        deBuffer.put((byte) PRESENCE_ACTION_1);
+        // Action2 header: length 1, type 6
+        deBuffer.put(new byte[]{(byte) 0b00010110});
+        // Action2 data
+        deBuffer.put((byte) PRESENCE_ACTION_2);
+        byte[] deBytes = deBuffer.array();
+        byte[] nonce = CryptorMicImp.generateAdvNonce(SALT);
+        byte[] ciphertext =
+                cryptor.encrypt(
+                        ArrayUtils.concatByteArrays(METADATA_ENCRYPTION_KEY, deBytes),
+                        nonce, AUTHENTICITY_KEY);
+        buffer.put(ciphertext);
 
-        byte[] data = deBuffer.array();
-        CryptorImpV1 cryptor = CryptorImpV1.getInstance();
-        buffer.put(cryptor.encrypt(data, SALT, AUTHENTICITY_KEY));
-        buffer.put(cryptor.sign(data, AUTHENTICITY_KEY));
+        byte[] dataToSign = ArrayUtils.concatByteArrays(
+                PRESENCE_UUID_BYTES, /* UUID */
+                new byte[]{(byte) 0b00100000}, /* header */
+                new byte[]{(byte) (EXTENDED_ADVERTISEMENT_BYTE_LENGTH - 2)} /* sectionHeader */,
+                saltBytes, /* salt */
+                nonce, identityHeader, ciphertext);
+        byte[] mic = cryptor.sign(dataToSign, AUTHENTICITY_KEY);
+        buffer.put(mic);
 
         return buffer.array();
     }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
index 9deb1eb..ca4f077 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
@@ -28,8 +28,6 @@
 import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -43,15 +41,13 @@
             new byte[] {-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
 
     @Mock private Context mContext;
-    private LocatorContextWrapper mLocatorContextWrapper;
     private PresenceManager mPresenceManager;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
-        mLocatorContextWrapper = new LocatorContextWrapper(mContext);
-        mPresenceManager = new PresenceManager(mLocatorContextWrapper);
+        mPresenceManager = new PresenceManager(mContext);
         when(mContext.getContentResolver())
                 .thenReturn(InstrumentationRegistry.getInstrumentation()
                         .getContext().getContentResolver());
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index 05b556b..0b1a742 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.provider;
 
+import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.times;
@@ -69,7 +70,7 @@
 
         AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
         mBleBroadcastProvider.onStartSuccess(settings);
-        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+        verify(mBroadcastListener, atLeast(1)).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
     }
 
     @Test
@@ -81,7 +82,7 @@
         // advertising set can not be mocked, so we will allow nulls
         mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
                 AdvertisingSetCallback.ADVERTISE_SUCCESS);
-        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+        verify(mBroadcastListener, atLeast(1)).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
     }
 
     @Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 154441b..590a46e 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -192,7 +192,12 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testOnNearbyDeviceDiscoveredWithDataElements() {
+    public void testOnNearbyDeviceDiscoveredWithDataElements_TIME() {
+        // The feature only supports user-debug builds.
+        if (!Build.isDebuggable()) {
+            return;
+        }
+
         // Disables the setting of test app support
         boolean isSupportedTestApp = getDeviceConfigBoolean(
                 NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
@@ -209,6 +214,7 @@
         // First byte is length of service data, padding zeros should be thrown away.
         final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
         final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        final long timestampNs = 1697765417070000000L;
 
         final List<DataElement> expectedExtendedProperties = new ArrayList<>();
         expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
@@ -262,6 +268,7 @@
                                 .setValue(ByteString.copyFrom(testData))
                                 .setValueLength(testData.length)
                         )
+                        .setTimestampNs(timestampNs)
                         .build();
         Blefilter.BleFilterResults results =
                 Blefilter.BleFilterResults.newBuilder().addResult(result).build();
@@ -285,11 +292,18 @@
             DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
             assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
         }
+        // Nanoseconds to Milliseconds
+        assertThat((mNearbyDevice.getValue().getPresenceDevice())
+                .getDiscoveryTimestampMillis()).isEqualTo(timestampNs / 1000000);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testOnNearbyDeviceDiscoveredWithTestDataElements() {
+        // The feature only supports user-debug builds.
+        if (!Build.isDebuggable()) {
+            return;
+        }
         // Enables the setting of test app support
         boolean isSupportedTestApp = getDeviceConfigBoolean(
                 NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/FastPairDataProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/FastPairDataProviderTest.java
deleted file mode 100644
index 300efbd..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/FastPairDataProviderTest.java
+++ /dev/null
@@ -1,376 +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.server.nearby.provider;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.accounts.Account;
-import android.content.Context;
-import android.nearby.FastPairDataProviderService;
-import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
-import android.nearby.aidl.FastPairDeviceMetadataParcel;
-import android.nearby.aidl.FastPairEligibleAccountParcel;
-import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
-import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
-import android.nearby.aidl.FastPairManageAccountRequestParcel;
-
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
-
-import com.google.common.collect.ImmutableList;
-import com.google.protobuf.ByteString;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import service.proto.Cache;
-import service.proto.FastPairString;
-import service.proto.Rpcs;
-
-public class FastPairDataProviderTest {
-
-    private static final Account ACCOUNT = new Account("abc@google.com", "type1");
-    private static final byte[] MODEL_ID = new byte[]{7, 9};
-    private static final int BLE_TX_POWER = 5;
-    private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
-    private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
-    private static final int DEVICE_TYPE = 1;
-    private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
-            "DOWNLOAD_COMPANION_APP_DESCRIPTION";
-    private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
-            "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
-    private static final byte[] IMAGE = new byte[]{7, 9};
-    private static final String IMAGE_URL = "IMAGE_URL";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION =
-            "INITIAL_NOTIFICATION_DESCRIPTION";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
-            "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
-    private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
-    private static final String INTENT_URI = "INTENT_URI";
-    private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
-    private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
-            "RETRO_ACTIVE_PAIRING_DESCRIPTION";
-    private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
-    private static final float TRIGGER_DISTANCE = 111;
-    private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
-    private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
-    private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
-    private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
-    private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
-    private static final String UPDATE_COMPANION_APP_DESCRIPTION =
-            "UPDATE_COMPANION_APP_DESCRIPTION";
-    private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
-            "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
-    private static final byte[] ACCOUNT_KEY = new byte[]{3};
-    private static final byte[] SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS = new byte[]{2, 8};
-    private static final byte[] ANTI_SPOOFING_KEY = new byte[]{4, 5, 6};
-    private static final String ACTION_URL = "ACTION_URL";
-    private static final String APP_NAME = "APP_NAME";
-    private static final byte[] AUTHENTICATION_PUBLIC_KEY_SEC_P256R1 = new byte[]{5, 7};
-    private static final String DESCRIPTION = "DESCRIPTION";
-    private static final String DEVICE_NAME = "DEVICE_NAME";
-    private static final String DISPLAY_URL = "DISPLAY_URL";
-    private static final long FIRST_OBSERVATION_TIMESTAMP_MILLIS = 8393L;
-    private static final String ICON_FIFE_URL = "ICON_FIFE_URL";
-    private static final byte[] ICON_PNG = new byte[]{2, 5};
-    private static final String ID = "ID";
-    private static final long LAST_OBSERVATION_TIMESTAMP_MILLIS = 934234L;
-    private static final String MAC_ADDRESS = "MAC_ADDRESS";
-    private static final String NAME = "NAME";
-    private static final String PACKAGE_NAME = "PACKAGE_NAME";
-    private static final long PENDING_APP_INSTALL_TIMESTAMP_MILLIS = 832393L;
-    private static final int RSSI = 9;
-    private static final String TITLE = "TITLE";
-    private static final String TRIGGER_ID = "TRIGGER_ID";
-    private static final int TX_POWER = 63;
-
-    @Mock ProxyFastPairDataProvider mProxyFastPairDataProvider;
-
-    FastPairDataProvider mFastPairDataProvider;
-    FastPairEligibleAccountParcel[] mFastPairEligibleAccountParcels =
-            { genHappyPathFastPairEligibleAccountParcel() };
-    FastPairAntispoofKeyDeviceMetadataParcel mFastPairAntispoofKeyDeviceMetadataParcel =
-            genHappyPathFastPairAntispoofKeyDeviceMetadataParcel();
-    FastPairUploadInfo mFastPairUploadInfo = genHappyPathFastPairUploadInfo();
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        mFastPairDataProvider = FastPairDataProvider.init(context);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testFailurePath_throwsException() throws IllegalStateException {
-        mFastPairDataProvider = FastPairDataProvider.getInstance();
-        assertThrows(
-                IllegalStateException.class,
-                () -> {
-                    mFastPairDataProvider.loadFastPairEligibleAccounts(); });
-        assertThrows(
-                IllegalStateException.class,
-                () -> {
-                    mFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(MODEL_ID); });
-        assertThrows(
-                IllegalStateException.class,
-                () -> {
-                    mFastPairDataProvider.loadFastPairDeviceWithAccountKey(ACCOUNT); });
-        assertThrows(
-                IllegalStateException.class,
-                () -> {
-                    mFastPairDataProvider.loadFastPairDeviceWithAccountKey(
-                            ACCOUNT, ImmutableList.of()); });
-        assertThrows(
-                IllegalStateException.class,
-                () -> {
-                    mFastPairDataProvider.optIn(ACCOUNT); });
-        assertThrows(
-                IllegalStateException.class,
-                () -> {
-                    mFastPairDataProvider.upload(ACCOUNT, mFastPairUploadInfo); });
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testLoadFastPairAntispoofKeyDeviceMetadata_receivesResponse()  {
-        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
-        when(mProxyFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(any()))
-                .thenReturn(mFastPairAntispoofKeyDeviceMetadataParcel);
-
-        mFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(MODEL_ID);
-        ArgumentCaptor<FastPairAntispoofKeyDeviceMetadataRequestParcel> captor =
-                ArgumentCaptor.forClass(FastPairAntispoofKeyDeviceMetadataRequestParcel.class);
-        verify(mProxyFastPairDataProvider).loadFastPairAntispoofKeyDeviceMetadata(captor.capture());
-        assertThat(captor.getValue().modelId).isSameInstanceAs(MODEL_ID);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testOptIn_finishesSuccessfully()  {
-        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
-        doNothing().when(mProxyFastPairDataProvider).manageFastPairAccount(any());
-        mFastPairDataProvider.optIn(ACCOUNT);
-        ArgumentCaptor<FastPairManageAccountRequestParcel> captor =
-                ArgumentCaptor.forClass(FastPairManageAccountRequestParcel.class);
-        verify(mProxyFastPairDataProvider).manageFastPairAccount(captor.capture());
-        assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
-        assertThat(captor.getValue().requestType).isEqualTo(
-                FastPairDataProviderService.MANAGE_REQUEST_ADD);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testUpload_finishesSuccessfully()  {
-        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
-        doNothing().when(mProxyFastPairDataProvider).manageFastPairAccountDevice(any());
-        mFastPairDataProvider.upload(ACCOUNT, mFastPairUploadInfo);
-        ArgumentCaptor<FastPairManageAccountDeviceRequestParcel> captor =
-                ArgumentCaptor.forClass(FastPairManageAccountDeviceRequestParcel.class);
-        verify(mProxyFastPairDataProvider).manageFastPairAccountDevice(captor.capture());
-        assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
-        assertThat(captor.getValue().requestType).isEqualTo(
-                FastPairDataProviderService.MANAGE_REQUEST_ADD);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testLoadFastPairEligibleAccounts_receivesOneAccount()  {
-        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
-        when(mProxyFastPairDataProvider.loadFastPairEligibleAccounts(any()))
-                .thenReturn(mFastPairEligibleAccountParcels);
-        assertThat(mFastPairDataProvider.loadFastPairEligibleAccounts().size())
-                .isEqualTo(1);
-        ArgumentCaptor<FastPairEligibleAccountsRequestParcel> captor =
-                ArgumentCaptor.forClass(FastPairEligibleAccountsRequestParcel.class);
-        verify(mProxyFastPairDataProvider).loadFastPairEligibleAccounts(captor.capture());
-        assertThat(captor.getValue()).isNotNull();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testLoadFastPairDeviceWithAccountKey_finishesSuccessfully()  {
-        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
-        when(mProxyFastPairDataProvider.loadFastPairAccountDevicesMetadata(any()))
-                .thenReturn(null);
-
-        mFastPairDataProvider.loadFastPairDeviceWithAccountKey(ACCOUNT);
-        ArgumentCaptor<FastPairAccountDevicesMetadataRequestParcel> captor =
-                ArgumentCaptor.forClass(FastPairAccountDevicesMetadataRequestParcel.class);
-        verify(mProxyFastPairDataProvider).loadFastPairAccountDevicesMetadata(captor.capture());
-        assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
-        assertThat(captor.getValue().deviceAccountKeys).isEmpty();
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testLoadFastPairDeviceWithAccountKeyDeviceAccountKeys_finishesSuccessfully()  {
-        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
-        when(mProxyFastPairDataProvider.loadFastPairAccountDevicesMetadata(any()))
-                .thenReturn(null);
-
-        mFastPairDataProvider.loadFastPairDeviceWithAccountKey(
-                ACCOUNT, ImmutableList.of(ACCOUNT_KEY));
-        ArgumentCaptor<FastPairAccountDevicesMetadataRequestParcel> captor =
-                ArgumentCaptor.forClass(FastPairAccountDevicesMetadataRequestParcel.class);
-        verify(mProxyFastPairDataProvider).loadFastPairAccountDevicesMetadata(captor.capture());
-        assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
-        assertThat(captor.getValue().deviceAccountKeys.length).isEqualTo(1);
-        assertThat(captor.getValue().deviceAccountKeys[0].byteArray).isSameInstanceAs(ACCOUNT_KEY);
-    }
-
-    private static FastPairEligibleAccountParcel genHappyPathFastPairEligibleAccountParcel() {
-        FastPairEligibleAccountParcel parcel = new FastPairEligibleAccountParcel();
-        parcel.account = ACCOUNT;
-        parcel.optIn = true;
-
-        return parcel;
-    }
-
-    private static FastPairAntispoofKeyDeviceMetadataParcel
-                genHappyPathFastPairAntispoofKeyDeviceMetadataParcel() {
-        FastPairAntispoofKeyDeviceMetadataParcel parcel =
-                new FastPairAntispoofKeyDeviceMetadataParcel();
-        parcel.antispoofPublicKey = ANTI_SPOOFING_KEY;
-        parcel.deviceMetadata = genHappyPathFastPairDeviceMetadataParcel();
-
-        return parcel;
-    }
-
-    private static FastPairDeviceMetadataParcel genHappyPathFastPairDeviceMetadataParcel() {
-        FastPairDeviceMetadataParcel parcel = new FastPairDeviceMetadataParcel();
-
-        parcel.bleTxPower = BLE_TX_POWER;
-        parcel.connectSuccessCompanionAppInstalled = CONNECT_SUCCESS_COMPANION_APP_INSTALLED;
-        parcel.connectSuccessCompanionAppNotInstalled =
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED;
-        parcel.deviceType = DEVICE_TYPE;
-        parcel.downloadCompanionAppDescription = DOWNLOAD_COMPANION_APP_DESCRIPTION;
-        parcel.failConnectGoToSettingsDescription = FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION;
-        parcel.image = IMAGE;
-        parcel.imageUrl = IMAGE_URL;
-        parcel.initialNotificationDescription = INITIAL_NOTIFICATION_DESCRIPTION;
-        parcel.initialNotificationDescriptionNoAccount =
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT;
-        parcel.initialPairingDescription = INITIAL_PAIRING_DESCRIPTION;
-        parcel.intentUri = INTENT_URI;
-        parcel.name = NAME;
-        parcel.openCompanionAppDescription = OPEN_COMPANION_APP_DESCRIPTION;
-        parcel.retroactivePairingDescription = RETRO_ACTIVE_PAIRING_DESCRIPTION;
-        parcel.subsequentPairingDescription = SUBSEQUENT_PAIRING_DESCRIPTION;
-        parcel.triggerDistance = TRIGGER_DISTANCE;
-        parcel.trueWirelessImageUrlCase = TRUE_WIRELESS_IMAGE_URL_CASE;
-        parcel.trueWirelessImageUrlLeftBud = TRUE_WIRELESS_IMAGE_URL_LEFT_BUD;
-        parcel.trueWirelessImageUrlRightBud = TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD;
-        parcel.unableToConnectDescription = UNABLE_TO_CONNECT_DESCRIPTION;
-        parcel.unableToConnectTitle = UNABLE_TO_CONNECT_TITLE;
-        parcel.updateCompanionAppDescription = UPDATE_COMPANION_APP_DESCRIPTION;
-        parcel.waitLaunchCompanionAppDescription = WAIT_LAUNCH_COMPANION_APP_DESCRIPTION;
-
-        return parcel;
-    }
-
-    private static Cache.StoredDiscoveryItem genHappyPathStoredDiscoveryItem() {
-        Cache.StoredDiscoveryItem.Builder storedDiscoveryItemBuilder =
-                Cache.StoredDiscoveryItem.newBuilder();
-        storedDiscoveryItemBuilder.setActionUrl(ACTION_URL);
-        storedDiscoveryItemBuilder.setActionUrlType(Cache.ResolvedUrlType.WEBPAGE);
-        storedDiscoveryItemBuilder.setAppName(APP_NAME);
-        storedDiscoveryItemBuilder.setAuthenticationPublicKeySecp256R1(
-                ByteString.copyFrom(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1));
-        storedDiscoveryItemBuilder.setDescription(DESCRIPTION);
-        storedDiscoveryItemBuilder.setDeviceName(DEVICE_NAME);
-        storedDiscoveryItemBuilder.setDisplayUrl(DISPLAY_URL);
-        storedDiscoveryItemBuilder.setFirstObservationTimestampMillis(
-                FIRST_OBSERVATION_TIMESTAMP_MILLIS);
-        storedDiscoveryItemBuilder.setIconFifeUrl(ICON_FIFE_URL);
-        storedDiscoveryItemBuilder.setIconPng(ByteString.copyFrom(ICON_PNG));
-        storedDiscoveryItemBuilder.setId(ID);
-        storedDiscoveryItemBuilder.setLastObservationTimestampMillis(
-                LAST_OBSERVATION_TIMESTAMP_MILLIS);
-        storedDiscoveryItemBuilder.setMacAddress(MAC_ADDRESS);
-        storedDiscoveryItemBuilder.setPackageName(PACKAGE_NAME);
-        storedDiscoveryItemBuilder.setPendingAppInstallTimestampMillis(
-                PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
-        storedDiscoveryItemBuilder.setRssi(RSSI);
-        storedDiscoveryItemBuilder.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
-        storedDiscoveryItemBuilder.setTitle(TITLE);
-        storedDiscoveryItemBuilder.setTriggerId(TRIGGER_ID);
-        storedDiscoveryItemBuilder.setTxPower(TX_POWER);
-
-        FastPairString.FastPairStrings.Builder stringsBuilder =
-                FastPairString.FastPairStrings.newBuilder();
-        stringsBuilder.setPairingFinishedCompanionAppInstalled(
-                CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        stringsBuilder.setPairingFinishedCompanionAppNotInstalled(
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-        stringsBuilder.setPairingFailDescription(
-                FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-        stringsBuilder.setTapToPairWithAccount(
-                INITIAL_NOTIFICATION_DESCRIPTION);
-        stringsBuilder.setTapToPairWithoutAccount(
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        stringsBuilder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
-        stringsBuilder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
-        stringsBuilder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
-        stringsBuilder.setWaitAppLaunchDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-        storedDiscoveryItemBuilder.setFastPairStrings(stringsBuilder.build());
-
-        Cache.FastPairInformation.Builder fpInformationBuilder =
-                Cache.FastPairInformation.newBuilder();
-        Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
-                Rpcs.TrueWirelessHeadsetImages.newBuilder();
-        imagesBuilder.setCaseUrl(TRUE_WIRELESS_IMAGE_URL_CASE);
-        imagesBuilder.setLeftBudUrl(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        imagesBuilder.setRightBudUrl(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        fpInformationBuilder.setTrueWirelessImages(imagesBuilder.build());
-        fpInformationBuilder.setDeviceType(Rpcs.DeviceType.HEADPHONES);
-
-        storedDiscoveryItemBuilder.setFastPairInformation(fpInformationBuilder.build());
-        storedDiscoveryItemBuilder.setTxPower(TX_POWER);
-
-        storedDiscoveryItemBuilder.setIconPng(ByteString.copyFrom(ICON_PNG));
-        storedDiscoveryItemBuilder.setIconFifeUrl(ICON_FIFE_URL);
-        storedDiscoveryItemBuilder.setActionUrl(ACTION_URL);
-
-        return storedDiscoveryItemBuilder.build();
-    }
-
-    private static FastPairUploadInfo genHappyPathFastPairUploadInfo() {
-        return new FastPairUploadInfo(
-                genHappyPathStoredDiscoveryItem(),
-                ByteString.copyFrom(ACCOUNT_KEY),
-                ByteString.copyFrom(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS));
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/UtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/UtilsTest.java
deleted file mode 100644
index 35f87f1..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/UtilsTest.java
+++ /dev/null
@@ -1,648 +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.server.nearby.provider;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.accounts.Account;
-import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
-import android.nearby.aidl.FastPairDeviceMetadataParcel;
-import android.nearby.aidl.FastPairDiscoveryItemParcel;
-import android.nearby.aidl.FastPairEligibleAccountParcel;
-
-import androidx.test.filters.SdkSuppress;
-
-import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
-
-import com.google.protobuf.ByteString;
-
-import org.junit.Test;
-
-import java.util.List;
-
-import service.proto.Cache;
-import service.proto.Data;
-import service.proto.FastPairString.FastPairStrings;
-import service.proto.Rpcs;
-
-public class UtilsTest {
-
-    private static final String ASSISTANT_SETUP_HALFSHEET = "ASSISTANT_SETUP_HALFSHEET";
-    private static final String ASSISTANT_SETUP_NOTIFICATION = "ASSISTANT_SETUP_NOTIFICATION";
-    private static final int BLE_TX_POWER = 5;
-    private static final String CONFIRM_PIN_DESCRIPTION = "CONFIRM_PIN_DESCRIPTION";
-    private static final String CONFIRM_PIN_TITLE = "CONFIRM_PIN_TITLE";
-    private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
-    private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
-            "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
-    private static final int DEVICE_TYPE = 1;
-    private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
-            "DOWNLOAD_COMPANION_APP_DESCRIPTION";
-    private static final Account ELIGIBLE_ACCOUNT_1 = new Account("abc@google.com", "type1");
-    private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
-            "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
-    private static final String FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION =
-            "FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION";
-    private static final byte[] IMAGE = new byte[]{7, 9};
-    private static final String IMAGE_URL = "IMAGE_URL";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION =
-            "INITIAL_NOTIFICATION_DESCRIPTION";
-    private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
-            "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
-    private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
-    private static final String INTENT_URI = "INTENT_URI";
-    private static final String LOCALE = "LOCALE";
-    private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
-    private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
-            "RETRO_ACTIVE_PAIRING_DESCRIPTION";
-    private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
-    private static final String SYNC_CONTACT_DESCRPTION = "SYNC_CONTACT_DESCRPTION";
-    private static final String SYNC_CONTACTS_TITLE = "SYNC_CONTACTS_TITLE";
-    private static final String SYNC_SMS_DESCRIPTION = "SYNC_SMS_DESCRIPTION";
-    private static final String SYNC_SMS_TITLE = "SYNC_SMS_TITLE";
-    private static final float TRIGGER_DISTANCE = 111;
-    private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
-    private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
-    private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
-            "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
-    private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
-    private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
-    private static final String UPDATE_COMPANION_APP_DESCRIPTION =
-            "UPDATE_COMPANION_APP_DESCRIPTION";
-    private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
-            "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
-    private static final byte[] ACCOUNT_KEY = new byte[]{3};
-    private static final byte[] SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS = new byte[]{2, 8};
-    private static final byte[] ANTI_SPOOFING_KEY = new byte[]{4, 5, 6};
-    private static final String ACTION_URL = "ACTION_URL";
-    private static final int ACTION_URL_TYPE = 1;
-    private static final String APP_NAME = "APP_NAME";
-    private static final int ATTACHMENT_TYPE = 1;
-    private static final byte[] AUTHENTICATION_PUBLIC_KEY_SEC_P256R1 = new byte[]{5, 7};
-    private static final byte[] BLE_RECORD_BYTES = new byte[]{2, 4};
-    private static final int DEBUG_CATEGORY = 1;
-    private static final String DEBUG_MESSAGE = "DEBUG_MESSAGE";
-    private static final String DESCRIPTION = "DESCRIPTION";
-    private static final String DEVICE_NAME = "DEVICE_NAME";
-    private static final String DISPLAY_URL = "DISPLAY_URL";
-    private static final String ENTITY_ID = "ENTITY_ID";
-    private static final String FEATURE_GRAPHIC_URL = "FEATURE_GRAPHIC_URL";
-    private static final long FIRST_OBSERVATION_TIMESTAMP_MILLIS = 8393L;
-    private static final String GROUP_ID = "GROUP_ID";
-    private static final String ICON_FIFE_URL = "ICON_FIFE_URL";
-    private static final byte[] ICON_PNG = new byte[]{2, 5};
-    private static final String ID = "ID";
-    private static final long LAST_OBSERVATION_TIMESTAMP_MILLIS = 934234L;
-    private static final String MAC_ADDRESS = "MAC_ADDRESS";
-    private static final String NAME = "NAME";
-    private static final String PACKAGE_NAME = "PACKAGE_NAME";
-    private static final long PENDING_APP_INSTALL_TIMESTAMP_MILLIS = 832393L;
-    private static final int RSSI = 9;
-    private static final int STATE = 1;
-    private static final String TITLE = "TITLE";
-    private static final String TRIGGER_ID = "TRIGGER_ID";
-    private static final int TX_POWER = 63;
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathConvertToFastPairDevicesWithAccountKey() {
-        FastPairAccountKeyDeviceMetadataParcel[] array = {
-                genHappyPathFastPairAccountkeyDeviceMetadataParcel()};
-
-        List<Data.FastPairDeviceWithAccountKey> deviceWithAccountKey =
-                Utils.convertToFastPairDevicesWithAccountKey(array);
-        assertThat(deviceWithAccountKey.size()).isEqualTo(1);
-        assertThat(deviceWithAccountKey.get(0)).isEqualTo(
-                genHappyPathFastPairDeviceWithAccountKey());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToFastPairDevicesWithAccountKeyWithNullArray() {
-        FastPairAccountKeyDeviceMetadataParcel[] array = null;
-        assertThat(Utils.convertToFastPairDevicesWithAccountKey(array).size()).isEqualTo(0);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToFastPairDevicesWithAccountKeyWithNullElement() {
-        FastPairAccountKeyDeviceMetadataParcel[] array = {null};
-        assertThat(Utils.convertToFastPairDevicesWithAccountKey(array).size()).isEqualTo(0);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToFastPairDevicesWithAccountKeyWithEmptyElementNoCrash() {
-        FastPairAccountKeyDeviceMetadataParcel[] array = {
-                genEmptyFastPairAccountkeyDeviceMetadataParcel()};
-
-        List<Data.FastPairDeviceWithAccountKey> deviceWithAccountKey =
-                Utils.convertToFastPairDevicesWithAccountKey(array);
-        assertThat(deviceWithAccountKey.size()).isEqualTo(1);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToFastPairDevicesWithAccountKeyWithEmptyMetadataDiscoveryNoCrash() {
-        FastPairAccountKeyDeviceMetadataParcel[] array = {
-                genFastPairAccountkeyDeviceMetadataParcelWithEmptyMetadataDiscoveryItem()};
-
-        List<Data.FastPairDeviceWithAccountKey> deviceWithAccountKey =
-                Utils.convertToFastPairDevicesWithAccountKey(array);
-        assertThat(deviceWithAccountKey.size()).isEqualTo(1);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToFastPairDevicesWithAccountKeyWithMixedArrayElements() {
-        FastPairAccountKeyDeviceMetadataParcel[] array = {
-                null,
-                genHappyPathFastPairAccountkeyDeviceMetadataParcel(),
-                genEmptyFastPairAccountkeyDeviceMetadataParcel(),
-                genFastPairAccountkeyDeviceMetadataParcelWithEmptyMetadataDiscoveryItem()};
-
-        assertThat(Utils.convertToFastPairDevicesWithAccountKey(array).size()).isEqualTo(3);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathConvertToAccountList() {
-        FastPairEligibleAccountParcel[] array = {genHappyPathFastPairEligibleAccountParcel()};
-
-        List<Account> accountList = Utils.convertToAccountList(array);
-        assertThat(accountList.size()).isEqualTo(1);
-        assertThat(accountList.get(0)).isEqualTo(ELIGIBLE_ACCOUNT_1);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToAccountListNullArray() {
-        FastPairEligibleAccountParcel[] array = null;
-
-        List<Account> accountList = Utils.convertToAccountList(array);
-        assertThat(accountList.size()).isEqualTo(0);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToAccountListWithNullElement() {
-        FastPairEligibleAccountParcel[] array = {null};
-
-        List<Account> accountList = Utils.convertToAccountList(array);
-        assertThat(accountList.size()).isEqualTo(0);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToAccountListWithEmptyElementNotCrash() {
-        FastPairEligibleAccountParcel[] array =
-                {genEmptyFastPairEligibleAccountParcel()};
-
-        List<Account> accountList = Utils.convertToAccountList(array);
-        assertThat(accountList.size()).isEqualTo(0);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToAccountListWithMixedArrayElements() {
-        FastPairEligibleAccountParcel[] array = {
-                genHappyPathFastPairEligibleAccountParcel(),
-                genEmptyFastPairEligibleAccountParcel(),
-                null,
-                genHappyPathFastPairEligibleAccountParcel()};
-
-        List<Account> accountList = Utils.convertToAccountList(array);
-        assertThat(accountList.size()).isEqualTo(2);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathConvertToGetObservedDeviceResponse() {
-        Rpcs.GetObservedDeviceResponse response =
-                Utils.convertToGetObservedDeviceResponse(
-                        genHappyPathFastPairAntispoofKeyDeviceMetadataParcel());
-        assertThat(response).isEqualTo(genHappyPathObservedDeviceResponse());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToGetObservedDeviceResponseWithNullInput() {
-        assertThat(Utils.convertToGetObservedDeviceResponse(null))
-                .isEqualTo(null);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToGetObservedDeviceResponseWithEmptyInputNotCrash() {
-        Utils.convertToGetObservedDeviceResponse(
-                genEmptyFastPairAntispoofKeyDeviceMetadataParcel());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToGetObservedDeviceResponseWithEmptyDeviceMetadataNotCrash() {
-        Utils.convertToGetObservedDeviceResponse(
-                genFastPairAntispoofKeyDeviceMetadataParcelWithEmptyDeviceMetadata());
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testHappyPathConvertToFastPairAccountKeyDeviceMetadata() {
-        FastPairAccountKeyDeviceMetadataParcel metadataParcel =
-                Utils.convertToFastPairAccountKeyDeviceMetadata(genHappyPathFastPairUploadInfo());
-        ensureHappyPathAsExpected(metadataParcel);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToFastPairAccountKeyDeviceMetadataWithNullInput() {
-        assertThat(Utils.convertToFastPairAccountKeyDeviceMetadata(null)).isEqualTo(null);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testConvertToFastPairAccountKeyDeviceMetadataWithEmptyFieldsNotCrash() {
-        Utils.convertToFastPairAccountKeyDeviceMetadata(
-                new FastPairUploadInfo(
-                        null /* discoveryItem */,
-                        null /* accountKey */,
-                        null /* sha256AccountKeyPublicAddress */));
-    }
-
-    private static FastPairUploadInfo genHappyPathFastPairUploadInfo() {
-        return new FastPairUploadInfo(
-                genHappyPathStoredDiscoveryItem(),
-                ByteString.copyFrom(ACCOUNT_KEY),
-                ByteString.copyFrom(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS));
-
-    }
-
-    private static void ensureHappyPathAsExpected(
-            FastPairAccountKeyDeviceMetadataParcel metadataParcel) {
-        assertThat(metadataParcel).isNotNull();
-        assertThat(metadataParcel.deviceAccountKey).isEqualTo(ACCOUNT_KEY);
-        assertThat(metadataParcel.sha256DeviceAccountKeyPublicAddress)
-                .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
-        ensureHappyPathAsExpected(metadataParcel.metadata);
-        ensureHappyPathAsExpected(metadataParcel.discoveryItem);
-    }
-
-    private static void ensureHappyPathAsExpected(FastPairDeviceMetadataParcel metadataParcel) {
-        assertThat(metadataParcel).isNotNull();
-
-        assertThat(metadataParcel.connectSuccessCompanionAppInstalled).isEqualTo(
-                CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        assertThat(metadataParcel.connectSuccessCompanionAppNotInstalled).isEqualTo(
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-
-        assertThat(metadataParcel.deviceType).isEqualTo(DEVICE_TYPE);
-
-        assertThat(metadataParcel.failConnectGoToSettingsDescription).isEqualTo(
-                FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-
-        assertThat(metadataParcel.initialNotificationDescription).isEqualTo(
-                INITIAL_NOTIFICATION_DESCRIPTION);
-        assertThat(metadataParcel.initialNotificationDescriptionNoAccount).isEqualTo(
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        assertThat(metadataParcel.initialPairingDescription).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
-
-
-        assertThat(metadataParcel.retroactivePairingDescription).isEqualTo(
-                RETRO_ACTIVE_PAIRING_DESCRIPTION);
-
-        assertThat(metadataParcel.subsequentPairingDescription).isEqualTo(
-                SUBSEQUENT_PAIRING_DESCRIPTION);
-
-        assertThat(metadataParcel.trueWirelessImageUrlCase).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
-        assertThat(metadataParcel.trueWirelessImageUrlLeftBud).isEqualTo(
-                TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        assertThat(metadataParcel.trueWirelessImageUrlRightBud).isEqualTo(
-                TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        assertThat(metadataParcel.waitLaunchCompanionAppDescription).isEqualTo(
-                WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-
-        /* do we need upload this? */
-        // assertThat(metadataParcel.locale).isEqualTo(LOCALE);
-        // assertThat(metadataParcel.name).isEqualTo(NAME);
-        // assertThat(metadataParcel.downloadCompanionAppDescription).isEqualTo(
-        //        DOWNLOAD_COMPANION_APP_DESCRIPTION);
-        // assertThat(metadataParcel.openCompanionAppDescription).isEqualTo(
-        //        OPEN_COMPANION_APP_DESCRIPTION);
-        // assertThat(metadataParcel.triggerDistance).isWithin(DELTA).of(TRIGGER_DISTANCE);
-        // assertThat(metadataParcel.unableToConnectDescription).isEqualTo(
-        //        UNABLE_TO_CONNECT_DESCRIPTION);
-        // assertThat(metadataParcel.unableToConnectTitle).isEqualTo(UNABLE_TO_CONNECT_TITLE);
-        // assertThat(metadataParcel.updateCompanionAppDescription).isEqualTo(
-        //        UPDATE_COMPANION_APP_DESCRIPTION);
-
-        // assertThat(metadataParcel.bleTxPower).isEqualTo(BLE_TX_POWER);
-        // assertThat(metadataParcel.image).isEqualTo(IMAGE);
-        // assertThat(metadataParcel.imageUrl).isEqualTo(IMAGE_URL);
-        // assertThat(metadataParcel.intentUri).isEqualTo(INTENT_URI);
-    }
-
-    private static void ensureHappyPathAsExpected(FastPairDiscoveryItemParcel itemParcel) {
-        assertThat(itemParcel.actionUrl).isEqualTo(ACTION_URL);
-        assertThat(itemParcel.actionUrlType).isEqualTo(ACTION_URL_TYPE);
-        assertThat(itemParcel.appName).isEqualTo(APP_NAME);
-        assertThat(itemParcel.authenticationPublicKeySecp256r1)
-                .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
-        assertThat(itemParcel.description).isEqualTo(DESCRIPTION);
-        assertThat(itemParcel.deviceName).isEqualTo(DEVICE_NAME);
-        assertThat(itemParcel.displayUrl).isEqualTo(DISPLAY_URL);
-        assertThat(itemParcel.firstObservationTimestampMillis)
-                .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
-        assertThat(itemParcel.iconFifeUrl).isEqualTo(ICON_FIFE_URL);
-        assertThat(itemParcel.iconPng).isEqualTo(ICON_PNG);
-        assertThat(itemParcel.id).isEqualTo(ID);
-        assertThat(itemParcel.lastObservationTimestampMillis)
-                .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS);
-        assertThat(itemParcel.macAddress).isEqualTo(MAC_ADDRESS);
-        assertThat(itemParcel.packageName).isEqualTo(PACKAGE_NAME);
-        assertThat(itemParcel.pendingAppInstallTimestampMillis)
-                .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
-        assertThat(itemParcel.rssi).isEqualTo(RSSI);
-        assertThat(itemParcel.state).isEqualTo(STATE);
-        assertThat(itemParcel.title).isEqualTo(TITLE);
-        assertThat(itemParcel.triggerId).isEqualTo(TRIGGER_ID);
-        assertThat(itemParcel.txPower).isEqualTo(TX_POWER);
-    }
-
-    private static FastPairEligibleAccountParcel genHappyPathFastPairEligibleAccountParcel() {
-        FastPairEligibleAccountParcel parcel = new FastPairEligibleAccountParcel();
-        parcel.account = ELIGIBLE_ACCOUNT_1;
-        parcel.optIn = true;
-
-        return parcel;
-    }
-
-    private static FastPairEligibleAccountParcel genEmptyFastPairEligibleAccountParcel() {
-        return new FastPairEligibleAccountParcel();
-    }
-
-    private static FastPairDeviceMetadataParcel genEmptyFastPairDeviceMetadataParcel() {
-        return new FastPairDeviceMetadataParcel();
-    }
-
-    private static FastPairDiscoveryItemParcel genEmptyFastPairDiscoveryItemParcel() {
-        return new FastPairDiscoveryItemParcel();
-    }
-
-    private static FastPairAccountKeyDeviceMetadataParcel
-            genEmptyFastPairAccountkeyDeviceMetadataParcel() {
-        return new FastPairAccountKeyDeviceMetadataParcel();
-    }
-
-    private static FastPairAccountKeyDeviceMetadataParcel
-            genFastPairAccountkeyDeviceMetadataParcelWithEmptyMetadataDiscoveryItem() {
-        FastPairAccountKeyDeviceMetadataParcel parcel =
-                new FastPairAccountKeyDeviceMetadataParcel();
-        parcel.metadata = genEmptyFastPairDeviceMetadataParcel();
-        parcel.discoveryItem = genEmptyFastPairDiscoveryItemParcel();
-
-        return parcel;
-    }
-
-    private static FastPairAccountKeyDeviceMetadataParcel
-            genHappyPathFastPairAccountkeyDeviceMetadataParcel() {
-        FastPairAccountKeyDeviceMetadataParcel parcel =
-                new FastPairAccountKeyDeviceMetadataParcel();
-        parcel.deviceAccountKey = ACCOUNT_KEY;
-        parcel.metadata = genHappyPathFastPairDeviceMetadataParcel();
-        parcel.sha256DeviceAccountKeyPublicAddress = SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS;
-        parcel.discoveryItem = genHappyPathFastPairDiscoveryItemParcel();
-
-        return parcel;
-    }
-
-    private static FastPairDeviceMetadataParcel genHappyPathFastPairDeviceMetadataParcel() {
-        FastPairDeviceMetadataParcel parcel = new FastPairDeviceMetadataParcel();
-
-        parcel.bleTxPower = BLE_TX_POWER;
-        parcel.connectSuccessCompanionAppInstalled = CONNECT_SUCCESS_COMPANION_APP_INSTALLED;
-        parcel.connectSuccessCompanionAppNotInstalled =
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED;
-        parcel.deviceType = DEVICE_TYPE;
-        parcel.downloadCompanionAppDescription = DOWNLOAD_COMPANION_APP_DESCRIPTION;
-        parcel.failConnectGoToSettingsDescription = FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION;
-        parcel.image = IMAGE;
-        parcel.imageUrl = IMAGE_URL;
-        parcel.initialNotificationDescription = INITIAL_NOTIFICATION_DESCRIPTION;
-        parcel.initialNotificationDescriptionNoAccount =
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT;
-        parcel.initialPairingDescription = INITIAL_PAIRING_DESCRIPTION;
-        parcel.intentUri = INTENT_URI;
-        parcel.name = NAME;
-        parcel.openCompanionAppDescription = OPEN_COMPANION_APP_DESCRIPTION;
-        parcel.retroactivePairingDescription = RETRO_ACTIVE_PAIRING_DESCRIPTION;
-        parcel.subsequentPairingDescription = SUBSEQUENT_PAIRING_DESCRIPTION;
-        parcel.triggerDistance = TRIGGER_DISTANCE;
-        parcel.trueWirelessImageUrlCase = TRUE_WIRELESS_IMAGE_URL_CASE;
-        parcel.trueWirelessImageUrlLeftBud = TRUE_WIRELESS_IMAGE_URL_LEFT_BUD;
-        parcel.trueWirelessImageUrlRightBud = TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD;
-        parcel.unableToConnectDescription = UNABLE_TO_CONNECT_DESCRIPTION;
-        parcel.unableToConnectTitle = UNABLE_TO_CONNECT_TITLE;
-        parcel.updateCompanionAppDescription = UPDATE_COMPANION_APP_DESCRIPTION;
-        parcel.waitLaunchCompanionAppDescription = WAIT_LAUNCH_COMPANION_APP_DESCRIPTION;
-
-        return parcel;
-    }
-
-    private static Cache.StoredDiscoveryItem genHappyPathStoredDiscoveryItem() {
-        Cache.StoredDiscoveryItem.Builder storedDiscoveryItemBuilder =
-                Cache.StoredDiscoveryItem.newBuilder();
-        storedDiscoveryItemBuilder.setActionUrl(ACTION_URL);
-        storedDiscoveryItemBuilder.setActionUrlType(Cache.ResolvedUrlType.WEBPAGE);
-        storedDiscoveryItemBuilder.setAppName(APP_NAME);
-        storedDiscoveryItemBuilder.setAuthenticationPublicKeySecp256R1(
-                ByteString.copyFrom(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1));
-        storedDiscoveryItemBuilder.setDescription(DESCRIPTION);
-        storedDiscoveryItemBuilder.setDeviceName(DEVICE_NAME);
-        storedDiscoveryItemBuilder.setDisplayUrl(DISPLAY_URL);
-        storedDiscoveryItemBuilder.setFirstObservationTimestampMillis(
-                FIRST_OBSERVATION_TIMESTAMP_MILLIS);
-        storedDiscoveryItemBuilder.setIconFifeUrl(ICON_FIFE_URL);
-        storedDiscoveryItemBuilder.setIconPng(ByteString.copyFrom(ICON_PNG));
-        storedDiscoveryItemBuilder.setId(ID);
-        storedDiscoveryItemBuilder.setLastObservationTimestampMillis(
-                LAST_OBSERVATION_TIMESTAMP_MILLIS);
-        storedDiscoveryItemBuilder.setMacAddress(MAC_ADDRESS);
-        storedDiscoveryItemBuilder.setPackageName(PACKAGE_NAME);
-        storedDiscoveryItemBuilder.setPendingAppInstallTimestampMillis(
-                PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
-        storedDiscoveryItemBuilder.setRssi(RSSI);
-        storedDiscoveryItemBuilder.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
-        storedDiscoveryItemBuilder.setTitle(TITLE);
-        storedDiscoveryItemBuilder.setTriggerId(TRIGGER_ID);
-        storedDiscoveryItemBuilder.setTxPower(TX_POWER);
-
-        FastPairStrings.Builder stringsBuilder = FastPairStrings.newBuilder();
-        stringsBuilder.setPairingFinishedCompanionAppInstalled(
-                CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
-        stringsBuilder.setPairingFinishedCompanionAppNotInstalled(
-                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
-        stringsBuilder.setPairingFailDescription(
-                FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
-        stringsBuilder.setTapToPairWithAccount(
-                INITIAL_NOTIFICATION_DESCRIPTION);
-        stringsBuilder.setTapToPairWithoutAccount(
-                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
-        stringsBuilder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
-        stringsBuilder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
-        stringsBuilder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
-        stringsBuilder.setWaitAppLaunchDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-        storedDiscoveryItemBuilder.setFastPairStrings(stringsBuilder.build());
-
-        Cache.FastPairInformation.Builder fpInformationBuilder =
-                Cache.FastPairInformation.newBuilder();
-        Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
-                Rpcs.TrueWirelessHeadsetImages.newBuilder();
-        imagesBuilder.setCaseUrl(TRUE_WIRELESS_IMAGE_URL_CASE);
-        imagesBuilder.setLeftBudUrl(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        imagesBuilder.setRightBudUrl(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        fpInformationBuilder.setTrueWirelessImages(imagesBuilder.build());
-        fpInformationBuilder.setDeviceType(Rpcs.DeviceType.HEADPHONES);
-
-        storedDiscoveryItemBuilder.setFastPairInformation(fpInformationBuilder.build());
-        storedDiscoveryItemBuilder.setTxPower(TX_POWER);
-
-        storedDiscoveryItemBuilder.setIconPng(ByteString.copyFrom(ICON_PNG));
-        storedDiscoveryItemBuilder.setIconFifeUrl(ICON_FIFE_URL);
-        storedDiscoveryItemBuilder.setActionUrl(ACTION_URL);
-
-        return storedDiscoveryItemBuilder.build();
-    }
-
-    private static Data.FastPairDeviceWithAccountKey genHappyPathFastPairDeviceWithAccountKey() {
-        Data.FastPairDeviceWithAccountKey.Builder fpDeviceBuilder =
-                Data.FastPairDeviceWithAccountKey.newBuilder();
-        fpDeviceBuilder.setAccountKey(ByteString.copyFrom(ACCOUNT_KEY));
-        fpDeviceBuilder.setSha256AccountKeyPublicAddress(
-                ByteString.copyFrom(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS));
-        fpDeviceBuilder.setDiscoveryItem(genHappyPathStoredDiscoveryItem());
-
-        return fpDeviceBuilder.build();
-    }
-
-    private static FastPairDiscoveryItemParcel genHappyPathFastPairDiscoveryItemParcel() {
-        FastPairDiscoveryItemParcel parcel = new FastPairDiscoveryItemParcel();
-        parcel.actionUrl = ACTION_URL;
-        parcel.actionUrlType = ACTION_URL_TYPE;
-        parcel.appName = APP_NAME;
-        parcel.authenticationPublicKeySecp256r1 = AUTHENTICATION_PUBLIC_KEY_SEC_P256R1;
-        parcel.description = DESCRIPTION;
-        parcel.deviceName = DEVICE_NAME;
-        parcel.displayUrl = DISPLAY_URL;
-        parcel.firstObservationTimestampMillis = FIRST_OBSERVATION_TIMESTAMP_MILLIS;
-        parcel.iconFifeUrl = ICON_FIFE_URL;
-        parcel.iconPng = ICON_PNG;
-        parcel.id = ID;
-        parcel.lastObservationTimestampMillis = LAST_OBSERVATION_TIMESTAMP_MILLIS;
-        parcel.macAddress = MAC_ADDRESS;
-        parcel.packageName = PACKAGE_NAME;
-        parcel.pendingAppInstallTimestampMillis = PENDING_APP_INSTALL_TIMESTAMP_MILLIS;
-        parcel.rssi = RSSI;
-        parcel.state = STATE;
-        parcel.title = TITLE;
-        parcel.triggerId = TRIGGER_ID;
-        parcel.txPower = TX_POWER;
-
-        return parcel;
-    }
-
-    private static Rpcs.GetObservedDeviceResponse genHappyPathObservedDeviceResponse() {
-        Rpcs.Device.Builder deviceBuilder = Rpcs.Device.newBuilder();
-        deviceBuilder.setAntiSpoofingKeyPair(Rpcs.AntiSpoofingKeyPair.newBuilder()
-                .setPublicKey(ByteString.copyFrom(ANTI_SPOOFING_KEY))
-                .build());
-        Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
-                Rpcs.TrueWirelessHeadsetImages.newBuilder();
-        imagesBuilder.setLeftBudUrl(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
-        imagesBuilder.setRightBudUrl(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
-        imagesBuilder.setCaseUrl(TRUE_WIRELESS_IMAGE_URL_CASE);
-        deviceBuilder.setTrueWirelessImages(imagesBuilder.build());
-        deviceBuilder.setImageUrl(IMAGE_URL);
-        deviceBuilder.setIntentUri(INTENT_URI);
-        deviceBuilder.setName(NAME);
-        deviceBuilder.setBleTxPower(BLE_TX_POWER);
-        deviceBuilder.setTriggerDistance(TRIGGER_DISTANCE);
-        deviceBuilder.setDeviceType(Rpcs.DeviceType.HEADPHONES);
-
-        return Rpcs.GetObservedDeviceResponse.newBuilder()
-                .setDevice(deviceBuilder.build())
-                .setImage(ByteString.copyFrom(IMAGE))
-                .setStrings(Rpcs.ObservedDeviceStrings.newBuilder()
-                        .setConnectSuccessCompanionAppInstalled(
-                                CONNECT_SUCCESS_COMPANION_APP_INSTALLED)
-                        .setConnectSuccessCompanionAppNotInstalled(
-                                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED)
-                        .setDownloadCompanionAppDescription(
-                                DOWNLOAD_COMPANION_APP_DESCRIPTION)
-                        .setFailConnectGoToSettingsDescription(
-                                FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION)
-                        .setInitialNotificationDescription(
-                                INITIAL_NOTIFICATION_DESCRIPTION)
-                        .setInitialNotificationDescriptionNoAccount(
-                                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT)
-                        .setInitialPairingDescription(
-                                INITIAL_PAIRING_DESCRIPTION)
-                        .setOpenCompanionAppDescription(
-                                OPEN_COMPANION_APP_DESCRIPTION)
-                        .setRetroactivePairingDescription(
-                                RETRO_ACTIVE_PAIRING_DESCRIPTION)
-                        .setSubsequentPairingDescription(
-                                SUBSEQUENT_PAIRING_DESCRIPTION)
-                        .setUnableToConnectDescription(
-                                UNABLE_TO_CONNECT_DESCRIPTION)
-                        .setUnableToConnectTitle(
-                                UNABLE_TO_CONNECT_TITLE)
-                        .setUpdateCompanionAppDescription(
-                                UPDATE_COMPANION_APP_DESCRIPTION)
-                        .setWaitLaunchCompanionAppDescription(
-                                WAIT_LAUNCH_COMPANION_APP_DESCRIPTION)
-                        .build())
-                .build();
-    }
-
-    private static FastPairAntispoofKeyDeviceMetadataParcel
-            genHappyPathFastPairAntispoofKeyDeviceMetadataParcel() {
-        FastPairAntispoofKeyDeviceMetadataParcel parcel =
-                new FastPairAntispoofKeyDeviceMetadataParcel();
-        parcel.antispoofPublicKey = ANTI_SPOOFING_KEY;
-        parcel.deviceMetadata = genHappyPathFastPairDeviceMetadataParcel();
-
-        return parcel;
-    }
-
-    private static FastPairAntispoofKeyDeviceMetadataParcel
-            genFastPairAntispoofKeyDeviceMetadataParcelWithEmptyDeviceMetadata() {
-        FastPairAntispoofKeyDeviceMetadataParcel parcel =
-                new FastPairAntispoofKeyDeviceMetadataParcel();
-        parcel.antispoofPublicKey = ANTI_SPOOFING_KEY;
-        parcel.deviceMetadata = genEmptyFastPairDeviceMetadataParcel();
-
-        return parcel;
-    }
-
-    private static FastPairAntispoofKeyDeviceMetadataParcel
-            genEmptyFastPairAntispoofKeyDeviceMetadataParcel() {
-        return new FastPairAntispoofKeyDeviceMetadataParcel();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
index a759baf..455c432 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -18,8 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.filters.SdkSuppress;
-
 import org.junit.Test;
 
 public final class ArrayUtilsTest {
@@ -30,51 +28,58 @@
     private static final byte[] BYTES_ALL = new byte[] {7, 9, 8};
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysNoInput() {
         assertThat(ArrayUtils.concatByteArrays().length).isEqualTo(0);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysOneEmptyArray() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_EMPTY).length).isEqualTo(0);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysOneNonEmptyArray() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_ONE)).isEqualTo(BYTES_ONE);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysMultipleNonEmptyArrays() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_TWO)).isEqualTo(BYTES_ALL);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysMultipleArrays() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_EMPTY, BYTES_TWO))
                 .isEqualTo(BYTES_ALL);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testIsEmptyNull_returnsTrue() {
         assertThat(ArrayUtils.isEmpty(null)).isTrue();
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testIsEmpty_returnsTrue() {
         assertThat(ArrayUtils.isEmpty(new byte[]{})).isTrue();
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testIsEmpty_returnsFalse() {
         assertThat(ArrayUtils.isEmpty(BYTES_ALL)).isFalse();
     }
+
+    @Test
+    public void testAppendByte() {
+        assertThat(ArrayUtils.append((byte) 2, BYTES_ONE)).isEqualTo(new byte[]{2, 7, 9});
+    }
+
+    @Test
+    public void testAppendByteNull() {
+        assertThat(ArrayUtils.append((byte) 2, null)).isEqualTo(new byte[]{2});
+    }
+
+    @Test
+    public void testAppendByteToArray() {
+        assertThat(ArrayUtils.append(BYTES_ONE, (byte) 2)).isEqualTo(new byte[]{7, 9, 2});
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
deleted file mode 100644
index ac90b9f..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
+++ /dev/null
@@ -1,180 +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.server.nearby.util;
-
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.protobuf.ByteString;
-
-import org.junit.Test;
-
-import service.proto.Cache;
-import service.proto.FastPairString.FastPairStrings;
-import service.proto.Rpcs;
-import service.proto.Rpcs.GetObservedDeviceResponse;
-
-public final class DataUtilsTest {
-    private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
-    private static final String MODEL_ID = "model_id";
-    private static final String APP_PACKAGE = "test_package";
-    private static final String APP_ACTION_URL =
-            "intent:#Intent;action=cto_be_set%3AACTION_MAGIC_PAIR;"
-                    + "package=to_be_set;"
-                    + "component=to_be_set;"
-                    + "to_be_set%3AEXTRA_COMPANION_APP="
-                    + APP_PACKAGE
-                    + ";end";
-    private static final long DEVICE_ID = 12;
-    private static final String DEVICE_NAME = "My device";
-    private static final byte[] DEVICE_PUBLIC_KEY = base16().decode("0123456789ABCDEF");
-    private static final String DEVICE_COMPANY = "Company name";
-    private static final byte[] DEVICE_IMAGE = new byte[] {0x00, 0x01, 0x10, 0x11};
-    private static final String DEVICE_IMAGE_URL = "device_image_url";
-    private static final String AUTHORITY = "com.android.test";
-    private static final String SIGNATURE_HASH = "as8dfbyu2duas7ikanvklpaclo2";
-    private static final String ACCOUNT = "test@gmail.com";
-
-    private static final String MESSAGE_INIT_NOTIFY_DESCRIPTION = "message 1";
-    private static final String MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT = "message 2";
-    private static final String MESSAGE_INIT_PAIR_DESCRIPTION = "message 3 %s";
-    private static final String MESSAGE_COMPANION_INSTALLED = "message 4";
-    private static final String MESSAGE_COMPANION_NOT_INSTALLED = "message 5";
-    private static final String MESSAGE_SUBSEQUENT_PAIR_DESCRIPTION = "message 6";
-    private static final String MESSAGE_RETROACTIVE_PAIR_DESCRIPTION = "message 7";
-    private static final String MESSAGE_WAIT_LAUNCH_COMPANION_APP_DESCRIPTION = "message 8";
-    private static final String MESSAGE_FAIL_CONNECT_DESCRIPTION = "message 9";
-
-    @Test
-    public void test_toScanFastPairStoreItem_withAccount() {
-        Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
-                createObservedDeviceResponse(), BLUETOOTH_ADDRESS, MODEL_ID, ACCOUNT);
-        assertThat(item.getAddress()).isEqualTo(BLUETOOTH_ADDRESS);
-        assertThat(item.getActionUrl()).isEqualTo(APP_ACTION_URL);
-        assertThat(item.getDeviceName()).isEqualTo(DEVICE_NAME);
-        assertThat(item.getIconPng()).isEqualTo(ByteString.copyFrom(DEVICE_IMAGE));
-        assertThat(item.getIconFifeUrl()).isEqualTo(DEVICE_IMAGE_URL);
-        assertThat(item.getAntiSpoofingPublicKey())
-                .isEqualTo(ByteString.copyFrom(DEVICE_PUBLIC_KEY));
-
-        FastPairStrings strings = item.getFastPairStrings();
-        assertThat(strings.getTapToPairWithAccount()).isEqualTo(MESSAGE_INIT_NOTIFY_DESCRIPTION);
-        assertThat(strings.getTapToPairWithoutAccount())
-                .isEqualTo(MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT);
-        assertThat(strings.getInitialPairingDescription())
-                .isEqualTo(String.format(MESSAGE_INIT_PAIR_DESCRIPTION, DEVICE_NAME));
-        assertThat(strings.getPairingFinishedCompanionAppInstalled())
-                .isEqualTo(MESSAGE_COMPANION_INSTALLED);
-        assertThat(strings.getPairingFinishedCompanionAppNotInstalled())
-                .isEqualTo(MESSAGE_COMPANION_NOT_INSTALLED);
-        assertThat(strings.getSubsequentPairingDescription())
-                .isEqualTo(MESSAGE_SUBSEQUENT_PAIR_DESCRIPTION);
-        assertThat(strings.getRetroactivePairingDescription())
-                .isEqualTo(MESSAGE_RETROACTIVE_PAIR_DESCRIPTION);
-        assertThat(strings.getWaitAppLaunchDescription())
-                .isEqualTo(MESSAGE_WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
-        assertThat(strings.getPairingFailDescription())
-                .isEqualTo(MESSAGE_FAIL_CONNECT_DESCRIPTION);
-    }
-
-    @Test
-    public void test_toScanFastPairStoreItem_withoutAccount() {
-        Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
-                createObservedDeviceResponse(), BLUETOOTH_ADDRESS, MODEL_ID, /* account= */ null);
-        FastPairStrings strings = item.getFastPairStrings();
-        assertThat(strings.getInitialPairingDescription())
-                .isEqualTo(MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT);
-    }
-
-    @Test
-    public void test_toString() {
-        Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
-                createObservedDeviceResponse(), BLUETOOTH_ADDRESS, MODEL_ID, ACCOUNT);
-
-        assertThat(DataUtils.toString(item))
-                .isEqualTo("ScanFastPairStoreItem=[address:00:11:22:33:FF:EE, "
-                        + "actionUrl:intent:#Intent;action=cto_be_set%3AACTION_MAGIC_PAIR;"
-                        + "package=to_be_set;component=to_be_set;"
-                        + "to_be_set%3AEXTRA_COMPANION_APP=test_package;"
-                        + "end, deviceName:My device, "
-                        + "iconFifeUrl:device_image_url, "
-                        + "fastPairStrings:FastPairStrings[tapToPairWithAccount=message 1, "
-                        + "tapToPairWithoutAccount=message 2, "
-                        + "initialPairingDescription=message 3 My device, "
-                        + "pairingFinishedCompanionAppInstalled=message 4, "
-                        + "pairingFinishedCompanionAppNotInstalled=message 5, "
-                        + "subsequentPairingDescription=message 6, "
-                        + "retroactivePairingDescription=message 7, "
-                        + "waitAppLaunchDescription=message 8, "
-                        + "pairingFailDescription=message 9]]");
-
-        FastPairStrings strings = item.getFastPairStrings();
-
-        assertThat(DataUtils.toString(strings))
-                .isEqualTo("FastPairStrings[tapToPairWithAccount=message 1, "
-                        + "tapToPairWithoutAccount=message 2, "
-                        + "initialPairingDescription=message 3 " + DEVICE_NAME + ", "
-                        + "pairingFinishedCompanionAppInstalled=message 4, "
-                        + "pairingFinishedCompanionAppNotInstalled=message 5, "
-                        + "subsequentPairingDescription=message 6, "
-                        + "retroactivePairingDescription=message 7, "
-                        + "waitAppLaunchDescription=message 8, "
-                        + "pairingFailDescription=message 9]");
-    }
-
-    private static GetObservedDeviceResponse createObservedDeviceResponse() {
-        return GetObservedDeviceResponse.newBuilder()
-                .setDevice(
-                        Rpcs.Device.newBuilder()
-                                .setId(DEVICE_ID)
-                                .setName(DEVICE_NAME)
-                                .setAntiSpoofingKeyPair(
-                                        Rpcs.AntiSpoofingKeyPair
-                                                .newBuilder()
-                                                .setPublicKey(
-                                                        ByteString.copyFrom(DEVICE_PUBLIC_KEY)))
-                                .setIntentUri(APP_ACTION_URL)
-                                .setDataOnlyConnection(true)
-                                .setAssistantSupported(false)
-                                .setCompanionDetail(
-                                        Rpcs.CompanionAppDetails.newBuilder()
-                                                .setAuthority(AUTHORITY)
-                                                .setCertificateHash(SIGNATURE_HASH)
-                                                .build())
-                                .setCompanyName(DEVICE_COMPANY)
-                                .setImageUrl(DEVICE_IMAGE_URL))
-                .setImage(ByteString.copyFrom(DEVICE_IMAGE))
-                .setStrings(
-                        Rpcs.ObservedDeviceStrings.newBuilder()
-                                .setInitialNotificationDescription(MESSAGE_INIT_NOTIFY_DESCRIPTION)
-                                .setInitialNotificationDescriptionNoAccount(
-                                        MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT)
-                                .setInitialPairingDescription(MESSAGE_INIT_PAIR_DESCRIPTION)
-                                .setConnectSuccessCompanionAppInstalled(MESSAGE_COMPANION_INSTALLED)
-                                .setConnectSuccessCompanionAppNotInstalled(
-                                        MESSAGE_COMPANION_NOT_INSTALLED)
-                                .setSubsequentPairingDescription(
-                                        MESSAGE_SUBSEQUENT_PAIR_DESCRIPTION)
-                                .setRetroactivePairingDescription(
-                                        MESSAGE_RETROACTIVE_PAIR_DESCRIPTION)
-                                .setWaitLaunchCompanionAppDescription(
-                                        MESSAGE_WAIT_LAUNCH_COMPANION_APP_DESCRIPTION)
-                                .setFailConnectGoToSettingsDescription(
-                                        MESSAGE_FAIL_CONNECT_DESCRIPTION))
-                .build();
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java
deleted file mode 100644
index e167cf4..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java
+++ /dev/null
@@ -1,28 +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.server.nearby.util;
-
-import org.junit.Test;
-
-public class EnvironmentTest {
-
-    @Test
-    public void getNearbyDirectory() {
-        Environment.getNearbyDirectory();
-        Environment.getNearbyDirectory(1);
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
deleted file mode 100644
index f0294fc..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
+++ /dev/null
@@ -1,76 +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.server.nearby.util.encryption;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-public class CryptorImpIdentityV1Test {
-    private static final String TAG = "CryptorImpIdentityV1Test";
-    private static final byte[] SALT = new byte[] {102, 22};
-    private static final byte[] DATA =
-            new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
-    private static final byte[] AUTHENTICITY_KEY =
-            new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
-
-    @Test
-    public void test_encrypt_decrypt() {
-        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
-        assertThat(identityCryptor.decrypt(encryptedData, SALT, AUTHENTICITY_KEY)).isEqualTo(DATA);
-    }
-
-    @Test
-    public void test_encryption() {
-        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
-        // for debugging
-        Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
-
-        assertThat(encryptedData).isEqualTo(getEncryptedData());
-    }
-
-    @Test
-    public void test_decryption() {
-        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] decryptedData =
-                identityCryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
-        // for debugging
-        Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
-
-        assertThat(decryptedData).isEqualTo(DATA);
-    }
-
-    @Test
-    public void generateHmacTag() {
-        CryptorImpIdentityV1 identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] generatedTag = identityCryptor.sign(DATA);
-        byte[] expectedTag = new byte[]{50, 116, 95, -87, 63, 123, -79, -43};
-        assertThat(generatedTag).isEqualTo(expectedTag);
-    }
-
-    private static byte[] getEncryptedData() {
-        return new byte[]{6, -31, -32, -123, 43, -92, -47, -110, -65, 126, -15, -51, -19, -43};
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
deleted file mode 100644
index 3ca2575..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
+++ /dev/null
@@ -1,114 +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.server.nearby.util.encryption;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-/**
- * Unit test for {@link CryptorImpV1}
- */
-public final class CryptorImpV1Test {
-    private static final String TAG = "CryptorImpV1Test";
-    private static final byte[] SALT = new byte[] {102, 22};
-    private static final byte[] DATA =
-            new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
-    private static final byte[] AUTHENTICITY_KEY =
-            new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
-
-    @Test
-    public void test_encryption() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        byte[] encryptedData = v1Cryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
-        // for debugging
-        Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
-
-        assertThat(encryptedData).isEqualTo(getEncryptedData());
-    }
-
-    @Test
-    public void test_encryption_invalidInput() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.encrypt(DATA, SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
-    }
-
-    @Test
-    public void test_decryption() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        byte[] decryptedData =
-                v1Cryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
-        // for debugging
-        Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
-
-        assertThat(decryptedData).isEqualTo(DATA);
-    }
-
-    @Test
-    public void test_decryption_invalidInput() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.decrypt(getEncryptedData(), SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
-    }
-
-    @Test
-    public void generateSign() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        byte[] generatedTag = v1Cryptor.sign(DATA, AUTHENTICITY_KEY);
-        byte[] expectedTag = new byte[]{
-                100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
-        assertThat(generatedTag).isEqualTo(expectedTag);
-    }
-
-    @Test
-    public void test_verify() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        byte[] expectedTag = new byte[]{
-                100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
-
-        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
-        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
-    }
-
-    @Test
-    public void test_generateHmacTag_sameResult() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
-        assertThat(res1)
-                .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
-    }
-
-    @Test
-    public void test_generateHmacTag_nullData() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
-    }
-
-    @Test
-    public void test_generateHmacTag_nullKey() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
-    }
-
-    private static byte[] getEncryptedData() {
-        return new byte[]{-92, 94, -99, -97, 81, -48, -7, 119, -64, -22, 45, -49, -50, 92};
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java
new file mode 100644
index 0000000..b6d2333
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+/**
+ * Unit test for {@link CryptorMicImp}
+ */
+public final class CryptorMicImpTest {
+    private static final String TAG = "CryptorImpV1Test";
+    private static final byte[] SALT = new byte[]{102, 22};
+    private static final byte[] DATA =
+            new byte[]{107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+    private static final byte[] AUTHENTICITY_KEY =
+            new byte[]{-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+    private static byte[] getEncryptedData() {
+        return new byte[]{112, 23, -111, 87, 122, -27, 45, -25, -35, 84, -89, 115, 61, 113};
+    }
+
+    @Test
+    public void test_encryption() throws Exception {
+        Cryptor v1Cryptor = CryptorMicImp.getInstance();
+        byte[] encryptedData =
+                v1Cryptor.encrypt(DATA,  CryptorMicImp.generateAdvNonce(SALT), AUTHENTICITY_KEY);
+        assertThat(encryptedData).isEqualTo(getEncryptedData());
+    }
+
+    @Test
+    public void test_decryption() throws Exception {
+        Cryptor v1Cryptor = CryptorMicImp.getInstance();
+        byte[] decryptedData =
+                v1Cryptor.decrypt(getEncryptedData(), CryptorMicImp.generateAdvNonce(SALT),
+                        AUTHENTICITY_KEY);
+        assertThat(decryptedData).isEqualTo(DATA);
+    }
+
+    @Test
+    public void test_verify() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        byte[] expectedTag = new byte[]{
+                -80, -51, -101, -7, -65, 110, 37, 68, 122, -128, 57, -90, -115, -59, -61, 46};
+        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
+        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
+    }
+
+    @Test
+    public void test_generateHmacTag_sameResult() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
+        assertThat(res1)
+                .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
+    }
+
+    @Test
+    public void test_generateHmacTag_nullData() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
+    }
+
+    @Test
+    public void test_generateHmacTag_nullKey() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
index ca612e3..1fb2236 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
@@ -42,7 +42,7 @@
         assertThat(res2).hasLength(outputSize);
         assertThat(res1).isNotEqualTo(res2);
         assertThat(res1)
-                .isEqualTo(CryptorImpV1.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
+                .isEqualTo(CryptorMicImp.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
     }
 
     @Test
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
new file mode 100644
index 0000000..908bb13
--- /dev/null
+++ b/netbpfload/Android.bp
@@ -0,0 +1,84 @@
+//
+// 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",
+}
+
+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",
+
+    defaults: ["bpf_defaults"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wthread-safety",
+    ],
+    sanitize: {
+        integer_overflow: true,
+    },
+
+    header_libs: ["bpf_headers"],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    srcs: [
+        "loader.cpp",
+        "NetBpfLoad.cpp",
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    // really should be Android 13/T (33), but we cannot include binaries built
+    // against newer sdk in the apex, which still targets 30(R):
+    // module "netbpfload" variant "android_x86_apex30": should support
+    // min_sdk_version(30) for "com.android.tethering": newer SDK(34).
+    min_sdk_version: "30",
+    installable: false,
+}
+
+// Versioned netbpfload init rc: init system will process it only on api T/33+ devices
+// Note: R[30] S[31] Sv2[32] T[33] U[34] V[35])
+//
+// 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.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
new file mode 100644
index 0000000..88604ab
--- /dev/null
+++ b/netbpfload/NetBpfLoad.cpp
@@ -0,0 +1,557 @@
+/*
+ * Copyright (C) 2017-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.
+ */
+
+#ifndef LOG_TAG
+#define LOG_TAG "NetBpfLoad"
+#endif
+
+#include <arpa/inet.h>
+#include <dirent.h>
+#include <elf.h>
+#include <error.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/bpf.h>
+#include <linux/unistd.h>
+#include <net/if.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <android/api-level.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 "BpfSyscallWrappers.h"
+#include "bpf/BpfUtils.h"
+#include "loader.h"
+
+namespace android {
+namespace bpf {
+
+using base::StartsWith;
+using base::EndsWith;
+using std::string;
+using std::vector;
+
+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 Location locations[] = {
+        // S+ Tethering mainline module (network_stack): tether offload
+        {
+                .dir = "/apex/com.android.tethering/etc/bpf/",
+                .prefix = "tethering/",
+        },
+        // T+ Tethering mainline module (shared with netd & system server)
+        // netutils_wrapper (for iptables xt_bpf) has access to programs
+        {
+                .dir = "/apex/com.android.tethering/etc/bpf/netd_shared/",
+                .prefix = "netd_shared/",
+        },
+        // T+ Tethering mainline module (shared with netd & system server)
+        // netutils_wrapper has no access, netd has read only access
+        {
+                .dir = "/apex/com.android.tethering/etc/bpf/netd_readonly/",
+                .prefix = "netd_readonly/",
+        },
+        // T+ Tethering mainline module (shared with system server)
+        {
+                .dir = "/apex/com.android.tethering/etc/bpf/net_shared/",
+                .prefix = "net_shared/",
+        },
+        // T+ Tethering mainline module (not shared, just network_stack)
+        {
+                .dir = "/apex/com.android.tethering/etc/bpf/net_private/",
+                .prefix = "net_private/",
+        },
+};
+
+static int loadAllElfObjects(const unsigned int bpfloader_ver, const Location& location) {
+    int retVal = 0;
+    DIR* dir;
+    struct dirent* ent;
+
+    if ((dir = opendir(location.dir)) != NULL) {
+        while ((ent = readdir(dir)) != NULL) {
+            string s = ent->d_name;
+            if (!EndsWith(s, ".o")) continue;
+
+            string progPath(location.dir);
+            progPath += s;
+
+            bool critical;
+            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 {
+                ALOGD("Loaded object: %s", progPath.c_str());
+            }
+        }
+        closedir(dir);
+    }
+    return retVal;
+}
+
+static int createSysFsBpfSubDir(const char* const prefix) {
+    if (*prefix) {
+        mode_t prevUmask = umask(0);
+
+        string s = "/sys/fs/bpf/";
+        s += prefix;
+
+        errno = 0;
+        int ret = mkdir(s.c_str(), S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO);
+        if (ret && errno != EEXIST) {
+            const int err = errno;
+            ALOGE("Failed to create directory: %s, ret: %s", s.c_str(), std::strerror(err));
+            return -err;
+        }
+
+        umask(prevUmask);
+    }
+    return 0;
+}
+
+// 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.
+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));
+        return -err;
+    }
+    int len = strlen(value);
+    int v = write(fd, value, len);
+    if (v < 0) {
+        const int err = errno;
+        ALOGE("write('%s', '%s', %d) -> %s", filename, value, len, strerror(err));
+        return -err;
+    }
+    if (v != len) {
+        // In practice, due to us only using this for /proc/sys/... files, this can't happen.
+        ALOGE("write('%s', '%s', %d) -> short write [%d]", filename, value, len, v);
+        return -EINVAL;
+    }
+    return 0;
+}
+
+#define APEX_MOUNT_POINT "/apex/com.android.tethering"
+const char * const platformBpfLoader = "/system/bin/bpfloader";
+
+static int logTetheringApexVersion(void) {
+    char * found_blockdev = NULL;
+    FILE * f = NULL;
+    char buf[4096];
+
+    f = fopen("/proc/mounts", "re");
+    if (!f) return 1;
+
+    // /proc/mounts format: block_device [space] mount_point [space] other stuff... newline
+    while (fgets(buf, sizeof(buf), f)) {
+        char * blockdev = buf;
+        char * space = strchr(blockdev, ' ');
+        if (!space) continue;
+        *space = '\0';
+        char * mntpath = space + 1;
+        space = strchr(mntpath, ' ');
+        if (!space) continue;
+        *space = '\0';
+        if (strcmp(mntpath, APEX_MOUNT_POINT)) continue;
+        found_blockdev = strdup(blockdev);
+        break;
+    }
+    fclose(f);
+    f = NULL;
+
+    if (!found_blockdev) return 2;
+    ALOGV("Found Tethering Apex mounted from blockdev %s", found_blockdev);
+
+    f = fopen("/proc/mounts", "re");
+    if (!f) { free(found_blockdev); return 3; }
+
+    while (fgets(buf, sizeof(buf), f)) {
+        char * blockdev = buf;
+        char * space = strchr(blockdev, ' ');
+        if (!space) continue;
+        *space = '\0';
+        char * mntpath = space + 1;
+        space = strchr(mntpath, ' ');
+        if (!space) continue;
+        *space = '\0';
+        if (strcmp(blockdev, found_blockdev)) continue;
+        if (strncmp(mntpath, APEX_MOUNT_POINT "@", strlen(APEX_MOUNT_POINT "@"))) continue;
+        char * at = strchr(mntpath, '@');
+        if (!at) continue;
+        char * ver = at + 1;
+        ALOGI("Tethering APEX version %s", ver);
+    }
+    fclose(f);
+    free(found_blockdev);
+    return 0;
+}
+
+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;
+}
+
+static bool isTV() {
+    if (hasGSM()) return false;  // TVs don't do GSM
+
+    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;
+}
+
+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;
+}
+
+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__);
+
+    const int first_api_level = base::GetIntProperty("ro.board.first_api_level",
+                                                     effective_api_level);
+
+    // 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");
+
+    // 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 (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
+        ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
+        return 1;
+    }
+
+    if (has_platform_bpfloader_rc && has_platform_netbpfload_rc) {
+        ALOGE("Platform has *both* bpfloader & netbpfload init scripts.");
+        return 1;
+    }
+
+    logTetheringApexVersion();
+
+    if (!isAtLeastT) {
+        ALOGE("Impossible - not reachable on Android <T.");
+        return 1;
+    }
+
+    // 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;
+    }
+
+    // U bumps the kernel requirement up to 4.14
+    if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
+        ALOGE("Android U requires kernel 4.14.");
+        return 1;
+    }
+
+    // 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;
+    }
+
+    // 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;
+    }
+
+    // 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());
+        }
+    }
+
+    /* Android 14/U should only launch on 64-bit kernels
+     *   T launches on 5.10/5.15
+     *   U launches on 5.15/6.1
+     * So >=5.16 implies isKernel64Bit()
+     *
+     * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
+     *
+     * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
+     * we also require 64-bit userspace.
+     *
+     * There are various known issues with 32-bit userspace talking to various
+     * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
+     * 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.
+     */
+    if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
+        // Stuff won't work reliably, but...
+        if (isTV()) {
+            // exempt TVs... they don't really need functional advanced networking
+            ALOGW("[TV] 32-bit userspace unsupported on 6.2+ kernels.");
+        } else if (isWear() && isArm()) {
+            // exempt Arm Wear devices (arm32 ABI is far less problematic than x86-32)
+            ALOGW("[Arm Wear] 32-bit userspace unsupported on 6.2+ kernels.");
+        } else if (first_api_level <= __ANDROID_API_T__ && isArm()) {
+            // also exempt Arm devices upgrading with major kernel rev from T-
+            // might possibly be better for them to run with a newer kernel...
+            ALOGW("[Arm KernelUpRev] 32-bit userspace unsupported on 6.2+ kernels.");
+        } else if (isArm()) {
+            ALOGE("[Arm] 64-bit userspace required on 6.2+ kernels (%d).", first_api_level);
+            return 1;
+        } else { // x86 since RiscV cannot be 32-bit
+            ALOGE("[x86] 64-bit userspace required on 6.2+ kernels.");
+            return 1;
+        }
+    }
+
+    // Ensure we can determine the Android build type.
+    if (!isEng() && !isUser() && !isUserdebug()) {
+        ALOGE("Failed to determine the build type: got %s, want 'eng', 'user', or 'userdebug'",
+              getBuildType().c_str());
+        return 1;
+    }
+
+    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") &&
+            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.
+        // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+        //  kernel does not have CONFIG_BPF_JIT=y)
+        // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
+        // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
+        if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
+
+        // Enable JIT kallsyms export for privileged users only
+        // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+        //  kernel does not have CONFIG_HAVE_EBPF_JIT=y)
+        if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
+    }
+
+    // Create all the pin subdirectories
+    // (this must be done first to allow selinux_context and pin_subdir functionality,
+    //  which could otherwise fail with ENOENT during object pinning or renaming,
+    //  due to ordering issues)
+    for (const auto& location : locations) {
+        if (createSysFsBpfSubDir(location.prefix)) return 1;
+    }
+
+    // Note: there's no actual src dir for fs_bpf_loader .o's,
+    // so it is not listed in 'locations[].prefix'.
+    // This is because this is primarily meant for triggering genfscon rules,
+    // and as such this will likely always be the case.
+    // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
+    if (createSysFsBpfSubDir("loader")) return 1;
+
+    // Load all ELF objects, create programs and maps, and pin them
+    for (const auto& location : locations) {
+        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 "
+                  "problems or startup script race.");
+            ALOGE("--- DO NOT EXPECT SYSTEM TO BOOT SUCCESSFULLY ---");
+            sleep(20);
+            return 2;
+        }
+    }
+
+    int key = 1;
+    int value = 123;
+    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/initrc-doc/README.txt b/netbpfload/initrc-doc/README.txt
new file mode 100644
index 0000000..42e1fc2
--- /dev/null
+++ b/netbpfload/initrc-doc/README.txt
@@ -0,0 +1,62 @@
+This directory contains comment stripped versions of
+  //system/bpf/bpfloader/bpfloader.rc
+from previous versions of Android.
+
+Generated via:
+  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/main:bpfloader/bpfloader.rc;              ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
+
+this is entirely equivalent to:
+  (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+  (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-dev:bpfloader/bpfloader.rc;  ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+  (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-dev:bpfloader/bpfloader.rc;  ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+  (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+  (cd /android1/system/bpf && git cat-file -p remotes/goog/main:bpfloader/bpfloader.rc;    ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
+
+it is also equivalent to:
+  (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+  (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-v2-dev:bpfloader/bpfloader.rc;   ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+  (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-qpr-dev:bpfloader/bpfloader.rc;  ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+  (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+
+ie. there were no changes between R/S/T and R/S/T QPR3, and no change between U and U QPR1.
+
+Note: Sv2 sdk/api level is actually 32, it just didn't change anything wrt. bpf, so doesn't matter.
+
+
+Key takeaways:
+
+= R bpfloader:
+  - CHOWN + SYS_ADMIN
+  - asynchronous startup
+  - platform only
+  - proc file setup handled by initrc
+
+= S bpfloader
+  - adds NET_ADMIN
+  - synchronous startup
+  - platform + mainline tethering offload
+
+= T bpfloader
+  - platform + mainline networking (including tethering offload)
+  - supported btf for maps via exec of btfloader
+
+= U bpfloader
+  - proc file setup moved into bpfloader binary
+  - explicitly specified user and groups:
+    group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+    user root
+
+= U QPR2 bpfloader
+  - drops support of btf for maps
+  - invocation of /system/bin/netbpfload binary, which after handling *all*
+    networking bpf related things executes the platform /system/bin/bpfloader
+    which handles non-networking bpf.
+
+Note that there is now a copy of 'netbpfload' provided by the tethering apex
+mainline module at /apex/com.android.tethering/bin/netbpfload, which due
+to the use of execve("/system/bin/bpfloader") relies on T+ selinux which was
+added for btf map support (specifically the ability to exec the "btfloader").
diff --git a/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc b/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc
new file mode 100644
index 0000000..482a7db
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+    write /proc/sys/net/core/bpf_jit_enable 1
+    write /proc/sys/net/core/bpf_jit_kallsyms 1
+    start bpfloader
+
+service bpfloader /system/bin/bpfloader
+    capabilities CHOWN SYS_ADMIN
+    rlimit memlock 1073741824 1073741824
+    oneshot
+    reboot_on_failure reboot,bpfloader-failed
+    updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc b/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc
new file mode 100644
index 0000000..4117887
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+    write /proc/sys/net/core/bpf_jit_enable 1
+    write /proc/sys/net/core/bpf_jit_kallsyms 1
+    exec_start bpfloader
+
+service bpfloader /system/bin/bpfloader
+    capabilities CHOWN SYS_ADMIN NET_ADMIN
+    rlimit memlock 1073741824 1073741824
+    oneshot
+    reboot_on_failure reboot,bpfloader-failed
+    updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc b/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc
new file mode 100644
index 0000000..f0b6700
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc
@@ -0,0 +1,12 @@
+on load_bpf_programs
+    write /proc/sys/kernel/unprivileged_bpf_disabled 0
+    write /proc/sys/net/core/bpf_jit_enable 1
+    write /proc/sys/net/core/bpf_jit_kallsyms 1
+    exec_start bpfloader
+
+service bpfloader /system/bin/bpfloader
+    capabilities CHOWN SYS_ADMIN NET_ADMIN
+    rlimit memlock 1073741824 1073741824
+    oneshot
+    reboot_on_failure reboot,bpfloader-failed
+    updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
new file mode 100644
index 0000000..8f3f462
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+    exec_start bpfloader
+
+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
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc b/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc
new file mode 100644
index 0000000..592303e
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+    exec_start bpfloader
+
+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
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
new file mode 100644
index 0000000..5141095
--- /dev/null
+++ b/netbpfload/loader.cpp
@@ -0,0 +1,1194 @@
+/*
+ * 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 "NetBpfLoad"
+
+#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>
+
+#include "BpfSyscallWrappers.h"
+#include "bpf/BpfUtils.h"
+#include "bpf/bpf_map_def.h"
+#include "loader.h"
+
+#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},
+        {"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},
+        {"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);
+    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;
+        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 (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,
+                  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);
+        // 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_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 (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, 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].expected_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;
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
new file mode 100644
index 0000000..4da6830
--- /dev/null
+++ b/netbpfload/loader.h
@@ -0,0 +1,94 @@
+/*
+ * 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 unsigned int bpfloader_ver,
+             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.35rc b/netbpfload/netbpfload.35rc
new file mode 100644
index 0000000..0fbcb5a
--- /dev/null
+++ b/netbpfload/netbpfload.35rc
@@ -0,0 +1,9 @@
+service bpfloader /apex/com.android.tethering/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
+    file /dev/kmsg w
+    rlimit memlock 1073741824 1073741824
+    oneshot
+    reboot_on_failure reboot,bpfloader-failed
+    override
diff --git a/netbpfload/netbpfload.rc b/netbpfload/netbpfload.rc
new file mode 100644
index 0000000..e1af47f
--- /dev/null
+++ b/netbpfload/netbpfload.rc
@@ -0,0 +1,88 @@
+# zygote-start is what officially starts netd (see //system/core/rootdir/init.rc)
+# However, on some hardware it's started from post-fs-data as well, which is just
+# a tad earlier.  There's no benefit to that though, since on 4.9+ P+ devices netd
+# will just block until bpfloader finishes and sets the bpf.progs_loaded property.
+#
+# It is important that we start bpfloader after:
+#   - /sys/fs/bpf is already mounted,
+#   - apex (incl. rollback) is initialized (so that in the future we can load bpf
+#     programs shipped as part of apex mainline modules)
+#   - logd is ready for us to log stuff
+#
+# At the same time we want to be as early as possible to reduce races and thus
+# failures (before memory is fragmented, and cpu is busy running tons of other
+# stuff) and we absolutely want to be before netd and the system boot slot is
+# considered to have booted successfully.
+#
+on load_bpf_programs
+    exec_start bpfloader
+
+# 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
+    # 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
+    user root
+    #
+    # Set RLIMIT_MEMLOCK to 1GiB for bpfloader
+    #
+    # Actually only 8MiB would be needed if bpfloader ran as its own uid.
+    #
+    # However, while the rlimit is per-thread, the accounting is system wide.
+    # So, for example, if the graphics stack has already allocated 10MiB of
+    # memlock data before bpfloader even gets a chance to run, it would fail
+    # if its memlock rlimit is only 8MiB - since there would be none left for it.
+    #
+    # bpfloader succeeding is critical to system health, since a failure will
+    # cause netd crashloop and thus system server crashloop... and the only
+    # recovery is a full kernel reboot.
+    #
+    # We've had issues where devices would sometimes (rarely) boot into
+    # a crashloop because bpfloader would occasionally lose a boot time
+    # race against the graphics stack's boot time locked memory allocation.
+    #
+    # Thus bpfloader's memlock has to be 8MB higher then the locked memory
+    # consumption of the root uid anywhere else in the system...
+    # But we don't know what that is for all possible devices...
+    #
+    # Ideally, we'd simply grant bpfloader the IPC_LOCK capability and it
+    # would simply ignore it's memlock rlimit... but it turns that this
+    # capability is not even checked by the kernel's bpf system call.
+    #
+    # As such we simply use 1GiB as a reasonable approximation of infinity.
+    #
+    #! rlimit memlock 1073741824 1073741824
+    oneshot
+    #
+    # How to debug bootloops caused by 'bpfloader-failed'.
+    #
+    # 1. On some lower RAM devices (like wembley) you may need to first enable developer mode
+    #    (from the Settings app UI), and change the developer option "Logger buffer sizes"
+    #    from the default (wembley: 64kB) to the maximum (1M) per log buffer.
+    #    Otherwise buffer will overflow before you manage to dump it and you'll get useless logs.
+    #
+    # 2. comment out 'reboot_on_failure reboot,bpfloader-failed' below
+    # 3. rebuild/reflash/reboot
+    # 4. as the device is booting up capture bpfloader logs via:
+    #    adb logcat -s 'bpfloader:*' 'LibBpfLoader:*' 'NetBpfLoad:*' 'NetBpfLoader:*'
+    #
+    # something like:
+    #   $ adb reboot; sleep 1; adb wait-for-device; adb root; sleep 1; adb wait-for-device; adb logcat -s 'bpfloader:*' 'LibBpfLoader:*' 'NetBpfLoad:*' 'NetBpfLoader:*'
+    # will take care of capturing logs as early as possible
+    #
+    # 5. look through the logs from the kernel's bpf verifier that bpfloader dumps out,
+    #    it usually makes sense to search back from the end and find the particular
+    #    bpf verifier failure that caused bpfloader to terminate early with an error code.
+    #    This will probably be something along the lines of 'too many jumps' or
+    #    'cannot prove return value is 0 or 1' or 'unsupported / unknown operation / helper',
+    #    'invalid bpf_context access', etc.
+    #
+    reboot_on_failure reboot,netbpfload-missing
+    updatable
diff --git a/netd/Android.bp b/netd/Android.bp
index 4325d89..fe4d999 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -14,6 +14,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -58,21 +59,26 @@
 cc_test {
     name: "netd_updatable_unit_test",
     defaults: ["netd_defaults"],
-    test_suites: ["general-tests", "mts-tethering"],
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
     test_config_template: ":net_native_test_config_template",
-    require_root: true,  // required by setrlimitForTest()
+    require_root: true, // required by setrlimitForTest()
     header_libs: [
         "bpf_connectivity_headers",
     ],
     srcs: [
         "BpfHandlerTest.cpp",
-        "BpfBaseTest.cpp"
+        "BpfBaseTest.cpp",
     ],
+    version_script: ":connectivity_mainline_test_map",
+    stl: "libc++_static",
     static_libs: [
+        "libbase",
         "libnetd_updatable",
     ],
     shared_libs: [
-        "libbase",
         "libcutils",
         "liblog",
         "libnetdutils",
diff --git a/netd/BpfBaseTest.cpp b/netd/BpfBaseTest.cpp
index 624d216..c979a7b 100644
--- a/netd/BpfBaseTest.cpp
+++ b/netd/BpfBaseTest.cpp
@@ -93,7 +93,7 @@
     ASSERT_EQ(TEST_TAG, tagResult.value().tag);
     ASSERT_EQ(0, close(sock));
     // Check map periodically until sk destroy handler have done its job.
-    for (int i = 0; i < 10; i++) {
+    for (int i = 0; i < 1000; i++) {
         usleep(5000);  // 5ms
         tagResult = cookieTagMap.readValue(cookie);
         if (!tagResult.ok()) {
@@ -101,7 +101,7 @@
             return;
         }
     }
-    FAIL() << "socket tag still exist after 50ms";
+    FAIL() << "socket tag still exist after 5s";
 }
 
 }
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 6409374..9682545 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-#define LOG_TAG "BpfHandler"
+#define LOG_TAG "NetdUpdatable"
 
 #include "BpfHandler.h"
 
 #include <linux/bpf.h>
+#include <inttypes.h>
 
 #include <android-base/unique_fd.h>
 #include <android-modules-utils/sdk_level.h>
@@ -33,6 +34,7 @@
 namespace net {
 
 using base::unique_fd;
+using base::WaitForProperty;
 using bpf::getSocketCookie;
 using bpf::retrieveProgram;
 using netdutils::Status;
@@ -51,37 +53,49 @@
 static Status attachProgramToCgroup(const char* programPath, const unique_fd& cgroupFd,
                                     bpf_attach_type type) {
     unique_fd cgroupProg(retrieveProgram(programPath));
-    if (cgroupProg == -1) {
-        int ret = errno;
-        ALOGE("Failed to get program from %s: %s", programPath, strerror(ret));
-        return statusFromErrno(ret, "cgroup program get failed");
+    if (!cgroupProg.ok()) {
+        return statusFromErrno(errno, fmt::format("Failed to get program from {}", programPath));
     }
     if (android::bpf::attachProgram(type, cgroupProg, cgroupFd)) {
-        int ret = errno;
-        ALOGE("Program from %s attach failed: %s", programPath, strerror(ret));
-        return statusFromErrno(ret, "program attach failed");
+        return statusFromErrno(errno, fmt::format("Program {} attach failed", programPath));
     }
     return netdutils::status::ok;
 }
 
 static Status checkProgramAccessible(const char* programPath) {
     unique_fd prog(retrieveProgram(programPath));
-    if (prog == -1) {
-        int ret = errno;
-        ALOGE("Failed to get program from %s: %s", programPath, strerror(ret));
-        return statusFromErrno(ret, "program retrieve failed");
+    if (!prog.ok()) {
+        return statusFromErrno(errno, fmt::format("Failed to get program from {}", programPath));
     }
     return netdutils::status::ok;
 }
 
 static Status initPrograms(const char* cg2_path) {
-    if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) abort();
+    if (!cg2_path) return Status("cg2_path is NULL");
+
+    // This code was mainlined in T, so this should be trivially satisfied.
+    if (!modules::sdklevel::IsAtLeastT()) return Status("S- platform is unsupported");
+
+    // S requires eBPF support which was only added in 4.9, so this should be satisfied.
+    if (!bpf::isAtLeastKernelVersion(4, 9, 0)) {
+        return Status("kernel version < 4.9.0 is unsupported");
+    }
+
+    // U bumps the kernel requirement up to 4.14
+    if (modules::sdklevel::IsAtLeastU() && !bpf::isAtLeastKernelVersion(4, 14, 0)) {
+        return Status("U+ platform with kernel version < 4.14.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");
+    }
 
     unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
-    if (cg_fd == -1) {
-        const int ret = errno;
-        ALOGE("Failed to open the cgroup directory: %s", strerror(ret));
-        return statusFromErrno(ret, "Open the cgroup directory failed");
+    if (!cg_fd.ok()) {
+        const int err = errno;
+        ALOGE("Failed to open the cgroup directory: %s", strerror(err));
+        return statusFromErrno(err, "Open the cgroup directory failed");
     }
     RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_ALLOWLIST_PROG_PATH));
     RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_DENYLIST_PROG_PATH));
@@ -96,9 +110,72 @@
     // 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)) {
+        RETURN_IF_NOT_OK(attachProgramToCgroup(
+                "/sys/fs/bpf/netd_readonly/prog_block_bind4_block_port",
+                cg_fd, BPF_CGROUP_INET4_BIND));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(
+                "/sys/fs/bpf/netd_readonly/prog_block_bind6_block_port",
+                cg_fd, BPF_CGROUP_INET6_BIND));
+
+        // This should trivially pass, since we just attached up above,
+        // but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
+        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;
 }
 
@@ -109,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));
@@ -120,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));
@@ -200,7 +344,7 @@
     // which might toggle the live stats map and clean it.
     const auto countUidStatsEntries = [chargeUid, &totalEntryCount, &perUidEntryCount](
                                               const StatsKey& key,
-                                              const BpfMap<StatsKey, StatsValue>&) {
+                                              const BpfMapRO<StatsKey, StatsValue>&) {
         if (key.uid == chargeUid) {
             perUidEntryCount++;
         }
@@ -209,8 +353,8 @@
     };
     auto configuration = mConfigurationMap.readValue(CURRENT_STATS_MAP_CONFIGURATION_KEY);
     if (!configuration.ok()) {
-        ALOGE("Failed to get current configuration: %s, fd: %d",
-              strerror(configuration.error().code()), mConfigurationMap.getMap().get());
+        ALOGE("Failed to get current configuration: %s",
+              strerror(configuration.error().code()));
         return -configuration.error().code();
     }
     if (configuration.value() != SELECT_MAP_A && configuration.value() != SELECT_MAP_B) {
@@ -218,12 +362,11 @@
         return -EINVAL;
     }
 
-    BpfMap<StatsKey, StatsValue>& currentMap =
+    BpfMapRO<StatsKey, StatsValue>& currentMap =
             (configuration.value() == SELECT_MAP_A) ? mStatsMapA : mStatsMapB;
-    // HACK: mStatsMapB becomes RW BpfMap here, but countUidStatsEntries doesn't modify so it works
     base::Result<void> res = currentMap.iterate(countUidStatsEntries);
     if (!res.ok()) {
-        ALOGE("Failed to count the stats entry in map %d: %s", currentMap.getMap().get(),
+        ALOGE("Failed to count the stats entry in map: %s",
               strerror(res.error().code()));
         return -res.error().code();
     }
@@ -242,10 +385,11 @@
     // should be fine to concurrently update the map while eBPF program is running.
     res = mCookieTagMap.writeValue(sock_cookie, newKey, BPF_ANY);
     if (!res.ok()) {
-        ALOGE("Failed to tag the socket: %s, fd: %d", strerror(res.error().code()),
-              mCookieTagMap.getMap().get());
+        ALOGE("Failed to tag the socket: %s", strerror(res.error().code()));
         return -res.error().code();
     }
+    ALOGD("Socket with cookie %" PRIu64 " tagged successfully with tag %" PRIu32 " uid %u "
+              "and real uid %u", sock_cookie, tag, chargeUid, realUid);
     return 0;
 }
 
@@ -259,6 +403,7 @@
         ALOGE("Failed to untag socket: %s", strerror(res.error().code()));
         return -res.error().code();
     }
+    ALOGD("Socket with cookie %" PRIu64 " untagged successfully.", sock_cookie);
     return 0;
 }
 
diff --git a/netd/BpfHandler.h b/netd/BpfHandler.h
index a6da4eb..9e69efc 100644
--- a/netd/BpfHandler.h
+++ b/netd/BpfHandler.h
@@ -59,10 +59,10 @@
     bool hasUpdateDeviceStatsPermission(uid_t uid);
 
     BpfMap<uint64_t, UidTagValue> mCookieTagMap;
-    BpfMap<StatsKey, StatsValue> mStatsMapA;
+    BpfMapRO<StatsKey, StatsValue> mStatsMapA;
     BpfMapRO<StatsKey, StatsValue> mStatsMapB;
     BpfMapRO<uint32_t, uint32_t> mConfigurationMap;
-    BpfMap<uint32_t, uint8_t> mUidPermissionMap;
+    BpfMapRO<uint32_t, uint8_t> mUidPermissionMap;
 
     // The limit on the number of stats entries a uid can have in the per uid stats map. BpfHandler
     // will block that specific uid from tagging new sockets after the limit is reached.
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index f5c9a68..b38fa16 100644
--- a/netd/BpfHandlerTest.cpp
+++ b/netd/BpfHandlerTest.cpp
@@ -49,7 +49,7 @@
     BpfHandler mBh;
     BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
     BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
-    BpfMapRO<uint32_t, uint32_t> mFakeConfigurationMap;
+    BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
     BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
 
     void SetUp() {
diff --git a/netd/NetdUpdatable.cpp b/netd/NetdUpdatable.cpp
index 41b1fdb..3b15916 100644
--- a/netd/NetdUpdatable.cpp
+++ b/netd/NetdUpdatable.cpp
@@ -31,8 +31,8 @@
 
     android::netdutils::Status ret = sBpfHandler.init(cg2_path);
     if (!android::netdutils::isOk(ret)) {
-        LOG(ERROR) << __func__ << ": BPF handler init failed";
-        return -ret.code();
+        LOG(ERROR) << __func__ << ": Failed: (" << ret.code() << ") " << ret.msg();
+        abort();
     }
     return 0;
 }
diff --git a/remoteauth/OWNERS b/remoteauth/OWNERS
new file mode 100644
index 0000000..25a32b9
--- /dev/null
+++ b/remoteauth/OWNERS
@@ -0,0 +1,14 @@
+# Bug component: 1145231
+# Bug template url: http://b/new?component=1145231&template=1715387
+billyhuang@google.com
+boetger@google.com
+casbor@google.com
+derekjedral@google.com
+dlm@google.com
+igorzas@google.com
+jacobhobbie@google.com
+jasonsun@google.com
+jianbing@google.com
+jinjiechen@google.com
+justinmcclain@google.com
+salilr@google.com
diff --git a/remoteauth/README.md b/remoteauth/README.md
new file mode 100644
index 0000000..f28154d
--- /dev/null
+++ b/remoteauth/README.md
@@ -0,0 +1,42 @@
+# RemoteAuth Mainline Module
+
+This directory contains code for the RemoteAuth module.
+
+## Directory Structure
+
+`framework`
+ - Contains client side APIs and AIDL files.
+
+`jni`
+ - JNI wrapper for invoking Android APIs from native code.
+
+`native`
+ - Native code implementation for RemoteAuth module services.
+
+`service`
+ - Server side implementation for RemoteAuth module services.
+
+`tests`
+ - Unit/Multi devices tests for RemoteAuth module (both Java and native code).
+
+## IDE setup
+
+### AIDEGen
+
+```sh
+$ source build/envsetup.sh && lunch <TARGET>
+$ cd packages/modules/Connectivity
+$ aidegen .
+# This will launch Intellij project for RemoteAuth module.
+```
+
+## Build and Install
+
+```sh
+$ source build/envsetup.sh && lunch <TARGET>
+$ m com.android.tethering deapexer
+$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input \
+    ${ANDROID_PRODUCT_OUT}/system/apex/com.android.tethering.capex \
+    --output /tmp/tethering.apex
+$ adb install -r /tmp/tethering.apex
+```
diff --git a/remoteauth/TEST_MAPPING b/remoteauth/TEST_MAPPING
new file mode 100644
index 0000000..5061319
--- /dev/null
+++ b/remoteauth/TEST_MAPPING
@@ -0,0 +1,13 @@
+{
+  "presubmit": [
+    {
+      "name": "RemoteAuthUnitTests"
+    }
+  ]
+  // TODO(b/193602229): uncomment once it's supported.
+  //"mainline-presubmit": [
+  //  {
+  //    "name": "RemoteAuthUnitTests[com.google.android.remoteauth.apex]"
+  //  }
+  //]
+}
diff --git a/remoteauth/framework/Android.bp b/remoteauth/framework/Android.bp
new file mode 100644
index 0000000..2f1737f
--- /dev/null
+++ b/remoteauth/framework/Android.bp
@@ -0,0 +1,56 @@
+// 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",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Sources included in the framework-connectivity jar
+filegroup {
+    name: "framework-remoteauth-java-sources",
+    srcs: [
+        "java/**/*.java",
+        "java/**/*.aidl",
+    ],
+    path: "java",
+    visibility: [
+        "//packages/modules/Connectivity/framework-t:__subpackages__",
+    ],
+}
+
+filegroup {
+    name: "framework-remoteauth-sources",
+    defaults: ["framework-sources-module-defaults"],
+    srcs: [
+        ":framework-remoteauth-java-sources",
+    ],
+}
+
+// Build of only framework-remoteauth (not as part of connectivity) for
+// unit tests
+java_library {
+    name: "framework-remoteauth-static",
+    srcs: [":framework-remoteauth-java-sources"],
+    sdk_version: "module_current",
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-annotations-lib",
+        "framework-bluetooth",
+    ],
+    static_libs: [
+        "modules-utils-preconditions",
+    ],
+    visibility: ["//packages/modules/Connectivity/remoteauth/tests:__subpackages__"],
+}
diff --git a/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java b/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java
new file mode 100644
index 0000000..1414f7e
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 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.remoteauth;
+
+import android.annotation.NonNull;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Reports newly discovered remote devices.
+ *
+ * @hide
+ */
+// TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
+public interface DeviceDiscoveryCallback {
+    /** The device is no longer seen in the discovery process. */
+    int STATE_LOST = 0;
+    /** The device is seen in the discovery process */
+    int STATE_SEEN = 1;
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({STATE_LOST, STATE_SEEN})
+    @interface State {}
+
+    /**
+     * Invoked for every change in remote device state.
+     *
+     * @param device remote device
+     * @param state indicates if found or lost
+     */
+    void onDeviceUpdate(@NonNull RemoteDevice device, @State int state);
+
+    /** Invoked when discovery is stopped due to timeout. */
+    void onTimeout();
+}
diff --git a/remoteauth/framework/java/android/remoteauth/IDeviceDiscoveryListener.aidl b/remoteauth/framework/java/android/remoteauth/IDeviceDiscoveryListener.aidl
new file mode 100644
index 0000000..2ad6a6a
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/IDeviceDiscoveryListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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.remoteauth;
+
+import android.remoteauth.RemoteDevice;
+
+/**
+ * Binder callback for DeviceDiscoveryCallback.
+ *
+ * {@hide}
+ */
+oneway interface IDeviceDiscoveryListener {
+        /** Reports a {@link RemoteDevice} being discovered. */
+        void onDiscovered(in RemoteDevice remoteDevice);
+
+        /** Reports a {@link RemoteDevice} is no longer within range. */
+        void onLost(in RemoteDevice remoteDevice);
+
+        /** Reports a timeout of {@link RemoteDevice} was reached. */
+        void onTimeout();
+}
diff --git a/remoteauth/framework/java/android/remoteauth/IRemoteAuthService.aidl b/remoteauth/framework/java/android/remoteauth/IRemoteAuthService.aidl
new file mode 100644
index 0000000..f4387e3
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/IRemoteAuthService.aidl
@@ -0,0 +1,42 @@
+/*
+ * 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.remoteauth;
+
+import android.remoteauth.IDeviceDiscoveryListener;
+
+/**
+ * Interface for communicating with the RemoteAuthService.
+ * These methods are all require MANAGE_REMOTE_AUTH signature permission.
+ * @hide
+ */
+interface IRemoteAuthService {
+    // This is protected by the MANAGE_REMOTE_AUTH signature permission.
+    boolean isRemoteAuthSupported();
+
+    // This is protected by the MANAGE_REMOTE_AUTH signature permission.
+    boolean registerDiscoveryListener(in IDeviceDiscoveryListener deviceDiscoveryListener,
+                                  int userId,
+                                  int timeoutMs,
+                                  String packageName,
+                                  @nullable String attributionTag);
+
+    // This is protected by the MANAGE_REMOTE_AUTH signature permission.
+    void unregisterDiscoveryListener(in IDeviceDiscoveryListener deviceDiscoveryListener,
+                                     int userId,
+                                     String packageName,
+                                     @nullable String attributionTag);
+}
\ No newline at end of file
diff --git a/remoteauth/framework/java/android/remoteauth/README.md b/remoteauth/framework/java/android/remoteauth/README.md
new file mode 100644
index 0000000..13fefee
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/README.md
@@ -0,0 +1 @@
+This is the source root for the RemoteAuth framework
\ No newline at end of file
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java b/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java
new file mode 100644
index 0000000..112ffa8
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 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.remoteauth;
+
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Class for initializing RemoteAuth service.
+ *
+ * @hide
+ */
+// TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
+public final class RemoteAuthFrameworkInitializer {
+    private RemoteAuthFrameworkInitializer() {}
+
+    /**
+     * Called by {@link SystemServiceRegistry}'s static initializer and registers all Nearby
+     * services to {@link Context}, so that {@link Context#getSystemService} can return them.
+     *
+     * @throws IllegalStateException if this is called from anywhere besides {@link
+     *     SystemServiceRegistry}
+     */
+    public static void registerServiceWrappers() {
+        // TODO(b/290092977): Change to Context.REMOTE_AUTH_SERVICE after aosp/2681375
+        // is automerges from aosp-main to udc-mainline-prod
+        SystemServiceRegistry.registerContextAwareService(
+                RemoteAuthManager.REMOTE_AUTH_SERVICE,
+                RemoteAuthManager.class,
+                (context, serviceBinder) -> {
+                    IRemoteAuthService service = IRemoteAuthService.Stub.asInterface(serviceBinder);
+                    return new RemoteAuthManager(context, service);
+                });
+    }
+}
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java b/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java
new file mode 100644
index 0000000..038af2a
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 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.remoteauth;
+
+import static android.remoteauth.DeviceDiscoveryCallback.STATE_LOST;
+import static android.remoteauth.DeviceDiscoveryCallback.STATE_SEEN;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.SystemService;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * A system service providing a way to perform remote authentication-related operations such as
+ * discovering, registering and authenticating via remote authenticator.
+ *
+ * <p>To get a {@link RemoteAuthManager} instance, call the <code>
+ * Context.getSystemService(Context.REMOTE_AUTH_SERVICE)</code>.
+ *
+ * @hide
+ */
+// TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
+// TODO(b/290092977): Change to Context.REMOTE_AUTH_SERVICE after aosp/2681375
+// is automerges from aosp-main to udc-mainline-prod
+@SystemService(RemoteAuthManager.REMOTE_AUTH_SERVICE)
+public class RemoteAuthManager {
+    private static final String TAG = "RemoteAuthManager";
+
+    /** @hide */
+    public static final String REMOTE_AUTH_SERVICE = "remote_auth";
+
+    private final Context mContext;
+    private final IRemoteAuthService mService;
+
+    @GuardedBy("mDiscoveryListeners")
+    private final WeakHashMap<
+                    DeviceDiscoveryCallback, WeakReference<DeviceDiscoveryListenerTransport>>
+            mDiscoveryListeners = new WeakHashMap<>();
+
+    /** @hide */
+    public RemoteAuthManager(@NonNull Context context, @NonNull IRemoteAuthService service) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(service);
+        mContext = context;
+        mService = service;
+    }
+
+    /**
+     * Returns if this device can be enrolled in the feature.
+     *
+     * @return true if this device can be enrolled
+     * @hide
+     */
+    // TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
+    public boolean isRemoteAuthSupported() {
+        try {
+            return mService.isRemoteAuthSupported();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Starts remote authenticator discovery process with timeout. Devices that are capable to
+     * operate as remote authenticators are reported via callback. The discovery stops by calling
+     * stopDiscovery or after a timeout.
+     *
+     * @param timeoutMs the duration in milliseconds after which discovery will stop automatically
+     * @param executor the callback will be executed in the executor thread
+     * @param callback to be used by the caller to get notifications about remote devices
+     * @return {@code true} if discovery began successfully, {@code false} otherwise
+     * @hide
+     */
+    // TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
+    public boolean startDiscovery(
+            int timeoutMs,
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull DeviceDiscoveryCallback callback) {
+        try {
+            Preconditions.checkNotNull(callback, "invalid null callback");
+            Preconditions.checkArgument(timeoutMs > 0, "invalid timeoutMs, must be > 0");
+            Preconditions.checkNotNull(executor, "invalid null executor");
+            DeviceDiscoveryListenerTransport transport;
+            synchronized (mDiscoveryListeners) {
+                WeakReference<DeviceDiscoveryListenerTransport> reference =
+                        mDiscoveryListeners.get(callback);
+                transport = (reference != null) ? reference.get() : null;
+                if (transport == null) {
+                    transport =
+                            new DeviceDiscoveryListenerTransport(
+                                    callback, mContext.getUser().getIdentifier(), executor);
+                }
+
+                boolean result =
+                        mService.registerDiscoveryListener(
+                                transport,
+                                mContext.getUser().getIdentifier(),
+                                timeoutMs,
+                                mContext.getPackageName(),
+                                mContext.getAttributionTag());
+                if (result) {
+                    mDiscoveryListeners.put(callback, new WeakReference<>(transport));
+                    return true;
+                }
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        return false;
+    }
+
+    /**
+     * Removes this listener from device discovery notifications. The given callback is guaranteed
+     * not to receive any invocations that happen after this method is invoked.
+     *
+     * @param callback the callback for the previously started discovery to be ended
+     * @hide
+     */
+    // Suppressed lint: Registration methods should have overload that accepts delivery Executor.
+    // Already have executor in startDiscovery() method.
+    @SuppressLint("ExecutorRegistration")
+    // TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
+    public void stopDiscovery(@NonNull DeviceDiscoveryCallback callback) {
+        Preconditions.checkNotNull(callback, "invalid null scanCallback");
+        try {
+            DeviceDiscoveryListenerTransport transport;
+            synchronized (mDiscoveryListeners) {
+                WeakReference<DeviceDiscoveryListenerTransport> reference =
+                        mDiscoveryListeners.remove(callback);
+                transport = (reference != null) ? reference.get() : null;
+            }
+            if (transport != null) {
+                mService.unregisterDiscoveryListener(
+                        transport,
+                        transport.getUserId(),
+                        mContext.getPackageName(),
+                        mContext.getAttributionTag());
+            } else {
+                Log.d(
+                        TAG,
+                        "Cannot stop discovery with this callback "
+                                + "because it is not registered.");
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private class DeviceDiscoveryListenerTransport extends IDeviceDiscoveryListener.Stub {
+
+        private volatile @NonNull DeviceDiscoveryCallback mDeviceDiscoveryCallback;
+        private Executor mExecutor;
+        private @UserIdInt int mUserId;
+
+        DeviceDiscoveryListenerTransport(
+                DeviceDiscoveryCallback deviceDiscoveryCallback,
+                @UserIdInt int userId,
+                @CallbackExecutor Executor executor) {
+            Preconditions.checkNotNull(deviceDiscoveryCallback, "invalid null callback");
+            mDeviceDiscoveryCallback = deviceDiscoveryCallback;
+            mUserId = userId;
+            mExecutor = executor;
+        }
+
+        @UserIdInt
+        int getUserId() {
+            return mUserId;
+        }
+
+        @Override
+        public void onDiscovered(RemoteDevice remoteDevice) throws RemoteException {
+            if (remoteDevice == null) {
+                Log.w(TAG, "onDiscovered is called with null device");
+                return;
+            }
+            Log.i(TAG, "Notifying the caller about discovered: " + remoteDevice);
+            mExecutor.execute(
+                    () -> {
+                        mDeviceDiscoveryCallback.onDeviceUpdate(remoteDevice, STATE_SEEN);
+                    });
+        }
+
+        @Override
+        public void onLost(RemoteDevice remoteDevice) throws RemoteException {
+            if (remoteDevice == null) {
+                Log.w(TAG, "onLost is called with null device");
+                return;
+            }
+            Log.i(TAG, "Notifying the caller about lost: " + remoteDevice);
+            mExecutor.execute(
+                    () -> {
+                        mDeviceDiscoveryCallback.onDeviceUpdate(remoteDevice, STATE_LOST);
+                    });
+        }
+
+        @Override
+        public void onTimeout() {
+            Log.i(TAG, "Notifying the caller about discovery timeout");
+            mExecutor.execute(
+                    () -> {
+                        mDeviceDiscoveryCallback.onTimeout();
+                    });
+            synchronized (mDiscoveryListeners) {
+                mDiscoveryListeners.remove(mDeviceDiscoveryCallback);
+            }
+            mDeviceDiscoveryCallback = null;
+        }
+    }
+}
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteDevice.aidl b/remoteauth/framework/java/android/remoteauth/RemoteDevice.aidl
new file mode 100644
index 0000000..ea38be2
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteDevice.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.remoteauth;
+
+parcelable RemoteDevice;
\ No newline at end of file
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteDevice.java b/remoteauth/framework/java/android/remoteauth/RemoteDevice.java
new file mode 100644
index 0000000..b6ede2e
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteDevice.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 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.remoteauth;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Remote device that can be registered as remote authenticator.
+ *
+ * @hide
+ */
+// TODO(b/295407748) Change to use @DataClass
+// TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
+public final class RemoteDevice implements Parcelable {
+    /** The remote device is not registered as remote authenticator. */
+    public static final int STATE_NOT_REGISTERED = 0;
+    /** The remote device is registered as remote authenticator. */
+    public static final int STATE_REGISTERED = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({STATE_NOT_REGISTERED, STATE_REGISTERED})
+    @interface RegistrationState {}
+
+    @NonNull private final String mName;
+    private final @RegistrationState int mRegistrationState;
+    private final int mConnectionId;
+
+    public static final @NonNull Creator<RemoteDevice> CREATOR =
+            new Creator<>() {
+                @Override
+                public RemoteDevice createFromParcel(Parcel in) {
+                    RemoteDevice.Builder builder = new RemoteDevice.Builder();
+                    builder.setName(in.readString());
+                    builder.setRegistrationState(in.readInt());
+                    builder.setConnectionId(in.readInt());
+
+                    return builder.build();
+                }
+
+                @Override
+                public RemoteDevice[] newArray(int size) {
+                    return new RemoteDevice[size];
+                }
+            };
+
+    private RemoteDevice(
+            @Nullable String name,
+            @RegistrationState int registrationState,
+            @NonNull int connectionId) {
+        this.mName = name;
+        this.mRegistrationState = registrationState;
+        this.mConnectionId = connectionId;
+    }
+
+    /** Gets the name of the {@link RemoteDevice} device. */
+    @Nullable
+    public String getName() {
+        return mName;
+    }
+
+    /** Returns registration state of the {@link RemoteDevice}. */
+    public @RegistrationState int getRegistrationState() {
+        return mRegistrationState;
+    }
+
+    /** Returns connection id of the {@link RemoteDevice}. */
+    @NonNull
+    public int getConnectionId() {
+        return mConnectionId;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Returns a string representation of {@link RemoteDevice}. */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("RemoteDevice [");
+        sb.append("name=").append(mName).append(", ");
+        sb.append("registered=").append(mRegistrationState).append(", ");
+        sb.append("connectionId=").append(mConnectionId);
+        sb.append("]");
+        return sb.toString();
+    }
+
+    /** Returns true if this {@link RemoteDevice} object is equals to other. */
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof RemoteDevice) {
+            RemoteDevice otherDevice = (RemoteDevice) other;
+            return Objects.equals(this.mName, otherDevice.mName)
+                    && this.getRegistrationState() == otherDevice.getRegistrationState()
+                    && this.mConnectionId == otherDevice.mConnectionId;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mName, mRegistrationState, mConnectionId);
+    }
+
+    /**
+     * Helper function for writing {@link RemoteDevice} to a Parcel.
+     *
+     * @param dest The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        String name = getName();
+        dest.writeString(name);
+        dest.writeInt(getRegistrationState());
+        dest.writeInt(getConnectionId());
+    }
+
+    /** Builder for {@link RemoteDevice} objects. */
+    public static final class Builder {
+        @Nullable private String mName;
+        // represents if device is already registered
+        private @RegistrationState int mRegistrationState;
+        private int mConnectionId;
+
+        private Builder() {
+        }
+
+        public Builder(final int connectionId) {
+            this.mConnectionId = connectionId;
+        }
+
+        /**
+         * Sets the name of the {@link RemoteDevice} device.
+         *
+         * @param name of the {@link RemoteDevice}. Can be {@code null} if there is no name.
+         */
+        @NonNull
+        public RemoteDevice.Builder setName(@Nullable String name) {
+            this.mName = name;
+            return this;
+        }
+
+        /**
+         * Sets the registration state of the {@link RemoteDevice} device.
+         *
+         * @param registrationState of the {@link RemoteDevice}.
+         */
+        @NonNull
+        public RemoteDevice.Builder setRegistrationState(@RegistrationState int registrationState) {
+            this.mRegistrationState = registrationState;
+            return this;
+        }
+
+        /**
+         * Sets the connectionInfo of the {@link RemoteDevice} device.
+         *
+         * @param connectionId of the RemoteDevice.
+         */
+        @NonNull
+        public RemoteDevice.Builder setConnectionId(int connectionId) {
+            this.mConnectionId = connectionId;
+            return this;
+        }
+
+        /**
+         * Creates the {@link RemoteDevice} instance.
+         *
+         * @return the configured {@link RemoteDevice} instance.
+         */
+        @NonNull
+        public RemoteDevice build() {
+            return new RemoteDevice(mName, mRegistrationState, mConnectionId);
+        }
+    }
+}
diff --git a/remoteauth/framework/java/android/remoteauth/aidl/README.md b/remoteauth/framework/java/android/remoteauth/aidl/README.md
new file mode 100644
index 0000000..1e9422e
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/aidl/README.md
@@ -0,0 +1 @@
+This is where the RemoteAuth AIDL files will go
\ No newline at end of file
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
new file mode 100644
index 0000000..32ae54f
--- /dev/null
+++ b/remoteauth/service/Android.bp
@@ -0,0 +1,76 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "remoteauth-service-srcs",
+    srcs: [],
+}
+
+// Main lib for remoteauth services.
+java_library {
+    name: "service-remoteauth-pre-jarjar",
+    srcs: [":remoteauth-service-srcs"],
+    required: ["libremoteauth_jni_rust"],
+    defaults: [
+        "enable-remoteauth-targets",
+        "framework-system-server-module-defaults",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-bluetooth",
+        "framework-connectivity",
+        "error_prone_annotations",
+        "framework-configinfrastructure",
+        "framework-connectivity-pre-jarjar",
+        "framework-connectivity-t-pre-jarjar",
+        "framework-statsd",
+    ],
+    static_libs: [
+        "modules-utils-build",
+        "modules-utils-handlerexecutor",
+        "modules-utils-preconditions",
+        "modules-utils-backgroundthread",
+        "uwb_androidx_backend",
+    ],
+    sdk_version: "system_server_current",
+    // 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",
+
+    dex_preopt: {
+        enabled: false,
+        app_image: false,
+    },
+    visibility: [
+        "//packages/modules/RemoteAuth/apex",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
+genrule {
+    name: "statslog-remoteauth-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module remoteauth " +
+        " --javaPackage com.android.server.remoteauth.proto --javaClass RemoteAuthStatsLog" +
+        " --minApiLevel 33",
+    out: ["com/android/server/remoteauth/proto/RemoteAuthStatsLog.java"],
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/README.md b/remoteauth/service/java/com/android/server/remoteauth/README.md
new file mode 100644
index 0000000..b659bf7
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/README.md
@@ -0,0 +1,10 @@
+This is the source root for the RemoteAuthService
+
+## Connectivity
+Provides the connectivity manager to manage connections with the peer device.
+
+## Ranging
+Provides the ranging manager to perform ranging with the peer devices.
+
+## Util
+Common utilities.
diff --git a/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthConnectionCache.java b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthConnectionCache.java
new file mode 100644
index 0000000..49481a2
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthConnectionCache.java
@@ -0,0 +1,141 @@
+/*
+ * 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.remoteauth;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.remoteauth.connectivity.Connection;
+import com.android.server.remoteauth.connectivity.ConnectionException;
+import com.android.server.remoteauth.connectivity.ConnectionInfo;
+import com.android.server.remoteauth.connectivity.ConnectivityManager;
+import com.android.server.remoteauth.connectivity.EventListener;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manages caching of remote devices {@link ConnectionInfo} and {@link Connection}.
+ *
+ * <p>Allows mapping between {@link ConnectionInfo#getConnectionId()} to {@link ConnectionInfo} and
+ * {@link Connection}
+ */
+public class RemoteAuthConnectionCache {
+    public static final String TAG = "RemoteAuthConCache";
+    private final Map<Integer, ConnectionInfo> mConnectionInfoMap = new ConcurrentHashMap<>();
+    private final Map<Integer, Connection> mConnectionMap = new ConcurrentHashMap<>();
+
+    private final ConnectivityManager mConnectivityManager;
+
+    public RemoteAuthConnectionCache(@NonNull ConnectivityManager connectivityManager) {
+        Preconditions.checkNotNull(connectivityManager);
+        this.mConnectivityManager = connectivityManager;
+    }
+
+    /** Returns the {@link ConnectivityManager}. */
+    ConnectivityManager getConnectivityManager() {
+        return mConnectivityManager;
+    }
+
+    /**
+     * Associates the connectionId with {@link ConnectionInfo}. Updates association with new value
+     * if already exists
+     *
+     * @param connectionInfo of the remote device
+     */
+    public void setConnectionInfo(@NonNull ConnectionInfo connectionInfo) {
+        Preconditions.checkNotNull(connectionInfo);
+        mConnectionInfoMap.put(connectionInfo.getConnectionId(), connectionInfo);
+    }
+
+    /** Returns {@link ConnectionInfo} associated with connectionId. */
+    public ConnectionInfo getConnectionInfo(int connectionId) {
+        return mConnectionInfoMap.get(connectionId);
+    }
+
+    /**
+     * Associates the connectionId with {@link Connection}. Updates association with new value if
+     * already exists
+     *
+     * @param connection to the remote device
+     */
+    public void setConnection(@NonNull Connection connection) {
+        Preconditions.checkNotNull(connection);
+        mConnectionMap.put(connection.getConnectionInfo().getConnectionId(), connection);
+    }
+
+    /**
+     * Returns {@link Connection} associated with connectionId. Uses {@link ConnectivityManager} to
+     * create and associate with new {@link Connection}, if mapping doesn't exist
+     *
+     * @param connectionId of the remote device
+     */
+    public Connection getConnection(int connectionId) {
+        return mConnectionMap.computeIfAbsent(
+                connectionId,
+                id -> {
+                    ConnectionInfo connectionInfo = getConnectionInfo(id);
+                    if (null == connectionInfo) {
+                        // TODO: Try accessing DB to fetch by connectionId
+                        Log.e(TAG, String.format("Unknown connectionId: %d", connectionId));
+                        return null;
+                    }
+                    try {
+                        Connection connection =
+                                mConnectivityManager.connect(
+                                        connectionInfo,
+                                        new EventListener() {
+                                            @Override
+                                            public void onDisconnect(
+                                                    @NonNull ConnectionInfo connectionInfo) {
+                                                removeConnection(connectionInfo.getConnectionId());
+                                                Log.i(
+                                                        TAG,
+                                                        String.format(
+                                                                "Disconnected from: %d",
+                                                                connectionInfo.getConnectionId()));
+                                            }
+                                        });
+                        if (null == connection) {
+                            Log.e(TAG, String.format("Failed to connect: %d", connectionId));
+                            return null;
+                        }
+                        return connection;
+                    } catch (ConnectionException e) {
+                        Log.e(
+                                TAG,
+                                String.format("Failed to create connection to %d.", connectionId),
+                                e);
+                        return null;
+                    }
+                });
+    }
+
+    /**
+     * Removes {@link Connection} from cache.
+     *
+     * @param connectionId of the remote device
+     */
+    public void removeConnection(int connectionId) {
+        if (null != mConnectionMap.remove(connectionId)) {
+            Log.i(
+                    TAG,
+                    String.format("Connection associated with id: %d was removed", connectionId));
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthPlatform.java b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthPlatform.java
new file mode 100644
index 0000000..a61aeff
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthPlatform.java
@@ -0,0 +1,65 @@
+/*
+ * 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.remoteauth;
+
+import android.util.Log;
+
+import com.android.server.remoteauth.connectivity.Connection;
+import com.android.server.remoteauth.jni.INativeRemoteAuthService;
+
+/** Implementation of the {@link INativeRemoteAuthService.IPlatform} interface. */
+public class RemoteAuthPlatform implements INativeRemoteAuthService.IPlatform {
+    public static final String TAG = "RemoteAuthPlatform";
+    private final RemoteAuthConnectionCache mConnectionCache;
+
+    public RemoteAuthPlatform(RemoteAuthConnectionCache connectionCache) {
+        mConnectionCache = connectionCache;
+    }
+
+    /**
+     * Sends message to the remote device via {@link Connection} created by
+     * {@link com.android.server.remoteauth.connectivity.ConnectivityManager}.
+     *
+     * @param connectionId connection ID of the {@link android.remoteauth.RemoteAuthenticator}
+     * @param request payload of the request
+     * @param callback to be used to pass the response result
+     * @return true if succeeded, false otherwise.
+     * @hide
+     */
+    @Override
+    public boolean sendRequest(int connectionId, byte[] request, ResponseCallback callback) {
+        Connection connection = mConnectionCache.getConnection(connectionId);
+        if (null == connection) {
+            Log.e(TAG, String.format("Failed to get a connection for: %d", connectionId));
+            return false;
+        }
+        connection.sendRequest(
+                request,
+                new Connection.MessageResponseCallback() {
+                    @Override
+                    public void onSuccess(byte[] buffer) {
+                        callback.onSuccess(buffer);
+                    }
+
+                    @Override
+                    public void onFailure(@Connection.ErrorCode int errorCode) {
+                        callback.onFailure(errorCode);
+                    }
+                });
+        return true;
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java
new file mode 100644
index 0000000..619785f
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 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.remoteauth;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.remoteauth.IDeviceDiscoveryListener;
+import android.remoteauth.IRemoteAuthService;
+
+import com.android.internal.util.Preconditions;
+
+/** Service implementing remoteauth functionality. */
+public class RemoteAuthService extends IRemoteAuthService.Stub {
+    public static final String TAG = "RemoteAuthService";
+    public static final String SERVICE_NAME = Context.REMOTE_AUTH_SERVICE;
+
+    public RemoteAuthService(Context context) {
+        Preconditions.checkNotNull(context);
+        // TODO(b/290280702): Create here RemoteConnectivityManager and RangingManager
+    }
+
+    @Override
+    public boolean isRemoteAuthSupported() {
+        // TODO(b/297301535): checkPermission(mContext, MANAGE_REMOTE_AUTH);
+        // TODO(b/290676192): integrate with RangingManager
+        //  (check if UWB is supported by this device)
+        return true;
+    }
+
+    @Override
+    public boolean registerDiscoveryListener(
+            IDeviceDiscoveryListener deviceDiscoveryListener,
+            @UserIdInt int userId,
+            int timeoutMs,
+            String packageName,
+            @Nullable String attributionTag) {
+        // TODO(b/297301535): checkPermission(mContext, MANAGE_REMOTE_AUTH);
+        // TODO(b/290280702): implement register discovery logic
+        return true;
+    }
+
+    @Override
+    public void unregisterDiscoveryListener(
+            IDeviceDiscoveryListener deviceDiscoveryListener,
+            @UserIdInt int userId,
+            String packageName,
+            @Nullable String attributionTag) {
+        // TODO(b/297301535): checkPermission(mContext, MANAGE_REMOTE_AUTH);
+        // TODO(b/290094221): implement unregister logic
+    }
+
+    private static void checkPermission(Context context, String permission) {
+        context.enforceCallingOrSelfPermission(
+                permission, "Must have " + permission + " permission.");
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java
new file mode 100644
index 0000000..d2a828c
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java
@@ -0,0 +1,109 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.TargetApi;
+import android.companion.AssociationInfo;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates the connection information for companion device manager connections.
+ *
+ * <p>Connection information captures the details of underlying connection such as connection id,
+ * type of connection and peer device mac address.
+ */
+// TODO(b/295407748): Change to use @DataClass.
+// TODO(b/296625303): Change to VANILLA_ICE_CREAM when AssociationInfo is available in V.
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public final class CdmConnectionInfo extends ConnectionInfo<AssociationInfo> {
+    @NonNull private final AssociationInfo mAssociationInfo;
+
+    public CdmConnectionInfo(int connectionId, @NonNull AssociationInfo associationInfo) {
+        super(connectionId);
+        mAssociationInfo = associationInfo;
+    }
+
+    private CdmConnectionInfo(@NonNull Parcel in) {
+        super(in);
+        mAssociationInfo = in.readTypedObject(AssociationInfo.CREATOR);
+    }
+
+    /** Used to read CdmConnectionInfo from a Parcel */
+    @NonNull
+    public static final Parcelable.Creator<CdmConnectionInfo> CREATOR =
+            new Parcelable.Creator<CdmConnectionInfo>() {
+                public CdmConnectionInfo createFromParcel(@NonNull Parcel in) {
+                    return new CdmConnectionInfo(in);
+                }
+
+                public CdmConnectionInfo[] newArray(int size) {
+                    return new CdmConnectionInfo[size];
+                }
+            };
+
+    /** No special parcel contents. */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Flatten this CdmConnectionInfo in to a Parcel.
+     *
+     * @param out The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        super.writeToParcel(out, flags);
+        out.writeTypedObject(mAssociationInfo, 0);
+    }
+
+    /** Returns a string representation of ConnectionInfo. */
+    @Override
+    public String toString() {
+        return super.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null || !(o instanceof CdmConnectionInfo)) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        CdmConnectionInfo other = (CdmConnectionInfo) o;
+        return super.equals(o) && mAssociationInfo.equals(other.getConnectionParams());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAssociationInfo);
+    }
+
+    @Override
+    public AssociationInfo getConnectionParams() {
+        return mAssociationInfo;
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectivityManager.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectivityManager.java
new file mode 100644
index 0000000..49745c0
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectivityManager.java
@@ -0,0 +1,201 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import static com.android.server.remoteauth.connectivity.DiscoveryFilter.DeviceType;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TargetApi;
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
+import android.os.Build;
+import android.util.Log;
+
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * Discovers devices associated with the companion device manager.
+ *
+ * TODO(b/296625303): Change to VANILLA_ICE_CREAM when AssociationInfo is available in V.
+ */
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class CdmConnectivityManager implements ConnectivityManager {
+    private static final String TAG = "CdmConnectivityManager";
+
+    private final CompanionDeviceManagerWrapper mCompanionDeviceManagerWrapper;
+
+    private ExecutorService mExecutor;
+    private Map<DiscoveredDeviceReceiver, Future> mPendingDiscoveryCallbacks =
+            new ConcurrentHashMap<>();
+
+    public CdmConnectivityManager(
+            @NonNull ExecutorService executor,
+            @NonNull CompanionDeviceManagerWrapper companionDeviceManagerWrapper) {
+        mExecutor = executor;
+        mCompanionDeviceManagerWrapper = companionDeviceManagerWrapper;
+    }
+
+    /**
+     * Runs associated discovery callbacks for discovered devices.
+     *
+     * @param discoveredDeviceReceiver callback.
+     * @param device discovered device.
+     */
+    private void notifyOnDiscovered(
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver,
+            @NonNull DiscoveredDevice device) {
+        Preconditions.checkNotNull(discoveredDeviceReceiver);
+        Preconditions.checkNotNull(device);
+
+        Log.i(TAG, "Notifying discovered device");
+        discoveredDeviceReceiver.onDiscovered(device);
+    }
+
+    /**
+     * Posts an async task to discover CDM associations and run callback if device is discovered.
+     *
+     * @param discoveryFilter filter for association.
+     * @param discoveredDeviceReceiver callback.
+     */
+    private void startDiscoveryAsync(@NonNull DiscoveryFilter discoveryFilter,
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) {
+        Preconditions.checkNotNull(discoveryFilter);
+        Preconditions.checkNotNull(discoveredDeviceReceiver);
+
+        List<AssociationInfo> associations = mCompanionDeviceManagerWrapper.getAllAssociations();
+        Log.i(TAG, "Found associations: " + associations.size());
+        for (AssociationInfo association : associations) {
+            String deviceProfile = getDeviceProfileFromType(discoveryFilter.getDeviceType());
+            // TODO(b/297574984): Include device presence signal before notifying discovery result.
+            if (mCompanionDeviceManagerWrapper.getDeviceProfile(association)
+                    .equals(deviceProfile)) {
+                notifyOnDiscovered(
+                        discoveredDeviceReceiver,
+                        createDiscoveredDeviceFromAssociation(association));
+            }
+        }
+    }
+
+    /**
+     * Returns the device profile from association info.
+     *
+     * @param deviceType Discovery filter device type.
+     * @return Device profile string defined in {@link AssociationRequest}.
+     * @throws AssertionError if type cannot be mapped.
+     */
+    private String getDeviceProfileFromType(@DeviceType int deviceType) {
+        if (deviceType == DiscoveryFilter.WATCH) {
+            return AssociationRequest.DEVICE_PROFILE_WATCH;
+        } else {
+            // Should not reach here.
+            throw new AssertionError(deviceType);
+        }
+    }
+
+    /**
+     * Creates discovered device from association info.
+     *
+     * @param info Association info.
+     * @return discovered device object.
+     */
+    private @NonNull DiscoveredDevice createDiscoveredDeviceFromAssociation(
+            @NonNull AssociationInfo info) {
+        return new DiscoveredDevice(
+                new CdmConnectionInfo(info.getId(), info),
+                info.getDisplayName() == null ? "" : info.getDisplayName().toString());
+    }
+
+    /**
+     * Triggers the discovery for CDM associations.
+     *
+     * Runs discovery only if a callback has not been previously registered.
+     *
+     * @param discoveryFilter filter for associations.
+     * @param discoveredDeviceReceiver callback to be run on discovery result.
+     */
+    @Override
+    public void startDiscovery(
+            @NonNull DiscoveryFilter discoveryFilter,
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) {
+        Preconditions.checkNotNull(mCompanionDeviceManagerWrapper);
+        Preconditions.checkNotNull(discoveryFilter);
+        Preconditions.checkNotNull(discoveredDeviceReceiver);
+
+        try {
+            mPendingDiscoveryCallbacks.computeIfAbsent(
+                    discoveredDeviceReceiver,
+                    discoveryFuture -> mExecutor.submit(
+                        () -> startDiscoveryAsync(discoveryFilter, discoveryFuture),
+                        /* result= */ null));
+        } catch (RejectedExecutionException | NullPointerException e) {
+            Log.e(TAG, "Failed to start async discovery: " + e.getMessage());
+        }
+    }
+
+    /** Stops discovery. */
+    @Override
+    public void stopDiscovery(
+            @NonNull DiscoveryFilter discoveryFilter,
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) {
+        Preconditions.checkNotNull(discoveryFilter);
+        Preconditions.checkNotNull(discoveredDeviceReceiver);
+
+        Future<Void> discoveryFuture = mPendingDiscoveryCallbacks.remove(discoveredDeviceReceiver);
+        if (null != discoveryFuture && !discoveryFuture.cancel(/* mayInterruptIfRunning= */ true)) {
+            Log.d(TAG, "Discovery was possibly completed.");
+        }
+    }
+
+    @Nullable
+    @Override
+    public Connection connect(@NonNull ConnectionInfo connectionInfo,
+            @NonNull EventListener eventListener) {
+        // Not implemented.
+        return null;
+    }
+
+    @Override
+    public void startListening(MessageReceiver messageReceiver) {
+        // Not implemented.
+    }
+
+    @Override
+    public void stopListening(MessageReceiver messageReceiver) {
+        // Not implemented.
+    }
+
+    /**
+     * Returns whether the callback is already registered and pending.
+     *
+     * @param discoveredDeviceReceiver callback
+     * @return true if the callback is pending, false otherwise.
+     */
+    @VisibleForTesting
+    boolean hasPendingCallbacks(@NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) {
+        return mPendingDiscoveryCallbacks.containsKey(discoveredDeviceReceiver);
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CompanionDeviceManagerWrapper.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CompanionDeviceManagerWrapper.java
new file mode 100644
index 0000000..eaf3edb
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CompanionDeviceManagerWrapper.java
@@ -0,0 +1,77 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TargetApi;
+import android.companion.AssociationInfo;
+import android.companion.CompanionDeviceManager;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import java.util.List;
+
+/** Wraps {@link android.companion.CompanionDeviceManager} for easier testing. */
+// TODO(b/296625303): Change to VANILLA_ICE_CREAM when AssociationInfo is available in V.
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class CompanionDeviceManagerWrapper {
+    private static final String TAG = "CompanionDeviceManagerWrapper";
+
+    private Context mContext;
+    private CompanionDeviceManager mCompanionDeviceManager;
+
+    public CompanionDeviceManagerWrapper(@NonNull Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Returns device profile string from the association info.
+     *
+     * @param associationInfo the association info.
+     * @return String indicating device profile
+     */
+    @Nullable
+    public String getDeviceProfile(@NonNull AssociationInfo associationInfo) {
+        return associationInfo.getDeviceProfile();
+    }
+
+    /**
+     * Returns all associations.
+     *
+     * @return associations or null if no associated devices present.
+     */
+    @Nullable
+    public List<AssociationInfo> getAllAssociations() {
+        if (mCompanionDeviceManager == null) {
+            try {
+                mCompanionDeviceManager = mContext.getSystemService(CompanionDeviceManager.class);
+            } catch (NullPointerException e) {
+                Log.e(TAG, "CompanionDeviceManager service does not exist: " + e);
+                return null;
+            }
+        }
+
+        try {
+            return mCompanionDeviceManager.getAllAssociations();
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Failed to get CompanionDeviceManager associations: " + e.getMessage());
+        }
+        return null;
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java
new file mode 100644
index 0000000..dca8b9f
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java
@@ -0,0 +1,85 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A connection with the peer device.
+ *
+ * <p>Connections are used to exchange data with the peer device.
+ */
+public interface Connection {
+    /** Unknown error. */
+    int ERROR_UNKNOWN = 0;
+
+    /** Message was sent successfully. */
+    int ERROR_OK = 1;
+
+    /** Timeout occurred while waiting for response from the peer. */
+    int ERROR_DEADLINE_EXCEEDED = 2;
+
+    /** Device became unavailable while sending the message. */
+    int ERROR_DEVICE_UNAVAILABLE = 3;
+
+    /** Represents error code. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ERROR_UNKNOWN, ERROR_OK, ERROR_DEADLINE_EXCEEDED, ERROR_DEVICE_UNAVAILABLE})
+    @interface ErrorCode {}
+
+    /**
+     * Callback for clients to get the response of sendRequest. {@link onSuccess} is called if the
+     * peer device responds with Status::OK, otherwise runs the {@link onFailure} callback.
+     */
+    abstract class MessageResponseCallback {
+        /**
+         * Called on a success.
+         *
+         * @param buffer response from the device.
+         */
+        public void onSuccess(byte[] buffer) {}
+
+        /**
+         * Called when message sending fails.
+         *
+         * @param errorCode indicating the error.
+         */
+        public void onFailure(@ErrorCode int errorCode) {}
+    }
+
+    /**
+     * Sends a request to the peer.
+     *
+     * @param request byte array to be sent to the peer device.
+     * @param messageResponseCallback callback to be run when the peer response is received or if an
+     *     error occurred.
+     */
+    void sendRequest(byte[] request, MessageResponseCallback messageResponseCallback);
+
+    /** Triggers a disconnect from the peer device. */
+    void disconnect();
+
+    /**
+     * Returns the connection information.
+     *
+     * @return A connection information object.
+     */
+    ConnectionInfo getConnectionInfo();
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionException.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionException.java
new file mode 100644
index 0000000..a8c7860
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionException.java
@@ -0,0 +1,56 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import static com.android.server.remoteauth.connectivity.ConnectivityManager.ReasonCode;
+
+import android.annotation.Nullable;
+
+/** Exception that signals that the connection request failed. */
+public final class ConnectionException extends RuntimeException {
+    private final @ReasonCode int mReasonCode;
+
+    public ConnectionException(@ReasonCode int reasonCode) {
+        super();
+        this.mReasonCode = reasonCode;
+    }
+
+    public ConnectionException(@ReasonCode int reasonCode, @Nullable String message) {
+        super(message);
+        this.mReasonCode = reasonCode;
+    }
+
+    public ConnectionException(@ReasonCode int reasonCode, @Nullable Throwable cause) {
+        super(cause);
+        this.mReasonCode = reasonCode;
+    }
+
+    public ConnectionException(
+            @ReasonCode int reasonCode, @Nullable String message, @Nullable Throwable cause) {
+        super(message, cause);
+        this.mReasonCode = reasonCode;
+    }
+
+    public @ReasonCode int getReasonCode() {
+        return this.mReasonCode;
+    }
+
+    @Override
+    public String getMessage() {
+        return super.getMessage() + " Reason code: " + this.mReasonCode;
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java
new file mode 100644
index 0000000..6e0edb4
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java
@@ -0,0 +1,94 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates the connection information.
+ *
+ * <p>Connection information captures the details of underlying connection such as connection id,
+ * type of connection and peer device mac address.
+ *
+ * @param <T> connection params per connection type.
+ */
+// TODO(b/295407748) Change to use @DataClass.
+public abstract class ConnectionInfo<T> implements Parcelable {
+    int mConnectionId;
+
+    public ConnectionInfo(int connectionId) {
+        mConnectionId = connectionId;
+    }
+
+    /** Create object from Parcel */
+    public ConnectionInfo(@NonNull Parcel in) {
+        mConnectionId = in.readInt();
+    }
+
+    public int getConnectionId() {
+        return mConnectionId;
+    }
+
+    /** No special parcel contents. */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Flattens this ConnectionInfo in to a Parcel.
+     *
+     * @param out The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeInt(mConnectionId);
+    }
+
+    /** Returns string representation of ConnectionInfo. */
+    @Override
+    public String toString() {
+        return "ConnectionInfo[" + "connectionId= " + mConnectionId + "]";
+    }
+
+    /** Returns true if this ConnectionInfo object is equal to the other. */
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof ConnectionInfo)) {
+            return false;
+        }
+
+        ConnectionInfo other = (ConnectionInfo) o;
+        return mConnectionId == other.getConnectionId();
+    }
+
+    /** Returns the hashcode of this object */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mConnectionId);
+    }
+
+    /**
+     * Returns connection related parameters.
+     */
+    public abstract T getConnectionParams();
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java
new file mode 100644
index 0000000..7b30285
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java
@@ -0,0 +1,117 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Performs discovery and triggers a connection to an associated device. */
+public interface ConnectivityManager {
+    /**
+     * Starts device discovery.
+     *
+     * <p>Discovery continues until stopped using {@link stopDiscovery} or times out.
+     *
+     * @param discoveryFilter to filter for devices during discovery.
+     * @param discoveredDeviceReceiver callback to run when device is found or lost.
+     */
+    void startDiscovery(
+            @NonNull DiscoveryFilter discoveryFilter,
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver);
+
+    /**
+     * Stops device discovery.
+     *
+     * @param discoveryFilter filter used to start discovery.
+     * @param discoveredDeviceReceiver callback passed with startDiscovery.
+     */
+    void stopDiscovery(
+            @NonNull DiscoveryFilter discoveryFilter,
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver);
+
+    /** Unknown reason for connection failure. */
+    int ERROR_REASON_UNKNOWN = 0;
+
+    /** Indicates that the connection request timed out. */
+    int ERROR_CONNECTION_TIMED_OUT = 1;
+
+    /** Indicates that the connection request was refused by the peer. */
+    int ERROR_CONNECTION_REFUSED = 2;
+
+    /** Indicates that the peer device was unavailable. */
+    int ERROR_DEVICE_UNAVAILABLE = 3;
+
+    /** Reason codes for connect failure. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+        ERROR_REASON_UNKNOWN,
+        ERROR_CONNECTION_TIMED_OUT,
+        ERROR_CONNECTION_REFUSED,
+        ERROR_DEVICE_UNAVAILABLE
+    })
+    @interface ReasonCode {}
+
+    /**
+     * Initiates a connection with the peer device.
+     *
+     * @param connectionInfo of the device discovered using {@link startDiscovery}.
+     * @param eventListener to listen for events from the underlying transport.
+     * @return {@link Connection} object or null connection is not established.
+     * @throws ConnectionException in case connection cannot be established.
+     */
+    @Nullable
+    Connection connect(
+            @NonNull ConnectionInfo connectionInfo, @NonNull EventListener eventListener);
+
+    /**
+     * Message received callback.
+     *
+     * <p>Clients should implement this callback to receive messages from the peer device.
+     */
+    abstract class MessageReceiver {
+        /**
+         * Receive message from the peer device.
+         *
+         * <p>Clients can set empty buffer as an ACK to the request.
+         *
+         * @param messageIn message from peer device.
+         * @param responseCallback {@link ResponseCallback} callback to send the response back.
+         */
+        public void onMessageReceived(byte[] messageIn, ResponseCallback responseCallback) {}
+    }
+
+    /**
+     * Starts listening for incoming messages.
+     *
+     * <p>Runs MessageReceiver callback when a message is received.
+     *
+     * @param messageReceiver to receive messages.
+     * @throws AssertionError if a listener is already configured.
+     */
+    void startListening(MessageReceiver messageReceiver);
+
+    /**
+     * Stops listening to incoming messages.
+     *
+     * @param messageReceiver to receive messages.
+     */
+    void stopListening(MessageReceiver messageReceiver);
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManagerFactory.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManagerFactory.java
new file mode 100644
index 0000000..3407fca
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManagerFactory.java
@@ -0,0 +1,51 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Factory class to create different types of connectivity managers based on the underlying
+ * network transports (for example CompanionDeviceManager).
+ */
+public final class ConnectivityManagerFactory {
+    private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();
+
+    /**
+     * Creates and returns a ConnectivityManager object depending on connection type.
+     *
+     * @param context of the caller.
+     * @return ConnectivityManager object.
+     */
+    public static ConnectivityManager getConnectivityManager(@NonNull Context context) {
+        Preconditions.checkNotNull(context);
+
+        // For now, we only have one case, but ideally this should create a new type based on some
+        // feature flag.
+        return new CdmConnectivityManager(EXECUTOR, new CompanionDeviceManagerWrapper(
+                new WeakReference<>(context.getApplicationContext()).get()));
+    }
+
+    private ConnectivityManagerFactory() {}
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java
new file mode 100644
index 0000000..8cad3eb
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Objects;
+
+/** Device discovered on a network interface like Bluetooth. */
+public final class DiscoveredDevice {
+    private @NonNull ConnectionInfo mConnectionInfo;
+    private @Nullable String mDisplayName;
+
+    public DiscoveredDevice(@NonNull ConnectionInfo connectionInfo, @Nullable String displayName) {
+        this.mConnectionInfo = connectionInfo;
+        this.mDisplayName = displayName;
+    }
+
+    /**
+     * Returns connection information.
+     *
+     * @return connection information.
+     */
+    @NonNull
+    public ConnectionInfo getConnectionInfo() {
+        return this.mConnectionInfo;
+    }
+
+    /**
+     * Returns display name of the device.
+     *
+     * @return display name string.
+     */
+    @Nullable
+    public String getDisplayName() {
+        return this.mDisplayName;
+    }
+
+    /**
+     * Checks for equality between this and other object.
+     *
+     * @return true if equal, false otherwise.
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof DiscoveredDevice)) {
+            return false;
+        }
+
+        DiscoveredDevice other = (DiscoveredDevice) o;
+        return mConnectionInfo.equals(other.getConnectionInfo())
+                && mDisplayName.equals(other.getDisplayName());
+    }
+
+    /**
+     * Returns hash code of the object.
+     *
+     * @return hash code.
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mDisplayName, mConnectionInfo.getConnectionId());
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDeviceReceiver.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDeviceReceiver.java
new file mode 100644
index 0000000..90a3e30
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDeviceReceiver.java
@@ -0,0 +1,34 @@
+/*
+ * 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.remoteauth.connectivity;
+
+/** Callbacks triggered on discovery. */
+public abstract class DiscoveredDeviceReceiver {
+    /**
+     * Callback called when a device matching the discovery filter is found.
+     *
+     * @param discoveredDevice the discovered device.
+     */
+    public void onDiscovered(DiscoveredDevice discoveredDevice) {}
+
+    /**
+     * Callback called when a previously discovered device using {@link
+     * ConnectivityManager#startDiscovery} is lost.
+     *
+     * @param discoveredDevice the lost device
+     */
+    public void onLost(DiscoveredDevice discoveredDevice) {}
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java
new file mode 100644
index 0000000..3590f20
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.remoteauth.connectivity;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Filter for device discovery.
+ *
+ * <p>Callers can use this class to provide a discovery filter to the {@link
+ * ConnectivityManager.startDiscovery} method. A device is discovered if it matches at least one of
+ * the filter criteria (device type, name or peer address).
+ */
+public final class DiscoveryFilter {
+    /** Device type WATCH. */
+    public static final int WATCH = 0;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({WATCH})
+    public @interface DeviceType {}
+
+    private @DeviceType int mDeviceType;
+    private final @Nullable String mDeviceName;
+    private final @Nullable String mPeerAddress;
+
+    public DiscoveryFilter(
+            @DeviceType int deviceType, @Nullable String deviceName, @Nullable String peerAddress) {
+        this.mDeviceType = deviceType;
+        this.mDeviceName = deviceName;
+        this.mPeerAddress = peerAddress;
+    }
+
+    /**
+     * Returns device type.
+     *
+     * @return device type.
+     */
+    public @DeviceType int getDeviceType() {
+        return this.mDeviceType;
+    }
+
+    /**
+     * Returns device name.
+     *
+     * @return device name.
+     */
+    public @Nullable String getDeviceName() {
+        return this.mDeviceName;
+    }
+
+    /**
+     * Returns mac address of the peer device .
+     *
+     * @return mac address string.
+     */
+    public @Nullable String getPeerAddress() {
+        return this.mPeerAddress;
+    }
+
+    /** Builder for {@link DiscoverFilter} */
+    public static class Builder {
+        private @DeviceType int mDeviceType;
+        private @Nullable String mDeviceName;
+        private @Nullable String mPeerAddress;
+
+        private Builder() {}
+
+        /** Static method to create a new builder */
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        /**
+         * Sets the device type of the DiscoveryFilter.
+         *
+         * @param deviceType of the peer device.
+         */
+        @NonNull
+        public Builder setDeviceType(@DeviceType int deviceType) {
+            mDeviceType = deviceType;
+            return this;
+        }
+
+        /**
+         * Sets the device name.
+         *
+         * @param deviceName May be null.
+         */
+        @NonNull
+        public Builder setDeviceName(String deviceName) {
+            mDeviceName = deviceName;
+            return this;
+        }
+
+        /**
+         * Sets the peer address.
+         *
+         * @param peerAddress Mac address of the peer device.
+         */
+        @NonNull
+        public Builder setPeerAddress(String peerAddress) {
+            mPeerAddress = peerAddress;
+            return this;
+        }
+
+        /** Builds the DiscoveryFilter object. */
+        @NonNull
+        public DiscoveryFilter build() {
+            return new DiscoveryFilter(this.mDeviceType, this.mDeviceName, this.mPeerAddress);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java
new file mode 100644
index 0000000..eaf57e0
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+
+/** Listens to the events from underlying transport. */
+public interface EventListener {
+    /** Called when remote device is disconnected from the underlying transport. */
+    void onDisconnect(@NonNull ConnectionInfo connectionInfo);
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ResponseCallback.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ResponseCallback.java
new file mode 100644
index 0000000..8a09ab3
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ResponseCallback.java
@@ -0,0 +1,63 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+
+/**
+ * Abstract class to expose a callback for clients to send a response to the peer device.
+ *
+ * <p>When a device receives a message on a connection, this object is constructed using the
+ * connection information of the connection and the message id from the incoming message. This
+ * object is forwarded to the clients of the connection to allow them to send a response to the peer
+ * device.
+ */
+public abstract class ResponseCallback {
+    private final long mMessageId;
+    private final ConnectionInfo mConnectionInfo;
+
+    public ResponseCallback(long messageId, @NonNull ConnectionInfo connectionInfo) {
+        mMessageId = messageId;
+        mConnectionInfo = connectionInfo;
+    }
+
+    /**
+     * Returns message id identifying the message.
+     *
+     * @return message id of this message.
+     */
+    public long getMessageId() {
+        return mMessageId;
+    }
+
+    /**
+     * Returns connection info from the response.
+     *
+     * @return connection info.
+     */
+    @NonNull
+    public ConnectionInfo getConnectionInfo() {
+        return mConnectionInfo;
+    }
+
+    /**
+     * Callback to send a response to the peer device.
+     *
+     * @param response buffer to send to the peer device.
+     */
+    public void onResponse(@NonNull byte[] response) {}
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/jni/INativeRemoteAuthService.java b/remoteauth/service/java/com/android/server/remoteauth/jni/INativeRemoteAuthService.java
new file mode 100644
index 0000000..4aaa760
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/jni/INativeRemoteAuthService.java
@@ -0,0 +1,65 @@
+/*
+ * 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.remoteauth.jni;
+
+/**
+ * Interface defining a proxy between Rust and Java implementation of RemoteAuth protocol.
+ *
+ * @hide
+ */
+public interface INativeRemoteAuthService {
+    /**
+     * Interface for RemoteAuth PAL
+     *
+     * @hide
+     */
+    interface IPlatform {
+        /**
+         * Sends message to the remote authenticator
+         *
+         * @param connectionId connection ID of the {@link android.remoteauth.RemoteAuthenticator}
+         * @param request payload of the request
+         * @param callback to be used to pass the response result
+         * @return true if succeeded, false otherwise.
+         * @hide
+         */
+        boolean sendRequest(int connectionId, byte[] request, ResponseCallback callback);
+
+        /**
+         * Interface for a callback to send a response back.
+         *
+         * @hide
+         */
+        interface ResponseCallback {
+            /**
+             * Invoked when message sending succeeds.
+             *
+             * @param response contains response
+             * @hide
+             */
+            void onSuccess(byte[] response);
+
+            /**
+             * Invoked when message sending fails.
+             *
+             * @param errorCode indicating the error
+             * @hide
+             */
+            void onFailure(int errorCode);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/jni/NativeRemoteAuthService.java b/remoteauth/service/java/com/android/server/remoteauth/jni/NativeRemoteAuthService.java
new file mode 100644
index 0000000..a676562
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/jni/NativeRemoteAuthService.java
@@ -0,0 +1,95 @@
+/*
+ * 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.remoteauth.jni;
+
+import android.util.Log;
+
+import com.android.internal.annotations.Keep;
+import com.android.server.remoteauth.jni.INativeRemoteAuthService.IPlatform;
+
+/**
+ * A service providing a proxy between Rust implementation and {@link
+ * com.android.server.remoteauth.RemoteAuthService}.
+ *
+ * @hide
+ */
+public class NativeRemoteAuthService {
+    private static final String TAG = NativeRemoteAuthService.class.getSimpleName();
+
+    private IPlatform mPlatform;
+    public final Object mNativeLock = new Object();
+
+    // Constructor should receive pointers to:
+    // ConnectivityManager, RangingManager and DB
+    public NativeRemoteAuthService() {
+        System.loadLibrary("remoteauth_jni_rust");
+        synchronized (mNativeLock) {
+            native_init();
+        }
+    }
+
+    public void setDeviceListener(final IPlatform platform) {
+        mPlatform = platform;
+    }
+
+    /**
+     * Sends message to the remote authenticator
+     *
+     * @param connectionId connection ID of the {@link android.remoteauth.RemoteAuthenticator}
+     * @param request payload of the request
+     * @param responseHandle a handle associated with the request, used to pass the response to the
+     *     platform
+     * @param platformHandle a handle associated with the platform object, used to pass the response
+     *     to the specific platform
+     * @hide
+     */
+    @Keep
+    public void sendRequest(
+            int connectionId, byte[] request, long responseHandle, long platformHandle) {
+        Log.d(TAG, String.format("sendRequest with connectionId: %d, rh: %d, ph: %d",
+                connectionId, responseHandle, platformHandle));
+        mPlatform.sendRequest(
+                connectionId,
+                request,
+                new IPlatform.ResponseCallback() {
+                    @Override
+                    public void onSuccess(byte[] response) {
+                        synchronized (mNativeLock) {
+                            native_on_send_request_success(
+                                    response, platformHandle, responseHandle);
+                        }
+                    }
+
+                    @Override
+                    public void onFailure(int errorCode) {
+                        synchronized (mNativeLock) {
+                            native_on_send_request_error(errorCode, platformHandle, responseHandle);
+                        }
+                    }
+                });
+    }
+
+    /* Native functions implemented in JNI */
+    // This function should be implemented in remoteauth_jni_android_protocol
+    private native boolean native_init();
+
+    private native void native_on_send_request_success(
+            byte[] appResponse, long platformHandle, long responseHandle);
+
+    private native void native_on_send_request_error(
+            int errorCode, long platformHandle, long responseHandle);
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/jni/PlatformBadHandleException.java b/remoteauth/service/java/com/android/server/remoteauth/jni/PlatformBadHandleException.java
new file mode 100644
index 0000000..4bf930c
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/jni/PlatformBadHandleException.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+/**
+ * Represents an unrecoverable error (invalid handle) that has occurred during accessing the
+ * platform.
+ */
+package com.android.server.remoteauth.jni;
+
+import com.android.internal.annotations.Keep;
+
+/**
+ * Exception thrown by native platform rust implementation of {@link
+ * com.android.server.remoteauth.RemoteAuthService}.
+ *
+ * @hide
+ */
+@Keep
+public class PlatformBadHandleException extends Exception {
+    public PlatformBadHandleException(final String message) {
+        super(message);
+    }
+
+    public PlatformBadHandleException(final Exception e) {
+        super(e);
+    }
+
+    public PlatformBadHandleException(final String message, final Exception e) {
+        super(message, e);
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingCapabilities.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingCapabilities.java
new file mode 100644
index 0000000..36aa585
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingCapabilities.java
@@ -0,0 +1,104 @@
+/*
+ * 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.remoteauth.ranging;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import androidx.annotation.IntDef;
+
+import com.google.common.collect.ImmutableList;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/** The ranging capabilities of the device. */
+public class RangingCapabilities {
+
+    /** Possible ranging methods */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                RANGING_METHOD_UNKNOWN,
+                RANGING_METHOD_UWB,
+            })
+    public @interface RangingMethod {}
+
+    /** Unknown ranging method. */
+    public static final int RANGING_METHOD_UNKNOWN = 0x0;
+
+    /** Ultra-wideband ranging. */
+    public static final int RANGING_METHOD_UWB = 0x1;
+
+    private final ImmutableList<Integer> mSupportedRangingMethods;
+    private final androidx.core.uwb.backend.impl.internal.RangingCapabilities
+            mUwbRangingCapabilities;
+
+    /**
+     * Gets the list of supported ranging methods of the device.
+     *
+     * @return list of {@link RangingMethod}
+     */
+    public ImmutableList<Integer> getSupportedRangingMethods() {
+        return mSupportedRangingMethods;
+    }
+
+    /**
+     * Gets the UWB ranging capabilities of the device.
+     *
+     * @return UWB ranging capabilities, null if UWB is not a supported {@link RangingMethod} in
+     *     {@link #getSupportedRangingMethods}.
+     */
+    @Nullable
+    public androidx.core.uwb.backend.impl.internal.RangingCapabilities getUwbRangingCapabilities() {
+        return mUwbRangingCapabilities;
+    }
+
+    private RangingCapabilities(
+            List<Integer> supportedRangingMethods,
+            androidx.core.uwb.backend.impl.internal.RangingCapabilities uwbRangingCapabilities) {
+        mSupportedRangingMethods = ImmutableList.copyOf(supportedRangingMethods);
+        mUwbRangingCapabilities = uwbRangingCapabilities;
+    }
+
+    /** Builder class for {@link RangingCapabilities}. */
+    public static final class Builder {
+        private List<Integer> mSupportedRangingMethods = new ArrayList<>();
+        private androidx.core.uwb.backend.impl.internal.RangingCapabilities mUwbRangingCapabilities;
+
+        /** Adds a supported {@link RangingMethod} */
+        public Builder addSupportedRangingMethods(@RangingMethod int rangingMethod) {
+            mSupportedRangingMethods.add(rangingMethod);
+            return this;
+        }
+
+        /** Sets the uwb ranging capabilities. */
+        public Builder setUwbRangingCapabilities(
+                @NonNull
+                        androidx.core.uwb.backend.impl.internal.RangingCapabilities
+                                uwbRangingCapabilities) {
+            mUwbRangingCapabilities = uwbRangingCapabilities;
+            return this;
+        }
+
+        /** Builds {@link RangingCapabilities}. */
+        public RangingCapabilities build() {
+            return new RangingCapabilities(mSupportedRangingMethods, mUwbRangingCapabilities);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingManager.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingManager.java
new file mode 100644
index 0000000..cf0db76
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingManager.java
@@ -0,0 +1,120 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static android.content.pm.PackageManager.FEATURE_UWB;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UNKNOWN;
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.uwb.backend.impl.internal.UwbFeatureFlags;
+import androidx.core.uwb.backend.impl.internal.UwbServiceImpl;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Manages the creation of generic device to device ranging session and obtaining device's ranging
+ * capabilities.
+ *
+ * <p>Out-of-band channel for ranging capabilities/parameters exchange is assumed being handled
+ * outside of this class.
+ */
+public class RangingManager {
+    private static final String TAG = "RangingManager";
+
+    private Context mContext;
+    @NonNull private RangingCapabilities mCachedRangingCapabilities;
+    @NonNull private UwbServiceImpl mUwbServiceImpl;
+
+    public RangingManager(@NonNull Context context) {
+        mContext = context;
+        if (mContext.getPackageManager().hasSystemFeature(FEATURE_UWB)) {
+            initiateUwb();
+        }
+    }
+
+    /**
+     * Shutdown and stop all listeners and tasks. After shutdown, RangingManager shall not be used
+     * anymore.
+     */
+    public void shutdown() {
+        if (mUwbServiceImpl != null) {
+            mUwbServiceImpl.shutdown();
+        }
+        Log.i(TAG, "shutdown");
+    }
+
+    /**
+     * Gets the {@link RangingCapabilities} of this device.
+     *
+     * @return RangingCapabilities.
+     */
+    @NonNull
+    public RangingCapabilities getRangingCapabilities() {
+        if (mCachedRangingCapabilities == null) {
+            RangingCapabilities.Builder builder = new RangingCapabilities.Builder();
+            if (mUwbServiceImpl != null) {
+                builder.addSupportedRangingMethods(RANGING_METHOD_UWB);
+                builder.setUwbRangingCapabilities(mUwbServiceImpl.getRangingCapabilities());
+            }
+            mCachedRangingCapabilities = builder.build();
+        }
+        return mCachedRangingCapabilities;
+    }
+
+    /**
+     * Creates a {@link RangingSession} based on the given {@link SessionParameters}, which shall be
+     * provided based on the rangingCapabilities of the device.
+     *
+     * @param sessionParameters parameters used to setup the session.
+     * @return the created RangingSession. Null if session creation failed.
+     * @throws IllegalArgumentException if sessionParameters is invalid.
+     */
+    @Nullable
+    public RangingSession createSession(@NonNull SessionParameters sessionParameters) {
+        Preconditions.checkNotNull(sessionParameters, "sessionParameters must not be null");
+        switch (sessionParameters.getRangingMethod()) {
+            case RANGING_METHOD_UWB:
+                if (mUwbServiceImpl == null) {
+                    Log.w(TAG, "createSession with UWB failed - UWB not supported");
+                    break;
+                }
+                return new UwbRangingSession(mContext, sessionParameters, mUwbServiceImpl);
+            case RANGING_METHOD_UNKNOWN:
+                break;
+        }
+        return null;
+    }
+
+    /** Initiation required for ranging with UWB. */
+    private void initiateUwb() {
+        UwbFeatureFlags uwbFeatureFlags =
+                new UwbFeatureFlags.Builder()
+                        .setSkipRangingCapabilitiesCheck(
+                                Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
+                        .setReversedByteOrderFiraParams(
+                                Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU)
+                        .build();
+        mUwbServiceImpl = new UwbServiceImpl(mContext, uwbFeatureFlags);
+        Log.i(TAG, "RangingManager initiateUwb complete");
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java
new file mode 100644
index 0000000..4b5874b
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.remoteauth.ranging;
+
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+
+/** The set of parameters to initiate {@link RangingSession#start}. */
+public class RangingParameters {
+
+    /** Parameters for {@link UwbRangingSession}. */
+    private final UwbAddress mUwbLocalAddress;
+
+    private final androidx.core.uwb.backend.impl.internal.RangingParameters mUwbRangingParameters;
+
+    public UwbAddress getUwbLocalAddress() {
+        return mUwbLocalAddress;
+    }
+
+    public androidx.core.uwb.backend.impl.internal.RangingParameters getUwbRangingParameters() {
+        return mUwbRangingParameters;
+    }
+
+    private RangingParameters(
+            UwbAddress uwbLocalAddress,
+            androidx.core.uwb.backend.impl.internal.RangingParameters uwbRangingParameters) {
+        mUwbLocalAddress = uwbLocalAddress;
+        mUwbRangingParameters = uwbRangingParameters;
+    }
+
+    /** Builder class for {@link RangingParameters}. */
+    public static final class Builder {
+        private UwbAddress mUwbLocalAddress;
+        private androidx.core.uwb.backend.impl.internal.RangingParameters mUwbRangingParameters;
+
+        /**
+         * Sets the uwb local address.
+         *
+         * <p>Only required if {@link SessionParameters#getRangingMethod}=={@link
+         * RANGING_METHOD_UWB} and {@link SessionParameters#getAutoDeriveParams} == false
+         */
+        public Builder setUwbLocalAddress(UwbAddress uwbLocalAddress) {
+            mUwbLocalAddress = uwbLocalAddress;
+            return this;
+        }
+
+        /**
+         * Sets the uwb ranging parameters.
+         *
+         * <p>Only required if {@link SessionParameters#getRangingMethod}=={@link
+         * RANGING_METHOD_UWB}.
+         *
+         * <p>If {@link SessionParameters#getAutoDeriveParams} == true, all required uwb parameters
+         * including uwbLocalAddress, complexChannel, peerAddresses, and sessionKeyInfo will be
+         * automatically derived, so unnecessary to provide and the other uwb parameters are
+         * optional.
+         */
+        public Builder setUwbRangingParameters(
+                androidx.core.uwb.backend.impl.internal.RangingParameters uwbRangingParameters) {
+            mUwbRangingParameters = uwbRangingParameters;
+            return this;
+        }
+
+        /** Builds {@link RangingParameters}. */
+        public RangingParameters build() {
+            return new RangingParameters(mUwbLocalAddress, mUwbRangingParameters);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingReport.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingReport.java
new file mode 100644
index 0000000..5e582b1
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingReport.java
@@ -0,0 +1,104 @@
+/*
+ * 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.remoteauth.ranging;
+
+import androidx.annotation.IntDef;
+
+/** Holds ranging report data. */
+public class RangingReport {
+
+    /**
+     * State of the proximity based on detected distance compared against specified near and far
+     * boundaries.
+     */
+    @IntDef(
+            value = {
+                PROXIMITY_STATE_UNKNOWN,
+                PROXIMITY_STATE_INSIDE,
+                PROXIMITY_STATE_OUTSIDE,
+            })
+    public @interface ProximityState {}
+
+    /** Unknown proximity state. */
+    public static final int PROXIMITY_STATE_UNKNOWN = 0x0;
+
+    /**
+     * Proximity is inside the lower and upper proximity boundary. lowerProximityBoundaryM <=
+     * proximity <= upperProximityBoundaryM
+     */
+    public static final int PROXIMITY_STATE_INSIDE = 0x1;
+
+    /**
+     * Proximity is outside the lower and upper proximity boundary. proximity <
+     * lowerProximityBoundaryM OR upperProximityBoundaryM < proximity
+     */
+    public static final int PROXIMITY_STATE_OUTSIDE = 0x2;
+
+    private final float mDistanceM;
+    @ProximityState private final int mProximityState;
+
+    /**
+     * Gets the distance measurement in meters.
+     *
+     * <p>Value may be negative for devices in very close proximity.
+     *
+     * @return distance in meters
+     */
+    public float getDistanceM() {
+        return mDistanceM;
+    }
+
+    /**
+     * Gets the {@link ProximityState}.
+     *
+     * <p>The state is computed based on {@link #getDistanceM} and proximity related session
+     * parameters.
+     *
+     * @return proximity state
+     */
+    @ProximityState
+    public int getProximityState() {
+        return mProximityState;
+    }
+
+    private RangingReport(float distanceM, @ProximityState int proximityState) {
+        mDistanceM = distanceM;
+        mProximityState = proximityState;
+    }
+
+    /** Builder class for {@link RangingReport}. */
+    public static final class Builder {
+        private float mDistanceM;
+        @ProximityState private int mProximityState;
+
+        /** Sets the distance in meters. */
+        public Builder setDistanceM(float distanceM) {
+            mDistanceM = distanceM;
+            return this;
+        }
+
+        /** Sets the proximity state. */
+        public Builder setProximityState(@ProximityState int proximityState) {
+            mProximityState = proximityState;
+            return this;
+        }
+
+        /** Builds {@link RangingReport}. */
+        public RangingReport build() {
+            return new RangingReport(mDistanceM, mProximityState);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
new file mode 100644
index 0000000..a922168
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
@@ -0,0 +1,255 @@
+/*
+ * 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.remoteauth.ranging;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.remoteauth.util.Crypto;
+
+import com.google.common.hash.Hashing;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * The controller for starting and stopping ranging during which callers receive callbacks with
+ * {@link RangingReport}s and {@link RangingError}s."
+ *
+ * <p>A session can be started and stopped multiple times. After starting, updates ({@link
+ * RangingReport}, {@link RangingError}, etc) will be reported via the provided {@link
+ * RangingCallback}. BaseKey and SyncData are used for auto derivation of supported ranging
+ * parameters, which will be implementation specific. All session creation shall only be conducted
+ * via {@link RangingManager#createSession}.
+ *
+ * <p>Ranging method specific implementation shall be implemented in the extended class.
+ */
+public abstract class RangingSession {
+    private static final String TAG = "RangingSession";
+
+    /** Types of ranging error. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                RANGING_ERROR_UNKNOWN,
+                RANGING_ERROR_INVALID_PARAMETERS,
+                RANGING_ERROR_STOPPED_BY_REQUEST,
+                RANGING_ERROR_STOPPED_BY_PEER,
+                RANGING_ERROR_FAILED_TO_START,
+                RANGING_ERROR_FAILED_TO_STOP,
+                RANGING_ERROR_SYSTEM_ERROR,
+                RANGING_ERROR_SYSTEM_TIMEOUT,
+            })
+    public @interface RangingError {}
+
+    /** Unknown ranging error type. */
+    public static final int RANGING_ERROR_UNKNOWN = 0x0;
+
+    /** Ranging error due to invalid parameters. */
+    public static final int RANGING_ERROR_INVALID_PARAMETERS = 0x1;
+
+    /** Ranging error due to stopped by calling {@link #stop}. */
+    public static final int RANGING_ERROR_STOPPED_BY_REQUEST = 0x2;
+
+    /** Ranging error due to stopped by the peer device. */
+    public static final int RANGING_ERROR_STOPPED_BY_PEER = 0x3;
+
+    /** Ranging error due to failure to start ranging. */
+    public static final int RANGING_ERROR_FAILED_TO_START = 0x4;
+
+    /** Ranging error due to failure to stop ranging. */
+    public static final int RANGING_ERROR_FAILED_TO_STOP = 0x5;
+
+    /**
+     * Ranging error due to system error cause by changes such as privacy policy, power management
+     * policy, permissions, and more.
+     */
+    public static final int RANGING_ERROR_SYSTEM_ERROR = 0x6;
+
+    /** Ranging error due to system timeout in retry attempts. */
+    public static final int RANGING_ERROR_SYSTEM_TIMEOUT = 0x7;
+
+    /** Interface for ranging update callbacks. */
+    public interface RangingCallback {
+        /**
+         * Call upon new {@link RangingReport}.
+         *
+         * @param sessionInfo info about this ranging session.
+         * @param rangingReport new ranging report
+         */
+        void onRangingReport(
+                @NonNull SessionInfo sessionInfo, @NonNull RangingReport rangingReport);
+
+        /**
+         * Call upon any ranging error events.
+         *
+         * @param sessionInfo info about this ranging session.
+         * @param rangingError error type
+         */
+        void onError(@NonNull SessionInfo sessionInfo, @RangingError int rangingError);
+    }
+
+    protected Context mContext;
+    protected SessionInfo mSessionInfo;
+    protected float mLowerProximityBoundaryM;
+    protected float mUpperProximityBoundaryM;
+    protected boolean mAutoDeriveParams;
+    protected byte[] mBaseKey;
+    protected byte[] mSyncData;
+    protected int mSyncCounter;
+    protected byte[] mDerivedData;
+    protected int mDerivedDataLength;
+
+    protected RangingSession(
+            @NonNull Context context,
+            @NonNull SessionParameters sessionParameters,
+            int derivedDataLength) {
+        Preconditions.checkNotNull(context);
+        Preconditions.checkNotNull(sessionParameters);
+        mContext = context;
+        mSessionInfo =
+                new SessionInfo.Builder()
+                        .setDeviceId(sessionParameters.getDeviceId())
+                        .setRangingMethod(sessionParameters.getRangingMethod())
+                        .build();
+        mLowerProximityBoundaryM = sessionParameters.getLowerProximityBoundaryM();
+        mUpperProximityBoundaryM = sessionParameters.getUpperProximityBoundaryM();
+        mAutoDeriveParams = sessionParameters.getAutoDeriveParams();
+        Log.i(
+                TAG,
+                "Creating a new RangingSession {info = "
+                        + mSessionInfo
+                        + ", autoDeriveParams = "
+                        + mAutoDeriveParams
+                        + "}");
+        if (mAutoDeriveParams) {
+            Preconditions.checkArgument(
+                    derivedDataLength > 0, "derivedDataLength must be greater than 0");
+            mDerivedDataLength = derivedDataLength;
+            resetBaseKey(sessionParameters.getBaseKey());
+            resetSyncData(sessionParameters.getSyncData());
+        }
+    }
+
+    /**
+     * Starts ranging based on the given {@link RangingParameters}.
+     *
+     * <p>Start can be called again after {@link #stop()} has been called, else it will result in a
+     * no-op.
+     *
+     * @param rangingParameters parameters to start the ranging.
+     * @param executor Executor to run the rangingCallback.
+     * @param rangingCallback callback to notify of ranging events.
+     * @throws NullPointerException if params are null.
+     * @throws IllegalArgumentException if rangingParameters is invalid.
+     */
+    public abstract void start(
+            @NonNull RangingParameters rangingParameters,
+            @NonNull Executor executor,
+            @NonNull RangingCallback rangingCallback);
+
+    /**
+     * Stops ranging.
+     *
+     * <p>Calling stop without first calling {@link #start()} will result in a no-op.
+     */
+    public abstract void stop();
+
+    /**
+     * Resets the base key that's used to derive all possible ranging parameters. The baseKey shall
+     * be reset whenever there is a risk that it may no longer be valid and secured. For example,
+     * the secure connection between the devices is lost.
+     *
+     * @param baseKey new baseKey must be 16 or 32 bytes.
+     * @throws NullPointerException if baseKey is null.
+     * @throws IllegalArgumentException if baseKey has invalid length.
+     */
+    public void resetBaseKey(@NonNull byte[] baseKey) {
+        if (!mAutoDeriveParams) {
+            Log.w(TAG, "autoDeriveParams is disabled, new baseKey is ignored.");
+            return;
+        }
+        Preconditions.checkNotNull(baseKey);
+        if (baseKey.length != 16 && baseKey.length != 32) {
+            throw new IllegalArgumentException("Invalid baseKey length: " + baseKey.length);
+        }
+        mBaseKey = baseKey;
+        updateDerivedData();
+        Log.i(TAG, "resetBaseKey");
+    }
+
+    /**
+     * Resets the synchronization by giving a new syncData used for ranging parameters derivation.
+     * Resetting the syncData is not required before each {@link #start}, but the more time the
+     * derivations are done before resetting syncData, the higher the risk the derivation will be
+     * out of sync between the devices. Therefore, syncData shall be refreshed in a best effort
+     * manner.
+     *
+     * @param syncData new syncData must be 16 bytes.
+     * @throws NullPointerException if baseKey is null.
+     * @throws IllegalArgumentException if syncData has invalid length.
+     */
+    public void resetSyncData(@NonNull byte[] syncData) {
+        if (!mAutoDeriveParams) {
+            Log.w(TAG, "autoDeriveParams is disabled, new syncData is ignored.");
+            return;
+        }
+        Preconditions.checkNotNull(syncData);
+        if (syncData.length != 16) {
+            throw new IllegalArgumentException("Invalid syncData length: " + syncData.length);
+        }
+        mSyncData = syncData;
+        mSyncCounter = 0;
+        updateDerivedData();
+        Log.i(TAG, "resetSyncData");
+    }
+
+    /** Recomputes mDerivedData using the latest mBaseKey, mSyncData, and mSyncCounter. */
+    protected boolean updateDerivedData() {
+        if (!mAutoDeriveParams) {
+            Log.w(TAG, "autoDeriveParams is disabled, updateDerivedData is skipped.");
+            return false;
+        }
+        if (mBaseKey == null
+                || mBaseKey.length == 0
+                || mSyncData == null
+                || mSyncData.length == 0) {
+            Log.w(TAG, "updateDerivedData: Missing baseKey/syncData");
+            return false;
+        }
+        byte[] hashedSyncData =
+                Hashing.sha256()
+                        .newHasher()
+                        .putBytes(mSyncData)
+                        .putInt(mSyncCounter)
+                        .hash()
+                        .asBytes();
+        byte[] newDerivedData = Crypto.computeHkdf(mBaseKey, hashedSyncData, mDerivedDataLength);
+        if (newDerivedData == null) {
+            Log.w(TAG, "updateDerivedData: computeHkdf failed");
+            return false;
+        }
+        mDerivedData = newDerivedData;
+        mSyncCounter++;
+        Log.i(TAG, "updateDerivedData");
+        return true;
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionInfo.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionInfo.java
new file mode 100644
index 0000000..0ec640c
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionInfo.java
@@ -0,0 +1,79 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UNKNOWN;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+
+/** Information about the {@link RangingSession}. */
+public class SessionInfo {
+
+    private final String mDeviceId;
+    @RangingMethod private final int mRangingMethod;
+
+    public String getDeviceId() {
+        return mDeviceId;
+    }
+
+    @RangingMethod
+    public int getRangingMethod() {
+        return mRangingMethod;
+    }
+
+    private SessionInfo(String deviceId, @RangingMethod int rangingMethod) {
+        mDeviceId = deviceId;
+        mRangingMethod = rangingMethod;
+    }
+
+    @Override
+    public String toString() {
+        return "SessionInfo { "
+                + "DeviceId = "
+                + mDeviceId
+                + "RangingMethod = "
+                + mRangingMethod
+                + " }";
+    }
+
+    /** Builder class for {@link SessionInfo}. */
+    public static final class Builder {
+        private String mDeviceId = "";
+        @RangingMethod private int mRangingMethod = RANGING_METHOD_UNKNOWN;
+
+        /** Sets the device id. */
+        public Builder setDeviceId(String deviceId) {
+            mDeviceId = deviceId;
+            return this;
+        }
+
+        /** Sets the ranging method. */
+        public Builder setRangingMethod(@RangingMethod int rangingMethod) {
+            mRangingMethod = rangingMethod;
+            return this;
+        }
+
+        /** Builds {@link SessionInfo}. */
+        public SessionInfo build() {
+            Preconditions.checkArgument(!mDeviceId.isEmpty(), "deviceId must not be empty.");
+            Preconditions.checkArgument(
+                    mRangingMethod != RANGING_METHOD_UNKNOWN, "Unknown rangingMethod");
+            return new SessionInfo(mDeviceId, mRangingMethod);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionParameters.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionParameters.java
new file mode 100644
index 0000000..2f71244
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionParameters.java
@@ -0,0 +1,263 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UNKNOWN;
+
+import android.annotation.NonNull;
+
+import androidx.annotation.IntDef;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The set of parameters to create a ranging session.
+ *
+ * <p>Required parameters must be provided, else {@link Builder} will throw an exception. The
+ * optional parameters only need to be provided if the functionality is necessary to the session,
+ * see the setter functions of the {@link Builder} for detailed info of each parameter.
+ */
+public class SessionParameters {
+
+    /** Ranging device role. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                DEVICE_ROLE_UNKNOWN,
+                DEVICE_ROLE_INITIATOR,
+                DEVICE_ROLE_RESPONDER,
+            })
+    public @interface DeviceRole {}
+
+    /** Unknown device role. */
+    public static final int DEVICE_ROLE_UNKNOWN = 0x0;
+
+    /** Device that initiates the ranging. */
+    public static final int DEVICE_ROLE_INITIATOR = 0x1;
+
+    /** Device that responds to ranging. */
+    public static final int DEVICE_ROLE_RESPONDER = 0x2;
+
+    /* Required parameters */
+    private final String mDeviceId;
+    @RangingMethod private final int mRangingMethod;
+    @DeviceRole private final int mDeviceRole;
+
+    /* Optional parameters */
+    private final float mLowerProximityBoundaryM;
+    private final float mUpperProximityBoundaryM;
+    private final boolean mAutoDeriveParams;
+    private final byte[] mBaseKey;
+    private final byte[] mSyncData;
+
+    public String getDeviceId() {
+        return mDeviceId;
+    }
+
+    @RangingMethod
+    public int getRangingMethod() {
+        return mRangingMethod;
+    }
+
+    @DeviceRole
+    public int getDeviceRole() {
+        return mDeviceRole;
+    }
+
+    public float getLowerProximityBoundaryM() {
+        return mLowerProximityBoundaryM;
+    }
+
+    public float getUpperProximityBoundaryM() {
+        return mUpperProximityBoundaryM;
+    }
+
+    public boolean getAutoDeriveParams() {
+        return mAutoDeriveParams;
+    }
+
+    public byte[] getBaseKey() {
+        return mBaseKey;
+    }
+
+    public byte[] getSyncData() {
+        return mSyncData;
+    }
+
+    private SessionParameters(
+            String deviceId,
+            @RangingMethod int rangingMethod,
+            @DeviceRole int deviceRole,
+            float lowerProximityBoundaryM,
+            float upperProximityBoundaryM,
+            boolean autoDeriveParams,
+            byte[] baseKey,
+            byte[] syncData) {
+        mDeviceId = deviceId;
+        mRangingMethod = rangingMethod;
+        mDeviceRole = deviceRole;
+        mLowerProximityBoundaryM = lowerProximityBoundaryM;
+        mUpperProximityBoundaryM = upperProximityBoundaryM;
+        mAutoDeriveParams = autoDeriveParams;
+        mBaseKey = baseKey;
+        mSyncData = syncData;
+    }
+
+    /** Builder class for {@link SessionParameters}. */
+    public static final class Builder {
+        private String mDeviceId = new String("");
+        @RangingMethod private int mRangingMethod = RANGING_METHOD_UNKNOWN;
+        @DeviceRole private int mDeviceRole = DEVICE_ROLE_UNKNOWN;
+        private float mLowerProximityBoundaryM;
+        private float mUpperProximityBoundaryM;
+        private boolean mAutoDeriveParams = false;
+        private byte[] mBaseKey = new byte[] {};
+        private byte[] mSyncData = new byte[] {};
+
+        /**
+         * Sets the device id.
+         *
+         * <p>This is used as the identity included in the {@link SessionInfo} for all {@link
+         * RangingCallback}s.
+         */
+        public Builder setDeviceId(@NonNull String deviceId) {
+            mDeviceId = deviceId;
+            return this;
+        }
+
+        /**
+         * Sets the {@link RangingMethod} to be used for the {@link RangingSession}.
+         *
+         * <p>Note: The ranging method should be ones in the list return by {@link
+         * RangingCapabilities#getSupportedRangingMethods};
+         */
+        public Builder setRangingMethod(@RangingMethod int rangingMethod) {
+            mRangingMethod = rangingMethod;
+            return this;
+        }
+
+        /** Sets the {@link DeviceRole} to be used for the {@link RangingSession}. */
+        public Builder setDeviceRole(@DeviceRole int deviceRole) {
+            mDeviceRole = deviceRole;
+            return this;
+        }
+
+        /**
+         * Sets the lower proximity boundary in meters, must be greater than or equals to zero.
+         *
+         * <p>This value is used to compute the {@link ProximityState} = {@link
+         * PROXIMITY_STATE_INSIDE} if lowerProximityBoundaryM <= proximity <=
+         * upperProximityBoundaryM, else {@link PROXIMITY_STATE_OUTSIDE}.
+         */
+        public Builder setLowerProximityBoundaryM(float lowerProximityBoundaryM) {
+            mLowerProximityBoundaryM = lowerProximityBoundaryM;
+            return this;
+        }
+
+        /**
+         * Sets the upper proximity boundary in meters, must be greater than or equals to
+         * lowerProximityBoundaryM.
+         *
+         * <p>This value is used to compute the {@link ProximityState} = {@link
+         * PROXIMITY_STATE_INSIDE} if lowerProximityBoundaryM <= proximity <=
+         * upperProximityBoundaryM, else {@link PROXIMITY_STATE_OUTSIDE}.
+         */
+        public Builder setUpperProximityBoundaryM(float upperProximityBoundaryM) {
+            mUpperProximityBoundaryM = upperProximityBoundaryM;
+            return this;
+        }
+
+        /**
+         * Sets the auto derive ranging parameters flag. Defaults to false.
+         *
+         * <p>This enables the {@link RangingSession} to automatically derive all possible {@link
+         * RangingParameters} at each {@link RangingSession#start} using the provided {@link
+         * #setBaseKey} and {@link #setSyncData}, which shall be securely shared between the ranging
+         * devices out of band.
+         */
+        public Builder setAutoDeriveParams(boolean autoDeriveParams) {
+            mAutoDeriveParams = autoDeriveParams;
+            return this;
+        }
+
+        /**
+         * Sets the base key. Only required if {@link #setAutoDeriveParams} is set to true.
+         *
+         * @param baseKey baseKey must be 16 or 32 bytes.
+         * @throws NullPointerException if baseKey is null
+         */
+        public Builder setBaseKey(@NonNull byte[] baseKey) {
+            Preconditions.checkNotNull(baseKey);
+            mBaseKey = baseKey;
+            return this;
+        }
+
+        /**
+         * Sets the sync data. Only required if {@link #setAutoDeriveParams} is set to true.
+         *
+         * @param syncData syncData must be 16 bytes.
+         * @throws NullPointerException if syncData is null
+         */
+        public Builder setSyncData(@NonNull byte[] syncData) {
+            Preconditions.checkNotNull(syncData);
+            mSyncData = syncData;
+            return this;
+        }
+
+        /**
+         * Builds {@link SessionParameters}.
+         *
+         * @throws IllegalArgumentException if any parameter is invalid.
+         */
+        public SessionParameters build() {
+            Preconditions.checkArgument(!mDeviceId.isEmpty(), "deviceId must not be empty.");
+            Preconditions.checkArgument(
+                    mRangingMethod != RANGING_METHOD_UNKNOWN, "Unknown rangingMethod");
+            Preconditions.checkArgument(mDeviceRole != DEVICE_ROLE_UNKNOWN, "Unknown deviceRole");
+            Preconditions.checkArgument(
+                    mLowerProximityBoundaryM >= 0,
+                    "Negative lowerProximityBoundaryM: " + mLowerProximityBoundaryM);
+            Preconditions.checkArgument(
+                    mLowerProximityBoundaryM <= mUpperProximityBoundaryM,
+                    "lowerProximityBoundaryM is greater than upperProximityBoundaryM: "
+                            + mLowerProximityBoundaryM
+                            + " > "
+                            + mUpperProximityBoundaryM);
+            // If mAutoDeriveParams is false, mBaseKey and mSyncData will not be used.
+            if (mAutoDeriveParams) {
+                Preconditions.checkArgument(
+                        mBaseKey.length == 16 || mBaseKey.length == 32,
+                        "Invalid baseKey length: " + mBaseKey.length);
+                Preconditions.checkArgument(
+                        mSyncData.length == 16, "Invalid syncData length: " + mSyncData.length);
+            }
+
+            return new SessionParameters(
+                    mDeviceId,
+                    mRangingMethod,
+                    mDeviceRole,
+                    mLowerProximityBoundaryM,
+                    mUpperProximityBoundaryM,
+                    mAutoDeriveParams,
+                    mBaseKey,
+                    mSyncData);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java
new file mode 100644
index 0000000..62463e1
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java
@@ -0,0 +1,233 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static androidx.core.uwb.backend.impl.internal.RangingDevice.SESSION_ID_UNSET;
+import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_OK;
+import static androidx.core.uwb.backend.impl.internal.Utils.SUPPORTED_BPRF_PREAMBLE_INDEX;
+import static androidx.core.uwb.backend.impl.internal.UwbAddress.SHORT_ADDRESS_LENGTH;
+
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_INSIDE;
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_OUTSIDE;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+
+import static com.google.uwb.support.fira.FiraParams.UWB_CHANNEL_9;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.uwb.backend.impl.internal.RangingController;
+import androidx.core.uwb.backend.impl.internal.RangingDevice;
+import androidx.core.uwb.backend.impl.internal.RangingPosition;
+import androidx.core.uwb.backend.impl.internal.RangingSessionCallback;
+import androidx.core.uwb.backend.impl.internal.RangingSessionCallback.RangingSuspendedReason;
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+import androidx.core.uwb.backend.impl.internal.UwbComplexChannel;
+import androidx.core.uwb.backend.impl.internal.UwbDevice;
+import androidx.core.uwb.backend.impl.internal.UwbServiceImpl;
+
+import com.android.internal.util.Preconditions;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/** UWB (ultra wide-band) specific implementation of {@link RangingSession}. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class UwbRangingSession extends RangingSession {
+    private static final String TAG = "UwbRangingSession";
+
+    private static final int COMPLEX_CHANNEL_LENGTH = 1;
+    private static final int STS_KEY_LENGTH = 16;
+    private static final int DERIVED_DATA_LENGTH =
+            COMPLEX_CHANNEL_LENGTH + SHORT_ADDRESS_LENGTH + SHORT_ADDRESS_LENGTH + STS_KEY_LENGTH;
+
+    private final UwbServiceImpl mUwbServiceImpl;
+    private final RangingDevice mRangingDevice;
+
+    private Executor mExecutor;
+    private RangingCallback mRangingCallback;
+
+    public UwbRangingSession(
+            @NonNull Context context,
+            @NonNull SessionParameters sessionParameters,
+            @NonNull UwbServiceImpl uwbServiceImpl) {
+        super(context, sessionParameters, DERIVED_DATA_LENGTH);
+        Preconditions.checkNotNull(uwbServiceImpl);
+        mUwbServiceImpl = uwbServiceImpl;
+        if (sessionParameters.getDeviceRole() == DEVICE_ROLE_INITIATOR) {
+            mRangingDevice = (RangingDevice) mUwbServiceImpl.getController(context);
+        } else {
+            mRangingDevice = (RangingDevice) mUwbServiceImpl.getControlee(context);
+        }
+    }
+
+    @Override
+    public void start(
+            @NonNull RangingParameters rangingParameters,
+            @NonNull Executor executor,
+            @NonNull RangingCallback rangingCallback) {
+        Preconditions.checkNotNull(rangingParameters, "rangingParameters must not be null");
+        Preconditions.checkNotNull(executor, "executor must not be null");
+        Preconditions.checkNotNull(rangingCallback, "rangingCallback must not be null");
+
+        setUwbRangingParameters(rangingParameters);
+        int status =
+                mRangingDevice.startRanging(
+                        convertCallback(rangingCallback, executor),
+                        Executors.newSingleThreadExecutor());
+        if (status != STATUS_OK) {
+            Log.w(TAG, String.format("Uwb ranging start failed with status %d", status));
+            executor.execute(
+                    () -> rangingCallback.onError(mSessionInfo, RANGING_ERROR_FAILED_TO_START));
+            return;
+        }
+        mExecutor = executor;
+        mRangingCallback = rangingCallback;
+        Log.i(TAG, "start");
+    }
+
+    @Override
+    public void stop() {
+        if (mRangingCallback == null) {
+            Log.w(TAG, String.format("Failed to stop unstarted session"));
+            return;
+        }
+        int status = mRangingDevice.stopRanging();
+        if (status != STATUS_OK) {
+            Log.w(TAG, String.format("Uwb ranging stop failed with status %d", status));
+            mExecutor.execute(
+                    () -> mRangingCallback.onError(mSessionInfo, RANGING_ERROR_FAILED_TO_STOP));
+            return;
+        }
+        mRangingCallback = null;
+        Log.i(TAG, "stop");
+    }
+
+    private void setUwbRangingParameters(RangingParameters rangingParameters) {
+        androidx.core.uwb.backend.impl.internal.RangingParameters params =
+                rangingParameters.getUwbRangingParameters();
+        Preconditions.checkNotNull(params, "uwbRangingParameters must not be null");
+        if (mAutoDeriveParams) {
+            Preconditions.checkArgument(mDerivedData.length == DERIVED_DATA_LENGTH);
+            ByteBuffer buffer = ByteBuffer.wrap(mDerivedData);
+
+            byte complexChannelByte = buffer.get();
+            int preambleIndex =
+                    SUPPORTED_BPRF_PREAMBLE_INDEX.get(
+                            Math.abs(complexChannelByte) % SUPPORTED_BPRF_PREAMBLE_INDEX.size());
+            // Selecting channel 9 since it's the only mandatory channel.
+            UwbComplexChannel complexChannel = new UwbComplexChannel(UWB_CHANNEL_9, preambleIndex);
+
+            byte[] localAddress = new byte[SHORT_ADDRESS_LENGTH];
+            byte[] peerAddress = new byte[SHORT_ADDRESS_LENGTH];
+            if (mRangingDevice instanceof RangingController) {
+                ((RangingController) mRangingDevice).setComplexChannel(complexChannel);
+                buffer.get(localAddress);
+                buffer.get(peerAddress);
+            } else {
+                buffer.get(peerAddress);
+                buffer.get(localAddress);
+            }
+            byte[] stsKey = new byte[STS_KEY_LENGTH];
+            buffer.get(stsKey);
+
+            mRangingDevice.setLocalAddress(UwbAddress.fromBytes(localAddress));
+            mRangingDevice.setRangingParameters(
+                    new androidx.core.uwb.backend.impl.internal.RangingParameters(
+                            params.getUwbConfigId(),
+                            SESSION_ID_UNSET,
+                            /* subSessionId= */ SESSION_ID_UNSET,
+                            stsKey,
+                            /* subSessionInfo= */ new byte[] {},
+                            complexChannel,
+                            List.of(UwbAddress.fromBytes(peerAddress)),
+                            params.getRangingUpdateRate(),
+                            params.getUwbRangeDataNtfConfig(),
+                            params.getSlotDuration(),
+                            params.isAoaDisabled()));
+        } else {
+            UwbAddress localAddress = rangingParameters.getUwbLocalAddress();
+            Preconditions.checkNotNull(localAddress, "localAddress must not be null");
+            UwbComplexChannel complexChannel = params.getComplexChannel();
+            Preconditions.checkNotNull(complexChannel, "complexChannel must not be null");
+            mRangingDevice.setLocalAddress(localAddress);
+            if (mRangingDevice instanceof RangingController) {
+                ((RangingController) mRangingDevice).setComplexChannel(complexChannel);
+            }
+            mRangingDevice.setRangingParameters(params);
+        }
+    }
+
+    private RangingSessionCallback convertCallback(RangingCallback callback, Executor executor) {
+        return new RangingSessionCallback() {
+
+            @Override
+            public void onRangingInitialized(UwbDevice device) {
+                Log.i(TAG, "onRangingInitialized");
+            }
+
+            @Override
+            public void onRangingResult(UwbDevice device, RangingPosition position) {
+                float distanceM = position.getDistance().getValue();
+                int proximityState =
+                        (mLowerProximityBoundaryM <= distanceM
+                                        && distanceM <= mUpperProximityBoundaryM)
+                                ? PROXIMITY_STATE_INSIDE
+                                : PROXIMITY_STATE_OUTSIDE;
+                position.getDistance().getValue();
+                RangingReport rangingReport =
+                        new RangingReport.Builder()
+                                .setDistanceM(distanceM)
+                                .setProximityState(proximityState)
+                                .build();
+                executor.execute(() -> callback.onRangingReport(mSessionInfo, rangingReport));
+            }
+
+            @Override
+            public void onRangingSuspended(UwbDevice device, @RangingSuspendedReason int reason) {
+                executor.execute(() -> callback.onError(mSessionInfo, convertError(reason)));
+            }
+        };
+    }
+
+    @RangingError
+    private static int convertError(@RangingSuspendedReason int reason) {
+        if (reason == RangingSessionCallback.REASON_WRONG_PARAMETERS) {
+            return RANGING_ERROR_INVALID_PARAMETERS;
+        }
+        if (reason == RangingSessionCallback.REASON_STOP_RANGING_CALLED) {
+            return RANGING_ERROR_STOPPED_BY_REQUEST;
+        }
+        if (reason == RangingSessionCallback.REASON_STOPPED_BY_PEER) {
+            return RANGING_ERROR_STOPPED_BY_PEER;
+        }
+        if (reason == RangingSessionCallback.REASON_FAILED_TO_START) {
+            return RANGING_ERROR_FAILED_TO_START;
+        }
+        if (reason == RangingSessionCallback.REASON_SYSTEM_POLICY) {
+            return RANGING_ERROR_SYSTEM_ERROR;
+        }
+        if (reason == RangingSessionCallback.REASON_MAX_RANGING_ROUND_RETRY_REACHED) {
+            return RANGING_ERROR_SYSTEM_TIMEOUT;
+        }
+        return RANGING_ERROR_UNKNOWN;
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/util/Crypto.java b/remoteauth/service/java/com/android/server/remoteauth/util/Crypto.java
new file mode 100644
index 0000000..573597f
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/util/Crypto.java
@@ -0,0 +1,106 @@
+/*
+ * 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.remoteauth.util;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/** Utility class of cryptographic functions. */
+public final class Crypto {
+    private static final String TAG = "Crypto";
+    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+
+    /**
+     * A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
+     * given size.
+     *
+     * @param ikm the input keying material.
+     * @param salt A possibly non-secret random value.
+     * @param size The length of the generated pseudorandom string in bytes. The maximal size is
+     *     255.DigestSize, where DigestSize is the size of the underlying HMAC.
+     * @return size pseudorandom bytes, null if failed.
+     */
+    // Based on
+    // google3/third_party/tink/java_src/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+    @Nullable
+    public static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
+        Mac mac;
+        try {
+            mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
+        } catch (NoSuchAlgorithmException e) {
+            Log.w(TAG, "HMAC_SHA256_ALGORITHM is not supported.", e);
+            return null;
+        }
+
+        if (size > 255 * mac.getMacLength()) {
+            Log.w(TAG, "Size too large. " + size + " > " + 255 * mac.getMacLength());
+            return null;
+        }
+
+        if (ikm == null || ikm.length == 0) {
+            Log.w(TAG, "Ikm cannot be empty.");
+            return null;
+        }
+
+        if (salt == null || salt.length == 0) {
+            Log.w(TAG, "Salt cannot be empty.");
+            return null;
+        }
+
+        try {
+            mac.init(new SecretKeySpec(salt, HMAC_SHA256_ALGORITHM));
+        } catch (InvalidKeyException e) {
+            Log.w(TAG, "Invalid key.", e);
+            return null;
+        }
+
+        byte[] prk = mac.doFinal(ikm);
+        byte[] result = new byte[size];
+        try {
+            mac.init(new SecretKeySpec(prk, HMAC_SHA256_ALGORITHM));
+        } catch (InvalidKeyException e) {
+            Log.w(TAG, "Invalid key.", e);
+            return null;
+        }
+
+        byte[] digest = new byte[0];
+        int ctr = 1;
+        int pos = 0;
+        while (true) {
+            mac.update(digest);
+            mac.update((byte) ctr);
+            digest = mac.doFinal();
+            if (pos + digest.length < size) {
+                System.arraycopy(digest, 0, result, pos, digest.length);
+                pos += digest.length;
+                ctr++;
+            } else {
+                System.arraycopy(digest, 0, result, pos, size - pos);
+                break;
+            }
+        }
+
+        return result;
+    }
+}
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
new file mode 100644
index 0000000..fc91e0c
--- /dev/null
+++ b/remoteauth/service/jni/Android.bp
@@ -0,0 +1,83 @@
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libremoteauth_jni_rust_defaults",
+    crate_name: "remoteauth_jni_rust",
+    lints: "android",
+    clippy_lints: "android",
+    min_sdk_version: "35",
+    srcs: ["src/lib.rs"],
+    rustlibs: [
+        "libbinder_rs",
+        "libjni_legacy",
+        "liblazy_static",
+        "liblog_rust",
+        "liblogger",
+        "libnum_traits",
+        "libthiserror",
+        "libtokio",
+        "libanyhow",
+    ],
+    proc_macros: [
+        "libasync_trait",
+    ],
+    prefer_rlib: true,
+    apex_available: [
+        "com.android.remoteauth",
+    ],
+    host_supported: true,
+}
+
+rust_ffi_shared {
+    name: "libremoteauth_jni_rust",
+    defaults: ["libremoteauth_jni_rust_defaults"],
+    rustlibs: [],
+}
+
+rust_test {
+    name: "libremoteauth_jni_rust_tests",
+    defaults: ["libremoteauth_jni_rust_defaults"],
+    rustlibs: [
+    ],
+    target: {
+        android: {
+            test_suites: [
+                "general-tests",
+            ],
+            test_config_template: "remoteauth_rust_test_config_template.xml",
+        },
+        host: {
+            test_suites: [
+                "general-tests",
+            ],
+            data_libs: [
+                "libandroid_runtime_lazy",
+                "libbase",
+                "libbinder",
+                "libbinder_ndk",
+                "libcutils",
+                "liblog",
+                "libutils",
+            ],
+        },
+    },
+    test_options: {
+        unit_test: true,
+    },
+    // Support multilib variants (using different suffix per sub-architecture), which is needed on
+    // build targets with secondary architectures, as the MTS test suite packaging logic flattens
+    // all test artifacts into a single `testcases` directory.
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
+    auto_gen_config: true,
+}
diff --git a/remoteauth/service/jni/remoteauth_rust_test_config_template.xml b/remoteauth/service/jni/remoteauth_rust_test_config_template.xml
new file mode 100644
index 0000000..673b451
--- /dev/null
+++ b/remoteauth/service/jni/remoteauth_rust_test_config_template.xml
@@ -0,0 +1,32 @@
+<?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="Configuration for {MODULE} Rust tests">
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="{MODULE}->/data/local/tmp/{MODULE}" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+        <option name="test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="{MODULE}" />
+    </test>
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.remoteauth" />
+    </object>
+</configuration>
\ No newline at end of file
diff --git a/remoteauth/service/jni/src/jnames.rs b/remoteauth/service/jni/src/jnames.rs
new file mode 100644
index 0000000..d7cc908
--- /dev/null
+++ b/remoteauth/service/jni/src/jnames.rs
@@ -0,0 +1,17 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Name of java classes and methods for RemoteAuth platform:
+pub(crate) const SEND_REQUEST_MNAME: &str = "sendRequest";
+pub(crate) const SEND_REQUEST_MSIG: &str = "(I[BII)V";
diff --git a/remoteauth/service/jni/src/lib.rs b/remoteauth/service/jni/src/lib.rs
new file mode 100644
index 0000000..c6f8c72
--- /dev/null
+++ b/remoteauth/service/jni/src/lib.rs
@@ -0,0 +1,27 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! New rust RemoteAuth JNI library.
+//!
+//! This library takes the JNI calls from RemoteAuthService to the remoteauth protocol library
+//! and from protocol library to platform (Java interface)
+
+mod jnames;
+mod unique_jvm;
+mod utils;
+
+/// Implementation of JNI platform functionality.
+pub mod remoteauth_jni_android_platform;
+/// Implementation of JNI protocol functionality.
+pub mod remoteauth_jni_android_protocol;
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
new file mode 100644
index 0000000..421fe7e
--- /dev/null
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -0,0 +1,301 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Implementation of JNI platform functionality.
+use crate::jnames::{SEND_REQUEST_MNAME, SEND_REQUEST_MSIG};
+use crate::unique_jvm;
+use anyhow::anyhow;
+use jni::errors::Error as JNIError;
+use jni::objects::{GlobalRef, JMethodID, JObject, JValue};
+use jni::signature::TypeSignature;
+use jni::sys::{jbyteArray, jint, jlong, jvalue};
+use jni::{JNIEnv, JavaVM};
+use lazy_static::lazy_static;
+use log::{debug, error, info};
+use std::collections::HashMap;
+use std::sync::{
+    atomic::{AtomicI64, Ordering},
+    Arc, Mutex,
+};
+
+/// Macro capturing the name of the function calling this macro.
+///
+/// function_name()! -> &'static str
+/// Returns the function name as 'static reference.
+macro_rules! function_name {
+    () => {{
+        // Declares function f inside current function.
+        fn f() {}
+        fn type_name_of<T>(_: T) -> &'static str {
+            std::any::type_name::<T>()
+        }
+        // type name of f is struct_or_crate_name::calling_function_name::f
+        let name = type_name_of(f);
+        // Find and cut the rest of the path:
+        // Third to last character, up to the first semicolon: is calling_function_name
+        match &name[..name.len() - 3].rfind(':') {
+            Some(pos) => &name[pos + 1..name.len() - 3],
+            None => &name[..name.len() - 3],
+        }
+    }};
+}
+
+lazy_static! {
+    static ref HANDLE_MAPPING: Mutex<HashMap<i64, Arc<Mutex<JavaPlatform>>>> =
+        Mutex::new(HashMap::new());
+    static ref HANDLE_RN: AtomicI64 = AtomicI64::new(0);
+}
+
+fn generate_platform_handle() -> i64 {
+    HANDLE_RN.fetch_add(1, Ordering::SeqCst)
+}
+
+fn insert_platform_handle(handle: i64, item: Arc<Mutex<JavaPlatform>>) {
+    if 0 == handle {
+        // Init once
+        logger::init(
+            logger::Config::default()
+                .with_tag_on_device("remoteauth")
+                .with_max_level(log::LevelFilter::Trace)
+                .with_filter("trace,jni=info"),
+        );
+    }
+    HANDLE_MAPPING.lock().unwrap().insert(handle, Arc::clone(&item));
+}
+
+/// Reports a response from remote device.
+pub trait ResponseCallback {
+    /// Invoked upon successful response
+    fn on_response(&mut self, response: Vec<u8>);
+    /// Invoked upon failure
+    fn on_error(&mut self, error_code: i32);
+}
+
+/// Trait to platform functionality
+pub trait Platform {
+    /// Send a binary message to the remote with the given connection id and return the response.
+    fn send_request(
+        &mut self,
+        connection_id: i32,
+        request: &[u8],
+        callback: Box<dyn ResponseCallback + Send>,
+    ) -> anyhow::Result<()>;
+}
+//////////////////////////////////
+
+/// Implementation of Platform trait
+pub struct JavaPlatform {
+    platform_handle: i64,
+    vm: &'static Arc<JavaVM>,
+    platform_native_obj: GlobalRef,
+    send_request_method_id: JMethodID,
+    map_futures: Mutex<HashMap<i64, Box<dyn ResponseCallback + Send>>>,
+    atomic_handle: AtomicI64,
+}
+
+impl JavaPlatform {
+    /// Creates JavaPlatform and associates with unique handle id
+    pub fn create(
+        java_platform_native: JObject<'_>,
+    ) -> Result<Arc<Mutex<impl Platform>>, JNIError> {
+        let platform_handle = generate_platform_handle();
+        let platform = Arc::new(Mutex::new(JavaPlatform::new(
+            platform_handle,
+            unique_jvm::get_static_ref().ok_or(JNIError::InvalidCtorReturn)?,
+            java_platform_native,
+        )?));
+        insert_platform_handle(platform_handle, Arc::clone(&platform));
+        Ok(Arc::clone(&platform))
+    }
+
+    fn new(
+        platform_handle: i64,
+        vm: &'static Arc<JavaVM>,
+        java_platform_native: JObject,
+    ) -> Result<JavaPlatform, JNIError> {
+        vm.attach_current_thread().and_then(|env| {
+            let platform_class = env.get_object_class(java_platform_native)?;
+            let platform_native_obj = env.new_global_ref(java_platform_native)?;
+            let send_request_method: JMethodID =
+                env.get_method_id(platform_class, SEND_REQUEST_MNAME, SEND_REQUEST_MSIG)?;
+
+            Ok(Self {
+                platform_handle,
+                vm,
+                platform_native_obj,
+                send_request_method_id: send_request_method,
+                map_futures: Mutex::new(HashMap::new()),
+                atomic_handle: AtomicI64::new(0),
+            })
+        })
+    }
+}
+
+impl Platform for JavaPlatform {
+    fn send_request(
+        &mut self,
+        connection_id: i32,
+        request: &[u8],
+        callback: Box<dyn ResponseCallback + Send>,
+    ) -> anyhow::Result<()> {
+        let type_signature = TypeSignature::from_str(SEND_REQUEST_MSIG)
+            .map_err(|e| anyhow!("JNI: Invalid type signature: {:?}", e))?;
+
+        let response_handle = self.atomic_handle.fetch_add(1, Ordering::SeqCst);
+        self.map_futures.lock().unwrap().insert(response_handle, callback);
+        self.vm
+            .attach_current_thread()
+            .and_then(|env| {
+                let request_jbytearray = env.byte_array_from_slice(request)?;
+                // Safety: request_jbytearray is safely instantiated above.
+                let request_jobject = unsafe { JObject::from_raw(request_jbytearray) };
+
+                let _ = env.call_method_unchecked(
+                    self.platform_native_obj.as_obj(),
+                    self.send_request_method_id,
+                    type_signature.ret,
+                    &[
+                        jvalue::from(JValue::Int(connection_id)),
+                        jvalue::from(JValue::Object(request_jobject)),
+                        jvalue::from(JValue::Long(response_handle)),
+                        jvalue::from(JValue::Long(self.platform_handle)),
+                    ],
+                );
+                Ok(info!(
+                    "{} successfully sent-message, waiting for response {}:{}",
+                    function_name!(),
+                    self.platform_handle,
+                    response_handle
+                ))
+            })
+            .map_err(|e| anyhow!("JNI: Failed to attach current thread: {:?}", e))?;
+        Ok(())
+    }
+}
+
+impl JavaPlatform {
+    fn on_send_request_success(&mut self, response: &[u8], response_handle: i64) {
+        info!(
+            "{} completed successfully {}:{}",
+            function_name!(),
+            self.platform_handle,
+            response_handle
+        );
+        if let Some(mut callback) = self.map_futures.lock().unwrap().remove(&response_handle) {
+            callback.on_response(response.to_vec());
+        } else {
+            error!(
+                "Failed to find TX for {} and {}:{}",
+                function_name!(),
+                self.platform_handle,
+                response_handle
+            );
+        }
+    }
+
+    fn on_send_request_error(&self, error_code: i32, response_handle: i64) {
+        error!(
+            "{} completed with error {} {}:{}",
+            function_name!(),
+            error_code,
+            self.platform_handle,
+            response_handle
+        );
+        if let Some(mut callback) = self.map_futures.lock().unwrap().remove(&response_handle) {
+            callback.on_error(error_code);
+        } else {
+            error!(
+                "Failed to find callback for {} and {}:{}",
+                function_name!(),
+                self.platform_handle,
+                response_handle
+            );
+        }
+    }
+}
+
+/// Returns successful response from remote device
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_remoteauth_jni_NativeRemoteAuthJavaPlatform_native_on_send_request_success(
+    env: JNIEnv,
+    _: JObject,
+    app_response: jbyteArray,
+    platform_handle: jlong,
+    response_handle: jlong,
+) {
+    debug!("{}: enter", function_name!());
+    native_on_send_request_success(env, app_response, platform_handle, response_handle);
+}
+
+fn native_on_send_request_success(
+    env: JNIEnv<'_>,
+    app_response: jbyteArray,
+    platform_handle: jlong,
+    response_handle: jlong,
+) {
+    if let Some(platform) = HANDLE_MAPPING.lock().unwrap().get(&platform_handle) {
+        let response =
+            env.convert_byte_array(app_response).map_err(|_| JNIError::InvalidCtorReturn).unwrap();
+        let mut platform = (*platform).lock().unwrap();
+        platform.on_send_request_success(&response, response_handle);
+    } else {
+        let _ = env.throw_new(
+            "com/android/server/remoteauth/jni/BadHandleException",
+            format!("Failed to find Platform with ID {} in {}", platform_handle, function_name!()),
+        );
+    }
+}
+
+/// Notifies about failure to receive a response from remote device
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_remoteauth_jni_NativeRemoteAuthJavaPlatform_native_on_send_request_error(
+    env: JNIEnv,
+    _: JObject,
+    error_code: jint,
+    platform_handle: jlong,
+    response_handle: jlong,
+) {
+    debug!("{}: enter", function_name!());
+    native_on_send_request_error(env, error_code, platform_handle, response_handle);
+}
+
+fn native_on_send_request_error(
+    env: JNIEnv<'_>,
+    error_code: jint,
+    platform_handle: jlong,
+    response_handle: jlong,
+) {
+    if let Some(platform) = HANDLE_MAPPING.lock().unwrap().get(&platform_handle) {
+        let platform = (*platform).lock().unwrap();
+        platform.on_send_request_error(error_code, response_handle);
+    } else {
+        let _ = env.throw_new(
+            "com/android/server/remoteauth/jni/BadHandleException",
+            format!("Failed to find Platform with ID {} in {}", platform_handle, function_name!()),
+        );
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    //use super::*;
+
+    //use tokio::runtime::Builder;
+
+    /// Checks validity of the function_name! macro.
+    #[test]
+    fn test_function_name() {
+        assert_eq!(function_name!(), "test_function_name");
+    }
+}
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs b/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
new file mode 100644
index 0000000..e44ab8b
--- /dev/null
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+//! Implementation of JNI protocol functionality.
+use crate::unique_jvm;
+use crate::utils::get_boolean_result;
+use jni::objects::JObject;
+use jni::sys::jboolean;
+use jni::JNIEnv;
+
+/// Initialize native library. Captures Java VM:
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_remoteauth_jni_NativeRemoteAuthJavaPlatform_native_init(
+    env: JNIEnv,
+    _: JObject,
+) -> jboolean {
+    logger::init(
+        logger::Config::default()
+            .with_tag_on_device("remoteauth")
+            .with_max_level(log::LevelFilter::Trace)
+            .with_filter("trace,jni=info"),
+    );
+    get_boolean_result(native_init(env), "native_init")
+}
+
+fn native_init(env: JNIEnv) -> anyhow::Result<()> {
+    let jvm = env.get_java_vm()?;
+    unique_jvm::set_once(jvm)
+}
diff --git a/remoteauth/service/jni/src/unique_jvm.rs b/remoteauth/service/jni/src/unique_jvm.rs
new file mode 100644
index 0000000..46cc361
--- /dev/null
+++ b/remoteauth/service/jni/src/unique_jvm.rs
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+//! takes a JavaVM to a static reference.
+//!
+//! JavaVM is shared as multiple JavaVM within a single process is not allowed
+//! per [JNI spec](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html)
+//! The unique JavaVM need to be shared over (potentially) different threads.
+
+use std::sync::{Arc, Once};
+
+use anyhow::Result;
+use jni::JavaVM;
+
+static mut JVM: Option<Arc<JavaVM>> = None;
+static INIT: Once = Once::new();
+/// set_once sets the unique JavaVM that can be then accessed using get_static_ref()
+///
+/// The function shall only be called once.
+pub(crate) fn set_once(jvm: JavaVM) -> Result<()> {
+    // Safety: follows [this pattern](https://doc.rust-lang.org/std/sync/struct.Once.html).
+    // Modification to static mut is nested inside call_once.
+    unsafe {
+        INIT.call_once(|| {
+            JVM = Some(Arc::new(jvm));
+        });
+    }
+    Ok(())
+}
+/// Gets a 'static reference to the unique JavaVM. Returns None if set_once() was never called.
+pub(crate) fn get_static_ref() -> Option<&'static Arc<JavaVM>> {
+    // Safety: follows [this pattern](https://doc.rust-lang.org/std/sync/struct.Once.html).
+    // Modification to static mut is nested inside call_once.
+    unsafe { JVM.as_ref() }
+}
diff --git a/remoteauth/service/jni/src/utils.rs b/remoteauth/service/jni/src/utils.rs
new file mode 100644
index 0000000..e61b895
--- /dev/null
+++ b/remoteauth/service/jni/src/utils.rs
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+use jni::sys::jboolean;
+use log::error;
+
+pub(crate) fn get_boolean_result<T>(result: anyhow::Result<T>, error_msg: &str) -> jboolean {
+    match result {
+        Ok(_) => true,
+        Err(e) => {
+            error!("{} failed with {:?}", error_msg, &e);
+            false
+        }
+    }
+    .into()
+}
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
new file mode 100644
index 0000000..47b9e31
--- /dev/null
+++ b/remoteauth/tests/unit/Android.bp
@@ -0,0 +1,63 @@
+// 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",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "RemoteAuthUnitTests",
+    defaults: [
+        "enable-remoteauth-targets",
+        "mts-target-sdk-version-current",
+    ],
+    sdk_version: "test_current",
+    min_sdk_version: "31",
+
+    // Include all test java files.
+    srcs: [],
+
+    libs: [
+        "android.test.base",
+        "android.test.mock",
+        "android.test.runner",
+        "framework-annotations-lib",
+    ],
+    compile_multilib: "both",
+
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.ext.truth",
+        "androidx.test.rules",
+        "com.uwb.support.generic",
+        "framework-remoteauth-static",
+        "junit",
+        "libprotobuf-java-lite",
+        "mockito-target-extended-minus-junit4",
+        "mockito-target-minus-junit4",
+        "platform-test-annotations",
+        "service-remoteauth-pre-jarjar",
+        "truth",
+    ],
+    // these are needed for Extended Mockito
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
+}
diff --git a/remoteauth/tests/unit/AndroidManifest.xml b/remoteauth/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..a5294c8
--- /dev/null
+++ b/remoteauth/tests/unit/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?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.remoteauth.test">
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.remoteauth.test"
+        android:label="RemoteAuth Mainline Module Tests"
+        android:directBootAware="true"/>
+</manifest>
diff --git a/remoteauth/tests/unit/AndroidTest.xml b/remoteauth/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..325fdc6
--- /dev/null
+++ b/remoteauth/tests/unit/AndroidTest.xml
@@ -0,0 +1,40 @@
+<?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 RemoteAuth Mainline API Tests.">
+    <!-- Only run tests if the device under test is SDK version 33 (Android 13) or above. -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="RemoteAuthUnitTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="RemoteAuthUnitTests" />
+    <option name="config-descriptor:metadata" key="mainline-param"
+            value="com.google.android.tethering.apex" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.remoteauth.test" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+
+    <!-- Only run RemoteAuthUnitTests in MTS if the RemoteAuth 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/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java b/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java
new file mode 100644
index 0000000..6b43355
--- /dev/null
+++ b/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.remoteauth;
+
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link RemoteAuth}. */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RemoteAuthManagerTest {
+    @Before
+    public void setUp() {}
+
+    @Test
+    public void testStub() {
+        assertTrue(true);
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/RemoteAuthConnectionCacheTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/RemoteAuthConnectionCacheTest.java
new file mode 100644
index 0000000..00f35d3
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/RemoteAuthConnectionCacheTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.remoteauth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.connectivity.Connection;
+import com.android.server.remoteauth.connectivity.ConnectionException;
+import com.android.server.remoteauth.connectivity.ConnectionInfo;
+import com.android.server.remoteauth.connectivity.ConnectivityManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit test for {@link com.android.server.remoteauth.RemoteAuthConnectionCache} */
+@RunWith(AndroidJUnit4.class)
+public class RemoteAuthConnectionCacheTest {
+    @Mock private Connection mConnection;
+    @Mock private ConnectionInfo mConnectionInfo;
+    @Mock private ConnectivityManager mConnectivityManager;
+    private RemoteAuthConnectionCache mConnectionCache;
+
+    private static final int CONNECTION_ID = 1;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        doReturn(CONNECTION_ID).when(mConnectionInfo).getConnectionId();
+        doReturn(mConnectionInfo).when(mConnection).getConnectionInfo();
+        mConnectionCache = new RemoteAuthConnectionCache(mConnectivityManager);
+    }
+
+    @Test
+    public void testCreateCache_managerIsNull() {
+        assertThrows(NullPointerException.class, () -> new RemoteAuthConnectionCache(null));
+    }
+
+    @Test
+    public void testGetManager_managerExists() {
+        assertEquals(mConnectivityManager, mConnectionCache.getConnectivityManager());
+    }
+
+    @Test
+    public void testSetConnectionInfo_infoIsNull() {
+        assertThrows(NullPointerException.class, () -> mConnectionCache.setConnectionInfo(null));
+    }
+
+    @Test
+    public void testSetConnectionInfo_infoIsValid() {
+        mConnectionCache.setConnectionInfo(mConnectionInfo);
+
+        assertEquals(mConnectionInfo, mConnectionCache.getConnectionInfo(CONNECTION_ID));
+    }
+
+    @Test
+    public void testSetConnection_connectionIsNull() {
+        assertThrows(NullPointerException.class, () -> mConnectionCache.setConnection(null));
+    }
+
+    @Test
+    public void testGetConnection_connectionAlreadyExists() {
+        mConnectionCache.setConnection(mConnection);
+
+        assertEquals(mConnection, mConnectionCache.getConnection(CONNECTION_ID));
+    }
+
+    @Test
+    public void testGetConnection_connectionInfoDoesntExists() {
+        assertNull(mConnectionCache.getConnection(CONNECTION_ID));
+    }
+
+    @Test
+    public void testGetConnection_failedToConnect() {
+        mConnectionCache.setConnectionInfo(mConnectionInfo);
+        doReturn(null).when(mConnectivityManager).connect(eq(mConnectionInfo), anyObject());
+
+        assertNull(mConnectionCache.getConnection(CONNECTION_ID));
+    }
+
+    @Test
+    public void testGetConnection_failedToConnectException() {
+        mConnectionCache.setConnectionInfo(mConnectionInfo);
+        doThrow(ConnectionException.class)
+                .when(mConnectivityManager)
+                .connect(eq(mConnectionInfo), anyObject());
+
+        assertNull(mConnectionCache.getConnection(CONNECTION_ID));
+    }
+
+    @Test
+    public void testGetConnection_connectionSucceed() {
+        mConnectionCache.setConnectionInfo(mConnectionInfo);
+        doReturn(mConnection).when(mConnectivityManager).connect(eq(mConnectionInfo), anyObject());
+
+        assertEquals(mConnection, mConnectionCache.getConnection(CONNECTION_ID));
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/RemoteAuthPlatformTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/RemoteAuthPlatformTest.java
new file mode 100644
index 0000000..8975d52
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/RemoteAuthPlatformTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.remoteauth;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.connectivity.Connection;
+import com.android.server.remoteauth.jni.INativeRemoteAuthService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit test for {@link com.android.server.remoteauth.RemoteAuthPlatform} */
+@RunWith(AndroidJUnit4.class)
+public class RemoteAuthPlatformTest {
+    @Mock private Connection mConnection;
+    @Mock private RemoteAuthConnectionCache mConnectionCache;
+    private RemoteAuthPlatform mPlatform;
+    private static final int CONNECTION_ID = 1;
+    private static final byte[] REQUEST = new byte[] {(byte) 0x01};
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mPlatform = new RemoteAuthPlatform(mConnectionCache);
+    }
+
+    @Test
+    public void testSendRequest_connectionIsNull() {
+        doReturn(null).when(mConnectionCache).getConnection(anyInt());
+        assertFalse(
+                mPlatform.sendRequest(
+                        CONNECTION_ID,
+                        REQUEST,
+                        new INativeRemoteAuthService.IPlatform.ResponseCallback() {
+                            @Override
+                            public void onSuccess(byte[] response) {}
+
+                            @Override
+                            public void onFailure(int errorCode) {}
+                        }));
+    }
+
+    @Test
+    public void testSendRequest_connectionExists() {
+        doReturn(mConnection).when(mConnectionCache).getConnection(anyInt());
+        assertTrue(
+                mPlatform.sendRequest(
+                        CONNECTION_ID,
+                        REQUEST,
+                        new INativeRemoteAuthService.IPlatform.ResponseCallback() {
+                            @Override
+                            public void onSuccess(byte[] response) {}
+
+                            @Override
+                            public void onFailure(int errorCode) {}
+                        }));
+        verify(mConnection, times(1)).sendRequest(eq(REQUEST), anyObject());
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/RemoteAuthServiceTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/RemoteAuthServiceTest.java
new file mode 100644
index 0000000..c6199ff
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/RemoteAuthServiceTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.remoteauth;
+
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link RemoteAuthServer}. */
+@RunWith(AndroidJUnit4.class)
+public class RemoteAuthServiceTest {
+    @Test
+    public void testStub() {
+        assertTrue(true);
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/CdmConnectivityManagerTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/CdmConnectivityManagerTest.java
new file mode 100644
index 0000000..824344a
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/CdmConnectivityManagerTest.java
@@ -0,0 +1,304 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.google.common.util.concurrent.SettableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/** Unit tests for {@link CdmConnectivityManager}. */
+@RunWith(AndroidJUnit4.class)
+public class CdmConnectivityManagerTest {
+    @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+
+    @Mock CompanionDeviceManagerWrapper mCompanionDeviceManagerWrapper;
+
+    private CdmConnectivityManager mCdmConnectivityManager;
+    private ExecutorService mTestExecutor = Executors.newSingleThreadExecutor();
+
+    @Before
+    public void setUp() {
+        mCdmConnectivityManager =
+                new CdmConnectivityManager(mTestExecutor, mCompanionDeviceManagerWrapper);
+    }
+
+    @After
+    public void tearDown() {
+        mTestExecutor.shutdown();
+    }
+
+    @Test
+    public void testStartDiscovery_callsGetAllAssociationsOnce() throws InterruptedException {
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), Utils.getFakeDiscoveredDeviceReceiver());
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations();
+    }
+
+    @Test
+    public void testStartDiscovery_fetchesNoAssociations() {
+        SettableFuture<Boolean> future = SettableFuture.create();
+
+        when(mCompanionDeviceManagerWrapper.getAllAssociations())
+                .thenReturn(Utils.getFakeAssociationInfoList(0));
+
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+
+                    @Override
+                    public void onLost(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+                };
+
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver);
+
+        assertThat(future.isDone()).isFalse();
+    }
+
+    @Test
+    public void testStartDiscovery_DoesNotReturnNonWatchAssociations() throws InterruptedException {
+        SettableFuture<Boolean> future = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+
+                    @Override
+                    public void onLost(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+                };
+
+        when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class)))
+                .thenReturn(Utils.FAKE_DEVICE_PROFILE);
+
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver);
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations();
+        verify(mCompanionDeviceManagerWrapper, times(0))
+                .getDeviceProfile(any(AssociationInfo.class));
+        assertThat(future.isDone()).isFalse();
+    }
+
+    @Test
+    public void testStartDiscovery_returnsOneWatchAssociation() throws InterruptedException {
+        SettableFuture<Boolean> future = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+                };
+
+        when(mCompanionDeviceManagerWrapper.getAllAssociations())
+                .thenReturn(Utils.getFakeAssociationInfoList(1));
+        when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class)))
+                .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH);
+
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver);
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations();
+        verify(mCompanionDeviceManagerWrapper, times(1))
+                .getDeviceProfile(any(AssociationInfo.class));
+        assertThat(future.isDone()).isTrue();
+    }
+
+    @Test
+    public void testStartDiscovery_returnsMultipleWatchAssociations() throws InterruptedException {
+        int numAssociations = 3;
+        SettableFuture<Boolean> future = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    int mNumCallbacks = 0;
+
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        ++mNumCallbacks;
+                        if (mNumCallbacks == numAssociations) {
+                            future.set(true);
+                        }
+                    }
+                };
+
+        when(mCompanionDeviceManagerWrapper.getAllAssociations())
+                .thenReturn(Utils.getFakeAssociationInfoList(numAssociations));
+        when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class)))
+                .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH);
+
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver);
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations();
+        verify(mCompanionDeviceManagerWrapper, times(numAssociations))
+                .getDeviceProfile(any(AssociationInfo.class));
+        assertThat(future.isDone()).isTrue();
+    }
+
+    @Test
+    public void testMultipleStartDiscovery_runsAllCallbacks() throws InterruptedException {
+        SettableFuture<Boolean> future1 = SettableFuture.create();
+        SettableFuture<Boolean> future2 = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver1 =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future1.set(true);
+                    }
+                };
+        DiscoveredDeviceReceiver discoveredDeviceReceiver2 =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future2.set(true);
+                    }
+                };
+
+        when(mCompanionDeviceManagerWrapper.getAllAssociations())
+                .thenReturn(Utils.getFakeAssociationInfoList(1));
+        when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class)))
+                .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH);
+
+        // Start discovery twice with different callbacks.
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver1);
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver2);
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(2)).getAllAssociations();
+        verify(mCompanionDeviceManagerWrapper, times(2))
+                .getDeviceProfile(any(AssociationInfo.class));
+        assertThat(future1.isDone()).isTrue();
+        assertThat(future2.isDone()).isTrue();
+    }
+
+    @Test
+    public void testStartDiscovery_returnsExpectedDiscoveredDevice() throws InterruptedException {
+        SettableFuture<Boolean> future = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice device) {
+                        assertThat(device.getConnectionInfo() instanceof CdmConnectionInfo)
+                                .isTrue();
+
+                        CdmConnectionInfo connectionInfo =
+                                (CdmConnectionInfo) device.getConnectionInfo();
+                        if (connectionInfo.getConnectionParams().getDeviceMacAddress().toString()
+                                .equals(Utils.FAKE_PEER_ADDRESS)
+                                && connectionInfo.getConnectionId() == Utils.FAKE_CONNECTION_ID) {
+                            future.set(true);
+                        }
+                    }
+                };
+
+        when(mCompanionDeviceManagerWrapper.getAllAssociations())
+                .thenReturn(Utils.getFakeAssociationInfoList(1));
+        when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class)))
+                .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH);
+
+        mCdmConnectivityManager.startDiscovery(
+                Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver);
+
+        mTestExecutor.awaitTermination(1, TimeUnit.SECONDS);
+
+        verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations();
+        verify(mCompanionDeviceManagerWrapper, times(1))
+                .getDeviceProfile(any(AssociationInfo.class));
+        assertThat(future.isDone()).isTrue();
+    }
+
+    @Test
+    public void testStopDiscovery_removesCallback() {
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {}
+                };
+
+        DiscoveryFilter discoveryFilter = Utils.getFakeDiscoveryFilter();
+        mCdmConnectivityManager.startDiscovery(discoveryFilter, discoveredDeviceReceiver);
+
+        assertThat(mCdmConnectivityManager.hasPendingCallbacks(discoveredDeviceReceiver)).isTrue();
+
+        mCdmConnectivityManager.stopDiscovery(discoveryFilter, discoveredDeviceReceiver);
+
+        assertThat(mCdmConnectivityManager.hasPendingCallbacks(discoveredDeviceReceiver)).isFalse();
+    }
+
+    @Test
+    public void testStopDiscovery_DoesNotRunCallback() {
+        SettableFuture future = SettableFuture.create();
+        DiscoveredDeviceReceiver discoveredDeviceReceiver =
+                new DiscoveredDeviceReceiver() {
+                    @Override
+                    public void onDiscovered(DiscoveredDevice unused) {
+                        future.set(true);
+                    }
+                };
+
+        DiscoveryFilter discoveryFilter = Utils.getFakeDiscoveryFilter();
+        mCdmConnectivityManager.startDiscovery(discoveryFilter, discoveredDeviceReceiver);
+        mCdmConnectivityManager.stopDiscovery(discoveryFilter, discoveredDeviceReceiver);
+
+        assertThat(future.isDone()).isFalse();
+    }
+}
+
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/ConnectivityManagerFactoryTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/ConnectivityManagerFactoryTest.java
new file mode 100644
index 0000000..42eff90
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/ConnectivityManagerFactoryTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link CdmConnectivityManager}. */
+@RunWith(AndroidJUnit4.class)
+public class ConnectivityManagerFactoryTest {
+
+    @Before
+    public void setUp() {}
+
+    @Test
+    public void testFactory_returnsConnectivityManager() {
+        final Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        ConnectivityManager connectivityManager =
+                ConnectivityManagerFactory.getConnectivityManager(context);
+
+        assertThat(connectivityManager).isNotNull();
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/Utils.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/Utils.java
new file mode 100644
index 0000000..a5c992a
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/Utils.java
@@ -0,0 +1,99 @@
+/*
+ * 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.remoteauth.connectivity;
+
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
+import android.content.pm.PackageManager;
+import android.net.MacAddress;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class Utils {
+    public static final int FAKE_CONNECTION_ID = 1;
+    public static final int FAKE_USER_ID = 0;
+    public static final String FAKE_DISPLAY_NAME = "FAKE-DISPLAY-NAME";
+    public static final String FAKE_PEER_ADDRESS = "ff:ff:ff:ff:ff:ff";
+    public static final String FAKE_DEVICE_PROFILE = "FAKE-DEVICE-PROFILE";
+    public static final String FAKE_PACKAGE_NAME = "FAKE-PACKAGE-NAME";
+
+    public static DiscoveryFilter getFakeDiscoveryFilter() {
+        return DiscoveryFilter.Builder.builder()
+                .setDeviceName(FAKE_DISPLAY_NAME)
+                .setPeerAddress("FAKE-PEER-ADDRESS")
+                .setDeviceType(DiscoveryFilter.WATCH)
+                .build();
+    }
+
+    public static DiscoveredDeviceReceiver getFakeDiscoveredDeviceReceiver() {
+        return new DiscoveredDeviceReceiver() {
+            @Override
+            public void onDiscovered(DiscoveredDevice unused) {}
+
+            @Override
+            public void onLost(DiscoveredDevice unused) {}
+        };
+    }
+
+    /**
+     * Returns a fake CDM connection info.
+     *
+     * @return connection info.
+     */
+    public static CdmConnectionInfo getFakeCdmConnectionInfo()
+            throws PackageManager.NameNotFoundException {
+        return new CdmConnectionInfo(FAKE_CONNECTION_ID, getFakeAssociationInfoList(1).get(0));
+    }
+
+    /**
+     * Returns a fake discovered device.
+     *
+     * @return discovered device.
+     */
+    public static DiscoveredDevice getFakeCdmDiscoveredDevice()
+            throws PackageManager.NameNotFoundException {
+        return new DiscoveredDevice(getFakeCdmConnectionInfo(), FAKE_DISPLAY_NAME);
+    }
+
+    /**
+     * Returns fake association info array.
+     *
+     * <p> Creates an AssociationInfo object with fake values.
+     *
+     * @param associationsSize number of fake association info entries to return.
+     * @return list of {@link AssociationInfo} or null.
+     */
+    public static List<AssociationInfo> getFakeAssociationInfoList(int associationsSize) {
+        if (associationsSize > 0) {
+            List<AssociationInfo> associations = new ArrayList<>();
+            // Association id starts from 1.
+            for (int i = 1; i <= associationsSize; ++i) {
+                associations.add(
+                        (new AssociationInfo.Builder(i, FAKE_USER_ID, FAKE_PACKAGE_NAME))
+                                .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
+                                .setDisplayName(FAKE_DISPLAY_NAME)
+                                .setDeviceMacAddress(MacAddress.fromString(FAKE_PEER_ADDRESS))
+                                .build());
+            }
+            return associations;
+        }
+        return new ArrayList<>();
+    }
+
+    private Utils() {}
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingCapabilitiesTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingCapabilitiesTest.java
new file mode 100644
index 0000000..8135b4f
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingCapabilitiesTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link RangingCapabilities}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingCapabilitiesTest {
+    private static final androidx.core.uwb.backend.impl.internal.RangingCapabilities
+            TEST_UWB_RANGING_CAPABILITIES =
+                    new androidx.core.uwb.backend.impl.internal.RangingCapabilities(
+                            /* supportsDistance= */ true,
+                            /* supportsAzimuthalAngle= */ true,
+                            /* supportsElevationAngle= */ true);
+
+    @Test
+    public void testBuildingRangingCapabilities_success() {
+        final RangingCapabilities rangingCapabilities =
+                new RangingCapabilities.Builder()
+                        .addSupportedRangingMethods(RANGING_METHOD_UWB)
+                        .setUwbRangingCapabilities(TEST_UWB_RANGING_CAPABILITIES)
+                        .build();
+
+        assertEquals(rangingCapabilities.getSupportedRangingMethods().size(), 1);
+        assertEquals(
+                (int) rangingCapabilities.getSupportedRangingMethods().get(0), RANGING_METHOD_UWB);
+        assertEquals(
+                rangingCapabilities.getUwbRangingCapabilities(), TEST_UWB_RANGING_CAPABILITIES);
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingManagerTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingManagerTest.java
new file mode 100644
index 0000000..6e343bb
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingManagerTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static android.content.pm.PackageManager.FEATURE_UWB;
+import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE;
+
+import static androidx.core.uwb.backend.impl.internal.RangingCapabilities.FIRA_DEFAULT_SUPPORTED_CONFIG_IDS;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_INDIVIDUAL_MULTICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_MULTICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_UNICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_UNICAST_DS_TWR_NO_AOA;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+
+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.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.uwb.UwbManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+import com.android.server.remoteauth.ranging.SessionParameters.DeviceRole;
+
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraSpecificationParams;
+import com.google.uwb.support.generic.GenericSpecificationParams;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+/** Unit test for {@link RangingManager}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingManagerTest {
+    private static final List<Integer> TEST_UWB_SUPPORTED_CHANNELS = List.of(8, 9);
+    private static final FiraSpecificationParams TEST_FIRA_SPEC =
+            new FiraSpecificationParams.Builder()
+                    .setSupportedChannels(TEST_UWB_SUPPORTED_CHANNELS)
+                    .setStsCapabilities(EnumSet.allOf(FiraParams.StsCapabilityFlag.class))
+                    .build();
+    private static final GenericSpecificationParams TEST_GENERIC_SPEC =
+            new GenericSpecificationParams.Builder()
+                    .setFiraSpecificationParams(TEST_FIRA_SPEC)
+                    .build();
+    private static final String TEST_DEVICE_ID = "test_device_id";
+    @RangingMethod private static final int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+    @DeviceRole private static final int TEST_DEVICE_ROLE = DEVICE_ROLE_INITIATOR;
+    private static final float TEST_LOWER_PROXIMITY_BOUNDARY_M = 1.0f;
+    private static final float TEST_UPPER_PROXIMITY_BOUNDARY_M = 2.5f;
+    private static final boolean TEST_AUTO_DERIVE_PARAMS = true;
+    private static final byte[] TEST_BASE_KEY =
+            new byte[] {
+                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+                0x0e, 0x0f
+            };
+    private static final byte[] TEST_SYNC_DATA =
+            new byte[] {
+                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+                0x0f, 0x00
+            };
+    private static final SessionParameters TEST_SESSION_PARAMETER =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(TEST_DEVICE_ROLE)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                    .setBaseKey(TEST_BASE_KEY)
+                    .setSyncData(TEST_SYNC_DATA)
+                    .build();
+
+    @Mock private Context mContext;
+    @Mock private PackageManager mPackageManager;
+    @Mock private UwbManager mUwbManager;
+
+    private RangingManager mRangingManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mContext.getSystemService(UwbManager.class)).thenReturn(mUwbManager);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(false);
+        when(mUwbManager.getAdapterState()).thenReturn(STATE_ENABLED_INACTIVE);
+        when(mUwbManager.getSpecificationInfo()).thenReturn(TEST_GENERIC_SPEC.toBundle());
+    }
+
+    @Test
+    public void testConstruction() {
+        mRangingManager = new RangingManager(mContext);
+        verifyZeroInteractions(mUwbManager);
+    }
+
+    @Test
+    public void testConstruction_withUwbEnabled() {
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(true);
+
+        mRangingManager = new RangingManager(mContext);
+
+        verify(mUwbManager).getAdapterState();
+        verify(mUwbManager).registerAdapterStateCallback(any(), any());
+    }
+
+    @Test
+    public void testShutdown_withUwbEnabled() {
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(true);
+
+        mRangingManager = new RangingManager(mContext);
+        mRangingManager.shutdown();
+
+        verify(mUwbManager).registerAdapterStateCallback(any(), any());
+        verify(mUwbManager).unregisterAdapterStateCallback(any());
+    }
+
+    @Test
+    public void testGetRangingCapabilities() {
+        mRangingManager = new RangingManager(mContext);
+        RangingCapabilities capabilities = mRangingManager.getRangingCapabilities();
+
+        assertEquals(capabilities.getSupportedRangingMethods().size(), 0);
+        assertEquals(capabilities.getUwbRangingCapabilities(), null);
+    }
+
+    @Test
+    public void testGetRangingCapabilities_withUwbEnabled() {
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(true);
+
+        mRangingManager = new RangingManager(mContext);
+        RangingCapabilities capabilities = mRangingManager.getRangingCapabilities();
+
+        List<Integer> supportedConfigIds = new ArrayList<>(FIRA_DEFAULT_SUPPORTED_CONFIG_IDS);
+        supportedConfigIds.add(CONFIG_PROVISIONED_UNICAST_DS_TWR);
+        supportedConfigIds.add(CONFIG_PROVISIONED_MULTICAST_DS_TWR);
+        supportedConfigIds.add(CONFIG_PROVISIONED_UNICAST_DS_TWR_NO_AOA);
+        supportedConfigIds.add(CONFIG_PROVISIONED_INDIVIDUAL_MULTICAST_DS_TWR);
+
+        verify(mUwbManager, times(1)).getSpecificationInfo();
+        assertEquals(capabilities.getSupportedRangingMethods().size(), 1);
+        assertEquals((int) capabilities.getSupportedRangingMethods().get(0), RANGING_METHOD_UWB);
+        androidx.core.uwb.backend.impl.internal.RangingCapabilities uwbCapabilities =
+                capabilities.getUwbRangingCapabilities();
+        assertNotNull(uwbCapabilities);
+        assertArrayEquals(
+                uwbCapabilities.getSupportedChannels().toArray(),
+                TEST_UWB_SUPPORTED_CHANNELS.toArray());
+        assertArrayEquals(
+                uwbCapabilities.getSupportedConfigIds().toArray(), supportedConfigIds.toArray());
+    }
+
+    @Test
+    public void testGetRangingCapabilities_multipleCalls() {
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(true);
+
+        mRangingManager = new RangingManager(mContext);
+        RangingCapabilities capabilities1 = mRangingManager.getRangingCapabilities();
+        RangingCapabilities capabilities2 = mRangingManager.getRangingCapabilities();
+        RangingCapabilities capabilities3 = mRangingManager.getRangingCapabilities();
+
+        verify(mUwbManager, times(1)).getSpecificationInfo();
+        assertEquals(capabilities1, capabilities2);
+        assertEquals(capabilities2, capabilities3);
+    }
+
+    @Test
+    public void testCreateSession_nullSessionParameters() {
+        mRangingManager = new RangingManager(mContext);
+
+        assertThrows(NullPointerException.class, () -> mRangingManager.createSession(null));
+    }
+
+    @Test
+    public void testCreateSession_uwbSessionWithUwbDisabled() {
+        mRangingManager = new RangingManager(mContext);
+
+        assertNull(mRangingManager.createSession(TEST_SESSION_PARAMETER));
+    }
+
+    @Test
+    public void testCreateSession_uwbSession() {
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(true);
+        mRangingManager = new RangingManager(mContext);
+
+        assertNotNull(mRangingManager.createSession(TEST_SESSION_PARAMETER));
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingParametersTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingParametersTest.java
new file mode 100644
index 0000000..3be5e70
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingParametersTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static androidx.core.uwb.backend.impl.internal.RangingDevice.SESSION_ID_UNSET;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_UNICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.DURATION_1_MS;
+import static androidx.core.uwb.backend.impl.internal.Utils.NORMAL;
+
+import static com.google.uwb.support.fira.FiraParams.UWB_CHANNEL_9;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+import androidx.core.uwb.backend.impl.internal.UwbComplexChannel;
+import androidx.core.uwb.backend.impl.internal.UwbRangeDataNtfConfig;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** Unit test for {@link RangingParameters}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingParametersTest {
+
+    private static final UwbAddress TEST_UWB_LOCAL_ADDRESS =
+            UwbAddress.fromBytes(new byte[] {0x00, 0x01});
+    private static final androidx.core.uwb.backend.impl.internal.RangingParameters
+            TEST_UWB_RANGING_PARAMETERS =
+                    new androidx.core.uwb.backend.impl.internal.RangingParameters(
+                            CONFIG_PROVISIONED_UNICAST_DS_TWR,
+                            /* sessionId= */ SESSION_ID_UNSET,
+                            /* subSessionId= */ SESSION_ID_UNSET,
+                            /* SessionInfo= */ new byte[] {},
+                            /* subSessionInfo= */ new byte[] {},
+                            new UwbComplexChannel(UWB_CHANNEL_9, /* preambleIndex= */ 9),
+                            List.of(UwbAddress.fromBytes(new byte[] {0x00, 0x02})),
+                            /* rangingUpdateRate= */ NORMAL,
+                            new UwbRangeDataNtfConfig.Builder().build(),
+                            /* slotDuration= */ DURATION_1_MS,
+                            /* isAoaDisabled= */ false);
+
+    @Test
+    public void testBuildingRangingParameters_success() {
+        final RangingParameters rangingParameters =
+                new RangingParameters.Builder()
+                        .setUwbLocalAddress(TEST_UWB_LOCAL_ADDRESS)
+                        .setUwbRangingParameters(TEST_UWB_RANGING_PARAMETERS)
+                        .build();
+
+        assertEquals(rangingParameters.getUwbLocalAddress(), TEST_UWB_LOCAL_ADDRESS);
+        assertEquals(rangingParameters.getUwbRangingParameters(), TEST_UWB_RANGING_PARAMETERS);
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingReportTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingReportTest.java
new file mode 100644
index 0000000..6ac56ea
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingReportTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_INSIDE;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingReport.ProximityState;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link RangingReport}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingReportTest {
+
+    private static final float TEST_DISTANCE_M = 1.5f;
+    @ProximityState private static final int TEST_PROXIMITY_STATE = PROXIMITY_STATE_INSIDE;
+
+    @Test
+    public void testBuildingRangingReport_success() {
+        final RangingReport rangingReport =
+                new RangingReport.Builder()
+                        .setDistanceM(TEST_DISTANCE_M)
+                        .setProximityState(TEST_PROXIMITY_STATE)
+                        .build();
+
+        assertEquals(rangingReport.getDistanceM(), TEST_DISTANCE_M, 0.0f);
+        assertEquals(rangingReport.getProximityState(), TEST_PROXIMITY_STATE);
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingSessionTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingSessionTest.java
new file mode 100644
index 0000000..0e547d6
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingSessionTest.java
@@ -0,0 +1,237 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+
+import static org.junit.Assert.assertArrayEquals;
+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 android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+import com.android.server.remoteauth.ranging.SessionParameters.DeviceRole;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.Executor;
+
+/** Unit test for {@link RangingSession}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingSessionTest {
+
+    private static final String TEST_DEVICE_ID = "test_device_id";
+    @RangingMethod private static final int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+    @DeviceRole private static final int TEST_DEVICE_ROLE = DEVICE_ROLE_INITIATOR;
+    private static final float TEST_LOWER_PROXIMITY_BOUNDARY_M = 1.0f;
+    private static final float TEST_UPPER_PROXIMITY_BOUNDARY_M = 2.5f;
+    private static final byte[] TEST_BASE_KEY =
+            new byte[] {
+                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+                0x0e, 0x0f
+            };
+    private static final byte[] TEST_BASE_KEY2 =
+            new byte[] {
+                0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x0,
+                0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7
+            };
+    private static final byte[] TEST_SYNC_DATA =
+            new byte[] {
+                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+                0x0f, 0x00
+            };
+    private static final byte[] TEST_SYNC_DATA2 =
+            new byte[] {
+                0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+                0x0f, 0x00
+            };
+
+    private static final SessionParameters TEST_SESSION_PARAMETER_WITH_AD =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(TEST_DEVICE_ROLE)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .setAutoDeriveParams(true)
+                    .setBaseKey(TEST_BASE_KEY)
+                    .setSyncData(TEST_SYNC_DATA)
+                    .build();
+    private static final SessionParameters TEST_SESSION_PARAMETER_WO_AD =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(TEST_DEVICE_ROLE)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .setAutoDeriveParams(false)
+                    .setBaseKey(TEST_BASE_KEY)
+                    .setSyncData(TEST_SYNC_DATA)
+                    .build();
+    private static final int TEST_DERIVE_DATA_LENGTH = 40;
+
+    /** Wrapper class for testing {@link RangingSession}. */
+    public static class RangingSessionWrapper extends RangingSession {
+        public RangingSessionWrapper(
+                Context context, SessionParameters sessionParameters, int derivedDataLength) {
+            super(context, sessionParameters, derivedDataLength);
+        }
+
+        @Override
+        public void start(
+                RangingParameters rangingParameters,
+                Executor executor,
+                RangingCallback rangingCallback) {}
+
+        @Override
+        public void stop() {}
+
+        @Override
+        public boolean updateDerivedData() {
+            return super.updateDerivedData();
+        }
+
+        public byte[] baseKey() {
+            return mBaseKey;
+        }
+
+        public byte[] syncData() {
+            return mSyncData;
+        }
+
+        public byte[] derivedData() {
+            return mDerivedData;
+        }
+
+        public int syncCounter() {
+            return mSyncCounter;
+        }
+    }
+
+    @Mock private Context mContext;
+
+    private RangingSessionWrapper mRangingSessionWithAD;
+    private RangingSessionWrapper mRangingSessionWithoutAD;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mRangingSessionWithAD =
+                new RangingSessionWrapper(
+                        mContext, TEST_SESSION_PARAMETER_WITH_AD, TEST_DERIVE_DATA_LENGTH);
+        mRangingSessionWithoutAD =
+                new RangingSessionWrapper(mContext, TEST_SESSION_PARAMETER_WO_AD, 0);
+    }
+
+    @Test
+    public void testResetBaseKey_autoDeriveDisabled() {
+        assertNull(mRangingSessionWithoutAD.baseKey());
+        mRangingSessionWithoutAD.resetBaseKey(TEST_BASE_KEY2);
+        assertNull(mRangingSessionWithoutAD.baseKey());
+    }
+
+    @Test
+    public void testResetBaseKey_nullBaseKey() {
+        assertThrows(NullPointerException.class, () -> mRangingSessionWithAD.resetBaseKey(null));
+    }
+
+    @Test
+    public void testResetBaseKey_invalidBaseKey() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mRangingSessionWithAD.resetBaseKey(new byte[] {0x1, 0x2, 0x3, 0x4}));
+    }
+
+    @Test
+    public void testResetBaseKey_success() {
+        mRangingSessionWithAD.resetBaseKey(TEST_BASE_KEY2);
+        assertArrayEquals(mRangingSessionWithAD.baseKey(), TEST_BASE_KEY2);
+        assertEquals(mRangingSessionWithAD.syncCounter(), 2);
+
+        mRangingSessionWithAD.resetBaseKey(TEST_BASE_KEY);
+        assertArrayEquals(mRangingSessionWithAD.baseKey(), TEST_BASE_KEY);
+        assertEquals(mRangingSessionWithAD.syncCounter(), 3);
+    }
+
+    @Test
+    public void testResetSyncData_autoDeriveDisabled() {
+        assertNull(mRangingSessionWithoutAD.syncData());
+        mRangingSessionWithoutAD.resetSyncData(TEST_SYNC_DATA2);
+        assertNull(mRangingSessionWithoutAD.syncData());
+    }
+
+    @Test
+    public void testResetSyncData_nullSyncData() {
+        assertThrows(NullPointerException.class, () -> mRangingSessionWithAD.resetSyncData(null));
+    }
+
+    @Test
+    public void testResetSyncData_invalidSyncData() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mRangingSessionWithAD.resetSyncData(new byte[] {0x1, 0x2, 0x3, 0x4}));
+    }
+
+    @Test
+    public void testResetSyncData_success() {
+        mRangingSessionWithAD.resetSyncData(TEST_SYNC_DATA2);
+        assertArrayEquals(mRangingSessionWithAD.syncData(), TEST_SYNC_DATA2);
+        assertEquals(mRangingSessionWithAD.syncCounter(), 1);
+
+        mRangingSessionWithAD.resetSyncData(TEST_SYNC_DATA);
+        assertArrayEquals(mRangingSessionWithAD.syncData(), TEST_SYNC_DATA);
+        assertEquals(mRangingSessionWithAD.syncCounter(), 1);
+    }
+
+    @Test
+    public void testUpdateDerivedData_autoDeriveDisabled() {
+        assertFalse(mRangingSessionWithoutAD.updateDerivedData());
+        assertEquals(mRangingSessionWithoutAD.syncCounter(), 0);
+    }
+
+    @Test
+    public void testUpdateDerivedData_hkdfFailed() {
+        // Max derivedDataLength is 32*255
+        RangingSessionWrapper rangingSession =
+                new RangingSessionWrapper(
+                        mContext, TEST_SESSION_PARAMETER_WITH_AD, /* derivedDataLength= */ 10000);
+        assertNull(rangingSession.derivedData());
+        assertFalse(rangingSession.updateDerivedData());
+        assertEquals(rangingSession.syncCounter(), 0);
+        assertNull(rangingSession.derivedData());
+    }
+
+    @Test
+    public void testUpdateDerivedData_success() {
+        assertNotNull(mRangingSessionWithAD.derivedData());
+        assertTrue(mRangingSessionWithAD.updateDerivedData());
+        assertEquals(mRangingSessionWithAD.syncCounter(), 2);
+        assertNotNull(mRangingSessionWithAD.derivedData());
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionInfoTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionInfoTest.java
new file mode 100644
index 0000000..9364092
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionInfoTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link SessionInfo}. */
+@RunWith(AndroidJUnit4.class)
+public class SessionInfoTest {
+
+    private static final String TEST_DEVICE_ID = new String("test_device_id");
+    private static final @RangingMethod int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+
+    @Test
+    public void testBuildingSessionInfo_success() {
+        final SessionInfo sessionInfo =
+                new SessionInfo.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .build();
+
+        assertEquals(sessionInfo.getDeviceId(), TEST_DEVICE_ID);
+        assertEquals(sessionInfo.getRangingMethod(), TEST_RANGING_METHOD);
+    }
+
+    @Test
+    public void testBuildingSessionInfo_invalidDeviceId() {
+        final SessionInfo.Builder builder =
+                new SessionInfo.Builder().setRangingMethod(TEST_RANGING_METHOD);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionInfo_invalidRangingMethod() {
+        final SessionInfo.Builder builder = new SessionInfo.Builder().setDeviceId(TEST_DEVICE_ID);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionParametersTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionParametersTest.java
new file mode 100644
index 0000000..522623e
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionParametersTest.java
@@ -0,0 +1,235 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+import com.android.server.remoteauth.ranging.SessionParameters.DeviceRole;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link SessionParameters}. */
+@RunWith(AndroidJUnit4.class)
+public class SessionParametersTest {
+
+    private static final String TEST_DEVICE_ID = "test_device_id";
+    @RangingMethod private static final int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+    @DeviceRole private static final int TEST_DEVICE_ROLE = DEVICE_ROLE_INITIATOR;
+    private static final float TEST_LOWER_PROXIMITY_BOUNDARY_M = 1.0f;
+    private static final float TEST_UPPER_PROXIMITY_BOUNDARY_M = 2.5f;
+    private static final boolean TEST_AUTO_DERIVE_PARAMS = true;
+    private static final byte[] TEST_BASE_KEY =
+            new byte[] {
+                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+                0x0e, 0x0f
+            };
+    private static final byte[] TEST_SYNC_DATA =
+            new byte[] {
+                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+                0x0f, 0x00
+            };
+
+    @Test
+    public void testBuildingSessionParameters_success() {
+        final SessionParameters sessionParameters =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA)
+                        .build();
+
+        assertEquals(sessionParameters.getDeviceId(), TEST_DEVICE_ID);
+        assertEquals(sessionParameters.getRangingMethod(), TEST_RANGING_METHOD);
+        assertEquals(
+                sessionParameters.getLowerProximityBoundaryM(),
+                TEST_LOWER_PROXIMITY_BOUNDARY_M,
+                0.0f);
+        assertEquals(
+                sessionParameters.getUpperProximityBoundaryM(),
+                TEST_UPPER_PROXIMITY_BOUNDARY_M,
+                0.0f);
+        assertEquals(sessionParameters.getAutoDeriveParams(), TEST_AUTO_DERIVE_PARAMS);
+        assertArrayEquals(sessionParameters.getBaseKey(), TEST_BASE_KEY);
+        assertArrayEquals(sessionParameters.getSyncData(), TEST_SYNC_DATA);
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidDeviceId() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidRangingMethod() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidDeviceRole() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidLowerProximityBoundaryM() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(-1.0f)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidUpperProximityBoundaryM() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M - 0.1f)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_disableAutoDeriveParams() {
+        final boolean autoDeriveParams = false;
+        final SessionParameters sessionParameters =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(autoDeriveParams)
+                        .build();
+
+        assertEquals(sessionParameters.getAutoDeriveParams(), autoDeriveParams);
+        assertArrayEquals(sessionParameters.getBaseKey(), new byte[] {});
+        assertArrayEquals(sessionParameters.getSyncData(), new byte[] {});
+    }
+
+    @Test
+    public void testBuildingSessionParameters_emptyBaseKey() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidBaseKey() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                        .setBaseKey(new byte[] {0x00, 0x01, 0x02, 0x13})
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_emptySyncData() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                        .setBaseKey(TEST_BASE_KEY);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidSyncData() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(new byte[] {0x00, 0x01, 0x02, 0x13});
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/UwbRangingSessionTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/UwbRangingSessionTest.java
new file mode 100644
index 0000000..91198ab
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/UwbRangingSessionTest.java
@@ -0,0 +1,375 @@
+/*
+ * 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.remoteauth.ranging;
+
+import static androidx.core.uwb.backend.impl.internal.RangingDevice.SESSION_ID_UNSET;
+import static androidx.core.uwb.backend.impl.internal.RangingMeasurement.CONFIDENCE_HIGH;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_UNICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.DURATION_1_MS;
+import static androidx.core.uwb.backend.impl.internal.Utils.NORMAL;
+import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_ERROR;
+import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_OK;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_INSIDE;
+import static com.android.server.remoteauth.ranging.RangingSession.RANGING_ERROR_FAILED_TO_START;
+import static com.android.server.remoteauth.ranging.RangingSession.RANGING_ERROR_FAILED_TO_STOP;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_RESPONDER;
+
+import static com.google.uwb.support.fira.FiraParams.UWB_CHANNEL_9;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.core.uwb.backend.impl.internal.RangingControlee;
+import androidx.core.uwb.backend.impl.internal.RangingController;
+import androidx.core.uwb.backend.impl.internal.RangingMeasurement;
+import androidx.core.uwb.backend.impl.internal.RangingPosition;
+import androidx.core.uwb.backend.impl.internal.RangingSessionCallback;
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+import androidx.core.uwb.backend.impl.internal.UwbComplexChannel;
+import androidx.core.uwb.backend.impl.internal.UwbDevice;
+import androidx.core.uwb.backend.impl.internal.UwbRangeDataNtfConfig;
+import androidx.core.uwb.backend.impl.internal.UwbServiceImpl;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+import com.android.server.remoteauth.ranging.RangingSession.RangingCallback;
+
+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 org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/** Unit test for {@link UwbRangingSession}. */
+@RunWith(AndroidJUnit4.class)
+public class UwbRangingSessionTest {
+
+    private static final String TEST_DEVICE_ID = "test_device_id";
+    @RangingMethod private static final int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+    private static final float TEST_LOWER_PROXIMITY_BOUNDARY_M = 1.0f;
+    private static final float TEST_UPPER_PROXIMITY_BOUNDARY_M = 2.5f;
+    private static final boolean TEST_AUTO_DERIVE_PARAMS = true;
+    private static final byte[] TEST_BASE_KEY =
+            new byte[] {
+                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+                0x0e, 0x0f
+            };
+    private static final byte[] TEST_SYNC_DATA =
+            new byte[] {
+                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+                0x0f, 0x00
+            };
+    private static final SessionParameters TEST_SESSION_PARAMETER_INITIATOR =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(DEVICE_ROLE_INITIATOR)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .build();
+    private static final SessionParameters TEST_SESSION_PARAMETER_RESPONDER =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(DEVICE_ROLE_RESPONDER)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .build();
+    private static final SessionParameters TEST_SESSION_PARAMETER_INITIATOR_W_AD =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(DEVICE_ROLE_INITIATOR)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                    .setBaseKey(TEST_BASE_KEY)
+                    .setSyncData(TEST_SYNC_DATA)
+                    .build();
+    private static final UwbAddress TEST_UWB_LOCAL_ADDRESS =
+            UwbAddress.fromBytes(new byte[] {0x00, 0x01});
+    private static final UwbAddress TEST_UWB_PEER_ADDRESS =
+            UwbAddress.fromBytes(new byte[] {0x00, 0x02});
+    private static final UwbComplexChannel TEST_UWB_COMPLEX_CHANNEL =
+            new UwbComplexChannel(UWB_CHANNEL_9, /* preambleIndex= */ 9);
+    private static final androidx.core.uwb.backend.impl.internal.RangingParameters
+            TEST_UWB_RANGING_PARAMETERS =
+                    new androidx.core.uwb.backend.impl.internal.RangingParameters(
+                            CONFIG_PROVISIONED_UNICAST_DS_TWR,
+                            /* sessionId= */ SESSION_ID_UNSET,
+                            /* subSessionId= */ SESSION_ID_UNSET,
+                            /* SessionInfo= */ new byte[] {},
+                            /* subSessionInfo= */ new byte[] {},
+                            TEST_UWB_COMPLEX_CHANNEL,
+                            List.of(TEST_UWB_PEER_ADDRESS),
+                            NORMAL,
+                            new UwbRangeDataNtfConfig.Builder().build(),
+                            DURATION_1_MS,
+                            /* isAoaDisabled= */ false);
+    private static final RangingParameters TEST_RANGING_PARAMETERS =
+            new RangingParameters.Builder()
+                    .setUwbLocalAddress(TEST_UWB_LOCAL_ADDRESS)
+                    .setUwbRangingParameters(TEST_UWB_RANGING_PARAMETERS)
+                    .build();
+    private static final UwbAddress TEST_DERIVED_UWB_LOCAL_ADDRESS =
+            UwbAddress.fromBytes(new byte[] {0x4C, (byte) 0xB4});
+    private static final UwbAddress TEST_DERIVED_UWB_PEER_ADDRESS =
+            UwbAddress.fromBytes(new byte[] {(byte) 0xAE, 0x2E});
+    private static final UwbComplexChannel TEST_DERIVED_UWB_COMPLEX_CHANNEL =
+            new UwbComplexChannel(UWB_CHANNEL_9, /* preambleIndex= */ 12);
+    private static final byte[] TEST_DERIVED_STS_KEY =
+            new byte[] {
+                0x76,
+                (byte) 0xD7,
+                (byte) 0xB6,
+                0x1A,
+                (byte) 0x8D,
+                0x29,
+                0x1A,
+                0x52,
+                (byte) 0xBB,
+                (byte) 0xBF,
+                (byte) 0xE6,
+                0x28,
+                (byte) 0xAD,
+                0x44,
+                (byte) 0xFB,
+                0x2E
+            };
+
+    private static final UwbDevice TEST_UWB_DEVICE =
+            UwbDevice.createForAddress(TEST_UWB_PEER_ADDRESS.toBytes());
+    private static final float TEST_DISTANCE = 1.5f;
+    private static final RangingMeasurement TEST_RANGING_MEASUREMENT =
+            new RangingMeasurement(
+                    /* confidence= */ CONFIDENCE_HIGH,
+                    /* value= */ TEST_DISTANCE,
+                    /* valid= */ true);
+    private static final RangingPosition TEST_RANGING_POSITION =
+            new RangingPosition(
+                    TEST_RANGING_MEASUREMENT,
+                    /* azimuth= */ null,
+                    /* elevation= */ null,
+                    /* dlTdoaMeasurement= */ null,
+                    /* elapsedRealtimeNanos= */ 0,
+                    /* rssi= */ 0);
+
+    @Mock private Context mContext;
+    @Mock private UwbServiceImpl mUwbServiceImpl;
+    @Mock private RangingController mRangingController;
+    @Mock private RangingControlee mRangingControlee;
+    @Mock private RangingCallback mRangingCallback;
+    @Mock private Executor mCallbackExecutor;
+
+    private UwbRangingSession mUwbRangingSession;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mUwbServiceImpl.getController(mContext)).thenReturn(mRangingController);
+        when(mUwbServiceImpl.getControlee(mContext)).thenReturn(mRangingControlee);
+        when(mRangingController.startRanging(any(), any())).thenReturn(STATUS_OK);
+        when(mRangingControlee.startRanging(any(), any())).thenReturn(STATUS_OK);
+        doAnswer(
+                invocation -> {
+                    Runnable t = invocation.getArgument(0);
+                    t.run();
+                    return true;
+                })
+                .when(mCallbackExecutor)
+                .execute(any(Runnable.class));
+    }
+
+    @Test
+    public void testConstruction_nullArgument() {
+        assertThrows(
+                NullPointerException.class,
+                () ->
+                        new UwbRangingSession(
+                                null, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl));
+        assertThrows(
+                NullPointerException.class,
+                () -> new UwbRangingSession(mContext, null, mUwbServiceImpl));
+        assertThrows(
+                NullPointerException.class,
+                () -> new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, null));
+    }
+
+    @Test
+    public void testConstruction_initiatorSuccess() {
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+        verify(mUwbServiceImpl, times(1)).getController(mContext);
+    }
+
+    @Test
+    public void testConstruction_responderSuccess() {
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_RESPONDER, mUwbServiceImpl);
+        verify(mUwbServiceImpl, times(1)).getControlee(mContext);
+    }
+
+    @Test
+    public void testStart_nullArgument() {
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+
+        assertThrows(
+                NullPointerException.class,
+                () -> mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, null));
+        assertThrows(
+                NullPointerException.class,
+                () -> mUwbRangingSession.start(null, mCallbackExecutor, mRangingCallback));
+        assertThrows(
+                NullPointerException.class,
+                () -> mUwbRangingSession.start(TEST_RANGING_PARAMETERS, null, mRangingCallback));
+        assertThrows(
+                NullPointerException.class,
+                () ->
+                        mUwbRangingSession.start(
+                                new RangingParameters.Builder().build(),
+                                mCallbackExecutor,
+                                mRangingCallback));
+    }
+
+    @Test
+    public void testStart_initiatorWithoutADFailed() {
+        when(mRangingController.startRanging(any(), any())).thenReturn(STATUS_ERROR);
+
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+        mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, mRangingCallback);
+
+        verify(mRangingController, times(1)).setComplexChannel(TEST_UWB_COMPLEX_CHANNEL);
+        verify(mRangingController, times(1)).setLocalAddress(TEST_UWB_LOCAL_ADDRESS);
+        verify(mRangingController, times(1)).setRangingParameters(TEST_UWB_RANGING_PARAMETERS);
+        verify(mRangingController, times(1)).startRanging(any(), any());
+        ArgumentCaptor<SessionInfo> captor = ArgumentCaptor.forClass(SessionInfo.class);
+        verify(mRangingCallback, times(1))
+                .onError(captor.capture(), eq(RANGING_ERROR_FAILED_TO_START));
+        assertEquals(captor.getValue().getDeviceId(), TEST_DEVICE_ID);
+    }
+
+    private void testRangingCallback() {
+        Answer startRangingResponse =
+                new Answer() {
+                    public Object answer(InvocationOnMock invocation) {
+                        Object[] args = invocation.getArguments();
+                        RangingSessionCallback cb = (RangingSessionCallback) args[0];
+                        cb.onRangingInitialized(TEST_UWB_DEVICE);
+                        cb.onRangingResult(TEST_UWB_DEVICE, TEST_RANGING_POSITION);
+                        return STATUS_OK;
+                    }
+                };
+        doAnswer(startRangingResponse)
+                .when(mRangingController)
+                .startRanging(any(RangingSessionCallback.class), any());
+    }
+
+    @Test
+    public void testStart_initiatorWithADSucceed() {
+        testRangingCallback();
+        mUwbRangingSession =
+                new UwbRangingSession(
+                        mContext, TEST_SESSION_PARAMETER_INITIATOR_W_AD, mUwbServiceImpl);
+        mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, mRangingCallback);
+
+        verify(mRangingController, times(1)).setComplexChannel(TEST_DERIVED_UWB_COMPLEX_CHANNEL);
+        verify(mRangingController, times(1)).setLocalAddress(TEST_DERIVED_UWB_LOCAL_ADDRESS);
+        ArgumentCaptor<androidx.core.uwb.backend.impl.internal.RangingParameters> captor =
+                ArgumentCaptor.forClass(
+                        androidx.core.uwb.backend.impl.internal.RangingParameters.class);
+        verify(mRangingController, times(1)).setRangingParameters(captor.capture());
+        assertEquals(
+                captor.getValue().getUwbConfigId(), TEST_UWB_RANGING_PARAMETERS.getUwbConfigId());
+        assertEquals(captor.getValue().getSessionId(), SESSION_ID_UNSET);
+        assertEquals(captor.getValue().getSubSessionId(), SESSION_ID_UNSET);
+        assertArrayEquals(captor.getValue().getSessionKeyInfo(), TEST_DERIVED_STS_KEY);
+        assertArrayEquals(captor.getValue().getSubSessionKeyInfo(), new byte[] {});
+        assertEquals(captor.getValue().getComplexChannel(), TEST_DERIVED_UWB_COMPLEX_CHANNEL);
+        assertEquals(captor.getValue().getPeerAddresses().get(0), TEST_DERIVED_UWB_PEER_ADDRESS);
+        assertEquals(
+                captor.getValue().getRangingUpdateRate(),
+                TEST_UWB_RANGING_PARAMETERS.getRangingUpdateRate());
+        assertEquals(
+                captor.getValue().getUwbRangeDataNtfConfig(),
+                TEST_UWB_RANGING_PARAMETERS.getUwbRangeDataNtfConfig());
+        assertEquals(
+                captor.getValue().getSlotDuration(), TEST_UWB_RANGING_PARAMETERS.getSlotDuration());
+        assertEquals(
+                captor.getValue().isAoaDisabled(), TEST_UWB_RANGING_PARAMETERS.isAoaDisabled());
+        verify(mRangingController, times(1)).startRanging(any(), any());
+        ArgumentCaptor<SessionInfo> captor2 = ArgumentCaptor.forClass(SessionInfo.class);
+        ArgumentCaptor<RangingReport> captor3 = ArgumentCaptor.forClass(RangingReport.class);
+        verify(mRangingCallback, times(1)).onRangingReport(captor2.capture(), captor3.capture());
+        assertEquals(captor2.getValue().getDeviceId(), TEST_DEVICE_ID);
+        RangingReport rangingReport = captor3.getValue();
+        assertEquals(rangingReport.getDistanceM(), TEST_DISTANCE, 0.0f);
+        assertEquals(rangingReport.getProximityState(), PROXIMITY_STATE_INSIDE);
+    }
+
+    @Test
+    public void testStop_sessionNotStarted() {
+        when(mRangingController.stopRanging()).thenReturn(STATUS_ERROR);
+
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+        mUwbRangingSession.stop();
+
+        verifyZeroInteractions(mRangingController);
+        verifyZeroInteractions(mRangingCallback);
+    }
+
+    @Test
+    public void testStop_failed() {
+        when(mRangingController.stopRanging()).thenReturn(STATUS_ERROR);
+
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+        mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, mRangingCallback);
+        mUwbRangingSession.stop();
+
+        verify(mRangingController, times(1)).setComplexChannel(any());
+        verify(mRangingController, times(1)).setLocalAddress(any());
+        verify(mRangingController, times(1)).setRangingParameters(any());
+        verify(mRangingController, times(1)).startRanging(any(), any());
+        verify(mRangingController, times(1)).stopRanging();
+        ArgumentCaptor<SessionInfo> captor = ArgumentCaptor.forClass(SessionInfo.class);
+        verify(mRangingCallback, times(1))
+                .onError(captor.capture(), eq(RANGING_ERROR_FAILED_TO_STOP));
+        assertEquals(captor.getValue().getDeviceId(), TEST_DEVICE_ID);
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/util/CryptoTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/util/CryptoTest.java
new file mode 100644
index 0000000..eb7a8c5
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/util/CryptoTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.remoteauth.util;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link Crypto}. */
+@RunWith(AndroidJUnit4.class)
+public class CryptoTest {
+    private static final byte[] TEST_IKM =
+            new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+    private static final byte[] TEST_SALT =
+            new byte[] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00};
+    private static final int TEST_SIZE = 40;
+
+    @Test
+    public void testComputeHkdf_exceedMaxSize() {
+        // Max size is 32*255
+        assertNull(Crypto.computeHkdf(TEST_IKM, TEST_SALT, /* size= */ 10000));
+    }
+
+    @Test
+    public void testComputeHkdf_emptySalt() {
+        assertNull(Crypto.computeHkdf(TEST_IKM, new byte[] {}, TEST_SIZE));
+    }
+
+    @Test
+    public void testComputeHkdf_emptyIkm() {
+        assertNull(Crypto.computeHkdf(new byte[] {}, TEST_SALT, TEST_SIZE));
+    }
+
+    @Test
+    public void testComputeHkdf_success() {
+        assertNotNull(Crypto.computeHkdf(TEST_IKM, TEST_SALT, TEST_SIZE));
+    }
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 7de749c..779f354 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -15,10 +15,13 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+service_remoteauth_pre_jarjar_lib = "service-remoteauth-pre-jarjar"
+
 // Include build rules from Sources.bp
 build = ["Sources.bp"]
 
@@ -29,6 +32,7 @@
     ],
     visibility: ["//visibility:private"],
 }
+
 // The above filegroup can be used to specify different sources depending
 // on the branch, while minimizing merge conflicts in the rest of the
 // build rules.
@@ -55,6 +59,8 @@
         "framework-wifi",
         "service-connectivity-pre-jarjar",
         "service-nearby-pre-jarjar",
+        "service-thread-pre-jarjar",
+        service_remoteauth_pre_jarjar_lib,
         "ServiceConnectivityResources",
         "unsupportedappusage",
     ],
@@ -69,11 +75,15 @@
         "com.android.tethering",
     ],
     visibility: [
+        "//frameworks/base/services/tests/VpnTests",
         "//frameworks/base/tests/vcn",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/IPsec/tests/iketests",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Test building mDNS as a standalone, so that it can be imported into other repositories as-is.
@@ -87,13 +97,24 @@
 java_library {
     name: "service-connectivity-mdns-standalone-build-test",
     sdk_version: "core_platform",
+    min_sdk_version: "21",
+    lint: {
+        error_checks: ["NewApi"],
+
+    },
     srcs: [
-        ":service-mdns-droidstubs",
         "src/com/android/server/connectivity/mdns/**/*.java",
+        ":framework-connectivity-t-mdns-standalone-build-sources",
+        ":service-mdns-droidstubs",
     ],
     exclude_srcs: [
         "src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
-        "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"
+        "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java",
+        "src/com/android/server/connectivity/mdns/MdnsAdvertiser.java",
+        "src/com/android/server/connectivity/mdns/MdnsAnnouncer.java",
+        "src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java",
+        "src/com/android/server/connectivity/mdns/MdnsProber.java",
+        "src/com/android/server/connectivity/mdns/MdnsRecordRepository.java",
     ],
     static_libs: [
         "net-utils-device-common-mdns-standalone-build-test",
@@ -113,9 +134,9 @@
     srcs: ["src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"],
     libs: [
         "net-utils-device-common-mdns-standalone-build-test",
-        "service-connectivity-tiramisu-pre-jarjar"
+        "service-connectivity-tiramisu-pre-jarjar",
     ],
     visibility: [
         "//visibility:private",
     ],
-}
\ No newline at end of file
+}
diff --git a/service-t/Sources.bp b/service-t/Sources.bp
index 187eadf..fbe02a5 100644
--- a/service-t/Sources.bp
+++ b/service-t/Sources.bp
@@ -20,7 +20,6 @@
     srcs: [
         "jni/com_android_server_net_NetworkStatsFactory.cpp",
     ],
-    path: "jni",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
     ],
@@ -32,7 +31,6 @@
         "jni/com_android_server_net_NetworkStatsFactory.cpp",
         "jni/com_android_server_net_NetworkStatsService.cpp",
     ],
-    path: "jni",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
     ],
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index dab9d07..c999398 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -34,73 +34,77 @@
 
 using android::bpf::bpfGetUidStats;
 using android::bpf::bpfGetIfaceStats;
+using android::bpf::bpfRegisterIface;
 using android::bpf::NetworkTraceHandler;
 
 namespace android {
 
-// NOTE: keep these in sync with TrafficStats.java
-static const uint64_t UNKNOWN = -1;
-
-enum StatsType {
-    RX_BYTES = 0,
-    RX_PACKETS = 1,
-    TX_BYTES = 2,
-    TX_PACKETS = 3,
-    TCP_RX_PACKETS = 4,
-    TCP_TX_PACKETS = 5
-};
-
-static uint64_t getStatsType(Stats* stats, StatsType type) {
-    switch (type) {
-        case RX_BYTES:
-            return stats->rxBytes;
-        case RX_PACKETS:
-            return stats->rxPackets;
-        case TX_BYTES:
-            return stats->txBytes;
-        case TX_PACKETS:
-            return stats->txPackets;
-        case TCP_RX_PACKETS:
-            return stats->tcpRxPackets;
-        case TCP_TX_PACKETS:
-            return stats->tcpTxPackets;
-        default:
-            return UNKNOWN;
-    }
-}
-
-static jlong nativeGetTotalStat(JNIEnv* env, jclass clazz, jint type) {
-    Stats stats = {};
-
-    if (bpfGetIfaceStats(NULL, &stats) == 0) {
-        return getStatsType(&stats, (StatsType) type);
-    } else {
-        return UNKNOWN;
-    }
-}
-
-static jlong nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) {
+static void nativeRegisterIface(JNIEnv* env, jclass clazz, jstring iface) {
     ScopedUtfChars iface8(env, iface);
-    if (iface8.c_str() == NULL) {
-        return UNKNOWN;
+    if (iface8.c_str() == nullptr) return;
+    bpfRegisterIface(iface8.c_str());
+}
+
+static jobject statsValueToEntry(JNIEnv* env, StatsValue* stats) {
+    // Find the Java class that represents the structure
+    jclass gEntryClass = env->FindClass("android/net/NetworkStats$Entry");
+    if (gEntryClass == nullptr) {
+        return nullptr;
     }
 
-    Stats stats = {};
+    // Find the constructor.
+    jmethodID constructorID = env->GetMethodID(gEntryClass, "<init>", "()V");
+    if (constructorID == nullptr) {
+        return nullptr;
+    }
+
+    // Create a new instance of the Java class
+    jobject result = env->NewObject(gEntryClass, constructorID);
+    if (result == nullptr) {
+        return nullptr;
+    }
+
+    // Set the values of the structure fields in the Java object
+    env->SetLongField(result, env->GetFieldID(gEntryClass, "rxBytes", "J"), stats->rxBytes);
+    env->SetLongField(result, env->GetFieldID(gEntryClass, "txBytes", "J"), stats->txBytes);
+    env->SetLongField(result, env->GetFieldID(gEntryClass, "rxPackets", "J"), stats->rxPackets);
+    env->SetLongField(result, env->GetFieldID(gEntryClass, "txPackets", "J"), stats->txPackets);
+
+    return result;
+}
+
+static jobject nativeGetTotalStat(JNIEnv* env, jclass clazz) {
+    StatsValue stats = {};
+
+    if (bpfGetIfaceStats(nullptr, &stats) == 0) {
+        return statsValueToEntry(env, &stats);
+    } else {
+        return nullptr;
+    }
+}
+
+static jobject nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface) {
+    ScopedUtfChars iface8(env, iface);
+    if (iface8.c_str() == nullptr) {
+        return nullptr;
+    }
+
+    StatsValue stats = {};
 
     if (bpfGetIfaceStats(iface8.c_str(), &stats) == 0) {
-        return getStatsType(&stats, (StatsType) type);
+        return statsValueToEntry(env, &stats);
     } else {
-        return UNKNOWN;
+        return nullptr;
     }
 }
 
-static jlong nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
-    Stats stats = {};
+static jobject nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid) {
+    StatsValue stats = {};
 
     if (bpfGetUidStats(uid, &stats) == 0) {
-        return getStatsType(&stats, (StatsType) type);
+        return statsValueToEntry(env, &stats);
     } else {
-        return UNKNOWN;
+        return nullptr;
     }
 }
 
@@ -109,10 +113,31 @@
 }
 
 static const JNINativeMethod gMethods[] = {
-        {"nativeGetTotalStat", "(I)J", (void*)nativeGetTotalStat},
-        {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)nativeGetIfaceStat},
-        {"nativeGetUidStat", "(II)J", (void*)nativeGetUidStat},
-        {"nativeInitNetworkTracing", "()V", (void*)nativeInitNetworkTracing},
+        {
+            "nativeRegisterIface",
+            "(Ljava/lang/String;)V",
+            (void*)nativeRegisterIface
+        },
+        {
+            "nativeGetTotalStat",
+            "()Landroid/net/NetworkStats$Entry;",
+            (void*)nativeGetTotalStat
+        },
+        {
+            "nativeGetIfaceStat",
+            "(Ljava/lang/String;)Landroid/net/NetworkStats$Entry;",
+            (void*)nativeGetIfaceStat
+        },
+        {
+            "nativeGetUidStat",
+            "(I)Landroid/net/NetworkStats$Entry;",
+            (void*)nativeGetUidStat
+        },
+        {
+            "nativeInitNetworkTracing",
+            "()V",
+            (void*)nativeInitNetworkTracing
+        },
 };
 
 int register_android_server_net_NetworkStatsService(JNIEnv* env) {
diff --git a/service-t/lint-baseline.xml b/service-t/lint-baseline.xml
index 38d3ab0..e4b92d6 100644
--- a/service-t/lint-baseline.xml
+++ b/service-t/lint-baseline.xml
@@ -1,104 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier#getInterfaceName`"
-        errorLine1="        if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
-        errorLine2="                                               ~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
-            line="224"
-            column="48"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getInterface`"
-        errorLine1="            delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
-        errorLine2="                                                      ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
-            line="276"
-            column="55"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getOwnerUid`"
-        errorLine1="            delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
-        errorLine2="                                  ~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
-            line="276"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getUnderlyingInterfaces`"
-        errorLine1="                    info.getUnderlyingInterfaces());"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
-            line="277"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
-        errorLine1="                        dnsAddresses.add(InetAddress.parseNumericAddress(address));"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
-            line="875"
-            column="54"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
-        errorLine1="                    staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));"
-        errorLine2="                                                                 ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
-            line="870"
-            column="66"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="                IoUtils.closeQuietly(os);"
-        errorLine2="                        ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsRecorder.java"
-            line="556"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="                IoUtils.closeQuietly(sockFd);"
-        errorLine2="                        ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
-            line="1309"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="            IoUtils.closeQuietly(mSocket);"
-        errorLine2="                    ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
-            line="1034"
-            column="21"/>
-    </issue>
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
@@ -113,6 +14,17 @@
 
     <issue
         id="NewApi"
+        message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+        errorLine1="                .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))"
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java"
+            line="156"
+            column="38"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
         errorLine1="            nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));"
         errorLine2="                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -124,39 +36,6 @@
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `new android.util.AtomicFile`"
-        errorLine1="        mFile = new AtomicFile(new File(path), logger);"
-        errorLine2="                ~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/net/PersistentInt.java"
-            line="53"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `new java.net.InetSocketAddress`"
-        errorLine1="        super(handler, new RecvBuffer(buffer, new InetSocketAddress()));"
-        errorLine2="                                              ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java"
-            line="66"
-            column="47"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
-        errorLine1="                .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))"
-        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java"
-            line="156"
-            column="38"/>
-    </issue>
-
-    <issue
-        id="NewApi"
         message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
         errorLine1="            nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));"
         errorLine2="                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -169,6 +48,28 @@
     <issue
         id="NewApi"
         message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
+        errorLine1="        if (!(spec instanceof EthernetNetworkSpecifier)) {"
+        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+            line="221"
+            column="31"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier#getInterfaceName`"
+        errorLine1="        if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
+        errorLine2="                                               ~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+            line="224"
+            column="48"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
         errorLine1="        if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
         errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -179,13 +80,178 @@
 
     <issue
         id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
-        errorLine1="        if (!(spec instanceof EthernetNetworkSpecifier)) {"
-        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+        errorLine1="                    staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));"
+        errorLine2="                                                                 ~~~~~~~~~~~~~~~~~~~">
         <location
-            file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
-            line="221"
-            column="31"/>
+            file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
+            line="885"
+            column="66"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+        errorLine1="                        dnsAddresses.add(InetAddress.parseNumericAddress(address));"
+        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
+            line="890"
+            column="54"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="            IoUtils.closeQuietly(mSocket);"
+        errorLine2="                    ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+            line="1042"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="                IoUtils.closeQuietly(sockFd);"
+        errorLine2="                        ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+            line="1318"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 31 (current min is 30): `android.system.OsConstants#UDP_ENCAP`"
+        errorLine1="                    OsConstants.UDP_ENCAP,"
+        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+            line="1326"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 31 (current min is 30): `android.system.OsConstants#UDP_ENCAP_ESPINUDP`"
+        errorLine1="                    OsConstants.UDP_ENCAP_ESPINUDP);"
+        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+            line="1327"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `BpfNetMaps`"
+        errorLine1="            return new BpfNetMaps(ctx);"
+        errorLine2="                   ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+            line="111"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `swapActiveStatsMap`"
+        errorLine1="            mBpfNetMaps.swapActiveStatsMap();"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+            line="185"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getInterface`"
+        errorLine1="            delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
+        errorLine2="                                                      ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+            line="240"
+            column="55"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getOwnerUid`"
+        errorLine1="            delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
+        errorLine2="                                  ~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+            line="240"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getUnderlyingInterfaces`"
+        errorLine1="                    info.getUnderlyingInterfaces());"
+        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+            line="241"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="                IoUtils.closeQuietly(os);"
+        errorLine2="                        ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsRecorder.java"
+            line="580"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 35 (current min is 34): `newInstance`"
+        errorLine1="                            opts = BroadcastOptionsShimImpl.newInstance("
+        errorLine2="                                                            ~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsService.java"
+            line="562"
+            column="61"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `new android.util.AtomicFile`"
+        errorLine1="        mFile = new AtomicFile(new File(path), logger);"
+        errorLine2="                ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/net/PersistentInt.java"
+            line="53"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `addOrUpdateInterfaceAddress`"
+        errorLine1="                    mCb.addOrUpdateInterfaceAddress(ifaddrMsg.index, la);"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java"
+            line="69"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `deleteInterfaceAddress`"
+        errorLine1="                mCb.deleteInterfaceAddress(ifaddrMsg.index, la);"
+        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java"
+            line="73"
+            column="21"/>
     </issue>
 
 </issues>
\ No newline at end of file
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index 0dfd0af..b1925bd 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -57,9 +58,12 @@
 
 cc_test {
     name: "libnetworkstats_test",
-    test_suites: ["general-tests", "mts-tethering"],
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
     test_config_template: ":net_native_test_config_template",
-    require_root: true,  // required by setrlimitForTest()
+    require_root: true, // required by setrlimitForTest()
     header_libs: ["bpf_connectivity_headers"],
     srcs: [
         "BpfNetworkStatsTest.cpp",
@@ -72,7 +76,10 @@
         "-Wno-unused-parameter",
         "-Wthread-safety",
     ],
+    version_script: ":connectivity_mainline_test_map",
+    stl: "libc++_static",
     static_libs: [
+        "libbase",
         "libgmock",
         "libnetworkstats",
         "libperfetto_client_experimental",
@@ -80,7 +87,6 @@
         "perfetto_trace_protos",
     ],
     shared_libs: [
-        "libbase",
         "liblog",
         "libcutils",
         "libandroid_net",
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index 1bc8ca5..d3e331e 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -40,48 +40,71 @@
 
 using base::Result;
 
-int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
-                           const BpfMap<uint32_t, StatsValue>& appUidStatsMap) {
-    auto statsEntry = appUidStatsMap.readValue(uid);
-    if (statsEntry.ok()) {
-        stats->rxPackets = statsEntry.value().rxPackets;
-        stats->txPackets = statsEntry.value().txPackets;
-        stats->rxBytes = statsEntry.value().rxBytes;
-        stats->txBytes = statsEntry.value().txBytes;
-    }
-    return (statsEntry.ok() || statsEntry.error().code() == ENOENT) ? 0
-                                                                    : -statsEntry.error().code();
+BpfMap<uint32_t, IfaceValue>& getIfaceIndexNameMap() {
+    static BpfMap<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    return ifaceIndexNameMap;
 }
 
-int bpfGetUidStats(uid_t uid, Stats* stats) {
+const BpfMapRO<uint32_t, StatsValue>& getIfaceStatsMap() {
+    static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+    return ifaceStatsMap;
+}
+
+Result<IfaceValue> ifindex2name(const uint32_t ifindex) {
+    Result<IfaceValue> v = getIfaceIndexNameMap().readValue(ifindex);
+    if (v.ok()) return v;
+    IfaceValue iv = {};
+    if (!if_indextoname(ifindex, iv.name)) return v;
+    getIfaceIndexNameMap().writeValue(ifindex, iv, BPF_ANY);
+    return iv;
+}
+
+void bpfRegisterIface(const char* iface) {
+    if (!iface) return;
+    if (strlen(iface) >= sizeof(IfaceValue)) return;
+    uint32_t ifindex = if_nametoindex(iface);
+    if (!ifindex) return;
+    IfaceValue ifname = {};
+    strlcpy(ifname.name, iface, sizeof(ifname.name));
+    getIfaceIndexNameMap().writeValue(ifindex, ifname, BPF_ANY);
+}
+
+int bpfGetUidStatsInternal(uid_t uid, StatsValue* stats,
+                           const BpfMapRO<uint32_t, StatsValue>& appUidStatsMap) {
+    auto statsEntry = appUidStatsMap.readValue(uid);
+    if (!statsEntry.ok()) {
+        *stats = {};
+        return (statsEntry.error().code() == ENOENT) ? 0 : -statsEntry.error().code();
+    }
+    *stats = statsEntry.value();
+    return 0;
+}
+
+int bpfGetUidStats(uid_t uid, StatsValue* stats) {
     static BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
     return bpfGetUidStatsInternal(uid, stats, appUidStatsMap);
 }
 
-int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
-                             const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
-                             const BpfMap<uint32_t, IfaceValue>& ifaceNameMap) {
+int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
+                             const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap,
+                             const IfIndexToNameFunc ifindex2name) {
+    *stats = {};
     int64_t unknownIfaceBytesTotal = 0;
-    stats->tcpRxPackets = -1;
-    stats->tcpTxPackets = -1;
     const auto processIfaceStats =
-            [iface, stats, &ifaceNameMap, &unknownIfaceBytesTotal](
+            [iface, stats, ifindex2name, &unknownIfaceBytesTotal](
                     const uint32_t& key,
-                    const BpfMap<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
-        char ifname[IFNAMSIZ];
-        if (getIfaceNameFromMap(ifaceNameMap, ifaceStatsMap, key, ifname, key,
-                                &unknownIfaceBytesTotal)) {
+                    const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
+        Result<IfaceValue> ifname = ifindex2name(key);
+        if (!ifname.ok()) {
+            maybeLogUnknownIface(key, ifaceStatsMap, key, &unknownIfaceBytesTotal);
             return Result<void>();
         }
-        if (!iface || !strcmp(iface, ifname)) {
+        if (!iface || !strcmp(iface, ifname.value().name)) {
             Result<StatsValue> statsEntry = ifaceStatsMap.readValue(key);
             if (!statsEntry.ok()) {
                 return statsEntry.error();
             }
-            stats->rxPackets += statsEntry.value().rxPackets;
-            stats->txPackets += statsEntry.value().txPackets;
-            stats->rxBytes += statsEntry.value().rxBytes;
-            stats->txBytes += statsEntry.value().txBytes;
+            *stats += statsEntry.value();
         }
         return Result<void>();
     };
@@ -89,16 +112,29 @@
     return res.ok() ? 0 : -res.error().code();
 }
 
-int bpfGetIfaceStats(const char* iface, Stats* stats) {
-    static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
-    static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
-    return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
+int bpfGetIfaceStats(const char* iface, StatsValue* stats) {
+    return bpfGetIfaceStatsInternal(iface, stats, getIfaceStatsMap(), ifindex2name);
+}
+
+int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
+                               const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap) {
+    auto statsEntry = ifaceStatsMap.readValue(ifindex);
+    if (!statsEntry.ok()) {
+        *stats = {};
+        return (statsEntry.error().code() == ENOENT) ? 0 : -statsEntry.error().code();
+    }
+    *stats = statsEntry.value();
+    return 0;
+}
+
+int bpfGetIfIndexStats(int ifindex, StatsValue* stats) {
+    return bpfGetIfIndexStatsInternal(ifindex, stats, getIfaceStatsMap());
 }
 
 stats_line populateStatsEntry(const StatsKey& statsKey, const StatsValue& statsEntry,
-                              const char* ifname) {
+                              const IfaceValue& ifname) {
     stats_line newLine;
-    strlcpy(newLine.iface, ifname, sizeof(newLine.iface));
+    strlcpy(newLine.iface, ifname.name, sizeof(newLine.iface));
     newLine.uid = (int32_t)statsKey.uid;
     newLine.set = (int32_t)statsKey.counterSet;
     newLine.tag = (int32_t)statsKey.tag;
@@ -110,23 +146,23 @@
 }
 
 int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
-                                       const BpfMap<StatsKey, StatsValue>& statsMap,
-                                       const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+                                       const BpfMapRO<StatsKey, StatsValue>& statsMap,
+                                       const IfIndexToNameFunc ifindex2name) {
     int64_t unknownIfaceBytesTotal = 0;
     const auto processDetailUidStats =
-            [&lines, &unknownIfaceBytesTotal, &ifaceMap](
+            [&lines, &unknownIfaceBytesTotal, &ifindex2name](
                     const StatsKey& key,
-                    const BpfMap<StatsKey, StatsValue>& statsMap) -> Result<void> {
-        char ifname[IFNAMSIZ];
-        if (getIfaceNameFromMap(ifaceMap, statsMap, key.ifaceIndex, ifname, key,
-                                &unknownIfaceBytesTotal)) {
+                    const BpfMapRO<StatsKey, StatsValue>& statsMap) -> Result<void> {
+        Result<IfaceValue> ifname = ifindex2name(key.ifaceIndex);
+        if (!ifname.ok()) {
+            maybeLogUnknownIface(key.ifaceIndex, statsMap, key, &unknownIfaceBytesTotal);
             return Result<void>();
         }
         Result<StatsValue> statsEntry = statsMap.readValue(key);
         if (!statsEntry.ok()) {
             return base::ResultError(statsEntry.error().message(), statsEntry.error().code());
         }
-        stats_line newLine = populateStatsEntry(key, statsEntry.value(), ifname);
+        stats_line newLine = populateStatsEntry(key, statsEntry.value(), ifname.value());
         lines.push_back(newLine);
         if (newLine.tag) {
             // account tagged traffic in the untagged stats (for historical reasons?)
@@ -156,7 +192,6 @@
 }
 
 int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines) {
-    static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
     static BpfMapRO<uint32_t, uint32_t> configurationMap(CONFIGURATION_MAP_PATH);
     static BpfMap<StatsKey, StatsValue> statsMapA(STATS_MAP_A_PATH);
     static BpfMap<StatsKey, StatsValue> statsMapB(STATS_MAP_B_PATH);
@@ -186,7 +221,7 @@
     // TODO: the above comment feels like it may be obsolete / out of date,
     // since we no longer swap the map via netd binder rpc - though we do
     // still swap it.
-    int ret = parseBpfNetworkStatsDetailInternal(*lines, *inactiveStatsMap, ifaceIndexNameMap);
+    int ret = parseBpfNetworkStatsDetailInternal(*lines, *inactiveStatsMap, ifindex2name);
     if (ret) {
         ALOGE("parse detail network stats failed: %s", strerror(errno));
         return ret;
@@ -202,14 +237,15 @@
 }
 
 int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
-                                    const BpfMap<uint32_t, StatsValue>& statsMap,
-                                    const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+                                    const BpfMapRO<uint32_t, StatsValue>& statsMap,
+                                    const IfIndexToNameFunc ifindex2name) {
     int64_t unknownIfaceBytesTotal = 0;
-    const auto processDetailIfaceStats = [&lines, &unknownIfaceBytesTotal, &ifaceMap, &statsMap](
+    const auto processDetailIfaceStats = [&lines, &unknownIfaceBytesTotal, ifindex2name, &statsMap](
                                              const uint32_t& key, const StatsValue& value,
-                                             const BpfMap<uint32_t, StatsValue>&) {
-        char ifname[IFNAMSIZ];
-        if (getIfaceNameFromMap(ifaceMap, statsMap, key, ifname, key, &unknownIfaceBytesTotal)) {
+                                             const BpfMapRO<uint32_t, StatsValue>&) {
+        Result<IfaceValue> ifname = ifindex2name(key);
+        if (!ifname.ok()) {
+            maybeLogUnknownIface(key, statsMap, key, &unknownIfaceBytesTotal);
             return Result<void>();
         }
         StatsKey fakeKey = {
@@ -217,7 +253,7 @@
                 .tag = (uint32_t)TAG_NONE,
                 .counterSet = (uint32_t)SET_ALL,
         };
-        lines.push_back(populateStatsEntry(fakeKey, value, ifname));
+        lines.push_back(populateStatsEntry(fakeKey, value, ifname.value()));
         return Result<void>();
     };
     Result<void> res = statsMap.iterateWithValue(processDetailIfaceStats);
@@ -232,9 +268,7 @@
 }
 
 int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
-    static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
-    static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
-    return parseBpfNetworkStatsDevInternal(*lines, ifaceStatsMap, ifaceIndexNameMap);
+    return parseBpfNetworkStatsDevInternal(*lines, getIfaceStatsMap(), ifindex2name);
 }
 
 void groupNetworkStats(std::vector<stats_line>& lines) {
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
index ccd3f5e..484c166 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -77,22 +77,26 @@
     BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
     BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap;
 
+    IfIndexToNameFunc mIfIndex2Name = [this](const uint32_t ifindex){
+        return mFakeIfaceIndexNameMap.readValue(ifindex);
+    };
+
     void SetUp() {
         ASSERT_EQ(0, setrlimitForTest());
 
-        mFakeCookieTagMap = BpfMap<uint64_t, UidTagValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_TRUE(mFakeCookieTagMap.isValid());
 
-        mFakeAppUidStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeAppUidStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_TRUE(mFakeAppUidStatsMap.isValid());
 
-        mFakeStatsMap = BpfMap<StatsKey, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_TRUE(mFakeStatsMap.isValid());
 
-        mFakeIfaceIndexNameMap = BpfMap<uint32_t, IfaceValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeIfaceIndexNameMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_TRUE(mFakeIfaceIndexNameMap.isValid());
 
-        mFakeIfaceStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeIfaceStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_TRUE(mFakeIfaceStatsMap.isValid());
     }
 
@@ -116,7 +120,7 @@
         EXPECT_RESULT_OK(mFakeIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY));
     }
 
-    void expectStatsEqual(const StatsValue& target, const Stats& result) {
+    void expectStatsEqual(const StatsValue& target, const StatsValue& result) {
         EXPECT_EQ(target.rxPackets, result.rxPackets);
         EXPECT_EQ(target.rxBytes, result.rxBytes);
         EXPECT_EQ(target.txPackets, result.txPackets);
@@ -194,7 +198,7 @@
             .txPackets = 0,
             .txBytes = 0,
     };
-    Stats result1 = {};
+    StatsValue result1 = {};
     ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
     expectStatsEqual(value1, result1);
 }
@@ -217,18 +221,18 @@
     };
     ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID1, value1, BPF_ANY));
     ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID2, value2, BPF_ANY));
-    Stats result1 = {};
+    StatsValue result1 = {};
     ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
     expectStatsEqual(value1, result1);
 
-    Stats result2 = {};
+    StatsValue result2 = {};
     ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID2, &result2, mFakeAppUidStatsMap));
     expectStatsEqual(value2, result2);
     std::vector<stats_line> lines;
     populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
     populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET1, value1, mFakeStatsMap);
     populateFakeStats(TEST_UID2, 0, IFACE_INDEX3, TEST_COUNTERSET1, value1, mFakeStatsMap);
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
     ASSERT_EQ((unsigned long)3, lines.size());
 }
 
@@ -255,17 +259,16 @@
     ifaceStatsKey = IFACE_INDEX3;
     EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
 
-    Stats result1 = {};
-    ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap,
-                                          mFakeIfaceIndexNameMap));
+    StatsValue result1 = {};
+    ASSERT_EQ(0,
+              bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap, mIfIndex2Name));
     expectStatsEqual(value1, result1);
-    Stats result2 = {};
-    ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap,
-                                          mFakeIfaceIndexNameMap));
+    StatsValue result2 = {};
+    ASSERT_EQ(0,
+              bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap, mIfIndex2Name));
     expectStatsEqual(value2, result2);
-    Stats totalResult = {};
-    ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap,
-                                          mFakeIfaceIndexNameMap));
+    StatsValue totalResult = {};
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap, mIfIndex2Name));
     StatsValue totalValue = {
             .rxPackets = TEST_PACKET0 * 2 + TEST_PACKET1,
             .rxBytes = TEST_BYTES0 * 2 + TEST_BYTES1,
@@ -275,6 +278,20 @@
     expectStatsEqual(totalValue, totalResult);
 }
 
+TEST_F(BpfNetworkStatsHelperTest, TestGetIfIndexStatsInternal) {
+    StatsValue value = {
+          .rxPackets = TEST_PACKET0,
+          .rxBytes = TEST_BYTES0,
+          .txPackets = TEST_PACKET1,
+          .txBytes = TEST_BYTES1,
+    };
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(IFACE_INDEX1, value, BPF_ANY));
+
+    StatsValue result = {};
+    ASSERT_EQ(0, bpfGetIfIndexStatsInternal(IFACE_INDEX1, &result, mFakeIfaceStatsMap));
+    expectStatsEqual(value, result);
+}
+
 TEST_F(BpfNetworkStatsHelperTest, TestGetStatsDetail) {
     updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
     updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
@@ -290,7 +307,7 @@
                       mFakeStatsMap);
     populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
     std::vector<stats_line> lines;
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
     ASSERT_EQ((unsigned long)7, lines.size());
 }
 
@@ -310,7 +327,7 @@
     populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET1, value1, mFakeStatsMap);
     populateFakeStats(TEST_UID2, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
     std::vector<stats_line> lines;
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
     ASSERT_EQ((unsigned long)4, lines.size());
 }
 
@@ -338,18 +355,20 @@
             .counterSet = TEST_COUNTERSET0,
             .ifaceIndex = ifaceIndex,
     };
-    char ifname[IFNAMSIZ];
     int64_t unknownIfaceBytesTotal = 0;
-    ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
-                                           ifname, curKey, &unknownIfaceBytesTotal));
+    ASSERT_EQ(false, mFakeIfaceIndexNameMap.readValue(ifaceIndex).ok());
+    maybeLogUnknownIface(ifaceIndex, mFakeStatsMap, curKey, &unknownIfaceBytesTotal);
+
     ASSERT_EQ(((int64_t)(TEST_BYTES0 * 20 + TEST_BYTES1 * 20)), unknownIfaceBytesTotal);
     curKey.ifaceIndex = IFACE_INDEX2;
-    ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
-                                           ifname, curKey, &unknownIfaceBytesTotal));
+
+    ASSERT_EQ(false, mFakeIfaceIndexNameMap.readValue(ifaceIndex).ok());
+    maybeLogUnknownIface(ifaceIndex, mFakeStatsMap, curKey, &unknownIfaceBytesTotal);
+
     ASSERT_EQ(-1, unknownIfaceBytesTotal);
     std::vector<stats_line> lines;
     // TODO: find a way to test the total of unknown Iface Bytes go above limit.
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
     ASSERT_EQ((unsigned long)1, lines.size());
     expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines.front());
 }
@@ -380,8 +399,7 @@
     ifaceStatsKey = IFACE_INDEX4;
     EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
     std::vector<stats_line> lines;
-    ASSERT_EQ(0,
-              parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mIfIndex2Name));
     ASSERT_EQ((unsigned long)4, lines.size());
 
     expectStatsLineEqual(value1, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
@@ -425,13 +443,13 @@
     std::vector<stats_line> lines;
 
     // Test empty stats.
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
     ASSERT_EQ((size_t) 0, lines.size());
     lines.clear();
 
     // Test 1 line stats.
     populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
     ASSERT_EQ((size_t) 2, lines.size());  // TEST_TAG != 0 -> 1 entry becomes 2 lines
     expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines[0]);
     expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[1]);
@@ -443,7 +461,7 @@
     populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value2,
                       mFakeStatsMap);
     populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
     ASSERT_EQ((size_t) 9, lines.size());
     lines.clear();
 
@@ -451,7 +469,7 @@
     populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
     populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
 
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
     ASSERT_EQ((size_t) 9, lines.size());
 
     // Verify Sorted & Grouped.
@@ -476,8 +494,7 @@
     ifaceStatsKey = IFACE_INDEX3;
     EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
 
-    ASSERT_EQ(0,
-              parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mIfIndex2Name));
     ASSERT_EQ((size_t) 2, lines.size());
 
     expectStatsLineEqual(value3, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
@@ -518,7 +535,7 @@
     // TODO: Mutate counterSet and enlarge TEST_MAP_SIZE if overflow on counterSet is possible.
 
     std::vector<stats_line> lines;
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
     ASSERT_EQ((size_t) 12, lines.size());
 
     // Uid 0 first
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
index c5f9631..9b1b72d 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
@@ -18,6 +18,7 @@
 
 #include "netdbpf/NetworkTraceHandler.h"
 
+#include <android-base/macros.h>
 #include <arpa/inet.h>
 #include <bpf/BpfUtils.h>
 #include <log/log.h>
@@ -75,9 +76,35 @@
   uint32_t bytes = 0;
 };
 
-#define AGG_FIELDS(x)                                              \
-  (x).ifindex, (x).uid, (x).tag, (x).sport, (x).dport, (x).egress, \
-      (x).ipProto, (x).tcpFlags
+BundleKey::BundleKey(const PacketTrace& pkt)
+    : ifindex(pkt.ifindex),
+      uid(pkt.uid),
+      tag(pkt.tag),
+      egress(pkt.egress),
+      ipProto(pkt.ipProto),
+      ipVersion(pkt.ipVersion) {
+  switch (ipProto) {
+    case IPPROTO_TCP:
+      tcpFlags = pkt.tcpFlags;
+      FALLTHROUGH_INTENDED;
+    case IPPROTO_DCCP:
+    case IPPROTO_UDP:
+    case IPPROTO_UDPLITE:
+    case IPPROTO_SCTP:
+      localPort = ntohs(pkt.egress ? pkt.sport : pkt.dport);
+      remotePort = ntohs(pkt.egress ? pkt.dport : pkt.sport);
+      break;
+    case IPPROTO_ICMP:
+    case IPPROTO_ICMPV6:
+      icmpType = ntohs(pkt.sport);
+      icmpCode = ntohs(pkt.dport);
+      break;
+  }
+}
+
+#define AGG_FIELDS(x)                                                    \
+  (x).ifindex, (x).uid, (x).tag, (x).egress, (x).ipProto, (x).ipVersion, \
+      (x).tcpFlags, (x).localPort, (x).remotePort, (x).icmpType, (x).icmpCode
 
 std::size_t BundleHash::operator()(const BundleKey& a) const {
   std::size_t seed = 0;
@@ -119,7 +146,14 @@
       // the session and delegates writing. The corresponding handler will write
       // with the setting specified in the trace config.
       NetworkTraceHandler::Trace([&](NetworkTraceHandler::TraceContext ctx) {
-        ctx.GetDataSourceLocked()->Write(packets, ctx);
+        perfetto::LockedHandle<NetworkTraceHandler> handle =
+            ctx.GetDataSourceLocked();
+        // The underlying handle can be invalidated between when Trace starts
+        // and GetDataSourceLocked is called, but not while the LockedHandle
+        // exists and holds the lock. Check validity prior to use.
+        if (handle.valid()) {
+          handle->Write(packets, ctx);
+        }
       });
     });
 
@@ -172,7 +206,7 @@
       dst->set_timestamp(pkt.timestampNs);
       auto* event = dst->set_network_packet();
       event->set_length(pkt.length);
-      Fill(pkt, event);
+      Fill(BundleKey(pkt), event);
     }
     return;
   }
@@ -180,14 +214,13 @@
   uint64_t minTs = std::numeric_limits<uint64_t>::max();
   std::unordered_map<BundleKey, BundleDetails, BundleHash, BundleEq> bundles;
   for (const PacketTrace& pkt : packets) {
-    BundleKey key = pkt;
+    BundleKey key(pkt);
 
     // Dropping fields should remove them from the output and remove them from
-    // the aggregation key. In order to do the latter without changing the hash
-    // function, set the dropped fields to zero.
-    if (mDropTcpFlags) key.tcpFlags = 0;
-    if (mDropLocalPort) (key.egress ? key.sport : key.dport) = 0;
-    if (mDropRemotePort) (key.egress ? key.dport : key.sport) = 0;
+    // the aggregation key. Reset the optionals to indicate omission.
+    if (mDropTcpFlags) key.tcpFlags.reset();
+    if (mDropLocalPort) key.localPort.reset();
+    if (mDropRemotePort) key.remotePort.reset();
 
     minTs = std::min(minTs, pkt.timestampNs);
 
@@ -238,22 +271,18 @@
   }
 }
 
-void NetworkTraceHandler::Fill(const PacketTrace& src,
+void NetworkTraceHandler::Fill(const BundleKey& src,
                                NetworkPacketEvent* event) {
   event->set_direction(src.egress ? TrafficDirection::DIR_EGRESS
                                   : TrafficDirection::DIR_INGRESS);
   event->set_uid(src.uid);
   event->set_tag(src.tag);
 
-  if (!mDropLocalPort) {
-    event->set_local_port(ntohs(src.egress ? src.sport : src.dport));
-  }
-  if (!mDropRemotePort) {
-    event->set_remote_port(ntohs(src.egress ? src.dport : src.sport));
-  }
-  if (!mDropTcpFlags) {
-    event->set_tcp_flags(src.tcpFlags);
-  }
+  if (src.tcpFlags.has_value()) event->set_tcp_flags(*src.tcpFlags);
+  if (src.localPort.has_value()) event->set_local_port(*src.localPort);
+  if (src.remotePort.has_value()) event->set_remote_port(*src.remotePort);
+  if (src.icmpType.has_value()) event->set_icmp_type(*src.icmpType);
+  if (src.icmpCode.has_value()) event->set_icmp_code(*src.icmpCode);
 
   event->set_ip_proto(src.ipProto);
 
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
index f2c1a86..0c4f049 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
@@ -113,7 +113,7 @@
           .length = 100,
           .uid = 10,
           .tag = 123,
-          .ipProto = 6,
+          .ipProto = IPPROTO_TCP,
           .tcpFlags = 1,
       },
   };
@@ -138,12 +138,14 @@
           .sport = htons(8080),
           .dport = htons(443),
           .egress = true,
+          .ipProto = IPPROTO_TCP,
       },
       PacketTrace{
           .timestampNs = 2,
           .sport = htons(443),
           .dport = htons(8080),
           .egress = false,
+          .ipProto = IPPROTO_TCP,
       },
   };
 
@@ -161,6 +163,42 @@
               TrafficDirection::DIR_INGRESS);
 }
 
+TEST_F(NetworkTraceHandlerTest, WriteIcmpTypeAndCode) {
+  std::vector<PacketTrace> input = {
+      PacketTrace{
+          .timestampNs = 1,
+          .sport = htons(11),  // type
+          .dport = htons(22),  // code
+          .egress = true,
+          .ipProto = IPPROTO_ICMP,
+      },
+      PacketTrace{
+          .timestampNs = 2,
+          .sport = htons(33),  // type
+          .dport = htons(44),  // code
+          .egress = false,
+          .ipProto = IPPROTO_ICMPV6,
+      },
+  };
+
+  std::vector<TracePacket> events;
+  ASSERT_TRUE(TraceAndSortPackets(input, &events));
+
+  ASSERT_EQ(events.size(), 2);
+  EXPECT_FALSE(events[0].network_packet().has_local_port());
+  EXPECT_FALSE(events[0].network_packet().has_remote_port());
+  EXPECT_THAT(events[0].network_packet().icmp_type(), 11);
+  EXPECT_THAT(events[0].network_packet().icmp_code(), 22);
+  EXPECT_THAT(events[0].network_packet().direction(),
+              TrafficDirection::DIR_EGRESS);
+  EXPECT_FALSE(events[1].network_packet().local_port());
+  EXPECT_FALSE(events[1].network_packet().remote_port());
+  EXPECT_THAT(events[1].network_packet().icmp_type(), 33);
+  EXPECT_THAT(events[1].network_packet().icmp_code(), 44);
+  EXPECT_THAT(events[1].network_packet().direction(),
+              TrafficDirection::DIR_INGRESS);
+}
+
 TEST_F(NetworkTraceHandlerTest, BasicBundling) {
   // TODO: remove this once bundling becomes default. Until then, set arbitrary
   // aggregation threshold to enable bundling.
@@ -168,12 +206,12 @@
   config.set_aggregation_threshold(10);
 
   std::vector<PacketTrace> input = {
-      PacketTrace{.uid = 123, .timestampNs = 2, .length = 200},
-      PacketTrace{.uid = 123, .timestampNs = 1, .length = 100},
-      PacketTrace{.uid = 123, .timestampNs = 4, .length = 300},
+      PacketTrace{.timestampNs = 2, .length = 200, .uid = 123},
+      PacketTrace{.timestampNs = 1, .length = 100, .uid = 123},
+      PacketTrace{.timestampNs = 4, .length = 300, .uid = 123},
 
-      PacketTrace{.uid = 456, .timestampNs = 2, .length = 400},
-      PacketTrace{.uid = 456, .timestampNs = 4, .length = 100},
+      PacketTrace{.timestampNs = 2, .length = 400, .uid = 456},
+      PacketTrace{.timestampNs = 4, .length = 100, .uid = 456},
   };
 
   std::vector<TracePacket> events;
@@ -203,12 +241,12 @@
   config.set_aggregation_threshold(3);
 
   std::vector<PacketTrace> input = {
-      PacketTrace{.uid = 123, .timestampNs = 2, .length = 200},
-      PacketTrace{.uid = 123, .timestampNs = 1, .length = 100},
-      PacketTrace{.uid = 123, .timestampNs = 4, .length = 300},
+      PacketTrace{.timestampNs = 2, .length = 200, .uid = 123},
+      PacketTrace{.timestampNs = 1, .length = 100, .uid = 123},
+      PacketTrace{.timestampNs = 4, .length = 300, .uid = 123},
 
-      PacketTrace{.uid = 456, .timestampNs = 2, .length = 400},
-      PacketTrace{.uid = 456, .timestampNs = 4, .length = 100},
+      PacketTrace{.timestampNs = 2, .length = 400, .uid = 456},
+      PacketTrace{.timestampNs = 4, .length = 100, .uid = 456},
   };
 
   std::vector<TracePacket> events;
@@ -239,12 +277,17 @@
   __be16 b = htons(10001);
   std::vector<PacketTrace> input = {
       // Recall that local is `src` for egress and `dst` for ingress.
-      PacketTrace{.timestampNs = 1, .length = 2, .egress = true, .sport = a},
-      PacketTrace{.timestampNs = 2, .length = 4, .egress = false, .dport = a},
-      PacketTrace{.timestampNs = 3, .length = 6, .egress = true, .sport = b},
-      PacketTrace{.timestampNs = 4, .length = 8, .egress = false, .dport = b},
+      PacketTrace{.timestampNs = 1, .length = 2, .sport = a, .egress = true},
+      PacketTrace{.timestampNs = 2, .length = 4, .dport = a, .egress = false},
+      PacketTrace{.timestampNs = 3, .length = 6, .sport = b, .egress = true},
+      PacketTrace{.timestampNs = 4, .length = 8, .dport = b, .egress = false},
   };
 
+  // Set common fields.
+  for (PacketTrace& pkt : input) {
+    pkt.ipProto = IPPROTO_TCP;
+  }
+
   std::vector<TracePacket> events;
   ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
   ASSERT_EQ(events.size(), 2);
@@ -274,12 +317,17 @@
   __be16 b = htons(80);
   std::vector<PacketTrace> input = {
       // Recall that remote is `dst` for egress and `src` for ingress.
-      PacketTrace{.timestampNs = 1, .length = 2, .egress = true, .dport = a},
-      PacketTrace{.timestampNs = 2, .length = 4, .egress = false, .sport = a},
-      PacketTrace{.timestampNs = 3, .length = 6, .egress = true, .dport = b},
-      PacketTrace{.timestampNs = 4, .length = 8, .egress = false, .sport = b},
+      PacketTrace{.timestampNs = 1, .length = 2, .dport = a, .egress = true},
+      PacketTrace{.timestampNs = 2, .length = 4, .sport = a, .egress = false},
+      PacketTrace{.timestampNs = 3, .length = 6, .dport = b, .egress = true},
+      PacketTrace{.timestampNs = 4, .length = 8, .sport = b, .egress = false},
   };
 
+  // Set common fields.
+  for (PacketTrace& pkt : input) {
+    pkt.ipProto = IPPROTO_TCP;
+  }
+
   std::vector<TracePacket> events;
   ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
   ASSERT_EQ(events.size(), 2);
@@ -306,12 +354,17 @@
   config.set_aggregation_threshold(10);
 
   std::vector<PacketTrace> input = {
-      PacketTrace{.timestampNs = 1, .uid = 123, .length = 1, .tcpFlags = 1},
-      PacketTrace{.timestampNs = 2, .uid = 123, .length = 2, .tcpFlags = 2},
-      PacketTrace{.timestampNs = 3, .uid = 456, .length = 3, .tcpFlags = 1},
-      PacketTrace{.timestampNs = 4, .uid = 456, .length = 4, .tcpFlags = 2},
+      PacketTrace{.timestampNs = 1, .length = 1, .uid = 123, .tcpFlags = 1},
+      PacketTrace{.timestampNs = 2, .length = 2, .uid = 123, .tcpFlags = 2},
+      PacketTrace{.timestampNs = 3, .length = 3, .uid = 456, .tcpFlags = 1},
+      PacketTrace{.timestampNs = 4, .length = 4, .uid = 456, .tcpFlags = 2},
   };
 
+  // Set common fields.
+  for (PacketTrace& pkt : input) {
+    pkt.ipProto = IPPROTO_TCP;
+  }
+
   std::vector<TracePacket> events;
   ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
 
diff --git a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
index d538368..450f380 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
@@ -25,20 +25,26 @@
 #include <perfetto/tracing/platform.h>
 #include <perfetto/tracing/tracing.h>
 
+#include <unordered_map>
+#include <unordered_set>
+
+#include "netdbpf/BpfNetworkStats.h"
+
 namespace android {
 namespace bpf {
 namespace internal {
+using ::android::base::StringPrintf;
 
-void NetworkTracePoller::SchedulePolling() {
-  // Schedules another run of ourselves to recursively poll periodically.
-  mTaskRunner->PostDelayedTask(
-      [this]() {
-        mMutex.lock();
-        SchedulePolling();
-        ConsumeAllLocked();
-        mMutex.unlock();
-      },
-      mPollMs);
+void NetworkTracePoller::PollAndSchedule(perfetto::base::TaskRunner* runner,
+                                         uint32_t poll_ms) {
+  // Always schedule another run of ourselves to recursively poll periodically.
+  // The task runner is sequential so these can't run on top of each other.
+  runner->PostDelayedTask([=]() { PollAndSchedule(runner, poll_ms); }, poll_ms);
+
+  if (mMutex.try_lock()) {
+    ConsumeAllLocked();
+    mMutex.unlock();
+  }
 }
 
 bool NetworkTracePoller::Start(uint32_t pollMs) {
@@ -81,7 +87,7 @@
   // Start a task runner to run ConsumeAll every mPollMs milliseconds.
   mTaskRunner = perfetto::Platform::GetDefaultPlatform()->CreateTaskRunner({});
   mPollMs = pollMs;
-  SchedulePolling();
+  PollAndSchedule(mTaskRunner.get(), mPollMs);
 
   mSessionCount++;
   return true;
@@ -116,6 +122,28 @@
   return res.ok();
 }
 
+void NetworkTracePoller::TraceIfaces(const std::vector<PacketTrace>& packets) {
+  if (packets.empty()) return;
+
+  std::unordered_set<uint32_t> uniqueIfindex;
+  for (const PacketTrace& pkt : packets) {
+    uniqueIfindex.insert(pkt.ifindex);
+  }
+
+  for (uint32_t ifindex : uniqueIfindex) {
+    char ifname[IF_NAMESIZE] = {};
+    if (if_indextoname(ifindex, ifname) != ifname) continue;
+
+    StatsValue stats = {};
+    if (bpfGetIfIndexStats(ifindex, &stats) != 0) continue;
+
+    std::string rxTrack = StringPrintf("%s [%d] Rx Bytes", ifname, ifindex);
+    std::string txTrack = StringPrintf("%s [%d] Tx Bytes", ifname, ifindex);
+    ATRACE_INT64(rxTrack.c_str(), stats.rxBytes);
+    ATRACE_INT64(txTrack.c_str(), stats.txBytes);
+  }
+}
+
 bool NetworkTracePoller::ConsumeAll() {
   std::scoped_lock<std::mutex> lock(mMutex);
   return ConsumeAllLocked();
@@ -137,6 +165,7 @@
 
   ATRACE_INT("NetworkTracePackets", packets.size());
 
+  TraceIfaces(packets);
   mCallback(packets);
 
   return true;
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
index 133009f..59eb195 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -55,35 +55,30 @@
 bool operator==(const stats_line& lhs, const stats_line& rhs);
 bool operator<(const stats_line& lhs, const stats_line& rhs);
 
+// This mirrors BpfMap.h's:
+//   Result<Value> readValue(const Key key) const
+// for a BpfMap<uint32_t, IfaceValue>
+using IfIndexToNameFunc = std::function<Result<IfaceValue>(const uint32_t)>;
+
 // For test only
-int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
-                           const BpfMap<uint32_t, StatsValue>& appUidStatsMap);
+int bpfGetUidStatsInternal(uid_t uid, StatsValue* stats,
+                           const BpfMapRO<uint32_t, StatsValue>& appUidStatsMap);
 // For test only
-int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
-                             const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
-                             const BpfMap<uint32_t, IfaceValue>& ifaceNameMap);
+int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
+                             const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap,
+                             const IfIndexToNameFunc ifindex2name);
+// For test only
+int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
+                               const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap);
 // For test only
 int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
-                                       const BpfMap<StatsKey, StatsValue>& statsMap,
-                                       const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+                                       const BpfMapRO<StatsKey, StatsValue>& statsMap,
+                                       const IfIndexToNameFunc ifindex2name);
 // For test only
 int cleanStatsMapInternal(const base::unique_fd& cookieTagMap, const base::unique_fd& tagStatsMap);
-// For test only
-template <class Key>
-int getIfaceNameFromMap(const BpfMap<uint32_t, IfaceValue>& ifaceMap,
-                        const BpfMap<Key, StatsValue>& statsMap, uint32_t ifaceIndex, char* ifname,
-                        const Key& curKey, int64_t* unknownIfaceBytesTotal) {
-    auto iface = ifaceMap.readValue(ifaceIndex);
-    if (!iface.ok()) {
-        maybeLogUnknownIface(ifaceIndex, statsMap, curKey, unknownIfaceBytesTotal);
-        return -ENODEV;
-    }
-    strlcpy(ifname, iface.value().name, sizeof(IfaceValue));
-    return 0;
-}
 
 template <class Key>
-void maybeLogUnknownIface(int ifaceIndex, const BpfMap<Key, StatsValue>& statsMap,
+void maybeLogUnknownIface(int ifaceIndex, const BpfMapRO<Key, StatsValue>& statsMap,
                           const Key& curKey, int64_t* unknownIfaceBytesTotal) {
     // Have we already logged an error?
     if (*unknownIfaceBytesTotal == -1) {
@@ -107,11 +102,13 @@
 
 // For test only
 int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
-                                    const BpfMap<uint32_t, StatsValue>& statsMap,
-                                    const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+                                    const BpfMapRO<uint32_t, StatsValue>& statsMap,
+                                    const IfIndexToNameFunc ifindex2name);
 
-int bpfGetUidStats(uid_t uid, Stats* stats);
-int bpfGetIfaceStats(const char* iface, Stats* stats);
+void bpfRegisterIface(const char* iface);
+int bpfGetUidStats(uid_t uid, StatsValue* stats);
+int bpfGetIfaceStats(const char* iface, StatsValue* stats);
+int bpfGetIfIndexStats(int ifindex, StatsValue* stats);
 int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines);
 
 int parseBpfNetworkStatsDev(std::vector<stats_line>* lines);
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
index bc10e68..6bf186a 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
@@ -30,15 +30,33 @@
 namespace android {
 namespace bpf {
 
-// BundleKeys are PacketTraces where timestamp and length are ignored.
-using BundleKey = PacketTrace;
+// BundleKey encodes a PacketTrace minus timestamp and length. The key should
+// match many packets over time for interning. For convenience, sport/dport
+// are parsed here as either local/remote port or icmp type/code.
+struct BundleKey {
+  explicit BundleKey(const PacketTrace& pkt);
 
-// BundleKeys are hashed using all fields except timestamp/length.
+  uint32_t ifindex;
+  uint32_t uid;
+  uint32_t tag;
+
+  bool egress;
+  uint8_t ipProto;
+  uint8_t ipVersion;
+
+  std::optional<uint8_t> tcpFlags;
+  std::optional<uint16_t> localPort;
+  std::optional<uint16_t> remotePort;
+  std::optional<uint8_t> icmpType;
+  std::optional<uint8_t> icmpCode;
+};
+
+// BundleKeys are hashed using a simple hash combine.
 struct BundleHash {
   std::size_t operator()(const BundleKey& a) const;
 };
 
-// BundleKeys are equal if all fields except timestamp/length are equal.
+// BundleKeys are equal if all fields are equal.
 struct BundleEq {
   bool operator()(const BundleKey& a, const BundleKey& b) const;
 };
@@ -84,13 +102,13 @@
              NetworkTraceHandler::TraceContext& ctx);
 
  private:
-  // Convert a PacketTrace into a Perfetto trace packet.
-  void Fill(const PacketTrace& src,
+  // Fills in contextual information from a bundle without interning.
+  void Fill(const BundleKey& src,
             ::perfetto::protos::pbzero::NetworkPacketEvent* event);
 
   // Fills in contextual information either inline or via interning.
   ::perfetto::protos::pbzero::NetworkPacketBundle* FillWithInterning(
-      NetworkTraceState* state, const BundleKey& key,
+      NetworkTraceState* state, const BundleKey& src,
       ::perfetto::protos::pbzero::TracePacket* dst);
 
   static internal::NetworkTracePoller sPoller;
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
index adde51e..092ab64 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
@@ -53,9 +53,19 @@
   bool ConsumeAll() EXCLUDES(mMutex);
 
  private:
-  void SchedulePolling() REQUIRES(mMutex);
+  // Poll the ring buffer for new data and schedule another run of ourselves
+  // after poll_ms (essentially polling periodically until stopped). This takes
+  // in the runner and poll duration to prevent a hard requirement on the lock
+  // and thus a deadlock while resetting the TaskRunner. The runner pointer is
+  // always valid within tasks run by that runner.
+  void PollAndSchedule(perfetto::base::TaskRunner* runner, uint32_t poll_ms);
   bool ConsumeAllLocked() REQUIRES(mMutex);
 
+  // Record sparse iface stats via atrace. This queries the per-iface stats maps
+  // for any iface present in the vector of packets. This is inexact, but should
+  // have sufficient coverage given these are cumulative counters.
+  void TraceIfaces(const std::vector<PacketTrace>& packets) REQUIRES(mMutex);
+
   std::mutex mMutex;
 
   // Records the number of successfully started active sessions so that only the
diff --git a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
new file mode 100644
index 0000000..a92dfaf
--- /dev/null
+++ b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
@@ -0,0 +1,314 @@
+/*
+ * 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.metrics;
+
+import static com.android.metrics.NetworkNsdReported.Builder;
+
+import android.stats.connectivity.MdnsQueryResult;
+import android.stats.connectivity.NsdEventType;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+import java.util.Random;
+
+/**
+ * Class to record the NetworkNsdReported into statsd. Each client should create this class to
+ * report its data.
+ */
+public class NetworkNsdReportedMetrics {
+    // The upper bound for the random number used in metrics data sampling determines the possible
+    // sample rate.
+    private static final int RANDOM_NUMBER_UPPER_BOUND = 1000;
+    // The client id.
+    private final int mClientId;
+    private final Dependencies mDependencies;
+    private final Random mRandom;
+
+    public NetworkNsdReportedMetrics(int clientId) {
+        this(clientId, new Dependencies());
+    }
+
+    @VisibleForTesting
+    NetworkNsdReportedMetrics(int clientId, Dependencies dependencies) {
+        mClientId = clientId;
+        mDependencies = dependencies;
+        mRandom = dependencies.makeRandomGenerator();
+    }
+
+    /**
+     * Dependencies of NetworkNsdReportedMetrics, for injection in tests.
+     */
+    public static class Dependencies {
+
+        /**
+         * @see ConnectivityStatsLog
+         */
+        public void statsWrite(NetworkNsdReported event) {
+            ConnectivityStatsLog.write(ConnectivityStatsLog.NETWORK_NSD_REPORTED,
+                    event.getIsLegacy(),
+                    event.getClientId(),
+                    event.getTransactionId(),
+                    event.getIsKnownService(),
+                    event.getType().getNumber(),
+                    event.getEventDurationMillisec(),
+                    event.getQueryResult().getNumber(),
+                    event.getFoundServiceCount(),
+                    event.getFoundCallbackCount(),
+                    event.getLostCallbackCount(),
+                    event.getRepliedRequestsCount(),
+                    event.getSentQueryCount(),
+                    event.getSentPacketCount(),
+                    event.getConflictDuringProbingCount(),
+                    event.getConflictAfterProbingCount(),
+                    event.getRandomNumber());
+        }
+
+        /**
+         * @see Random
+         */
+        public Random makeRandomGenerator() {
+            return new Random();
+        }
+    }
+
+    private Builder makeReportedBuilder(boolean isLegacy, int transactionId) {
+        final Builder builder = NetworkNsdReported.newBuilder();
+        builder.setIsLegacy(isLegacy);
+        builder.setClientId(mClientId);
+        builder.setRandomNumber(mRandom.nextInt(RANDOM_NUMBER_UPPER_BOUND));
+        builder.setTransactionId(transactionId);
+        return builder;
+    }
+
+    /**
+     * Report service registration succeeded metric data.
+     *
+     * @param isLegacy Whether this call is using legacy backend.
+     * @param transactionId The transaction id of service registration.
+     * @param durationMs The duration of service registration success.
+     */
+    public void reportServiceRegistrationSucceeded(boolean isLegacy, int transactionId,
+            long durationMs) {
+        final Builder builder = makeReportedBuilder(isLegacy, transactionId);
+        builder.setType(NsdEventType.NET_REGISTER);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_REGISTERED);
+        builder.setEventDurationMillisec(durationMs);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service registration failed metric data.
+     *
+     * @param isLegacy Whether this call is using legacy backend.
+     * @param transactionId The transaction id of service registration.
+     * @param durationMs The duration of service registration failed.
+     */
+    public void reportServiceRegistrationFailed(boolean isLegacy, int transactionId,
+            long durationMs) {
+        final Builder builder = makeReportedBuilder(isLegacy, transactionId);
+        builder.setType(NsdEventType.NET_REGISTER);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_REGISTRATION_FAILED);
+        builder.setEventDurationMillisec(durationMs);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service unregistration success metric data.
+     *
+     * @param isLegacy Whether this call is using legacy backend.
+     * @param transactionId The transaction id of service registration.
+     * @param durationMs The duration of service stayed registered.
+     * @param repliedRequestsCount The replied request count of this service before unregistered it.
+     * @param sentPacketCount The total sent packet count of this service before unregistered it.
+     * @param conflictDuringProbingCount The number of conflict during probing.
+     * @param conflictAfterProbingCount The number of conflict after probing.
+     */
+    public void reportServiceUnregistration(boolean isLegacy, int transactionId, long durationMs,
+            int repliedRequestsCount, int sentPacketCount, int conflictDuringProbingCount,
+            int conflictAfterProbingCount) {
+        final Builder builder = makeReportedBuilder(isLegacy, transactionId);
+        builder.setType(NsdEventType.NET_REGISTER);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_UNREGISTERED);
+        builder.setEventDurationMillisec(durationMs);
+        builder.setRepliedRequestsCount(repliedRequestsCount);
+        builder.setSentPacketCount(sentPacketCount);
+        builder.setConflictDuringProbingCount(conflictDuringProbingCount);
+        builder.setConflictAfterProbingCount(conflictAfterProbingCount);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service discovery started metric data.
+     *
+     * @param isLegacy Whether this call is using legacy backend.
+     * @param transactionId The transaction id of service discovery.
+     */
+    public void reportServiceDiscoveryStarted(boolean isLegacy, int transactionId) {
+        final Builder builder = makeReportedBuilder(isLegacy, transactionId);
+        builder.setType(NsdEventType.NET_DISCOVER);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_DISCOVERY_STARTED);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service discovery failed metric data.
+     *
+     * @param isLegacy Whether this call is using legacy backend.
+     * @param transactionId The transaction id of service discovery.
+     * @param durationMs The duration of service discovery failed.
+     */
+    public void reportServiceDiscoveryFailed(boolean isLegacy, int transactionId,
+            long durationMs) {
+        final Builder builder = makeReportedBuilder(isLegacy, transactionId);
+        builder.setType(NsdEventType.NET_DISCOVER);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_DISCOVERY_FAILED);
+        builder.setEventDurationMillisec(durationMs);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service discovery stop metric data.
+     *
+     * @param isLegacy Whether this call is using legacy backend.
+     * @param transactionId The transaction id of service discovery.
+     * @param durationMs The duration of discovering services.
+     * @param foundCallbackCount The count of found service callbacks before stop discovery.
+     * @param lostCallbackCount The count of lost service callbacks before stop discovery.
+     * @param servicesCount The count of found services.
+     * @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,
+            boolean isServiceFromCache) {
+        final Builder builder = makeReportedBuilder(isLegacy, transactionId);
+        builder.setType(NsdEventType.NET_DISCOVER);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_DISCOVERY_STOP);
+        builder.setEventDurationMillisec(durationMs);
+        builder.setFoundCallbackCount(foundCallbackCount);
+        builder.setLostCallbackCount(lostCallbackCount);
+        builder.setFoundServiceCount(servicesCount);
+        builder.setSentQueryCount(sentQueryCount);
+        builder.setIsKnownService(isServiceFromCache);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service resolution success metric data.
+     *
+     * @param isLegacy Whether this call is using legacy backend.
+     * @param transactionId The transaction id of service resolution.
+     * @param durationMs The duration of resolving services.
+     * @param isServiceFromCache Whether the resolved service is from cache.
+     * @param sentQueryCount The count of sent queries during resolving.
+     */
+    public void reportServiceResolved(boolean isLegacy, int transactionId, long durationMs,
+            boolean isServiceFromCache, int sentQueryCount) {
+        final Builder builder = makeReportedBuilder(isLegacy, transactionId);
+        builder.setType(NsdEventType.NET_RESOLVE);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_RESOLVED);
+        builder.setEventDurationMillisec(durationMs);
+        builder.setIsKnownService(isServiceFromCache);
+        builder.setSentQueryCount(sentQueryCount);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service resolution failed metric data.
+     *
+     * @param isLegacy Whether this call is using legacy backend.
+     * @param transactionId The transaction id of service resolution.
+     * @param durationMs The duration of service resolution failed.
+     */
+    public void reportServiceResolutionFailed(boolean isLegacy, int transactionId,
+            long durationMs) {
+        final Builder builder = makeReportedBuilder(isLegacy, transactionId);
+        builder.setType(NsdEventType.NET_RESOLVE);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_RESOLUTION_FAILED);
+        builder.setEventDurationMillisec(durationMs);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service resolution stop metric data.
+     *
+     * @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,
+            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());
+    }
+
+    /**
+     * Report service info callback registered metric data.
+     *
+     * @param transactionId The transaction id of service info callback registration.
+     */
+    public void reportServiceInfoCallbackRegistered(int transactionId) {
+        // service info callback is always using new backend.
+        final Builder builder = makeReportedBuilder(false /* isLegacy */, transactionId);
+        builder.setType(NsdEventType.NET_SERVICE_INFO_CALLBACK);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_INFO_CALLBACK_REGISTERED);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service info callback registration failed metric data.
+     *
+     * @param transactionId The transaction id of service callback registration.
+     */
+    public void reportServiceInfoCallbackRegistrationFailed(int transactionId) {
+        // service info callback is always using new backend.
+        final Builder builder = makeReportedBuilder(false /* isLegacy */, transactionId);
+        builder.setType(NsdEventType.NET_SERVICE_INFO_CALLBACK);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_INFO_CALLBACK_REGISTRATION_FAILED);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service callback unregistered metric data.
+     *
+     * @param transactionId The transaction id of service callback registration.
+     * @param durationMs The duration of service callback stayed registered.
+     * @param updateCallbackCount The count of service update callbacks during this registration.
+     * @param lostCallbackCount The count of service lost callbacks during this registration.
+     * @param isServiceFromCache Whether the resolved service is from cache.
+     * @param sentQueryCount The count of sent queries during this registration.
+     */
+    public void reportServiceInfoCallbackUnregistered(int transactionId, long durationMs,
+            int updateCallbackCount, int lostCallbackCount, boolean isServiceFromCache,
+            int sentQueryCount) {
+        // service info callback is always using new backend.
+        final Builder builder = makeReportedBuilder(false /* isLegacy */, transactionId);
+        builder.setType(NsdEventType.NET_SERVICE_INFO_CALLBACK);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_INFO_CALLBACK_UNREGISTERED);
+        builder.setEventDurationMillisec(durationMs);
+        builder.setFoundCallbackCount(updateCallbackCount);
+        builder.setLostCallbackCount(lostCallbackCount);
+        builder.setIsKnownService(isServiceFromCache);
+        builder.setSentQueryCount(sentQueryCount);
+        mDependencies.statsWrite(builder.build());
+    }
+}
diff --git a/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
new file mode 100644
index 0000000..08a8603
--- /dev/null
+++ b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
@@ -0,0 +1,168 @@
+/*
+ * 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.metrics;
+
+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 com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_DISABLED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_ENABLED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__OPERATION_TYPE__ROT_READ;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UNKNOWN;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to log NetworkStats related metrics.
+ *
+ * This class does not provide thread-safe.
+ */
+public class NetworkStatsMetricsLogger {
+    final Dependencies mDeps;
+    int mReadIndex = 1;
+
+    /** Dependency class */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * Writes a NETWORK_STATS_RECORDER_FILE_OPERATION_REPORTED event to ConnectivityStatsLog.
+         */
+        public void writeRecorderFileReadingStats(int recorderType, int readIndex,
+                                                  int readLatencyMillis,
+                                                  int fileCount, int totalFileSize,
+                                                  int keys, int uids, int totalHistorySize,
+                                                  boolean useFastDataInput) {
+            ConnectivityStatsLog.write(NETWORK_STATS_RECORDER_FILE_OPERATED,
+                    NETWORK_STATS_RECORDER_FILE_OPERATED__OPERATION_TYPE__ROT_READ,
+                    recorderType,
+                    readIndex,
+                    readLatencyMillis,
+                    fileCount,
+                    totalFileSize,
+                    keys,
+                    uids,
+                    totalHistorySize,
+                    useFastDataInput
+                            ? NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_ENABLED
+                            : NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_DISABLED);
+        }
+    }
+
+    public NetworkStatsMetricsLogger() {
+        mDeps = new Dependencies();
+    }
+
+    @VisibleForTesting
+    public NetworkStatsMetricsLogger(Dependencies deps) {
+        mDeps = deps;
+    }
+
+    private static int prefixToRecorderType(@NonNull String prefix) {
+        switch (prefix) {
+            case PREFIX_XT:
+                return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
+            case PREFIX_UID:
+                return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+            case PREFIX_UID_TAG:
+                return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+            default:
+                return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UNKNOWN;
+        }
+    }
+
+    /**
+     * Get file count and total byte count for the given directory and prefix.
+     *
+     * @return File count and total byte count as a pair, or 0s if met errors.
+     */
+    private static Pair<Integer, Integer> getStatsFilesAttributes(
+            @Nullable File statsDir, @NonNull String prefix) {
+        if (statsDir == null || !statsDir.isDirectory()) return new Pair<>(0, 0);
+
+        // Only counts the matching files.
+        // The files are named in the following format:
+        //   <prefix>.<startTimestamp>-[<endTimestamp>]
+        //   e.g. uid_tag.12345-
+        // See FileRotator#FileInfo for more detail.
+        final Pattern pattern = Pattern.compile("^" + prefix + "\\.[0-9]+-[0-9]*$");
+
+        int totalFiles = 0;
+        int totalBytes = 0;
+        for (String name : emptyIfNull(statsDir.list())) {
+            if (!pattern.matcher(name).matches()) continue;
+
+            totalFiles++;
+            // Cast to int is safe since stats persistent files are several MBs in total.
+            totalBytes += (int) (new File(statsDir, name).length());
+
+        }
+        return new Pair<>(totalFiles, totalBytes);
+    }
+
+    private static String [] emptyIfNull(@Nullable String [] array) {
+        return (array == null) ? new String[0] : array;
+    }
+
+    /**
+     * Log statistics from the NetworkStatsRecorder file reading process into statsd.
+     */
+    public void logRecorderFileReading(@NonNull String prefix, int readLatencyMillis,
+            @Nullable File statsDir, @NonNull NetworkStatsCollection collection,
+            boolean useFastDataInput) {
+        final Set<Integer> uids = new HashSet<>();
+        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> entries =
+                collection.getEntries();
+
+        for (final NetworkStatsCollection.Key key : entries.keySet()) {
+            uids.add(key.uid);
+        }
+
+        int totalHistorySize = 0;
+        for (final NetworkStatsHistory history : entries.values()) {
+            totalHistorySize += history.size();
+        }
+
+        final Pair<Integer, Integer> fileAttributes = getStatsFilesAttributes(statsDir, prefix);
+        mDeps.writeRecorderFileReadingStats(prefixToRecorderType(prefix),
+                mReadIndex++,
+                readLatencyMillis,
+                fileAttributes.first /* fileCount */,
+                fileAttributes.second /* totalFileSize */,
+                entries.size(),
+                uids.size(),
+                totalHistorySize,
+                useFastDataInput);
+    }
+}
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 626c2eb..1ac2f6e 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -16,7 +16,10 @@
 
 package com.android.server;
 
+import android.annotation.Nullable;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.thread.ThreadNetworkManager;
 import android.util.Log;
 
 import com.android.modules.utils.build.SdkLevel;
@@ -25,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.thread.ThreadNetworkService;
 
 /**
  * Connectivity service initializer for core networking. This is called by system server to create
@@ -38,6 +42,7 @@
     private final NsdService mNsdService;
     private final NearbyService mNearbyService;
     private final EthernetServiceImpl mEthernetServiceImpl;
+    private final ThreadNetworkService mThreadNetworkService;
 
     public ConnectivityServiceInitializer(Context context) {
         super(context);
@@ -49,6 +54,7 @@
         mConnectivityNative = createConnectivityNativeService(context);
         mNsdService = createNsdService(context);
         mNearbyService = createNearbyService(context);
+        mThreadNetworkService = createThreadNetworkService(context);
     }
 
     @Override
@@ -85,6 +91,11 @@
                     /* allowIsolated= */ false);
         }
 
+        if (mThreadNetworkService != null) {
+            Log.i(TAG, "Registering " + ThreadNetworkManager.SERVICE_NAME);
+            publishBinderService(ThreadNetworkManager.SERVICE_NAME, mThreadNetworkService,
+                    /* allowIsolated= */ false);
+        }
     }
 
     @Override
@@ -96,6 +107,10 @@
         if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY && mEthernetServiceImpl != null) {
             mEthernetServiceImpl.start();
         }
+
+        if (mThreadNetworkService != null) {
+            mThreadNetworkService.onBootPhase(phase);
+        }
     }
 
     /**
@@ -150,4 +165,25 @@
         }
         return EthernetService.create(context);
     }
+
+    /**
+     * Returns Thread network service instance if supported.
+     * Thread is supported if all of below are satisfied:
+     * 1. the FEATURE_THREAD_NETWORK is available
+     * 2. the SDK level is V+, or SDK level is U and the device is a TV
+     */
+    @Nullable
+    private ThreadNetworkService createThreadNetworkService(final Context context) {
+        final PackageManager pm = context.getPackageManager();
+        if (!pm.hasSystemFeature(ThreadNetworkManager.FEATURE_NAME)) {
+            return null;
+        }
+        if (!SdkLevel.isAtLeastU()) {
+            return null;
+        }
+        if (!SdkLevel.isAtLeastV() && !pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+            return null;
+        }
+        return new ThreadNetworkService(context);
+    }
 }
diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java
index a884840..54b9ced 100644
--- a/service-t/src/com/android/server/IpSecService.java
+++ b/service-t/src/com/android/server/IpSecService.java
@@ -42,6 +42,7 @@
 import android.net.IpSecSpiResponse;
 import android.net.IpSecTransform;
 import android.net.IpSecTransformResponse;
+import android.net.IpSecTransformState;
 import android.net.IpSecTunnelInterfaceResponse;
 import android.net.IpSecUdpEncapResponse;
 import android.net.LinkAddress;
@@ -70,6 +71,7 @@
 import com.android.net.module.util.BinderUtils;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
 
 import libcore.io.IoUtils;
 
@@ -109,6 +111,7 @@
     @VisibleForTesting static final int MAX_PORT_BIND_ATTEMPTS = 10;
 
     private final INetd mNetd;
+    private final IpSecXfrmController mIpSecXfrmCtrl;
 
     static {
         try {
@@ -152,6 +155,11 @@
             }
             return netd;
         }
+
+        /** Get a instance of IpSecXfrmController */
+        public IpSecXfrmController getIpSecXfrmController() {
+            return new IpSecXfrmController();
+        }
     }
 
     final UidFdTagger mUidFdTagger;
@@ -1111,6 +1119,7 @@
         mContext = context;
         mDeps = Objects.requireNonNull(deps, "Missing dependencies.");
         mUidFdTagger = uidFdTagger;
+        mIpSecXfrmCtrl = mDeps.getIpSecXfrmController();
         try {
             mNetd = mDeps.getNetdInstance(mContext);
         } catch (RemoteException e) {
@@ -1862,6 +1871,52 @@
         releaseResource(userRecord.mTransformRecords, resourceId);
     }
 
+    @Override
+    public synchronized IpSecTransformState getTransformState(int transformId)
+            throws IllegalStateException, RemoteException {
+        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);
+
+        final int spi = transformInfo.getSpiRecord().getSpi();
+        final InetAddress destAddress =
+                InetAddresses.parseNumericAddress(
+                        transformInfo.getConfig().getDestinationAddress());
+        Log.d(TAG, "getTransformState for spi " + spi + " destAddress " + destAddress);
+
+        // Make netlink call
+        final XfrmNetlinkNewSaMessage xfrmNewSaMsg;
+        try {
+            xfrmNewSaMsg = mIpSecXfrmCtrl.ipSecGetSa(destAddress, Integer.toUnsignedLong(spi));
+        } catch (ErrnoException | IOException e) {
+            Log.e(TAG, "getTransformState: failed to get IpSecTransformState" + e.toString());
+            throw new IllegalStateException("Failed to get IpSecTransformState", e);
+        }
+
+        // Keep the netlink socket open to save time for the next call. It is cheap to have a
+        // persistent netlink socket in the system server
+
+        if (xfrmNewSaMsg == null) {
+            Log.e(TAG, "getTransformState: failed to get IpSecTransformState xfrmNewSaMsg is null");
+            throw new IllegalStateException("Failed to get IpSecTransformState");
+        }
+
+        return new IpSecTransformState.Builder()
+                .setTxHighestSequenceNumber(xfrmNewSaMsg.getTxSequenceNumber())
+                .setRxHighestSequenceNumber(xfrmNewSaMsg.getRxSequenceNumber())
+                .setPacketCount(xfrmNewSaMsg.getPacketCount())
+                .setByteCount(xfrmNewSaMsg.getByteCount())
+                .setReplayBitmap(xfrmNewSaMsg.getBitmap())
+                .build();
+    }
+
     /**
      * Apply an active transport mode transform to a socket, which will apply the IPsec security
      * association as a correspondent policy to the provided socket
diff --git a/service-t/src/com/android/server/IpSecXfrmController.java b/service-t/src/com/android/server/IpSecXfrmController.java
new file mode 100644
index 0000000..3cfbf83
--- /dev/null
+++ b/service-t/src/com/android/server/IpSecXfrmController.java
@@ -0,0 +1,200 @@
+/*
+ * 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 static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_RECV_BUFSIZE;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MSG_NEWSA;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkErrorMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkGetSaMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+
+/**
+ * This class handles IPSec XFRM commands between IpSecService and the Linux kernel
+ *
+ * <p>Synchronization in IpSecXfrmController is done on all entrypoints due to potential race
+ * conditions at the kernel/xfrm level.
+ */
+public class IpSecXfrmController {
+    private static final String TAG = IpSecXfrmController.class.getSimpleName();
+
+    private static final boolean VDBG = false; // STOPSHIP: if true
+
+    private static final int TIMEOUT_MS = 500;
+    private static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
+
+    @NonNull private final Dependencies mDependencies;
+    @Nullable private FileDescriptor mNetlinkSocket;
+
+    @VisibleForTesting
+    public IpSecXfrmController(@NonNull Dependencies dependencies) {
+        mDependencies = dependencies;
+    }
+
+    public IpSecXfrmController() {
+        this(new Dependencies());
+    }
+
+    /**
+     * Start the XfrmController
+     *
+     * <p>The method is idempotent
+     */
+    public synchronized void openNetlinkSocketIfNeeded() throws ErrnoException, SocketException {
+        if (mNetlinkSocket == null) {
+            mNetlinkSocket = mDependencies.newNetlinkSocket();
+        }
+    }
+
+    /**
+     * Stop the XfrmController
+     *
+     * <p>The method is idempotent
+     */
+    public synchronized void closeNetlinkSocketIfNeeded() {
+        if (mNetlinkSocket != null) {
+            mDependencies.releaseNetlinkSocket(mNetlinkSocket);
+            mNetlinkSocket = null;
+        }
+    }
+
+    @VisibleForTesting
+    public synchronized FileDescriptor getNetlinkSocket() {
+        return mNetlinkSocket;
+    }
+
+    /** Dependencies of IpSecXfrmController, for injection in tests. */
+    @VisibleForTesting
+    public static class Dependencies {
+        /** Get a new XFRM netlink socket and connect it */
+        public FileDescriptor newNetlinkSocket() throws ErrnoException, SocketException {
+            final FileDescriptor fd =
+                    NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM, SOCKET_RECV_BUFSIZE);
+            NetlinkUtils.connectToKernel(fd);
+            return fd;
+        }
+
+        /** Close the netlink socket */
+        // TODO: b/205923322 This annotation is to suppress the lint error complaining that
+        // #closeQuietly requires Android S. It can be removed when the infra supports setting
+        // service-connectivity min_sdk to 31
+        @TargetApi(Build.VERSION_CODES.S)
+        public void releaseNetlinkSocket(FileDescriptor fd) {
+            IoUtils.closeQuietly(fd);
+        }
+
+        /** Send a netlink message to a socket */
+        public void sendMessage(FileDescriptor fd, byte[] bytes)
+                throws ErrnoException, InterruptedIOException {
+            NetlinkUtils.sendMessage(fd, bytes, 0, bytes.length, TIMEOUT_MS);
+        }
+
+        /** Receive a netlink message from a socket */
+        public ByteBuffer recvMessage(FileDescriptor fd)
+                throws ErrnoException, InterruptedIOException {
+            return NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TIMEOUT_MS);
+        }
+    }
+
+    @GuardedBy("IpSecXfrmController.this")
+    private NetlinkMessage sendRequestAndGetResponse(String methodTag, byte[] req)
+            throws ErrnoException, InterruptedIOException, IOException {
+        openNetlinkSocketIfNeeded();
+
+        logD(methodTag + ":  send request " + req.length + " bytes");
+        logV(HexDump.dumpHexString(req));
+        mDependencies.sendMessage(mNetlinkSocket, req);
+
+        final ByteBuffer response = mDependencies.recvMessage(mNetlinkSocket);
+        logD(methodTag + ": receive response " + response.limit() + " bytes");
+        logV(HexDump.dumpHexString(response.array(), 0 /* offset */, response.limit()));
+
+        final NetlinkMessage msg = XfrmNetlinkMessage.parse(response, NETLINK_XFRM);
+        if (msg == null) {
+            throw new IOException("Fail to parse the response message");
+        }
+
+        final int msgType = msg.getHeader().nlmsg_type;
+        if (msgType == NetlinkConstants.NLMSG_ERROR) {
+            final NetlinkErrorMessage errorMsg = (NetlinkErrorMessage) msg;
+            final int errorCode = errorMsg.getNlMsgError().error;
+            throw new ErrnoException(methodTag, errorCode);
+        }
+
+        return msg;
+    }
+
+    /** Get the state of an IPsec SA */
+    @NonNull
+    public synchronized XfrmNetlinkNewSaMessage ipSecGetSa(
+            @NonNull final InetAddress destAddress, long spi)
+            throws ErrnoException, InterruptedIOException, IOException {
+        logD("ipSecGetSa: destAddress=" + destAddress + " spi=" + spi);
+
+        final byte[] req =
+                XfrmNetlinkGetSaMessage.newXfrmNetlinkGetSaMessage(
+                        destAddress, spi, (short) IPPROTO_ESP);
+        try {
+            final NetlinkMessage msg = sendRequestAndGetResponse("ipSecGetSa", req);
+
+            final int messageType = msg.getHeader().nlmsg_type;
+            if (messageType != XFRM_MSG_NEWSA) {
+                throw new IOException("unexpected response type " + messageType);
+            }
+
+            return (XfrmNetlinkNewSaMessage) msg;
+        } catch (IllegalArgumentException exception) {
+            // Maybe thrown from Struct.parse
+            throw new IOException("Failed to parse the response " + exception);
+        }
+    }
+
+    private static void logV(String details) {
+        if (VDBG) {
+            Log.v(TAG, details);
+        }
+    }
+
+    private static void logD(String details) {
+        Log.d(TAG, details);
+    }
+}
diff --git a/service-t/src/com/android/server/NetworkStatsServiceInitializer.java b/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
index 82a4fbd..675e5a1 100644
--- a/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
+++ b/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
@@ -22,6 +22,7 @@
 import android.util.Log;
 
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.server.net.NetworkStatsService;
 
 /**
@@ -30,6 +31,8 @@
  */
 public final class NetworkStatsServiceInitializer extends SystemService {
     private static final String TAG = NetworkStatsServiceInitializer.class.getSimpleName();
+    private static final String ENABLE_NETWORK_TRACING = "enable_network_tracing";
+    private final boolean mNetworkTracingFlagEnabled;
     private final NetworkStatsService mStatsService;
 
     public NetworkStatsServiceInitializer(Context context) {
@@ -37,6 +40,8 @@
         // Load JNI libraries used by NetworkStatsService and its dependencies
         System.loadLibrary("service-connectivity");
         mStatsService = maybeCreateNetworkStatsService(context);
+        mNetworkTracingFlagEnabled = DeviceConfigUtils.isTetheringFeatureEnabled(
+            context, ENABLE_NETWORK_TRACING);
     }
 
     @Override
@@ -48,11 +53,10 @@
             TrafficStats.init(getContext());
         }
 
-        // The following code registers the Perfetto Network Trace Handler on non-user builds.
-        // The enhanced tracing is intended to be used for debugging and diagnosing issues. This
-        // is conditional on the build type rather than `isDebuggable` to match the system_server
-        // selinux rules which only allow the Perfetto connection under the same circumstances.
-        if (SdkLevel.isAtLeastU() && !Build.TYPE.equals("user")) {
+        // The following code registers the Perfetto Network Trace Handler. The enhanced tracing
+        // is intended to be used for debugging and diagnosing issues. This is enabled by default
+        // on userdebug/eng builds and flag protected in user builds.
+        if (SdkLevel.isAtLeastU() && (mNetworkTracingFlagEnabled || !Build.TYPE.equals("user"))) {
             Log.i(TAG, "Initializing network tracing hooks");
             NetworkStatsService.nativeInitNetworkTracing();
         }
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 47a1022..64624ae 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -16,17 +16,34 @@
 
 package com.android.server;
 
+import static android.Manifest.permission.DEVICE_POWER;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_STACK;
 import static android.net.ConnectivityManager.NETID_UNSET;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
 import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
 import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
+import static android.net.nsd.NsdManager.SUBTYPE_LABEL_REGEX;
+import static android.net.nsd.NsdManager.TYPE_REGEX;
+import static android.os.Process.SYSTEM_UID;
 import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
+import static com.android.networkstack.apishim.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
+import static com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserMetrics;
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
 import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
+import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
 import android.net.ConnectivityManager;
@@ -39,21 +56,31 @@
 import android.net.mdns.aidl.IMDnsEventListener;
 import android.net.mdns.aidl.RegistrationInfo;
 import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
+import android.net.nsd.DiscoveryRequest;
 import android.net.nsd.INsdManager;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
+import android.net.nsd.IOffloadEngine;
 import android.net.nsd.MDnsManager;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.net.nsd.OffloadEngine;
+import android.net.nsd.OffloadServiceInfo;
+import android.net.wifi.WifiManager;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.provider.DeviceConfig;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
@@ -62,13 +89,20 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.metrics.NetworkNsdReportedMetrics;
+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.HandlerUtils;
 import com.android.net.module.util.InetAddressUtils;
 import com.android.net.module.util.PermissionUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.ExecutorProvider;
 import com.android.server.connectivity.mdns.MdnsAdvertiser;
+import com.android.server.connectivity.mdns.MdnsAdvertisingOptions;
 import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
+import com.android.server.connectivity.mdns.MdnsFeatureFlags;
+import com.android.server.connectivity.mdns.MdnsInterfaceSocket;
 import com.android.server.connectivity.mdns.MdnsMultinetworkSocketClient;
 import com.android.server.connectivity.mdns.MdnsSearchOptions;
 import com.android.server.connectivity.mdns.MdnsServiceBrowserListener;
@@ -83,11 +117,17 @@
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.net.UnknownHostException;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -97,6 +137,7 @@
  *
  * @hide
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class NsdService extends INsdManager.Stub {
     private static final String TAG = "NsdService";
     private static final String MDNS_TAG = "mDnsConnector";
@@ -127,7 +168,7 @@
      * "mdns_advertiser_allowlist_othertype_version"
      * would be used to toggle MdnsDiscoveryManager / MdnsAdvertiser for each type. The flags will
      * be read with
-     * {@link DeviceConfigUtils#isFeatureEnabled(Context, String, String, String, boolean)}.
+     * {@link DeviceConfigUtils#isTetheringFeatureEnabled}
      *
      * @see #MDNS_DISCOVERY_MANAGER_ALLOWLIST_FLAG_PREFIX
      * @see #MDNS_ADVERTISER_ALLOWLIST_FLAG_PREFIX
@@ -141,14 +182,32 @@
             "mdns_advertiser_allowlist_";
     private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
 
+    private static final String FORCE_ENABLE_FLAG_FOR_TEST_PREFIX = "test_";
+
+    @VisibleForTesting
+    static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
+            "mdns_config_running_app_active_importance_cutoff";
+    @VisibleForTesting
+    static final int DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
+            ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
+    private final int mRunningAppActiveImportanceCutoff;
+
     public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
     private static final long CLEANUP_DELAY_MS = 10000;
     private static final int IFACE_IDX_ANY = 0;
+    private static final int MAX_SERVICES_COUNT_METRIC_PER_CLIENT = 100;
+    @VisibleForTesting
+    static final int NO_TRANSACTION = -1;
+    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;
     private final NsdStateMachine mNsdStateMachine;
-    private final MDnsManager mMDnsManager;
+    // It can be null on V+ device since mdns native service provided by netd is removed.
+    private final @Nullable MDnsManager mMDnsManager;
     private final MDnsEventCallback mMDnsEventCallback;
     @NonNull
     private final Dependencies mDeps;
@@ -160,6 +219,8 @@
     private final MdnsSocketProvider mMdnsSocketProvider;
     @NonNull
     private final MdnsAdvertiser mAdvertiser;
+    @NonNull
+    private final Clock mClock;
     private final SharedLog mServiceLogs = LOGGER.forSubComponent(TAG);
     // WARNING : Accessing these values in any thread is not safe, it must only be changed in the
     // state machine thread. If change this outside state machine, it will need to introduce
@@ -172,8 +233,18 @@
      */
     private final HashMap<NsdServiceConnector, ClientInfo> mClients = new HashMap<>();
 
-    /* A map from unique id to client info */
-    private final SparseArray<ClientInfo> mIdToClientInfoMap= new SparseArray<>();
+    /* A map from transaction(unique) id to client info */
+    private final SparseArray<ClientInfo> mTransactionIdToClientInfoMap = new SparseArray<>();
+
+    // Note this is not final to avoid depending on the Wi-Fi service starting before NsdService
+    @Nullable
+    private WifiManager.MulticastLock mHeldMulticastLock;
+    // Fulfilled network requests that require the Wi-Fi lock: key is the obtained Network
+    // (non-null), value is the requested Network (nullable)
+    @NonNull
+    private final ArraySet<Network> mWifiLockRequiredNetworks = new ArraySet<>();
+    @NonNull
+    private final ArraySet<Integer> mRunningAppActiveUids = new ArraySet<>();
 
     private final long mCleanupDelayMs;
 
@@ -184,19 +255,36 @@
     // The number of client that ever connected.
     private int mClientNumberId = 1;
 
-    private static class MdnsListener implements MdnsServiceBrowserListener {
-        protected final int mClientId;
+    private final RemoteCallbackList<IOffloadEngine> mOffloadEngines =
+            new RemoteCallbackList<>();
+    @NonNull
+    private final MdnsFeatureFlags mMdnsFeatureFlags;
+
+    private static class OffloadEngineInfo {
+        @NonNull final String mInterfaceName;
+        final long mOffloadCapabilities;
+        final long mOffloadType;
+        @NonNull final IOffloadEngine mOffloadEngine;
+
+        OffloadEngineInfo(@NonNull IOffloadEngine offloadEngine,
+                @NonNull String interfaceName, long capabilities, long offloadType) {
+            this.mOffloadEngine = offloadEngine;
+            this.mInterfaceName = interfaceName;
+            this.mOffloadCapabilities = capabilities;
+            this.mOffloadType = offloadType;
+        }
+    }
+
+    @VisibleForTesting
+    abstract static class MdnsListener implements MdnsServiceBrowserListener {
+        protected final int mClientRequestId;
         protected final int mTransactionId;
         @NonNull
-        protected final NsdServiceInfo mReqServiceInfo;
-        @NonNull
         protected final String mListenedServiceType;
 
-        MdnsListener(int clientId, int transactionId, @NonNull NsdServiceInfo reqServiceInfo,
-                @NonNull String listenedServiceType) {
-            mClientId = clientId;
+        MdnsListener(int clientRequestId, int transactionId, @NonNull String listenedServiceType) {
+            mClientRequestId = clientRequestId;
             mTransactionId = transactionId;
-            mReqServiceInfo = reqServiceInfo;
             mListenedServiceType = listenedServiceType;
         }
 
@@ -206,7 +294,8 @@
         }
 
         @Override
-        public void onServiceFound(@NonNull MdnsServiceInfo serviceInfo) { }
+        public void onServiceFound(@NonNull MdnsServiceInfo serviceInfo,
+                boolean isServiceFromCache) { }
 
         @Override
         public void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo) { }
@@ -215,7 +304,8 @@
         public void onServiceRemoved(@NonNull MdnsServiceInfo serviceInfo) { }
 
         @Override
-        public void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo) { }
+        public void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo,
+                boolean isServiceFromCache) { }
 
         @Override
         public void onServiceNameRemoved(@NonNull MdnsServiceInfo serviceInfo) { }
@@ -227,95 +317,256 @@
         public void onSearchFailedToStart() { }
 
         @Override
-        public void onDiscoveryQuerySent(@NonNull List<String> subtypes, int transactionId) { }
+        public void onDiscoveryQuerySent(@NonNull List<String> subtypes,
+                int sentQueryTransactionId) { }
 
         @Override
         public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) { }
+
+        // Ensure toString gets overridden
+        @NonNull
+        public abstract String toString();
     }
 
     private class DiscoveryListener extends MdnsListener {
 
-        DiscoveryListener(int clientId, int transactionId, @NonNull NsdServiceInfo reqServiceInfo,
+        DiscoveryListener(int clientRequestId, int transactionId,
                 @NonNull String listenServiceType) {
-            super(clientId, transactionId, reqServiceInfo, listenServiceType);
+            super(clientRequestId, transactionId, listenServiceType);
         }
 
         @Override
-        public void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo) {
+        public void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo,
+                boolean isServiceFromCache) {
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     NsdManager.SERVICE_FOUND,
-                    new MdnsEvent(mClientId, serviceInfo));
+                    new MdnsEvent(mClientRequestId, serviceInfo, isServiceFromCache));
         }
 
         @Override
         public void onServiceNameRemoved(@NonNull MdnsServiceInfo serviceInfo) {
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     NsdManager.SERVICE_LOST,
-                    new MdnsEvent(mClientId, serviceInfo));
+                    new MdnsEvent(mClientRequestId, serviceInfo));
+        }
+
+        @Override
+        public void onDiscoveryQuerySent(@NonNull List<String> subtypes,
+                int sentQueryTransactionId) {
+            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 clientId, int transactionId, @NonNull NsdServiceInfo reqServiceInfo,
-                @NonNull String listenServiceType) {
-            super(clientId, transactionId, reqServiceInfo, listenServiceType);
+        ResolutionListener(int clientRequestId, int transactionId,
+                @NonNull String listenServiceType, @NonNull String serviceName) {
+            super(clientRequestId, transactionId, listenServiceType);
+            mServiceName = serviceName;
         }
 
         @Override
-        public void onServiceFound(MdnsServiceInfo serviceInfo) {
+        public void onServiceFound(MdnsServiceInfo serviceInfo, boolean isServiceFromCache) {
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     NsdManager.RESOLVE_SERVICE_SUCCEEDED,
-                    new MdnsEvent(mClientId, serviceInfo));
+                    new MdnsEvent(mClientRequestId, serviceInfo, isServiceFromCache));
+        }
+
+        @Override
+        public void onDiscoveryQuerySent(@NonNull List<String> subtypes,
+                int sentQueryTransactionId) {
+            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 clientId, int transactionId, @NonNull NsdServiceInfo reqServiceInfo,
-                @NonNull String listenServiceType) {
-            super(clientId, transactionId, reqServiceInfo, listenServiceType);
+        ServiceInfoListener(int clientRequestId, int transactionId,
+                @NonNull String listenServiceType, @NonNull String serviceName) {
+            super(clientRequestId, transactionId, listenServiceType);
+            this.mServiceName = serviceName;
         }
 
         @Override
-        public void onServiceFound(@NonNull MdnsServiceInfo serviceInfo) {
+        public void onServiceFound(@NonNull MdnsServiceInfo serviceInfo,
+                boolean isServiceFromCache) {
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     NsdManager.SERVICE_UPDATED,
-                    new MdnsEvent(mClientId, serviceInfo));
+                    new MdnsEvent(mClientRequestId, serviceInfo, isServiceFromCache));
         }
 
         @Override
         public void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo) {
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     NsdManager.SERVICE_UPDATED,
-                    new MdnsEvent(mClientId, serviceInfo));
+                    new MdnsEvent(mClientRequestId, serviceInfo));
         }
 
         @Override
         public void onServiceRemoved(@NonNull MdnsServiceInfo serviceInfo) {
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     NsdManager.SERVICE_UPDATED_LOST,
-                    new MdnsEvent(mClientId, serviceInfo));
+                    new MdnsEvent(mClientRequestId, serviceInfo));
         }
+
+        @Override
+        public void onDiscoveryQuerySent(@NonNull List<String> subtypes,
+                int sentQueryTransactionId) {
+            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 {
+        @Override
+        public void onSocketRequestFulfilled(@Nullable Network socketNetwork,
+                @NonNull MdnsInterfaceSocket socket, @NonNull int[] transports) {
+            // The network may be null for Wi-Fi SoftAp interfaces (tethering), but there is no APF
+            // filtering on such interfaces, so taking the multicast lock is not necessary to
+            // disable APF filtering of multicast.
+            if (socketNetwork == null
+                    || !CollectionUtils.contains(transports, TRANSPORT_WIFI)
+                    || CollectionUtils.contains(transports, TRANSPORT_VPN)) {
+                return;
+            }
+
+            if (mWifiLockRequiredNetworks.add(socketNetwork)) {
+                updateMulticastLock();
+            }
+        }
+
+        @Override
+        public void onSocketDestroyed(@Nullable Network socketNetwork,
+                @NonNull MdnsInterfaceSocket socket) {
+            if (mWifiLockRequiredNetworks.remove(socketNetwork)) {
+                updateMulticastLock();
+            }
+        }
+    }
+
+    private class UidImportanceListener implements ActivityManager.OnUidImportanceListener {
+        private final Handler mHandler;
+
+        private UidImportanceListener(Handler handler) {
+            mHandler = handler;
+        }
+
+        @Override
+        public void onUidImportance(int uid, int importance) {
+            mHandler.post(() -> handleUidImportanceChanged(uid, importance));
+        }
+    }
+
+    private void handleUidImportanceChanged(int uid, int importance) {
+        // Lower importance values are more "important"
+        final boolean modified = importance <= mRunningAppActiveImportanceCutoff
+                ? mRunningAppActiveUids.add(uid)
+                : mRunningAppActiveUids.remove(uid);
+        if (modified) {
+            updateMulticastLock();
+        }
+    }
+
+    /**
+     * Take or release the lock based on updated internal state.
+     *
+     * This determines whether the lock needs to be held based on
+     * {@link #mWifiLockRequiredNetworks}, {@link #mRunningAppActiveUids} and
+     * {@link ClientInfo#mClientRequests}, so it must be called after any of the these have been
+     * updated.
+     */
+    private void updateMulticastLock() {
+        final int needsLockUid = getMulticastLockNeededUid();
+        if (needsLockUid >= 0 && mHeldMulticastLock == null) {
+            final WifiManager wm = mContext.getSystemService(WifiManager.class);
+            if (wm == null) {
+                Log.wtf(TAG, "Got a TRANSPORT_WIFI network without WifiManager");
+                return;
+            }
+            mHeldMulticastLock = wm.createMulticastLock(TAG);
+            mHeldMulticastLock.acquire();
+            mServiceLogs.log("Taking multicast lock for uid " + needsLockUid);
+        } else if (needsLockUid < 0 && mHeldMulticastLock != null) {
+            mHeldMulticastLock.release();
+            mHeldMulticastLock = null;
+            mServiceLogs.log("Released multicast lock");
+        }
+    }
+
+    /**
+     * @return The UID of an app requiring the multicast lock, or -1 if none.
+     */
+    private int getMulticastLockNeededUid() {
+        if (mWifiLockRequiredNetworks.size() == 0) {
+            // Return early if NSD is not active, or not on any relevant network
+            return -1;
+        }
+        for (int i = 0; i < mTransactionIdToClientInfoMap.size(); i++) {
+            final ClientInfo clientInfo = mTransactionIdToClientInfoMap.valueAt(i);
+            if (!mRunningAppActiveUids.contains(clientInfo.mUid)) {
+                // Ignore non-active UIDs
+                continue;
+            }
+
+            if (clientInfo.hasAnyJavaBackendRequestForNetworks(mWifiLockRequiredNetworks)) {
+                return clientInfo.mUid;
+            }
+        }
+        return -1;
     }
 
     /**
      * Data class of mdns service callback information.
      */
     private static class MdnsEvent {
-        final int mClientId;
-        @NonNull
+        final int mClientRequestId;
+        @Nullable
         final MdnsServiceInfo mMdnsServiceInfo;
+        final boolean mIsServiceFromCache;
 
-        MdnsEvent(int clientId, @NonNull MdnsServiceInfo mdnsServiceInfo) {
-            mClientId = clientId;
+        MdnsEvent(int clientRequestId) {
+            this(clientRequestId, null /* mdnsServiceInfo */, false /* isServiceFromCache */);
+        }
+
+        MdnsEvent(int clientRequestId, @Nullable MdnsServiceInfo mdnsServiceInfo) {
+            this(clientRequestId, mdnsServiceInfo, false /* isServiceFromCache */);
+        }
+
+        MdnsEvent(int clientRequestId, @Nullable MdnsServiceInfo mdnsServiceInfo,
+                boolean isServiceFromCache) {
+            mClientRequestId = clientRequestId;
             mMdnsServiceInfo = mdnsServiceInfo;
+            mIsServiceFromCache = isServiceFromCache;
         }
     }
 
+    // TODO: Use a Handler instead of a StateMachine since there are no state changes.
     private class NsdStateMachine extends StateMachine {
 
-        private final DefaultState mDefaultState = new DefaultState();
         private final EnabledState mEnabledState = new EnabledState();
 
         @Override
@@ -328,6 +579,11 @@
                 if (DBG) Log.d(TAG, "Daemon is already started.");
                 return;
             }
+
+            if (mMDnsManager == null) {
+                Log.wtf(TAG, "maybeStartDaemon: mMDnsManager is null");
+                return;
+            }
             mMDnsManager.registerEventListener(mMDnsEventCallback);
             mMDnsManager.startDaemon();
             mIsDaemonStarted = true;
@@ -340,6 +596,11 @@
                 if (DBG) Log.d(TAG, "Daemon has not been started.");
                 return;
             }
+
+            if (mMDnsManager == null) {
+                Log.wtf(TAG, "maybeStopDaemon: mMDnsManager is null");
+                return;
+            }
             mMDnsManager.unregisterEventListener(mMDnsEventCallback);
             mMDnsManager.stopDaemon();
             mIsDaemonStarted = false;
@@ -347,7 +608,7 @@
         }
 
         private boolean isAnyRequestActive() {
-            return mIdToClientInfoMap.size() != 0;
+            return mTransactionIdToClientInfoMap.size() != 0;
         }
 
         private void scheduleStop() {
@@ -385,120 +646,12 @@
 
         NsdStateMachine(String name, Handler handler) {
             super(name, handler);
-            addState(mDefaultState);
-                addState(mEnabledState, mDefaultState);
+            addState(mEnabledState);
             State initialState = mEnabledState;
             setInitialState(initialState);
             setLogRecSize(25);
         }
 
-        class DefaultState extends State {
-            @Override
-            public boolean processMessage(Message msg) {
-                final ClientInfo cInfo;
-                final int clientId = msg.arg2;
-                switch (msg.what) {
-                    case NsdManager.REGISTER_CLIENT:
-                        final ConnectorArgs arg = (ConnectorArgs) msg.obj;
-                        final INsdManagerCallback cb = arg.callback;
-                        try {
-                            cb.asBinder().linkToDeath(arg.connector, 0);
-                            final String tag = "Client" + arg.uid + "-" + mClientNumberId++;
-                            cInfo = new ClientInfo(cb, arg.useJavaBackend,
-                                    mServiceLogs.forSubComponent(tag));
-                            mClients.put(arg.connector, cInfo);
-                        } catch (RemoteException e) {
-                            Log.w(TAG, "Client " + clientId + " has already died");
-                        }
-                        break;
-                    case NsdManager.UNREGISTER_CLIENT:
-                        final NsdServiceConnector connector = (NsdServiceConnector) msg.obj;
-                        cInfo = mClients.remove(connector);
-                        if (cInfo != null) {
-                            cInfo.expungeAllRequests();
-                            if (cInfo.isPreSClient()) {
-                                mLegacyClientCount -= 1;
-                            }
-                        }
-                        maybeStopMonitoringSocketsIfNoActiveRequest();
-                        maybeScheduleStop();
-                        break;
-                    case NsdManager.DISCOVER_SERVICES:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onDiscoverServicesFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
-                        }
-                       break;
-                    case NsdManager.STOP_DISCOVERY:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onStopDiscoveryFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
-                        }
-                        break;
-                    case NsdManager.REGISTER_SERVICE:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onRegisterServiceFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
-                        }
-                        break;
-                    case NsdManager.UNREGISTER_SERVICE:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onUnregisterServiceFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
-                        }
-                        break;
-                    case NsdManager.RESOLVE_SERVICE:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onResolveServiceFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
-                        }
-                        break;
-                    case NsdManager.STOP_RESOLUTION:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onStopResolutionFailed(
-                                    clientId, NsdManager.FAILURE_OPERATION_NOT_RUNNING);
-                        }
-                        break;
-                    case NsdManager.REGISTER_SERVICE_CALLBACK:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onServiceInfoCallbackRegistrationFailed(
-                                    clientId, NsdManager.FAILURE_BAD_PARAMETERS);
-                        }
-                        break;
-                    case NsdManager.DAEMON_CLEANUP:
-                        maybeStopDaemon();
-                        break;
-                    // This event should be only sent by the legacy (target SDK < S) clients.
-                    // Mark the sending client as legacy.
-                    case NsdManager.DAEMON_STARTUP:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cancelStop();
-                            cInfo.setPreSClient();
-                            mLegacyClientCount += 1;
-                            maybeStartDaemon();
-                        }
-                        break;
-                    default:
-                        Log.e(TAG, "Unhandled " + msg);
-                        return NOT_HANDLED;
-                }
-                return HANDLED;
-            }
-
-            private ClientInfo getClientInfoForReply(Message msg) {
-                final ListenerArgs args = (ListenerArgs) msg.obj;
-                return mClients.get(args.connector);
-            }
-        }
-
         class EnabledState extends State {
             @Override
             public void enter() {
@@ -520,38 +673,52 @@
                 return false;
             }
 
-            private void storeLegacyRequestMap(int clientId, int globalId, ClientInfo clientInfo,
-                    int what) {
-                clientInfo.mClientRequests.put(clientId, new LegacyClientRequest(globalId, what));
-                mIdToClientInfoMap.put(globalId, clientInfo);
+            private ClientRequest storeLegacyRequestMap(int clientRequestId, int transactionId,
+                    ClientInfo clientInfo, int what, long startTimeMs) {
+                final LegacyClientRequest request =
+                        new LegacyClientRequest(transactionId, what, startTimeMs);
+                clientInfo.mClientRequests.put(clientRequestId, request);
+                mTransactionIdToClientInfoMap.put(transactionId, clientInfo);
                 // Remove the cleanup event because here comes a new request.
                 cancelStop();
+                return request;
             }
 
-            private void storeAdvertiserRequestMap(int clientId, int globalId,
-                    ClientInfo clientInfo) {
-                clientInfo.mClientRequests.put(clientId, new AdvertiserClientRequest(globalId));
-                mIdToClientInfoMap.put(globalId, clientInfo);
+            private void storeAdvertiserRequestMap(int clientRequestId, int transactionId,
+                    ClientInfo clientInfo, @NonNull NsdServiceInfo serviceInfo) {
+                final String serviceFullName =
+                        serviceInfo.getServiceName() + "." + serviceInfo.getServiceType();
+                clientInfo.mClientRequests.put(clientRequestId, new AdvertiserClientRequest(
+                        transactionId, serviceInfo.getNetwork(), serviceFullName,
+                        mClock.elapsedRealtime()));
+                mTransactionIdToClientInfoMap.put(transactionId, clientInfo);
+                updateMulticastLock();
             }
 
-            private void removeRequestMap(int clientId, int globalId, ClientInfo clientInfo) {
-                final ClientRequest existing = clientInfo.mClientRequests.get(clientId);
+            private void removeRequestMap(
+                    int clientRequestId, int transactionId, ClientInfo clientInfo) {
+                final ClientRequest existing = clientInfo.mClientRequests.get(clientRequestId);
                 if (existing == null) return;
-                clientInfo.mClientRequests.remove(clientId);
-                mIdToClientInfoMap.remove(globalId);
+                clientInfo.mClientRequests.remove(clientRequestId);
+                mTransactionIdToClientInfoMap.remove(transactionId);
 
                 if (existing instanceof LegacyClientRequest) {
                     maybeScheduleStop();
                 } else {
                     maybeStopMonitoringSocketsIfNoActiveRequest();
+                    updateMulticastLock();
                 }
             }
 
-            private void storeDiscoveryManagerRequestMap(int clientId, int globalId,
-                    MdnsListener listener, ClientInfo clientInfo) {
-                clientInfo.mClientRequests.put(clientId,
-                        new DiscoveryManagerRequest(globalId, listener));
-                mIdToClientInfoMap.put(globalId, clientInfo);
+            private ClientRequest storeDiscoveryManagerRequestMap(int clientRequestId,
+                    int transactionId, MdnsListener listener, ClientInfo clientInfo,
+                    @Nullable Network requestedNetwork) {
+                final DiscoveryManagerRequest request = new DiscoveryManagerRequest(transactionId,
+                        listener, requestedNetwork, mClock.elapsedRealtime());
+                clientInfo.mClientRequests.put(clientRequestId, request);
+                mTransactionIdToClientInfoMap.put(transactionId, clientInfo);
+                updateMulticastLock();
+                return request;
             }
 
             /**
@@ -566,23 +733,84 @@
                 return MdnsUtils.truncateServiceName(originalName, MAX_LABEL_LENGTH);
             }
 
-            private void stopDiscoveryManagerRequest(ClientRequest request, int clientId, int id,
-                    ClientInfo clientInfo) {
+            private void stopDiscoveryManagerRequest(ClientRequest request, int clientRequestId,
+                    int transactionId, ClientInfo clientInfo) {
                 clientInfo.unregisterMdnsListenerFromRequest(request);
-                removeRequestMap(clientId, id, clientInfo);
+                removeRequestMap(clientRequestId, transactionId, clientInfo);
+            }
+
+            private ClientInfo getClientInfoForReply(Message msg) {
+                final ListenerArgs args = (ListenerArgs) msg.obj;
+                return mClients.get(args.connector);
+            }
+
+            /**
+             * Returns {@code false} if {@code subtypes} exceeds the maximum number limit or
+             * contains invalid subtype label.
+             */
+            private boolean checkSubtypeLabels(Set<String> subtypes) {
+                if (subtypes.size() > MAX_SUBTYPE_COUNT) {
+                    mServiceLogs.e(
+                            "Too many subtypes: " + subtypes.size() + " (max = "
+                                    + MAX_SUBTYPE_COUNT + ")");
+                    return false;
+                }
+
+                for (String subtype : subtypes) {
+                    if (!checkSubtypeLabel(subtype)) {
+                        mServiceLogs.e("Subtype " + subtype + " is invalid");
+                        return false;
+                    }
+                }
+                return true;
+            }
+
+            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);
+                }
+                return new ArraySet<>(subtypeMap.values());
+            }
+
+            private boolean checkTtl(
+                        @Nullable Duration ttl, @NonNull ClientInfo clientInfo) {
+                if (ttl == null) {
+                    return true;
+                }
+
+                final long ttlSeconds = ttl.toSeconds();
+                final int uid = clientInfo.getUid();
+
+                // Allows Thread module in the system_server to register TTL that is smaller than
+                // 30 seconds
+                final long minTtlSeconds = uid == SYSTEM_UID ? 0 : NsdManager.TTL_SECONDS_MIN;
+
+                // Allows Thread module in the system_server to register TTL that is larger than
+                // 10 hours
+                final long maxTtlSeconds =
+                        uid == SYSTEM_UID ? 0xffffffffL : NsdManager.TTL_SECONDS_MAX;
+
+                if (ttlSeconds < minTtlSeconds || ttlSeconds > maxTtlSeconds) {
+                    mServiceLogs.e("ttlSeconds exceeds allowed range (value = "
+                            + ttlSeconds + ", allowedRange = [" + minTtlSeconds
+                            + ", " + maxTtlSeconds + " ])");
+                    return false;
+                }
+                return true;
             }
 
             @Override
             public boolean processMessage(Message msg) {
                 final ClientInfo clientInfo;
-                final int id;
-                final int clientId = msg.arg2;
-                final ListenerArgs args;
+                final int transactionId;
+                final int clientRequestId = msg.arg2;
+                final OffloadEngineInfo offloadEngineInfo;
                 switch (msg.what) {
                     case NsdManager.DISCOVER_SERVICES: {
                         if (DBG) Log.d(TAG, "Discover services");
-                        args = (ListenerArgs) msg.obj;
-                        clientInfo = mClients.get(args.connector);
+                        final DiscoveryArgs discoveryArgs = (DiscoveryArgs) msg.obj;
+                        clientInfo = mClients.get(discoveryArgs.connector);
                         // If the binder death notification for a INsdManagerCallback was received
                         // before any calls are received by NsdService, the clientInfo would be
                         // cleared and cause NPE. Add a null check here to prevent this corner case.
@@ -592,66 +820,86 @@
                         }
 
                         if (requestLimitReached(clientInfo)) {
-                            clientInfo.onDiscoverServicesFailed(
-                                    clientId, NsdManager.FAILURE_MAX_LIMIT);
+                            clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
+                                    NsdManager.FAILURE_MAX_LIMIT, true /* isLegacy */);
                             break;
                         }
 
-                        final NsdServiceInfo info = args.serviceInfo;
-                        id = getUniqueId();
-                        final Pair<String, String> typeAndSubtype =
-                                parseTypeAndSubtype(info.getServiceType());
+                        final DiscoveryRequest discoveryRequest = discoveryArgs.discoveryRequest;
+                        transactionId = getUniqueId();
+                        final Pair<String, List<String>> typeAndSubtype =
+                                parseTypeAndSubtype(discoveryRequest.getServiceType());
                         final String serviceType = typeAndSubtype == null
                                 ? null : typeAndSubtype.first;
                         if (clientInfo.mUseJavaBackend
                                 || mDeps.isMdnsDiscoveryManagerEnabled(mContext)
                                 || useDiscoveryManagerForType(serviceType)) {
-                            if (serviceType == null) {
-                                clientInfo.onDiscoverServicesFailed(clientId,
-                                        NsdManager.FAILURE_INTERNAL_ERROR);
+                            if (serviceType == null || typeAndSubtype.second.size() > 1) {
+                                clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
+                                break;
+                            }
+
+                            String subtype = discoveryRequest.getSubtype();
+                            if (subtype == null && !typeAndSubtype.second.isEmpty()) {
+                                subtype = typeAndSubtype.second.get(0);
+                            }
+
+                            if (subtype != null && !checkSubtypeLabel(subtype)) {
+                                clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
                                 break;
                             }
 
                             final String listenServiceType = serviceType + ".local";
                             maybeStartMonitoringSockets();
-                            final MdnsListener listener =
-                                    new DiscoveryListener(clientId, id, info, listenServiceType);
+                            final MdnsListener listener = new DiscoveryListener(clientRequestId,
+                                    transactionId, listenServiceType);
                             final MdnsSearchOptions.Builder optionsBuilder =
                                     MdnsSearchOptions.newBuilder()
-                                            .setNetwork(info.getNetwork())
+                                            .setNetwork(discoveryRequest.getNetwork())
                                             .setRemoveExpiredService(true)
-                                            .setIsPassiveMode(true);
-                            if (typeAndSubtype.second != null) {
-                                // The parsing ensures subtype starts with an underscore.
+                                            .setQueryMode(
+                                                    mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+                                                            ? AGGRESSIVE_QUERY_MODE
+                                                            : PASSIVE_QUERY_MODE);
+                            if (subtype != null) {
+                                // checkSubtypeLabels() ensures that subtypes start with '_' but
                                 // MdnsSearchOptions expects the underscore to not be present.
-                                optionsBuilder.addSubtype(typeAndSubtype.second.substring(1));
+                                optionsBuilder.addSubtype(subtype.substring(1));
                             }
                             mMdnsDiscoveryManager.registerListener(
                                     listenServiceType, listener, optionsBuilder.build());
-                            storeDiscoveryManagerRequestMap(clientId, id, listener, clientInfo);
-                            clientInfo.onDiscoverServicesStarted(clientId, info);
-                            clientInfo.log("Register a DiscoveryListener " + id
+                            final ClientRequest request = storeDiscoveryManagerRequestMap(
+                                    clientRequestId, transactionId, listener, clientInfo,
+                                    discoveryRequest.getNetwork());
+                            clientInfo.onDiscoverServicesStarted(
+                                    clientRequestId, discoveryRequest, request);
+                            clientInfo.log("Register a DiscoveryListener " + transactionId
                                     + " for service type:" + listenServiceType);
                         } else {
                             maybeStartDaemon();
-                            if (discoverServices(id, info)) {
+                            if (discoverServices(transactionId, discoveryRequest)) {
                                 if (DBG) {
-                                    Log.d(TAG, "Discover " + msg.arg2 + " " + id
-                                            + info.getServiceType());
+                                    Log.d(TAG, "Discover " + msg.arg2 + " " + transactionId
+                                            + discoveryRequest.getServiceType());
                                 }
-                                storeLegacyRequestMap(clientId, id, clientInfo, msg.what);
-                                clientInfo.onDiscoverServicesStarted(clientId, info);
+                                final ClientRequest request = storeLegacyRequestMap(clientRequestId,
+                                        transactionId, clientInfo, msg.what,
+                                        mClock.elapsedRealtime());
+                                clientInfo.onDiscoverServicesStarted(
+                                        clientRequestId, discoveryRequest, request);
                             } else {
-                                stopServiceDiscovery(id);
-                                clientInfo.onDiscoverServicesFailed(clientId,
-                                        NsdManager.FAILURE_INTERNAL_ERROR);
+                                stopServiceDiscovery(transactionId);
+                                clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */);
                             }
                         }
                         break;
                     }
                     case NsdManager.STOP_DISCOVERY: {
                         if (DBG) Log.d(TAG, "Stop service discovery");
-                        args = (ListenerArgs) msg.obj;
+                        final ListenerArgs args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
                         // If the binder death notification for a INsdManagerCallback was received
                         // before any calls are received by NsdService, the clientInfo would be
@@ -661,33 +909,35 @@
                             break;
                         }
 
-                        final ClientRequest request = clientInfo.mClientRequests.get(clientId);
+                        final ClientRequest request =
+                                clientInfo.mClientRequests.get(clientRequestId);
                         if (request == null) {
                             Log.e(TAG, "Unknown client request in STOP_DISCOVERY");
                             break;
                         }
-                        id = request.mGlobalId;
+                        transactionId = request.mTransactionId;
                         // Note isMdnsDiscoveryManagerEnabled may have changed to false at this
                         // point, so this needs to check the type of the original request to
                         // unregister instead of looking at the flag value.
                         if (request instanceof DiscoveryManagerRequest) {
-                            stopDiscoveryManagerRequest(request, clientId, id, clientInfo);
-                            clientInfo.onStopDiscoverySucceeded(clientId);
-                            clientInfo.log("Unregister the DiscoveryListener " + id);
+                            stopDiscoveryManagerRequest(
+                                    request, clientRequestId, transactionId, clientInfo);
+                            clientInfo.onStopDiscoverySucceeded(clientRequestId, request);
+                            clientInfo.log("Unregister the DiscoveryListener " + transactionId);
                         } else {
-                            removeRequestMap(clientId, id, clientInfo);
-                            if (stopServiceDiscovery(id)) {
-                                clientInfo.onStopDiscoverySucceeded(clientId);
+                            removeRequestMap(clientRequestId, transactionId, clientInfo);
+                            if (stopServiceDiscovery(transactionId)) {
+                                clientInfo.onStopDiscoverySucceeded(clientRequestId, request);
                             } else {
                                 clientInfo.onStopDiscoveryFailed(
-                                        clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                                        clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
                             }
                         }
                         break;
                     }
                     case NsdManager.REGISTER_SERVICE: {
                         if (DBG) Log.d(TAG, "Register service");
-                        args = (ListenerArgs) msg.obj;
+                        final AdvertisingArgs args = (AdvertisingArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
                         // If the binder death notification for a INsdManagerCallback was received
                         // before any calls are received by NsdService, the clientInfo would be
@@ -698,47 +948,127 @@
                         }
 
                         if (requestLimitReached(clientInfo)) {
-                            clientInfo.onRegisterServiceFailed(
-                                    clientId, NsdManager.FAILURE_MAX_LIMIT);
+                            clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+                                    NsdManager.FAILURE_MAX_LIMIT, true /* isLegacy */);
                             break;
                         }
-
-                        id = getUniqueId();
-                        final NsdServiceInfo serviceInfo = args.serviceInfo;
+                        final AdvertisingRequest advertisingRequest = args.advertisingRequest;
+                        if (advertisingRequest == null) {
+                            Log.e(TAG, "Unknown advertisingRequest in registration");
+                            break;
+                        }
+                        final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
                         final String serviceType = serviceInfo.getServiceType();
-                        final Pair<String, String> typeSubtype = parseTypeAndSubtype(serviceType);
+                        final Pair<String, List<String>> typeSubtype = parseTypeAndSubtype(
+                                serviceType);
                         final String registerServiceType = typeSubtype == null
                                 ? null : typeSubtype.first;
+                        final String hostname = serviceInfo.getHostname();
+                        // 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.
+                        if (hostname == null) {
+                            serviceInfo.setHostAddresses(Collections.emptyList());
+                        }
                         if (clientInfo.mUseJavaBackend
                                 || mDeps.isMdnsAdvertiserEnabled(mContext)
                                 || useAdvertiserForType(registerServiceType)) {
-                            if (registerServiceType == null) {
+                            if (serviceType != null && registerServiceType == null) {
                                 Log.e(TAG, "Invalid service type: " + serviceType);
-                                clientInfo.onRegisterServiceFailed(clientId,
-                                        NsdManager.FAILURE_INTERNAL_ERROR);
+                                clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
                                 break;
                             }
-                            serviceInfo.setServiceType(registerServiceType);
-                            serviceInfo.setServiceName(truncateServiceName(
-                                    serviceInfo.getServiceName()));
+                            boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig()
+                                    & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0;
+                            // If it is an update request, then reuse the old transactionId
+                            if (isUpdateOnly) {
+                                final ClientRequest existingClientRequest =
+                                        clientInfo.mClientRequests.get(clientRequestId);
+                                if (existingClientRequest == null) {
+                                    Log.e(TAG, "Invalid update on requestId: " + clientRequestId);
+                                    clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+                                            NsdManager.FAILURE_INTERNAL_ERROR,
+                                            false /* isLegacy */);
+                                    break;
+                                }
+                                transactionId = existingClientRequest.mTransactionId;
+                            } else {
+                                transactionId = getUniqueId();
+                            }
 
+                            if (registerServiceType != null) {
+                                serviceInfo.setServiceType(registerServiceType);
+                                serviceInfo.setServiceName(
+                                        truncateServiceName(serviceInfo.getServiceName()));
+                            }
+
+                            if (!checkHostname(hostname)) {
+                                clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+                                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) {
+                                    if (!TextUtils.isEmpty(subType)) {
+                                        subtypes.add(subType);
+                                    }
+                                }
+                            }
+                            subtypes = dedupSubtypeLabels(subtypes);
+
+                            if (!checkSubtypeLabels(subtypes)) {
+                                clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+                                break;
+                            }
+
+                            if (!checkTtl(advertisingRequest.getTtl(), clientInfo)) {
+                                clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+                                break;
+                            }
+
+                            serviceInfo.setSubtypes(subtypes);
                             maybeStartMonitoringSockets();
-                            // TODO: pass in the subtype as well. Including the subtype in the
-                            // service type would generate service instance names like
-                            // Name._subtype._sub._type._tcp, which is incorrect
-                            // (it should be Name._type._tcp).
-                            mAdvertiser.addService(id, serviceInfo, typeSubtype.second);
-                            storeAdvertiserRequestMap(clientId, id, clientInfo);
+                            final MdnsAdvertisingOptions mdnsAdvertisingOptions =
+                                    MdnsAdvertisingOptions.newBuilder()
+                                            .setIsOnlyUpdate(isUpdateOnly)
+                                            .setTtl(advertisingRequest.getTtl())
+                                            .build();
+                            mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
+                                    mdnsAdvertisingOptions, clientInfo.mUid);
+                            storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
+                                    serviceInfo);
                         } else {
                             maybeStartDaemon();
-                            if (registerService(id, serviceInfo)) {
-                                if (DBG) Log.d(TAG, "Register " + clientId + " " + id);
-                                storeLegacyRequestMap(clientId, id, clientInfo, msg.what);
+                            transactionId = getUniqueId();
+                            if (registerService(transactionId, serviceInfo)) {
+                                if (DBG) {
+                                    Log.d(TAG, "Register " + clientRequestId
+                                            + " " + transactionId);
+                                }
+                                storeLegacyRequestMap(clientRequestId, transactionId, clientInfo,
+                                        msg.what, mClock.elapsedRealtime());
                                 // Return success after mDns reports success
                             } else {
-                                unregisterService(id);
-                                clientInfo.onRegisterServiceFailed(
-                                        clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                                unregisterService(transactionId);
+                                clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */);
                             }
 
                         }
@@ -746,7 +1076,7 @@
                     }
                     case NsdManager.UNREGISTER_SERVICE: {
                         if (DBG) Log.d(TAG, "unregister service");
-                        args = (ListenerArgs) msg.obj;
+                        final ListenerArgs args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
                         // If the binder death notification for a INsdManagerCallback was received
                         // before any calls are received by NsdService, the clientInfo would be
@@ -755,33 +1085,41 @@
                             Log.e(TAG, "Unknown connector in unregistration");
                             break;
                         }
-                        final ClientRequest request = clientInfo.mClientRequests.get(clientId);
+                        final ClientRequest request =
+                                clientInfo.mClientRequests.get(clientRequestId);
                         if (request == null) {
                             Log.e(TAG, "Unknown client request in UNREGISTER_SERVICE");
                             break;
                         }
-                        id = request.mGlobalId;
-                        removeRequestMap(clientId, id, clientInfo);
+                        transactionId = request.mTransactionId;
+                        removeRequestMap(clientRequestId, transactionId, clientInfo);
 
                         // Note isMdnsAdvertiserEnabled may have changed to false at this point,
                         // so this needs to check the type of the original request to unregister
                         // instead of looking at the flag value.
                         if (request instanceof AdvertiserClientRequest) {
-                            mAdvertiser.removeService(id);
-                            clientInfo.onUnregisterServiceSucceeded(clientId);
+                            final AdvertiserMetrics metrics =
+                                    mAdvertiser.getAdvertiserMetrics(transactionId);
+                            mAdvertiser.removeService(transactionId);
+                            clientInfo.onUnregisterServiceSucceeded(
+                                    clientRequestId, request, metrics);
                         } else {
-                            if (unregisterService(id)) {
-                                clientInfo.onUnregisterServiceSucceeded(clientId);
+                            if (unregisterService(transactionId)) {
+                                clientInfo.onUnregisterServiceSucceeded(clientRequestId, request,
+                                        new AdvertiserMetrics(NO_PACKET /* repliedRequestsCount */,
+                                                NO_PACKET /* sentPacketCount */,
+                                                0 /* conflictDuringProbingCount */,
+                                                0 /* conflictAfterProbingCount */));
                             } else {
                                 clientInfo.onUnregisterServiceFailed(
-                                        clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                                        clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
                             }
                         }
                         break;
                     }
                     case NsdManager.RESOLVE_SERVICE: {
                         if (DBG) Log.d(TAG, "Resolve service");
-                        args = (ListenerArgs) msg.obj;
+                        final ListenerArgs args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
                         // If the binder death notification for a INsdManagerCallback was received
                         // before any calls are received by NsdService, the clientInfo would be
@@ -792,8 +1130,8 @@
                         }
 
                         final NsdServiceInfo info = args.serviceInfo;
-                        id = getUniqueId();
-                        final Pair<String, String> typeSubtype =
+                        transactionId = getUniqueId();
+                        final Pair<String, List<String>> typeSubtype =
                                 parseTypeAndSubtype(info.getServiceType());
                         final String serviceType = typeSubtype == null
                                 ? null : typeSubtype.first;
@@ -801,47 +1139,54 @@
                                 ||  mDeps.isMdnsDiscoveryManagerEnabled(mContext)
                                 || useDiscoveryManagerForType(serviceType)) {
                             if (serviceType == null) {
-                                clientInfo.onResolveServiceFailed(clientId,
-                                        NsdManager.FAILURE_INTERNAL_ERROR);
+                                clientInfo.onResolveServiceFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
                                 break;
                             }
                             final String resolveServiceType = serviceType + ".local";
 
                             maybeStartMonitoringSockets();
-                            final MdnsListener listener =
-                                    new ResolutionListener(clientId, id, info, resolveServiceType);
+                            final MdnsListener listener = new ResolutionListener(clientRequestId,
+                                    transactionId, resolveServiceType, info.getServiceName());
+                            final int ifaceIdx = info.getNetwork() != null
+                                    ? 0 : info.getInterfaceIndex();
                             final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
                                     .setNetwork(info.getNetwork())
-                                    .setIsPassiveMode(true)
+                                    .setInterfaceIndex(ifaceIdx)
+                                    .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+                                            ? AGGRESSIVE_QUERY_MODE
+                                            : PASSIVE_QUERY_MODE)
                                     .setResolveInstanceName(info.getServiceName())
                                     .setRemoveExpiredService(true)
                                     .build();
                             mMdnsDiscoveryManager.registerListener(
                                     resolveServiceType, listener, options);
-                            storeDiscoveryManagerRequestMap(clientId, id, listener, clientInfo);
-                            clientInfo.log("Register a ResolutionListener " + id
+                            storeDiscoveryManagerRequestMap(clientRequestId, transactionId,
+                                    listener, clientInfo, info.getNetwork());
+                            clientInfo.log("Register a ResolutionListener " + transactionId
                                     + " for service type:" + resolveServiceType);
                         } else {
                             if (clientInfo.mResolvedService != null) {
-                                clientInfo.onResolveServiceFailed(
-                                        clientId, NsdManager.FAILURE_ALREADY_ACTIVE);
+                                clientInfo.onResolveServiceFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_ALREADY_ACTIVE, true /* isLegacy */);
                                 break;
                             }
 
                             maybeStartDaemon();
-                            if (resolveService(id, info)) {
+                            if (resolveService(transactionId, info)) {
                                 clientInfo.mResolvedService = new NsdServiceInfo();
-                                storeLegacyRequestMap(clientId, id, clientInfo, msg.what);
+                                storeLegacyRequestMap(clientRequestId, transactionId, clientInfo,
+                                        msg.what, mClock.elapsedRealtime());
                             } else {
-                                clientInfo.onResolveServiceFailed(
-                                        clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                                clientInfo.onResolveServiceFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */);
                             }
                         }
                         break;
                     }
                     case NsdManager.STOP_RESOLUTION: {
                         if (DBG) Log.d(TAG, "Stop service resolution");
-                        args = (ListenerArgs) msg.obj;
+                        final ListenerArgs args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
                         // If the binder death notification for a INsdManagerCallback was received
                         // before any calls are received by NsdService, the clientInfo would be
@@ -851,26 +1196,28 @@
                             break;
                         }
 
-                        final ClientRequest request = clientInfo.mClientRequests.get(clientId);
+                        final ClientRequest request =
+                                clientInfo.mClientRequests.get(clientRequestId);
                         if (request == null) {
                             Log.e(TAG, "Unknown client request in STOP_RESOLUTION");
                             break;
                         }
-                        id = request.mGlobalId;
+                        transactionId = request.mTransactionId;
                         // Note isMdnsDiscoveryManagerEnabled may have changed to false at this
                         // point, so this needs to check the type of the original request to
                         // unregister instead of looking at the flag value.
                         if (request instanceof DiscoveryManagerRequest) {
-                            stopDiscoveryManagerRequest(request, clientId, id, clientInfo);
-                            clientInfo.onStopResolutionSucceeded(clientId);
-                            clientInfo.log("Unregister the ResolutionListener " + id);
+                            stopDiscoveryManagerRequest(
+                                    request, clientRequestId, transactionId, clientInfo);
+                            clientInfo.onStopResolutionSucceeded(clientRequestId, request);
+                            clientInfo.log("Unregister the ResolutionListener " + transactionId);
                         } else {
-                            removeRequestMap(clientId, id, clientInfo);
-                            if (stopResolveService(id)) {
-                                clientInfo.onStopResolutionSucceeded(clientId);
+                            removeRequestMap(clientRequestId, transactionId, clientInfo);
+                            if (stopResolveService(transactionId)) {
+                                clientInfo.onStopResolutionSucceeded(clientRequestId, request);
                             } else {
                                 clientInfo.onStopResolutionFailed(
-                                        clientId, NsdManager.FAILURE_OPERATION_NOT_RUNNING);
+                                        clientRequestId, NsdManager.FAILURE_OPERATION_NOT_RUNNING);
                             }
                             clientInfo.mResolvedService = null;
                         }
@@ -878,7 +1225,7 @@
                     }
                     case NsdManager.REGISTER_SERVICE_CALLBACK: {
                         if (DBG) Log.d(TAG, "Register a service callback");
-                        args = (ListenerArgs) msg.obj;
+                        final ListenerArgs args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
                         // If the binder death notification for a INsdManagerCallback was received
                         // before any calls are received by NsdService, the clientInfo would be
@@ -889,37 +1236,44 @@
                         }
 
                         final NsdServiceInfo info = args.serviceInfo;
-                        id = getUniqueId();
-                        final Pair<String, String> typeAndSubtype =
+                        transactionId = getUniqueId();
+                        final Pair<String, List<String>> typeAndSubtype =
                                 parseTypeAndSubtype(info.getServiceType());
                         final String serviceType = typeAndSubtype == null
                                 ? null : typeAndSubtype.first;
                         if (serviceType == null) {
-                            clientInfo.onServiceInfoCallbackRegistrationFailed(clientId,
+                            clientInfo.onServiceInfoCallbackRegistrationFailed(clientRequestId,
                                     NsdManager.FAILURE_BAD_PARAMETERS);
                             break;
                         }
                         final String resolveServiceType = serviceType + ".local";
 
                         maybeStartMonitoringSockets();
-                        final MdnsListener listener =
-                                new ServiceInfoListener(clientId, id, info, resolveServiceType);
+                        final MdnsListener listener = new ServiceInfoListener(clientRequestId,
+                                transactionId, resolveServiceType, info.getServiceName());
+                        final int ifIndex = info.getNetwork() != null
+                                ? 0 : info.getInterfaceIndex();
                         final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
                                 .setNetwork(info.getNetwork())
-                                .setIsPassiveMode(true)
+                                .setInterfaceIndex(ifIndex)
+                                .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+                                        ? AGGRESSIVE_QUERY_MODE
+                                        : PASSIVE_QUERY_MODE)
                                 .setResolveInstanceName(info.getServiceName())
                                 .setRemoveExpiredService(true)
                                 .build();
                         mMdnsDiscoveryManager.registerListener(
                                 resolveServiceType, listener, options);
-                        storeDiscoveryManagerRequestMap(clientId, id, listener, clientInfo);
-                        clientInfo.log("Register a ServiceInfoListener " + id
+                        storeDiscoveryManagerRequestMap(clientRequestId, transactionId, listener,
+                                clientInfo, info.getNetwork());
+                        clientInfo.onServiceInfoCallbackRegistered(transactionId);
+                        clientInfo.log("Register a ServiceInfoListener " + transactionId
                                 + " for service type:" + resolveServiceType);
                         break;
                     }
                     case NsdManager.UNREGISTER_SERVICE_CALLBACK: {
                         if (DBG) Log.d(TAG, "Unregister a service callback");
-                        args = (ListenerArgs) msg.obj;
+                        final ListenerArgs args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
                         // If the binder death notification for a INsdManagerCallback was received
                         // before any calls are received by NsdService, the clientInfo would be
@@ -929,16 +1283,18 @@
                             break;
                         }
 
-                        final ClientRequest request = clientInfo.mClientRequests.get(clientId);
+                        final ClientRequest request =
+                                clientInfo.mClientRequests.get(clientRequestId);
                         if (request == null) {
                             Log.e(TAG, "Unknown client request in UNREGISTER_SERVICE_CALLBACK");
                             break;
                         }
-                        id = request.mGlobalId;
+                        transactionId = request.mTransactionId;
                         if (request instanceof DiscoveryManagerRequest) {
-                            stopDiscoveryManagerRequest(request, clientId, id, clientInfo);
-                            clientInfo.onServiceInfoCallbackUnregistered(clientId);
-                            clientInfo.log("Unregister the ServiceInfoListener " + id);
+                            stopDiscoveryManagerRequest(
+                                    request, clientRequestId, transactionId, clientInfo);
+                            clientInfo.onServiceInfoCallbackUnregistered(clientRequestId, request);
+                            clientInfo.log("Unregister the ServiceInfoListener " + transactionId);
                         } else {
                             loge("Unregister failed with non-DiscoveryManagerRequest.");
                         }
@@ -954,32 +1310,93 @@
                             return NOT_HANDLED;
                         }
                         break;
+                    case NsdManager.REGISTER_OFFLOAD_ENGINE:
+                        offloadEngineInfo = (OffloadEngineInfo) msg.obj;
+                        // TODO: Limits the number of registrations created by a given class.
+                        mOffloadEngines.register(offloadEngineInfo.mOffloadEngine,
+                                offloadEngineInfo);
+                        sendAllOffloadServiceInfos(offloadEngineInfo);
+                        break;
+                    case NsdManager.UNREGISTER_OFFLOAD_ENGINE:
+                        mOffloadEngines.unregister((IOffloadEngine) msg.obj);
+                        break;
+                    case NsdManager.REGISTER_CLIENT:
+                        final ConnectorArgs arg = (ConnectorArgs) msg.obj;
+                        final INsdManagerCallback cb = arg.callback;
+                        try {
+                            cb.asBinder().linkToDeath(arg.connector, 0);
+                            final String tag = "Client" + arg.uid + "-" + mClientNumberId++;
+                            final NetworkNsdReportedMetrics metrics =
+                                    mDeps.makeNetworkNsdReportedMetrics(
+                                            (int) mClock.elapsedRealtime());
+                            clientInfo = new ClientInfo(cb, arg.uid, arg.useJavaBackend,
+                                    mServiceLogs.forSubComponent(tag), metrics);
+                            mClients.put(arg.connector, clientInfo);
+                        } catch (RemoteException e) {
+                            Log.w(TAG, "Client request id " + clientRequestId
+                                    + " has already died");
+                        }
+                        break;
+                    case NsdManager.UNREGISTER_CLIENT:
+                        final NsdServiceConnector connector = (NsdServiceConnector) msg.obj;
+                        clientInfo = mClients.remove(connector);
+                        if (clientInfo != null) {
+                            clientInfo.expungeAllRequests();
+                            if (clientInfo.isPreSClient()) {
+                                mLegacyClientCount -= 1;
+                            }
+                        }
+                        maybeStopMonitoringSocketsIfNoActiveRequest();
+                        maybeScheduleStop();
+                        break;
+                    case NsdManager.DAEMON_CLEANUP:
+                        maybeStopDaemon();
+                        break;
+                    // This event should be only sent by the legacy (target SDK < S) clients.
+                    // Mark the sending client as legacy.
+                    case NsdManager.DAEMON_STARTUP:
+                        clientInfo = getClientInfoForReply(msg);
+                        if (clientInfo != null) {
+                            cancelStop();
+                            clientInfo.setPreSClient();
+                            mLegacyClientCount += 1;
+                            maybeStartDaemon();
+                        }
+                        break;
                     default:
+                        Log.wtf(TAG, "Unhandled " + msg);
                         return NOT_HANDLED;
                 }
                 return HANDLED;
             }
 
-            private boolean handleMDnsServiceEvent(int code, int id, Object obj) {
+            private boolean handleMDnsServiceEvent(int code, int transactionId, Object obj) {
                 NsdServiceInfo servInfo;
-                ClientInfo clientInfo = mIdToClientInfoMap.get(id);
+                ClientInfo clientInfo = mTransactionIdToClientInfoMap.get(transactionId);
                 if (clientInfo == null) {
-                    Log.e(TAG, String.format("id %d for %d has no client mapping", id, code));
+                    Log.e(TAG, String.format(
+                            "transactionId %d for %d has no client mapping", transactionId, code));
                     return false;
                 }
 
                 /* This goes in response as msg.arg2 */
-                int clientId = clientInfo.getClientId(id);
-                if (clientId < 0) {
+                int clientRequestId = clientInfo.getClientRequestId(transactionId);
+                if (clientRequestId < 0) {
                     // This can happen because of race conditions. For example,
                     // SERVICE_FOUND may race with STOP_SERVICE_DISCOVERY,
                     // and we may get in this situation.
-                    Log.d(TAG, String.format("%d for listener id %d that is no longer active",
-                            code, id));
+                    Log.d(TAG, String.format("%d for transactionId %d that is no longer active",
+                            code, transactionId));
+                    return false;
+                }
+                final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
+                if (request == null) {
+                    Log.e(TAG, "Unknown client request. clientRequestId=" + clientRequestId);
                     return false;
                 }
                 if (DBG) {
-                    Log.d(TAG, String.format("MDns service event code:%d id=%d", code, id));
+                    Log.d(TAG, String.format(
+                            "MDns service event code:%d transactionId=%d", code, transactionId));
                 }
                 switch (code) {
                     case IMDnsEventListener.SERVICE_FOUND: {
@@ -1001,7 +1418,8 @@
                             break;
                         }
                         setServiceNetworkForCallback(servInfo, info.netId, info.interfaceIdx);
-                        clientInfo.onServiceFound(clientId, servInfo);
+
+                        clientInfo.onServiceFound(clientRequestId, servInfo, request);
                         break;
                     }
                     case IMDnsEventListener.SERVICE_LOST: {
@@ -1015,23 +1433,27 @@
                         // TODO: avoid returning null in that case, possibly by remembering
                         // found services on the same interface index and their network at the time
                         setServiceNetworkForCallback(servInfo, lostNetId, info.interfaceIdx);
-                        clientInfo.onServiceLost(clientId, servInfo);
+                        clientInfo.onServiceLost(clientRequestId, servInfo, request);
                         break;
                     }
                     case IMDnsEventListener.SERVICE_DISCOVERY_FAILED:
-                        clientInfo.onDiscoverServicesFailed(
-                                clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        clientInfo.onDiscoverServicesFailed(clientRequestId,
+                                NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */,
+                                transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                         break;
                     case IMDnsEventListener.SERVICE_REGISTERED: {
                         final RegistrationInfo info = (RegistrationInfo) obj;
                         final String name = info.serviceName;
                         servInfo = new NsdServiceInfo(name, null /* serviceType */);
-                        clientInfo.onRegisterServiceSucceeded(clientId, servInfo);
+                        clientInfo.onRegisterServiceSucceeded(clientRequestId, servInfo, request);
                         break;
                     }
                     case IMDnsEventListener.SERVICE_REGISTRATION_FAILED:
-                        clientInfo.onRegisterServiceFailed(
-                                clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        clientInfo.onRegisterServiceFailed(clientRequestId,
+                                NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */,
+                                transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                         break;
                     case IMDnsEventListener.SERVICE_RESOLVED: {
                         final ResolutionInfo info = (ResolutionInfo) obj;
@@ -1059,34 +1481,40 @@
                         serviceInfo.setTxtRecords(info.txtRecord);
                         // Network will be added after SERVICE_GET_ADDR_SUCCESS
 
-                        stopResolveService(id);
-                        removeRequestMap(clientId, id, clientInfo);
+                        stopResolveService(transactionId);
+                        removeRequestMap(clientRequestId, transactionId, clientInfo);
 
-                        final int id2 = getUniqueId();
-                        if (getAddrInfo(id2, info.hostname, info.interfaceIdx)) {
-                            storeLegacyRequestMap(clientId, id2, clientInfo,
-                                    NsdManager.RESOLVE_SERVICE);
+                        final int transactionId2 = getUniqueId();
+                        if (getAddrInfo(transactionId2, info.hostname, info.interfaceIdx)) {
+                            storeLegacyRequestMap(clientRequestId, transactionId2, clientInfo,
+                                    NsdManager.RESOLVE_SERVICE, request.mStartTimeMs);
                         } else {
-                            clientInfo.onResolveServiceFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            clientInfo.onResolveServiceFailed(clientRequestId,
+                                    NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */,
+                                    transactionId,
+                                    request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                             clientInfo.mResolvedService = null;
                         }
                         break;
                     }
                     case IMDnsEventListener.SERVICE_RESOLUTION_FAILED:
                         /* NNN resolveId errorCode */
-                        stopResolveService(id);
-                        removeRequestMap(clientId, id, clientInfo);
-                        clientInfo.onResolveServiceFailed(
-                                clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        stopResolveService(transactionId);
+                        removeRequestMap(clientRequestId, transactionId, clientInfo);
+                        clientInfo.onResolveServiceFailed(clientRequestId,
+                                NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */,
+                                transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                         clientInfo.mResolvedService = null;
                         break;
                     case IMDnsEventListener.SERVICE_GET_ADDR_FAILED:
                         /* NNN resolveId errorCode */
-                        stopGetAddrInfo(id);
-                        removeRequestMap(clientId, id, clientInfo);
-                        clientInfo.onResolveServiceFailed(
-                                clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        stopGetAddrInfo(transactionId);
+                        removeRequestMap(clientRequestId, transactionId, clientInfo);
+                        clientInfo.onResolveServiceFailed(clientRequestId,
+                                NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */,
+                                transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                         clientInfo.mResolvedService = null;
                         break;
                     case IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS: {
@@ -1109,13 +1537,15 @@
                             setServiceNetworkForCallback(clientInfo.mResolvedService,
                                     netId, info.interfaceIdx);
                             clientInfo.onResolveServiceSucceeded(
-                                    clientId, clientInfo.mResolvedService);
+                                    clientRequestId, clientInfo.mResolvedService, request);
                         } else {
-                            clientInfo.onResolveServiceFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            clientInfo.onResolveServiceFailed(clientRequestId,
+                                    NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */,
+                                    transactionId,
+                                    request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                         }
-                        stopGetAddrInfo(id);
-                        removeRequestMap(clientId, id, clientInfo);
+                        stopGetAddrInfo(transactionId);
+                        removeRequestMap(clientRequestId, transactionId, clientInfo);
                         clientInfo.mResolvedService = null;
                         break;
                     }
@@ -1167,12 +1597,14 @@
                         servInfo,
                         network == null ? INetd.LOCAL_NET_ID : network.netId,
                         serviceInfo.getInterfaceIndex());
+                servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
+                servInfo.setExpirationTime(serviceInfo.getExpirationTime());
                 return servInfo;
             }
 
             private boolean handleMdnsDiscoveryManagerEvent(
                     int transactionId, int code, Object obj) {
-                final ClientInfo clientInfo = mIdToClientInfoMap.get(transactionId);
+                final ClientInfo clientInfo = mTransactionIdToClientInfoMap.get(transactionId);
                 if (clientInfo == null) {
                     Log.e(TAG, String.format(
                             "id %d for %d has no client mapping", transactionId, code));
@@ -1180,27 +1612,40 @@
                 }
 
                 final MdnsEvent event = (MdnsEvent) obj;
-                final int clientId = event.mClientId;
+                final int clientRequestId = event.mClientRequestId;
+                final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
+                if (request == null) {
+                    Log.e(TAG, "Unknown client request. clientRequestId=" + clientRequestId);
+                    return false;
+                }
+
+                // Deal with the discovery sent callback
+                if (code == DISCOVERY_QUERY_SENT_CALLBACK) {
+                    request.onQuerySent();
+                    return true;
+                }
+
+                // Deal with other callbacks.
                 final NsdServiceInfo info = buildNsdServiceInfoFromMdnsEvent(event, code);
                 // Errors are already logged if null
                 if (info == null) return false;
-                if (DBG) {
-                    Log.d(TAG, String.format("MdnsDiscoveryManager event code=%s transactionId=%d",
-                            NsdManager.nameOf(code), transactionId));
-                }
+                mServiceLogs.log(String.format(
+                        "MdnsDiscoveryManager event code=%s transactionId=%d",
+                        NsdManager.nameOf(code), transactionId));
                 switch (code) {
                     case NsdManager.SERVICE_FOUND:
-                        clientInfo.onServiceFound(clientId, info);
+                        // 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:
-                        clientInfo.onServiceLost(clientId, info);
+                        clientInfo.onServiceLost(clientRequestId, info, request);
                         break;
                     case NsdManager.RESOLVE_SERVICE_SUCCEEDED: {
-                        final ClientRequest request = clientInfo.mClientRequests.get(clientId);
-                        if (request == null) {
-                            Log.e(TAG, "Unknown client request in RESOLVE_SERVICE_SUCCEEDED");
-                            break;
-                        }
                         final MdnsServiceInfo serviceInfo = event.mMdnsServiceInfo;
                         info.setPort(serviceInfo.getPort());
 
@@ -1213,14 +1658,18 @@
                                 Log.e(TAG, "Invalid attribute", e);
                             }
                         }
+                        info.setHostname(getHostname(serviceInfo));
                         final List<InetAddress> addresses = getInetAddresses(serviceInfo);
                         if (addresses.size() != 0) {
                             info.setHostAddresses(addresses);
-                            clientInfo.onResolveServiceSucceeded(clientId, info);
+                            request.setServiceFromCache(event.mIsServiceFromCache);
+                            clientInfo.onResolveServiceSucceeded(clientRequestId, info, request);
                         } else {
                             // No address. Notify resolution failure.
-                            clientInfo.onResolveServiceFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            clientInfo.onResolveServiceFailed(clientRequestId,
+                                    NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */,
+                                    transactionId,
+                                    request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                         }
 
                         // Unregister the listener immediately like IMDnsEventListener design
@@ -1228,7 +1677,8 @@
                             Log.wtf(TAG, "non-DiscoveryManager request in DiscoveryManager event");
                             break;
                         }
-                        stopDiscoveryManagerRequest(request, clientId, transactionId, clientInfo);
+                        stopDiscoveryManagerRequest(
+                                request, clientRequestId, transactionId, clientInfo);
                         break;
                     }
                     case NsdManager.SERVICE_UPDATED: {
@@ -1245,13 +1695,20 @@
                             }
                         }
 
+                        info.setHostname(getHostname(serviceInfo));
                         final List<InetAddress> addresses = getInetAddresses(serviceInfo);
                         info.setHostAddresses(addresses);
-                        clientInfo.onServiceUpdated(clientId, info);
+                        clientInfo.onServiceUpdated(clientRequestId, info, request);
+                        // Set the ServiceFromCache flag only if the service is actually being
+                        // retrieved from the cache. This flag should not be overridden by later
+                        // service updates, which may not be cached.
+                        if (event.mIsServiceFromCache) {
+                            request.setServiceFromCache(true);
+                        }
                         break;
                     }
                     case NsdManager.SERVICE_UPDATED_LOST:
-                        clientInfo.onServiceUpdatedLost(clientId);
+                        clientInfo.onServiceUpdatedLost(clientRequestId, request);
                         break;
                     default:
                         return false;
@@ -1285,6 +1742,16 @@
         return addresses;
     }
 
+    @NonNull
+    private static String getHostname(@NonNull MdnsServiceInfo serviceInfo) {
+        String[] hostname = serviceInfo.getHostName();
+        // Strip the "local" top-level domain.
+        if (hostname.length >= 2 && hostname[hostname.length - 1].equals("local")) {
+            hostname = Arrays.copyOf(hostname, hostname.length - 1);
+        }
+        return String.join(".", hostname);
+    }
+
     private static void setServiceNetworkForCallback(NsdServiceInfo info, int netId, int ifaceIdx) {
         switch (netId) {
             case NETID_UNSET:
@@ -1343,34 +1810,78 @@
      * underscore; they are alphanumerical characters or dashes or underscore, except the
      * last one that is just alphanumerical. The last label must be _tcp or _udp.
      *
-     * <p>The subtype may also be specified with a comma after the service type, for example
-     * _type._tcp,_subtype.
+     * <p>The subtypes may also be specified with a comma after the service type, for example
+     * _type._tcp,_subtype1,_subtype2
      *
      * @param serviceType the request service type for discovery / resolution service
      * @return constructed service type or null if the given service type is invalid.
      */
     @Nullable
-    public static Pair<String, String> parseTypeAndSubtype(String serviceType) {
+    public static Pair<String, List<String>> parseTypeAndSubtype(String serviceType) {
         if (TextUtils.isEmpty(serviceType)) return null;
-
-        final String typeOrSubtypePattern = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
-        final Pattern serviceTypePattern = Pattern.compile(
-                // Optional leading subtype (_subtype._type._tcp)
-                // (?: xxx) is a non-capturing parenthesis, don't capture the dot
-                "^(?:(" + typeOrSubtypePattern + ")\\.)?"
-                        // Actual type (_type._tcp.local)
-                        + "(" + typeOrSubtypePattern + "\\._(?:tcp|udp))"
-                        // Drop '.' at the end of service type that is compatible with old backend.
-                        // e.g. allow "_type._tcp.local."
-                        + "\\.?"
-                        // Optional subtype after comma, for "_type._tcp,_subtype" format
-                        + "(?:,(" + typeOrSubtypePattern + "))?"
-                        + "$");
+        final Pattern serviceTypePattern = Pattern.compile(TYPE_REGEX);
         final Matcher matcher = serviceTypePattern.matcher(serviceType);
         if (!matcher.matches()) return null;
-        // Use the subtype either at the beginning or after the comma
-        final String subtype = matcher.group(1) != null ? matcher.group(1) : matcher.group(3);
-        return new Pair<>(matcher.group(2), subtype);
+        final String queryType = matcher.group(2);
+        // Use the subtype at the beginning
+        if (matcher.group(1) != null) {
+            return new Pair<>(queryType, List.of(matcher.group(1)));
+        }
+        // Use the subtypes at the end
+        final String subTypesStr = matcher.group(3);
+        if (subTypesStr != null && !subTypesStr.isEmpty()) {
+            final String[] subTypes = subTypesStr.substring(1).split(",");
+            return new Pair<>(queryType, List.of(subTypes));
+        }
+
+        return new Pair<>(queryType, Collections.emptyList());
+    }
+
+    /**
+     * Checks if the hostname is valid.
+     *
+     * <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-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();
     }
 
     @VisibleForTesting
@@ -1384,22 +1895,60 @@
         mContext = ctx;
         mNsdStateMachine = new NsdStateMachine(TAG, handler);
         mNsdStateMachine.start();
-        mMDnsManager = ctx.getSystemService(MDnsManager.class);
+        // It can fail on V+ device since mdns native service provided by netd is removed.
+        mMDnsManager = SdkLevel.isAtLeastV() ? null : ctx.getSystemService(MDnsManager.class);
         mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine);
         mDeps = deps;
 
         mMdnsSocketProvider = deps.makeMdnsSocketProvider(ctx, handler.getLooper(),
-                LOGGER.forSubComponent("MdnsSocketProvider"));
+                LOGGER.forSubComponent("MdnsSocketProvider"), new SocketRequestMonitor());
         // Netlink monitor starts on boot, and intentionally never stopped, to ensure that all
-        // address events are received.
+        // address events are received. When the netlink monitor starts, any IP addresses already
+        // on the interfaces will not be seen. In practice, the network will not connect at boot
+        // time As a result, all the netlink message should be observed if the netlink monitor
+        // starts here.
         handler.post(mMdnsSocketProvider::startNetLinkMonitor);
+
+        // NsdService is started after ActivityManager (startOtherServices in SystemServer, vs.
+        // startBootstrapServices).
+        mRunningAppActiveImportanceCutoff = mDeps.getDeviceConfigInt(
+                MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF,
+                DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF);
+        final ActivityManager am = ctx.getSystemService(ActivityManager.class);
+        am.addOnUidImportanceListener(new UidImportanceListener(handler),
+                mRunningAppActiveImportanceCutoff);
+
+        mMdnsFeatureFlags = new MdnsFeatureFlags.Builder()
+                .setIsMdnsOffloadFeatureEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+                        mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
+                .setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
+                        mContext, MdnsFeatureFlags.INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING))
+                .setIsExpiredServicesRemovalEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+                        mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
+                .setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+                        mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
+                .setIsKnownAnswerSuppressionEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+                        mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION))
+                .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))
+                .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
+                        mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
+                .build();
         mMdnsSocketClient =
-                new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider);
+                new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
+                        LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), mMdnsFeatureFlags);
         mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(),
-                mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"));
+                mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"),
+                mMdnsFeatureFlags);
         handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
         mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
-                new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"));
+                new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"),
+                mMdnsFeatureFlags, mContext);
+        mClock = deps.makeClock();
     }
 
     /**
@@ -1414,9 +1963,8 @@
          * @return true if the MdnsDiscoveryManager feature is enabled.
          */
         public boolean isMdnsDiscoveryManagerEnabled(Context context) {
-            return isAtLeastU() || DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING,
-                    MDNS_DISCOVERY_MANAGER_VERSION, DeviceConfigUtils.TETHERING_MODULE_NAME,
-                    false /* defaultEnabled */);
+            return isAtLeastU() || DeviceConfigUtils.isTetheringFeatureEnabled(context,
+                    MDNS_DISCOVERY_MANAGER_VERSION);
         }
 
         /**
@@ -1426,9 +1974,8 @@
          * @return true if the MdnsAdvertiser feature is enabled.
          */
         public boolean isMdnsAdvertiserEnabled(Context context) {
-            return isAtLeastU() || DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING,
-                    MDNS_ADVERTISER_VERSION, DeviceConfigUtils.TETHERING_MODULE_NAME,
-                    false /* defaultEnabled */);
+            return isAtLeastU() || DeviceConfigUtils.isTetheringFeatureEnabled(context,
+                    MDNS_ADVERTISER_VERSION);
         }
 
         /**
@@ -1442,11 +1989,17 @@
         }
 
         /**
-         * @see DeviceConfigUtils#isFeatureEnabled(Context, String, String, String, boolean)
+         * @see DeviceConfigUtils#isTetheringFeatureEnabled
          */
         public boolean isFeatureEnabled(Context context, String feature) {
-            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING,
-                    feature, DeviceConfigUtils.TETHERING_MODULE_NAME, false /* defaultEnabled */);
+            return DeviceConfigUtils.isTetheringFeatureEnabled(context, feature);
+        }
+
+        /**
+         * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut
+         */
+        public boolean isTetheringFeatureNotChickenedOut(Context context, String feature) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, feature);
         }
 
         /**
@@ -1454,8 +2007,10 @@
          */
         public MdnsDiscoveryManager makeMdnsDiscoveryManager(
                 @NonNull ExecutorProvider executorProvider,
-                @NonNull MdnsMultinetworkSocketClient socketClient, @NonNull SharedLog sharedLog) {
-            return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog);
+                @NonNull MdnsMultinetworkSocketClient socketClient, @NonNull SharedLog sharedLog,
+                @NonNull MdnsFeatureFlags featureFlags) {
+            return new MdnsDiscoveryManager(
+                    executorProvider, socketClient, sharedLog, featureFlags);
         }
 
         /**
@@ -1463,16 +2018,46 @@
          */
         public MdnsAdvertiser makeMdnsAdvertiser(
                 @NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
-                @NonNull MdnsAdvertiser.AdvertiserCallback cb, @NonNull SharedLog sharedLog) {
-            return new MdnsAdvertiser(looper, socketProvider, cb, sharedLog);
+                @NonNull MdnsAdvertiser.AdvertiserCallback cb, @NonNull SharedLog sharedLog,
+                MdnsFeatureFlags featureFlags, Context context) {
+            return new MdnsAdvertiser(looper, socketProvider, cb, sharedLog, featureFlags, context);
         }
 
         /**
          * @see MdnsSocketProvider
          */
         public MdnsSocketProvider makeMdnsSocketProvider(@NonNull Context context,
-                @NonNull Looper looper, @NonNull SharedLog sharedLog) {
-            return new MdnsSocketProvider(context, looper, sharedLog);
+                @NonNull Looper looper, @NonNull SharedLog sharedLog,
+                @NonNull MdnsSocketProvider.SocketRequestMonitor socketCreationCallback) {
+            return new MdnsSocketProvider(context, looper, sharedLog, socketCreationCallback);
+        }
+
+        /**
+         * @see DeviceConfig#getInt(String, String, int)
+         */
+        public int getDeviceConfigInt(@NonNull String config, int defaultValue) {
+            return DeviceConfig.getInt(NAMESPACE_TETHERING, config, defaultValue);
+        }
+
+        /**
+         * @see Binder#getCallingUid()
+         */
+        public int getCallingUid() {
+            return Binder.getCallingUid();
+        }
+
+        /**
+         * @see NetworkNsdReportedMetrics
+         */
+        public NetworkNsdReportedMetrics makeNetworkNsdReportedMetrics(int clientId) {
+            return new NetworkNsdReportedMetrics(clientId);
+        }
+
+        /**
+         * @see MdnsUtils.Clock
+         */
+        public Clock makeClock() {
+            return new Clock();
         }
     }
 
@@ -1561,46 +2146,113 @@
         }
     }
 
+    private void sendAllOffloadServiceInfos(@NonNull OffloadEngineInfo offloadEngineInfo) {
+        final String targetInterface = offloadEngineInfo.mInterfaceName;
+        final IOffloadEngine offloadEngine = offloadEngineInfo.mOffloadEngine;
+        final List<MdnsAdvertiser.OffloadServiceInfoWrapper> offloadWrappers =
+                mAdvertiser.getAllInterfaceOffloadServiceInfos(targetInterface);
+        for (MdnsAdvertiser.OffloadServiceInfoWrapper wrapper : offloadWrappers) {
+            try {
+                offloadEngine.onOffloadServiceUpdated(wrapper.mOffloadServiceInfo);
+            } catch (RemoteException e) {
+                // Can happen in regular cases, do not log a stacktrace
+                Log.i(TAG, "Failed to send offload callback, remote died: " + e.getMessage());
+            }
+        }
+    }
+
+    private void sendOffloadServiceInfosUpdate(@NonNull String targetInterfaceName,
+            @NonNull OffloadServiceInfo offloadServiceInfo, boolean isRemove) {
+        final int count = mOffloadEngines.beginBroadcast();
+        try {
+            for (int i = 0; i < count; i++) {
+                final OffloadEngineInfo offloadEngineInfo =
+                        (OffloadEngineInfo) mOffloadEngines.getBroadcastCookie(i);
+                final String interfaceName = offloadEngineInfo.mInterfaceName;
+                if (!targetInterfaceName.equals(interfaceName)
+                        || ((offloadEngineInfo.mOffloadType
+                        & offloadServiceInfo.getOffloadType()) == 0)) {
+                    continue;
+                }
+                try {
+                    if (isRemove) {
+                        mOffloadEngines.getBroadcastItem(i).onOffloadServiceRemoved(
+                                offloadServiceInfo);
+                    } else {
+                        mOffloadEngines.getBroadcastItem(i).onOffloadServiceUpdated(
+                                offloadServiceInfo);
+                    }
+                } catch (RemoteException e) {
+                    // Can happen in regular cases, do not log a stacktrace
+                    Log.i(TAG, "Failed to send offload callback, remote died: " + e.getMessage());
+                }
+            }
+        } finally {
+            mOffloadEngines.finishBroadcast();
+        }
+    }
+
     private class AdvertiserCallback implements MdnsAdvertiser.AdvertiserCallback {
+        // TODO: add a callback to notify when a service is being added on each interface (as soon
+        // as probing starts), and call mOffloadCallbacks. This callback is for
+        // OFFLOAD_CAPABILITY_FILTER_REPLIES offload type.
+
         @Override
-        public void onRegisterServiceSucceeded(int serviceId, NsdServiceInfo registeredInfo) {
-            final ClientInfo clientInfo = getClientInfoOrLog(serviceId);
+        public void onRegisterServiceSucceeded(int transactionId, NsdServiceInfo registeredInfo) {
+            mServiceLogs.log("onRegisterServiceSucceeded: transactionId " + transactionId);
+            final ClientInfo clientInfo = getClientInfoOrLog(transactionId);
             if (clientInfo == null) return;
 
-            final int clientId = getClientIdOrLog(clientInfo, serviceId);
-            if (clientId < 0) return;
+            final int clientRequestId = getClientRequestIdOrLog(clientInfo, transactionId);
+            if (clientRequestId < 0) return;
 
-            // onRegisterServiceSucceeded only has the service name in its info. This aligns with
-            // historical behavior.
+            // onRegisterServiceSucceeded only has the service name and hostname in its info. This
+            // aligns with historical behavior.
             final NsdServiceInfo cbInfo = new NsdServiceInfo(registeredInfo.getServiceName(), null);
-            clientInfo.onRegisterServiceSucceeded(clientId, cbInfo);
+            cbInfo.setHostname(registeredInfo.getHostname());
+            final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
+            clientInfo.onRegisterServiceSucceeded(clientRequestId, cbInfo, request);
         }
 
         @Override
-        public void onRegisterServiceFailed(int serviceId, int errorCode) {
-            final ClientInfo clientInfo = getClientInfoOrLog(serviceId);
+        public void onRegisterServiceFailed(int transactionId, int errorCode) {
+            final ClientInfo clientInfo = getClientInfoOrLog(transactionId);
             if (clientInfo == null) return;
 
-            final int clientId = getClientIdOrLog(clientInfo, serviceId);
-            if (clientId < 0) return;
-
-            clientInfo.onRegisterServiceFailed(clientId, errorCode);
+            final int clientRequestId = getClientRequestIdOrLog(clientInfo, transactionId);
+            if (clientRequestId < 0) return;
+            final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
+            clientInfo.onRegisterServiceFailed(clientRequestId, errorCode, false /* isLegacy */,
+                    transactionId, request.calculateRequestDurationMs(mClock.elapsedRealtime()));
         }
 
-        private ClientInfo getClientInfoOrLog(int serviceId) {
-            final ClientInfo clientInfo = mIdToClientInfoMap.get(serviceId);
+        @Override
+        public void onOffloadStartOrUpdate(@NonNull String interfaceName,
+                @NonNull OffloadServiceInfo offloadServiceInfo) {
+            sendOffloadServiceInfosUpdate(interfaceName, offloadServiceInfo, false /* isRemove */);
+        }
+
+        @Override
+        public void onOffloadStop(@NonNull String interfaceName,
+                @NonNull OffloadServiceInfo offloadServiceInfo) {
+            sendOffloadServiceInfosUpdate(interfaceName, offloadServiceInfo, true /* isRemove */);
+        }
+
+        private ClientInfo getClientInfoOrLog(int transactionId) {
+            final ClientInfo clientInfo = mTransactionIdToClientInfoMap.get(transactionId);
             if (clientInfo == null) {
-                Log.e(TAG, String.format("Callback for service %d has no client", serviceId));
+                Log.e(TAG, String.format("Callback for service %d has no client", transactionId));
             }
             return clientInfo;
         }
 
-        private int getClientIdOrLog(@NonNull ClientInfo info, int serviceId) {
-            final int clientId = info.getClientId(serviceId);
-            if (clientId < 0) {
-                Log.e(TAG, String.format("Client ID not found for service %d", serviceId));
+        private int getClientRequestIdOrLog(@NonNull ClientInfo info, int transactionId) {
+            final int clientRequestId = info.getClientRequestId(transactionId);
+            if (clientRequestId < 0) {
+                Log.e(TAG, String.format(
+                        "Client request ID not found for service %d", transactionId));
             }
-            return clientId;
+            return clientRequestId;
         }
     }
 
@@ -1622,11 +2274,14 @@
     @Override
     public INsdServiceConnector connect(INsdManagerCallback cb, boolean useJavaBackend) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "NsdService");
+        final int uid = mDeps.getCallingUid();
+        if (cb == null) {
+            throw new IllegalArgumentException("Unknown client callback from uid=" + uid);
+        }
         if (DBG) Log.d(TAG, "New client connect. useJavaBackend=" + useJavaBackend);
         final INsdServiceConnector connector = new NsdServiceConnector();
         mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.REGISTER_CLIENT,
-                new ConnectorArgs((NsdServiceConnector) connector, cb, useJavaBackend,
-                        Binder.getCallingUid())));
+                new ConnectorArgs((NsdServiceConnector) connector, cb, useJavaBackend, uid)));
         return connector;
     }
 
@@ -1639,33 +2294,56 @@
         }
     }
 
+    private static class AdvertisingArgs {
+        public final NsdServiceConnector connector;
+        public final AdvertisingRequest advertisingRequest;
+
+        AdvertisingArgs(NsdServiceConnector connector, AdvertisingRequest advertisingRequest) {
+            this.connector = connector;
+            this.advertisingRequest = advertisingRequest;
+        }
+    }
+
+    private static final class DiscoveryArgs {
+        public final NsdServiceConnector connector;
+        public final DiscoveryRequest discoveryRequest;
+        DiscoveryArgs(NsdServiceConnector connector, DiscoveryRequest discoveryRequest) {
+            this.connector = connector;
+            this.discoveryRequest = discoveryRequest;
+        }
+    }
+
     private class NsdServiceConnector extends INsdServiceConnector.Stub
             implements IBinder.DeathRecipient  {
+
         @Override
-        public void registerService(int listenerKey, NsdServiceInfo serviceInfo) {
+        public void registerService(int listenerKey, AdvertisingRequest advertisingRequest)
+                throws RemoteException {
+            NsdManager.checkServiceInfoForRegistration(advertisingRequest.getServiceInfo());
             mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
                     NsdManager.REGISTER_SERVICE, 0, listenerKey,
-                    new ListenerArgs(this, serviceInfo)));
+                    new AdvertisingArgs(this, advertisingRequest)
+            ));
         }
 
         @Override
         public void unregisterService(int listenerKey) {
             mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
                     NsdManager.UNREGISTER_SERVICE, 0, listenerKey,
-                    new ListenerArgs(this, null)));
+                    new ListenerArgs(this, (NsdServiceInfo) null)));
         }
 
         @Override
-        public void discoverServices(int listenerKey, NsdServiceInfo serviceInfo) {
+        public void discoverServices(int listenerKey, DiscoveryRequest discoveryRequest) {
             mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
                     NsdManager.DISCOVER_SERVICES, 0, listenerKey,
-                    new ListenerArgs(this, serviceInfo)));
+                    new DiscoveryArgs(this, discoveryRequest)));
         }
 
         @Override
         public void stopDiscovery(int listenerKey) {
-            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
-                    NsdManager.STOP_DISCOVERY, 0, listenerKey, new ListenerArgs(this, null)));
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_DISCOVERY,
+                    0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
         }
 
         @Override
@@ -1677,8 +2355,8 @@
 
         @Override
         public void stopResolution(int listenerKey) {
-            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
-                    NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null)));
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_RESOLUTION,
+                    0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
         }
 
         @Override
@@ -1692,19 +2370,66 @@
         public void unregisterServiceInfoCallback(int listenerKey) {
             mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
                     NsdManager.UNREGISTER_SERVICE_CALLBACK, 0, listenerKey,
-                    new ListenerArgs(this, null)));
+                    new ListenerArgs(this, (NsdServiceInfo) null)));
         }
 
         @Override
         public void startDaemon() {
-            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
-                    NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.DAEMON_STARTUP,
+                    new ListenerArgs(this, (NsdServiceInfo) null)));
         }
 
         @Override
         public void binderDied() {
             mNsdStateMachine.sendMessage(
                     mNsdStateMachine.obtainMessage(NsdManager.UNREGISTER_CLIENT, this));
+
+        }
+
+        @Override
+        public void registerOffloadEngine(String ifaceName, IOffloadEngine cb,
+                @OffloadEngine.OffloadCapability long offloadCapabilities,
+                @OffloadEngine.OffloadType long offloadTypes) {
+            checkOffloadEnginePermission(mContext);
+            Objects.requireNonNull(ifaceName);
+            Objects.requireNonNull(cb);
+            mNsdStateMachine.sendMessage(
+                    mNsdStateMachine.obtainMessage(NsdManager.REGISTER_OFFLOAD_ENGINE,
+                            new OffloadEngineInfo(cb, ifaceName, offloadCapabilities,
+                                    offloadTypes)));
+        }
+
+        @Override
+        public void unregisterOffloadEngine(IOffloadEngine cb) {
+            checkOffloadEnginePermission(mContext);
+            Objects.requireNonNull(cb);
+            mNsdStateMachine.sendMessage(
+                    mNsdStateMachine.obtainMessage(NsdManager.UNREGISTER_OFFLOAD_ENGINE, cb));
+        }
+
+        private static void checkOffloadEnginePermission(Context context) {
+            if (!SdkLevel.isAtLeastT()) {
+                throw new SecurityException("API is not available in before API level 33");
+            }
+
+            final ArrayList<String> permissionsList = new ArrayList<>(Arrays.asList(NETWORK_STACK,
+                    PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS));
+
+            if (SdkLevel.isAtLeastV()) {
+                // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V.
+                permissionsList.add(REGISTER_NSD_OFFLOAD_ENGINE);
+            } else if (SdkLevel.isAtLeastU()) {
+                // REGISTER_NSD_OFFLOAD_ENGINE cannot be backport to U. In U, check the DEVICE_POWER
+                // permission instead.
+                permissionsList.add(DEVICE_POWER);
+            }
+
+            if (PermissionUtils.hasAnyPermissionOf(context,
+                    permissionsList.toArray(new String[0]))) {
+                return;
+            }
+            throw new SecurityException("Requires one of the following permissions: "
+                    + String.join(", ", permissionsList) + ".");
         }
     }
 
@@ -1721,9 +2446,14 @@
         return mUniqueId;
     }
 
-    private boolean registerService(int regId, NsdServiceInfo service) {
+    private boolean registerService(int transactionId, NsdServiceInfo service) {
+        if (mMDnsManager == null) {
+            Log.wtf(TAG, "registerService: mMDnsManager is null");
+            return false;
+        }
+
         if (DBG) {
-            Log.d(TAG, "registerService: " + regId + " " + service);
+            Log.d(TAG, "registerService: " + transactionId + " " + service);
         }
         String name = service.getServiceName();
         String type = service.getServiceType();
@@ -1734,28 +2464,46 @@
             Log.e(TAG, "Interface to register service on not found");
             return false;
         }
-        return mMDnsManager.registerService(regId, name, type, port, textRecord, registerInterface);
+        return mMDnsManager.registerService(
+                transactionId, name, type, port, textRecord, registerInterface);
     }
 
-    private boolean unregisterService(int regId) {
-        return mMDnsManager.stopOperation(regId);
+    private boolean unregisterService(int transactionId) {
+        if (mMDnsManager == null) {
+            Log.wtf(TAG, "unregisterService: mMDnsManager is null");
+            return false;
+        }
+        return mMDnsManager.stopOperation(transactionId);
     }
 
-    private boolean discoverServices(int discoveryId, NsdServiceInfo serviceInfo) {
-        final String type = serviceInfo.getServiceType();
-        final int discoverInterface = getNetworkInterfaceIndex(serviceInfo);
-        if (serviceInfo.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
+    private boolean discoverServices(int transactionId, DiscoveryRequest discoveryRequest) {
+        if (mMDnsManager == null) {
+            Log.wtf(TAG, "discoverServices: mMDnsManager is null");
+            return false;
+        }
+
+        final String type = discoveryRequest.getServiceType();
+        final int discoverInterface = getNetworkInterfaceIndex(discoveryRequest);
+        if (discoveryRequest.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
             Log.e(TAG, "Interface to discover service on not found");
             return false;
         }
-        return mMDnsManager.discover(discoveryId, type, discoverInterface);
+        return mMDnsManager.discover(transactionId, type, discoverInterface);
     }
 
-    private boolean stopServiceDiscovery(int discoveryId) {
-        return mMDnsManager.stopOperation(discoveryId);
+    private boolean stopServiceDiscovery(int transactionId) {
+        if (mMDnsManager == null) {
+            Log.wtf(TAG, "stopServiceDiscovery: mMDnsManager is null");
+            return false;
+        }
+        return mMDnsManager.stopOperation(transactionId);
     }
 
-    private boolean resolveService(int resolveId, NsdServiceInfo service) {
+    private boolean resolveService(int transactionId, NsdServiceInfo service) {
+        if (mMDnsManager == null) {
+            Log.wtf(TAG, "resolveService: mMDnsManager is null");
+            return false;
+        }
         final String name = service.getServiceName();
         final String type = service.getServiceType();
         final int resolveInterface = getNetworkInterfaceIndex(service);
@@ -1763,7 +2511,7 @@
             Log.e(TAG, "Interface to resolve service on not found");
             return false;
         }
-        return mMDnsManager.resolve(resolveId, name, type, "local.", resolveInterface);
+        return mMDnsManager.resolve(transactionId, name, type, "local.", resolveInterface);
     }
 
     /**
@@ -1786,135 +2534,325 @@
             }
             return IFACE_IDX_ANY;
         }
+        return getNetworkInterfaceIndex(network);
+    }
 
-        final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
-        if (cm == null) {
-            Log.wtf(TAG, "No ConnectivityManager for resolveService");
+    /**
+     * Returns the interface to use to discover a service on a specific network, or {@link
+     * IFACE_IDX_ANY} if no network is specified.
+     */
+    private int getNetworkInterfaceIndex(DiscoveryRequest discoveryRequest) {
+        final Network network = discoveryRequest.getNetwork();
+        if (network == null) {
             return IFACE_IDX_ANY;
         }
-        final LinkProperties lp = cm.getLinkProperties(network);
-        if (lp == null) return IFACE_IDX_ANY;
+        return getNetworkInterfaceIndex(network);
+    }
 
+    /**
+     * Returns the interface of a specific network, or {@link IFACE_IDX_ANY} if no interface is
+     * associated with {@code network}.
+     */
+    private int getNetworkInterfaceIndex(@NonNull Network network) {
+        String interfaceName = getNetworkInterfaceName(network);
+        if (interfaceName == null) {
+            return IFACE_IDX_ANY;
+        }
+        return getNetworkInterfaceIndexByName(interfaceName);
+    }
+
+    private String getNetworkInterfaceName(@Nullable Network network) {
+        if (network == null) {
+            return null;
+        }
+        final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        if (cm == null) {
+            Log.wtf(TAG, "No ConnectivityManager");
+            return null;
+        }
+        final LinkProperties lp = cm.getLinkProperties(network);
+        if (lp == null) {
+            return null;
+        }
         // Only resolve on non-stacked interfaces
+        return lp.getInterfaceName();
+    }
+
+    private int getNetworkInterfaceIndexByName(final String ifaceName) {
         final NetworkInterface iface;
         try {
-            iface = NetworkInterface.getByName(lp.getInterfaceName());
+            iface = NetworkInterface.getByName(ifaceName);
         } catch (SocketException e) {
             Log.e(TAG, "Error querying interface", e);
             return IFACE_IDX_ANY;
         }
 
         if (iface == null) {
-            Log.e(TAG, "Interface not found: " + lp.getInterfaceName());
+            Log.e(TAG, "Interface not found: " + ifaceName);
             return IFACE_IDX_ANY;
         }
 
         return iface.getIndex();
     }
 
-    private boolean stopResolveService(int resolveId) {
-        return mMDnsManager.stopOperation(resolveId);
+    private boolean stopResolveService(int transactionId) {
+        if (mMDnsManager == null) {
+            Log.wtf(TAG, "stopResolveService: mMDnsManager is null");
+            return false;
+        }
+        return mMDnsManager.stopOperation(transactionId);
     }
 
-    private boolean getAddrInfo(int resolveId, String hostname, int interfaceIdx) {
-        return mMDnsManager.getServiceAddress(resolveId, hostname, interfaceIdx);
+    private boolean getAddrInfo(int transactionId, String hostname, int interfaceIdx) {
+        if (mMDnsManager == null) {
+            Log.wtf(TAG, "getAddrInfo: mMDnsManager is null");
+            return false;
+        }
+        return mMDnsManager.getServiceAddress(transactionId, hostname, interfaceIdx);
     }
 
-    private boolean stopGetAddrInfo(int resolveId) {
-        return mMDnsManager.stopOperation(resolveId);
+    private boolean stopGetAddrInfo(int transactionId) {
+        if (mMDnsManager == null) {
+            Log.wtf(TAG, "stopGetAddrInfo: mMDnsManager is null");
+            return false;
+        }
+        return mMDnsManager.stopOperation(transactionId);
     }
 
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
-        if (!PermissionUtils.checkDumpPermission(mContext, TAG, writer)) return;
+        if (!PermissionUtils.hasDumpPermission(mContext, TAG, writer)) return;
 
         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
         // 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:");
         pw.increaseIndent();
         mServiceLogs.reverseDump(pw);
         pw.decreaseIndent();
+
+        //Dump DiscoveryManager
+        pw.println();
+        pw.println("DiscoveryManager:");
+        pw.increaseIndent();
+        HandlerUtils.runWithScissorsForDump(
+                mNsdStateMachine.getHandler(), () -> mMdnsDiscoveryManager.dump(pw), 10_000);
+        pw.decreaseIndent();
     }
 
     private abstract static class ClientRequest {
-        private final int mGlobalId;
+        private final int mTransactionId;
+        private final long mStartTimeMs;
+        private int mFoundServiceCount = 0;
+        private int mLostServiceCount = 0;
+        private final Set<String> mServices = new ArraySet<>();
+        private boolean mIsServiceFromCache = false;
+        private int mSentQueryCount = NO_SENT_QUERY_COUNT;
 
-        private ClientRequest(int globalId) {
-            mGlobalId = globalId;
+        private ClientRequest(int transactionId, long startTimeMs) {
+            mTransactionId = transactionId;
+            mStartTimeMs = startTimeMs;
         }
+
+        public long calculateRequestDurationMs(long stopTimeMs) {
+            return stopTimeMs - mStartTimeMs;
+        }
+
+        public void onServiceFound(String serviceName) {
+            mFoundServiceCount++;
+            if (mServices.size() <= MAX_SERVICES_COUNT_METRIC_PER_CLIENT) {
+                mServices.add(serviceName);
+            }
+        }
+
+        public void onServiceLost() {
+            mLostServiceCount++;
+        }
+
+        public int getFoundServiceCount() {
+            return mFoundServiceCount;
+        }
+
+        public int getLostServiceCount() {
+            return mLostServiceCount;
+        }
+
+        public int getServicesCount() {
+            return mServices.size();
+        }
+
+        public void setServiceFromCache(boolean isServiceFromCache) {
+            mIsServiceFromCache = isServiceFromCache;
+        }
+
+        public boolean isServiceFromCache() {
+            return mIsServiceFromCache;
+        }
+
+        public void onQuerySent() {
+            mSentQueryCount++;
+        }
+
+        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 {
         private final int mRequestCode;
 
-        private LegacyClientRequest(int globalId, int requestCode) {
-            super(globalId);
+        private LegacyClientRequest(int transactionId, int requestCode, long startTimeMs) {
+            super(transactionId, startTimeMs);
             mRequestCode = requestCode;
         }
-    }
 
-    private static class AdvertiserClientRequest extends ClientRequest {
-        private AdvertiserClientRequest(int globalId) {
-            super(globalId);
+        @NonNull
+        @Override
+        protected String getRequestDescriptor() {
+            return "Legacy (" + mRequestCode + ")";
         }
     }
 
-    private static class DiscoveryManagerRequest extends ClientRequest {
+    private abstract static class JavaBackendClientRequest extends ClientRequest {
+        @Nullable
+        private final Network mRequestedNetwork;
+
+        private JavaBackendClientRequest(int transactionId, @Nullable Network requestedNetwork,
+                long startTimeMs) {
+            super(transactionId, startTimeMs);
+            mRequestedNetwork = requestedNetwork;
+        }
+
+        @Nullable
+        public Network getRequestedNetwork() {
+            return mRequestedNetwork;
+        }
+    }
+
+    private static class AdvertiserClientRequest extends JavaBackendClientRequest {
+        @NonNull
+        private final String mServiceFullName;
+
+        private AdvertiserClientRequest(int transactionId, @Nullable Network requestedNetwork,
+                @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());
+        }
+    }
+
+    private static class DiscoveryManagerRequest extends JavaBackendClientRequest {
         @NonNull
         private final MdnsListener mListener;
 
-        private DiscoveryManagerRequest(int globalId, @NonNull MdnsListener listener) {
-            super(globalId);
+        private DiscoveryManagerRequest(int transactionId, @NonNull MdnsListener listener,
+                @Nullable Network requestedNetwork, long startTimeMs) {
+            super(transactionId, requestedNetwork, startTimeMs);
             mListener = listener;
         }
+
+        @NonNull
+        @Override
+        public String getRequestDescriptor() {
+            return String.format("Discovery/%s, net=%s", mListener, getRequestedNetwork());
+        }
     }
 
     /* Information tracked per client */
     private class ClientInfo {
 
-        private static final int MAX_LIMIT = 10;
+        /**
+         * Maximum number of requests (callbacks) for a client.
+         *
+         * 200 listeners should be more than enough for most use-cases: even if a client tries to
+         * file callbacks for every service on a local network, there are generally much less than
+         * 200 devices on a local network (a /24 only allows 255 IPv4 devices), and while some
+         * devices may have multiple services, many devices do not advertise any.
+         */
+        private static final int MAX_LIMIT = 200;
         private final INsdManagerCallback mCb;
         /* Remembers a resolved service until getaddrinfo completes */
         private NsdServiceInfo mResolvedService;
 
-        /* A map from client-side ID (listenerKey) to the request */
+        /* A map from client request ID (listenerKey) to the request */
         private final SparseArray<ClientRequest> mClientRequests = new SparseArray<>();
 
         // The target SDK of this client < Build.VERSION_CODES.S
         private boolean mIsPreSClient = false;
+        private final int mUid;
         // The flag of using java backend if the client's target SDK >= U
         private final boolean mUseJavaBackend;
         // Store client logs
         private final SharedLog mClientLogs;
+        // Report the nsd metrics data
+        private final NetworkNsdReportedMetrics mMetrics;
 
-        private ClientInfo(INsdManagerCallback cb, boolean useJavaBackend, SharedLog sharedLog) {
+        private ClientInfo(INsdManagerCallback cb, int uid, boolean useJavaBackend,
+                SharedLog sharedLog, NetworkNsdReportedMetrics metrics) {
             mCb = cb;
+            mUid = uid;
             mUseJavaBackend = useJavaBackend;
             mClientLogs = sharedLog;
             mClientLogs.log("New client. useJavaBackend=" + useJavaBackend);
+            mMetrics = metrics;
         }
 
         @Override
         public String toString() {
             StringBuilder sb = new StringBuilder();
-            sb.append("mResolvedService ").append(mResolvedService).append("\n");
-            sb.append("mIsLegacy ").append(mIsPreSClient).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 clientID = mClientRequests.keyAt(i);
-                sb.append("clientId ")
-                        .append(clientID)
-                        .append(" mDnsId ").append(mClientRequests.valueAt(i).mGlobalId)
-                        .append(" type ").append(
-                                mClientRequests.valueAt(i).getClass().getSimpleName())
+                int clientRequestId = mClientRequests.keyAt(i);
+                sb.append("  ").append(clientRequestId)
+                        .append(": ").append(mClientRequests.valueAt(i).toString())
                         .append("\n");
             }
             return sb.toString();
         }
 
+        public int getUid() {
+            return mUid;
+        }
+
         private boolean isPreSClient() {
             return mIsPreSClient;
         }
@@ -1923,11 +2861,12 @@
             mIsPreSClient = true;
         }
 
-        private void unregisterMdnsListenerFromRequest(ClientRequest request) {
+        private MdnsListener unregisterMdnsListenerFromRequest(ClientRequest request) {
             final MdnsListener listener =
                     ((DiscoveryManagerRequest) request).mListener;
             mMdnsDiscoveryManager.unregisterListener(
                     listener.getListenedServiceType(), listener);
+            return listener;
         }
 
         // Remove any pending requests from the global map when we get rid of a client,
@@ -1936,22 +2875,50 @@
             mClientLogs.log("Client unregistered. expungeAllRequests!");
             // TODO: to keep handler responsive, do not clean all requests for that client at once.
             for (int i = 0; i < mClientRequests.size(); i++) {
-                final int clientId = mClientRequests.keyAt(i);
+                final int clientRequestId = mClientRequests.keyAt(i);
                 final ClientRequest request = mClientRequests.valueAt(i);
-                final int globalId = request.mGlobalId;
-                mIdToClientInfoMap.remove(globalId);
+                final int transactionId = request.mTransactionId;
+                mTransactionIdToClientInfoMap.remove(transactionId);
                 if (DBG) {
-                    Log.d(TAG, "Terminating client-ID " + clientId
-                            + " global-ID " + globalId + " type " + mClientRequests.get(clientId));
+                    Log.d(TAG, "Terminating clientRequestId " + clientRequestId
+                            + " transactionId " + transactionId
+                            + " type " + mClientRequests.get(clientRequestId));
                 }
 
                 if (request instanceof DiscoveryManagerRequest) {
-                    unregisterMdnsListenerFromRequest(request);
+                    final MdnsListener listener = unregisterMdnsListenerFromRequest(request);
+                    if (listener instanceof DiscoveryListener) {
+                        mMetrics.reportServiceDiscoveryStop(false /* isLegacy */, transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                request.getFoundServiceCount(),
+                                request.getLostServiceCount(),
+                                request.getServicesCount(),
+                                request.getSentQueryCount(),
+                                request.isServiceFromCache());
+                    } else if (listener instanceof ResolutionListener) {
+                        mMetrics.reportServiceResolutionStop(false /* isLegacy */, transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                request.getSentQueryCount());
+                    } else if (listener instanceof ServiceInfoListener) {
+                        mMetrics.reportServiceInfoCallbackUnregistered(transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                request.getFoundServiceCount(),
+                                request.getLostServiceCount(),
+                                request.isServiceFromCache(),
+                                request.getSentQueryCount());
+                    }
                     continue;
                 }
 
                 if (request instanceof AdvertiserClientRequest) {
-                    mAdvertiser.removeService(globalId);
+                    final AdvertiserMetrics metrics =
+                            mAdvertiser.getAdvertiserMetrics(transactionId);
+                    mAdvertiser.removeService(transactionId);
+                    mMetrics.reportServiceUnregistration(false /* isLegacy */, transactionId,
+                            request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                            metrics.mRepliedRequestsCount, metrics.mSentPacketCount,
+                            metrics.mConflictDuringProbingCount,
+                            metrics.mConflictAfterProbingCount);
                     continue;
                 }
 
@@ -1961,27 +2928,62 @@
 
                 switch (((LegacyClientRequest) request).mRequestCode) {
                     case NsdManager.DISCOVER_SERVICES:
-                        stopServiceDiscovery(globalId);
+                        stopServiceDiscovery(transactionId);
+                        mMetrics.reportServiceDiscoveryStop(true /* isLegacy */, transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                request.getFoundServiceCount(),
+                                request.getLostServiceCount(),
+                                request.getServicesCount(),
+                                NO_SENT_QUERY_COUNT,
+                                request.isServiceFromCache());
                         break;
                     case NsdManager.RESOLVE_SERVICE:
-                        stopResolveService(globalId);
+                        stopResolveService(transactionId);
+                        mMetrics.reportServiceResolutionStop(true /* isLegacy */, transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                NO_SENT_QUERY_COUNT);
                         break;
                     case NsdManager.REGISTER_SERVICE:
-                        unregisterService(globalId);
+                        unregisterService(transactionId);
+                        mMetrics.reportServiceUnregistration(true /* isLegacy */, transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                NO_PACKET /* repliedRequestsCount */,
+                                NO_PACKET /* sentPacketCount */,
+                                0 /* conflictDuringProbingCount */,
+                                0 /* conflictAfterProbingCount */);
                         break;
                     default:
                         break;
                 }
             }
             mClientRequests.clear();
+            updateMulticastLock();
         }
 
-        // mClientRequests is a sparse array of listener id -> ClientRequest.  For a given
-        // mDnsClient id, return the corresponding listener id.  mDnsClient id is also called a
-        // global id.
-        private int getClientId(final int globalId) {
+        /**
+         * Returns true if this client has any Java backend request that requests one of the given
+         * networks.
+         */
+        boolean hasAnyJavaBackendRequestForNetworks(@NonNull ArraySet<Network> networks) {
             for (int i = 0; i < mClientRequests.size(); i++) {
-                if (mClientRequests.valueAt(i).mGlobalId == globalId) {
+                final ClientRequest req = mClientRequests.valueAt(i);
+                if (!(req instanceof JavaBackendClientRequest)) {
+                    continue;
+                }
+                final Network reqNetwork = ((JavaBackendClientRequest) mClientRequests.valueAt(i))
+                        .getRequestedNetwork();
+                if (MdnsUtils.isAnyNetworkMatched(reqNetwork, networks)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        // mClientRequests is a sparse array of client request id -> ClientRequest.  For a given
+        // transaction id, return the corresponding client request id.
+        private int getClientRequestId(final int transactionId) {
+            for (int i = 0; i < mClientRequests.size(); i++) {
+                if (mClientRequests.valueAt(i).mTransactionId == transactionId) {
                     return mClientRequests.keyAt(i);
                 }
             }
@@ -1992,15 +2994,29 @@
             mClientLogs.log(message);
         }
 
-        void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
+        private static boolean isLegacyClientRequest(@NonNull ClientRequest request) {
+            return !(request instanceof DiscoveryManagerRequest)
+                    && !(request instanceof AdvertiserClientRequest);
+        }
+
+        void onDiscoverServicesStarted(int listenerKey, DiscoveryRequest discoveryRequest,
+                ClientRequest request) {
+            mMetrics.reportServiceDiscoveryStarted(
+                    isLegacyClientRequest(request), request.mTransactionId);
             try {
-                mCb.onDiscoverServicesStarted(listenerKey, info);
+                mCb.onDiscoverServicesStarted(listenerKey, discoveryRequest);
             } catch (RemoteException e) {
                 Log.e(TAG, "Error calling onDiscoverServicesStarted", e);
             }
         }
+        void onDiscoverServicesFailedImmediately(int listenerKey, int error, boolean isLegacy) {
+            onDiscoverServicesFailed(listenerKey, error, isLegacy, NO_TRANSACTION,
+                    0L /* durationMs */);
+        }
 
-        void onDiscoverServicesFailed(int listenerKey, int error) {
+        void onDiscoverServicesFailed(int listenerKey, int error, boolean isLegacy,
+                int transactionId, long durationMs) {
+            mMetrics.reportServiceDiscoveryFailed(isLegacy, transactionId, durationMs);
             try {
                 mCb.onDiscoverServicesFailed(listenerKey, error);
             } catch (RemoteException e) {
@@ -2008,7 +3024,8 @@
             }
         }
 
-        void onServiceFound(int listenerKey, NsdServiceInfo info) {
+        void onServiceFound(int listenerKey, NsdServiceInfo info, ClientRequest request) {
+            request.onServiceFound(info.getServiceName());
             try {
                 mCb.onServiceFound(listenerKey, info);
             } catch (RemoteException e) {
@@ -2016,7 +3033,8 @@
             }
         }
 
-        void onServiceLost(int listenerKey, NsdServiceInfo info) {
+        void onServiceLost(int listenerKey, NsdServiceInfo info, ClientRequest request) {
+            request.onServiceLost();
             try {
                 mCb.onServiceLost(listenerKey, info);
             } catch (RemoteException e) {
@@ -2032,7 +3050,16 @@
             }
         }
 
-        void onStopDiscoverySucceeded(int listenerKey) {
+        void onStopDiscoverySucceeded(int listenerKey, ClientRequest request) {
+            mMetrics.reportServiceDiscoveryStop(
+                    isLegacyClientRequest(request),
+                    request.mTransactionId,
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                    request.getFoundServiceCount(),
+                    request.getLostServiceCount(),
+                    request.getServicesCount(),
+                    request.getSentQueryCount(),
+                    request.isServiceFromCache());
             try {
                 mCb.onStopDiscoverySucceeded(listenerKey);
             } catch (RemoteException e) {
@@ -2040,7 +3067,14 @@
             }
         }
 
-        void onRegisterServiceFailed(int listenerKey, int error) {
+        void onRegisterServiceFailedImmediately(int listenerKey, int error, boolean isLegacy) {
+            onRegisterServiceFailed(listenerKey, error, isLegacy, NO_TRANSACTION,
+                    0L /* durationMs */);
+        }
+
+        void onRegisterServiceFailed(int listenerKey, int error, boolean isLegacy,
+                int transactionId, long durationMs) {
+            mMetrics.reportServiceRegistrationFailed(isLegacy, transactionId, durationMs);
             try {
                 mCb.onRegisterServiceFailed(listenerKey, error);
             } catch (RemoteException e) {
@@ -2048,7 +3082,11 @@
             }
         }
 
-        void onRegisterServiceSucceeded(int listenerKey, NsdServiceInfo info) {
+        void onRegisterServiceSucceeded(int listenerKey, NsdServiceInfo info,
+                ClientRequest request) {
+            mMetrics.reportServiceRegistrationSucceeded(isLegacyClientRequest(request),
+                    request.mTransactionId,
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()));
             try {
                 mCb.onRegisterServiceSucceeded(listenerKey, info);
             } catch (RemoteException e) {
@@ -2064,7 +3102,13 @@
             }
         }
 
-        void onUnregisterServiceSucceeded(int listenerKey) {
+        void onUnregisterServiceSucceeded(int listenerKey, ClientRequest request,
+                AdvertiserMetrics metrics) {
+            mMetrics.reportServiceUnregistration(isLegacyClientRequest(request),
+                    request.mTransactionId,
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                    metrics.mRepliedRequestsCount, metrics.mSentPacketCount,
+                    metrics.mConflictDuringProbingCount, metrics.mConflictAfterProbingCount);
             try {
                 mCb.onUnregisterServiceSucceeded(listenerKey);
             } catch (RemoteException e) {
@@ -2072,7 +3116,14 @@
             }
         }
 
-        void onResolveServiceFailed(int listenerKey, int error) {
+        void onResolveServiceFailedImmediately(int listenerKey, int error, boolean isLegacy) {
+            onResolveServiceFailed(listenerKey, error, isLegacy, NO_TRANSACTION,
+                    0L /* durationMs */);
+        }
+
+        void onResolveServiceFailed(int listenerKey, int error, boolean isLegacy,
+                int transactionId, long durationMs) {
+            mMetrics.reportServiceResolutionFailed(isLegacy, transactionId, durationMs);
             try {
                 mCb.onResolveServiceFailed(listenerKey, error);
             } catch (RemoteException e) {
@@ -2080,7 +3131,14 @@
             }
         }
 
-        void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info) {
+        void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info,
+                ClientRequest request) {
+            mMetrics.reportServiceResolved(
+                    isLegacyClientRequest(request),
+                    request.mTransactionId,
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                    request.isServiceFromCache(),
+                    request.getSentQueryCount());
             try {
                 mCb.onResolveServiceSucceeded(listenerKey, info);
             } catch (RemoteException e) {
@@ -2096,7 +3154,12 @@
             }
         }
 
-        void onStopResolutionSucceeded(int listenerKey) {
+        void onStopResolutionSucceeded(int listenerKey, ClientRequest request) {
+            mMetrics.reportServiceResolutionStop(
+                    isLegacyClientRequest(request),
+                    request.mTransactionId,
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                    request.getSentQueryCount());
             try {
                 mCb.onStopResolutionSucceeded(listenerKey);
             } catch (RemoteException e) {
@@ -2105,6 +3168,7 @@
         }
 
         void onServiceInfoCallbackRegistrationFailed(int listenerKey, int error) {
+            mMetrics.reportServiceInfoCallbackRegistrationFailed(NO_TRANSACTION);
             try {
                 mCb.onServiceInfoCallbackRegistrationFailed(listenerKey, error);
             } catch (RemoteException e) {
@@ -2112,7 +3176,12 @@
             }
         }
 
-        void onServiceUpdated(int listenerKey, NsdServiceInfo info) {
+        void onServiceInfoCallbackRegistered(int transactionId) {
+            mMetrics.reportServiceInfoCallbackRegistered(transactionId);
+        }
+
+        void onServiceUpdated(int listenerKey, NsdServiceInfo info, ClientRequest request) {
+            request.onServiceFound(info.getServiceName());
             try {
                 mCb.onServiceUpdated(listenerKey, info);
             } catch (RemoteException e) {
@@ -2120,7 +3189,8 @@
             }
         }
 
-        void onServiceUpdatedLost(int listenerKey) {
+        void onServiceUpdatedLost(int listenerKey, ClientRequest request) {
+            request.onServiceLost();
             try {
                 mCb.onServiceUpdatedLost(listenerKey);
             } catch (RemoteException e) {
@@ -2128,7 +3198,14 @@
             }
         }
 
-        void onServiceInfoCallbackUnregistered(int listenerKey) {
+        void onServiceInfoCallbackUnregistered(int listenerKey, ClientRequest request) {
+            mMetrics.reportServiceInfoCallbackUnregistered(
+                    request.mTransactionId,
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                    request.getFoundServiceCount(),
+                    request.getLostServiceCount(),
+                    request.isServiceFromCache(),
+                    request.getSentQueryCount());
             try {
                 mCb.onServiceInfoCallbackUnregistered(listenerKey);
             } catch (RemoteException e) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/AbstractSocketNetlink.java b/service-t/src/com/android/server/connectivity/mdns/AbstractSocketNetlink.java
deleted file mode 100644
index b792e46..0000000
--- a/service-t/src/com/android/server/connectivity/mdns/AbstractSocketNetlink.java
+++ /dev/null
@@ -1,42 +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.server.connectivity.mdns;
-
-/**
- * The interface for netlink monitor.
- */
-public interface AbstractSocketNetlink {
-
-    /**
-     * Returns if the netlink monitor is supported or not. By default, it is not supported.
-     */
-    default boolean isSupported() {
-        return false;
-    }
-
-    /**
-     * Starts the monitor.
-     */
-    default void startMonitoring() {
-    }
-
-    /**
-     * Stops the monitor.
-     */
-    default void stopMonitoring() {
-    }
-}
diff --git a/service-t/src/com/android/server/connectivity/mdns/AbstractSocketNetlinkMonitor.java b/service-t/src/com/android/server/connectivity/mdns/AbstractSocketNetlinkMonitor.java
new file mode 100644
index 0000000..bba3338
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/AbstractSocketNetlinkMonitor.java
@@ -0,0 +1,42 @@
+/*
+ * 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.server.connectivity.mdns;
+
+/**
+ * The interface for netlink monitor.
+ */
+public interface AbstractSocketNetlinkMonitor {
+
+    /**
+     * Returns if the netlink monitor is supported or not. By default, it is not supported.
+     */
+    default boolean isSupported() {
+        return false;
+    }
+
+    /**
+     * Starts the monitor.
+     */
+    default void startMonitoring() {
+    }
+
+    /**
+     * Stops the monitor.
+     */
+    default void stopMonitoring() {
+    }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java b/service-t/src/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
index 551e3db..87aa0d2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
@@ -25,13 +25,12 @@
 import android.net.NetworkRequest;
 import android.os.Build;
 
-import com.android.server.connectivity.mdns.util.MdnsLogger;
+import com.android.net.module.util.SharedLog;
 
 /** Class for monitoring connectivity changes using {@link ConnectivityManager}. */
 public class ConnectivityMonitorWithConnectivityManager implements ConnectivityMonitor {
     private static final String TAG = "ConnMntrWConnMgr";
-    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
-
+    private final SharedLog sharedLog;
     private final Listener listener;
     private final ConnectivityManager.NetworkCallback networkCallback;
     private final ConnectivityManager connectivityManager;
@@ -42,8 +41,10 @@
 
     @SuppressWarnings({"nullness:assignment", "nullness:method.invocation"})
     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    public ConnectivityMonitorWithConnectivityManager(Context context, Listener listener) {
+    public ConnectivityMonitorWithConnectivityManager(Context context, Listener listener,
+            SharedLog sharedLog) {
         this.listener = listener;
+        this.sharedLog = sharedLog;
 
         connectivityManager =
                 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -51,20 +52,20 @@
                 new ConnectivityManager.NetworkCallback() {
                     @Override
                     public void onAvailable(Network network) {
-                        LOGGER.log("network available.");
+                        sharedLog.log("network available.");
                         lastAvailableNetwork = network;
                         notifyConnectivityChange();
                     }
 
                     @Override
                     public void onLost(Network network) {
-                        LOGGER.log("network lost.");
+                        sharedLog.log("network lost.");
                         notifyConnectivityChange();
                     }
 
                     @Override
                     public void onUnavailable() {
-                        LOGGER.log("network unavailable.");
+                        sharedLog.log("network unavailable.");
                         notifyConnectivityChange();
                     }
                 };
@@ -82,7 +83,7 @@
     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
     @Override
     public void startWatchingConnectivityChanges() {
-        LOGGER.log("Start watching connectivity changes");
+        sharedLog.log("Start watching connectivity changes");
         if (isCallbackRegistered) {
             return;
         }
@@ -98,7 +99,7 @@
     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
     @Override
     public void stopWatchingConnectivityChanges() {
-        LOGGER.log("Stop watching connectivity changes");
+        sharedLog.log("Stop watching connectivity changes");
         if (!isCallbackRegistered) {
             return;
         }
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 84faf12..54943c7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -16,23 +16,24 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsServiceTypeClient.INVALID_TRANSACTION_ID;
+
 import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.net.Network;
+import android.os.Build;
 import android.text.TextUtils;
-import android.util.Log;
 import android.util.Pair;
 
-import com.android.server.connectivity.mdns.util.MdnsLogger;
+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.lang.ref.WeakReference;
 import java.net.DatagramPacket;
-import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.Callable;
 
@@ -45,7 +46,6 @@
 public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<String>>> {
 
     private static final String TAG = "MdnsQueryCallable";
-    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
     private static final List<Integer> castShellEmulatorMdnsPorts;
 
     static {
@@ -64,194 +64,209 @@
     @NonNull
     private final WeakReference<MdnsSocketClientBase> weakRequestSender;
     @NonNull
-    private final MdnsPacketWriter packetWriter;
-    @NonNull
     private final String[] serviceTypeLabels;
     @NonNull
     private final List<String> subtypes;
     private final boolean expectUnicastResponse;
     private final int transactionId;
-    @Nullable
-    private final Network network;
+    @NonNull
+    private final SocketKey socketKey;
     private final boolean sendDiscoveryQueries;
     @NonNull
     private final List<MdnsResponse> servicesToResolve;
     @NonNull
-    private final MdnsResponseDecoder.Clock clock;
+    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,
             int transactionId,
-            @Nullable Network network,
+            @NonNull SocketKey socketKey,
+            boolean onlyUseIpv6OnIpv6OnlyNetworks,
             boolean sendDiscoveryQueries,
             @NonNull Collection<MdnsResponse> servicesToResolve,
-            @NonNull MdnsResponseDecoder.Clock clock) {
+            @NonNull MdnsUtils.Clock clock,
+            @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;
         this.transactionId = transactionId;
-        this.network = network;
+        this.socketKey = socketKey;
+        this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
         this.sendDiscoveryQueries = sendDiscoveryQueries;
         this.servicesToResolve = new ArrayList<>(servicesToResolve);
         this.clock = clock;
+        this.sharedLog = sharedLog;
+        this.dependencies = dependencies;
+        this.existingServices = new ArrayList<>(existingServices);
+        this.isQueryWithKnownAnswer = isQueryWithKnownAnswer;
     }
 
+    /**
+     * Call to execute the mdns query.
+     *
+     * @return The pair of transaction id and the subtypes for the query.
+     */
     // Incompatible return type for override of Callable#call().
     @SuppressWarnings("nullness:override.return.invalid")
     @Override
-    @Nullable
     public Pair<Integer, List<String>> call() {
         try {
             MdnsSocketClientBase requestSender = weakRequestSender.get();
             if (requestSender == null) {
-                return null;
+                return Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
             }
 
-            int numQuestions = 0;
+            final List<MdnsRecord> questions = new ArrayList<>();
 
             if (sendDiscoveryQueries) {
-                numQuestions++; // Base service type
-                if (!subtypes.isEmpty()) {
-                    numQuestions += subtypes.size();
-                }
-            }
-
-            // List of (name, type) to query
-            final ArrayList<Pair<String[], Integer>> missingKnownAnswerRecords = new ArrayList<>();
-            final long now = clock.elapsedRealtime();
-            for (MdnsResponse response : servicesToResolve) {
-                final String[] serviceName = response.getServiceName();
-                if (serviceName == null) continue;
-                if (!response.hasTextRecord() || MdnsUtils.isRecordRenewalNeeded(
-                        response.getTextRecord(), now)) {
-                    missingKnownAnswerRecords.add(new Pair<>(serviceName, MdnsRecord.TYPE_TXT));
-                }
-                if (!response.hasServiceRecord() || MdnsUtils.isRecordRenewalNeeded(
-                        response.getServiceRecord(), now)) {
-                    missingKnownAnswerRecords.add(new Pair<>(serviceName, MdnsRecord.TYPE_SRV));
-                    // The hostname is not yet known, so queries for address records will be sent
-                    // the next time the EnqueueMdnsQueryCallable is enqueued if the reply does not
-                    // contain them. In practice, advertisers should include the address records
-                    // when queried for SRV, although it's not a MUST requirement (RFC6763 12.2).
-                    // TODO: Figure out how to renew the A/AAAA record. Usually A/AAAA record will
-                    //  be included in the response to the SRV record so in high chances there is
-                    //  no need to renew them individually.
-                } else if (!response.hasInet4AddressRecord() && !response.hasInet6AddressRecord()) {
-                    final String[] host = response.getServiceRecord().getServiceHost();
-                    missingKnownAnswerRecords.add(new Pair<>(host, MdnsRecord.TYPE_A));
-                    missingKnownAnswerRecords.add(new Pair<>(host, MdnsRecord.TYPE_AAAA));
-                }
-            }
-            numQuestions += missingKnownAnswerRecords.size();
-
-            if (numQuestions == 0) {
-                // No query to send
-                return null;
-            }
-
-            // Header.
-            packetWriter.writeUInt16(transactionId); // transaction ID
-            packetWriter.writeUInt16(MdnsConstants.FLAGS_QUERY); // flags
-            packetWriter.writeUInt16(numQuestions); // number of questions
-            packetWriter.writeUInt16(0); // number of answers (not yet known; will be written later)
-            packetWriter.writeUInt16(0); // number of authority entries
-            packetWriter.writeUInt16(0); // number of additional records
-
-            // Question(s) for missing records on known answers
-            for (Pair<String[], Integer> question : missingKnownAnswerRecords) {
-                writeQuestion(question.first, question.second);
-            }
-
-            // Question(s) for discovering other services with the type. There will be one question
-            // for each (fqdn+subtype, recordType) combination, as well as one for each (fqdn,
-            // recordType) combination.
-            if (sendDiscoveryQueries) {
+                // Base service type
+                questions.add(new MdnsPointerRecord(serviceTypeLabels, expectUnicastResponse));
                 for (String subtype : subtypes) {
-                    String[] labels = new String[serviceTypeLabels.length + 2];
+                    final String[] labels = new String[serviceTypeLabels.length + 2];
                     labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
                     labels[1] = MdnsConstants.SUBTYPE_LABEL;
                     System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
 
-                    writeQuestion(labels, MdnsRecord.TYPE_PTR);
+                    questions.add(new MdnsPointerRecord(labels, expectUnicastResponse));
                 }
-                writeQuestion(serviceTypeLabels, MdnsRecord.TYPE_PTR);
             }
 
-            if (requestSender instanceof MdnsMultinetworkSocketClient) {
-                sendPacketToIpv4AndIpv6(requestSender, MdnsConstants.MDNS_PORT, network);
-                for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
-                    sendPacketToIpv4AndIpv6(requestSender, emulatorPort, network);
+            // List of (name, type) to query
+            final long now = clock.elapsedRealtime();
+            for (MdnsResponse response : servicesToResolve) {
+                final String[] serviceName = response.getServiceName();
+                if (serviceName == null) continue;
+                boolean renewTxt = !response.hasTextRecord() || MdnsUtils.isRecordRenewalNeeded(
+                        response.getTextRecord(), now);
+                boolean renewSrv = !response.hasServiceRecord() || MdnsUtils.isRecordRenewalNeeded(
+                        response.getServiceRecord(), now);
+                if (renewSrv && renewTxt) {
+                    questions.add(new MdnsAnyRecord(serviceName, expectUnicastResponse));
+                } else {
+                    if (renewTxt) {
+                        questions.add(new MdnsTextRecord(serviceName, expectUnicastResponse));
+                    }
+                    if (renewSrv) {
+                        questions.add(new MdnsServiceRecord(serviceName, expectUnicastResponse));
+                        // The hostname is not yet known, so queries for address records will be
+                        // sent the next time the EnqueueMdnsQueryCallable is enqueued if the reply
+                        // does not contain them. In practice, advertisers should include the
+                        // address records when queried for SRV, although it's not a MUST
+                        // requirement (RFC6763 12.2).
+                    } else if (!response.hasInet4AddressRecord()
+                            && !response.hasInet6AddressRecord()) {
+                        final String[] host = response.getServiceRecord().getServiceHost();
+                        questions.add(new MdnsInetAddressRecord(
+                                host, MdnsRecord.TYPE_A, expectUnicastResponse));
+                        questions.add(new MdnsInetAddressRecord(
+                                host, MdnsRecord.TYPE_AAAA, expectUnicastResponse));
+                    }
                 }
-            } else if (requestSender instanceof MdnsSocketClient) {
-                final MdnsSocketClient client = (MdnsSocketClient) requestSender;
-                InetAddress mdnsAddress = MdnsConstants.getMdnsIPv4Address();
-                if (client.isOnIPv6OnlyNetwork()) {
-                    mdnsAddress = MdnsConstants.getMdnsIPv6Address();
-                }
+            }
 
-                sendPacketTo(client, new InetSocketAddress(mdnsAddress, MdnsConstants.MDNS_PORT));
-                for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
-                    sendPacketTo(client, new InetSocketAddress(mdnsAddress, emulatorPort));
+            if (questions.size() == 0) {
+                // No query to send
+                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
+                                        && MdnsUtils.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()));
+                    }
                 }
-            } else {
-                throw new IOException("Unknown socket client type: " + requestSender.getClass());
+            }
+
+            final MdnsPacket queryPacket = new MdnsPacket(
+                    transactionId,
+                    MdnsConstants.FLAGS_QUERY,
+                    questions,
+                    knownAnswers,
+                    Collections.emptyList(), /* authorityRecords */
+                    Collections.emptyList() /* additionalRecords */);
+            sendPacketToIpv4AndIpv6(requestSender, MdnsConstants.MDNS_PORT, queryPacket);
+            for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
+                sendPacketToIpv4AndIpv6(requestSender, emulatorPort, queryPacket);
             }
             return Pair.create(transactionId, subtypes);
-        } catch (IOException e) {
-            LOGGER.e(String.format("Failed to create mDNS packet for subtype: %s.",
+        } catch (Exception e) {
+            sharedLog.e(String.format("Failed to create mDNS packet for subtype: %s.",
                     TextUtils.join(",", subtypes)), e);
-            return null;
+            return Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
         }
     }
 
-    private void writeQuestion(String[] labels, int type) throws IOException {
-        packetWriter.writeLabels(labels);
-        packetWriter.writeUInt16(type);
-        packetWriter.writeUInt16(
-                MdnsConstants.QCLASS_INTERNET
-                        | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
-    }
-
-    private void sendPacketTo(MdnsSocketClient 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) {
-            requestSender.sendUnicastPacket(packet);
+            // MdnsMultinetworkSocketClient is only available on T+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+                    && requestSender instanceof MdnsMultinetworkSocketClient) {
+                ((MdnsMultinetworkSocketClient) requestSender).sendPacketRequestingUnicastResponse(
+                        packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
+            } else {
+                requestSender.sendPacketRequestingUnicastResponse(
+                        packets, onlyUseIpv6OnIpv6OnlyNetworks);
+            }
         } else {
-            requestSender.sendMulticastPacket(packet);
-        }
-    }
-
-    private void sendPacketFromNetwork(MdnsSocketClientBase requestSender,
-            InetSocketAddress address, Network network)
-            throws IOException {
-        DatagramPacket packet = packetWriter.getPacket(address);
-        if (expectUnicastResponse) {
-            requestSender.sendUnicastPacket(packet, network);
-        } else {
-            requestSender.sendMulticastPacket(packet, network);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+                    && requestSender instanceof MdnsMultinetworkSocketClient) {
+                ((MdnsMultinetworkSocketClient) requestSender)
+                        .sendPacketRequestingMulticastResponse(
+                                packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
+            } else {
+                requestSender.sendPacketRequestingMulticastResponse(
+                        packets, onlyUseIpv6OnIpv6OnlyNetworks);
+            }
         }
     }
 
     private void sendPacketToIpv4AndIpv6(MdnsSocketClientBase requestSender, int port,
-            Network network) {
+            MdnsPacket mdnsPacket) {
         try {
-            sendPacketFromNetwork(requestSender,
-                    new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), port), network);
+            sendPacket(requestSender,
+                    new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), port), mdnsPacket);
         } catch (IOException e) {
-            Log.i(TAG, "Can't send packet to IPv4", e);
+            sharedLog.e("Can't send packet to IPv4", e);
         }
         try {
-            sendPacketFromNetwork(requestSender,
-                    new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), port), network);
+            sendPacket(requestSender,
+                    new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), port), mdnsPacket);
         } catch (IOException e) {
-            Log.i(TAG, "Can't send packet to IPv6", e);
+            sharedLog.e("Can't send packet to IPv6", e);
         }
     }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/ExecutorProvider.java b/service-t/src/com/android/server/connectivity/mdns/ExecutorProvider.java
index 72b65e0..5d75b48 100644
--- a/service-t/src/com/android/server/connectivity/mdns/ExecutorProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/ExecutorProvider.java
@@ -16,7 +16,9 @@
 
 package com.android.server.connectivity.mdns;
 
-import android.util.ArraySet;
+import android.annotation.NonNull;
+
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
@@ -29,7 +31,7 @@
 public class ExecutorProvider {
 
     private final Set<ScheduledExecutorService> serviceTypeClientSchedulerExecutors =
-            new ArraySet<>();
+            MdnsUtils.newSet();
 
     /** Returns a new {@link ScheduledExecutorService} instance. */
     public ScheduledExecutorService newServiceTypeClientSchedulerExecutor() {
@@ -42,7 +44,22 @@
     /** Shuts down all the created {@link ScheduledExecutorService} instances. */
     public void shutdownAll() {
         for (ScheduledExecutorService executor : serviceTypeClientSchedulerExecutors) {
+            if (executor.isShutdown()) {
+                continue;
+            }
             executor.shutdownNow();
         }
+        serviceTypeClientSchedulerExecutors.clear();
+    }
+
+    /**
+     * Shutdown one executor service and remove the executor service from the set.
+     * @param executorService the executorService to be shutdown
+     */
+    public void shutdownExecutorService(@NonNull ScheduledExecutorService executorService) {
+        if (!executorService.isShutdown()) {
+            executorService.shutdownNow();
+        }
+        serviceTypeClientSchedulerExecutors.remove(executorService);
     }
 }
\ No newline at end of file
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 cc08ea1..b870477 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -16,25 +16,41 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE;
 import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
+import android.content.Context;
 import android.net.LinkAddress;
 import android.net.Network;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.net.nsd.OffloadEngine;
+import android.net.nsd.OffloadServiceInfo;
+import android.os.Build;
 import android.os.Looper;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
 
+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.SharedLog;
+import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.UUID;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
@@ -44,6 +60,7 @@
  *
  * All methods except the constructor must be called on the looper thread.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsAdvertiser {
     private static final String TAG = MdnsAdvertiser.class.getSimpleName();
     static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
@@ -68,9 +85,12 @@
             new ArrayMap<>();
     private final SparseArray<Registration> mRegistrations = new SparseArray<>();
     private final Dependencies mDeps;
-
     private String[] mDeviceHostName;
     @NonNull private final SharedLog mSharedLog;
+    private final Map<String, List<OffloadServiceInfoWrapper>> mInterfaceOffloadServices =
+            new ArrayMap<>();
+    private final MdnsFeatureFlags mMdnsFeatureFlags;
+    private final Map<String, Integer> mServiceTypeToOffloadPriority;
 
     /**
      * Dependencies for {@link MdnsAdvertiser}, useful for testing.
@@ -85,10 +105,11 @@
                 @NonNull Looper looper, @NonNull byte[] packetCreationBuffer,
                 @NonNull MdnsInterfaceAdvertiser.Callback cb,
                 @NonNull String[] deviceHostName,
-                @NonNull SharedLog sharedLog) {
+                @NonNull SharedLog sharedLog,
+                @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
             // Note NetworkInterface is final and not mockable
             return new MdnsInterfaceAdvertiser(socket, initialAddresses, looper,
-                    packetCreationBuffer, cb, deviceHostName, sharedLog);
+                    packetCreationBuffer, cb, deviceHostName, sharedLog, mdnsFeatureFlags);
         }
 
         /**
@@ -112,21 +133,50 @@
         }
     }
 
+    /**
+     * Gets the current status of the OffloadServiceInfos per interface.
+     * @param interfaceName the target interfaceName
+     * @return the list of current offloaded services.
+     */
+    @NonNull
+    public List<OffloadServiceInfoWrapper> getAllInterfaceOffloadServiceInfos(
+            @NonNull String interfaceName) {
+        return mInterfaceOffloadServices.getOrDefault(interfaceName, Collections.emptyList());
+    }
+
     private final MdnsInterfaceAdvertiser.Callback mInterfaceAdvertiserCb =
             new MdnsInterfaceAdvertiser.Callback() {
         @Override
-        public void onRegisterServiceSucceeded(
+        public void onServiceProbingSucceeded(
                 @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId) {
+            final Registration registration = mRegistrations.get(serviceId);
+            if (registration == null) {
+                mSharedLog.wtf("Register succeeded for unknown registration");
+                return;
+            }
+            if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled
+                    // TODO: Enable offload when the serviceInfo contains a custom host.
+                    && TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
+                final String interfaceName = advertiser.getSocketInterfaceName();
+                final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
+                        mInterfaceOffloadServices.computeIfAbsent(interfaceName,
+                                k -> new ArrayList<>());
+                // Remove existing offload services from cache for update.
+                existingOffloadServiceInfoWrappers.removeIf(item -> item.mServiceId == serviceId);
+
+                byte[] rawOffloadPacket = advertiser.getRawOffloadPayload(serviceId);
+                final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper = createOffloadService(
+                        serviceId, registration, rawOffloadPacket);
+                existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper);
+                mCb.onOffloadStartOrUpdate(interfaceName,
+                        newOffloadServiceInfoWrapper.mOffloadServiceInfo);
+            }
+
             // Wait for all current interfaces to be done probing before notifying of success.
             if (any(mAllAdvertisers, (k, a) -> a.isProbing(serviceId))) return;
             // The service may still be unregistered/renamed if a conflict is found on a later added
             // interface, or if a conflicting announcement/reply is detected (RFC6762 9.)
 
-            final Registration registration = mRegistrations.get(serviceId);
-            if (registration == null) {
-                Log.wtf(TAG, "Register succeeded for unknown registration");
-                return;
-            }
             if (!registration.mNotifiedRegistrationSuccess) {
                 mCb.onRegisterServiceSucceeded(serviceId, registration.getServiceInfo());
                 registration.mNotifiedRegistrationSuccess = true;
@@ -134,8 +184,11 @@
         }
 
         @Override
-        public void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId) {
-            mSharedLog.i("Found conflict, restarted probing for service " + serviceId);
+        public void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId,
+                int conflictType) {
+            mSharedLog.i("Found conflict, restarted probing for service "
+                    + serviceId + " "
+                    + conflictType);
 
             final Registration registration = mRegistrations.get(serviceId);
             if (registration == null) return;
@@ -146,16 +199,37 @@
                 // (with the old, conflicting, actually not used name as argument... The new
                 // implementation will send callbacks with the new name).
                 registration.mNotifiedRegistrationSuccess = false;
+                registration.mConflictAfterProbingCount++;
 
                 // The service was done probing, just reset it to probing state (RFC6762 9.)
-                forAllAdvertisers(a -> a.restartProbingForConflict(serviceId));
+                forAllAdvertisers(a -> {
+                    if (!a.maybeRestartProbingForConflict(serviceId)) {
+                        return;
+                    }
+                    if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) {
+                        maybeSendOffloadStop(a.getSocketInterfaceName(), serviceId);
+                    }
+                });
                 return;
             }
 
-            // Conflict was found during probing; rename once to find a name that has no conflict
-            registration.updateForConflict(
-                    registration.makeNewServiceInfoForConflict(1 /* renameCount */),
-                    1 /* renameCount */);
+            if ((conflictType & CONFLICT_SERVICE) != 0) {
+                // Service conflict was found during probing; rename once to find a name that has no
+                // conflict
+                registration.updateForServiceConflict(
+                        registration.makeNewServiceInfoForServiceConflict(1 /* renameCount */),
+                        1 /* renameCount */);
+            }
+
+            if ((conflictType & CONFLICT_HOST) != 0) {
+                // Host conflict was found during probing; rename once to find a name that has no
+                // conflict
+                registration.updateForHostConflict(
+                        registration.makeNewServiceInfoForHostConflict(1 /* renameCount */),
+                        1 /* renameCount */);
+            }
+
+            registration.mConflictDuringProbingCount++;
 
             // Keep renaming if the new name conflicts in local registrations
             updateRegistrationUntilNoConflict((net, adv) -> adv.hasRegistration(registration),
@@ -167,33 +241,101 @@
         }
 
         @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 */);
         }
     };
 
-    private boolean hasAnyConflict(
+    private boolean hasAnyServiceConflict(
             @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
-            @NonNull NsdServiceInfo newInfo) {
-        return any(mAdvertiserRequests, (network, adv) ->
-                applicableAdvertiserFilter.test(network, adv) && adv.hasConflict(newInfo));
+            @NonNull NsdServiceInfo newInfo,
+            @NonNull Registration originalRegistration) {
+        return any(
+                mAdvertiserRequests,
+                (network, adv) ->
+                        applicableAdvertiserFilter.test(network, adv)
+                                && adv.hasServiceConflict(newInfo, originalRegistration));
+    }
+
+    private boolean hasAnyHostConflict(
+            @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
+            @NonNull NsdServiceInfo newInfo,
+            int clientUid) {
+        // Check if it conflicts with custom hosts.
+        if (any(
+                mAdvertiserRequests,
+                (network, adv) ->
+                        applicableAdvertiserFilter.test(network, adv)
+                                && adv.hasHostConflict(newInfo, clientUid))) {
+            return true;
+        }
+        // Check if it conflicts with the default hostname.
+        return MdnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
     }
 
     private void updateRegistrationUntilNoConflict(
             @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
             @NonNull Registration registration) {
-        int renameCount = 0;
         NsdServiceInfo newInfo = registration.getServiceInfo();
-        while (hasAnyConflict(applicableAdvertiserFilter, newInfo)) {
-            renameCount++;
-            newInfo = registration.makeNewServiceInfoForConflict(renameCount);
+
+        int renameServiceCount = 0;
+        while (hasAnyServiceConflict(applicableAdvertiserFilter, newInfo, registration)) {
+            renameServiceCount++;
+            newInfo = registration.makeNewServiceInfoForServiceConflict(renameServiceCount);
         }
-        registration.updateForConflict(newInfo, renameCount);
+        registration.updateForServiceConflict(newInfo, renameServiceCount);
+
+        if (!TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
+            int renameHostCount = 0;
+            while (hasAnyHostConflict(
+                    applicableAdvertiserFilter, newInfo, registration.mClientUid)) {
+                renameHostCount++;
+                newInfo = registration.makeNewServiceInfoForHostConflict(renameHostCount);
+            }
+            registration.updateForHostConflict(newInfo, renameHostCount);
+        }
+    }
+
+    private void maybeSendOffloadStop(final String interfaceName, int serviceId) {
+        final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
+                mInterfaceOffloadServices.get(interfaceName);
+        if (existingOffloadServiceInfoWrappers == null) {
+            return;
+        }
+        // Stop the offloaded service by matching the service id
+        int idx = CollectionUtils.indexOf(existingOffloadServiceInfoWrappers,
+                item -> item.mServiceId == serviceId);
+        if (idx >= 0) {
+            mCb.onOffloadStop(interfaceName,
+                    existingOffloadServiceInfoWrappers.get(idx).mOffloadServiceInfo);
+            existingOffloadServiceInfoWrappers.remove(idx);
+        }
+    }
+
+    /**
+     * 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);
+            }
+        }
     }
 
     /**
@@ -215,13 +357,37 @@
         }
 
         /**
-         * 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) {
-            mAdvertisers.remove(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
+                // interface.
+                final List<OffloadServiceInfoWrapper> offloadServiceInfoWrappers =
+                        mInterfaceOffloadServices.remove(interfaceName);
+                if (offloadServiceInfoWrappers != null) {
+                    for (OffloadServiceInfoWrapper offloadServiceInfoWrapper :
+                            offloadServiceInfoWrappers) {
+                        mCb.onOffloadStop(interfaceName,
+                                offloadServiceInfoWrapper.mOffloadServiceInfo);
+                    }
+                }
+            }
+
             if (mAdvertisers.size() == 0 && mPendingRegistrations.size() == 0) {
                 // No advertiser is using sockets from this request anymore (in particular for exit
                 // announcements), and there is no registration so newer sockets will not be
@@ -241,20 +407,38 @@
 
         /**
          * Return whether using the proposed new {@link NsdServiceInfo} to add a registration would
-         * cause a conflict in this {@link InterfaceAdvertiserRequest}.
+         * cause a conflict of the service in this {@link InterfaceAdvertiserRequest}.
          */
-        boolean hasConflict(@NonNull NsdServiceInfo newInfo) {
-            return getConflictingService(newInfo) >= 0;
+        boolean hasServiceConflict(
+                @NonNull NsdServiceInfo newInfo, @NonNull Registration originalRegistration) {
+            return getConflictingRegistrationDueToService(newInfo, originalRegistration) >= 0;
         }
 
         /**
-         * Get the ID of a conflicting service, or -1 if none.
+         * Return whether using the proposed new {@link NsdServiceInfo} to add a registration would
+         * cause a conflict of the host in this {@link InterfaceAdvertiserRequest}.
+         *
+         * @param clientUid UID of the user who wants to advertise the serviceInfo.
          */
-        int getConflictingService(@NonNull NsdServiceInfo info) {
+        boolean hasHostConflict(@NonNull NsdServiceInfo newInfo, int clientUid) {
+            return getConflictingRegistrationDueToHost(newInfo, clientUid) >= 0;
+        }
+
+        /** Get the ID of a conflicting registration due to service, or -1 if none. */
+        int getConflictingRegistrationDueToService(
+                @NonNull NsdServiceInfo info, @NonNull Registration originalRegistration) {
+            if (TextUtils.isEmpty(info.getServiceName())) {
+                return -1;
+            }
             for (int i = 0; i < mPendingRegistrations.size(); i++) {
+                // Never conflict with itself
+                if (mPendingRegistrations.valueAt(i) == originalRegistration) {
+                    continue;
+                }
                 final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
-                if (info.getServiceName().equals(other.getServiceName())
-                        && info.getServiceType().equals(other.getServiceType())) {
+                if (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
+                        && MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
+                        other.getServiceType())) {
                     return mPendingRegistrations.keyAt(i);
                 }
             }
@@ -262,38 +446,106 @@
         }
 
         /**
-         * Add a service.
+         * Get the ID of a conflicting registration due to host, or -1 if none.
          *
-         * Conflicts must be checked via {@link #getConflictingService} before attempting to add.
+         * <p>If there's already another registration with the same hostname requested by another
+         * UID, this is a conflict.
+         *
+         * <p>If there're two registrations both containing address records using the same hostname,
+         * this is a conflict.
          */
-        void addService(int id, Registration registration) {
+        int getConflictingRegistrationDueToHost(@NonNull NsdServiceInfo info, int clientUid) {
+            if (TextUtils.isEmpty(info.getHostname())) {
+                return -1;
+            }
+            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(
+                                info.getHostname(), otherInfo.getHostname())) {
+                    return otherServiceId;
+                }
+                if (!info.getHostAddresses().isEmpty()
+                        && !otherInfo.getHostAddresses().isEmpty()
+                        && MdnsUtils.equalsIgnoreDnsCase(
+                                info.getHostname(), otherInfo.getHostname())) {
+                    return otherServiceId;
+                }
+            }
+            return -1;
+        }
+
+        /**
+         * Add a service to advertise.
+         *
+         * <p>Conflicts must be checked via {@link #getConflictingRegistrationDueToService} and
+         * {@link #getConflictingRegistrationDueToHost} before attempting to add.
+         */
+        void addService(int id, @NonNull Registration registration) {
             mPendingRegistrations.put(id, registration);
             for (int i = 0; i < mAdvertisers.size(); i++) {
                 try {
-                    mAdvertisers.valueAt(i).addService(
-                            id, registration.getServiceInfo(), registration.getSubtype());
+                    mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
+                            registration.getAdvertisingOptions());
                 } catch (NameConflictException e) {
-                    Log.wtf(TAG, "Name conflict adding services that should have unique names", e);
+                    mSharedLog.wtf("Name conflict adding services that should have unique names",
+                            e);
                 }
             }
         }
 
+        /**
+         * Update an already registered service.
+         * The caller is expected to check that the service being updated doesn't change its name
+         */
+        void updateService(int id, @NonNull Registration registration) {
+            mPendingRegistrations.put(id, registration);
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                mAdvertisers.valueAt(i).updateService(
+                        id, registration.getServiceInfo().getSubtypes());
+            }
+        }
+
         void removeService(int id) {
             mPendingRegistrations.remove(id);
             for (int i = 0; i < mAdvertisers.size(); i++) {
-                mAdvertisers.valueAt(i).removeService(id);
+                final MdnsInterfaceAdvertiser advertiser = mAdvertisers.valueAt(i);
+                advertiser.removeService(id);
+
+                if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) {
+                    maybeSendOffloadStop(advertiser.getSocketInterfaceName(), id);
+                }
             }
         }
 
+        int getServiceRepliedRequestsCount(int id) {
+            int repliedRequestsCount = NO_PACKET;
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                repliedRequestsCount += mAdvertisers.valueAt(i).getServiceRepliedRequestsCount(id);
+            }
+            return repliedRequestsCount;
+        }
+
+        int getSentPacketCount(int id) {
+            int sentPacketCount = NO_PACKET;
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                sentPacketCount += mAdvertisers.valueAt(i).getSentPacketCount(id);
+            }
+            return sentPacketCount;
+        }
+
         @Override
-        public void onSocketCreated(@NonNull Network network,
+        public void onSocketCreated(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket,
                 @NonNull List<LinkAddress> addresses) {
             MdnsInterfaceAdvertiser advertiser = mAllAdvertisers.get(socket);
             if (advertiser == null) {
                 advertiser = mDeps.makeAdvertiser(socket, addresses, mLooper, mPacketCreationBuffer,
                         mInterfaceAdvertiserCb, mDeviceHostName,
-                        mSharedLog.forSubComponent(socket.getInterface().getName()));
+                        mSharedLog.forSubComponent(socket.getInterface().getName()),
+                        mMdnsFeatureFlags);
                 mAllAdvertisers.put(socket, advertiser);
                 advertiser.start();
             }
@@ -302,42 +554,107 @@
                 final Registration registration = mPendingRegistrations.valueAt(i);
                 try {
                     advertiser.addService(mPendingRegistrations.keyAt(i),
-                            registration.getServiceInfo(), registration.getSubtype());
+                            registration.getServiceInfo(), registration.getAdvertisingOptions());
                 } catch (NameConflictException e) {
-                    Log.wtf(TAG, "Name conflict adding services that should have unique names", e);
+                    mSharedLog.wtf("Name conflict adding services that should have unique names",
+                            e);
                 }
             }
         }
 
         @Override
-        public void onInterfaceDestroyed(@NonNull Network network,
+        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
-        public void onAddressesChanged(@NonNull Network network,
+        public void onAddressesChanged(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) {
             final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket);
-            if (advertiser != null) advertiser.updateAddresses(addresses);
+            if (advertiser == null)  {
+                return;
+            }
+            advertiser.updateAddresses(addresses);
+
+            if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) {
+                // Update address should trigger offload packet update.
+                final String interfaceName = advertiser.getSocketInterfaceName();
+                final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
+                        mInterfaceOffloadServices.get(interfaceName);
+                if (existingOffloadServiceInfoWrappers == null) {
+                    return;
+                }
+                final List<OffloadServiceInfoWrapper> updatedOffloadServiceInfoWrappers =
+                        new ArrayList<>(existingOffloadServiceInfoWrappers.size());
+                for (OffloadServiceInfoWrapper oldWrapper : existingOffloadServiceInfoWrappers) {
+                    OffloadServiceInfoWrapper newWrapper = new OffloadServiceInfoWrapper(
+                            oldWrapper.mServiceId,
+                            oldWrapper.mOffloadServiceInfo.withOffloadPayload(
+                                    advertiser.getRawOffloadPayload(oldWrapper.mServiceId))
+                    );
+                    updatedOffloadServiceInfoWrappers.add(newWrapper);
+                    mCb.onOffloadStartOrUpdate(interfaceName, newWrapper.mOffloadServiceInfo);
+                }
+                mInterfaceOffloadServices.put(interfaceName, updatedOffloadServiceInfoWrappers);
+            }
+        }
+    }
+
+    /**
+     * The wrapper class for OffloadServiceInfo including the serviceId.
+     */
+    public static class OffloadServiceInfoWrapper {
+        public final @NonNull OffloadServiceInfo mOffloadServiceInfo;
+        public final int mServiceId;
+
+        OffloadServiceInfoWrapper(int serviceId, OffloadServiceInfo offloadServiceInfo) {
+            mOffloadServiceInfo = offloadServiceInfo;
+            mServiceId = serviceId;
         }
     }
 
     private static class Registration {
-        @NonNull
-        final String mOriginalName;
+        @Nullable
+        final String mOriginalServiceName;
+        @Nullable
+        final String mOriginalHostname;
         boolean mNotifiedRegistrationSuccess;
-        private int mConflictCount;
+        private int mServiceNameConflictCount;
+        private int mHostnameConflictCount;
         @NonNull
         private NsdServiceInfo mServiceInfo;
-        @Nullable
-        private final String mSubtype;
+        final int mClientUid;
+        private final MdnsAdvertisingOptions mAdvertisingOptions;
+        int mConflictDuringProbingCount;
+        int mConflictAfterProbingCount;
 
-        private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
-            this.mOriginalName = serviceInfo.getServiceName();
+        private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid,
+                @NonNull MdnsAdvertisingOptions advertisingOptions) {
+            this.mOriginalServiceName = serviceInfo.getServiceName();
+            this.mOriginalHostname = serviceInfo.getHostname();
             this.mServiceInfo = serviceInfo;
-            this.mSubtype = subtype;
+            this.mClientUid = clientUid;
+            this.mAdvertisingOptions = advertisingOptions;
+        }
+
+        /** Check if the new {@link NsdServiceInfo} doesn't update any data other than subtypes. */
+        public boolean isSubtypeOnlyUpdate(@NonNull NsdServiceInfo newInfo) {
+            return Objects.equals(newInfo.getServiceName(), mOriginalServiceName)
+                    && Objects.equals(newInfo.getServiceType(), mServiceInfo.getServiceType())
+                    && newInfo.getPort() == mServiceInfo.getPort()
+                    && Objects.equals(newInfo.getHostname(), mOriginalHostname)
+                    && Objects.equals(newInfo.getHostAddresses(), mServiceInfo.getHostAddresses())
+                    && Objects.equals(newInfo.getNetwork(), mServiceInfo.getNetwork());
+        }
+
+        /**
+         * Update subTypes for the registration.
+         */
+        public void updateSubtypes(@NonNull Set<String> subtypes) {
+            mServiceInfo = new NsdServiceInfo(mServiceInfo);
+            mServiceInfo.setSubtypes(subtypes);
         }
 
         /**
@@ -346,8 +663,19 @@
          * @param newInfo New service info to use.
          * @param renameCount How many renames were done before reaching the current name.
          */
-        private void updateForConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
-            mConflictCount += renameCount;
+        private void updateForServiceConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
+            mServiceNameConflictCount += renameCount;
+            mServiceInfo = newInfo;
+        }
+
+        /**
+         * Update the registration to use a different host name, after a conflict was found.
+         *
+         * @param newInfo New service info to use.
+         * @param renameCount How many renames were done before reaching the current name.
+         */
+        private void updateForHostConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
+            mHostnameConflictCount += renameCount;
             mServiceInfo = newInfo;
         }
 
@@ -363,39 +691,57 @@
          * @param renameCount How much to increase the number suffix for this conflict.
          */
         @NonNull
-        public NsdServiceInfo makeNewServiceInfoForConflict(int renameCount) {
+        public NsdServiceInfo makeNewServiceInfoForServiceConflict(int renameCount) {
             // In case of conflict choose a different service name. After the first conflict use
             // "Name (2)", then "Name (3)" etc.
             // TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t
-            final NsdServiceInfo newInfo = new NsdServiceInfo();
+            final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
             newInfo.setServiceName(getUpdatedServiceName(renameCount));
-            newInfo.setServiceType(mServiceInfo.getServiceType());
-            for (Map.Entry<String, byte[]> attr : mServiceInfo.getAttributes().entrySet()) {
-                newInfo.setAttribute(attr.getKey(),
-                        attr.getValue() == null ? null : new String(attr.getValue()));
-            }
-            newInfo.setHost(mServiceInfo.getHost());
-            newInfo.setPort(mServiceInfo.getPort());
-            newInfo.setNetwork(mServiceInfo.getNetwork());
-            // interfaceIndex is not set when registering
+            return newInfo;
+        }
+
+        /**
+         * Make a new hostname for the registration, after a conflict was found.
+         *
+         * <p>If a name conflict was found during probing or because different advertising requests
+         * used the same name, the registration is attempted again with a new name (here using a
+         * number suffix, -1, -2, etc). Registration success is notified once probing succeeds with
+         * a new name.
+         *
+         * @param renameCount How much to increase the number suffix for this conflict.
+         */
+        @NonNull
+        public NsdServiceInfo makeNewServiceInfoForHostConflict(int renameCount) {
+            // In case of conflict choose a different hostname. After the first conflict use
+            // "Name-2", then "Name-3" etc.
+            final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
+            newInfo.setHostname(getUpdatedHostname(renameCount));
             return newInfo;
         }
 
         private String getUpdatedServiceName(int renameCount) {
-            final String suffix = " (" + (mConflictCount + renameCount + 1) + ")";
-            final String truncatedServiceName = MdnsUtils.truncateServiceName(mOriginalName,
+            final String suffix = " (" + (mServiceNameConflictCount + renameCount + 1) + ")";
+            final String truncatedServiceName = MdnsUtils.truncateServiceName(mOriginalServiceName,
                     MAX_LABEL_LENGTH - suffix.length());
             return truncatedServiceName + suffix;
         }
 
+        private String getUpdatedHostname(int renameCount) {
+            final String suffix = "-" + (mHostnameConflictCount + renameCount + 1);
+            final String truncatedHostname =
+                    MdnsUtils.truncateServiceName(
+                            mOriginalHostname, MAX_LABEL_LENGTH - suffix.length());
+            return truncatedHostname + suffix;
+        }
+
         @NonNull
         public NsdServiceInfo getServiceInfo() {
             return mServiceInfo;
         }
 
-        @Nullable
-        public String getSubtype() {
-            return mSubtype;
+        @NonNull
+        public MdnsAdvertisingOptions getAdvertisingOptions() {
+            return mAdvertisingOptions;
         }
     }
 
@@ -424,23 +770,88 @@
 
         // Unregistration is notified immediately as success in NsdService so no callback is needed
         // here.
+
+        /**
+         * Called when a service is ready to be sent for hardware offloading.
+         *
+         * @param interfaceName the interface for sending the update to.
+         * @param offloadServiceInfo the offloading content.
+         */
+        void onOffloadStartOrUpdate(@NonNull String interfaceName,
+                @NonNull OffloadServiceInfo offloadServiceInfo);
+
+        /**
+         * Called when a service is removed or the MdnsInterfaceAdvertiser is destroyed.
+         *
+         * @param interfaceName the interface for sending the update to.
+         * @param offloadServiceInfo the offloading content.
+         */
+        void onOffloadStop(@NonNull String interfaceName,
+                @NonNull OffloadServiceInfo offloadServiceInfo);
+    }
+
+    /**
+     * Data class of avdverting metrics.
+     */
+    public static class AdvertiserMetrics {
+        public final int mRepliedRequestsCount;
+        public final int mSentPacketCount;
+        public final int mConflictDuringProbingCount;
+        public final int mConflictAfterProbingCount;
+
+        public AdvertiserMetrics(int repliedRequestsCount, int sentPacketCount,
+                int conflictDuringProbingCount, int conflictAfterProbingCount) {
+            mRepliedRequestsCount = repliedRequestsCount;
+            mSentPacketCount = sentPacketCount;
+            mConflictDuringProbingCount = conflictDuringProbingCount;
+            mConflictAfterProbingCount = conflictAfterProbingCount;
+        }
     }
 
     public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
-            @NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog) {
-        this(looper, socketProvider, cb, new Dependencies(), sharedLog);
+            @NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog,
+            @NonNull MdnsFeatureFlags mDnsFeatureFlags, @NonNull Context context) {
+        this(looper, socketProvider, cb, new Dependencies(), sharedLog, mDnsFeatureFlags,
+                context);
     }
 
     @VisibleForTesting
     MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
             @NonNull AdvertiserCallback cb, @NonNull Dependencies deps,
-            @NonNull SharedLog sharedLog) {
+            @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags,
+            @NonNull Context context) {
         mLooper = looper;
         mCb = cb;
         mSocketProvider = socketProvider;
         mDeps = deps;
         mDeviceHostName = deps.generateHostname();
         mSharedLog = sharedLog;
+        mMdnsFeatureFlags = mDnsFeatureFlags;
+        final ConnectivityResources res = new ConnectivityResources(context);
+        mServiceTypeToOffloadPriority = parseOffloadPriorityList(
+                res.get().getStringArray(R.array.config_nsdOffloadServicesPriority), sharedLog);
+    }
+
+    private static Map<String, Integer> parseOffloadPriorityList(
+            @NonNull String[] resValues, SharedLog sharedLog) {
+        final Map<String, Integer> priorities = new ArrayMap<>(resValues.length);
+        for (String entry : resValues) {
+            final String[] priorityAndType = entry.split(":", 2);
+            if (priorityAndType.length != 2) {
+                sharedLog.wtf("Invalid config_nsdOffloadServicesPriority ignored: " + entry);
+                continue;
+            }
+
+            final int priority;
+            try {
+                priority = Integer.parseInt(priorityAndType[0]);
+            } catch (NumberFormatException e) {
+                sharedLog.wtf("Invalid priority in config_nsdOffloadServicesPriority: " + entry);
+                continue;
+            }
+            priorities.put(MdnsUtils.toDnsLowerCase(priorityAndType[1]), priority);
+        }
+        return priorities;
     }
 
     private void checkThread() {
@@ -450,42 +861,69 @@
     }
 
     /**
-     * Add a service to advertise.
+     * Add or update a service to advertise.
+     *
      * @param id A unique ID for the service.
      * @param service The service info to advertise.
-     * @param subtype An optional subtype to advertise the service with.
+     * @param advertisingOptions The advertising options.
+     * @param clientUid The UID who wants to advertise the service.
      */
-    public void addService(int id, NsdServiceInfo service, @Nullable String subtype) {
+    public void addOrUpdateService(int id, NsdServiceInfo service,
+            MdnsAdvertisingOptions advertisingOptions, int clientUid) {
         checkThread();
-        if (mRegistrations.get(id) != null) {
-            Log.e(TAG, "Adding duplicate registration for " + service);
-            // TODO (b/264986328): add a more specific error code
-            mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
-            return;
-        }
-
-        mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype);
-
+        final Registration existingRegistration = mRegistrations.get(id);
         final Network network = service.getNetwork();
-        final Registration registration = new Registration(service, subtype);
-        final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
-        if (network == null) {
-            // If registering on all networks, no advertiser must have conflicts
-            checkConflictFilter = (net, adv) -> true;
-        } else {
-            // If registering on one network, the matching network advertiser and the one for all
-            // networks must not have conflicts
-            checkConflictFilter = (net, adv) -> net == null || network.equals(net);
-        }
+        final Set<String> subtypes = service.getSubtypes();
+        Registration registration;
+        if (advertisingOptions.isOnlyUpdate()) {
+            if (existingRegistration == null) {
+                mSharedLog.e("Update non existing registration for " + service);
+                mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+                return;
+            }
+            if (!(existingRegistration.isSubtypeOnlyUpdate(service))) {
+                mSharedLog.e("Update request can only update subType, serviceInfo: " + service
+                        + ", existing serviceInfo: " + existingRegistration.getServiceInfo());
+                mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+                return;
 
-        updateRegistrationUntilNoConflict(checkConflictFilter, registration);
+            }
+            mSharedLog.i("Update service " + service + " with ID " + id + " and subtypes "
+                    + subtypes + " advertisingOptions " + advertisingOptions);
+            registration = existingRegistration;
+            registration.updateSubtypes(subtypes);
+        } else {
+            if (existingRegistration != null) {
+                mSharedLog.e("Adding duplicate registration for " + service);
+                // TODO (b/264986328): add a more specific error code
+                mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+                return;
+            }
+            mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
+                    + subtypes + " advertisingOptions " + advertisingOptions);
+            registration = new Registration(service, clientUid, advertisingOptions);
+            final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
+            if (network == null) {
+                // If registering on all networks, no advertiser must have conflicts
+                checkConflictFilter = (net, adv) -> true;
+            } else {
+                // If registering on one network, the matching network advertiser and the one
+                // for all networks must not have conflicts
+                checkConflictFilter = (net, adv) -> net == null || network.equals(net);
+            }
+            updateRegistrationUntilNoConflict(checkConflictFilter, registration);
+        }
 
         InterfaceAdvertiserRequest advertiser = mAdvertiserRequests.get(network);
         if (advertiser == null) {
             advertiser = new InterfaceAdvertiserRequest(network);
             mAdvertiserRequests.put(network, advertiser);
         }
-        advertiser.addService(id, registration);
+        if (advertisingOptions.isOnlyUpdate()) {
+            advertiser.updateService(id, registration);
+        } else {
+            advertiser.addService(id, registration);
+        }
         mRegistrations.put(id, registration);
     }
 
@@ -508,6 +946,34 @@
         }
     }
 
+    /**
+     * Get advertising metrics.
+     *
+     * @param id ID used when registering.
+     * @return The advertising metrics includes replied requests count, send packet count, conflict
+     *         count during/after probing.
+     */
+    public AdvertiserMetrics getAdvertiserMetrics(int id) {
+        checkThread();
+        final Registration registration = mRegistrations.get(id);
+        if (registration == null) {
+            return new AdvertiserMetrics(
+                    NO_PACKET /* repliedRequestsCount */,
+                    NO_PACKET /* sentPacketCount */,
+                    0 /* conflictDuringProbingCount */,
+                    0 /* conflictAfterProbingCount */);
+        }
+        int repliedRequestsCount = NO_PACKET;
+        int sentPacketCount = NO_PACKET;
+        for (int i = 0; i < mAdvertiserRequests.size(); i++) {
+            repliedRequestsCount +=
+                    mAdvertiserRequests.valueAt(i).getServiceRepliedRequestsCount(id);
+            sentPacketCount += mAdvertiserRequests.valueAt(i).getSentPacketCount(id);
+        }
+        return new AdvertiserMetrics(repliedRequestsCount, sentPacketCount,
+                registration.mConflictDuringProbingCount, registration.mConflictAfterProbingCount);
+    }
+
     private static <K, V> boolean any(@NonNull ArrayMap<K, V> map,
             @NonNull BiPredicate<K, V> predicate) {
         for (int i = 0; i < map.size(); i++) {
@@ -524,4 +990,23 @@
             return false;
         });
     }
+
+    private OffloadServiceInfoWrapper createOffloadService(int serviceId,
+            @NonNull Registration registration, byte[] rawOffloadPacket) {
+        final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
+        final Integer mapPriority = mServiceTypeToOffloadPriority.get(
+                MdnsUtils.toDnsLowerCase(nsdServiceInfo.getServiceType()));
+        // Higher values of priority are less prioritized
+        final int priority = mapPriority == null ? Integer.MAX_VALUE : mapPriority;
+        final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
+                new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
+                        nsdServiceInfo.getServiceType()),
+                new ArrayList<>(nsdServiceInfo.getSubtypes()),
+                String.join(".", mDeviceHostName),
+                rawOffloadPacket,
+                priority,
+                // TODO: set the offloadType based on the callback timing.
+                OffloadEngine.OFFLOAD_TYPE_REPLY);
+        return new OffloadServiceInfoWrapper(serviceId, offloadServiceInfo);
+    }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
new file mode 100644
index 0000000..a81d1e4
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -0,0 +1,136 @@
+/*
+ * 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.server.connectivity.mdns;
+
+import android.annotation.Nullable;
+
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * API configuration parameters for advertising the mDNS service.
+ *
+ * <p>Use {@link MdnsAdvertisingOptions.Builder} to create {@link MdnsAdvertisingOptions}.
+ *
+ * @hide
+ */
+public class MdnsAdvertisingOptions {
+
+    private static MdnsAdvertisingOptions sDefaultOptions;
+    private final boolean mIsOnlyUpdate;
+    @Nullable
+    private final Duration mTtl;
+
+    /**
+     * Parcelable constructs for a {@link MdnsAdvertisingOptions}.
+     */
+    MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) {
+        this.mIsOnlyUpdate = isOnlyUpdate;
+        this.mTtl = ttl;
+    }
+
+    /**
+     * Returns a {@link Builder} for {@link MdnsAdvertisingOptions}.
+     */
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns a default search options.
+     */
+    public static synchronized MdnsAdvertisingOptions getDefaultOptions() {
+        if (sDefaultOptions == null) {
+            sDefaultOptions = newBuilder().build();
+        }
+        return sDefaultOptions;
+    }
+
+    /**
+     * @return {@code true} if the advertising request is an update request.
+     */
+    public boolean isOnlyUpdate() {
+        return mIsOnlyUpdate;
+    }
+
+    /**
+     * Returns the TTL for all records in a service.
+     */
+    @Nullable
+    public Duration getTtl() {
+        return mTtl;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (!(other instanceof MdnsAdvertisingOptions)) {
+            return false;
+        } else {
+            final MdnsAdvertisingOptions otherOptions = (MdnsAdvertisingOptions) other;
+            return mIsOnlyUpdate == otherOptions.mIsOnlyUpdate
+                    && Objects.equals(mTtl, otherOptions.mTtl);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIsOnlyUpdate, mTtl);
+    }
+
+    @Override
+    public String toString() {
+        return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + ", mTtl=" + mTtl
+                + '}';
+    }
+
+    /**
+     * A builder to create {@link MdnsAdvertisingOptions}.
+     */
+    public static final class Builder {
+        private boolean mIsOnlyUpdate = false;
+        @Nullable
+        private Duration mTtl;
+
+        private Builder() {
+        }
+
+        /**
+         * Sets if the advertising request is an update request.
+         */
+        public Builder setIsOnlyUpdate(boolean isOnlyUpdate) {
+            this.mIsOnlyUpdate = isOnlyUpdate;
+            return this;
+        }
+
+        /**
+         * Sets the TTL duration for all records of the service.
+         */
+        public Builder setTtl(@Nullable Duration ttl) {
+            this.mTtl = ttl;
+            return this;
+        }
+
+        /**
+         * Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
+         */
+        public MdnsAdvertisingOptions build() {
+            return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl);
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
index 27fc945..5812797 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
@@ -18,9 +18,12 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
+import android.os.Build;
 import android.os.Looper;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
 
 import java.util.Collections;
 import java.util.List;
@@ -30,6 +33,7 @@
  *
  * This allows maintaining other hosts' caches up-to-date. See RFC6762 8.3.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsAnnouncer extends MdnsPacketRepeater<MdnsAnnouncer.BaseAnnouncementInfo> {
     private static final long ANNOUNCEMENT_INITIAL_DELAY_MS = 1000L;
     @VisibleForTesting
@@ -39,9 +43,6 @@
     private static final long EXIT_DELAY_MS = 2000L;
     private static final int EXIT_COUNT = 3;
 
-    @NonNull
-    private final String mLogTag;
-
     /** Base class for announcement requests to send with {@link MdnsAnnouncer}. */
     public abstract static class BaseAnnouncementInfo implements MdnsPacketRepeater.Request {
         private final int mServiceId;
@@ -105,16 +106,11 @@
         }
     }
 
-    public MdnsAnnouncer(@NonNull String interfaceTag, @NonNull Looper looper,
+    public MdnsAnnouncer(@NonNull Looper looper,
             @NonNull MdnsReplySender replySender,
-            @Nullable PacketRepeaterCallback<BaseAnnouncementInfo> cb) {
-        super(looper, replySender, cb);
-        mLogTag = MdnsAnnouncer.class.getSimpleName() + "/" + interfaceTag;
-    }
-
-    @Override
-    protected String getTag() {
-        return mLogTag;
+            @Nullable PacketRepeaterCallback<BaseAnnouncementInfo> cb,
+            @NonNull SharedLog sharedLog) {
+        super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
     }
 
     // TODO: Notify MdnsRecordRepository that the records were announced for that service ID,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsConfigs.java b/service-t/src/com/android/server/connectivity/mdns/MdnsConfigs.java
index 761c477..d4aeacf 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsConfigs.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsConfigs.java
@@ -50,14 +50,6 @@
         return false;
     }
 
-    public static boolean useSessionIdToScheduleMdnsTask() {
-        return false;
-    }
-
-    public static boolean shouldCancelScanTaskWhenFutureIsNull() {
-        return false;
-    }
-
     public static long sleepTimeForSocketThreadMs() {
         return 20_000L;
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
index f0e1717..b83a6a0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -18,14 +18,12 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.android.internal.annotations.VisibleForTesting;
-
 import java.net.InetAddress;
+import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
 import java.nio.charset.Charset;
 
 /** mDNS-related constants. */
-@VisibleForTesting
 public final class MdnsConstants {
     public static final int MDNS_PORT = 5353;
     // Flags word format is:
@@ -40,10 +38,15 @@
     public static final int FLAG_TRUNCATED = 0x0200;
     public static final int QCLASS_INTERNET = 0x0001;
     public static final int QCLASS_UNICAST = 0x8000;
+    public static final int NO_PACKET = 0;
     public static final String SUBTYPE_LABEL = "_sub";
     public static final String SUBTYPE_PREFIX = "_";
     private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
     private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
+    public static final InetSocketAddress IPV6_SOCKET_ADDR = new InetSocketAddress(
+            getMdnsIPv6Address(), MDNS_PORT);
+    public static final InetSocketAddress IPV4_SOCKET_ADDR = new InetSocketAddress(
+            getMdnsIPv4Address(), MDNS_PORT);
     private static InetAddress mdnsAddress;
     private MdnsConstants() {
     }
@@ -77,4 +80,4 @@
     public static Charset getUtf8Charset() {
         return UTF_8;
     }
-}
\ No newline at end of file
+}
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 c3deeca..0ab7a76 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,26 +16,29 @@
 
 package com.android.server.connectivity.mdns;
 
-import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-
 import android.Manifest.permission;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
-import android.net.Network;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.Looper;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.GuardedBy;
+
 import com.android.internal.annotations.VisibleForTesting;
 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.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
@@ -49,52 +52,69 @@
     private final MdnsSocketClientBase socketClient;
     @NonNull private final SharedLog sharedLog;
 
-    @NonNull private final PerNetworkServiceTypeClients perNetworkServiceTypeClients;
-    @NonNull private final Handler handler;
-    @Nullable private final HandlerThread handlerThread;
+    @NonNull private final PerSocketServiceTypeClients perSocketServiceTypeClients;
+    @NonNull private final DiscoveryExecutor discoveryExecutor;
+    @NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
 
-    private static class PerNetworkServiceTypeClients {
-        private final ArrayMap<Pair<String, Network>, MdnsServiceTypeClient> clients =
+    // Only accessed on the handler thread, initialized before first use
+    @Nullable
+    private MdnsServiceCache serviceCache;
+
+    private static class PerSocketServiceTypeClients {
+        private final ArrayMap<Pair<String, SocketKey>, MdnsServiceTypeClient> clients =
                 new ArrayMap<>();
 
-        public void put(@NonNull String serviceType, @Nullable Network network,
+        public void put(@NonNull String serviceType, @NonNull SocketKey socketKey,
                 @NonNull MdnsServiceTypeClient client) {
-            final Pair<String, Network> perNetworkServiceType = new Pair<>(serviceType, network);
-            clients.put(perNetworkServiceType, client);
+            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
+            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+                    socketKey);
+            clients.put(perSocketServiceType, client);
         }
 
         @Nullable
-        public MdnsServiceTypeClient get(@NonNull String serviceType, @Nullable Network network) {
-            final Pair<String, Network> perNetworkServiceType = new Pair<>(serviceType, network);
-            return clients.getOrDefault(perNetworkServiceType, null);
+        public MdnsServiceTypeClient get(
+                @NonNull String serviceType, @NonNull SocketKey socketKey) {
+            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
+            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+                    socketKey);
+            return clients.getOrDefault(perSocketServiceType, null);
         }
 
         public List<MdnsServiceTypeClient> getByServiceType(@NonNull String serviceType) {
+            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
             final List<MdnsServiceTypeClient> list = new ArrayList<>();
             for (int i = 0; i < clients.size(); i++) {
-                final Pair<String, Network> perNetworkServiceType = clients.keyAt(i);
-                if (serviceType.equals(perNetworkServiceType.first)) {
+                final Pair<String, SocketKey> perSocketServiceType = clients.keyAt(i);
+                if (dnsLowerServiceType.equals(perSocketServiceType.first)) {
                     list.add(clients.valueAt(i));
                 }
             }
             return list;
         }
 
-        public List<MdnsServiceTypeClient> getByNetwork(@Nullable Network network) {
+        public List<MdnsServiceTypeClient> getBySocketKey(@NonNull SocketKey socketKey) {
             final List<MdnsServiceTypeClient> list = new ArrayList<>();
             for (int i = 0; i < clients.size(); i++) {
-                final Pair<String, Network> perNetworkServiceType = clients.keyAt(i);
-                final Network serviceTypeNetwork = perNetworkServiceType.second;
-                if (Objects.equals(network, serviceTypeNetwork)) {
+                final Pair<String, SocketKey> perSocketServiceType = clients.keyAt(i);
+                if (socketKey.equals(perSocketServiceType.second)) {
                     list.add(clients.valueAt(i));
                 }
             }
             return list;
         }
 
+        public List<MdnsServiceTypeClient> getAllMdnsServiceTypeClient() {
+            return new ArrayList<>(clients.values());
+        }
+
         public void remove(@NonNull MdnsServiceTypeClient client) {
-            final int index = clients.indexOfValue(client);
-            clients.removeAt(index);
+            for (int i = 0; i < clients.size(); ++i) {
+                if (Objects.equals(client, clients.valueAt(i))) {
+                    clients.removeAt(i);
+                    break;
+                }
+            }
         }
 
         public boolean isEmpty() {
@@ -103,36 +123,89 @@
     }
 
     public MdnsDiscoveryManager(@NonNull ExecutorProvider executorProvider,
-            @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog) {
+            @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog,
+            @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
         this.executorProvider = executorProvider;
         this.socketClient = socketClient;
         this.sharedLog = sharedLog;
-        this.perNetworkServiceTypeClients = new PerNetworkServiceTypeClients();
-        if (socketClient.getLooper() != null) {
-            this.handlerThread = null;
-            this.handler = new Handler(socketClient.getLooper());
-        } else {
-            this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName());
-            this.handlerThread.start();
-            this.handler = new Handler(handlerThread.getLooper());
-        }
+        this.perSocketServiceTypeClients = new PerSocketServiceTypeClients();
+        this.mdnsFeatureFlags = mdnsFeatureFlags;
+        this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper());
     }
 
-    private void checkAndRunOnHandlerThread(@NonNull Runnable function) {
-        if (this.handlerThread == null) {
-            function.run();
-        } else {
+    private static class DiscoveryExecutor implements Executor {
+        private final HandlerThread handlerThread;
+
+        @GuardedBy("pendingTasks")
+        @Nullable private Handler handler;
+        @GuardedBy("pendingTasks")
+        @NonNull private final ArrayList<Runnable> pendingTasks = new ArrayList<>();
+
+        DiscoveryExecutor(@Nullable Looper defaultLooper) {
+            if (defaultLooper != null) {
+                this.handlerThread = null;
+                synchronized (pendingTasks) {
+                    this.handler = new Handler(defaultLooper);
+                }
+            } else {
+                this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName()) {
+                    @Override
+                    protected void onLooperPrepared() {
+                        synchronized (pendingTasks) {
+                            handler = new Handler(getLooper());
+                            for (Runnable pendingTask : pendingTasks) {
+                                handler.post(pendingTask);
+                            }
+                            pendingTasks.clear();
+                        }
+                    }
+                };
+                this.handlerThread.start();
+            }
+        }
+
+        public void checkAndRunOnHandlerThread(@NonNull Runnable function) {
+            if (this.handlerThread == null) {
+                // Callers are expected to already be running on the handler when a defaultLooper
+                // was provided
+                function.run();
+            } else {
+                execute(function);
+            }
+        }
+
+        @Override
+        public void execute(Runnable function) {
+            final Handler handler;
+            synchronized (pendingTasks) {
+                if (this.handler == null) {
+                    pendingTasks.add(function);
+                    return;
+                } else {
+                    handler = this.handler;
+                }
+            }
             handler.post(function);
         }
+
+        void shutDown() {
+            if (this.handlerThread != null) {
+                this.handlerThread.quitSafely();
+            }
+        }
+
+        void ensureRunningOnHandlerThread() {
+            synchronized (pendingTasks) {
+                MdnsUtils.ensureRunningOnHandlerThread(handler);
+            }
+        }
     }
 
     /**
      * Do the cleanup of the MdnsDiscoveryManager
      */
     public void shutDown() {
-        if (this.handlerThread != null) {
-            this.handlerThread.quitSafely();
-        }
+        discoveryExecutor.shutDown();
     }
 
     /**
@@ -150,7 +223,7 @@
             @NonNull MdnsServiceBrowserListener listener,
             @NonNull MdnsSearchOptions searchOptions) {
         sharedLog.i("Registering listener for serviceType: " + serviceType);
-        checkAndRunOnHandlerThread(() ->
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
                 handleRegisterListener(serviceType, listener, searchOptions));
     }
 
@@ -158,7 +231,7 @@
             @NonNull String serviceType,
             @NonNull MdnsServiceBrowserListener listener,
             @NonNull MdnsSearchOptions searchOptions) {
-        if (perNetworkServiceTypeClients.isEmpty()) {
+        if (perSocketServiceTypeClients.isEmpty()) {
             // First listener. Starts the socket client.
             try {
                 socketClient.startDiscovery();
@@ -168,32 +241,52 @@
             }
         }
         // 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(@Nullable Network network) {
-                        ensureRunningOnHandlerThread(handler);
+                    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 =
-                                perNetworkServiceTypeClients.get(serviceType, network);
+                                perSocketServiceTypeClients.get(serviceType, socketKey);
                         if (serviceTypeClient == null) {
-                            serviceTypeClient = createServiceTypeClient(serviceType, network);
-                            perNetworkServiceTypeClients.put(serviceType, network,
+                            serviceTypeClient = createServiceTypeClient(serviceType, socketKey);
+                            perSocketServiceTypeClients.put(serviceType, socketKey,
                                     serviceTypeClient);
                         }
                         serviceTypeClient.startSendAndReceive(listener, searchOptions);
                     }
 
                     @Override
-                    public void onAllSocketsDestroyed(@Nullable Network network) {
-                        ensureRunningOnHandlerThread(handler);
+                    public void onSocketDestroyed(@NonNull SocketKey socketKey) {
+                        discoveryExecutor.ensureRunningOnHandlerThread();
                         final MdnsServiceTypeClient serviceTypeClient =
-                                perNetworkServiceTypeClients.get(serviceType, network);
+                                perSocketServiceTypeClients.get(serviceType, socketKey);
                         if (serviceTypeClient == null) return;
                         // Notify all listeners that all services are removed from this socket.
                         serviceTypeClient.notifySocketDestroyed();
-                        perNetworkServiceTypeClients.remove(serviceTypeClient);
+                        executorProvider.shutdownExecutorService(serviceTypeClient.getExecutor());
+                        perSocketServiceTypeClients.remove(serviceTypeClient);
                     }
                 });
     }
@@ -209,7 +302,8 @@
     public void unregisterListener(
             @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
         sharedLog.i("Unregistering listener for serviceType:" + serviceType);
-        checkAndRunOnHandlerThread(() -> handleUnregisterListener(serviceType, listener));
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
+                handleUnregisterListener(serviceType, listener));
     }
 
     private void handleUnregisterListener(
@@ -218,7 +312,7 @@
         socketClient.notifyNetworkUnrequested(listener);
 
         final List<MdnsServiceTypeClient> serviceTypeClients =
-                perNetworkServiceTypeClients.getByServiceType(serviceType);
+                perSocketServiceTypeClients.getByServiceType(serviceType);
         if (serviceTypeClients.isEmpty()) {
             return;
         }
@@ -227,52 +321,80 @@
             if (serviceTypeClient.stopSendAndReceive(listener)) {
                 // No listener is registered for the service type anymore, remove it from the list
                 // of the service type clients.
-                perNetworkServiceTypeClients.remove(serviceTypeClient);
+                executorProvider.shutdownExecutorService(serviceTypeClient.getExecutor());
+                perSocketServiceTypeClients.remove(serviceTypeClient);
             }
         }
-        if (perNetworkServiceTypeClients.isEmpty()) {
+        if (perSocketServiceTypeClients.isEmpty()) {
             // No discovery request. Stops the socket client.
+            sharedLog.i("All service type listeners unregistered; stopping discovery");
             socketClient.stopDiscovery();
         }
     }
 
     @Override
-    public void onResponseReceived(@NonNull MdnsPacket packet,
-            int interfaceIndex, @Nullable Network network) {
-        checkAndRunOnHandlerThread(() ->
-                handleOnResponseReceived(packet, interfaceIndex, network));
+    public void onResponseReceived(@NonNull MdnsPacket packet, @NonNull SocketKey socketKey) {
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
+                handleOnResponseReceived(packet, socketKey));
     }
 
-    private void handleOnResponseReceived(@NonNull MdnsPacket packet, int interfaceIndex,
-            @Nullable Network network) {
-        for (MdnsServiceTypeClient serviceTypeClient
-                : perNetworkServiceTypeClients.getByNetwork(network)) {
-            serviceTypeClient.processResponse(packet, interfaceIndex, network);
+    private void handleOnResponseReceived(@NonNull MdnsPacket packet,
+            @NonNull SocketKey socketKey) {
+        for (MdnsServiceTypeClient serviceTypeClient : getMdnsServiceTypeClient(socketKey)) {
+            serviceTypeClient.processResponse(packet, socketKey);
+        }
+    }
+
+    private List<MdnsServiceTypeClient> getMdnsServiceTypeClient(@NonNull SocketKey socketKey) {
+        if (socketClient.supportsRequestingSpecificNetworks()) {
+            return perSocketServiceTypeClients.getBySocketKey(socketKey);
+        } else {
+            return perSocketServiceTypeClients.getAllMdnsServiceTypeClient();
         }
     }
 
     @Override
     public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
-            @Nullable Network network) {
-        checkAndRunOnHandlerThread(() ->
-                handleOnFailedToParseMdnsResponse(receivedPacketNumber, errorCode, network));
+            @NonNull SocketKey socketKey) {
+        discoveryExecutor.checkAndRunOnHandlerThread(() ->
+                handleOnFailedToParseMdnsResponse(receivedPacketNumber, errorCode, socketKey));
     }
 
     private void handleOnFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
-            @Nullable Network network) {
-        for (MdnsServiceTypeClient serviceTypeClient
-                : perNetworkServiceTypeClients.getByNetwork(network)) {
+            @NonNull SocketKey socketKey) {
+        for (MdnsServiceTypeClient serviceTypeClient : getMdnsServiceTypeClient(socketKey)) {
             serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
         }
     }
 
     @VisibleForTesting
     MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
-            @Nullable Network network) {
-        sharedLog.log("createServiceTypeClient for type:" + serviceType + ", net:" + network);
+            @NonNull SocketKey socketKey) {
+        discoveryExecutor.ensureRunningOnHandlerThread();
+        sharedLog.log("createServiceTypeClient for type:" + serviceType + " " + socketKey);
+        final String tag = serviceType + "-" + socketKey.getNetwork()
+                + "/" + socketKey.getInterfaceIndex();
+        final Looper looper = Looper.myLooper();
+        if (serviceCache == null) {
+            serviceCache = new MdnsServiceCache(looper, mdnsFeatureFlags);
+        }
         return new MdnsServiceTypeClient(
                 serviceType, socketClient,
-                executorProvider.newServiceTypeClientSchedulerExecutor(), network,
-                sharedLog.forSubComponent(serviceType + "-" + network));
+                executorProvider.newServiceTypeClientSchedulerExecutor(), socketKey,
+                sharedLog.forSubComponent(tag), looper, serviceCache, mdnsFeatureFlags);
+    }
+
+    /**
+     * Dump DiscoveryManager state.
+     */
+    public void dump(PrintWriter pw) {
+        discoveryExecutor.checkAndRunOnHandlerThread(() -> {
+            pw.println();
+            // Dump ServiceTypeClients
+            for (MdnsServiceTypeClient serviceTypeClient
+                    : perSocketServiceTypeClients.getAllMdnsServiceTypeClient()) {
+                serviceTypeClient.dump(pw);
+            }
+        });
     }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
new file mode 100644
index 0000000..c264f25
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -0,0 +1,308 @@
+/*
+ * 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.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * The class that contains mDNS feature flags;
+ */
+public class MdnsFeatureFlags {
+    /**
+     * A feature flag to control whether the mDNS offload is enabled or not.
+     */
+    public static final String NSD_FORCE_DISABLE_MDNS_OFFLOAD = "nsd_force_disable_mdns_offload";
+
+    /**
+     * A feature flag to control whether the probing question should include
+     * InetAddressRecords or not.
+     */
+    public static final String INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING =
+            "include_inet_address_records_in_probing";
+    /**
+     * A feature flag to control whether expired services removal should be enabled.
+     */
+    public static final String NSD_EXPIRED_SERVICES_REMOVAL =
+            "nsd_expired_services_removal";
+
+    /**
+     * A feature flag to control whether the label count limit should be enabled.
+     */
+    public static final String NSD_LIMIT_LABEL_COUNT = "nsd_limit_label_count";
+
+    /**
+     * A feature flag to control whether the known-answer suppression should be enabled.
+     */
+    public static final String NSD_KNOWN_ANSWER_SUPPRESSION = "nsd_known_answer_suppression";
+
+    /**
+     * A feature flag to control whether unicast replies should be enabled.
+     *
+     * <p>Enabling this feature causes replies to queries with the Query Unicast (QU) flag set to be
+     * sent unicast instead of multicast, as per RFC6762 5.4.
+     */
+    public static final String NSD_UNICAST_REPLY_ENABLED = "nsd_unicast_reply_enabled";
+
+    /**
+     * A feature flag to control whether the aggressive query mode should be enabled.
+     */
+    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";
+
+    // Flag for offload feature
+    public final boolean mIsMdnsOffloadFeatureEnabled;
+
+    // Flag for including InetAddressRecords in probing questions.
+    public final boolean mIncludeInetAddressRecordsInProbing;
+
+    // Flag for expired services removal
+    public final boolean mIsExpiredServicesRemovalEnabled;
+
+    // Flag for label count limit
+    public final boolean mIsLabelCountLimitEnabled;
+
+    // Flag for known-answer suppression
+    public final boolean mIsKnownAnswerSuppressionEnabled;
+
+    // Flag to enable replying unicast to queries requesting unicast replies
+    public final boolean mIsUnicastReplyEnabled;
+
+    // Flag for aggressive query mode
+    public final boolean mIsAggressiveQueryModeEnabled;
+
+    // Flag for query with known-answer
+    public final boolean mIsQueryWithKnownAnswerEnabled;
+
+    @Nullable
+    private final FlagOverrideProvider mOverrideProvider;
+
+    /**
+     * A provider that can indicate whether a flag should be force-enabled for testing purposes.
+     */
+    public interface FlagOverrideProvider {
+        /**
+         * Indicates whether the flag should be force-enabled for testing purposes.
+         */
+        boolean isForceEnabledForTest(@NonNull String flag);
+    }
+
+    /**
+     * Indicates whether the flag should be force-enabled for testing purposes.
+     */
+    private boolean isForceEnabledForTest(@NonNull String flag) {
+        return mOverrideProvider != null && mOverrideProvider.isForceEnabledForTest(flag);
+    }
+
+    /**
+     * Indicates whether {@link #NSD_UNICAST_REPLY_ENABLED} is enabled, including for testing.
+     */
+    public boolean isUnicastReplyEnabled() {
+        return mIsUnicastReplyEnabled || isForceEnabledForTest(NSD_UNICAST_REPLY_ENABLED);
+    }
+
+    /**
+     * Indicates whether {@link #NSD_AGGRESSIVE_QUERY_MODE} is enabled, including for testing.
+     */
+    public boolean isAggressiveQueryModeEnabled() {
+        return mIsAggressiveQueryModeEnabled || isForceEnabledForTest(NSD_AGGRESSIVE_QUERY_MODE);
+    }
+
+    /**
+     * Indicates whether {@link #NSD_KNOWN_ANSWER_SUPPRESSION} is enabled, including for testing.
+     */
+    public boolean isKnownAnswerSuppressionEnabled() {
+        return mIsKnownAnswerSuppressionEnabled
+                || isForceEnabledForTest(NSD_KNOWN_ANSWER_SUPPRESSION);
+    }
+
+    /**
+     * Indicates whether {@link #NSD_QUERY_WITH_KNOWN_ANSWER} is enabled, including for testing.
+     */
+    public boolean isQueryWithKnownAnswerEnabled() {
+        return mIsQueryWithKnownAnswerEnabled
+                || isForceEnabledForTest(NSD_QUERY_WITH_KNOWN_ANSWER);
+    }
+
+    /**
+     * The constructor for {@link MdnsFeatureFlags}.
+     */
+    public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
+            boolean includeInetAddressRecordsInProbing,
+            boolean isExpiredServicesRemovalEnabled,
+            boolean isLabelCountLimitEnabled,
+            boolean isKnownAnswerSuppressionEnabled,
+            boolean isUnicastReplyEnabled,
+            boolean isAggressiveQueryModeEnabled,
+            boolean isQueryWithKnownAnswerEnabled,
+            @Nullable FlagOverrideProvider overrideProvider) {
+        mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
+        mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
+        mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
+        mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
+        mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
+        mIsUnicastReplyEnabled = isUnicastReplyEnabled;
+        mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
+        mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+        mOverrideProvider = overrideProvider;
+    }
+
+
+    /** Returns a {@link Builder} for {@link MdnsFeatureFlags}. */
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /** A builder to create {@link MdnsFeatureFlags}. */
+    public static final class Builder {
+
+        private boolean mIsMdnsOffloadFeatureEnabled;
+        private boolean mIncludeInetAddressRecordsInProbing;
+        private boolean mIsExpiredServicesRemovalEnabled;
+        private boolean mIsLabelCountLimitEnabled;
+        private boolean mIsKnownAnswerSuppressionEnabled;
+        private boolean mIsUnicastReplyEnabled;
+        private boolean mIsAggressiveQueryModeEnabled;
+        private boolean mIsQueryWithKnownAnswerEnabled;
+        private FlagOverrideProvider mOverrideProvider;
+
+        /**
+         * The constructor for {@link Builder}.
+         */
+        public Builder() {
+            mIsMdnsOffloadFeatureEnabled = false;
+            mIncludeInetAddressRecordsInProbing = false;
+            mIsExpiredServicesRemovalEnabled = true; // Default enabled.
+            mIsLabelCountLimitEnabled = true; // Default enabled.
+            mIsKnownAnswerSuppressionEnabled = true; // Default enabled.
+            mIsUnicastReplyEnabled = true; // Default enabled.
+            mIsAggressiveQueryModeEnabled = false;
+            mIsQueryWithKnownAnswerEnabled = false;
+            mOverrideProvider = null;
+        }
+
+        /**
+         * Set whether the mDNS offload feature is enabled.
+         *
+         * @see #NSD_FORCE_DISABLE_MDNS_OFFLOAD
+         */
+        public Builder setIsMdnsOffloadFeatureEnabled(boolean isMdnsOffloadFeatureEnabled) {
+            mIsMdnsOffloadFeatureEnabled = isMdnsOffloadFeatureEnabled;
+            return this;
+        }
+
+        /**
+         * Set whether the probing question should include InetAddressRecords.
+         *
+         * @see #INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING
+         */
+        public Builder setIncludeInetAddressRecordsInProbing(
+                boolean includeInetAddressRecordsInProbing) {
+            mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
+            return this;
+        }
+
+        /**
+         * Set whether the expired services removal is enabled.
+         *
+         * @see #NSD_EXPIRED_SERVICES_REMOVAL
+         */
+        public Builder setIsExpiredServicesRemovalEnabled(boolean isExpiredServicesRemovalEnabled) {
+            mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
+            return this;
+        }
+
+        /**
+         * Set whether the label count limit is enabled.
+         *
+         * @see #NSD_LIMIT_LABEL_COUNT
+         */
+        public Builder setIsLabelCountLimitEnabled(boolean isLabelCountLimitEnabled) {
+            mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
+            return this;
+        }
+
+        /**
+         * Set whether the known-answer suppression is enabled.
+         *
+         * @see #NSD_KNOWN_ANSWER_SUPPRESSION
+         */
+        public Builder setIsKnownAnswerSuppressionEnabled(boolean isKnownAnswerSuppressionEnabled) {
+            mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
+            return this;
+        }
+
+        /**
+         * Set whether the unicast reply feature is enabled.
+         *
+         * @see #NSD_UNICAST_REPLY_ENABLED
+         */
+        public Builder setIsUnicastReplyEnabled(boolean isUnicastReplyEnabled) {
+            mIsUnicastReplyEnabled = isUnicastReplyEnabled;
+            return this;
+        }
+
+        /**
+         * Set a {@link FlagOverrideProvider} to be used by {@link #isForceEnabledForTest(String)}.
+         *
+         * If non-null, features that use {@link #isForceEnabledForTest(String)} will use that
+         * provider to query whether the flag should be force-enabled.
+         */
+        public Builder setOverrideProvider(@Nullable FlagOverrideProvider overrideProvider) {
+            mOverrideProvider = overrideProvider;
+            return this;
+        }
+
+        /**
+         * Set whether the aggressive query mode is enabled.
+         *
+         * @see #NSD_AGGRESSIVE_QUERY_MODE
+         */
+        public Builder setIsAggressiveQueryModeEnabled(boolean isAggressiveQueryModeEnabled) {
+            mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
+            return this;
+        }
+
+        /**
+         * Set whether the query with known-answer is enabled.
+         *
+         * @see #NSD_QUERY_WITH_KNOWN_ANSWER
+         */
+        public Builder setIsQueryWithKnownAnswerEnabled(boolean isQueryWithKnownAnswerEnabled) {
+            mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+            return this;
+        }
+
+        /**
+         * Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
+         */
+        public MdnsFeatureFlags build() {
+            return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled,
+                    mIncludeInetAddressRecordsInProbing,
+                    mIsExpiredServicesRemovalEnabled,
+                    mIsLabelCountLimitEnabled,
+                    mIsKnownAnswerSuppressionEnabled,
+                    mIsUnicastReplyEnabled,
+                    mIsAggressiveQueryModeEnabled,
+                    mIsQueryWithKnownAnswerEnabled,
+                    mOverrideProvider);
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
index dd8a526..4399f2d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
@@ -18,7 +18,7 @@
 
 import android.annotation.Nullable;
 
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
 
 import java.io.IOException;
 import java.net.Inet4Address;
@@ -29,7 +29,7 @@
 import java.util.Objects;
 
 /** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
 public class MdnsInetAddressRecord extends MdnsRecord {
     @Nullable private Inet6Address inet6Address;
     @Nullable private Inet4Address inet4Address;
@@ -60,6 +60,12 @@
         super(name, type, reader, isQuestion);
     }
 
+    public MdnsInetAddressRecord(String[] name, int type, boolean isUnicast) {
+        super(name, type,
+                MdnsConstants.QCLASS_INTERNET | (isUnicast ? MdnsConstants.QCLASS_UNICAST : 0),
+                0L /* receiptTimeMillis */, false /* cacheFlush */, 0L /* ttlMillis */);
+    }
+
     public MdnsInetAddressRecord(String[] name, long receiptTimeMillis, boolean cacheFlush,
                     long ttlMillis, InetAddress address) {
         super(name, address instanceof Inet4Address ? TYPE_A : TYPE_AAAA,
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 724a704..0b2003f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -16,34 +16,43 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.net.LinkAddress;
 import android.net.nsd.NsdServiceInfo;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
-import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.HexDump;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.MdnsAnnouncer.BaseAnnouncementInfo;
 import com.android.server.connectivity.mdns.MdnsPacketRepeater.PacketRepeaterCallback;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHandler {
+    public static final int CONFLICT_SERVICE = 1 << 0;
+    public static final int CONFLICT_HOST = 1 << 1;
+
     private static final boolean DBG = MdnsAdvertiser.DBG;
     @VisibleForTesting
     public static final long EXIT_ANNOUNCEMENT_DELAY_MS = 100L;
     @NonNull
-    private final String mTag;
-    @NonNull
     private final ProbingCallback mProbingCallback = new ProbingCallback();
     @NonNull
     private final AnnouncingCallback mAnnouncingCallback = new AnnouncingCallback();
@@ -62,9 +71,12 @@
     private final MdnsProber mProber;
     @NonNull
     private final MdnsReplySender mReplySender;
-
     @NonNull
     private final SharedLog mSharedLog;
+    @NonNull
+    private final byte[] mPacketCreationBuffer;
+    @NonNull
+    private final MdnsFeatureFlags mMdnsFeatureFlags;
 
     /**
      * Callbacks called by {@link MdnsInterfaceAdvertiser} to report status updates.
@@ -73,35 +85,46 @@
         /**
          * Called by the advertiser after it successfully registered a service, after probing.
          */
-        void onRegisterServiceSucceeded(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
+        void onServiceProbingSucceeded(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
 
         /**
          * Called by the advertiser when a conflict was found, during or after probing.
          *
-         * If a conflict is found during probing, the {@link #renameServiceForConflict} must be
+         * <p>If a conflict is found during probing, the {@link #renameServiceForConflict} must be
          * called to restart probing and attempt registration with a different name.
+         *
+         * <p>{@code conflictType} is a bitmap telling which part of the service is conflicting. See
+         * {@link MdnsInterfaceAdvertiser#CONFLICT_SERVICE} and {@link
+         * MdnsInterfaceAdvertiser#CONFLICT_HOST}.
          */
-        void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
+        void onServiceConflict(
+                @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);
     }
 
     /**
      * Callbacks from {@link MdnsProber}.
      */
-    private class ProbingCallback implements
-            PacketRepeaterCallback<MdnsProber.ProbingInfo> {
+    private class ProbingCallback implements PacketRepeaterCallback<MdnsProber.ProbingInfo> {
+        @Override
+        public void onSent(int index, @NonNull MdnsProber.ProbingInfo info, int sentPacketCount) {
+            mRecordRepository.onProbingSent(info.getServiceId(), sentPacketCount);
+        }
         @Override
         public void onFinished(MdnsProber.ProbingInfo info) {
             final MdnsAnnouncer.AnnouncementInfo announcementInfo;
             mSharedLog.i("Probing finished for service " + info.getServiceId());
-            mCbHandler.post(() -> mCb.onRegisterServiceSucceeded(
+            mCbHandler.post(() -> mCb.onServiceProbingSucceeded(
                     MdnsInterfaceAdvertiser.this, info.getServiceId()));
             try {
                 announcementInfo = mRecordRepository.onProbingSucceeded(info);
@@ -112,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);
+            }
         }
     }
 
@@ -120,18 +152,19 @@
      */
     private class AnnouncingCallback implements PacketRepeaterCallback<BaseAnnouncementInfo> {
         @Override
-        public void onSent(int index, @NonNull BaseAnnouncementInfo info) {
-            mRecordRepository.onAdvertisementSent(info.getServiceId());
+        public void onSent(int index, @NonNull BaseAnnouncementInfo info, int sentPacketCount) {
+            mRecordRepository.onAdvertisementSent(info.getServiceId(), sentPacketCount);
         }
 
         @Override
         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);
+                    }
+                });
             }
         }
     }
@@ -144,77 +177,102 @@
         /** @see MdnsRecordRepository */
         @NonNull
         public MdnsRecordRepository makeRecordRepository(@NonNull Looper looper,
-                @NonNull String[] deviceHostName) {
-            return new MdnsRecordRepository(looper, deviceHostName);
+                @NonNull String[] deviceHostName, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+            return new MdnsRecordRepository(looper, deviceHostName, mdnsFeatureFlags);
         }
 
         /** @see MdnsReplySender */
         @NonNull
         public MdnsReplySender makeReplySender(@NonNull String interfaceTag, @NonNull Looper looper,
-                @NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer) {
-            return new MdnsReplySender(interfaceTag, looper, socket, packetCreationBuffer);
+                @NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer,
+                @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+            return new MdnsReplySender(looper, socket, packetCreationBuffer,
+                    sharedLog.forSubComponent(
+                            MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG,
+                    mdnsFeatureFlags);
         }
 
         /** @see MdnsAnnouncer */
         public MdnsAnnouncer makeMdnsAnnouncer(@NonNull String interfaceTag, @NonNull Looper looper,
                 @NonNull MdnsReplySender replySender,
-                @Nullable PacketRepeaterCallback<MdnsAnnouncer.BaseAnnouncementInfo> cb) {
-            return new MdnsAnnouncer(interfaceTag, looper, replySender, cb);
+                @Nullable PacketRepeaterCallback<MdnsAnnouncer.BaseAnnouncementInfo> cb,
+                @NonNull SharedLog sharedLog) {
+            return new MdnsAnnouncer(looper, replySender, cb,
+                    sharedLog.forSubComponent(
+                            MdnsAnnouncer.class.getSimpleName() + "/" + interfaceTag));
         }
 
         /** @see MdnsProber */
         public MdnsProber makeMdnsProber(@NonNull String interfaceTag, @NonNull Looper looper,
                 @NonNull MdnsReplySender replySender,
-                @NonNull PacketRepeaterCallback<MdnsProber.ProbingInfo> cb) {
-            return new MdnsProber(interfaceTag, looper, replySender, cb);
+                @NonNull PacketRepeaterCallback<MdnsProber.ProbingInfo> cb,
+                @NonNull SharedLog sharedLog) {
+            return new MdnsProber(looper, replySender, cb, sharedLog.forSubComponent(
+                    MdnsProber.class.getSimpleName() + "/" + interfaceTag));
         }
     }
 
     public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket,
             @NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper,
             @NonNull byte[] packetCreationBuffer, @NonNull Callback cb,
-            @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog) {
+            @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog,
+            @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
         this(socket, initialAddresses, looper, packetCreationBuffer, cb,
-                new Dependencies(), deviceHostName, sharedLog);
+                new Dependencies(), deviceHostName, sharedLog, mdnsFeatureFlags);
     }
 
     public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket,
             @NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper,
             @NonNull byte[] packetCreationBuffer, @NonNull Callback cb, @NonNull Dependencies deps,
-            @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog) {
-        mTag = MdnsInterfaceAdvertiser.class.getSimpleName() + "/" + sharedLog.getTag();
-        mRecordRepository = deps.makeRecordRepository(looper, deviceHostName);
+            @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog,
+            @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+        mRecordRepository = deps.makeRecordRepository(looper, deviceHostName, mdnsFeatureFlags);
         mRecordRepository.updateAddresses(initialAddresses);
         mSocket = socket;
         mCb = cb;
         mCbHandler = new Handler(looper);
         mReplySender = deps.makeReplySender(sharedLog.getTag(), looper, socket,
-                packetCreationBuffer);
+                packetCreationBuffer, sharedLog, mdnsFeatureFlags);
+        mPacketCreationBuffer = packetCreationBuffer;
         mAnnouncer = deps.makeMdnsAnnouncer(sharedLog.getTag(), looper, mReplySender,
-                mAnnouncingCallback);
-        mProber = deps.makeMdnsProber(sharedLog.getTag(), looper, mReplySender, mProbingCallback);
+                mAnnouncingCallback, sharedLog);
+        mProber = deps.makeMdnsProber(sharedLog.getTag(), looper, mReplySender, mProbingCallback,
+                sharedLog);
         mSharedLog = sharedLog;
+        mMdnsFeatureFlags = mdnsFeatureFlags;
     }
 
     /**
      * 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);
     }
 
     /**
+     * Update an already registered service without sending exit/re-announcement packet.
+     *
+     * @param id An exiting service id
+     * @param subtypes New subtypes
+     */
+    public void updateService(int id, @NonNull Set<String> subtypes) {
+        // The current implementation is intended to be used in cases where subtypes don't get
+        // announced.
+        mRecordRepository.updateService(id, subtypes);
+    }
+
+    /**
      * Start advertising a service.
      *
      * @throws NameConflictException There is already a service being advertised with that name.
      */
-    public void addService(int id, NsdServiceInfo service, @Nullable String subtype)
-            throws NameConflictException {
-        final int replacedExitingService = mRecordRepository.addService(id, service, subtype);
+    public void addService(int id, NsdServiceInfo service,
+            @NonNull MdnsAdvertisingOptions advertisingOptions) throws NameConflictException {
+        final int replacedExitingService =
+                mRecordRepository.addService(id, service, advertisingOptions.getTtl());
         // Cancel announcements for the existing service. This only happens for exiting services
         // (so cancelling exiting announcements), as per RecordRepository.addService.
         if (replacedExitingService >= 0) {
@@ -234,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
@@ -247,10 +306,39 @@
         } 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);
+        }
+    }
+
+    /**
+     * Get the replied request count from given service id.
+     */
+    public int getServiceRepliedRequestsCount(int id) {
+        if (!mRecordRepository.hasActiveService(id)) return NO_PACKET;
+        return mRecordRepository.getServiceRepliedRequestsCount(id);
+    }
+
+    /**
+     * Get the total sent packet count from given service id.
+     */
+    public int getSentPacketCount(int id) {
+        if (!mRecordRepository.hasActiveService(id)) return NO_PACKET;
+        return mRecordRepository.getSentPacketCount(id);
     }
 
     /**
@@ -267,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()) {
@@ -276,17 +365,18 @@
         }
         mReplySender.cancelAll();
         mSocket.removePacketHandler(this);
-        mCbHandler.post(() -> mCb.onDestroyed(mSocket));
     }
 
     /**
      * Reset a service to the probing state due to a conflict found on the network.
      */
-    public void restartProbingForConflict(int serviceId) {
+    public boolean maybeRestartProbingForConflict(int serviceId) {
         final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(serviceId);
-        if (probingInfo == null) return;
+        if (probingInfo == null) return false;
 
+        mAnnouncer.stop(serviceId);
         mProber.restartForConflict(probingInfo);
+        return true;
     }
 
     /**
@@ -315,35 +405,81 @@
     public void handlePacket(byte[] recvbuf, int length, InetSocketAddress src) {
         final MdnsPacket packet;
         try {
-            packet = MdnsPacket.parse(new MdnsPacketReader(recvbuf, length));
+            packet = MdnsPacket.parse(new MdnsPacketReader(recvbuf, length, mMdnsFeatureFlags));
         } catch (MdnsPacket.ParseException e) {
-            Log.e(mTag, "Error parsing mDNS packet", e);
+            mSharedLog.e("Error parsing mDNS packet", e);
             if (DBG) {
-                Log.v(
-                        mTag, "Packet: " + HexDump.toHexString(recvbuf, 0, length));
+                mSharedLog.v("Packet: " + HexDump.toHexString(recvbuf, 0, length));
             }
             return;
         }
+        // recvbuf and src are reused after this returns; ensure references to src are not kept.
+        final InetSocketAddress srcCopy = new InetSocketAddress(src.getAddress(), src.getPort());
 
         if (DBG) {
-            Log.v(mTag,
-                    "Parsed packet with " + packet.questions.size() + " questions, "
-                            + packet.answers.size() + " answers, "
-                            + packet.authorityRecords.size() + " authority, "
-                            + packet.additionalRecords.size() + " additional from " + src);
+            mSharedLog.v("Parsed packet with " + packet.questions.size() + " questions, "
+                    + packet.answers.size() + " answers, "
+                    + packet.authorityRecords.size() + " authority, "
+                    + packet.additionalRecords.size() + " additional from " + srcCopy);
         }
 
-        for (int conflictServiceId : mRecordRepository.getConflictingServices(packet)) {
-            mCbHandler.post(() -> mCb.onServiceConflict(this, conflictServiceId));
+        Map<Integer, Integer> conflictingServices =
+                mRecordRepository.getConflictingServices(packet);
+
+        for (Map.Entry<Integer, Integer> entry : conflictingServices.entrySet()) {
+            int serviceId = entry.getKey();
+            int conflictType = entry.getValue();
+            mCbHandler.post(
+                    () -> {
+                        mCb.onServiceConflict(this, serviceId, conflictType);
+                    });
         }
 
         // Even in case of conflict, add replies for other services. But in general conflicts would
         // happen when the incoming packet has answer records (not a question), so there will be no
         // answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the
         // conflicting service is still probing and won't reply either.
-        final MdnsRecordRepository.ReplyInfo answers = mRecordRepository.getReply(packet, src);
+        final MdnsReplyInfo answers = mRecordRepository.getReply(packet, srcCopy);
 
         if (answers == null) return;
         mReplySender.queueReply(answers);
     }
+
+    /**
+     * Get the socket interface name.
+     */
+    public String getSocketInterfaceName() {
+        return mSocket.getInterface().getName();
+    }
+
+    /**
+     * Gets the offload MdnsPacket.
+     * @param serviceId The serviceId.
+     * @return the raw offload payload
+     */
+    @NonNull
+    public byte[] getRawOffloadPayload(int serviceId) {
+        try {
+            return MdnsUtils.createRawDnsPacket(mPacketCreationBuffer,
+                    mRecordRepository.getOffloadPacket(serviceId));
+        } catch (IOException | IllegalArgumentException e) {
+            mSharedLog.wtf("Cannot create rawOffloadPacket: ", e);
+            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/MdnsInterfaceSocket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java
index 119c7a8..63dd703 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java
@@ -20,15 +20,18 @@
 import static com.android.server.connectivity.mdns.MdnsSocket.MULTICAST_IPV6_ADDRESS;
 
 import android.annotation.NonNull;
+import android.annotation.RequiresApi;
 import android.net.LinkAddress;
 import android.net.util.SocketUtils;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
-import android.util.Log;
+
+import com.android.net.module.util.SharedLog;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -48,17 +51,19 @@
  * @see MulticastSocket for javadoc of each public method.
  * @see MulticastSocket for javadoc of each public method.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsInterfaceSocket {
     private static final String TAG = MdnsInterfaceSocket.class.getSimpleName();
     @NonNull private final MulticastSocket mMulticastSocket;
     @NonNull private final NetworkInterface mNetworkInterface;
     @NonNull private final MulticastPacketReader mPacketReader;
     @NonNull private final ParcelFileDescriptor mFileDescriptor;
+    @NonNull private final SharedLog mSharedLog;
     private boolean mJoinedIpv4 = false;
     private boolean mJoinedIpv6 = false;
 
     public MdnsInterfaceSocket(@NonNull NetworkInterface networkInterface, int port,
-            @NonNull Looper looper, @NonNull byte[] packetReadBuffer)
+            @NonNull Looper looper, @NonNull byte[] packetReadBuffer, @NonNull SharedLog sharedLog)
             throws IOException {
         mNetworkInterface = networkInterface;
         mMulticastSocket = new MulticastSocket(port);
@@ -80,6 +85,8 @@
         mPacketReader = new MulticastPacketReader(networkInterface.getName(), mFileDescriptor,
                 new Handler(looper), packetReadBuffer);
         mPacketReader.start();
+
+        mSharedLog = sharedLog;
     }
 
     /**
@@ -117,7 +124,7 @@
             return true;
         } catch (IOException e) {
             // The address may have just been removed
-            Log.e(TAG, "Error joining multicast group for " + mNetworkInterface, e);
+            mSharedLog.e("Error joining multicast group for " + mNetworkInterface, e);
             return false;
         }
     }
@@ -148,7 +155,7 @@
         try {
             mFileDescriptor.close();
         } catch (IOException e) {
-            Log.e(TAG, "Close file descriptor failed.");
+            mSharedLog.e("Close file descriptor failed.");
         }
         mMulticastSocket.close();
     }
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 d9bfb26..c575d40 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -20,143 +20,144 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.net.LinkAddress;
 import android.net.Network;
+import android.os.Build;
 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;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetSocketAddress;
 import java.util.List;
-import java.util.Objects;
 
 /**
  * The {@link MdnsMultinetworkSocketClient} manages the multinetwork socket for mDns
  *
  *  * <p>This class is not thread safe.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsMultinetworkSocketClient implements MdnsSocketClientBase {
     private static final String TAG = MdnsMultinetworkSocketClient.class.getSimpleName();
     private static final boolean DBG = MdnsDiscoveryManager.DBG;
 
     @NonNull private final Handler mHandler;
     @NonNull private final MdnsSocketProvider mSocketProvider;
+    @NonNull private final SharedLog mSharedLog;
+    @NonNull private final MdnsFeatureFlags mMdnsFeatureFlags;
 
-    private final ArrayMap<MdnsServiceBrowserListener, InterfaceSocketCallback> mRequestedNetworks =
+    private final ArrayMap<MdnsServiceBrowserListener, InterfaceSocketCallback> mSocketRequests =
             new ArrayMap<>();
-    private final ArrayMap<MdnsInterfaceSocket, ReadPacketHandler> mSocketPacketHandlers =
-            new ArrayMap<>();
+    private final ArrayMap<SocketKey, ReadPacketHandler> mSocketPacketHandlers = new ArrayMap<>();
     private MdnsSocketClientBase.Callback mCallback = null;
     private int mReceivedPacketNumber = 0;
 
     public MdnsMultinetworkSocketClient(@NonNull Looper looper,
-            @NonNull MdnsSocketProvider provider) {
+            @NonNull MdnsSocketProvider provider, @NonNull SharedLog sharedLog,
+            @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
         mHandler = new Handler(looper);
         mSocketProvider = provider;
+        mSharedLog = sharedLog;
+        mMdnsFeatureFlags = mdnsFeatureFlags;
     }
 
     private class InterfaceSocketCallback implements MdnsSocketProvider.SocketCallback {
         @NonNull
         private final SocketCreationCallback mSocketCreationCallback;
         @NonNull
-        private final ArrayMap<MdnsInterfaceSocket, Network> mActiveNetworkSockets =
-                new ArrayMap<>();
+        private final ArrayMap<SocketKey, MdnsInterfaceSocket> mActiveSockets = new ArrayMap<>();
 
         InterfaceSocketCallback(SocketCreationCallback socketCreationCallback) {
             mSocketCreationCallback = socketCreationCallback;
         }
 
         @Override
-        public void onSocketCreated(@Nullable Network network,
+        public void onSocketCreated(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) {
             // The socket may be already created by other request before, try to get the stored
             // ReadPacketHandler.
-            ReadPacketHandler handler = mSocketPacketHandlers.get(socket);
+            ReadPacketHandler handler = mSocketPacketHandlers.get(socketKey);
             if (handler == null) {
                 // First request to create this socket. Initial a ReadPacketHandler for this socket.
-                handler = new ReadPacketHandler(network, socket.getInterface().getIndex());
-                mSocketPacketHandlers.put(socket, handler);
+                handler = new ReadPacketHandler(socketKey);
+                mSocketPacketHandlers.put(socketKey, handler);
             }
             socket.addPacketHandler(handler);
-            mActiveNetworkSockets.put(socket, network);
-            mSocketCreationCallback.onSocketCreated(network);
+            mActiveSockets.put(socketKey, socket);
+            mSocketCreationCallback.onSocketCreated(socketKey);
         }
 
         @Override
-        public void onInterfaceDestroyed(@Nullable Network network,
+        public void onInterfaceDestroyed(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket) {
-            notifySocketDestroyed(socket);
-            maybeCleanupPacketHandler(socket);
+            mActiveSockets.remove(socketKey);
+            mSocketCreationCallback.onSocketDestroyed(socketKey);
+            maybeCleanupPacketHandler(socketKey);
         }
 
-        private void notifySocketDestroyed(@NonNull MdnsInterfaceSocket socket) {
-            final Network network = mActiveNetworkSockets.remove(socket);
-            if (!isAnySocketActive(network)) {
-                mSocketCreationCallback.onAllSocketsDestroyed(network);
+        private void notifySocketDestroyed(@NonNull SocketKey socketKey) {
+            mActiveSockets.remove(socketKey);
+            if (!isSocketActive(socketKey)) {
+                mSocketCreationCallback.onSocketDestroyed(socketKey);
             }
         }
 
         void onNetworkUnrequested() {
-            for (int i = mActiveNetworkSockets.size() - 1; i >= 0; i--) {
+            for (int i = mActiveSockets.size() - 1; i >= 0; i--) {
                 // Iterate from the end so the socket can be removed
-                final MdnsInterfaceSocket socket = mActiveNetworkSockets.keyAt(i);
-                notifySocketDestroyed(socket);
-                maybeCleanupPacketHandler(socket);
+                final SocketKey socketKey = mActiveSockets.keyAt(i);
+                notifySocketDestroyed(socketKey);
+                maybeCleanupPacketHandler(socketKey);
             }
         }
     }
 
-    private boolean isSocketActive(@NonNull MdnsInterfaceSocket socket) {
-        for (int i = 0; i < mRequestedNetworks.size(); i++) {
-            final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
-            if (isc.mActiveNetworkSockets.containsKey(socket)) {
+    private boolean isSocketActive(@NonNull SocketKey socketKey) {
+        for (int i = 0; i < mSocketRequests.size(); i++) {
+            final InterfaceSocketCallback ifaceSocketCallback = mSocketRequests.valueAt(i);
+            if (ifaceSocketCallback.mActiveSockets.containsKey(socketKey)) {
                 return true;
             }
         }
         return false;
     }
 
-    private boolean isAnySocketActive(@Nullable Network network) {
-        for (int i = 0; i < mRequestedNetworks.size(); i++) {
-            final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
-            if (isc.mActiveNetworkSockets.containsValue(network)) {
-                return true;
+    @Nullable
+    private MdnsInterfaceSocket getTargetSocket(@NonNull SocketKey targetSocketKey) {
+        for (int i = 0; i < mSocketRequests.size(); i++) {
+            final InterfaceSocketCallback ifaceSocketCallback = mSocketRequests.valueAt(i);
+            final int index = ifaceSocketCallback.mActiveSockets.indexOfKey(targetSocketKey);
+            if (index >= 0) {
+                return ifaceSocketCallback.mActiveSockets.valueAt(index);
             }
         }
-        return false;
+        return null;
     }
 
-    private ArrayMap<MdnsInterfaceSocket, Network> getActiveSockets() {
-        final ArrayMap<MdnsInterfaceSocket, Network> sockets = new ArrayMap<>();
-        for (int i = 0; i < mRequestedNetworks.size(); i++) {
-            final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
-            sockets.putAll(isc.mActiveNetworkSockets);
-        }
-        return sockets;
-    }
-
-    private void maybeCleanupPacketHandler(@NonNull MdnsInterfaceSocket socket) {
-        if (isSocketActive(socket)) return;
-        mSocketPacketHandlers.remove(socket);
+    private void maybeCleanupPacketHandler(@NonNull SocketKey socketKey) {
+        if (isSocketActive(socketKey)) return;
+        mSocketPacketHandlers.remove(socketKey);
     }
 
     private class ReadPacketHandler implements MulticastPacketReader.PacketHandler {
-        private final Network mNetwork;
-        private final int mInterfaceIndex;
+        @NonNull private final SocketKey mSocketKey;
 
-        ReadPacketHandler(@NonNull Network network, int interfaceIndex) {
-            mNetwork = network;
-            mInterfaceIndex = interfaceIndex;
+        ReadPacketHandler(@NonNull SocketKey socketKey) {
+            mSocketKey = socketKey;
         }
 
         @Override
         public void handlePacket(byte[] recvbuf, int length, InetSocketAddress src) {
-            processResponsePacket(recvbuf, length, mInterfaceIndex, mNetwork);
+            processResponsePacket(recvbuf, length, mSocketKey);
         }
     }
 
@@ -179,14 +180,14 @@
     public void notifyNetworkRequested(@NonNull MdnsServiceBrowserListener listener,
             @Nullable Network network, @NonNull SocketCreationCallback socketCreationCallback) {
         ensureRunningOnHandlerThread(mHandler);
-        InterfaceSocketCallback callback = mRequestedNetworks.get(listener);
+        InterfaceSocketCallback callback = mSocketRequests.get(listener);
         if (callback != null) {
             throw new IllegalArgumentException("Can not register duplicated listener");
         }
 
-        if (DBG) Log.d(TAG, "notifyNetworkRequested: network=" + network);
+        if (DBG) mSharedLog.v("notifyNetworkRequested: network=" + network);
         callback = new InterfaceSocketCallback(socketCreationCallback);
-        mRequestedNetworks.put(listener, callback);
+        mSocketRequests.put(listener, callback);
         mSocketProvider.requestSocket(network, callback);
     }
 
@@ -194,14 +195,14 @@
     @Override
     public void notifyNetworkUnrequested(@NonNull MdnsServiceBrowserListener listener) {
         ensureRunningOnHandlerThread(mHandler);
-        final InterfaceSocketCallback callback = mRequestedNetworks.get(listener);
+        final InterfaceSocketCallback callback = mSocketRequests.get(listener);
         if (callback == null) {
-            Log.e(TAG, "Can not be unrequested with unknown listener=" + listener);
+            mSharedLog.e("Can not be unrequested with unknown listener=" + listener);
             return;
         }
         callback.onNetworkUnrequested();
-        // onNetworkUnrequested does cleanups based on mRequestedNetworks, only remove afterwards
-        mRequestedNetworks.remove(listener);
+        // onNetworkUnrequested does cleanups based on mSocketRequests, only remove afterwards
+        mSocketRequests.remove(listener);
         mSocketProvider.unrequestSocket(callback);
     }
 
@@ -210,68 +211,98 @@
         return mHandler.getLooper();
     }
 
-    private void sendMdnsPacket(@NonNull DatagramPacket packet, @Nullable Network targetNetwork) {
-        final boolean isIpv6 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
-                instanceof Inet6Address;
-        final boolean isIpv4 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
-                instanceof Inet4Address;
-        final ArrayMap<MdnsInterfaceSocket, Network> activeSockets = getActiveSockets();
-        for (int i = 0; i < activeSockets.size(); i++) {
-            final MdnsInterfaceSocket socket = activeSockets.keyAt(i);
-            final Network network = activeSockets.valueAt(i);
-            // Check ip capability and network before sending packet
-            if (((isIpv6 && socket.hasJoinedIpv6()) || (isIpv4 && socket.hasJoinedIpv4()))
-                    // Contrary to MdnsUtils.isNetworkMatched, only send packets targeting
-                    // the null network to interfaces that have the null network (tethering
-                    // downstream interfaces).
-                    && Objects.equals(network, targetNetwork)) {
-                try {
+    @Override
+    public boolean supportsRequestingSpecificNetworks() {
+        return true;
+    }
+
+    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) 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 {
+                for (DatagramPacket packet : packets) {
                     socket.send(packet);
-                } catch (IOException e) {
-                    Log.e(TAG, "Failed to send a mDNS packet.", e);
                 }
+            } catch (IOException e) {
+                mSharedLog.e("Failed to send a mDNS packet.", e);
             }
         }
     }
 
-    private void processResponsePacket(byte[] recvbuf, int length, int interfaceIndex,
-            @NonNull Network network) {
+    private void processResponsePacket(byte[] recvbuf, int length, @NonNull SocketKey socketKey) {
         int packetNumber = ++mReceivedPacketNumber;
 
         final MdnsPacket response;
         try {
-            response = MdnsResponseDecoder.parseResponse(recvbuf, length);
+            response = MdnsResponseDecoder.parseResponse(recvbuf, length, mMdnsFeatureFlags);
         } catch (MdnsPacket.ParseException e) {
             if (e.code != MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE) {
-                Log.e(TAG, e.getMessage(), e);
+                mSharedLog.e(e.getMessage(), e);
                 if (mCallback != null) {
-                    mCallback.onFailedToParseMdnsResponse(packetNumber, e.code, network);
+                    mCallback.onFailedToParseMdnsResponse(packetNumber, e.code, socketKey);
                 }
             }
             return;
         }
 
         if (mCallback != null) {
-            mCallback.onResponseReceived(response, interfaceIndex, network);
+            mCallback.onResponseReceived(response, socketKey);
         }
     }
 
     /**
-     * Sends a mDNS request packet via given network that asks for multicast response. Null network
-     * means sending packet via all networks.
+     * Send mDNS request packets via given socket key that asks for multicast response.
      */
+    public void sendPacketRequestingMulticastResponse(@NonNull List<DatagramPacket> packets,
+            @NonNull SocketKey socketKey, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+        mHandler.post(() -> sendMdnsPackets(packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks));
+    }
+
     @Override
-    public void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
-        mHandler.post(() -> sendMdnsPacket(packet, network));
+    public void sendPacketRequestingMulticastResponse(
+            @NonNull List<DatagramPacket> packets, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+        throw new UnsupportedOperationException("This socket client need to specify the socket to"
+                + "send packet");
     }
 
     /**
-     * Sends a mDNS request packet via given network that asks for unicast response. Null network
-     * means sending packet via all networks.
+     * 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.
      */
-    @Override
-    public void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
-        // TODO: Separate unicast packet.
-        mHandler.post(() -> sendMdnsPacket(packet, network));
+    public void sendPacketRequestingUnicastResponse(@NonNull List<DatagramPacket> packets,
+            @NonNull SocketKey socketKey, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+        mHandler.post(() -> sendMdnsPackets(packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks));
     }
-}
+
+    @Override
+    public void sendPacketRequestingUnicastResponse(
+            @NonNull List<DatagramPacket> packets, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+        throw new UnsupportedOperationException("This socket client need to specify the socket to"
+                + "send packet");
+    }
+}
\ No newline at end of file
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 6ec2f99..1239180 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
@@ -20,6 +20,7 @@
 import android.text.TextUtils;
 
 import com.android.net.module.util.CollectionUtils;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -152,7 +153,8 @@
     @Override
     public int hashCode() {
         return Objects.hash(super.hashCode(),
-                Arrays.hashCode(mNextDomain), Arrays.hashCode(mTypes));
+                Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(mNextDomain)),
+                Arrays.hashCode(mTypes));
     }
 
     @Override
@@ -165,7 +167,8 @@
         }
 
         return super.equals(other)
-                && Arrays.equals(mNextDomain, ((MdnsNsecRecord) other).mNextDomain)
+                && MdnsUtils.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 27002b9..aef8211 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.util.Log;
 
 import java.io.EOFException;
 import java.io.IOException;
@@ -32,6 +31,7 @@
 public class MdnsPacket {
     private static final String TAG = MdnsPacket.class.getSimpleName();
 
+    public final int transactionId;
     public final int flags;
     @NonNull
     public final List<MdnsRecord> questions;
@@ -42,11 +42,20 @@
     @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,
             @NonNull List<MdnsRecord> additionalRecords) {
+        this(0, flags, questions, answers, authorityRecords, additionalRecords);
+    }
+
+    MdnsPacket(int transactionId, int flags,
+            @NonNull List<MdnsRecord> questions,
+            @NonNull List<MdnsRecord> answers,
+            @NonNull List<MdnsRecord> authorityRecords,
+            @NonNull List<MdnsRecord> additionalRecords) {
+        this.transactionId = transactionId;
         this.flags = flags;
         this.questions = Collections.unmodifiableList(questions);
         this.answers = Collections.unmodifiableList(answers);
@@ -71,15 +80,16 @@
      */
     @NonNull
     public static MdnsPacket parse(@NonNull MdnsPacketReader reader) throws ParseException {
+        final int transactionId;
         final int flags;
         try {
-            reader.readUInt16(); // transaction ID (not used)
+            transactionId = reader.readUInt16();
             flags = reader.readUInt16();
         } catch (EOFException e) {
             throw new ParseException(MdnsResponseErrorCode.ERROR_END_OF_FILE,
                     "Reached the end of the mDNS response unexpectedly.", e);
         }
-        return parseRecordsSection(reader, flags);
+        return parseRecordsSection(reader, flags, transactionId);
     }
 
     /**
@@ -87,8 +97,8 @@
      *
      * The records section starts with the questions count, just after the packet flags.
      */
-    public static MdnsPacket parseRecordsSection(@NonNull MdnsPacketReader reader, int flags)
-            throws ParseException {
+    public static MdnsPacket parseRecordsSection(@NonNull MdnsPacketReader reader, int flags,
+            int transactionId) throws ParseException {
         try {
             final int numQuestions = reader.readUInt16();
             final int numAnswers = reader.readUInt16();
@@ -100,7 +110,7 @@
             final ArrayList<MdnsRecord> authority = parseRecords(reader, numAuthority, false);
             final ArrayList<MdnsRecord> additional = parseRecords(reader, numAdditional, false);
 
-            return new MdnsPacket(flags, questions, answers, authority, additional);
+            return new MdnsPacket(transactionId, flags, questions, answers, authority, additional);
         } catch (EOFException e) {
             throw new ParseException(MdnsResponseErrorCode.ERROR_END_OF_FILE,
                     "Reached the end of the mDNS response unexpectedly.", e);
@@ -186,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);
@@ -206,9 +225,6 @@
 
             default: {
                 try {
-                    if (MdnsAdvertiser.DBG) {
-                        Log.i(TAG, "Skipping parsing of record of unhandled type " + type);
-                    }
                     skipMdnsRecord(reader, isQuestion);
                     return null;
                 } catch (IOException e) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java
index aa38844..32060a2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity.mdns;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.util.SparseArray;
 
@@ -30,24 +31,31 @@
 
 /** Simple decoder for mDNS packets. */
 public class MdnsPacketReader {
+    // The total length in bytes should be less than 255 bytes anyway (including labels and label
+    // length) per RFC9267, so limit the number of labels to 128 (each label is 2 bytes with the
+    // length).
+    // https://www.rfc-editor.org/rfc/rfc9267.html#name-label-and-name-length-valid
+    private static final int LABEL_COUNT_LIMIT = 128;
     private final byte[] buf;
     private final int count;
     private final SparseArray<LabelEntry> labelDictionary;
+    private final MdnsFeatureFlags mMdnsFeatureFlags;
     private int pos;
     private int limit;
 
     /** Constructs a reader for the given packet. */
     public MdnsPacketReader(DatagramPacket packet) {
-        this(packet.getData(), packet.getLength());
+        this(packet.getData(), packet.getLength(), MdnsFeatureFlags.newBuilder().build());
     }
 
     /** Constructs a reader for the given packet. */
-    public MdnsPacketReader(byte[] buffer, int length) {
+    public MdnsPacketReader(byte[] buffer, int length, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
         buf = buffer;
         count = length;
         pos = 0;
         limit = -1;
         labelDictionary = new SparseArray<>(16);
+        mMdnsFeatureFlags = mdnsFeatureFlags;
     }
 
     /**
@@ -137,6 +145,7 @@
     public String[] readLabels() throws IOException {
         List<String> result = new ArrayList<>(5);
         LabelEntry previousEntry = null;
+        int tracingHops = 0;
 
         while (getRemaining() > 0) {
             byte nextByte = peekByte();
@@ -161,6 +170,10 @@
                 // Follow the chain of labels starting at this pointer, adding all of them onto the
                 // result.
                 while (labelOffset != 0) {
+                    if (mMdnsFeatureFlags.mIsLabelCountLimitEnabled
+                            && tracingHops > LABEL_COUNT_LIMIT) {
+                        throw new IOException("Invalid MDNS response packet: Too many labels.");
+                    }
                     LabelEntry entry = labelDictionary.get(labelOffset);
                     if (entry == null) {
                         throw new IOException(
@@ -169,6 +182,7 @@
                     }
                     result.add(entry.label);
                     labelOffset = entry.nextOffset;
+                    tracingHops++;
                 }
                 break;
             } else {
@@ -269,4 +283,4 @@
             this.label = label;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
index 4c385da..e84cead 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
@@ -16,15 +16,18 @@
 
 package com.android.server.connectivity.mdns;
 
-import static com.android.server.connectivity.mdns.MdnsRecordRepository.IPV4_ADDR;
-import static com.android.server.connectivity.mdns.MdnsRecordRepository.IPV6_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.util.Log;
+
+import com.android.net.module.util.SharedLog;
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
@@ -33,10 +36,10 @@
  * A class used to send several packets at given time intervals.
  * @param <T> The type of the request providing packet repeating parameters.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public abstract class MdnsPacketRepeater<T extends MdnsPacketRepeater.Request> {
-    private static final boolean DBG = MdnsAdvertiser.DBG;
     private static final InetSocketAddress[] ALL_ADDRS = new InetSocketAddress[] {
-            IPV4_ADDR, IPV6_ADDR
+            IPV4_SOCKET_ADDR, IPV6_SOCKET_ADDR
     };
 
     @NonNull
@@ -45,6 +48,9 @@
     protected final Handler mHandler;
     @Nullable
     private final PacketRepeaterCallback<T> mCb;
+    @NonNull
+    private final SharedLog mSharedLog;
+    private final boolean mEnableDebugLog;
 
     /**
      * Status callback from {@link MdnsPacketRepeater}.
@@ -56,7 +62,7 @@
         /**
          * Called when a packet was sent.
          */
-        default void onSent(int index, @NonNull T info) {}
+        default void onSent(int index, @NonNull T info, int sentPacketCount) {}
 
         /**
          * Called when the {@link MdnsPacketRepeater} is done sending packets.
@@ -87,12 +93,6 @@
         int getNumSends();
     }
 
-    /**
-     * Get the logging tag to use.
-     */
-    @NonNull
-    protected abstract String getTag();
-
     private final class ProbeHandler extends Handler {
         ProbeHandler(@NonNull Looper looper) {
             super(looper);
@@ -111,17 +111,18 @@
             }
 
             final MdnsPacket packet = request.getPacket(index);
-            if (DBG) {
-                Log.v(getTag(), "Sending packets for iteration " + index + " out of "
+            if (mEnableDebugLog) {
+                mSharedLog.v("Sending packets for iteration " + index + " out of "
                         + request.getNumSends() + " for ID " + msg.what);
             }
             // Send to both v4 and v6 addresses; the reply sender will take care of ignoring the
             // send when the socket has not joined the relevant group.
+            int sentPacketCount = 0;
             for (InetSocketAddress destination : ALL_ADDRS) {
                 try {
-                    mReplySender.sendNow(packet, destination);
+                    sentPacketCount += mReplySender.sendNow(packet, destination);
                 } catch (IOException e) {
-                    Log.e(getTag(), "Error sending packet to " + destination, e);
+                    mSharedLog.e("Error sending packet to " + destination, e);
                 }
             }
 
@@ -133,26 +134,29 @@
                 // likely not to be available since the device is in deep sleep anyway.
                 final long delay = request.getDelayMs(nextIndex);
                 sendMessageDelayed(obtainMessage(msg.what, nextIndex, 0, request), delay);
-                if (DBG) Log.v(getTag(), "Scheduled next packet in " + delay + "ms");
+                if (mEnableDebugLog) mSharedLog.v("Scheduled next packet in " + delay + "ms");
             }
 
             // Call onSent after scheduling the next run, to allow the callback to cancel it
             if (mCb != null) {
-                mCb.onSent(index, request);
+                mCb.onSent(index, request, sentPacketCount);
             }
         }
     }
 
     protected MdnsPacketRepeater(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
-            @Nullable PacketRepeaterCallback<T> cb) {
+            @Nullable PacketRepeaterCallback<T> cb, @NonNull SharedLog sharedLog,
+            boolean enableDebugLog) {
         mHandler = new ProbeHandler(looper);
         mReplySender = replySender;
         mCb = cb;
+        mSharedLog = sharedLog;
+        mEnableDebugLog = enableDebugLog;
     }
 
     protected void startSending(int id, @NonNull T request, long initialDelayMs) {
-        if (DBG) {
-            Log.v(getTag(), "Starting send with id " + id + ", request "
+        if (mEnableDebugLog) {
+            mSharedLog.v("Starting send with id " + id + ", request "
                     + request.getClass().getSimpleName() + ", delay " + initialDelayMs);
         }
         mHandler.sendMessageDelayed(mHandler.obtainMessage(id, 0, 0, request), initialDelayMs);
@@ -170,8 +174,8 @@
         // all in the handler queue; unless this method is called from a message, but the current
         // message cannot be cancelled.
         if (mHandler.hasMessages(id)) {
-            if (DBG) {
-                Log.v(getTag(), "Stopping send on id " + id);
+            if (mEnableDebugLog) {
+                mSharedLog.v("Stopping send on id " + id);
             }
             mHandler.removeMessages(id);
             return true;
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 c0f9b8b..6879a64 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -17,11 +17,11 @@
 package com.android.server.connectivity.mdns;
 
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
 import java.net.SocketAddress;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -180,7 +180,7 @@
             int existingOffset = entry.getKey();
             String[] existingLabels = entry.getValue();
 
-            if (Arrays.equals(existingLabels, labels)) {
+            if (MdnsUtils.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 2c7b26b..e5c90a4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -18,13 +18,15 @@
 
 import android.annotation.Nullable;
 
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
 
 /** An mDNS "PTR" record, which holds a name (the "pointer"). */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
 public class MdnsPointerRecord extends MdnsRecord {
     private String[] pointer;
 
@@ -37,6 +39,12 @@
         super(name, TYPE_PTR, reader, isQuestion);
     }
 
+    public MdnsPointerRecord(String[] name, boolean isUnicast) {
+        super(name, TYPE_PTR,
+                MdnsConstants.QCLASS_INTERNET | (isUnicast ? MdnsConstants.QCLASS_UNICAST : 0),
+                0L /* receiptTimeMillis */, false /* cacheFlush */, 0L /* ttlMillis */);
+    }
+
     public MdnsPointerRecord(String[] name, long receiptTimeMillis, boolean cacheFlush,
                     long ttlMillis, String[] pointer) {
         super(name, TYPE_PTR, MdnsConstants.QCLASS_INTERNET, receiptTimeMillis, cacheFlush,
@@ -60,7 +68,8 @@
     }
 
     public boolean hasSubtype() {
-        return (name != null) && (name.length > 2) && name[1].equals(MdnsConstants.SUBTYPE_LABEL);
+        return (name != null) && (name.length > 2) && MdnsUtils.equalsIgnoreDnsCase(name[1],
+                MdnsConstants.SUBTYPE_LABEL);
     }
 
     public String getSubtype() {
@@ -74,7 +83,7 @@
 
     @Override
     public int hashCode() {
-        return (super.hashCode() * 31) + Arrays.hashCode(pointer);
+        return (super.hashCode() * 31) + Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(pointer));
     }
 
     @Override
@@ -86,6 +95,7 @@
             return false;
         }
 
-        return super.equals(other) && Arrays.equals(pointer, ((MdnsPointerRecord) other).pointer);
+        return super.equals(other) && MdnsUtils.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 669b323..e88947a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -17,13 +17,16 @@
 package com.android.server.connectivity.mdns;
 
 import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.os.Build;
 import android.os.Looper;
 
 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.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
@@ -32,16 +35,13 @@
  *
  * TODO: implement receiving replies and handling conflicts.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsProber extends MdnsPacketRepeater<MdnsProber.ProbingInfo> {
     private static final long CONFLICT_RETRY_DELAY_MS = 5_000L;
-    @NonNull
-    private final String mLogTag;
 
-    public MdnsProber(@NonNull String interfaceTag, @NonNull Looper looper,
-            @NonNull MdnsReplySender replySender,
-            @NonNull PacketRepeaterCallback<ProbingInfo> cb) {
-        super(looper, replySender, cb);
-        mLogTag = MdnsProber.class.getSimpleName() + "/" + interfaceTag;
+    public MdnsProber(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
+            @NonNull PacketRepeaterCallback<ProbingInfo> cb, @NonNull SharedLog sharedLog) {
+        super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
     }
 
     /** Probing request to send with {@link MdnsProber}. */
@@ -113,15 +113,11 @@
          */
         private static boolean containsName(@NonNull List<MdnsRecord> records,
                 @NonNull String[] name) {
-            return CollectionUtils.any(records, r -> Arrays.equals(name, r.getName()));
+            return CollectionUtils.any(records,
+                    r -> MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
         }
     }
 
-    @NonNull
-    @Override
-    protected String getTag() {
-        return mLogTag;
-    }
 
     @VisibleForTesting
     protected long getInitialDelay() {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
new file mode 100644
index 0000000..3fcf0d4
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -0,0 +1,144 @@
+/*
+ * 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.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * The query scheduler class for calculating next query tasks parameters.
+ * <p>
+ * The class is not thread-safe and needs to be used on a consistent thread.
+ */
+public class MdnsQueryScheduler {
+
+    /**
+     * The argument for tracking the query tasks status.
+     */
+    public static class ScheduledQueryTaskArgs {
+        public final QueryTaskConfig config;
+        public final long timeToRun;
+        public final long minTtlExpirationTimeWhenScheduled;
+        public final long sessionId;
+
+        ScheduledQueryTaskArgs(@NonNull QueryTaskConfig config, long timeToRun,
+                long minTtlExpirationTimeWhenScheduled, long sessionId) {
+            this.config = config;
+            this.timeToRun = timeToRun;
+            this.minTtlExpirationTimeWhenScheduled = minTtlExpirationTimeWhenScheduled;
+            this.sessionId = sessionId;
+        }
+    }
+
+    @Nullable
+    private ScheduledQueryTaskArgs mLastScheduledQueryTaskArgs;
+
+    public MdnsQueryScheduler() {
+    }
+
+    /**
+     * Cancel the scheduled run. The method needed to be called when the scheduled task need to
+     * be canceled and rescheduling is not need.
+     */
+    public void cancelScheduledRun() {
+        mLastScheduledQueryTaskArgs = null;
+    }
+
+    /**
+     * Calculates ScheduledQueryTaskArgs for rescheduling the current task. Returns null if the
+     * rescheduling is not necessary.
+     */
+    @Nullable
+    public ScheduledQueryTaskArgs maybeRescheduleCurrentRun(long now,
+            long minRemainingTtl, long lastSentTime, long sessionId) {
+        if (mLastScheduledQueryTaskArgs == null) {
+            return null;
+        }
+        if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff()) {
+            return null;
+        }
+
+        final long timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
+                mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime);
+
+        if (timeToRun <= mLastScheduledQueryTaskArgs.timeToRun) {
+            return null;
+        }
+
+        mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(mLastScheduledQueryTaskArgs.config,
+                timeToRun,
+                minRemainingTtl + now,
+                sessionId);
+        return mLastScheduledQueryTaskArgs;
+    }
+
+    /**
+     *  Calculates the ScheduledQueryTaskArgs for the next run.
+     */
+    @NonNull
+    public ScheduledQueryTaskArgs scheduleNextRun(
+            @NonNull QueryTaskConfig currentConfig,
+            long minRemainingTtl,
+            long now,
+            long lastSentTime,
+            long sessionId) {
+        final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun();
+        final long timeToRun;
+        if (mLastScheduledQueryTaskArgs == null) {
+            timeToRun = now + nextRunConfig.delayUntilNextTaskWithoutBackoffMs;
+        } else {
+            timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
+                    nextRunConfig, now, minRemainingTtl, lastSentTime);
+        }
+        mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(nextRunConfig, timeToRun,
+                minRemainingTtl + now,
+                sessionId);
+        return mLastScheduledQueryTaskArgs;
+    }
+
+    /**
+     *  Calculates the ScheduledQueryTaskArgs for the initial run.
+     */
+    public ScheduledQueryTaskArgs scheduleFirstRun(@NonNull QueryTaskConfig taskConfig,
+            long now, long minRemainingTtl, long currentSessionId) {
+        mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(taskConfig, now /* timeToRun */,
+                now + minRemainingTtl/* minTtlExpirationTimeWhenScheduled */,
+                currentSessionId);
+        return mLastScheduledQueryTaskArgs;
+    }
+
+    private static long calculateTimeToRun(@NonNull ScheduledQueryTaskArgs taskArgs,
+            QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime) {
+        final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
+        if (!queryTaskConfig.shouldUseQueryBackoff()) {
+            return lastSentTime + baseDelayInMs;
+        }
+        if (minRemainingTtl <= 0) {
+            // There's no service, or there is an expired service. In any case, schedule for the
+            // minimum time, which is the base delay.
+            return lastSentTime + baseDelayInMs;
+        }
+        // If the next TTL expiration time hasn't changed, then use previous calculated timeToRun.
+        if (lastSentTime < now
+                && taskArgs.minTtlExpirationTimeWhenScheduled == now + minRemainingTtl) {
+            // Use the original scheduling time if the TTL has not changed, to avoid continuously
+            // rescheduling to 80% of the remaining TTL as time passes
+            return taskArgs.timeToRun;
+        }
+        return Math.max(now + (long) (0.8 * minRemainingTtl), lastSentTime + baseDelayInMs);
+    }
+}
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 19630e3..b865319 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -23,7 +23,9 @@
 import android.os.SystemClock;
 import android.text.TextUtils;
 
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -39,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;
 
@@ -136,7 +139,7 @@
         }
 
         for (int i = 0; i < list1.length; ++i) {
-            if (!list1[i].equals(list2[i + offset])) {
+            if (!MdnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
                 return false;
             }
         }
@@ -175,6 +178,16 @@
     }
 
     /**
+     * For questions, returns whether a unicast reply was requested.
+     *
+     * In practice this is identical to {@link #getCacheFlush()}, as the "cache flush" flag in
+     * replies is the same as "unicast reply requested" in questions.
+     */
+    public final boolean isUnicastReplyRequested() {
+        return (cls & MdnsConstants.QCLASS_UNICAST) != 0;
+    }
+
+    /**
      * Returns the record's remaining TTL.
      *
      * If the record was not sent yet (receipt time {@link #RECEIPT_TIME_NOT_SENT}), this is the
@@ -220,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);
 
@@ -271,12 +284,13 @@
 
         MdnsRecord otherRecord = (MdnsRecord) other;
 
-        return Arrays.equals(name, otherRecord.name) && (type == otherRecord.type);
+        return MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
+                == otherRecord.type);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(Arrays.hashCode(name), type);
+        return Objects.hash(Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(name)), type);
     }
 
     /**
@@ -297,7 +311,7 @@
 
         public Key(int recordType, String[] recordName) {
             this.recordType = recordType;
-            this.recordName = recordName;
+            this.recordName = MdnsUtils.toDnsLabelsLowerCase(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 f756459..36f3982 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -16,6 +16,12 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TargetApi;
@@ -24,23 +30,29 @@
 import android.os.Build;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.text.TextUtils;
+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.HexDump;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.NetworkInterface;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -48,6 +60,8 @@
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 /**
  * A repository of records advertised through {@link MdnsInterfaceAdvertiser}.
@@ -63,23 +77,22 @@
     // TTL for records with a host name as the resource record's name (e.g., A, AAAA, HINFO) or a
     // host name contained within the resource record's rdata (e.g., SRV, reverse mapping PTR
     // record)
-    private static final long NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
+    private static final long DEFAULT_NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
     // TTL for other records
-    private static final long NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
+    private static final long DEFAULT_NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
 
     // Top-level domain for link-local queries, as per RFC6762 3.
     private static final String LOCAL_TLD = "local";
-    // Subtype separator as per RFC6763 7.1 (_printer._sub._http._tcp.local)
-    private static final String SUBTYPE_SEPARATOR = "_sub";
 
     // Service type for service enumeration (RFC6763 9.)
     private static final String[] DNS_SD_SERVICE_TYPE =
             new String[] { "_services", "_dns-sd", "_udp", LOCAL_TLD };
 
-    public static final InetSocketAddress IPV6_ADDR = new InetSocketAddress(
-            MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
-    public static final InetSocketAddress IPV4_ADDR = new InetSocketAddress(
-            MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
+    private enum RecordConflictType {
+        NO_CONFLICT,
+        CONFLICT,
+        IDENTICAL
+    }
 
     @NonNull
     private final Random mDelayGenerator = new Random();
@@ -91,17 +104,24 @@
     @NonNull
     private final Looper mLooper;
     @NonNull
+    private final Dependencies mDeps;
+    @NonNull
     private final String[] mDeviceHostname;
+    @NonNull
+    private final MdnsFeatureFlags mMdnsFeatureFlags;
 
-    public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname) {
-        this(looper, new Dependencies(), deviceHostname);
+    public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname,
+            @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+        this(looper, new Dependencies(), deviceHostname, mdnsFeatureFlags);
     }
 
     @VisibleForTesting
     public MdnsRecordRepository(@NonNull Looper looper, @NonNull Dependencies deps,
-            @NonNull String[] deviceHostname) {
+            @NonNull String[] deviceHostname, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
         mDeviceHostname = deviceHostname;
         mLooper = looper;
+        mDeps = deps;
+        mMdnsFeatureFlags = mdnsFeatureFlags;
     }
 
     /**
@@ -117,6 +137,10 @@
         public Enumeration<InetAddress> getInterfaceInetAddresses(@NonNull NetworkInterface iface) {
             return iface.getInetAddresses();
         }
+
+        public long elapsedRealTime() {
+            return SystemClock.elapsedRealtime();
+        }
     }
 
     private static class RecordInfo<T extends MdnsRecord> {
@@ -130,27 +154,31 @@
         public final boolean isSharedName;
 
         /**
-         * Whether probing is still in progress for the record.
+         * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast on IPv4, 0
+         * if never
          */
-        public boolean isProbing;
+        public long lastAdvertisedOnIpv4TimeMs;
 
         /**
-         * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast, 0 if never
+         * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast on IPv6, 0
+         * if never
          */
-        public long lastAdvertisedTimeMs;
+        public long lastAdvertisedOnIpv6TimeMs;
 
         /**
-         * Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast,
-         * 0 if never
+         * 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,
-                 boolean probing) {
+        RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
             this.serviceInfo = serviceInfo;
             this.record = record;
             this.isSharedName = sharedName;
-            this.isProbing = probing;
         }
     }
 
@@ -159,19 +187,203 @@
         public final List<RecordInfo<?>> allRecords;
         @NonNull
         public final List<RecordInfo<MdnsPointerRecord>> ptrRecords;
-        @NonNull
+        @Nullable
         public final RecordInfo<MdnsServiceRecord> srvRecord;
-        @NonNull
+        @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
         public final NsdServiceInfo serviceInfo;
-        @Nullable
-        public final String subtype;
 
         /**
          * Whether the service is sending exit announcements and will be destroyed soon.
          */
-        public boolean exiting = false;
+        public boolean exiting;
+
+        /**
+         * The replied query packet count of this service.
+         */
+        public int repliedServiceCount = NO_PACKET;
+
+        /**
+         * The sent packet count of this service (including announcements and probes).
+         */
+        public int sentPacketCount = NO_PACKET;
+
+        /**
+         * Whether probing is still in progress.
+         */
+        private boolean isProbing;
+
+        @Nullable
+        private Duration ttl;
+
+        /**
+         * Create a ServiceRegistration with only update the subType.
+         */
+        ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
+            NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
+            newServiceInfo.setSubtypes(newSubtypes);
+            return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
+                    repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
+        }
+
+        /**
+         * Create a ServiceRegistration for dns-sd service registration (RFC6763).
+         */
+        ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
+                int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
+                @Nullable Duration ttl) {
+            this.serviceInfo = serviceInfo;
+
+            final long nonNameRecordsTtlMillis;
+            final long nameRecordsTtlMillis;
+
+            // When custom TTL is specified, all records of the service will use the custom TTL.
+            // This is typically useful for SRP (Service Registration Protocol:
+            // https://datatracker.ietf.org/doc/html/draft-ietf-dnssd-srp-24) Advertising Proxy
+            // where all records in a single SRP are required the same TTL.
+            if (ttl != null) {
+                nonNameRecordsTtlMillis = ttl.toMillis();
+                nameRecordsTtlMillis = ttl.toMillis();
+            } else {
+                nonNameRecordsTtlMillis = DEFAULT_NON_NAME_RECORDS_TTL_MILLIS;
+                nameRecordsTtlMillis = DEFAULT_NAME_RECORDS_TTL_MILLIS;
+            }
+
+            final boolean hasCustomHost = !TextUtils.isEmpty(serviceInfo.getHostname());
+            final String[] hostname =
+                    hasCustomHost
+                            ? new String[] {serviceInfo.getHostname(), LOCAL_TLD}
+                            : deviceHostname;
+            final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(5);
+
+            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<>(
+                        serviceInfo,
+                        new MdnsPointerRecord(
+                                serviceType,
+                                0L /* receiptTimeMillis */,
+                                false /* cacheFlush */,
+                                nonNameRecordsTtlMillis,
+                                serviceName),
+                        true /* sharedName */));
+                for (String subtype : serviceInfo.getSubtypes()) {
+                    ptrRecords.add(new RecordInfo<>(
+                            serviceInfo,
+                            new MdnsPointerRecord(
+                                    MdnsUtils.constructFullSubtype(serviceType, subtype),
+                                    0L /* receiptTimeMillis */,
+                                    false /* cacheFlush */,
+                                    nonNameRecordsTtlMillis,
+                                    serviceName),
+                            true /* sharedName */));
+                }
+
+                srvRecord = new RecordInfo<>(
+                        serviceInfo,
+                        new MdnsServiceRecord(serviceName,
+                                0L /* receiptTimeMillis */,
+                                true /* cacheFlush */,
+                                nameRecordsTtlMillis,
+                                0 /* servicePriority */, 0 /* serviceWeight */,
+                                serviceInfo.getPort(),
+                                hostname),
+                        false /* sharedName */);
+
+                txtRecord = new RecordInfo<>(
+                        serviceInfo,
+                        new MdnsTextRecord(serviceName,
+                                0L /* receiptTimeMillis */,
+                                // Service name is verified unique after probing
+                                true /* cacheFlush */,
+                                nonNameRecordsTtlMillis,
+                                attrsToTextEntries(serviceInfo.getAttributes())),
+                        false /* sharedName */);
+
+                allRecords.addAll(ptrRecords);
+                allRecords.add(srvRecord);
+                allRecords.add(txtRecord);
+                // Service type enumeration record (RFC6763 9.)
+                allRecords.add(new RecordInfo<>(
+                        serviceInfo,
+                        new MdnsPointerRecord(
+                                DNS_SD_SERVICE_TYPE,
+                                0L /* receiptTimeMillis */,
+                                false /* cacheFlush */,
+                                nonNameRecordsTtlMillis,
+                                serviceType),
+                        true /* sharedName */));
+            } else {
+                ptrRecords = Collections.emptyList();
+                srvRecord = null;
+                txtRecord = null;
+            }
+
+            if (hasCustomHost) {
+                addressRecords = new ArrayList<>(serviceInfo.getHostAddresses().size());
+                for (InetAddress address : serviceInfo.getHostAddresses()) {
+                    addressRecords.add(new RecordInfo<>(
+                                    serviceInfo,
+                                    new MdnsInetAddressRecord(hostname,
+                                            0L /* receiptTimeMillis */,
+                                            true /* cacheFlush */,
+                                            nameRecordsTtlMillis,
+                                            address),
+                                    false /* sharedName */));
+                }
+                allRecords.addAll(addressRecords);
+            } else {
+                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;
+            this.isProbing = isProbing;
+            this.exiting = exiting;
+        }
 
         /**
          * Create a ServiceRegistration for dns-sd service registration (RFC6763).
@@ -180,86 +392,15 @@
          * @param serviceInfo Service to advertise
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                @Nullable String subtype) {
-            this.serviceInfo = serviceInfo;
-            this.subtype = subtype;
-
-            final String[] serviceType = splitServiceType(serviceInfo);
-            final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
-
-            // Service PTR record
-            final RecordInfo<MdnsPointerRecord> ptrRecord = new RecordInfo<>(
-                    serviceInfo,
-                    new MdnsPointerRecord(
-                            serviceType,
-                            0L /* receiptTimeMillis */,
-                            false /* cacheFlush */,
-                            NON_NAME_RECORDS_TTL_MILLIS,
-                            serviceName),
-                    true /* sharedName */, true /* probing */);
-
-            if (subtype == null) {
-                this.ptrRecords = Collections.singletonList(ptrRecord);
-            } else {
-                final String[] subtypeName = new String[serviceType.length + 2];
-                System.arraycopy(serviceType, 0, subtypeName, 2, serviceType.length);
-                subtypeName[0] = subtype;
-                subtypeName[1] = SUBTYPE_SEPARATOR;
-                final RecordInfo<MdnsPointerRecord> subtypeRecord = new RecordInfo<>(
-                        serviceInfo,
-                        new MdnsPointerRecord(
-                                subtypeName,
-                                0L /* receiptTimeMillis */,
-                                false /* cacheFlush */,
-                                NON_NAME_RECORDS_TTL_MILLIS,
-                                serviceName),
-                        true /* sharedName */, true /* probing */);
-
-                this.ptrRecords = List.of(ptrRecord, subtypeRecord);
-            }
-
-            srvRecord = new RecordInfo<>(
-                    serviceInfo,
-                    new MdnsServiceRecord(serviceName,
-                            0L /* receiptTimeMillis */,
-                            true /* cacheFlush */,
-                            NAME_RECORDS_TTL_MILLIS, 0 /* servicePriority */, 0 /* serviceWeight */,
-                            serviceInfo.getPort(),
-                            deviceHostname),
-                    false /* sharedName */, true /* probing */);
-
-            txtRecord = new RecordInfo<>(
-                    serviceInfo,
-                    new MdnsTextRecord(serviceName,
-                            0L /* receiptTimeMillis */,
-                            true /* cacheFlush */, // Service name is verified unique after probing
-                            NON_NAME_RECORDS_TTL_MILLIS,
-                            attrsToTextEntries(serviceInfo.getAttributes())),
-                    false /* sharedName */, true /* probing */);
-
-            final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(5);
-            allRecords.addAll(ptrRecords);
-            allRecords.add(srvRecord);
-            allRecords.add(txtRecord);
-            // Service type enumeration record (RFC6763 9.)
-            allRecords.add(new RecordInfo<>(
-                    serviceInfo,
-                    new MdnsPointerRecord(
-                            DNS_SD_SERVICE_TYPE,
-                            0L /* receiptTimeMillis */,
-                            false /* cacheFlush */,
-                            NON_NAME_RECORDS_TTL_MILLIS,
-                            serviceType),
-                    true /* sharedName */, true /* probing */));
-
-            this.allRecords = Collections.unmodifiableList(allRecords);
+                int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
+            this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
+                    false /* exiting */, true /* isProbing */, ttl);
         }
 
         void setProbing(boolean probing) {
-            for (RecordInfo<?> info : allRecords) {
-                info.isProbing = probing;
-            }
+            this.isProbing = probing;
         }
+
     }
 
     /**
@@ -275,9 +416,9 @@
                             revDnsAddr,
                             0L /* receiptTimeMillis */,
                             true /* cacheFlush */,
-                            NAME_RECORDS_TTL_MILLIS,
+                            DEFAULT_NAME_RECORDS_TTL_MILLIS,
                             mDeviceHostname),
-                    false /* sharedName */, false /* probing */));
+                    false /* sharedName */));
 
             mGeneralRecords.add(new RecordInfo<>(
                     null /* serviceInfo */,
@@ -285,37 +426,57 @@
                             mDeviceHostname,
                             0L /* receiptTimeMillis */,
                             true /* cacheFlush */,
-                            NAME_RECORDS_TTL_MILLIS,
+                            DEFAULT_NAME_RECORDS_TTL_MILLIS,
                             addr.getAddress()),
-                    false /* sharedName */, false /* probing */));
+                    false /* sharedName */));
         }
     }
 
     /**
+     * Update a service that already registered in the repository.
+     *
+     * @param serviceId An existing service ID.
+     * @param subtypes New subtypes
+     */
+    public void updateService(int serviceId, @NonNull Set<String> subtypes) {
+        final ServiceRegistration existingRegistration = mServices.get(serviceId);
+        if (existingRegistration == null) {
+            throw new IllegalArgumentException(
+                    "Service ID must already exist for an update request: " + serviceId);
+        }
+        final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
+                subtypes);
+        mServices.put(serviceId, updatedRegistration);
+    }
+
+    /**
      * Add a service to the repository.
      *
      * This may remove/replace any existing service that used the name added but is exiting.
      * @param serviceId A unique service ID.
      * @param serviceInfo Service info to add.
+     * @param ttl the TTL duration for all records of {@code serviceInfo} or {@code null}
      * @return If the added service replaced another with a matching name (which was exiting), the
      *         ID of the replaced service.
      * @throws NameConflictException There is already a (non-exiting) service using the name.
      */
-    public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable String subtype)
+    public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable Duration ttl)
             throws NameConflictException {
         if (mServices.contains(serviceId)) {
             throw new IllegalArgumentException(
                     "Service ID must not be reused across registrations: " + serviceId);
         }
 
-        final int existing = getServiceByName(serviceInfo.getServiceName());
+        final int existing =
+                getServiceByNameAndType(serviceInfo.getServiceName(), serviceInfo.getServiceType());
         // It's OK to re-add a service that is exiting
         if (existing >= 0 && !mServices.get(existing).exiting) {
             throw new NameConflictException(existing);
         }
 
         final ServiceRegistration registration = new ServiceRegistration(
-                mDeviceHostname, serviceInfo, subtype);
+                mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
+                NO_PACKET /* sentPacketCount */, ttl);
         mServices.put(serviceId, registration);
 
         // Remove existing exiting service
@@ -324,31 +485,65 @@
     }
 
     /**
-     * @return The ID of the service identified by its name, or -1 if none.
+     * @return The ID of the service identified by its name and type, or -1 if none.
      */
-    private int getServiceByName(@NonNull String serviceName) {
+    private int getServiceByNameAndType(
+            @Nullable String serviceName, @Nullable String serviceType) {
+        if (TextUtils.isEmpty(serviceName) || TextUtils.isEmpty(serviceType)) {
+            return -1;
+        }
         for (int i = 0; i < mServices.size(); i++) {
-            final ServiceRegistration registration = mServices.valueAt(i);
-            if (serviceName.equals(registration.serviceInfo.getServiceName())) {
+            final NsdServiceInfo info = mServices.valueAt(i).serviceInfo;
+            if (MdnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
+                    && MdnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
                 return mServices.keyAt(i);
             }
         }
         return -1;
     }
 
-    private MdnsProber.ProbingInfo makeProbingInfo(int serviceId,
-            @NonNull MdnsServiceRecord srvRecord) {
+    private MdnsProber.ProbingInfo makeProbingInfo(
+            int serviceId, ServiceRegistration registration) {
         final List<MdnsRecord> probingRecords = new ArrayList<>();
         // Probe with cacheFlush cleared; it is set when announcing, as it was verified unique:
         // RFC6762 10.2
-        probingRecords.add(new MdnsServiceRecord(srvRecord.getName(),
-                0L /* receiptTimeMillis */,
-                false /* cacheFlush */,
-                srvRecord.getTtl(),
-                srvRecord.getServicePriority(), srvRecord.getServiceWeight(),
-                srvRecord.getServicePort(),
-                srvRecord.getServiceHost()));
+        if (registration.srvRecord != null) {
+            MdnsServiceRecord srvRecord = registration.srvRecord.record;
+            probingRecords.add(new MdnsServiceRecord(srvRecord.getName(),
+                    0L /* receiptTimeMillis */,
+                    false /* cacheFlush */,
+                    srvRecord.getTtl(),
+                    srvRecord.getServicePriority(), srvRecord.getServiceWeight(),
+                    srvRecord.getServicePort(),
+                    srvRecord.getServiceHost()));
+        }
 
+        for (MdnsInetAddressRecord inetAddressRecord :
+                makeProbingInetAddressRecords(registration.serviceInfo)) {
+            probingRecords.add(new MdnsInetAddressRecord(inetAddressRecord.getName(),
+                    0L /* receiptTimeMillis */,
+                    false /* cacheFlush */,
+                    inetAddressRecord.getTtl(),
+                    inetAddressRecord.getInet4Address() == null
+                            ? 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);
     }
 
@@ -382,7 +577,9 @@
                 r -> new MdnsPointerRecord(
                         r.record.getName(),
                         0L /* receiptTimeMillis */,
-                        true /* cacheFlush */,
+                        // RFC6762#10.1, the cache flush bit should be false for existing
+                        // announcement. Otherwise, the record will be deleted immediately.
+                        false /* cacheFlush */,
                         0L /* ttlMillis */,
                         r.record.getPointer()));
 
@@ -404,6 +601,24 @@
     }
 
     /**
+     * @return The replied request count of the service.
+     */
+    public int getServiceRepliedRequestsCount(int id) {
+        final ServiceRegistration service = mServices.get(id);
+        if (service == null) return NO_PACKET;
+        return service.repliedServiceCount;
+    }
+
+    /**
+     * @return The total sent packet count of the service.
+     */
+    public int getSentPacketCount(int id) {
+        final ServiceRegistration service = mServices.get(id);
+        if (service == null) return NO_PACKET;
+        return service.sentPacketCount;
+    }
+
+    /**
      * Remove all services from the repository
      * @return IDs of the removed services
      */
@@ -417,35 +632,14 @@
         return ret;
     }
 
-    /**
-     * Info about a reply to be sent.
-     */
-    public static class ReplyInfo {
-        @NonNull
-        public final List<MdnsRecord> answers;
-        @NonNull
-        public final List<MdnsRecord> additionalAnswers;
-        public final long sendDelayMs;
-        @NonNull
-        public final InetSocketAddress destination;
-
-        public ReplyInfo(
-                @NonNull List<MdnsRecord> answers,
-                @NonNull List<MdnsRecord> additionalAnswers,
-                long sendDelayMs,
-                @NonNull InetSocketAddress destination) {
-            this.answers = answers;
-            this.additionalAnswers = additionalAnswers;
-            this.sendDelayMs = sendDelayMs;
-            this.destination = destination;
+    private boolean isTruncatedKnownAnswerPacket(MdnsPacket packet) {
+        if (!mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
+                // Should ignore the response packet.
+                || (packet.flags & MdnsConstants.FLAGS_RESPONSE) != 0) {
+            return false;
         }
-
-        @Override
-        public String toString() {
-            return "{ReplyInfo to " + destination + ", answers: " + answers.size()
-                    + ", additionalAnswers: " + additionalAnswers.size()
-                    + ", sendDelayMs " + sendDelayMs + "}";
-        }
+        // Check the packet contains no questions and as many more Known-Answer records as will fit.
+        return packet.questions.size() == 0 && packet.answers.size() != 0;
     }
 
     /**
@@ -455,29 +649,86 @@
      * @param src The source address of the incoming packet.
      */
     @Nullable
-    public ReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
-        final long now = SystemClock.elapsedRealtime();
-        final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
-        final ArrayList<MdnsRecord> additionalAnswerRecords = new ArrayList<>();
-        final ArrayList<RecordInfo<?>> answerInfo = new ArrayList<>();
+    public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
+        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.
+
+        // Use LinkedHashSet for preserving the insert order of the RRs, so that RRs of the same
+        // service or host are grouped together (which is more developer-friendly).
+        final Set<RecordInfo<?>> answerInfo = new LinkedHashSet<>();
+        final Set<RecordInfo<?>> additionalAnswerInfo = new LinkedHashSet<>();
+        // Reply unicast if the feature is enabled AND all replied questions request unicast
+        final boolean replyUnicastEnabled = mMdnsFeatureFlags.isUnicastReplyEnabled();
+        boolean replyUnicast = replyUnicastEnabled;
         for (MdnsRecord question : packet.questions) {
             // Add answers from general records
-            addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
-                    null /* serviceSrvRecord */, null /* serviceTxtRecord */, replyUnicast, now,
-                    answerInfo, additionalAnswerRecords);
+            if (addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
+                    null /* serviceSrvRecord */, null /* serviceTxtRecord */,
+                    null /* hostname */,
+                    replyUnicastEnabled, now, answerInfo, additionalAnswerInfo,
+                    Collections.emptyList(), isQuestionOnIpv4)) {
+                replyUnicast &= question.isUnicastReplyRequested();
+            }
 
             // Add answers from each service
             for (int i = 0; i < mServices.size(); i++) {
                 final ServiceRegistration registration = mServices.valueAt(i);
-                if (registration.exiting) continue;
-                addReplyFromService(question, registration.allRecords, registration.ptrRecords,
-                        registration.srvRecord, registration.txtRecord, replyUnicast, now,
-                        answerInfo, additionalAnswerRecords);
+                if (registration.exiting || registration.isProbing) continue;
+                if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
+                        registration.srvRecord, registration.txtRecord,
+                        registration.serviceInfo.getHostname(),
+                        replyUnicastEnabled, now,
+                        answerInfo, additionalAnswerInfo, packet.answers, isQuestionOnIpv4)) {
+                    replyUnicast &= question.isUnicastReplyRequested();
+                    registration.repliedServiceCount++;
+                    registration.sentPacketCount++;
+                }
             }
         }
 
+        // If any record was already in the answer section, remove it from the additional answer
+        // section. This can typically happen when there are both queries for
+        // SRV / TXT / A / AAAA and PTR (which can cause SRV / TXT / A / AAAA records being added
+        // to the additional answer section).
+        additionalAnswerInfo.removeAll(answerInfo);
+
+        final List<MdnsRecord> additionalAnswerRecords =
+                new ArrayList<>(additionalAnswerInfo.size());
+        for (RecordInfo<?> info : additionalAnswerInfo) {
+            // Different RecordInfos may contain the same record.
+            // For example, when there are multiple services referring to the same custom host,
+            // there are multiple RecordInfos containing the same address record.
+            if (!additionalAnswerRecords.contains(info.record)) {
+                additionalAnswerRecords.add(info.record);
+            }
+        }
+
+        // RFC6762 6.1: negative responses
+        // "On receipt of a question for a particular name, rrtype, and rrclass, for which a
+        // responder does have one or more unique answers, the responder MAY also include an NSEC
+        // record in the Additional Record Section indicating the nonexistence of other rrtypes
+        // for that name and rrclass."
+        addNsecRecordsForUniqueNames(additionalAnswerRecords,
+                answerInfo.iterator(), additionalAnswerInfo.iterator());
+
         if (answerInfo.size() == 0 && additionalAnswerRecords.size() == 0) {
-            return null;
+            // RFC6762 7.2. Multipacket Known-Answer Suppression
+            // Sometimes a Multicast DNS querier will already have too many answers
+            // to fit in the Known-Answer Section of its query packets. In this
+            // case, it should issue a Multicast DNS query containing a question and
+            // as many Known-Answer records as will fit.  It MUST then set the TC
+            // (Truncated) bit in the header before sending the query.  It MUST
+            // immediately follow the packet with another query packet containing no
+            // questions and as many more Known-Answer records as will fit.  If
+            // there are still too many records remaining to fit in the packet, it
+            // again sets the TC bit and continues until all the Known-Answer
+            // records have been sent.
+            if (!isTruncatedKnownAnswerPacket(packet)) {
+                return null;
+            }
         }
 
         // Determine the send delay
@@ -501,11 +752,17 @@
         // Determine the send destination
         final InetSocketAddress dest;
         if (replyUnicast) {
+            // As per RFC6762 5.4, "if the responder has not multicast that record recently (within
+            // one quarter of its TTL), then the responder SHOULD instead multicast the response so
+            // as to keep all the peer caches up to date": this SHOULD is not implemented to
+            // minimize latency for queriers who have just started, so they did not receive previous
+            // 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) {
-            dest = IPV4_ADDR;
+        } else if (isQuestionOnIpv4) {
+            dest = IPV4_SOCKET_ADDR;
         } else {
-            dest = IPV6_ADDR;
+            dest = IPV6_SOCKET_ADDR;
         }
 
         // Build the list of answer records from their RecordInfo
@@ -514,38 +771,59 @@
             // 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;
+                }
             }
-            answerRecords.add(info.record);
+            // Different RecordInfos may the contain the same record
+            if (!answerRecords.contains(info.record)) {
+                answerRecords.add(info.record);
+            }
         }
 
-        return new ReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
+        return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest, src,
+                new ArrayList<>(packet.answers));
+    }
+
+    private boolean isKnownAnswer(MdnsRecord answer, @NonNull List<MdnsRecord> knownAnswerRecords) {
+        for (MdnsRecord knownAnswer : knownAnswerRecords) {
+            if (answer.equals(knownAnswer) && knownAnswer.getTtl() > (answer.getTtl() / 2)) {
+                return true;
+            }
+        }
+        return false;
     }
 
     /**
      * Add answers and additional answers for a question, from a ServiceRegistration.
      */
-    private void addReplyFromService(@NonNull MdnsRecord question,
+    private boolean addReplyFromService(@NonNull MdnsRecord question,
             @NonNull List<RecordInfo<?>> serviceRecords,
             @Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
             @Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
             @Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
-            boolean replyUnicast, long now, @NonNull List<RecordInfo<?>> answerInfo,
-            @NonNull List<MdnsRecord> additionalAnswerRecords) {
+            @Nullable String hostname,
+            boolean replyUnicastEnabled, long now, @NonNull Set<RecordInfo<?>> answerInfo,
+            @NonNull Set<RecordInfo<?>> additionalAnswerInfo,
+            @NonNull List<MdnsRecord> knownAnswerRecords,
+            boolean isQuestionOnIpv4) {
         boolean hasDnsSdPtrRecordAnswer = false;
         boolean hasDnsSdSrvRecordAnswer = false;
         boolean hasFullyOwnedNameMatch = false;
         boolean hasKnownAnswer = false;
 
-        final int answersStartIndex = answerInfo.size();
+        final int answersStartSize = answerInfo.size();
         for (RecordInfo<?> info : serviceRecords) {
-            if (info.isProbing) continue;
 
              /* RFC6762 6.: the record name must match the question name, the record rrtype
              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 (!Arrays.equals(info.record.getName(), question.getName())) continue;
+            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
+                continue;
+            }
             hasFullyOwnedNameMatch |= !info.isSharedName;
 
             // The repository does not store CNAME records
@@ -559,19 +837,42 @@
             }
 
             hasKnownAnswer = true;
+
+            // RFC6762 7.1. Known-Answer Suppression:
+            // A Multicast DNS responder MUST NOT answer a Multicast DNS query if
+            // the answer it would give is already included in the Answer Section
+            // with an RR TTL at least half the correct value.  If the RR TTL of the
+            // answer as given in the Answer Section is less than half of the true
+            // RR TTL as known by the Multicast DNS responder, the responder MUST
+            // send an answer so as to update the querier's cache before the record
+            // becomes in danger of expiration.
+            if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
+                    && isKnownAnswer(info.record, knownAnswerRecords)) {
+                continue;
+            }
+
             hasDnsSdPtrRecordAnswer |= (servicePtrRecords != null
                     && CollectionUtils.any(servicePtrRecords, r -> info == r));
             hasDnsSdSrvRecordAnswer |= (info == serviceSrvRecord);
 
             // 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 (!replyUnicast && 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;
+                    }
+                }
             }
 
-            // TODO: Don't reply if in known answers of the querier (7.1) if TTL is > half
-
             answerInfo.add(info);
         }
 
@@ -580,21 +881,22 @@
         // ownership, for a type for which that name has no records, the responder MUST [...]
         // respond asserting the nonexistence of that record"
         if (hasFullyOwnedNameMatch && !hasKnownAnswer) {
-            additionalAnswerRecords.add(new MdnsNsecRecord(
+            MdnsNsecRecord nsecRecord = new MdnsNsecRecord(
                     question.getName(),
                     0L /* receiptTimeMillis */,
                     true /* cacheFlush */,
                     // TODO: RFC6762 6.1: "In general, the TTL given for an NSEC record SHOULD
                     // be the same as the TTL that the record would have had, had it existed."
-                    NAME_RECORDS_TTL_MILLIS,
+                    DEFAULT_NAME_RECORDS_TTL_MILLIS,
                     question.getName(),
-                    new int[] { question.getType() }));
+                    new int[] { question.getType() });
+            additionalAnswerInfo.add(
+                    new RecordInfo<>(null /* serviceInfo */, nsecRecord, false /* isSharedName */));
         }
 
         // No more records to add if no answer
-        if (answerInfo.size() == answersStartIndex) return;
+        if (answerInfo.size() == answersStartSize) return false;
 
-        final List<RecordInfo<?>> additionalAnswerInfo = new ArrayList<>();
         // RFC6763 12.1: if including PTR record, include the SRV and TXT records it names
         if (hasDnsSdPtrRecordAnswer) {
             if (serviceTxtRecord != null) {
@@ -607,21 +909,9 @@
 
         // RFC6763 12.1&.2: if including PTR or SRV record, include the address records it names
         if (hasDnsSdPtrRecordAnswer || hasDnsSdSrvRecordAnswer) {
-            for (RecordInfo<?> record : mGeneralRecords) {
-                if (record.record instanceof MdnsInetAddressRecord) {
-                    additionalAnswerInfo.add(record);
-                }
-            }
+            additionalAnswerInfo.addAll(getInetAddressRecordsForHostname(hostname));
         }
-
-        for (RecordInfo<?> info : additionalAnswerInfo) {
-            additionalAnswerRecords.add(info.record);
-        }
-
-        // RFC6762 6.1: negative responses
-        addNsecRecordsForUniqueNames(additionalAnswerRecords,
-                answerInfo.listIterator(answersStartIndex),
-                additionalAnswerInfo.listIterator());
+        return true;
     }
 
     /**
@@ -637,7 +927,7 @@
      *                      answer and additionalAnswer sections)
      */
     @SafeVarargs
-    private static void addNsecRecordsForUniqueNames(
+    private void addNsecRecordsForUniqueNames(
             List<MdnsRecord> destinationList,
             Iterator<RecordInfo<?>>... answerRecords) {
         // Group unique records by name. Use a TreeMap with comparator as arrays don't implement
@@ -653,6 +943,12 @@
 
         for (String[] nsecName : namesInAddedOrder) {
             final List<MdnsRecord> entryRecords = nsecByName.get(nsecName);
+
+            // Add NSEC records only when the answers include all unique records of this name
+            if (entryRecords.size() != countUniqueRecords(nsecName)) {
+                continue;
+            }
+
             long minTtl = Long.MAX_VALUE;
             final Set<Integer> types = new ArraySet<>(entryRecords.size());
             for (MdnsRecord record : entryRecords) {
@@ -670,6 +966,27 @@
         }
     }
 
+    /** Returns the number of unique records on this device for a given {@code name}. */
+    private int countUniqueRecords(String[] name) {
+        int cnt = countUniqueRecords(mGeneralRecords, name);
+
+        for (int i = 0; i < mServices.size(); i++) {
+            final ServiceRegistration registration = mServices.valueAt(i);
+            cnt += countUniqueRecords(registration.allRecords, name);
+        }
+        return cnt;
+    }
+
+    private static int countUniqueRecords(List<RecordInfo<?>> records, String[] name) {
+        int cnt = 0;
+        for (RecordInfo<?> record : records) {
+            if (!record.isSharedName && Arrays.equals(name, record.record.getName())) {
+                cnt++;
+            }
+        }
+        return cnt;
+    }
+
     /**
      * Add non-shared records to a map listing them by record name, and to a list of names that
      * remembers the adding order.
@@ -684,10 +1001,10 @@
     private static void addNonSharedRecordsToMap(
             Iterator<RecordInfo<?>> records,
             Map<String[], List<MdnsRecord>> dest,
-            List<String[]> namesInAddedOrder) {
+            @Nullable List<String[]> namesInAddedOrder) {
         while (records.hasNext()) {
             final RecordInfo<?> record = records.next();
-            if (record.isSharedName) continue;
+            if (record.isSharedName || record.record instanceof MdnsNsecRecord) continue;
             final List<MdnsRecord> recordsForName = dest.computeIfAbsent(record.record.name,
                     key -> {
                         namesInAddedOrder.add(key);
@@ -697,89 +1014,349 @@
         }
     }
 
+    @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 {
-
-        final ServiceRegistration registration = mServices.get(probeSuccessInfo.getServiceId());
-        if (registration == null) throw new IOException(
-                "Service is not registered: " + 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);
 
-        final ArrayList<MdnsRecord> answers = new ArrayList<>();
+        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<>();
 
-        // Interface address records in general records
-        for (RecordInfo<?> record : mGeneralRecords) {
-            answers.add(record.record);
+        // When using default host, add interface address records from general records
+        if (TextUtils.isEmpty(registration.serviceInfo.getHostname())) {
+            for (RecordInfo<?> record : mGeneralRecords) {
+                answersSet.add(record.record);
+            }
+        } else {
+            // TODO: b/321617573 - include PTR records for addresses
+            // The custom host may have more addresses in other registrations
+            forEachActiveServiceRegistrationWithHostname(
+                    registration.serviceInfo.getHostname(),
+                    (id, otherRegistration) -> {
+                        if (otherRegistration.isProbing) {
+                            return;
+                        }
+                        for (RecordInfo<?> addressRecordInfo : otherRegistration.addressRecords) {
+                            answersSet.add(addressRecordInfo.record);
+                        }
+                    });
         }
 
         // All service records
         for (RecordInfo<?> info : registration.allRecords) {
-            answers.add(info.record);
+            answersSet.add(info.record);
         }
 
         addNsecRecordsForUniqueNames(additionalAnswers,
                 mGeneralRecords.iterator(), registration.allRecords.iterator());
 
-        return new MdnsAnnouncer.AnnouncementInfo(probeSuccessInfo.getServiceId(),
-                answers, additionalAnswers);
+        return new MdnsAnnouncer.AnnouncementInfo(serviceId,
+                new ArrayList<>(answersSet), additionalAnswers);
+    }
+
+    /**
+     * Gets the offload MdnsPacket.
+     * @param serviceId The serviceId.
+     * @return The offload {@link MdnsPacket} that contains PTR/SRV/TXT/A/AAAA records.
+     */
+    public MdnsPacket getOffloadPacket(int serviceId) throws IllegalArgumentException {
+        final ServiceRegistration registration = mServices.get(serviceId);
+        if (registration == null) throw new IllegalArgumentException(
+                "Service is not registered: " + serviceId);
+
+        final ArrayList<MdnsRecord> answers = new ArrayList<>();
+
+        // Adds all PTR, SRV, TXT, A/AAAA records.
+        for (RecordInfo<MdnsPointerRecord> ptrRecord : registration.ptrRecords) {
+            answers.add(ptrRecord.record);
+        }
+        if (registration.srvRecord != null) {
+            answers.add(registration.srvRecord.record);
+        }
+        if (registration.txtRecord != null) {
+            answers.add(registration.txtRecord.record);
+        }
+        // TODO: Support custom host. It currently only supports default host.
+        for (RecordInfo<?> record : mGeneralRecords) {
+            if (record.record instanceof MdnsInetAddressRecord) {
+                answers.add(record.record);
+            }
+        }
+
+        final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
+        return new MdnsPacket(flags,
+                Collections.emptyList() /* questions */,
+                answers,
+                Collections.emptyList() /* authorityRecords */,
+                Collections.emptyList() /* additionalRecords */);
+    }
+
+    /** 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;
     }
 
     /**
      * Get the service IDs of services conflicting with a received packet.
+     *
+     * <p>It returns a Map of service ID => conflict type. Conflict type is a bitmap telling which
+     * part of the service is conflicting. See {@link MdnsInterfaceAdvertiser#CONFLICT_SERVICE} and
+     * {@link MdnsInterfaceAdvertiser#CONFLICT_HOST}.
      */
-    public Set<Integer> getConflictingServices(MdnsPacket packet) {
-        // Avoid allocating a new set for each incoming packet: use an empty set by default.
-        Set<Integer> conflicting = Collections.emptySet();
+    public Map<Integer, Integer> getConflictingServices(MdnsPacket packet) {
+        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;
 
-                // Only look for conflicts in service name, as a different service name can be used
-                // if there is a conflict, but there is nothing actionable if any other conflict
-                // happens. In fact probing is only done for the service name in the SRV record.
-                // This means only SRV and TXT records need to be checked.
-                final RecordInfo<MdnsServiceRecord> srvRecord = registration.srvRecord;
-                if (!Arrays.equals(record.getName(), srvRecord.record.getName())) continue;
+                final RecordConflictType conflictForService =
+                        conflictForService(record, registration);
+                final RecordConflictType conflictForHost = conflictForHost(record, registration);
 
-                // 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)) {
-                        continue;
-                    }
+                // 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 (record instanceof MdnsTextRecord) {
-                    final MdnsTextRecord local = registration.txtRecord.record;
-                    final MdnsTextRecord other = (MdnsTextRecord) record;
-                    if (Objects.equals(local, other)) {
-                        continue;
-                    }
+                int conflictType = 0;
+                if (conflictForService == RecordConflictType.CONFLICT) {
+                    conflictType |= CONFLICT_SERVICE;
+                }
+                if (conflictForHost == RecordConflictType.CONFLICT) {
+                    conflictType |= CONFLICT_HOST;
                 }
 
-                if (conflicting.size() == 0) {
-                    // Conflict was found: use a mutable set
-                    conflicting = new ArraySet<>();
+                if (conflictType != 0) {
+                    final int serviceId = mServices.keyAt(i);
+                    conflictingWithRecord.put(serviceId, conflictType);
                 }
-                final int serviceId = mServices.keyAt(i);
-                conflicting.add(serviceId);
+            }
+            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 RecordConflictType conflictForService(
+            @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
+        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;
+        }
+
+        if (!MdnsUtils.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 && 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;
+        }
+
+        return RecordConflictType.CONFLICT;
+    }
+
+    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 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 RecordConflictType.NO_CONFLICT;
+        }
+
+        // Different names. There won't be a conflict.
+        if (!MdnsUtils.equalsIgnoreDnsCase(
+                record.getName()[0], registration.serviceInfo.getHostname())) {
+            return RecordConflictType.NO_CONFLICT;
+        }
+
+        // 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;
+        }
+
+        // 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(
+            @Nullable String hostname) {
+        List<RecordInfo<MdnsInetAddressRecord>> records = new ArrayList<>();
+        if (TextUtils.isEmpty(hostname)) {
+            forEachAddressRecord(mGeneralRecords, records::add);
+        } else {
+            forEachActiveServiceRegistrationWithHostname(
+                    hostname,
+                    (id, service) -> {
+                        if (service.isProbing) return;
+                        records.addAll(service.addressRecords);
+                    });
+        }
+        return records;
+    }
+
+    private List<MdnsInetAddressRecord> makeProbingInetAddressRecords(
+            @NonNull NsdServiceInfo serviceInfo) {
+        final List<MdnsInetAddressRecord> records = new ArrayList<>();
+        if (TextUtils.isEmpty(serviceInfo.getHostname())) {
+            if (mMdnsFeatureFlags.mIncludeInetAddressRecordsInProbing) {
+                forEachAddressRecord(mGeneralRecords, r -> records.add(r.record));
+            }
+        } else {
+            forEachActiveServiceRegistrationWithHostname(
+                    serviceInfo.getHostname(),
+                    (id, service) -> {
+                        for (RecordInfo<MdnsInetAddressRecord> recordInfo :
+                                service.addressRecords) {
+                            records.add(recordInfo.record);
+                        }
+                    });
+        }
+        return records;
+    }
+
+    private static void forEachAddressRecord(
+            List<RecordInfo<?>> records, Consumer<RecordInfo<MdnsInetAddressRecord>> consumer) {
+        for (RecordInfo<?> record : records) {
+            if (record.record instanceof MdnsInetAddressRecord) {
+                consumer.accept((RecordInfo<MdnsInetAddressRecord>) record);
+            }
+        }
+    }
+
+    private void forEachActiveServiceRegistrationWithHostname(
+            @NonNull String hostname, BiConsumer<Integer, ServiceRegistration> consumer) {
+        for (int i = 0; i < mServices.size(); ++i) {
+            int id = mServices.keyAt(i);
+            ServiceRegistration service = mServices.valueAt(i);
+            if (service.exiting) continue;
+            if (MdnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
+                consumer.accept(id, service);
+            }
+        }
+    }
+
     /**
      * (Re)set a service to the probing state.
      * @return The {@link MdnsProber.ProbingInfo} to send for probing.
@@ -790,7 +1367,8 @@
         if (registration == null) return null;
 
         registration.setProbing(true);
-        return makeProbingInfo(serviceId, registration.srvRecord.record);
+
+        return makeProbingInfo(serviceId, registration);
     }
 
     /**
@@ -800,7 +1378,7 @@
         final ServiceRegistration registration = mServices.get(serviceId);
         if (registration == null) return false;
 
-        return registration.srvRecord.isProbing;
+        return registration.isProbing;
     }
 
     /**
@@ -823,27 +1401,39 @@
         final ServiceRegistration existing = mServices.get(serviceId);
         if (existing == null) return null;
 
-        final ServiceRegistration newService = new ServiceRegistration(
-                mDeviceHostname, newInfo, existing.subtype);
+        final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
+                existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
         mServices.put(serviceId, newService);
-        return makeProbingInfo(serviceId, newService.srvRecord.record);
+        return makeProbingInfo(serviceId, newService);
     }
 
     /**
      * Called when {@link MdnsAdvertiser} sent an advertisement for the given service.
      */
-    public void onAdvertisementSent(int serviceId) {
+    public void onAdvertisementSent(int serviceId, int sentPacketCount) {
         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;
     }
 
     /**
+     * Called when {@link MdnsAdvertiser} sent a probing for the given service.
+     */
+    public void onProbingSent(int serviceId, int sentPacketCount) {
+        final ServiceRegistration registration = mServices.get(serviceId);
+        if (registration == null) return;
+        registration.sentPacketCount += sentPacketCount;
+    }
+
+
+    /**
      * Compute:
      * 2001:db8::1 --> 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa
      *
@@ -893,4 +1483,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/MdnsReplyInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
new file mode 100644
index 0000000..8747f67
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
@@ -0,0 +1,63 @@
+/*
+ * 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.connectivity.mdns;
+
+import android.annotation.NonNull;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+
+/**
+ * Info about a mDNS reply to be sent.
+ */
+public final class MdnsReplyInfo {
+    @NonNull
+    public final List<MdnsRecord> answers;
+    @NonNull
+    public final List<MdnsRecord> additionalAnswers;
+    public final long sendDelayMs;
+    @NonNull
+    public final InetSocketAddress destination;
+    @NonNull
+    public final InetSocketAddress source;
+    @NonNull
+    public final List<MdnsRecord> knownAnswers;
+
+    public MdnsReplyInfo(
+            @NonNull List<MdnsRecord> answers,
+            @NonNull List<MdnsRecord> additionalAnswers,
+            long sendDelayMs,
+            @NonNull InetSocketAddress destination,
+            @NonNull InetSocketAddress source,
+            @NonNull List<MdnsRecord> knownAnswers) {
+        this.answers = answers;
+        this.additionalAnswers = additionalAnswers;
+        this.sendDelayMs = sendDelayMs;
+        this.destination = destination;
+        this.source = source;
+        this.knownAnswers = knownAnswers;
+    }
+
+    @Override
+    public String toString() {
+        return "{MdnsReplyInfo: " + source + " to " + destination
+                + ", answers: " + answers.size()
+                + ", additionalAnswers: " + additionalAnswers.size()
+                + ", knownAnswers: " + knownAnswers.size()
+                + ", sendDelayMs " + sendDelayMs + "}";
+    }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index 8bc598d..db3845a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -16,15 +16,22 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
 
 import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.util.Log;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 
-import com.android.server.connectivity.mdns.MdnsRecordRepository.ReplyInfo;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -32,7 +39,10 @@
 import java.net.Inet6Address;
 import java.net.InetSocketAddress;
 import java.net.MulticastSocket;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * A class that handles sending mDNS replies to a {@link MulticastSocket}, possibly queueing them
@@ -40,36 +50,150 @@
  *
  * TODO: implement sending after a delay, combining queued replies and duplicate answer suppression
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsReplySender {
-    private static final boolean DBG = MdnsAdvertiser.DBG;
     private static final int MSG_SEND = 1;
+    private static final int PACKET_NOT_SENT = 0;
+    private static final int PACKET_SENT = 1;
 
-    private final String mLogTag;
     @NonNull
     private final MdnsInterfaceSocket mSocket;
     @NonNull
     private final Handler mHandler;
     @NonNull
     private final byte[] mPacketCreationBuffer;
+    @NonNull
+    private final SharedLog mSharedLog;
+    private final boolean mEnableDebugLog;
+    @NonNull
+    private final Dependencies mDependencies;
+    // RFC6762 15.2. Multipacket Known-Answer lists
+    // Multicast DNS responders associate the initial truncated query with its
+    // continuation packets by examining the source IP address in each packet.
+    private final Map<InetSocketAddress, MdnsReplyInfo> mSrcReplies = new ArrayMap<>();
+    @NonNull
+    private final MdnsFeatureFlags mMdnsFeatureFlags;
 
-    public MdnsReplySender(@NonNull String interfaceTag, @NonNull Looper looper,
-            @NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer) {
+    /**
+     * Dependencies of MdnsReplySender, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * @see Handler#sendMessageDelayed(Message, long)
+         */
+        public void sendMessageDelayed(@NonNull Handler handler, @NonNull Message message,
+                long delayMillis) {
+            handler.sendMessageDelayed(message, delayMillis);
+        }
+
+        /**
+         * @see Handler#removeMessages(int)
+         */
+        public void removeMessages(@NonNull Handler handler, int what) {
+            handler.removeMessages(what);
+        }
+
+        /**
+         * @see Handler#removeMessages(int)
+         */
+        public void removeMessages(@NonNull Handler handler, int what, @NonNull Object object) {
+            handler.removeMessages(what, object);
+        }
+    }
+
+    public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
+            @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
+            boolean enableDebugLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+        this(looper, socket, packetCreationBuffer, sharedLog, enableDebugLog, new Dependencies(),
+                mdnsFeatureFlags);
+    }
+
+    @VisibleForTesting
+    public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
+            @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
+            boolean enableDebugLog, @NonNull Dependencies dependencies,
+            @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
         mHandler = new SendHandler(looper);
-        mLogTag = MdnsReplySender.class.getSimpleName() + "/" +  interfaceTag;
         mSocket = socket;
         mPacketCreationBuffer = packetCreationBuffer;
+        mSharedLog = sharedLog;
+        mEnableDebugLog = enableDebugLog;
+        mDependencies = dependencies;
+        mMdnsFeatureFlags = mdnsFeatureFlags;
+    }
+
+    static InetSocketAddress getReplyDestination(@NonNull InetSocketAddress queuingDest,
+            @NonNull InetSocketAddress incomingDest) {
+        // The queuing reply is multicast, just use the current destination.
+        if (queuingDest.equals(IPV4_SOCKET_ADDR) || queuingDest.equals(IPV6_SOCKET_ADDR)) {
+            return queuingDest;
+        }
+
+        // The incoming reply is multicast, change the reply from unicast to multicast since
+        // replying unicast when the query requests unicast reply is optional.
+        if (incomingDest.equals(IPV4_SOCKET_ADDR) || incomingDest.equals(IPV6_SOCKET_ADDR)) {
+            return incomingDest;
+        }
+
+        return queuingDest;
     }
 
     /**
      * Queue a reply to be sent when its send delay expires.
      */
-    public void queueReply(@NonNull ReplyInfo reply) {
+    public void queueReply(@NonNull MdnsReplyInfo reply) {
         ensureRunningOnHandlerThread(mHandler);
-        // TODO: implement response aggregation (RFC 6762 6.4)
-        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
 
-        if (DBG) {
-            Log.v(mLogTag, "Scheduling " + reply);
+        if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()) {
+            mDependencies.removeMessages(mHandler, MSG_SEND, reply.source);
+
+            final MdnsReplyInfo queuingReply = mSrcReplies.remove(reply.source);
+            final ArraySet<MdnsRecord> answers = new ArraySet<>();
+            final Set<MdnsRecord> additionalAnswers = new ArraySet<>();
+            final Set<MdnsRecord> knownAnswers = new ArraySet<>();
+            if (queuingReply != null) {
+                answers.addAll(queuingReply.answers);
+                additionalAnswers.addAll(queuingReply.additionalAnswers);
+                knownAnswers.addAll(queuingReply.knownAnswers);
+            }
+            answers.addAll(reply.answers);
+            additionalAnswers.addAll(reply.additionalAnswers);
+            knownAnswers.addAll(reply.knownAnswers);
+            // RFC6762 7.2. Multipacket Known-Answer Suppression
+            // If the responder sees any of its answers listed in the Known-Answer
+            // lists of subsequent packets from the querying host, it MUST delete
+            // that answer from the list of answers it is planning to give.
+            for (MdnsRecord knownAnswer : knownAnswers) {
+                final int idx = answers.indexOf(knownAnswer);
+                if (idx >= 0 && knownAnswer.getTtl() > answers.valueAt(idx).getTtl() / 2) {
+                    answers.removeAt(idx);
+                }
+            }
+
+            if (answers.size() == 0) {
+                return;
+            }
+
+            final MdnsReplyInfo newReply = new MdnsReplyInfo(
+                    new ArrayList<>(answers),
+                    new ArrayList<>(additionalAnswers),
+                    reply.sendDelayMs,
+                    queuingReply == null ? reply.destination
+                            : getReplyDestination(queuingReply.destination, reply.destination),
+                    reply.source,
+                    new ArrayList<>(knownAnswers));
+
+            mSrcReplies.put(newReply.source, newReply);
+            mDependencies.sendMessageDelayed(mHandler,
+                    mHandler.obtainMessage(MSG_SEND, newReply.source), newReply.sendDelayMs);
+        } else {
+            mDependencies.sendMessageDelayed(
+                    mHandler, mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
+        }
+
+        if (mEnableDebugLog) {
+            mSharedLog.v("Scheduling " + reply);
         }
     }
 
@@ -78,44 +202,17 @@
      *
      * Must be called on the looper thread used by the {@link MdnsReplySender}.
      */
-    public void sendNow(@NonNull MdnsPacket packet, @NonNull InetSocketAddress destination)
+    public int sendNow(@NonNull MdnsPacket packet, @NonNull InetSocketAddress destination)
             throws IOException {
         ensureRunningOnHandlerThread(mHandler);
         if (!((destination.getAddress() instanceof Inet6Address && mSocket.hasJoinedIpv6())
                 || (destination.getAddress() instanceof Inet4Address && mSocket.hasJoinedIpv4()))) {
             // Skip sending if the socket has not joined the v4/v6 group (there was no address)
-            return;
+            return PACKET_NOT_SENT;
         }
-
-        // TODO: support packets over size (send in multiple packets with TC bit set)
-        final MdnsPacketWriter writer = new MdnsPacketWriter(mPacketCreationBuffer);
-
-        writer.writeUInt16(0); // Transaction ID (advertisement: 0)
-        writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4)
-        writer.writeUInt16(packet.questions.size()); // questions count
-        writer.writeUInt16(packet.answers.size()); // answers count
-        writer.writeUInt16(packet.authorityRecords.size()); // authority entries count
-        writer.writeUInt16(packet.additionalRecords.size()); // additional records count
-
-        for (MdnsRecord record : packet.questions) {
-            // Questions do not have TTL or data
-            record.writeHeaderFields(writer);
-        }
-        for (MdnsRecord record : packet.answers) {
-            record.write(writer, 0L);
-        }
-        for (MdnsRecord record : packet.authorityRecords) {
-            record.write(writer, 0L);
-        }
-        for (MdnsRecord record : packet.additionalRecords) {
-            record.write(writer, 0L);
-        }
-
-        final int len = writer.getWritePosition();
-        final byte[] outBuffer = new byte[len];
-        System.arraycopy(mPacketCreationBuffer, 0, outBuffer, 0, len);
-
-        mSocket.send(new DatagramPacket(outBuffer, 0, len, destination));
+        final byte[] outBuffer = MdnsUtils.createRawDnsPacket(mPacketCreationBuffer, packet);
+        mSocket.send(new DatagramPacket(outBuffer, 0, outBuffer.length, destination));
+        return PACKET_SENT;
     }
 
     /**
@@ -123,7 +220,7 @@
      */
     public void cancelAll() {
         ensureRunningOnHandlerThread(mHandler);
-        mHandler.removeMessages(MSG_SEND);
+        mDependencies.removeMessages(mHandler, MSG_SEND);
     }
 
     private class SendHandler extends Handler {
@@ -133,8 +230,22 @@
 
         @Override
         public void handleMessage(@NonNull Message msg) {
-            final ReplyInfo replyInfo = (ReplyInfo) msg.obj;
-            if (DBG) Log.v(mLogTag, "Sending " + replyInfo);
+            final MdnsReplyInfo replyInfo;
+            if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()) {
+                // Retrieve the MdnsReplyInfo from the map via a source address, as the reply info
+                // will be combined or updated.
+                final InetSocketAddress source = (InetSocketAddress) msg.obj;
+                replyInfo = mSrcReplies.remove(source);
+            } else {
+                replyInfo = (MdnsReplyInfo) msg.obj;
+            }
+
+            if (replyInfo == null) {
+                mSharedLog.wtf("Unknown reply info.");
+                return;
+            }
+
+            if (mEnableDebugLog) mSharedLog.v("Sending " + replyInfo);
 
             final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
             final MdnsPacket packet = new MdnsPacket(flags,
@@ -146,7 +257,7 @@
             try {
                 sendNow(packet, replyInfo.destination);
             } catch (IOException e) {
-                Log.e(mLogTag, "Error sending MDNS response", e);
+                mSharedLog.e("Error sending MDNS response", e);
             }
         }
     }
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 e765d53..05ad1be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -21,10 +21,10 @@
 import android.net.Network;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -33,6 +33,7 @@
 
 /** An mDNS response. */
 public class MdnsResponse {
+    public static final long EXPIRATION_NEVER = Long.MAX_VALUE;
     private final List<MdnsRecord> records;
     private final List<MdnsPointerRecord> pointerRecords;
     private MdnsServiceRecord serviceRecord;
@@ -110,7 +111,7 @@
      * pointer record is already present in the response with the same TTL.
      */
     public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
-        if (!Arrays.equals(serviceName, pointerRecord.getPointer())) {
+        if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
             throw new IllegalArgumentException(
                     "Pointer records for different service names cannot be added");
         }
@@ -304,13 +305,13 @@
         boolean dropAddressRecords = false;
 
         for (MdnsInetAddressRecord inetAddressRecord : getInet4AddressRecords()) {
-            if (!Arrays.equals(
+            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
                     this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
                 dropAddressRecords = true;
             }
         }
         for (MdnsInetAddressRecord inetAddressRecord : getInet6AddressRecords()) {
-            if (!Arrays.equals(
+            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
                     this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
                 dropAddressRecords = true;
             }
@@ -349,6 +350,21 @@
         return serviceName;
     }
 
+    /** Get the min remaining ttl time from received records */
+    public long getMinRemainingTtl(long now) {
+        long minRemainingTtl = EXPIRATION_NEVER;
+        // TODO: Check other records(A, AAAA, TXT) ttl time.
+        if (!hasServiceRecord()) {
+            return EXPIRATION_NEVER;
+        }
+        // Check ttl time.
+        long remainingTtl = serviceRecord.getRemainingTTL(now);
+        if (remainingTtl < minRemainingTtl) {
+            minRemainingTtl = remainingTtl;
+        }
+        return minRemainingTtl;
+    }
+
     /**
      * Tests if this response is a goodbye message. This will be true if a service record is present
      * and any of the records have a TTL of 0.
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 6648729..b812bb4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -19,31 +19,28 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.Network;
-import android.os.SystemClock;
 import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.Pair;
 
-import com.android.server.connectivity.mdns.util.MdnsLogger;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.EOFException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 
 /** A class that decodes mDNS responses from UDP packets. */
 public class MdnsResponseDecoder {
     public static final int SUCCESS = 0;
     private static final String TAG = "MdnsResponseDecoder";
-    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
     private final boolean allowMultipleSrvRecordsPerHost =
             MdnsConfigs.allowMultipleSrvRecordsPerHost();
     @Nullable private final String[] serviceType;
-    private final Clock clock;
+    private final MdnsUtils.Clock clock;
 
     /** Constructs a new decoder that will extract responses for the given service type. */
-    public MdnsResponseDecoder(@NonNull Clock clock, @Nullable String[] serviceType) {
+    public MdnsResponseDecoder(@NonNull MdnsUtils.Clock clock, @Nullable String[] serviceType) {
         this.clock = clock;
         this.serviceType = serviceType;
     }
@@ -52,7 +49,7 @@
             List<MdnsResponse> responses, String[] pointer) {
         if (responses != null) {
             for (MdnsResponse response : responses) {
-                if (Arrays.equals(response.getServiceName(), pointer)) {
+                if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
                     return response;
                 }
             }
@@ -68,7 +65,8 @@
                 if (serviceRecord == null) {
                     continue;
                 }
-                if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
+                if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
+                        hostName)) {
                     return response;
                 }
             }
@@ -86,20 +84,20 @@
      * @throws MdnsPacket.ParseException if a response packet could not be parsed.
      */
     @NonNull
-    public static MdnsPacket parseResponse(@NonNull byte[] recvbuf, int length)
-            throws MdnsPacket.ParseException {
-        MdnsPacketReader reader = new MdnsPacketReader(recvbuf, length);
+    public static MdnsPacket parseResponse(@NonNull byte[] recvbuf, int length,
+            @NonNull MdnsFeatureFlags mdnsFeatureFlags) throws MdnsPacket.ParseException {
+        final MdnsPacketReader reader = new MdnsPacketReader(recvbuf, length, mdnsFeatureFlags);
 
         final MdnsPacket mdnsPacket;
         try {
-            reader.readUInt16(); // transaction ID (not used)
+            final int transactionId = reader.readUInt16();
             int flags = reader.readUInt16();
             if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
                 throw new MdnsPacket.ParseException(
                         MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE, "Not a response", null);
             }
 
-            mdnsPacket = MdnsPacket.parseRecordsSection(reader, flags);
+            mdnsPacket = MdnsPacket.parseRecordsSection(reader, flags, transactionId);
             if (mdnsPacket.answers.size() < 1) {
                 throw new MdnsPacket.ParseException(
                         MdnsResponseErrorCode.ERROR_NO_ANSWERS, "Response has no answers",
@@ -127,7 +125,7 @@
      *                     2) A copy of the original responses with some of them have records
      *                     update or only contains receive time updated.
      */
-    public Pair<ArraySet<MdnsResponse>, ArrayList<MdnsResponse>> augmentResponses(
+    public Pair<Set<MdnsResponse>, ArrayList<MdnsResponse>> augmentResponses(
             @NonNull MdnsPacket mdnsPacket,
             @NonNull Collection<MdnsResponse> existingResponses, int interfaceIndex,
             @Nullable Network network) {
@@ -138,7 +136,7 @@
         records.addAll(mdnsPacket.authorityRecords);
         records.addAll(mdnsPacket.additionalRecords);
 
-        final ArraySet<MdnsResponse> modified = new ArraySet<>();
+        final Set<MdnsResponse> modified = MdnsUtils.newSet();
         final ArrayList<MdnsResponse> responses = new ArrayList<>(existingResponses.size());
         final ArrayMap<MdnsResponse, MdnsResponse> augmentedToOriginal = new ArrayMap<>();
         for (MdnsResponse existing : existingResponses) {
@@ -174,11 +172,8 @@
         for (MdnsRecord record : records) {
             if (record instanceof MdnsPointerRecord) {
                 String[] name = record.getName();
-                if ((serviceType == null)
-                        || Arrays.equals(name, serviceType)
-                        || ((name.length == (serviceType.length + 2))
-                        && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
-                        && MdnsRecord.labelsAreSuffix(serviceType, name))) {
+                if ((serviceType == null) || MdnsUtils.typeEqualsOrIsSubtype(
+                        serviceType, name)) {
                     MdnsPointerRecord pointerRecord = (MdnsPointerRecord) record;
                     // Group PTR records that refer to the same service instance name into a single
                     // response.
@@ -323,7 +318,7 @@
             if (serviceRecord == null) {
                 continue;
             }
-            if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
+            if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
                 if (result == null) {
                     result = new ArrayList<>(/* initialCapacity= */ responses.size());
                 }
@@ -332,10 +327,4 @@
         }
         return result == null ? List.of() : result;
     }
-
-    public static class Clock {
-        public long elapsedRealtime() {
-            return SystemClock.elapsedRealtime();
-        }
-    }
 }
\ No newline at end of file
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 3da6bd0..73405ab 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -22,7 +22,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
-import android.util.ArraySet;
+
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -38,16 +39,30 @@
  * @hide
  */
 public class MdnsSearchOptions implements Parcelable {
+    // Passive query mode scans less frequently in order to conserve battery and produce less
+    // network traffic.
+    public static final int PASSIVE_QUERY_MODE = 0;
+    // Active query mode scans frequently.
+    public static final int ACTIVE_QUERY_MODE = 1;
+    // Aggressive query mode scans more frequently than the active mode at first, and sends both
+    // unicast and multicast queries simultaneously, but in long sessions it eventually sends as
+    // many queries as the PASSIVE mode.
+    public static final int AGGRESSIVE_QUERY_MODE = 2;
 
     /** @hide */
     public static final Parcelable.Creator<MdnsSearchOptions> CREATOR =
             new Parcelable.Creator<MdnsSearchOptions>() {
                 @Override
                 public MdnsSearchOptions createFromParcel(Parcel source) {
-                    return new MdnsSearchOptions(source.createStringArrayList(),
-                            source.readBoolean(), source.readBoolean(),
+                    return new MdnsSearchOptions(
+                            source.createStringArrayList(),
+                            source.readInt(),
+                            source.readInt() == 1,
                             source.readParcelable(null),
-                            source.readString());
+                            source.readInt(),
+                            source.readString(),
+                            source.readInt() == 1,
+                            source.readInt());
                 }
 
                 @Override
@@ -59,22 +74,35 @@
     private final List<String> subtypes;
     @Nullable
     private final String resolveInstanceName;
-
-    private final boolean isPassiveMode;
+    private final int queryMode;
+    private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
+    private final int numOfQueriesBeforeBackoff;
     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(List<String> subtypes, boolean isPassiveMode, boolean removeExpiredService,
-            @Nullable Network network, @Nullable String resolveInstanceName) {
+    MdnsSearchOptions(
+            List<String> subtypes,
+            int queryMode,
+            boolean removeExpiredService,
+            @Nullable Network network,
+            int interfaceIndex,
+            @Nullable String resolveInstanceName,
+            boolean onlyUseIpv6OnIpv6OnlyNetworks,
+            int numOfQueriesBeforeBackoff) {
         this.subtypes = new ArrayList<>();
         if (subtypes != null) {
             this.subtypes.addAll(subtypes);
         }
-        this.isPassiveMode = isPassiveMode;
+        this.queryMode = queryMode;
+        this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
+        this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
         this.removeExpiredService = removeExpiredService;
         mNetwork = network;
+        mInterfaceIndex = interfaceIndex;
         this.resolveInstanceName = resolveInstanceName;
     }
 
@@ -97,11 +125,26 @@
     }
 
     /**
-     * @return {@code true} if the passive mode is used. The passive mode scans less frequently in
-     * order to conserve battery and produce less network traffic.
+     * @return the current query mode.
      */
-    public boolean isPassiveMode() {
-        return isPassiveMode;
+    public int getQueryMode() {
+        return queryMode;
+    }
+
+    /**
+     * @return {@code true} if only the IPv4 mDNS host should be queried on network that supports
+     * both IPv6 as well as IPv4. On an IPv6-only network, this is ignored.
+     */
+    public boolean onlyUseIpv6OnIpv6OnlyNetworks() {
+        return onlyUseIpv6OnIpv6OnlyNetworks;
+    }
+
+    /**
+     *  Returns number of queries should be executed before backoff mode is enabled.
+     *  The default number is 3 if it is not set.
+     */
+    public int numOfQueriesBeforeBackoff() {
+        return numOfQueriesBeforeBackoff;
     }
 
     /** Returns {@code true} if service will be removed after its TTL expires. */
@@ -110,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.
@@ -136,22 +191,28 @@
     @Override
     public void writeToParcel(Parcel out, int flags) {
         out.writeStringList(subtypes);
-        out.writeBoolean(isPassiveMode);
-        out.writeBoolean(removeExpiredService);
+        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);
     }
 
     /** A builder to create {@link MdnsSearchOptions}. */
     public static final class Builder {
         private final Set<String> subtypes;
-        private boolean isPassiveMode = true;
+        private int queryMode = PASSIVE_QUERY_MODE;
+        private boolean onlyUseIpv6OnIpv6OnlyNetworks = false;
+        private int numOfQueriesBeforeBackoff = 3;
         private boolean removeExpiredService;
         private Network mNetwork;
+        private int mInterfaceIndex;
         private String resolveInstanceName;
 
         private Builder() {
-            subtypes = new ArraySet<>();
+            subtypes = MdnsUtils.newSet();
         }
 
         /**
@@ -178,14 +239,29 @@
         }
 
         /**
-         * Sets if the passive mode scan should be used. The passive mode scans less frequently in
-         * order to conserve battery and produce less network traffic.
+         * Sets which query mode should be used.
          *
-         * @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
-         *                      false}, active mode will be used.
+         * @param queryMode the query mode should be used.
          */
-        public Builder setIsPassiveMode(boolean isPassiveMode) {
-            this.isPassiveMode = isPassiveMode;
+        public Builder setQueryMode(int queryMode) {
+            this.queryMode = queryMode;
+            return this;
+        }
+
+        /**
+         * Sets if only the IPv4 mDNS host should be queried on a network that is both IPv4 & IPv6.
+         * On an IPv6-only network, this is ignored.
+         */
+        public Builder setOnlyUseIpv6OnIpv6OnlyNetworks(boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+            this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
+            return this;
+        }
+
+        /**
+         * Sets if the query backoff mode should be turned on.
+         */
+        public Builder setNumOfQueriesBeforeBackoff(int numOfQueriesBeforeBackoff) {
+            this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
             return this;
         }
 
@@ -221,10 +297,27 @@
             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(new ArrayList<>(subtypes), isPassiveMode,
-                    removeExpiredService, mNetwork, resolveInstanceName);
+            return new MdnsSearchOptions(
+                    new ArrayList<>(subtypes),
+                    queryMode,
+                    removeExpiredService,
+                    mNetwork,
+                    mInterfaceIndex,
+                    resolveInstanceName,
+                    onlyUseIpv6OnIpv6OnlyNetworks,
+                    numOfQueriesBeforeBackoff);
         }
     }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
index 7c19359..4c3cbc0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
@@ -32,8 +32,9 @@
      * service records (PTR, SRV, TXT, A or AAAA) are received .
      *
      * @param serviceInfo The found mDNS service instance.
+     * @param isServiceFromCache Whether the found mDNS service is from cache.
      */
-    void onServiceFound(@NonNull MdnsServiceInfo serviceInfo);
+    void onServiceFound(@NonNull MdnsServiceInfo serviceInfo, boolean isServiceFromCache);
 
     /**
      * Called when an mDNS service instance is updated. This method would be called only if all
@@ -84,8 +85,9 @@
      * record has been received.
      *
      * @param serviceInfo The discovered mDNS service instance.
+     * @param isServiceFromCache Whether the discovered mDNS service is from cache.
      */
-    void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo);
+    void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo, boolean isServiceFromCache);
 
     /**
      * Called when a discovered mDNS service instance is no longer valid and removed.
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 cd0be67..e9a41d1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -16,17 +16,22 @@
 
 package com.android.server.connectivity.mdns;
 
+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;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.net.Network;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.ArrayMap;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -43,17 +48,17 @@
  *  to their default value (0, false or null).
  */
 public class MdnsServiceCache {
-    private static class CacheKey {
+    static class CacheKey {
         @NonNull final String mLowercaseServiceType;
-        @Nullable final Network mNetwork;
+        @NonNull final SocketKey mSocketKey;
 
-        CacheKey(@NonNull String serviceType, @Nullable Network network) {
+        CacheKey(@NonNull String serviceType, @NonNull SocketKey socketKey) {
             mLowercaseServiceType = toDnsLowerCase(serviceType);
-            mNetwork = network;
+            mSocketKey = socketKey;
         }
 
         @Override public int hashCode() {
-            return Objects.hash(mLowercaseServiceType, mNetwork);
+            return Objects.hash(mLowercaseServiceType, mSocketKey);
         }
 
         @Override public boolean equals(Object other) {
@@ -64,40 +69,69 @@
                 return false;
             }
             return Objects.equals(mLowercaseServiceType, ((CacheKey) other).mLowercaseServiceType)
-                    && Objects.equals(mNetwork, ((CacheKey) other).mNetwork);
+                    && Objects.equals(mSocketKey, ((CacheKey) other).mSocketKey);
         }
     }
     /**
-     * A map of cached services. Key is composed of service name, type and network. Value is the
-     * service which use the service type to discover from each socket.
+     * A map of cached services. Key is composed of service type and socket. Value is the list of
+     * services which are discovered from the given CacheKey.
+     * When the MdnsFeatureFlags#NSD_EXPIRED_SERVICES_REMOVAL flag is enabled, the lists are sorted
+     * by expiration time, with the earliest entries appearing first. This sorting allows the
+     * removal process to progress through the expiration check efficiently.
      */
     @NonNull
     private final ArrayMap<CacheKey, List<MdnsResponse>> mCachedServices = new ArrayMap<>();
+    /**
+     * A map of service expire callbacks. Key is composed of service type and socket and value is
+     * the callback listener.
+     */
+    @NonNull
+    private final ArrayMap<CacheKey, ServiceExpiredCallback> mCallbacks = new ArrayMap<>();
     @NonNull
     private final Handler mHandler;
+    @NonNull
+    private final MdnsFeatureFlags mMdnsFeatureFlags;
+    @NonNull
+    private final MdnsUtils.Clock mClock;
+    private long mNextExpirationTime = EXPIRATION_NEVER;
 
-    public MdnsServiceCache(@NonNull Looper looper) {
+    public MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+        this(looper, mdnsFeatureFlags, new MdnsUtils.Clock());
+    }
+
+    @VisibleForTesting
+    MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags,
+            @NonNull MdnsUtils.Clock clock) {
         mHandler = new Handler(looper);
+        mMdnsFeatureFlags = mdnsFeatureFlags;
+        mClock = clock;
     }
 
     /**
-     * Get the cache services which are queried from given service type and network.
+     * Get the cache services which are queried from given service type and socket.
      *
-     * @param serviceType the target service type.
-     * @param network the target network
+     * @param cacheKey the target CacheKey.
      * @return the set of services which matches the given service type.
      */
     @NonNull
-    public List<MdnsResponse> getCachedServices(@NonNull String serviceType,
-            @Nullable Network network) {
+    public List<MdnsResponse> getCachedServices(@NonNull CacheKey cacheKey) {
         ensureRunningOnHandlerThread(mHandler);
-        final CacheKey key = new CacheKey(serviceType, network);
-        return mCachedServices.containsKey(key)
-                ? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(key)))
+        if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+            maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
+        }
+        return mCachedServices.containsKey(cacheKey)
+                ? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(cacheKey)))
                 : Collections.emptyList();
     }
 
-    private MdnsResponse findMatchedResponse(@NonNull List<MdnsResponse> responses,
+    /**
+     * Find a matched response for given service name
+     *
+     * @param responses the responses to be searched.
+     * @param serviceName the target service name
+     * @return the response which matches the given service name or null if not found.
+     */
+    public static MdnsResponse findMatchedResponse(@NonNull List<MdnsResponse> responses,
             @NonNull String serviceName) {
         for (MdnsResponse response : responses) {
             if (equalsIgnoreDnsCase(serviceName, response.getServiceInstanceName())) {
@@ -111,16 +145,16 @@
      * Get the cache service.
      *
      * @param serviceName the target service name.
-     * @param serviceType the target service type.
-     * @param network the target network
+     * @param cacheKey the target CacheKey.
      * @return the service which matches given conditions.
      */
     @Nullable
-    public MdnsResponse getCachedService(@NonNull String serviceName,
-            @NonNull String serviceType, @Nullable Network network) {
+    public MdnsResponse getCachedService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
         ensureRunningOnHandlerThread(mHandler);
-        final List<MdnsResponse> responses =
-                mCachedServices.get(new CacheKey(serviceType, network));
+        if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+            maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
+        }
+        final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
         if (responses == null) {
             return null;
         }
@@ -128,51 +162,188 @@
         return response != null ? new MdnsResponse(response) : null;
     }
 
+    static void insertResponseAndSortList(
+            List<MdnsResponse> responses, MdnsResponse response, long now) {
+        // binarySearch returns "the index of the search key, if it is contained in the list;
+        // otherwise, (-(insertion point) - 1)"
+        final int searchRes = Collections.binarySearch(responses, response,
+                // Sort the list by ttl.
+                (o1, o2) -> Long.compare(o1.getMinRemainingTtl(now), o2.getMinRemainingTtl(now)));
+        responses.add(searchRes >= 0 ? searchRes : (-searchRes - 1), response);
+    }
+
     /**
      * Add or update a service.
      *
-     * @param serviceType the service type.
-     * @param network the target network
+     * @param cacheKey the target CacheKey.
      * @param response the response of the discovered service.
      */
-    public void addOrUpdateService(@NonNull String serviceType, @Nullable Network network,
-            @NonNull MdnsResponse response) {
+    public void addOrUpdateService(@NonNull CacheKey cacheKey, @NonNull MdnsResponse response) {
         ensureRunningOnHandlerThread(mHandler);
         final List<MdnsResponse> responses = mCachedServices.computeIfAbsent(
-                new CacheKey(serviceType, network), key -> new ArrayList<>());
+                cacheKey, key -> new ArrayList<>());
         // Remove existing service if present.
         final MdnsResponse existing =
                 findMatchedResponse(responses, response.getServiceInstanceName());
         responses.remove(existing);
-        responses.add(response);
+        if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+            final long now = mClock.elapsedRealtime();
+            // Insert and sort service
+            insertResponseAndSortList(responses, response, now);
+            // Update the next expiration check time when a new service is added.
+            mNextExpirationTime = getNextExpirationTime(now);
+        } else {
+            responses.add(response);
+        }
     }
 
     /**
-     * Remove a service which matches the given service name, type and network.
+     * Remove a service which matches the given service name, type and socket.
      *
      * @param serviceName the target service name.
-     * @param serviceType the target service type.
-     * @param network the target network.
+     * @param cacheKey the target CacheKey.
      */
     @Nullable
-    public MdnsResponse removeService(@NonNull String serviceName, @NonNull String serviceType,
-            @Nullable Network network) {
+    public MdnsResponse removeService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
         ensureRunningOnHandlerThread(mHandler);
-        final List<MdnsResponse> responses =
-                mCachedServices.get(new CacheKey(serviceType, network));
+        final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
         if (responses == null) {
             return null;
         }
         final Iterator<MdnsResponse> iterator = responses.iterator();
+        MdnsResponse removedResponse = null;
         while (iterator.hasNext()) {
             final MdnsResponse response = iterator.next();
             if (equalsIgnoreDnsCase(serviceName, response.getServiceInstanceName())) {
                 iterator.remove();
-                return response;
+                removedResponse = response;
+                break;
             }
         }
-        return null;
+
+        if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+            // Remove the serviceType if no response.
+            if (responses.isEmpty()) {
+                mCachedServices.remove(cacheKey);
+            }
+            // Update the next expiration check time when a service is removed.
+            mNextExpirationTime = getNextExpirationTime(mClock.elapsedRealtime());
+        }
+        return removedResponse;
     }
 
-    // TODO: check ttl expiration for each service and notify to the clients.
+    /**
+     * Register a callback to listen to service expiration.
+     *
+     * <p> Registering the same callback instance twice is a no-op, since MdnsServiceTypeClient
+     * relies on this.
+     *
+     * @param cacheKey the target CacheKey.
+     * @param callback the callback that notify the service is expired.
+     */
+    public void registerServiceExpiredCallback(@NonNull CacheKey cacheKey,
+            @NonNull ServiceExpiredCallback callback) {
+        ensureRunningOnHandlerThread(mHandler);
+        mCallbacks.put(cacheKey, callback);
+    }
+
+    /**
+     * Unregister the service expired callback.
+     *
+     * @param cacheKey the CacheKey that is registered to listen service expiration before.
+     */
+    public void unregisterServiceExpiredCallback(@NonNull CacheKey cacheKey) {
+        ensureRunningOnHandlerThread(mHandler);
+        mCallbacks.remove(cacheKey);
+    }
+
+    private void notifyServiceExpired(@NonNull CacheKey cacheKey,
+            @NonNull MdnsResponse previousResponse, @Nullable MdnsResponse newResponse) {
+        final ServiceExpiredCallback callback = mCallbacks.get(cacheKey);
+        if (callback == null) {
+            // The cached service is no listener.
+            return;
+        }
+        mHandler.post(()-> callback.onServiceRecordExpired(previousResponse, newResponse));
+    }
+
+    static List<MdnsResponse> removeExpiredServices(@NonNull List<MdnsResponse> responses,
+            long now) {
+        final List<MdnsResponse> removedResponses = new ArrayList<>();
+        final Iterator<MdnsResponse> iterator = responses.iterator();
+        while (iterator.hasNext()) {
+            final MdnsResponse response = iterator.next();
+            // TODO: Check other records (A, AAAA, TXT) ttl time and remove the record if it's
+            //  expired. Then send service update notification.
+            if (!response.hasServiceRecord() || response.getMinRemainingTtl(now) > 0) {
+                // The responses are sorted by the service record ttl time. Break out of loop
+                // early if service is not expired or no service record.
+                break;
+            }
+            // Remove the ttl expired service.
+            iterator.remove();
+            removedResponses.add(response);
+        }
+        return removedResponses;
+    }
+
+    private long getNextExpirationTime(long now) {
+        if (mCachedServices.isEmpty()) {
+            return EXPIRATION_NEVER;
+        }
+
+        long minRemainingTtl = EXPIRATION_NEVER;
+        for (int i = 0; i < mCachedServices.size(); i++) {
+            minRemainingTtl = min(minRemainingTtl,
+                    // The empty lists are not kept in the map, so there's always at least one
+                    // element in the list. Therefore, it's fine to get the first element without a
+                    // null check.
+                    mCachedServices.valueAt(i).get(0).getMinRemainingTtl(now));
+        }
+        return minRemainingTtl == EXPIRATION_NEVER ? EXPIRATION_NEVER : now + minRemainingTtl;
+    }
+
+    /**
+     * Check whether the ttl time is expired on each service and notify to the listeners
+     */
+    private void maybeRemoveExpiredServices(CacheKey cacheKey, long now) {
+        ensureRunningOnHandlerThread(mHandler);
+        if (now < mNextExpirationTime) {
+            // Skip the check if ttl time is not expired.
+            return;
+        }
+
+        final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
+        if (responses == null) {
+            // No such services.
+            return;
+        }
+
+        final List<MdnsResponse> removedResponses = removeExpiredServices(responses, now);
+        if (removedResponses.isEmpty()) {
+            // No expired services.
+            return;
+        }
+
+        for (MdnsResponse previousResponse : removedResponses) {
+            notifyServiceExpired(cacheKey, previousResponse, null /* newResponse */);
+        }
+
+        // Remove the serviceType if no response.
+        if (responses.isEmpty()) {
+            mCachedServices.remove(cacheKey);
+        }
+
+        // Update next expiration time.
+        mNextExpirationTime = getNextExpirationTime(now);
+    }
+
+    /*** Callbacks for listening service expiration */
+    public interface ServiceExpiredCallback {
+        /*** Notify the service is expired */
+        void onServiceRecordExpired(@NonNull MdnsResponse previousResponse,
+                @Nullable MdnsResponse newResponse);
+    }
+
+    // TODO: Schedule a job to check ttl expiration for all services and notify to the clients.
 }
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 78df6df..1ec9e39 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -28,6 +28,7 @@
 import com.android.net.module.util.ByteUtils;
 
 import java.nio.charset.Charset;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -62,7 +63,8 @@
                             source.createStringArrayList(),
                             source.createTypedArrayList(TextEntry.CREATOR),
                             source.readInt(),
-                            source.readParcelable(null));
+                            source.readParcelable(Network.class.getClassLoader()),
+                            Instant.ofEpochSecond(source.readLong()));
                 }
 
                 @Override
@@ -89,54 +91,8 @@
     @Nullable
     private final Network network;
 
-    /** 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);
-    }
-
-    /** 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);
-    }
+    @NonNull
+    private final Instant expirationTime;
 
     /**
      * Constructs a {@link MdnsServiceInfo} object with default values.
@@ -165,7 +121,8 @@
                 textStrings,
                 textEntries,
                 interfaceIndex,
-                /* network= */ null);
+                /* network= */ null,
+                /* expirationTime= */ Instant.MAX);
     }
 
     /**
@@ -184,7 +141,8 @@
             @Nullable List<String> textStrings,
             @Nullable List<TextEntry> textEntries,
             int interfaceIndex,
-            @Nullable Network network) {
+            @Nullable Network network,
+            @NonNull Instant expirationTime) {
         this.serviceInstanceName = serviceInstanceName;
         this.serviceType = serviceType;
         this.subtypes = new ArrayList<>();
@@ -217,6 +175,7 @@
         this.attributes = Collections.unmodifiableMap(attributes);
         this.interfaceIndex = interfaceIndex;
         this.network = network;
+        this.expirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
     }
 
     private static List<TextEntry> parseTextStrings(List<String> textStrings) {
@@ -314,6 +273,17 @@
     }
 
     /**
+     * Returns the timestamp after when this service is expired or {@code null} if the expiration
+     * time is unknown.
+     *
+     * A service is considered expired if any of its DNS record is expired.
+     */
+    @NonNull
+    public Instant getExpirationTime() {
+        return expirationTime;
+    }
+
+    /**
      * Returns attribute value for {@code key} as a UTF-8 string. It's the caller who must make sure
      * that the value of {@code key} is indeed a UTF-8 string. {@code null} will be returned if no
      * attribute value exists for {@code key}.
@@ -364,6 +334,7 @@
         out.writeTypedList(textEntries);
         out.writeInt(interfaceIndex);
         out.writeParcelable(network, 0);
+        out.writeLong(expirationTime.getEpochSecond());
     }
 
     @Override
@@ -377,7 +348,8 @@
                 + ", interfaceIndex: " + interfaceIndex
                 + ", network: " + network
                 + ", textStrings: " + textStrings
-                + ", textEntries: " + textEntries;
+                + ", textEntries: " + textEntries
+                + ", expirationTime: " + expirationTime;
     }
 
 
@@ -496,4 +468,4 @@
             out.writeByteArray(value);
         }
     }
-}
\ No newline at end of file
+}
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 ebd8b77..0d6a9ec 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -18,7 +18,9 @@
 
 import android.annotation.Nullable;
 
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -26,7 +28,7 @@
 import java.util.Objects;
 
 /** An mDNS "SRV" record, which contains service information. */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
 public class MdnsServiceRecord extends MdnsRecord {
     public static final int PROTO_NONE = 0;
     public static final int PROTO_TCP = 1;
@@ -47,6 +49,12 @@
         super(name, TYPE_SRV, reader, isQuestion);
     }
 
+    public MdnsServiceRecord(String[] name, boolean isUnicast) {
+        super(name, TYPE_SRV,
+                MdnsConstants.QCLASS_INTERNET | (isUnicast ? MdnsConstants.QCLASS_UNICAST : 0),
+                0L /* receiptTimeMillis */, false /* cacheFlush */, 0L /* ttlMillis */);
+    }
+
     public MdnsServiceRecord(String[] name, long receiptTimeMillis, boolean cacheFlush,
                     long ttlMillis, int servicePriority, int serviceWeight, int servicePort,
                     String[] serviceHost) {
@@ -142,7 +150,8 @@
     @Override
     public int hashCode() {
         return (super.hashCode() * 31)
-                + Objects.hash(servicePriority, serviceWeight, Arrays.hashCode(serviceHost),
+                + Objects.hash(servicePriority, serviceWeight,
+                Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(serviceHost)),
                 servicePort);
     }
 
@@ -159,7 +168,7 @@
         return super.equals(other)
                 && (servicePriority == otherRecord.servicePriority)
                 && (serviceWeight == otherRecord.serviceWeight)
-                && Arrays.equals(serviceHost, otherRecord.serviceHost)
+                && MdnsUtils.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 14302c2..643430a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -16,33 +16,40 @@
 
 package com.android.server.connectivity.mdns;
 
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
+import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
+import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
+import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.net.Network;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.text.TextUtils;
 import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.Pair;
 
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.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.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;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Future;
+import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
 
 /**
@@ -51,23 +58,41 @@
  */
 public class MdnsServiceTypeClient {
 
-    private static final int DEFAULT_MTU = 1500;
+    private static final String TAG = MdnsServiceTypeClient.class.getSimpleName();
+    @VisibleForTesting
+    static final int EVENT_START_QUERYTASK = 1;
+    static final int EVENT_QUERY_RESULT = 2;
+    static final int INVALID_TRANSACTION_ID = -1;
 
     private final String serviceType;
     private final String[] serviceTypeLabels;
     private final MdnsSocketClientBase socketClient;
     private final MdnsResponseDecoder responseDecoder;
     private final ScheduledExecutorService executor;
-    @Nullable private final Network network;
+    @NonNull private final SocketKey socketKey;
     @NonNull private final SharedLog sharedLog;
-    private final Object lock = new Object();
-    private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners =
+    @NonNull private final Handler handler;
+    @NonNull private final MdnsQueryScheduler mdnsQueryScheduler;
+    @NonNull private final Dependencies dependencies;
+    /**
+     * The service caches for each socket. It should be accessed from looper thread only.
+     */
+    @NonNull private final MdnsServiceCache serviceCache;
+    @NonNull private final MdnsServiceCache.CacheKey cacheKey;
+    @NonNull private final ServiceExpiredCallback serviceExpiredCallback =
+            new ServiceExpiredCallback() {
+                @Override
+                public void onServiceRecordExpired(@NonNull MdnsResponse previousResponse,
+                        @Nullable MdnsResponse newResponse) {
+                    notifyRemovedServiceToListeners(previousResponse, "Service record expired");
+                }
+            };
+    @NonNull private final MdnsFeatureFlags featureFlags;
+    private final ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners =
             new ArrayMap<>();
-    // TODO: change instanceNameToResponse to TreeMap with case insensitive comparator.
-    private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>();
     private final boolean removeServiceAfterTtlExpires =
             MdnsConfigs.removeServiceAfterTtlExpires();
-    private final MdnsResponseDecoder.Clock clock;
+    private final Clock clock;
 
     @Nullable private MdnsSearchOptions searchOptions;
 
@@ -75,10 +100,152 @@
     // QueryTask for
     // new subtypes. It stays the same between packets for same subtypes.
     private long currentSessionId = 0;
+    private long lastSentTime;
 
-    @GuardedBy("lock")
-    @Nullable
-    private Future<?> requestTaskFuture;
+    private static class ListenerInfo {
+        @NonNull
+        final MdnsSearchOptions searchOptions;
+        final Set<String> discoveredServiceNames;
+
+        ListenerInfo(@NonNull MdnsSearchOptions searchOptions,
+                @Nullable ListenerInfo previousInfo) {
+            this.searchOptions = searchOptions;
+            this.discoveredServiceNames = previousInfo == null
+                    ? MdnsUtils.newSet() : previousInfo.discoveredServiceNames;
+        }
+
+        /**
+         * Set the given service name as discovered.
+         *
+         * @return true if the service name was not discovered before.
+         */
+        boolean setServiceDiscovered(@NonNull String serviceName) {
+            return discoveredServiceNames.add(MdnsUtils.toDnsLowerCase(serviceName));
+        }
+
+        void unsetServiceDiscovered(@NonNull String serviceName) {
+            discoveredServiceNames.remove(MdnsUtils.toDnsLowerCase(serviceName));
+        }
+    }
+
+    private class QueryTaskHandler extends Handler {
+        QueryTaskHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        @SuppressWarnings("FutureReturnValueIgnored")
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_START_QUERYTASK: {
+                    final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs =
+                            (MdnsQueryScheduler.ScheduledQueryTaskArgs) msg.obj;
+                    // QueryTask should be run immediately after being created (not be scheduled in
+                    // advance). Because the result of "makeResponsesForResolve" depends on answers
+                    // that were received before it is called, so to take into account all answers
+                    // 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),
+                            getExistingServices());
+                    executor.submit(queryTask);
+                    break;
+                }
+                case EVENT_QUERY_RESULT: {
+                    final QuerySentArguments sentResult = (QuerySentArguments) msg.obj;
+                    // If a task is cancelled while the Executor is running it, EVENT_QUERY_RESULT
+                    // will still be sent when it ends. So use session ID to check if this task
+                    // should continue to schedule more.
+                    if (sentResult.taskArgs.sessionId != currentSessionId) {
+                        break;
+                    }
+
+                    if ((sentResult.transactionId != INVALID_TRANSACTION_ID)) {
+                        for (int i = 0; i < listeners.size(); i++) {
+                            listeners.keyAt(i).onDiscoveryQuerySent(
+                                    sentResult.subTypes, sentResult.transactionId);
+                        }
+                    }
+
+                    tryRemoveServiceAfterTtlExpires();
+
+                    final long now = clock.elapsedRealtime();
+                    lastSentTime = now;
+                    final long minRemainingTtl = getMinRemainingTtl(now);
+                    MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+                            mdnsQueryScheduler.scheduleNextRun(
+                                    sentResult.taskArgs.config,
+                                    minRemainingTtl,
+                                    now,
+                                    lastSentTime,
+                                    sentResult.taskArgs.sessionId
+                            );
+                    dependencies.sendMessageDelayed(
+                            handler,
+                            handler.obtainMessage(EVENT_START_QUERYTASK, args),
+                            calculateTimeToNextTask(args, now, sharedLog));
+                    break;
+                }
+                default:
+                    sharedLog.e("Unrecognized event " + msg.what);
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Dependencies of MdnsServiceTypeClient, for injection in tests.
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+    public static class Dependencies {
+        /**
+         * @see Handler#sendMessageDelayed(Message, long)
+         */
+        public void sendMessageDelayed(@NonNull Handler handler, @NonNull Message message,
+                long delayMillis) {
+            handler.sendMessageDelayed(message, delayMillis);
+        }
+
+        /**
+         * @see Handler#removeMessages(int)
+         */
+        public void removeMessages(@NonNull Handler handler, int what) {
+            handler.removeMessages(what);
+        }
+
+        /**
+         * @see Handler#hasMessages(int)
+         */
+        public boolean hasMessages(@NonNull Handler handler, int what) {
+            return handler.hasMessages(what);
+        }
+
+        /**
+         * @see Handler#post(Runnable)
+         */
+        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));
+            }
+        }
+    }
 
     /**
      * Constructor of {@link MdnsServiceTypeClient}.
@@ -90,10 +257,13 @@
             @NonNull String serviceType,
             @NonNull MdnsSocketClientBase socketClient,
             @NonNull ScheduledExecutorService executor,
-            @Nullable Network network,
-            @NonNull SharedLog sharedLog) {
-        this(serviceType, socketClient, executor, new MdnsResponseDecoder.Clock(), network,
-                sharedLog);
+            @NonNull SocketKey socketKey,
+            @NonNull SharedLog sharedLog,
+            @NonNull Looper looper,
+            @NonNull MdnsServiceCache serviceCache,
+            @NonNull MdnsFeatureFlags featureFlags) {
+        this(serviceType, socketClient, executor, new Clock(), socketKey, sharedLog, looper,
+                new Dependencies(), serviceCache, featureFlags);
     }
 
     @VisibleForTesting
@@ -101,21 +271,40 @@
             @NonNull String serviceType,
             @NonNull MdnsSocketClientBase socketClient,
             @NonNull ScheduledExecutorService executor,
-            @NonNull MdnsResponseDecoder.Clock clock,
-            @Nullable Network network,
-            @NonNull SharedLog sharedLog) {
+            @NonNull Clock clock,
+            @NonNull SocketKey socketKey,
+            @NonNull SharedLog sharedLog,
+            @NonNull Looper looper,
+            @NonNull Dependencies dependencies,
+            @NonNull MdnsServiceCache serviceCache,
+            @NonNull MdnsFeatureFlags featureFlags) {
         this.serviceType = serviceType;
         this.socketClient = socketClient;
         this.executor = executor;
         this.serviceTypeLabels = TextUtils.split(serviceType, "\\.");
         this.responseDecoder = new MdnsResponseDecoder(clock, serviceTypeLabels);
         this.clock = clock;
-        this.network = network;
+        this.socketKey = socketKey;
         this.sharedLog = sharedLog;
+        this.handler = new QueryTaskHandler(looper);
+        this.dependencies = dependencies;
+        this.serviceCache = serviceCache;
+        this.mdnsQueryScheduler = new MdnsQueryScheduler();
+        this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
+        this.featureFlags = featureFlags;
     }
 
-    private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
-            @NonNull MdnsResponse response, @NonNull String[] serviceTypeLabels) {
+    /**
+     * Do the cleanup of the MdnsServiceTypeClient
+     */
+    private void shutDown() {
+        removeScheduledTask();
+        mdnsQueryScheduler.cancelScheduledRun();
+        serviceCache.unregisterServiceExpiredCallback(cacheKey);
+    }
+
+    private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(@NonNull MdnsResponse response,
+            @NonNull String[] serviceTypeLabels, long elapsedRealtimeMillis) {
         String[] hostName = null;
         int port = 0;
         if (response.hasServiceRecord()) {
@@ -148,6 +337,7 @@
             textStrings = response.getTextRecord().getStrings();
             textEntries = response.getTextRecord().getEntries();
         }
+        Instant now = Instant.now();
         // TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
         return new MdnsServiceInfo(
                 serviceInstanceName,
@@ -160,7 +350,13 @@
                 textStrings,
                 textEntries,
                 response.getInterfaceIndex(),
-                response.getNetwork());
+                response.getNetwork(),
+                now.plusMillis(response.getMinRemainingTtl(elapsedRealtimeMillis)));
+    }
+
+    private List<MdnsResponse> getExistingServices() {
+        return featureFlags.isQueryWithKnownAnswerEnabled()
+                ? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
     }
 
     /**
@@ -171,41 +367,91 @@
      * @param listener      The {@link MdnsServiceBrowserListener} to register.
      * @param searchOptions {@link MdnsSearchOptions} contains the list of subtypes to discover.
      */
+    @SuppressWarnings("FutureReturnValueIgnored")
     public void startSendAndReceive(
             @NonNull MdnsServiceBrowserListener listener,
             @NonNull MdnsSearchOptions searchOptions) {
-        synchronized (lock) {
-            this.searchOptions = searchOptions;
-            boolean hadReply = false;
-            if (listeners.put(listener, searchOptions) == null) {
-                for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
-                    if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
-                    final MdnsServiceInfo info =
-                            buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
-                    listener.onServiceNameDiscovered(info);
-                    if (existingResponse.isComplete()) {
-                        listener.onServiceFound(info);
-                        hadReply = true;
-                    }
+        ensureRunningOnHandlerThread(handler);
+        this.searchOptions = searchOptions;
+        boolean hadReply = false;
+        final ListenerInfo existingInfo = listeners.get(listener);
+        final ListenerInfo listenerInfo = new ListenerInfo(searchOptions, existingInfo);
+        listeners.put(listener, listenerInfo);
+        if (existingInfo == null) {
+            for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
+                if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
+                final MdnsServiceInfo info = buildMdnsServiceInfoFromResponse(
+                        existingResponse, serviceTypeLabels, clock.elapsedRealtime());
+                listener.onServiceNameDiscovered(info, true /* isServiceFromCache */);
+                listenerInfo.setServiceDiscovered(info.getServiceInstanceName());
+                if (existingResponse.isComplete()) {
+                    listener.onServiceFound(info, true /* isServiceFromCache */);
+                    hadReply = true;
                 }
             }
-            // Cancel the next scheduled periodical task.
-            if (requestTaskFuture != null) {
-                requestTaskFuture.cancel(true);
-            }
-            // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
-            // interested anymore.
-            final QueryTaskConfig taskConfig = new QueryTaskConfig(
-                    searchOptions.getSubtypes(),
-                    searchOptions.isPassiveMode(),
-                    ++currentSessionId,
-                    network);
-            if (hadReply) {
-                requestTaskFuture = scheduleNextRunLocked(taskConfig);
-            } else {
-                requestTaskFuture = executor.submit(new QueryTask(taskConfig));
-            }
         }
+        // Remove the next scheduled periodical task.
+        removeScheduledTask();
+        mdnsQueryScheduler.cancelScheduledRun();
+        // 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);
+        final long now = clock.elapsedRealtime();
+        if (lastSentTime == 0) {
+            lastSentTime = now;
+        }
+        final long minRemainingTtl = getMinRemainingTtl(now);
+        if (hadReply) {
+            MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+                    mdnsQueryScheduler.scheduleNextRun(
+                            taskConfig,
+                            minRemainingTtl,
+                            now,
+                            lastSentTime,
+                            currentSessionId
+                    );
+            dependencies.sendMessageDelayed(
+                    handler,
+                    handler.obtainMessage(EVENT_START_QUERYTASK, args),
+                    calculateTimeToNextTask(args, now, sharedLog));
+        } else {
+            final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
+            final QueryTask queryTask = new QueryTask(
+                    mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
+                            minRemainingTtl, currentSessionId), servicesToResolve,
+                    getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
+                    getExistingServices());
+            executor.submit(queryTask);
+        }
+
+        serviceCache.registerServiceExpiredCallback(cacheKey, serviceExpiredCallback);
+    }
+
+    private Set<String> getAllDiscoverySubtypes() {
+        final Set<String> subtypes = MdnsUtils.newSet();
+        for (int i = 0; i < listeners.size(); i++) {
+            final MdnsSearchOptions listenerOptions = listeners.valueAt(i).searchOptions;
+            subtypes.addAll(listenerOptions.getSubtypes());
+        }
+        return subtypes;
+    }
+
+    /**
+     * Get the executor service.
+     */
+    public ScheduledExecutorService getExecutor() {
+        return executor;
+    }
+
+    private void removeScheduledTask() {
+        dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
+        sharedLog.log("Remove EVENT_START_QUERYTASK"
+                + ", current session: " + currentSessionId);
+        ++currentSessionId;
     }
 
     private boolean responseMatchesOptions(@NonNull MdnsResponse response,
@@ -213,7 +459,7 @@
         final boolean matchesInstanceName = options.getResolveInstanceName() == null
                 // DNS is case-insensitive, so ignore case in the comparison
                 || MdnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
-                        response.getServiceInstanceName());
+                response.getServiceInstanceName());
 
         // If discovery is requiring some subtypes, the response must have one that matches a
         // requested one.
@@ -236,128 +482,150 @@
      * listener}. Otherwise returns {@code false}.
      */
     public boolean stopSendAndReceive(@NonNull MdnsServiceBrowserListener listener) {
-        synchronized (lock) {
-            if (listeners.remove(listener) == null) {
-                return listeners.isEmpty();
-            }
-            if (listeners.isEmpty() && requestTaskFuture != null) {
-                requestTaskFuture.cancel(true);
-                requestTaskFuture = null;
-            }
+        ensureRunningOnHandlerThread(handler);
+        if (listeners.remove(listener) == null) {
             return listeners.isEmpty();
         }
-    }
-
-    public String[] getServiceTypeLabels() {
-        return serviceTypeLabels;
+        if (listeners.isEmpty()) {
+            shutDown();
+        }
+        return listeners.isEmpty();
     }
 
     /**
      * Process an incoming response packet.
      */
-    public synchronized void processResponse(@NonNull MdnsPacket packet, int interfaceIndex,
-            Network network) {
-        synchronized (lock) {
-            // Augment the list of current known responses, and generated responses for resolve
-            // requests if there is no known response
-            final List<MdnsResponse> currentList = new ArrayList<>(instanceNameToResponse.values());
-
-            List<MdnsResponse> additionalResponses = makeResponsesForResolve(interfaceIndex,
-                    network);
-            for (MdnsResponse additionalResponse : additionalResponses) {
-                if (!instanceNameToResponse.containsKey(
-                        additionalResponse.getServiceInstanceName())) {
-                    currentList.add(additionalResponse);
-                }
+    public synchronized void processResponse(@NonNull MdnsPacket packet,
+            @NonNull SocketKey socketKey) {
+        ensureRunningOnHandlerThread(handler);
+        // Augment the list of current known responses, and generated responses for resolve
+        // requests if there is no known response
+        final List<MdnsResponse> cachedList = serviceCache.getCachedServices(cacheKey);
+        final List<MdnsResponse> currentList = new ArrayList<>(cachedList);
+        List<MdnsResponse> additionalResponses = makeResponsesForResolve(socketKey);
+        for (MdnsResponse additionalResponse : additionalResponses) {
+            if (findMatchedResponse(
+                    cachedList, additionalResponse.getServiceInstanceName()) == null) {
+                currentList.add(additionalResponse);
             }
-            final Pair<ArraySet<MdnsResponse>, ArrayList<MdnsResponse>> augmentedResult =
-                    responseDecoder.augmentResponses(packet, currentList, interfaceIndex, network);
+        }
+        final Pair<Set<MdnsResponse>, ArrayList<MdnsResponse>> augmentedResult =
+                responseDecoder.augmentResponses(packet, currentList,
+                        socketKey.getInterfaceIndex(), socketKey.getNetwork());
 
-            final ArraySet<MdnsResponse> modifiedResponse = augmentedResult.first;
-            final ArrayList<MdnsResponse> allResponses = augmentedResult.second;
+        final Set<MdnsResponse> modifiedResponse = augmentedResult.first;
+        final ArrayList<MdnsResponse> allResponses = augmentedResult.second;
 
-            for (MdnsResponse response : allResponses) {
-                if (modifiedResponse.contains(response)) {
-                    if (response.isGoodbye()) {
-                        onGoodbyeReceived(response.getServiceInstanceName());
-                    } else {
-                        onResponseModified(response);
-                    }
-                } else if (instanceNameToResponse.containsKey(response.getServiceInstanceName())) {
-                    // If the response is not modified and already in the cache. The cache will
-                    // need to be updated to refresh the last receipt time.
-                    instanceNameToResponse.put(response.getServiceInstanceName(), response);
+        for (MdnsResponse response : allResponses) {
+            final String serviceInstanceName = response.getServiceInstanceName();
+            if (modifiedResponse.contains(response)) {
+                if (response.isGoodbye()) {
+                    onGoodbyeReceived(serviceInstanceName);
+                } else {
+                    onResponseModified(response);
                 }
+            } else if (findMatchedResponse(cachedList, serviceInstanceName) != null) {
+                // If the response is not modified and already in the cache. The cache will
+                // need to be updated to refresh the last receipt time.
+                serviceCache.addOrUpdateService(cacheKey, response);
+            }
+        }
+        if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
+            final long now = clock.elapsedRealtime();
+            final long minRemainingTtl = getMinRemainingTtl(now);
+            MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+                    mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl,
+                            lastSentTime, currentSessionId + 1);
+            if (args != null) {
+                removeScheduledTask();
+                dependencies.sendMessageDelayed(
+                        handler,
+                        handler.obtainMessage(EVENT_START_QUERYTASK, args),
+                        calculateTimeToNextTask(args, now, sharedLog));
             }
         }
     }
 
     public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+        ensureRunningOnHandlerThread(handler);
         for (int i = 0; i < listeners.size(); i++) {
             listeners.keyAt(i).onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
         }
     }
 
-    /** Notify all services are removed because the socket is destroyed. */
-    public void notifySocketDestroyed() {
-        synchronized (lock) {
-            for (MdnsResponse response : instanceNameToResponse.values()) {
-                final String name = response.getServiceInstanceName();
-                if (name == null) continue;
-                for (int i = 0; i < listeners.size(); i++) {
-                    if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
-                    final MdnsServiceBrowserListener listener = listeners.keyAt(i);
-                    final MdnsServiceInfo serviceInfo =
-                            buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
-                    if (response.isComplete()) {
-                        sharedLog.log("Socket destroyed. onServiceRemoved: " + name);
-                        listener.onServiceRemoved(serviceInfo);
-                    }
-                    sharedLog.log("Socket destroyed. onServiceNameRemoved: " + name);
-                    listener.onServiceNameRemoved(serviceInfo);
+    private void notifyRemovedServiceToListeners(@NonNull MdnsResponse response,
+            @NonNull String message) {
+        for (int i = 0; i < listeners.size(); i++) {
+            if (!responseMatchesOptions(response, listeners.valueAt(i).searchOptions)) continue;
+            final MdnsServiceBrowserListener listener = listeners.keyAt(i);
+            if (response.getServiceInstanceName() != null) {
+                listeners.valueAt(i).unsetServiceDiscovered(response.getServiceInstanceName());
+                final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
+                        response, serviceTypeLabels, clock.elapsedRealtime());
+                if (response.isComplete()) {
+                    sharedLog.log(message + ". onServiceRemoved: " + serviceInfo);
+                    listener.onServiceRemoved(serviceInfo);
                 }
-            }
-
-            if (requestTaskFuture != null) {
-                requestTaskFuture.cancel(true);
-                requestTaskFuture = null;
+                sharedLog.log(message + ". onServiceNameRemoved: " + serviceInfo);
+                listener.onServiceNameRemoved(serviceInfo);
             }
         }
     }
 
+    /** Notify all services are removed because the socket is destroyed. */
+    public void notifySocketDestroyed() {
+        ensureRunningOnHandlerThread(handler);
+        for (MdnsResponse response : serviceCache.getCachedServices(cacheKey)) {
+            final String name = response.getServiceInstanceName();
+            if (name == null) continue;
+            notifyRemovedServiceToListeners(response, "Socket destroyed");
+        }
+        shutDown();
+    }
+
     private void onResponseModified(@NonNull MdnsResponse response) {
         final String serviceInstanceName = response.getServiceInstanceName();
         final MdnsResponse currentResponse =
-                instanceNameToResponse.get(serviceInstanceName);
+                serviceCache.getCachedService(serviceInstanceName, cacheKey);
 
-        boolean newServiceFound = false;
+        final boolean newInCache = currentResponse == null;
         boolean serviceBecomesComplete = false;
-        if (currentResponse == null) {
-            newServiceFound = true;
+        if (newInCache) {
             if (serviceInstanceName != null) {
-                instanceNameToResponse.put(serviceInstanceName, response);
+                serviceCache.addOrUpdateService(cacheKey, response);
             }
         } else {
             boolean before = currentResponse.isComplete();
-            instanceNameToResponse.put(serviceInstanceName, response);
+            serviceCache.addOrUpdateService(cacheKey, response);
             boolean after = response.isComplete();
             serviceBecomesComplete = !before && after;
         }
-        MdnsServiceInfo serviceInfo =
-                buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
+        sharedLog.i(String.format(
+                "Handling response from service: %s, newInCache: %b, serviceBecomesComplete:"
+                        + " %b, responseIsComplete: %b",
+                serviceInstanceName, newInCache, serviceBecomesComplete,
+                response.isComplete()));
+        final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
+                response, serviceTypeLabels, clock.elapsedRealtime());
 
         for (int i = 0; i < listeners.size(); i++) {
-            if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
+            // If a service stops matching the options (currently can only happen if it loses a
+            // subtype), service lost callbacks should also be sent; this is not done today as
+            // only expiration of SRV records is used, not PTR records used for subtypes, so
+            // services never lose PTR record subtypes.
+            if (!responseMatchesOptions(response, listeners.valueAt(i).searchOptions)) continue;
             final MdnsServiceBrowserListener listener = listeners.keyAt(i);
+            final ListenerInfo listenerInfo = listeners.valueAt(i);
+            final boolean newServiceFound = listenerInfo.setServiceDiscovered(serviceInstanceName);
             if (newServiceFound) {
                 sharedLog.log("onServiceNameDiscovered: " + serviceInfo);
-                listener.onServiceNameDiscovered(serviceInfo);
+                listener.onServiceNameDiscovered(serviceInfo, false /* isServiceFromCache */);
             }
 
             if (response.isComplete()) {
                 if (newServiceFound || serviceBecomesComplete) {
                     sharedLog.log("onServiceFound: " + serviceInfo);
-                    listener.onServiceFound(serviceInfo);
+                    listener.onServiceFound(serviceInfo, false /* isServiceFromCache */);
                 } else {
                     sharedLog.log("onServiceUpdated: " + serviceInfo);
                     listener.onServiceUpdated(serviceInfo);
@@ -367,22 +635,12 @@
     }
 
     private void onGoodbyeReceived(@Nullable String serviceInstanceName) {
-        final MdnsResponse response = instanceNameToResponse.remove(serviceInstanceName);
+        final MdnsResponse response =
+                serviceCache.removeService(serviceInstanceName, cacheKey);
         if (response == null) {
             return;
         }
-        for (int i = 0; i < listeners.size(); i++) {
-            if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
-            final MdnsServiceBrowserListener listener = listeners.keyAt(i);
-            final MdnsServiceInfo serviceInfo =
-                    buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
-            if (response.isComplete()) {
-                sharedLog.log("onServiceRemoved: " + serviceInfo);
-                listener.onServiceRemoved(serviceInfo);
-            }
-            sharedLog.log("onServiceNameRemoved: " + serviceInfo);
-            listener.onServiceNameRemoved(serviceInfo);
-        }
+        notifyRemovedServiceToListeners(response, "Goodbye received");
     }
 
     private boolean shouldRemoveServiceAfterTtlExpires() {
@@ -392,115 +650,19 @@
         return searchOptions != null && searchOptions.removeExpiredService();
     }
 
-    @VisibleForTesting
-    MdnsPacketWriter createMdnsPacketWriter() {
-        return new MdnsPacketWriter(DEFAULT_MTU);
-    }
-
-    // A configuration for the PeriodicalQueryTask that contains parameters to build a query packet.
-    // Call to getConfigForNextRun returns a config that can be used to build the next query task.
-    @VisibleForTesting
-    static class QueryTaskConfig {
-
-        private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
-                (int) MdnsConfigs.initialTimeBetweenBurstsMs();
-        private static final int TIME_BETWEEN_BURSTS_MS = (int) MdnsConfigs.timeBetweenBurstsMs();
-        private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
-        private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
-                (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
-        private static final int QUERIES_PER_BURST_PASSIVE_MODE =
-                (int) MdnsConfigs.queriesPerBurstPassive();
-        private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
-        // The following fields are used by QueryTask so we need to test them.
-        @VisibleForTesting
-        final List<String> subtypes;
-        private final boolean alwaysAskForUnicastResponse =
-                MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
-        private final boolean usePassiveMode;
-        private final long sessionId;
-        @VisibleForTesting
-        int transactionId;
-        @VisibleForTesting
-        boolean expectUnicastResponse;
-        private int queriesPerBurst;
-        private int timeBetweenBurstsInMs;
-        private int burstCounter;
-        private int timeToRunNextTaskInMs;
-        private boolean isFirstBurst;
-        @Nullable private final Network network;
-
-        QueryTaskConfig(@NonNull Collection<String> subtypes, boolean usePassiveMode,
-                long sessionId, @Nullable Network network) {
-            this.usePassiveMode = usePassiveMode;
-            this.subtypes = new ArrayList<>(subtypes);
-            this.queriesPerBurst = QUERIES_PER_BURST;
-            this.burstCounter = 0;
-            this.transactionId = 1;
-            this.expectUnicastResponse = true;
-            this.isFirstBurst = true;
-            this.sessionId = sessionId;
-            // Config the scan frequency based on the scan mode.
-            if (this.usePassiveMode) {
-                // 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.
-                this.timeBetweenBurstsInMs = TIME_BETWEEN_BURSTS_MS;
-            } else {
-                // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
-                // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
-                // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
-                // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
-                this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
-            }
-            this.network = network;
-        }
-
-        QueryTaskConfig getConfigForNextRun() {
-            if (++transactionId > UNSIGNED_SHORT_MAX_VALUE) {
-                transactionId = 1;
-            }
-            // Only the first query expects uni-cast response.
-            expectUnicastResponse = false;
-            if (++burstCounter == queriesPerBurst) {
-                burstCounter = 0;
-
-                if (alwaysAskForUnicastResponse) {
-                    expectUnicastResponse = true;
-                }
-                // 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.
-                if (isFirstBurst) {
-                    isFirstBurst = false;
-                    if (usePassiveMode) {
-                        queriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
-                    }
-                }
-                // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
-                // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
-                // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
-                // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
-                timeToRunNextTaskInMs = timeBetweenBurstsInMs;
-                if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
-                    timeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
-                            TIME_BETWEEN_BURSTS_MS);
-                }
-            } else {
-                timeToRunNextTaskInMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
-            }
-            return this;
-        }
-    }
-
-    private List<MdnsResponse> makeResponsesForResolve(int interfaceIndex,
-            @NonNull Network network) {
+    private List<MdnsResponse> makeResponsesForResolve(@NonNull SocketKey socketKey) {
         final List<MdnsResponse> resolveResponses = new ArrayList<>();
         for (int i = 0; i < listeners.size(); i++) {
-            final String resolveName = listeners.valueAt(i).getResolveInstanceName();
+            final String resolveName = listeners.valueAt(i).searchOptions.getResolveInstanceName();
             if (resolveName == null) {
                 continue;
             }
-            MdnsResponse knownResponse = instanceNameToResponse.get(resolveName);
+            if (CollectionUtils.any(resolveResponses,
+                    r -> MdnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
+                continue;
+            }
+            MdnsResponse knownResponse =
+                    serviceCache.getCachedService(resolveName, cacheKey);
             if (knownResponse == null) {
                 final ArrayList<String> instanceFullName = new ArrayList<>(
                         serviceTypeLabels.length + 1);
@@ -508,119 +670,135 @@
                 instanceFullName.addAll(Arrays.asList(serviceTypeLabels));
                 knownResponse = new MdnsResponse(
                         0L /* lastUpdateTime */, instanceFullName.toArray(new String[0]),
-                        interfaceIndex, network);
+                        socketKey.getInterfaceIndex(), socketKey.getNetwork());
             }
             resolveResponses.add(knownResponse);
         }
         return resolveResponses;
     }
 
+    private static boolean needSendDiscoveryQueries(
+            @NonNull ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners) {
+        // Note iterators are discouraged on ArrayMap as per its documentation
+        for (int i = 0; i < listeners.size(); i++) {
+            if (listeners.valueAt(i).searchOptions.getResolveInstanceName() == null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void tryRemoveServiceAfterTtlExpires() {
+        if (!shouldRemoveServiceAfterTtlExpires()) return;
+
+        final Iterator<MdnsResponse> iter = serviceCache.getCachedServices(cacheKey).iterator();
+        while (iter.hasNext()) {
+            MdnsResponse existingResponse = iter.next();
+            if (existingResponse.hasServiceRecord()
+                    && existingResponse.getServiceRecord()
+                    .getRemainingTTL(clock.elapsedRealtime()) == 0) {
+                serviceCache.removeService(existingResponse.getServiceInstanceName(), cacheKey);
+                notifyRemovedServiceToListeners(existingResponse, "TTL expired");
+            }
+        }
+    }
+
+    private static class QuerySentArguments {
+        private final int transactionId;
+        private final List<String> subTypes = new ArrayList<>();
+        private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
+
+        QuerySentArguments(int transactionId, @NonNull List<String> subTypes,
+                @NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs) {
+            this.transactionId = transactionId;
+            this.subTypes.addAll(subTypes);
+            this.taskArgs = taskArgs;
+        }
+    }
+
     // A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
     private class QueryTask implements Runnable {
-
-        private final QueryTaskConfig config;
-
-        QueryTask(@NonNull QueryTaskConfig config) {
-            this.config = config;
+        private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
+        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<>();
+        QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
+                @NonNull Collection<MdnsResponse> servicesToResolve,
+                @NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
+                @NonNull Collection<MdnsResponse> existingServices) {
+            this.taskArgs = taskArgs;
+            this.servicesToResolve.addAll(servicesToResolve);
+            this.subtypes.addAll(subtypes);
+            this.sendDiscoveryQueries = sendDiscoveryQueries;
+            this.existingServices.addAll(existingServices);
         }
 
         @Override
         public void run() {
-            final List<MdnsResponse> servicesToResolve;
-            final boolean sendDiscoveryQueries;
-            synchronized (lock) {
-                // The listener is requesting to resolve a service that has no info in
-                // cache. Use the provided name to generate a minimal response, so other records are
-                // queried to complete it.
-                // Only the names are used to know which queries to send, other parameters like
-                // interfaceIndex do not matter.
-                servicesToResolve = makeResponsesForResolve(
-                        0 /* interfaceIndex */, config.network);
-                sendDiscoveryQueries = servicesToResolve.size() < listeners.size();
-            }
             Pair<Integer, List<String>> result;
             try {
                 result =
                         new EnqueueMdnsQueryCallable(
                                 socketClient,
-                                createMdnsPacketWriter(),
                                 serviceType,
-                                config.subtypes,
-                                config.expectUnicastResponse,
-                                config.transactionId,
-                                config.network,
+                                subtypes,
+                                taskArgs.config.expectUnicastResponse,
+                                taskArgs.config.transactionId,
+                                taskArgs.config.socketKey,
+                                taskArgs.config.onlyUseIpv6OnIpv6OnlyNetworks,
                                 sendDiscoveryQueries,
                                 servicesToResolve,
-                                clock)
+                                clock,
+                                sharedLog,
+                                dependencies,
+                                existingServices,
+                                featureFlags.isQueryWithKnownAnswerEnabled())
                                 .call();
             } catch (RuntimeException e) {
                 sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
-                        TextUtils.join(",", config.subtypes)), e);
-                result = null;
+                        TextUtils.join(",", subtypes)), e);
+                result = Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
             }
-            synchronized (lock) {
-                if (MdnsConfigs.useSessionIdToScheduleMdnsTask()) {
-                    // In case that the task is not canceled successfully, use session ID to check
-                    // if this task should continue to schedule more.
-                    if (config.sessionId != currentSessionId) {
-                        return;
-                    }
-                }
-
-                if (MdnsConfigs.shouldCancelScanTaskWhenFutureIsNull()) {
-                    if (requestTaskFuture == null) {
-                        // If requestTaskFuture is set to null, the task is cancelled. We can't use
-                        // isCancelled() here because this QueryTask is different from the future
-                        // that is returned from executor.schedule(). See b/71646910.
-                        return;
-                    }
-                }
-                if ((result != null)) {
-                    for (int i = 0; i < listeners.size(); i++) {
-                        listeners.keyAt(i).onDiscoveryQuerySent(result.second, result.first);
-                    }
-                }
-                if (shouldRemoveServiceAfterTtlExpires()) {
-                    Iterator<MdnsResponse> iter = instanceNameToResponse.values().iterator();
-                    while (iter.hasNext()) {
-                        MdnsResponse existingResponse = iter.next();
-                        if (existingResponse.hasServiceRecord()
-                                && existingResponse
-                                .getServiceRecord()
-                                .getRemainingTTL(clock.elapsedRealtime())
-                                == 0) {
-                            iter.remove();
-                            for (int i = 0; i < listeners.size(); i++) {
-                                if (!responseMatchesOptions(existingResponse,
-                                        listeners.valueAt(i)))  {
-                                    continue;
-                                }
-                                final MdnsServiceBrowserListener listener = listeners.keyAt(i);
-                                if (existingResponse.getServiceInstanceName() != null) {
-                                    final MdnsServiceInfo serviceInfo =
-                                            buildMdnsServiceInfoFromResponse(
-                                                    existingResponse, serviceTypeLabels);
-                                    if (existingResponse.isComplete()) {
-                                        sharedLog.log("TTL expired. onServiceRemoved: "
-                                                + serviceInfo);
-                                        listener.onServiceRemoved(serviceInfo);
-                                    }
-                                    sharedLog.log("TTL expired. onServiceNameRemoved: "
-                                            + serviceInfo);
-                                    listener.onServiceNameRemoved(serviceInfo);
-                                }
-                            }
-                        }
-                    }
-                }
-                requestTaskFuture = scheduleNextRunLocked(this.config);
-            }
+            dependencies.sendMessage(
+                    handler, handler.obtainMessage(EVENT_QUERY_RESULT,
+                            new QuerySentArguments(result.first, result.second, taskArgs)));
         }
     }
 
-    @NonNull
-    private Future<?> scheduleNextRunLocked(@NonNull QueryTaskConfig lastRunConfig) {
-        QueryTaskConfig config = lastRunConfig.getConfigForNextRun();
-        return executor.schedule(new QueryTask(config), config.timeToRunNextTaskInMs, MILLISECONDS);
+    private long getMinRemainingTtl(long now) {
+        long minRemainingTtl = Long.MAX_VALUE;
+        for (MdnsResponse response : serviceCache.getCachedServices(cacheKey)) {
+            if (!response.isComplete()) {
+                continue;
+            }
+            long remainingTtl =
+                    response.getServiceRecord().getRemainingTTL(now);
+            // remainingTtl is <= 0 means the service expired.
+            if (remainingTtl <= 0) {
+                return 0;
+            }
+            if (remainingTtl < minRemainingTtl) {
+                minRemainingTtl = remainingTtl;
+            }
+        }
+        return minRemainingTtl == Long.MAX_VALUE ? 0 : minRemainingTtl;
+    }
+
+    private static long calculateTimeToNextTask(MdnsQueryScheduler.ScheduledQueryTaskArgs args,
+            long now, SharedLog sharedLog) {
+        long timeToNextTasksWithBackoffInMs = Math.max(args.timeToRun - now, 0);
+        sharedLog.log(String.format("Next run: sessionId: %d, in %d ms",
+                args.sessionId, timeToNextTasksWithBackoffInMs));
+        return timeToNextTasksWithBackoffInMs;
+    }
+
+    /**
+     * Dump ServiceTypeClient state.
+     */
+    public void dump(PrintWriter pw) {
+        ensureRunningOnHandlerThread(handler);
+        pw.println("ServiceTypeClient: Type{" + serviceType + "} " + socketKey + " with "
+                + listeners.size() + " listeners.");
     }
 }
\ No newline at end of file
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 5fd1354..653ea6c 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
@@ -21,7 +21,7 @@
 import android.net.Network;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsLogger;
+import com.android.net.module.util.SharedLog;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -37,8 +37,6 @@
  * @see MulticastSocket for javadoc of each public method.
  */
 public class MdnsSocket {
-    private static final MdnsLogger LOGGER = new MdnsLogger("MdnsSocket");
-
     static final int INTERFACE_INDEX_UNSPECIFIED = -1;
     public static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
             new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
@@ -47,19 +45,21 @@
     private final MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider;
     private final MulticastSocket multicastSocket;
     private boolean isOnIPv6OnlyNetwork;
+    private final SharedLog sharedLog;
 
     public MdnsSocket(
-            @NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider, int port)
+            @NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider, int port,
+            SharedLog sharedLog)
             throws IOException {
-        this(multicastNetworkInterfaceProvider, new MulticastSocket(port));
+        this(multicastNetworkInterfaceProvider, new MulticastSocket(port), sharedLog);
     }
 
     @VisibleForTesting
     MdnsSocket(@NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider,
-            MulticastSocket multicastSocket) throws IOException {
+            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
         // Time to live is set 255, which is similar to the jMDNS implementation.
         multicastSocket.setTimeToLive(255);
@@ -93,6 +93,10 @@
         }
         for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
             multicastSocket.joinGroup(multicastAddress, networkInterface.getNetworkInterface());
+            if (!isOnIPv6OnlyNetwork) {
+                multicastSocket.joinGroup(
+                        MULTICAST_IPV6_ADDRESS, networkInterface.getNetworkInterface());
+            }
         }
     }
 
@@ -105,13 +109,16 @@
         }
         for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
             multicastSocket.leaveGroup(multicastAddress, networkInterface.getNetworkInterface());
+            if (!isOnIPv6OnlyNetwork) {
+                multicastSocket.leaveGroup(
+                        MULTICAST_IPV6_ADDRESS, networkInterface.getNetworkInterface());
+            }
         }
     }
 
     public void close() {
         // This is a race with the use of the file descriptor (b/27403984).
         multicastSocket.close();
-        multicastNetworkInterfaceProvider.stopWatchingConnectivityChanges();
     }
 
     /**
@@ -119,10 +126,14 @@
      * cannot be determined, returns -1.
      */
     public int getInterfaceIndex() {
+        if (multicastSocket.isClosed()) {
+            sharedLog.e("Socket is closed");
+            return -1;
+        }
         try {
             return multicastSocket.getNetworkInterface().getIndex();
-        } catch (SocketException e) {
-            LOGGER.e("Failed to retrieve interface index for socket.", e);
+        } catch (SocketException | NullPointerException e) {
+            sharedLog.e("Failed to retrieve interface index for socket.", e);
             return -1;
         }
     }
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 1144d16..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,12 +25,18 @@
 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.server.connectivity.mdns.util.MdnsLogger;
+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;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.List;
@@ -54,7 +60,6 @@
     private static final String CAST_SENDER_LOG_SOURCE = "CAST_SENDER_SDK";
     private static final String CAST_PREFS_NAME = "google_cast";
     private static final String PREF_CAST_SENDER_ID = "PREF_CAST_SENDER_ID";
-    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
     private static final String MULTICAST_TYPE = "multicast";
     private static final String UNICAST_TYPE = "unicast";
 
@@ -102,8 +107,13 @@
     @Nullable private Timer logMdnsPacketTimer;
     private AtomicInteger packetsCount;
     @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) {
+    public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock,
+            SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+        this.sharedLog = sharedLog;
         this.context = context;
         this.multicastLock = multicastLock;
         if (useSeparateSocketForUnicast) {
@@ -111,6 +121,8 @@
         } else {
             unicastReceiverBuffer = null;
         }
+        this.mdnsFeatureFlags = mdnsFeatureFlags;
+        this.interfaceProvider = new MulticastNetworkInterfaceProvider(context, sharedLog);
     }
 
     @Override
@@ -122,7 +134,7 @@
     @Override
     public synchronized void startDiscovery() throws IOException {
         if (multicastSocket != null) {
-            LOGGER.w("Discovery is already in progress.");
+            sharedLog.w("Discovery is already in progress.");
             return;
         }
 
@@ -131,13 +143,14 @@
         cannotReceiveMulticastResponse.set(false);
 
         shouldStopSocketLoop = false;
+        interfaceProvider.startWatchingConnectivityChanges();
         try {
             // TODO (changed when importing code): consider setting thread stats tag
-            multicastSocket = createMdnsSocket(MdnsConstants.MDNS_PORT);
+            multicastSocket = createMdnsSocket(MdnsConstants.MDNS_PORT, sharedLog);
             multicastSocket.joinGroup();
             if (useSeparateSocketForUnicast) {
                 // For unicast, use port 0 and the system will assign it with any available port.
-                unicastSocket = createMdnsSocket(0);
+                unicastSocket = createMdnsSocket(0, sharedLog);
             }
             multicastLock.acquire();
         } catch (IOException e) {
@@ -161,7 +174,7 @@
     @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
     @Override
     public void stopDiscovery() {
-        LOGGER.log("Stop discovery.");
+        sharedLog.log("Stop discovery.");
         if (multicastSocket == null && unicastSocket == null) {
             return;
         }
@@ -176,6 +189,7 @@
         }
 
         multicastLock.release();
+        interfaceProvider.stopWatchingConnectivityChanges();
 
         shouldStopSocketLoop = true;
         waitForSendThreadToStop();
@@ -194,39 +208,23 @@
         }
     }
 
-    /** Sends a mDNS request packet that asks for multicast response. */
-    public void sendMulticastPacket(@NonNull DatagramPacket packet) {
-        sendMdnsPacket(packet, multicastPacketQueue);
+    @Override
+    public void sendPacketRequestingMulticastResponse(@NonNull List<DatagramPacket> packets,
+            boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+        sendMdnsPackets(packets, multicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
     }
 
-    /** Sends a mDNS request packet that asks for unicast response. */
-    public void sendUnicastPacket(DatagramPacket packet) {
+    @Override
+    public void sendPacketRequestingUnicastResponse(@NonNull List<DatagramPacket> packets,
+            boolean onlyUseIpv6OnIpv6OnlyNetworks) {
         if (useSeparateSocketForUnicast) {
-            sendMdnsPacket(packet, unicastPacketQueue);
+            sendMdnsPackets(packets, unicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
         } else {
-            sendMdnsPacket(packet, multicastPacketQueue);
+            sendMdnsPackets(packets, multicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
         }
     }
 
     @Override
-    public void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
-        if (network != null) {
-            throw new IllegalArgumentException("This socket client does not support sending to "
-                    + "specific networks");
-        }
-        sendMulticastPacket(packet);
-    }
-
-    @Override
-    public void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
-        if (network != null) {
-            throw new IllegalArgumentException("This socket client does not support sending to "
-                    + "specific networks");
-        }
-        sendUnicastPacket(packet);
-    }
-
-    @Override
     public void notifyNetworkRequested(
             @NonNull MdnsServiceBrowserListener listener,
             @Nullable Network network,
@@ -235,26 +233,56 @@
             throw new IllegalArgumentException("This socket client does not support requesting "
                     + "specific networks");
         }
-        socketCreationCallback.onSocketCreated(null);
+        socketCreationCallback.onSocketCreated(new SocketKey(multicastSocket.getInterfaceIndex()));
     }
 
-    private void sendMdnsPacket(DatagramPacket packet, Queue<DatagramPacket> packetQueueToUse) {
+    @Override
+    public boolean supportsRequestingSpecificNetworks() {
+        return false;
+    }
+
+    private void sendMdnsPackets(List<DatagramPacket> packets,
+            Queue<DatagramPacket> packetQueueToUse, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
         if (shouldStopSocketLoop && !MdnsConfigs.allowAddMdnsPacketAfterDiscoveryStops()) {
-            LOGGER.w("sendMdnsPacket() is called after discovery already stopped");
+            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) 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;
+        }
+        if (isIpv6 && !ipv6Only && onlyUseIpv6OnIpv6OnlyNetworks) {
+            return;
+        }
+
         synchronized (packetQueueToUse) {
-            while (packetQueueToUse.size() >= MdnsConfigs.mdnsPacketQueueMaxSize()) {
+            while ((packetQueueToUse.size() + packets.size())
+                    > MdnsConfigs.mdnsPacketQueueMaxSize()) {
                 packetQueueToUse.remove();
             }
-            packetQueueToUse.add(packet);
+            packetQueueToUse.addAll(packets);
         }
         triggerSendThread();
     }
 
     private void createAndStartSendThread() {
         if (sendThread != null) {
-            LOGGER.w("A socket thread already exists.");
+            sharedLog.w("A socket thread already exists.");
             return;
         }
         sendThread = new Thread(this::sendThreadMain);
@@ -264,7 +292,7 @@
 
     private void createAndStartReceiverThreads() {
         if (multicastReceiveThread != null) {
-            LOGGER.w("A multicast receiver thread already exists.");
+            sharedLog.w("A multicast receiver thread already exists.");
             return;
         }
         multicastReceiveThread =
@@ -286,12 +314,12 @@
     }
 
     private void triggerSendThread() {
-        LOGGER.log("Trigger send thread.");
+        sharedLog.log("Trigger send thread.");
         Thread sendThread = this.sendThread;
         if (sendThread != null) {
             sendThread.interrupt();
         } else {
-            LOGGER.w("Socket thread is null");
+            sharedLog.w("Socket thread is null");
         }
     }
 
@@ -308,9 +336,9 @@
     }
 
     private void waitForSendThreadToStop() {
-        LOGGER.log("wait For Send Thread To Stop");
+        sharedLog.log("wait For Send Thread To Stop");
         if (sendThread == null) {
-            LOGGER.w("socket thread is already dead.");
+            sharedLog.w("socket thread is already dead.");
             return;
         }
         waitForThread(sendThread);
@@ -325,7 +353,7 @@
                 thread.interrupt();
                 thread.join(waitMs);
                 if (thread.isAlive()) {
-                    LOGGER.w("Failed to join thread: " + thread);
+                    sharedLog.w("Failed to join thread: " + thread);
                 }
                 break;
             } catch (InterruptedException e) {
@@ -384,13 +412,13 @@
                 }
             }
         } finally {
-            LOGGER.log("Send thread stopped.");
+            sharedLog.log("Send thread stopped.");
             try {
                 if (multicastSocket != null) {
                     multicastSocket.leaveGroup();
                 }
             } catch (Exception t) {
-                LOGGER.e("Failed to leave the group.", t);
+                sharedLog.e("Failed to leave the group.", t);
             }
 
             // Close the socket first. This is the only way to interrupt a blocking receive.
@@ -403,7 +431,7 @@
                     unicastSocket.close();
                 }
             } catch (RuntimeException t) {
-                LOGGER.e("Failed to close the mdns socket.", t);
+                sharedLog.e("Failed to close the mdns socket.", t);
             }
         }
     }
@@ -433,11 +461,11 @@
                 }
             } catch (IOException e) {
                 if (!shouldStopSocketLoop) {
-                    LOGGER.e("Failed to receive mDNS packets.", e);
+                    sharedLog.e("Failed to receive mDNS packets.", e);
                 }
             }
         }
-        LOGGER.log("Receive thread stopped.");
+        sharedLog.log("Receive thread stopped.");
     }
 
     private int processResponsePacket(@NonNull DatagramPacket packet, String responseType,
@@ -446,12 +474,14 @@
 
         final MdnsPacket response;
         try {
-            response = MdnsResponseDecoder.parseResponse(packet.getData(), packet.getLength());
+            response = MdnsResponseDecoder.parseResponse(
+                    packet.getData(), packet.getLength(), mdnsFeatureFlags);
         } catch (MdnsPacket.ParseException e) {
-            LOGGER.w(String.format("Error while decoding %s packet (%d): %d",
+            sharedLog.w(String.format("Error while decoding %s packet (%d): %d",
                     responseType, packetNumber, e.code));
             if (callback != null) {
-                callback.onFailedToParseMdnsResponse(packetNumber, e.code, network);
+                callback.onFailedToParseMdnsResponse(packetNumber, e.code,
+                        new SocketKey(network, interfaceIndex));
             }
             return e.code;
         }
@@ -461,15 +491,16 @@
         }
 
         if (callback != null) {
-            callback.onResponseReceived(response, interfaceIndex, network);
+            callback.onResponseReceived(
+                    response, new SocketKey(network, interfaceIndex));
         }
 
         return MdnsResponseErrorCode.SUCCESS;
     }
 
     @VisibleForTesting
-    MdnsSocket createMdnsSocket(int port) throws IOException {
-        return new MdnsSocket(new MulticastNetworkInterfaceProvider(context), port);
+    MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) throws IOException {
+        return new MdnsSocket(interfaceProvider, port, sharedLog);
     }
 
     private void sendPackets(List<DatagramPacket> packets, MdnsSocket socket) {
@@ -479,7 +510,7 @@
                 break;
             }
             try {
-                LOGGER.log("Sending a %s mDNS packet...", requestType);
+                sharedLog.log(String.format("Sending a %s mDNS packet...", requestType));
                 socket.send(packet);
 
                 // Start the timer task to monitor the response.
@@ -508,7 +539,7 @@
                                                 }
                                                 if ((!receivedMulticastResponse)
                                                         && receivedUnicastResponse) {
-                                                    LOGGER.e(String.format(
+                                                    sharedLog.e(String.format(
                                                             "Haven't received multicast response"
                                                                     + " in the last %d ms.",
                                                             checkMulticastResponseIntervalMs));
@@ -523,13 +554,9 @@
                     }
                 }
             } catch (IOException e) {
-                LOGGER.e(String.format("Failed to send a %s mDNS packet.", requestType), e);
+                sharedLog.e(String.format("Failed to send a %s mDNS packet.", requestType), e);
             }
         }
         packets.clear();
     }
-
-    public boolean isOnIPv6OnlyNetwork() {
-        return multicastSocket != null && multicastSocket.isOnIPv6OnlyNetwork();
-    }
 }
\ No newline at end of file
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 deadc58..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,20 +41,16 @@
     void setCallback(@Nullable Callback callback);
 
     /**
-     * Send a mDNS request packet via given network that asks for multicast 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.
+     * Send mDNS request packets via given network that asks for multicast response.
      */
-    void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network);
+    void sendPacketRequestingMulticastResponse(@NonNull List<DatagramPacket> packets,
+            boolean onlyUseIpv6OnIpv6OnlyNetworks);
 
     /**
-     * Send a mDNS request packet via given network 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.
+     * Send mDNS request packets via given network that asks for unicast response.
      */
-    void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network);
+    void sendPacketRequestingUnicastResponse(@NonNull List<DatagramPacket> packets,
+            boolean onlyUseIpv6OnIpv6OnlyNetworks);
 
     /*** Notify that the given network is requested for mdns discovery / resolution */
     void notifyNetworkRequested(@NonNull MdnsServiceBrowserListener listener,
@@ -67,23 +64,25 @@
         return null;
     }
 
+    /** Returns whether the socket client support requesting per network */
+    boolean supportsRequestingSpecificNetworks();
+
     /*** Callback for mdns response  */
     interface Callback {
         /*** Receive a mdns response */
-        void onResponseReceived(@NonNull MdnsPacket packet, int interfaceIndex,
-                @Nullable Network network);
+        void onResponseReceived(@NonNull MdnsPacket packet, @NonNull SocketKey socketKey);
 
         /*** Parse a mdns response failed */
         void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
-                @Nullable Network network);
+                @NonNull SocketKey socketKey);
     }
 
     /*** Callback for requested socket creation  */
     interface SocketCreationCallback {
         /*** Notify requested socket is created */
-        void onSocketCreated(@Nullable Network network);
+        void onSocketCreated(@NonNull SocketKey socketKey);
 
         /*** Notify requested socket is destroyed */
-        void onAllSocketsDestroyed(@Nullable Network network);
+        void onSocketDestroyed(@NonNull SocketKey socketKey);
     }
-}
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
index e67c655..5c9ec09 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
@@ -19,12 +19,12 @@
 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.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.isNetworkMatched;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -41,10 +41,10 @@
 import android.net.wifi.p2p.WifiP2pGroup;
 import android.net.wifi.p2p.WifiP2pInfo;
 import android.net.wifi.p2p.WifiP2pManager;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.ArrayMap;
-import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -68,6 +68,7 @@
  * to their default value (0, false or null).
  *
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsSocketProvider {
     private static final String TAG = MdnsSocketProvider.class.getSimpleName();
     private static final boolean DBG = MdnsDiscoveryManager.DBG;
@@ -82,7 +83,7 @@
     @NonNull private final Dependencies mDependencies;
     @NonNull private final NetworkCallback mNetworkCallback;
     @NonNull private final TetheringEventCallback mTetheringEventCallback;
-    @NonNull private final AbstractSocketNetlink mSocketNetlinkMonitor;
+    @NonNull private final AbstractSocketNetlinkMonitor mSocketNetlinkMonitor;
     @NonNull private final SharedLog mSharedLog;
     private final ArrayMap<Network, SocketInfo> mNetworkSockets = new ArrayMap<>();
     private final ArrayMap<String, SocketInfo> mTetherInterfaceSockets = new ArrayMap<>();
@@ -97,6 +98,8 @@
     // the netlink monitor is never stop and the old states must be kept.
     private final SparseArray<LinkProperties> mIfaceIdxToLinkProperties = new SparseArray<>();
     private final byte[] mPacketReadBuffer = new byte[READ_BUFFER_SIZE];
+    @NonNull
+    private final SocketRequestMonitor mSocketRequestMonitor;
     private boolean mMonitoringSockets = false;
     private boolean mRequestStop = false;
     private String mWifiP2pTetherInterface = null;
@@ -116,7 +119,7 @@
 
             if (mWifiP2pTetherInterface != null) {
                 if (newP2pIface != null) {
-                    Log.wtf(TAG, "Wifi p2p interface is changed from " + mWifiP2pTetherInterface
+                    mSharedLog.wtf("Wifi p2p interface is changed from " + mWifiP2pTetherInterface
                             + " to " + newP2pIface + " without null broadcast");
                 }
                 // Remove the socket.
@@ -131,7 +134,7 @@
             if (newP2pIface != null && !socketAlreadyExists) {
                 // Create a socket for wifi p2p interface.
                 final int ifaceIndex =
-                        mDependencies.getNetworkInterfaceIndexByName(newP2pIface);
+                        mDependencies.getNetworkInterfaceIndexByName(newP2pIface, mSharedLog);
                 createSocket(LOCAL_NET, createLPForTetheredInterface(newP2pIface, ifaceIndex));
             }
         }
@@ -155,17 +158,20 @@
     }
 
     public MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper,
-            @NonNull SharedLog sharedLog) {
-        this(context, looper, new Dependencies(), sharedLog);
+            @NonNull SharedLog sharedLog,
+            @NonNull SocketRequestMonitor socketRequestMonitor) {
+        this(context, looper, new Dependencies(), sharedLog, socketRequestMonitor);
     }
 
     MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper,
-            @NonNull Dependencies deps, @NonNull SharedLog sharedLog) {
+            @NonNull Dependencies deps, @NonNull SharedLog sharedLog,
+            @NonNull SocketRequestMonitor socketRequestMonitor) {
         mContext = context;
         mLooper = looper;
         mHandler = new Handler(looper);
         mDependencies = deps;
         mSharedLog = sharedLog;
+        mSocketRequestMonitor = socketRequestMonitor;
         mNetworkCallback = new NetworkCallback() {
             @Override
             public void onLost(Network network) {
@@ -228,27 +234,30 @@
         /*** Create a MdnsInterfaceSocket */
         public MdnsInterfaceSocket createMdnsInterfaceSocket(
                 @NonNull NetworkInterface networkInterface, int port, @NonNull Looper looper,
-                @NonNull byte[] packetReadBuffer) throws IOException {
-            return new MdnsInterfaceSocket(networkInterface, port, looper, packetReadBuffer);
+                @NonNull byte[] packetReadBuffer, @NonNull SharedLog sharedLog) throws IOException {
+            return new MdnsInterfaceSocket(networkInterface, port, looper, packetReadBuffer,
+                    sharedLog);
         }
 
         /*** Get network interface by given interface name */
-        public int getNetworkInterfaceIndexByName(@NonNull final String ifaceName) {
+        public int getNetworkInterfaceIndexByName(@NonNull final String ifaceName,
+                @NonNull SharedLog sharedLog) {
             final NetworkInterface iface;
             try {
                 iface = NetworkInterface.getByName(ifaceName);
             } catch (SocketException e) {
-                Log.e(TAG, "Error querying interface", e);
+                sharedLog.e("Error querying interface", e);
                 return IFACE_IDX_NOT_EXIST;
             }
             if (iface == null) {
-                Log.e(TAG, "Interface not found: " + ifaceName);
+                sharedLog.e("Interface not found: " + ifaceName);
                 return IFACE_IDX_NOT_EXIST;
             }
             return iface.getIndex();
         }
         /*** Creates a SocketNetlinkMonitor */
-        public AbstractSocketNetlink createSocketNetlinkMonitor(@NonNull final Handler handler,
+        public AbstractSocketNetlinkMonitor createSocketNetlinkMonitor(
+                @NonNull final Handler handler,
                 @NonNull final SharedLog log,
                 @NonNull final NetLinkMonitorCallBack cb) {
             return SocketNetLinkMonitorFactory.createNetLinkMonitor(handler, log, cb);
@@ -312,10 +321,15 @@
     private static class SocketInfo {
         final MdnsInterfaceSocket mSocket;
         final List<LinkAddress> mAddresses;
+        final int[] mTransports;
+        @NonNull final SocketKey mSocketKey;
 
-        SocketInfo(MdnsInterfaceSocket socket, List<LinkAddress> addresses) {
+        SocketInfo(MdnsInterfaceSocket socket, List<LinkAddress> addresses, int[] transports,
+                @NonNull SocketKey socketKey) {
             mSocket = socket;
             mAddresses = new ArrayList<>(addresses);
+            mTransports = transports;
+            mSocketKey = socketKey;
         }
     }
 
@@ -324,7 +338,7 @@
         ensureRunningOnHandlerThread(mHandler);
         mRequestStop = false; // Reset stop request flag.
         if (mMonitoringSockets) {
-            Log.d(TAG, "Already monitoring sockets.");
+            mSharedLog.v("Already monitoring sockets.");
             return;
         }
         mSharedLog.i("Start monitoring sockets.");
@@ -379,7 +393,7 @@
     public void requestStopWhenInactive() {
         ensureRunningOnHandlerThread(mHandler);
         if (!mMonitoringSockets) {
-            Log.d(TAG, "Monitoring sockets hasn't been started.");
+            mSharedLog.v("Monitoring sockets hasn't been started.");
             return;
         }
         mRequestStop = true;
@@ -399,7 +413,7 @@
         mActiveNetworksLinkProperties.put(network, lp);
         if (!matchRequestedNetwork(network)) {
             if (DBG) {
-                Log.d(TAG, "Ignore LinkProperties change. There is no request for the"
+                mSharedLog.v("Ignore LinkProperties change. There is no request for the"
                         + " Network:" + network);
             }
             return;
@@ -417,7 +431,7 @@
             @NonNull final List<LinkAddress> updatedAddresses) {
         for (int i = 0; i < mTetherInterfaceSockets.size(); ++i) {
             String tetheringInterfaceName = mTetherInterfaceSockets.keyAt(i);
-            if (mDependencies.getNetworkInterfaceIndexByName(tetheringInterfaceName)
+            if (mDependencies.getNetworkInterfaceIndexByName(tetheringInterfaceName, mSharedLog)
                     == ifaceIndex) {
                 updateSocketInfoAddress(null /* network */,
                         mTetherInterfaceSockets.valueAt(i), updatedAddresses);
@@ -435,7 +449,7 @@
         // Try to join the group again.
         socketInfo.mSocket.joinGroup(addresses);
 
-        notifyAddressesChanged(network, socketInfo.mSocket, addresses);
+        notifyAddressesChanged(network, socketInfo, addresses);
     }
     private LinkProperties createLPForTetheredInterface(@NonNull final String interfaceName,
             int ifaceIndex) {
@@ -451,7 +465,7 @@
             // tethering are only created if there is a request for all networks (interfaces).
             // Therefore, only update the interface list and skip this change if no such request.
             if (DBG) {
-                Log.d(TAG, "Ignore tether interfaces change. There is no request for all"
+                mSharedLog.v("Ignore tether interfaces change. There is no request for all"
                         + " networks.");
             }
             current.clear();
@@ -471,7 +485,7 @@
                 continue;
             }
 
-            int ifaceIndex = mDependencies.getNetworkInterfaceIndexByName(name);
+            int ifaceIndex = mDependencies.getNetworkInterfaceIndexByName(name, mSharedLog);
             createSocket(LOCAL_NET, createLPForTetheredInterface(name, ifaceIndex));
         }
         for (String name : interfaceDiff.removed) {
@@ -484,7 +498,7 @@
     private void createSocket(NetworkKey networkKey, LinkProperties lp) {
         final String interfaceName = lp.getInterfaceName();
         if (interfaceName == null) {
-            Log.e(TAG, "Can not create socket with null interface name.");
+            mSharedLog.e("Can not create socket with null interface name.");
             return;
         }
 
@@ -498,8 +512,14 @@
             if (networkKey == LOCAL_NET) {
                 transports = new int[0];
             } else {
-                transports = mActiveNetworksTransports.getOrDefault(
-                        ((NetworkAsKey) networkKey).mNetwork, new int[0]);
+                final int[] knownTransports =
+                        mActiveNetworksTransports.get(((NetworkAsKey) networkKey).mNetwork);
+                if (knownTransports != null) {
+                    transports = knownTransports;
+                } else {
+                    mSharedLog.wtf("transports is missing for key: " + networkKey);
+                    transports = new int[0];
+                }
             }
             if (networkInterface == null || !isMdnsCapableInterface(networkInterface, transports)) {
                 return;
@@ -508,23 +528,26 @@
             mSharedLog.log("Create socket on net:" + networkKey + ", ifName:" + interfaceName);
             final MdnsInterfaceSocket socket = mDependencies.createMdnsInterfaceSocket(
                     networkInterface.getNetworkInterface(), MdnsConstants.MDNS_PORT, mLooper,
-                    mPacketReadBuffer);
+                    mPacketReadBuffer, mSharedLog.forSubComponent(
+                            MdnsInterfaceSocket.class.getSimpleName() + "/" + interfaceName));
             final List<LinkAddress> addresses = lp.getLinkAddresses();
+            final Network network =
+                    networkKey == LOCAL_NET ? null : ((NetworkAsKey) networkKey).mNetwork;
+            final SocketKey socketKey = new SocketKey(network, networkInterface.getIndex());
+            // TODO: technically transport types are mutable, although generally not in ways that
+            // would meaningfully impact the logic using it here. Consider updating logic to
+            // support transports being added/removed.
+            final SocketInfo socketInfo = new SocketInfo(socket, addresses, transports, socketKey);
             if (networkKey == LOCAL_NET) {
-                mTetherInterfaceSockets.put(interfaceName, new SocketInfo(socket, addresses));
+                mTetherInterfaceSockets.put(interfaceName, socketInfo);
             } else {
-                mNetworkSockets.put(((NetworkAsKey) networkKey).mNetwork,
-                        new SocketInfo(socket, addresses));
+                mNetworkSockets.put(network, socketInfo);
             }
             // Try to join IPv4/IPv6 group.
             socket.joinGroup(addresses);
 
             // Notify the listeners which need this socket.
-            if (networkKey == LOCAL_NET) {
-                notifySocketCreated(null /* network */, socket, addresses);
-            } else {
-                notifySocketCreated(((NetworkAsKey) networkKey).mNetwork, socket, addresses);
-            }
+            notifySocketCreated(network, socketInfo);
         } catch (IOException e) {
             mSharedLog.e("Create socket failed ifName:" + interfaceName, e);
         }
@@ -565,7 +588,8 @@
         if (socketInfo == null) return;
 
         socketInfo.mSocket.destroy();
-        notifyInterfaceDestroyed(network, socketInfo.mSocket);
+        notifyInterfaceDestroyed(network, socketInfo);
+        mSocketRequestMonitor.onSocketDestroyed(network, socketInfo.mSocket);
         mSharedLog.log("Remove socket on net:" + network);
     }
 
@@ -573,36 +597,40 @@
         final SocketInfo socketInfo = mTetherInterfaceSockets.remove(interfaceName);
         if (socketInfo == null) return;
         socketInfo.mSocket.destroy();
-        notifyInterfaceDestroyed(null /* network */, socketInfo.mSocket);
+        notifyInterfaceDestroyed(null /* network */, socketInfo);
+        mSocketRequestMonitor.onSocketDestroyed(null /* network */, socketInfo.mSocket);
         mSharedLog.log("Remove socket on ifName:" + interfaceName);
     }
 
-    private void notifySocketCreated(Network network, MdnsInterfaceSocket socket,
-            List<LinkAddress> addresses) {
+    private void notifySocketCreated(Network network, SocketInfo socketInfo) {
         for (int i = 0; i < mCallbacksToRequestedNetworks.size(); i++) {
             final Network requestedNetwork = mCallbacksToRequestedNetworks.valueAt(i);
             if (isNetworkMatched(requestedNetwork, network)) {
-                mCallbacksToRequestedNetworks.keyAt(i).onSocketCreated(network, socket, addresses);
+                mCallbacksToRequestedNetworks.keyAt(i).onSocketCreated(socketInfo.mSocketKey,
+                        socketInfo.mSocket, socketInfo.mAddresses);
+                mSocketRequestMonitor.onSocketRequestFulfilled(network, socketInfo.mSocket,
+                        socketInfo.mTransports);
             }
         }
     }
 
-    private void notifyInterfaceDestroyed(Network network, MdnsInterfaceSocket socket) {
+    private void notifyInterfaceDestroyed(Network network, SocketInfo socketInfo) {
         for (int i = 0; i < mCallbacksToRequestedNetworks.size(); i++) {
             final Network requestedNetwork = mCallbacksToRequestedNetworks.valueAt(i);
             if (isNetworkMatched(requestedNetwork, network)) {
-                mCallbacksToRequestedNetworks.keyAt(i).onInterfaceDestroyed(network, socket);
+                mCallbacksToRequestedNetworks.keyAt(i)
+                        .onInterfaceDestroyed(socketInfo.mSocketKey, socketInfo.mSocket);
             }
         }
     }
 
-    private void notifyAddressesChanged(Network network, MdnsInterfaceSocket socket,
+    private void notifyAddressesChanged(Network network, SocketInfo socketInfo,
             List<LinkAddress> addresses) {
         for (int i = 0; i < mCallbacksToRequestedNetworks.size(); i++) {
             final Network requestedNetwork = mCallbacksToRequestedNetworks.valueAt(i);
             if (isNetworkMatched(requestedNetwork, network)) {
                 mCallbacksToRequestedNetworks.keyAt(i)
-                        .onAddressesChanged(network, socket, addresses);
+                        .onAddressesChanged(socketInfo.mSocketKey, socketInfo.mSocket, addresses);
             }
         }
     }
@@ -613,27 +641,31 @@
             final LinkProperties lp = mActiveNetworksLinkProperties.get(network);
             if (lp == null) {
                 // The requested network is not existed. Maybe wait for LinkProperties change later.
-                if (DBG) Log.d(TAG, "There is no LinkProperties for this network:" + network);
+                if (DBG) mSharedLog.v("There is no LinkProperties for this network:" + network);
                 return;
             }
             createSocket(new NetworkAsKey(network), lp);
         } else {
             // Notify the socket for requested network.
-            cb.onSocketCreated(network, socketInfo.mSocket, socketInfo.mAddresses);
+            cb.onSocketCreated(socketInfo.mSocketKey, socketInfo.mSocket, socketInfo.mAddresses);
+            mSocketRequestMonitor.onSocketRequestFulfilled(network, socketInfo.mSocket,
+                    socketInfo.mTransports);
         }
     }
 
     private void retrieveAndNotifySocketFromInterface(String interfaceName, SocketCallback cb) {
         final SocketInfo socketInfo = mTetherInterfaceSockets.get(interfaceName);
         if (socketInfo == null) {
-            int ifaceIndex = mDependencies.getNetworkInterfaceIndexByName(interfaceName);
+            int ifaceIndex = mDependencies.getNetworkInterfaceIndexByName(interfaceName,
+                    mSharedLog);
             createSocket(
                     LOCAL_NET,
                     createLPForTetheredInterface(interfaceName, ifaceIndex));
         } else {
             // Notify the socket for requested network.
-            cb.onSocketCreated(
-                    null /* network */, socketInfo.mSocket, socketInfo.mAddresses);
+            cb.onSocketCreated(socketInfo.mSocketKey, socketInfo.mSocket, socketInfo.mAddresses);
+            mSocketRequestMonitor.onSocketRequestFulfilled(null /* socketNetwork */,
+                    socketInfo.mSocket, socketInfo.mTransports);
         }
     }
 
@@ -688,6 +720,7 @@
             if (matchRequestedNetwork(network)) continue;
             final SocketInfo info = mNetworkSockets.removeAt(i);
             info.mSocket.destroy();
+            mSocketRequestMonitor.onSocketDestroyed(network, info.mSocket);
             mSharedLog.log("Remove socket on net:" + network + " after unrequestSocket");
         }
 
@@ -697,6 +730,7 @@
         for (int i = mTetherInterfaceSockets.size() - 1; i >= 0; i--) {
             final SocketInfo info = mTetherInterfaceSockets.valueAt(i);
             info.mSocket.destroy();
+            mSocketRequestMonitor.onSocketDestroyed(null /* network */, info.mSocket);
             mSharedLog.log("Remove socket on ifName:" + mTetherInterfaceSockets.keyAt(i)
                     + " after unrequestSocket");
         }
@@ -707,17 +741,59 @@
     }
 
 
-    /*** Callbacks for listening socket changes */
+    /**
+     * Callback used to register socket requests.
+     */
     public interface SocketCallback {
-        /*** Notify the socket is created */
-        default void onSocketCreated(@Nullable Network network, @NonNull MdnsInterfaceSocket socket,
-                @NonNull List<LinkAddress> addresses) {}
-        /*** Notify the interface is destroyed */
-        default void onInterfaceDestroyed(@Nullable Network network,
-                @NonNull MdnsInterfaceSocket socket) {}
-        /*** Notify the addresses is changed on the network */
-        default void onAddressesChanged(@Nullable Network network,
+        /**
+         * Notify the socket was created for the registered request.
+         *
+         * This may be called immediately when the request is registered with an existing socket,
+         * if it had been created previously for other requests.
+         */
+        default void onSocketCreated(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) {}
+
+        /**
+         * Notify that the interface was destroyed, so the provided socket cannot be used anymore.
+         *
+         * This indicates that although the socket was still requested, it had to be destroyed.
+         */
+        default void onInterfaceDestroyed(@NonNull SocketKey socketKey,
+                @NonNull MdnsInterfaceSocket socket) {}
+
+        /**
+         * Notify the interface addresses have changed for the network.
+         */
+        default void onAddressesChanged(@NonNull SocketKey socketKey,
+                @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) {}
+    }
+
+    /**
+     * Global callback indicating when sockets are created or destroyed for requests.
+     */
+    public interface SocketRequestMonitor {
+        /**
+         * Indicates that the socket was used to fulfill the request of one requester.
+         *
+         * There is always at most one socket created for each interface. The interface is available
+         * in {@link MdnsInterfaceSocket#getInterface()}.
+         * @param socketNetwork The network of the socket interface, if any.
+         * @param socket The socket that was provided to a requester.
+         * @param transports Array of TRANSPORT_* from {@link NetworkCapabilities}. Empty if the
+         *                   interface is not part of a network with known transports.
+         */
+        default void onSocketRequestFulfilled(@Nullable Network socketNetwork,
+                @NonNull MdnsInterfaceSocket socket, @NonNull int[] transports) {}
+
+        /**
+         * Indicates that a previously created socket was destroyed.
+         *
+         * @param socketNetwork The network of the socket interface, if any.
+         * @param socket The destroyed socket.
+         */
+        default void onSocketDestroyed(@Nullable Network socketNetwork,
+                @NonNull MdnsInterfaceSocket socket) {}
     }
 
     private interface NetworkKey {
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 4149dbe..92cf324 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -18,7 +18,8 @@
 
 import android.annotation.Nullable;
 
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
 
 import java.io.IOException;
@@ -28,7 +29,7 @@
 import java.util.Objects;
 
 /** An mDNS "TXT" record, which contains a list of {@link TextEntry}. */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
 public class MdnsTextRecord extends MdnsRecord {
     private List<TextEntry> entries;
 
@@ -41,6 +42,12 @@
         super(name, TYPE_TXT, reader, isQuestion);
     }
 
+    public MdnsTextRecord(String[] name, boolean isUnicast) {
+        super(name, TYPE_TXT,
+                MdnsConstants.QCLASS_INTERNET | (isUnicast ? MdnsConstants.QCLASS_UNICAST : 0),
+                0L /* receiptTimeMillis */, false /* cacheFlush */, 0L /* ttlMillis */);
+    }
+
     public MdnsTextRecord(String[] name, long receiptTimeMillis, boolean cacheFlush, long ttlMillis,
             List<TextEntry> entries) {
         super(name, TYPE_TXT, MdnsConstants.QCLASS_INTERNET, receiptTimeMillis, cacheFlush,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java b/service-t/src/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
index f248c98..da82e96 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
@@ -22,7 +22,7 @@
 import android.net.Network;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsLogger;
+import com.android.net.module.util.SharedLog;
 
 import java.io.IOException;
 import java.net.Inet4Address;
@@ -41,7 +41,7 @@
 public class MulticastNetworkInterfaceProvider {
 
     private static final String TAG = "MdnsNIProvider";
-    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+    private final SharedLog sharedLog;
     private static final boolean PREFER_IPV6 = MdnsConfigs.preferIpv6();
 
     private final List<NetworkInterfaceWrapper> multicastNetworkInterfaces = new ArrayList<>();
@@ -51,10 +51,12 @@
     private volatile boolean connectivityChanged = true;
 
     @SuppressWarnings("nullness:methodref.receiver.bound")
-    public MulticastNetworkInterfaceProvider(@NonNull Context context) {
+    public MulticastNetworkInterfaceProvider(@NonNull Context context,
+            @NonNull SharedLog sharedLog) {
+        this.sharedLog = sharedLog;
         // IMPORT CHANGED
         this.connectivityMonitor = new ConnectivityMonitorWithConnectivityManager(
-                context, this::onConnectivityChanged);
+                context, this::onConnectivityChanged, sharedLog);
     }
 
     private synchronized void onConnectivityChanged() {
@@ -83,7 +85,7 @@
             connectivityChanged = false;
             updateMulticastNetworkInterfaces();
             if (multicastNetworkInterfaces.isEmpty()) {
-                LOGGER.log("No network interface available for mDNS scanning.");
+                sharedLog.log("No network interface available for mDNS scanning.");
             }
         }
         return new ArrayList<>(multicastNetworkInterfaces);
@@ -93,7 +95,7 @@
         multicastNetworkInterfaces.clear();
         List<NetworkInterfaceWrapper> networkInterfaceWrappers = getNetworkInterfaces();
         for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaceWrappers) {
-            if (canScanOnInterface(interfaceWrapper)) {
+            if (canScanOnInterface(interfaceWrapper, sharedLog)) {
                 multicastNetworkInterfaces.add(interfaceWrapper);
             }
         }
@@ -133,10 +135,10 @@
                 }
             }
         } catch (SocketException e) {
-            LOGGER.e("Failed to get network interfaces.", e);
+            sharedLog.e("Failed to get network interfaces.", e);
         } catch (NullPointerException e) {
             // Android R has a bug that could lead to a NPE. See b/159277702.
-            LOGGER.e("Failed to call getNetworkInterfaces API", e);
+            sharedLog.e("Failed to call getNetworkInterfaces API", e);
         }
 
         return networkInterfaceWrappers;
@@ -148,7 +150,8 @@
     }
 
     /*** Check whether given network interface can support mdns */
-    private static boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) {
+    private static boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface,
+            @NonNull SharedLog sharedLog) {
         try {
             if ((networkInterface == null)
                     || networkInterface.isLoopback()
@@ -160,7 +163,7 @@
             }
             return hasInet4Address(networkInterface) || hasInet6Address(networkInterface);
         } catch (IOException e) {
-            LOGGER.e(String.format("Failed to check interface %s.",
+            sharedLog.e(String.format("Failed to check interface %s.",
                     networkInterface.getNetworkInterface().getDisplayName()), e);
         }
 
diff --git a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
index 63119ac..70451f3 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
@@ -22,9 +22,9 @@
 import android.os.Handler;
 import android.os.ParcelFileDescriptor;
 import android.system.Os;
-import android.util.ArraySet;
 
 import com.android.net.module.util.FdEventsReader;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.FileDescriptor;
 import java.net.InetSocketAddress;
@@ -39,9 +39,15 @@
     @NonNull
     private final Handler mHandler;
     @NonNull
-    private final Set<PacketHandler> mPacketHandlers = new ArraySet<>();
+    private final Set<PacketHandler> mPacketHandlers = MdnsUtils.newSet();
 
     interface PacketHandler {
+        /**
+         * Handle an incoming packet.
+         *
+         * The recvbuf and src <b>will be reused and modified</b> after this method returns, so
+         * implementers must ensure that they are not accessed after handlePacket returns.
+         */
         void handlePacket(byte[] recvbuf, int length, InetSocketAddress src);
     }
 
diff --git a/service-t/src/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java b/service-t/src/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
index 0ecae48..48c396e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
+++ b/service-t/src/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
@@ -57,6 +57,10 @@
         return networkInterface.getInterfaceAddresses();
     }
 
+    public int getIndex() {
+        return networkInterface.getIndex();
+    }
+
     @Override
     public String toString() {
         return networkInterface.toString();
diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
new file mode 100644
index 0000000..0894166
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -0,0 +1,196 @@
+/*
+ * 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.connectivity.mdns;
+
+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;
+
+/**
+ * A configuration for the PeriodicalQueryTask that contains parameters to build a query packet.
+ * Call to getConfigForNextRun returns a config that can be used to build the next query task.
+ */
+public class QueryTaskConfig {
+
+    private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
+            (int) MdnsConfigs.initialTimeBetweenBurstsMs();
+    private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS =
+            (int) MdnsConfigs.timeBetweenBurstsMs();
+    private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
+    private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
+            (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
+    private static final int QUERIES_PER_BURST_PASSIVE_MODE =
+            (int) MdnsConfigs.queriesPerBurstPassive();
+    private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
+    @VisibleForTesting
+    // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
+    static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
+    @VisibleForTesting
+    // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
+    // chances that a query will be received if devices are using a DTIM multiplier (in which case
+    // they only listen once every [multiplier] DTIM intervals).
+    static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
+    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
+    final boolean expectUnicastResponse;
+    private final int queriesPerBurst;
+    private final int timeBetweenBurstsInMs;
+    private final int burstCounter;
+    final long delayUntilNextTaskWithoutBackoffMs;
+    private final boolean isFirstBurst;
+    private final long queryCount;
+    @NonNull
+    final SocketKey socketKey;
+
+    QueryTaskConfig(@NonNull QueryTaskConfig other, 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;
+        this.timeBetweenBurstsInMs = timeBetweenBurstsInMs;
+        this.burstCounter = burstCounter;
+        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;
+        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) {
+            this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
+            this.delayUntilNextTaskWithoutBackoffMs =
+                    TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
+        } else if (this.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.
+            this.timeBetweenBurstsInMs = MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
+            this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+        } else {
+            // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+            // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+            // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+            // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+            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) {
+        if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
+            return 0;
+        }
+        if (isLastQueryInBurst) {
+            return timeBetweenBurstsInMs;
+        }
+        return queryMode == AGGRESSIVE_QUERY_MODE
+                ? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS
+                : TIME_BETWEEN_QUERIES_IN_BURST_MS;
+    }
+
+    boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst) {
+        if (!isLastQueryInBurst) {
+            return false;
+        }
+        if (queryMode == AGGRESSIVE_QUERY_MODE) {
+            return true;
+        }
+        return alwaysAskForUnicastResponse;
+    }
+
+    int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst) {
+        if (!isLastQueryInBurst) {
+            return timeBetweenBurstsInMs;
+        }
+        final int maxTimeBetweenBursts = queryMode == AGGRESSIVE_QUERY_MODE
+                ? MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS : MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
+        return Math.min(timeBetweenBurstsInMs * 2, maxTimeBetweenBursts);
+    }
+
+    /**
+     * Get new QueryTaskConfig for next run.
+     */
+    public QueryTaskConfig getConfigForNextRun() {
+        long newQueryCount = queryCount + 1;
+        int newTransactionId = transactionId + 1;
+        if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
+            newTransactionId = 1;
+        }
+
+        int newQueriesPerBurst = queriesPerBurst;
+        int newBurstCounter = burstCounter + 1;
+        final boolean isFirstQueryInBurst = newBurstCounter == 1;
+        final boolean isLastQueryInBurst = newBurstCounter == queriesPerBurst;
+        boolean newIsFirstBurst = isFirstBurst && !isLastQueryInBurst;
+        if (isLastQueryInBurst) {
+            newBurstCounter = 0;
+            // 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.
+            if (isFirstBurst && queryMode == PASSIVE_QUERY_MODE) {
+                newQueriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
+            }
+        }
+
+        return new QueryTaskConfig(this, newQueryCount, newTransactionId,
+                getNextExpectUnicastResponse(isLastQueryInBurst), newIsFirstBurst, newBurstCounter,
+                newQueriesPerBurst, getNextTimeBetweenBurstsMs(isLastQueryInBurst),
+                getDelayUntilNextTaskWithoutBackoff(isFirstQueryInBurst, isLastQueryInBurst));
+    }
+
+    /**
+     * Determine if the query backoff should be used.
+     */
+    public boolean shouldUseQueryBackoff() {
+        // Don't enable backoff mode during the burst or in the first burst
+        if (burstCounter != 0 || isFirstBurst) {
+            return false;
+        }
+        return queryCount > numOfQueriesBeforeBackoff;
+    }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/SocketKey.java b/service-t/src/com/android/server/connectivity/mdns/SocketKey.java
new file mode 100644
index 0000000..f13d0e0
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/SocketKey.java
@@ -0,0 +1,73 @@
+/*
+ * 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.connectivity.mdns;
+
+import android.annotation.Nullable;
+import android.net.Network;
+
+import java.util.Objects;
+
+/**
+ * A class that identifies a socket.
+ *
+ * <p> A socket is typically created with an associated network. However, tethering interfaces do
+ * not have an associated network, only an interface index. This means that the socket cannot be
+ * identified in some places. Therefore, this class is necessary for identifying a socket. It
+ * includes both the network and interface index.
+ */
+public class SocketKey {
+    @Nullable
+    private final Network mNetwork;
+    private final int mInterfaceIndex;
+
+    SocketKey(int interfaceIndex) {
+        this(null /* network */, interfaceIndex);
+    }
+
+    SocketKey(@Nullable Network network, int interfaceIndex) {
+        mNetwork = network;
+        mInterfaceIndex = interfaceIndex;
+    }
+
+    @Nullable
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
+    public int getInterfaceIndex() {
+        return mInterfaceIndex;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mNetwork, mInterfaceIndex);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (!(other instanceof SocketKey)) {
+            return false;
+        }
+        return Objects.equals(mNetwork, ((SocketKey) other).mNetwork)
+                && mInterfaceIndex == ((SocketKey) other).mInterfaceIndex;
+    }
+
+    @Override
+    public String toString() {
+        return "SocketKey{ network=" + mNetwork + " interfaceIndex=" + mInterfaceIndex + " }";
+    }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java b/service-t/src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java
index 6bc7941..77c8f9c 100644
--- a/service-t/src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java
+++ b/service-t/src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java
@@ -30,7 +30,7 @@
     /**
      * Creates a new netlink monitor.
      */
-    public static AbstractSocketNetlink createNetLinkMonitor(@NonNull final Handler handler,
+    public static AbstractSocketNetlinkMonitor createNetLinkMonitor(@NonNull final Handler handler,
             @NonNull SharedLog log, @NonNull MdnsSocketProvider.NetLinkMonitorCallBack cb) {
         return new SocketNetlinkMonitor(handler, log, cb);
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java b/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java
index 40191cf..6f16436 100644
--- a/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java
+++ b/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java
@@ -20,7 +20,6 @@
 import android.net.LinkAddress;
 import android.os.Handler;
 import android.system.OsConstants;
-import android.util.Log;
 
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.ip.NetlinkMonitor;
@@ -28,15 +27,17 @@
 import com.android.net.module.util.netlink.NetlinkMessage;
 import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
 import com.android.net.module.util.netlink.StructIfaddrMsg;
-import com.android.server.connectivity.mdns.AbstractSocketNetlink;
+import com.android.server.connectivity.mdns.AbstractSocketNetlinkMonitor;
 import com.android.server.connectivity.mdns.MdnsSocketProvider;
 
 /**
  * The netlink monitor for MdnsSocketProvider.
  */
-public class SocketNetlinkMonitor extends NetlinkMonitor implements AbstractSocketNetlink {
+public class SocketNetlinkMonitor extends NetlinkMonitor implements AbstractSocketNetlinkMonitor {
 
     public static final String TAG = SocketNetlinkMonitor.class.getSimpleName();
+    @NonNull
+    private final SharedLog mSharedLog;
 
     @NonNull
     private final MdnsSocketProvider.NetLinkMonitorCallBack mCb;
@@ -46,6 +47,7 @@
         super(handler, log, TAG, OsConstants.NETLINK_ROUTE,
                 NetlinkConstants.RTMGRP_IPV4_IFADDR | NetlinkConstants.RTMGRP_IPV6_IFADDR);
         mCb = cb;
+        mSharedLog = log;
     }
     @Override
     public void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) {
@@ -61,19 +63,17 @@
         final StructIfaddrMsg ifaddrMsg = msg.getIfaddrHeader();
         final LinkAddress la = new LinkAddress(msg.getIpAddress(), ifaddrMsg.prefixLen,
                 msg.getFlags(), ifaddrMsg.scope);
-        if (!la.isPreferred()) {
-            // Skip the unusable ip address.
-            return;
-        }
         switch (msg.getHeader().nlmsg_type) {
             case NetlinkConstants.RTM_NEWADDR:
-                mCb.addOrUpdateInterfaceAddress(ifaddrMsg.index, la);
+                if (la.isPreferred()) {
+                    mCb.addOrUpdateInterfaceAddress(ifaddrMsg.index, la);
+                }
                 break;
             case NetlinkConstants.RTM_DELADDR:
                 mCb.deleteInterfaceAddress(ifaddrMsg.index, la);
                 break;
             default:
-                Log.e(TAG, "Unknown rtnetlink address msg type " + msg.getHeader().nlmsg_type);
+                mSharedLog.e("Unknown rtnetlink address msg type " + msg.getHeader().nlmsg_type);
         }
     }
 
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 3added6..226867f 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,19 +16,37 @@
 
 package com.android.server.connectivity.mdns.util;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.Network;
+import android.os.Build;
 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;
+import com.android.server.connectivity.mdns.MdnsPacketWriter;
 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;
 
 /**
  * Mdns utility functions.
@@ -53,9 +71,34 @@
     }
 
     /**
+     * Create a ArraySet or HashSet based on the sdk version.
+     */
+    public static <Type> Set<Type> newSet() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return new ArraySet<>();
+        } else {
+            return new HashSet<>();
+        }
+    }
+
+    /**
+     * 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(@NonNull String a, @NonNull String b) {
+    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))) {
@@ -65,6 +108,39 @@
         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;
     }
@@ -85,12 +161,21 @@
         return false;
     }
 
-    /*** Check whether the target network is matched current network */
+    /*** Check whether the target network matches the current network */
     public static boolean isNetworkMatched(@Nullable Network targetNetwork,
             @Nullable Network currentNetwork) {
         return targetNetwork == null || targetNetwork.equals(currentNetwork);
     }
 
+    /*** Check whether the target network matches any of the current networks */
+    public static boolean isAnyNetworkMatched(@Nullable Network targetNetwork,
+            Set<Network> currentNetworks) {
+        if (targetNetwork == null) {
+            return !currentNetworks.isEmpty();
+        }
+        return currentNetworks.contains(targetNetwork);
+    }
+
     /**
      * Truncate a service name to up to maxLength UTF-8 bytes.
      */
@@ -110,6 +195,140 @@
     }
 
     /**
+     * Write the mdns packet from given MdnsPacket.
+     */
+    public static void writeMdnsPacket(@NonNull MdnsPacketWriter writer, @NonNull MdnsPacket packet)
+            throws IOException {
+        writer.writeUInt16(packet.transactionId); // Transaction ID (advertisement: 0)
+        writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4)
+        writer.writeUInt16(packet.questions.size()); // questions count
+        writer.writeUInt16(packet.answers.size()); // answers count
+        writer.writeUInt16(packet.authorityRecords.size()); // authority entries count
+        writer.writeUInt16(packet.additionalRecords.size()); // additional records count
+
+        for (MdnsRecord record : packet.questions) {
+            // Questions do not have TTL or data
+            record.writeHeaderFields(writer);
+        }
+        for (MdnsRecord record : packet.answers) {
+            record.write(writer, 0L);
+        }
+        for (MdnsRecord record : packet.authorityRecords) {
+            record.write(writer, 0L);
+        }
+        for (MdnsRecord record : packet.additionalRecords) {
+            record.write(writer, 0L);
+        }
+    }
+
+    /**
+     * Create a raw DNS packet.
+     */
+    public static byte[] createRawDnsPacket(@NonNull byte[] packetCreationBuffer,
+            @NonNull MdnsPacket packet) throws IOException {
+        // TODO: support packets over size (send in multiple packets with TC bit set)
+        final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer);
+        writeMdnsPacket(writer, packet);
+
+        final int len = writer.getWritePosition();
+        return Arrays.copyOfRange(packetCreationBuffer, 0, len);
+    }
+
+    /**
+     * 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,
@@ -119,4 +338,47 @@
         return mdnsRecord.getTtl() > 0
                 && mdnsRecord.getRemainingTTL(now) <= mdnsRecord.getTtl() / 2;
     }
+
+    /**
+     * Creates a new full subtype name with given service type and subtype labels.
+     *
+     * For example, given ["_http", "_tcp"] and "_printer", this method returns a new String array
+     * of ["_printer", "_sub", "_http", "_tcp"].
+     */
+    public static String[] constructFullSubtype(String[] serviceType, String subtype) {
+        String[] fullSubtype = new String[serviceType.length + 2];
+        fullSubtype[0] = subtype;
+        fullSubtype[1] = MdnsConstants.SUBTYPE_LABEL;
+        System.arraycopy(serviceType, 0, fullSubtype, 2, serviceType.length);
+        return fullSubtype;
+    }
+
+    /** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
+    public static class Clock {
+        /**
+         * @see SystemClock#elapsedRealtime
+         */
+        public long elapsedRealtime() {
+            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 6776920..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;
@@ -313,17 +324,12 @@
                 mIpClientShutdownCv.block();
             }
 
-            // At the time IpClient is stopped, an IpClient event may have already been posted on
-            // the back of the handler and is awaiting execution. Once that event is executed, the
-            // associated callback object may not be valid anymore
-            // (NetworkInterfaceState#mIpClientCallback points to a different object / null).
-            private boolean isCurrentCallback() {
-                return this == mIpClientCallback;
-            }
-
-            private void handleIpEvent(final @NonNull Runnable r) {
+            private void safelyPostOnHandler(Runnable r) {
                 mHandler.post(() -> {
-                    if (!isCurrentCallback()) {
+                    if (this != mIpClientCallback) {
+                        // At the time IpClient is stopped, an IpClient event may have already been
+                        // posted on the handler and is awaiting execution. Once that event is
+                        // executed, the associated callback object may not be valid anymore.
                         Log.i(TAG, "Ignoring stale IpClientCallbacks " + this);
                         return;
                     }
@@ -333,24 +339,24 @@
 
             @Override
             public void onProvisioningSuccess(LinkProperties newLp) {
-                handleIpEvent(() -> onIpLayerStarted(newLp));
+                safelyPostOnHandler(() -> handleOnProvisioningSuccess(newLp));
             }
 
             @Override
             public void onProvisioningFailure(LinkProperties newLp) {
                 // This cannot happen due to provisioning timeout, because our timeout is 0. It can
                 // happen due to errors while provisioning or on provisioning loss.
-                handleIpEvent(() -> onIpLayerStopped());
+                safelyPostOnHandler(() -> handleOnProvisioningFailure());
             }
 
             @Override
             public void onLinkPropertiesChange(LinkProperties newLp) {
-                handleIpEvent(() -> updateLinkProperties(newLp));
+                safelyPostOnHandler(() -> handleOnLinkPropertiesChange(newLp));
             }
 
             @Override
             public void onReachabilityLost(String logMsg) {
-                handleIpEvent(() -> updateNeighborLostEvent(logMsg));
+                safelyPostOnHandler(() -> handleOnReachabilityLost(logMsg));
             }
 
             @Override
@@ -504,7 +510,7 @@
             mIpClient.startProvisioning(createProvisioningConfiguration(mIpConfig));
         }
 
-        void onIpLayerStarted(@NonNull final LinkProperties linkProperties) {
+        private void handleOnProvisioningSuccess(@NonNull final LinkProperties linkProperties) {
             if (mNetworkAgent != null) {
                 Log.e(TAG, "Already have a NetworkAgent - aborting new request");
                 stop();
@@ -538,7 +544,7 @@
             mNetworkAgent.markConnected();
         }
 
-        void onIpLayerStopped() {
+        private void handleOnProvisioningFailure() {
             // There is no point in continuing if the interface is gone as stop() will be triggered
             // by removeInterface() when processed on the handler thread and start() won't
             // work for a non-existent interface.
@@ -558,15 +564,15 @@
             }
         }
 
-        void updateLinkProperties(LinkProperties linkProperties) {
+        private void handleOnLinkPropertiesChange(LinkProperties linkProperties) {
             mLinkProperties = linkProperties;
             if (mNetworkAgent != null) {
                 mNetworkAgent.sendLinkPropertiesImpl(linkProperties);
             }
         }
 
-        void updateNeighborLostEvent(String logMsg) {
-            Log.i(TAG, "updateNeighborLostEvent " + logMsg);
+        private void handleOnReachabilityLost(String logMsg) {
+            Log.i(TAG, "handleOnReachabilityLost " + logMsg);
             if (mIpConfig.getIpAssignment() == IpAssignment.STATIC) {
                 // Ignore NUD failures for static IP configurations, where restarting the IpClient
                 // will not fix connectivity.
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index e7af569..b8689d6 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 
+import android.annotation.CheckResult;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -72,8 +73,9 @@
                 methodName + " is only available on automotive devices.");
     }
 
-    private boolean checkUseRestrictedNetworksPermission() {
-        return PermissionUtils.checkAnyPermissionOf(mContext,
+    @CheckResult
+    private boolean hasUseRestrictedNetworksPermission() {
+        return PermissionUtils.hasAnyPermissionOf(mContext,
                 android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS);
     }
 
@@ -92,7 +94,7 @@
     @Override
     public String[] getAvailableInterfaces() throws RemoteException {
         PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
-        return mTracker.getClientModeInterfaces(checkUseRestrictedNetworksPermission());
+        return mTracker.getClientModeInterfaces(hasUseRestrictedNetworksPermission());
     }
 
     /**
@@ -146,7 +148,7 @@
     public void addListener(IEthernetServiceListener listener) throws RemoteException {
         Objects.requireNonNull(listener, "listener must not be null");
         PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
-        mTracker.addListener(listener, checkUseRestrictedNetworksPermission());
+        mTracker.addListener(listener, hasUseRestrictedNetworksPermission());
     }
 
     /**
@@ -187,7 +189,7 @@
     @Override
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
-        if (!PermissionUtils.checkDumpPermission(mContext, TAG, pw)) return;
+        if (!PermissionUtils.hasDumpPermission(mContext, TAG, pw)) return;
 
         pw.println("Current Ethernet state: ");
         pw.increaseIndent();
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 1f22b02..71f289e 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -48,6 +48,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.PermissionUtils;
 import com.android.net.module.util.SharedLog;
@@ -130,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;
@@ -237,7 +242,18 @@
         mDeps = deps;
 
         // Interface match regex.
-        mIfaceMatch = mDeps.getInterfaceRegexFromResource(mContext);
+        String ifaceMatchRegex = mDeps.getInterfaceRegexFromResource(mContext);
+        // "*" is a magic string to indicate "pick the default".
+        if (ifaceMatchRegex.equals("*")) {
+            if (SdkLevel.isAtLeastV()) {
+                // On V+, include both usb%d and eth%d interfaces.
+                ifaceMatchRegex = "(usb|eth)\\d+";
+            } else {
+                // On T and U, include only eth%d interfaces.
+                ifaceMatchRegex = "eth\\d+";
+            }
+        }
+        mIfaceMatch = ifaceMatchRegex;
 
         // Read default Ethernet interface configuration from resources
         final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
@@ -325,7 +341,7 @@
     protected void unicastInterfaceStateChange(@NonNull IEthernetServiceListener listener,
             @NonNull String iface) {
         ensureRunningOnEthernetServiceThread();
-        final int state = mFactory.getInterfaceState(iface);
+        final int state = getInterfaceState(iface);
         final int role = getInterfaceRole(iface);
         final IpConfiguration config = getIpConfigurationForCallback(iface, state);
         try {
@@ -370,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) {
@@ -431,7 +446,7 @@
             for (String iface : getClientModeInterfaces(canUseRestrictedNetworks)) {
                 unicastInterfaceStateChange(listener, iface);
             }
-            if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER) {
+            if (mTetheringInterface != null && mTetheringInterfaceMode == INTERFACE_MODE_SERVER) {
                 unicastInterfaceStateChange(listener, mTetheringInterface);
             }
 
@@ -449,7 +464,7 @@
             if (!include) {
                 removeTestData();
             }
-            mHandler.post(() -> trackAvailableInterfaces());
+            trackAvailableInterfaces();
         });
     }
 
@@ -571,6 +586,7 @@
         removeInterface(iface);
         if (iface.equals(mTetheringInterface)) {
             mTetheringInterface = null;
+            mTetheringInterfaceHwAddr = null;
         }
         broadcastInterfaceStateChange(iface);
     }
@@ -599,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
@@ -631,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/BpfInterfaceMapHelper.java b/service-t/src/com/android/server/net/BpfInterfaceMapHelper.java
new file mode 100644
index 0000000..3c95b8e
--- /dev/null
+++ b/service-t/src/com/android/server/net/BpfInterfaceMapHelper.java
@@ -0,0 +1,101 @@
+/*
+ * 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.server.net;
+
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.VisibleForTesting;
+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.Struct.S32;
+
+/**
+ * Monitor interface added (without removed) and right interface name and its index to bpf map.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class BpfInterfaceMapHelper {
+    private static final String TAG = BpfInterfaceMapHelper.class.getSimpleName();
+    // This is current path but may be changed soon.
+    private static final String IFACE_INDEX_NAME_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_iface_index_name_map";
+    private final IBpfMap<S32, InterfaceMapValue> mIndexToIfaceBpfMap;
+
+    public BpfInterfaceMapHelper() {
+        this(new Dependencies());
+    }
+
+    @VisibleForTesting
+    public BpfInterfaceMapHelper(Dependencies deps) {
+        mIndexToIfaceBpfMap = deps.getInterfaceMap();
+    }
+
+    /**
+     * Dependencies of BpfInerfaceMapUpdater, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /** Create BpfMap for updating interface and index mapping. */
+        public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
+            try {
+                return new BpfMap<>(IFACE_INDEX_NAME_MAP_PATH,
+                    S32.class, InterfaceMapValue.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create interface map: " + e);
+                return null;
+            }
+        }
+    }
+
+    /** get interface name by interface index from bpf map */
+    public String getIfNameByIndex(final int index) {
+        try {
+            final InterfaceMapValue value = mIndexToIfaceBpfMap.getValue(new S32(index));
+            if (value == null) {
+                Log.e(TAG, "No if name entry for index " + index);
+                return null;
+            }
+            return value.getInterfaceNameString();
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to get entry for index " + index + ": " + e);
+            return null;
+        }
+    }
+
+    /**
+     * Dump BPF map
+     *
+     * @param pw print writer
+     */
+    public void dump(final IndentingPrintWriter pw) {
+        pw.println("BPF map status:");
+        pw.increaseIndent();
+        BpfDump.dumpMapStatus(mIndexToIfaceBpfMap, pw, "IfaceIndexNameMap",
+                IFACE_INDEX_NAME_MAP_PATH);
+        pw.decreaseIndent();
+        pw.println("BPF map content:");
+        pw.increaseIndent();
+        BpfDump.dumpMap(mIndexToIfaceBpfMap, pw, "IfaceIndexNameMap",
+                (key, value) -> "ifaceIndex=" + key.val
+                        + " ifaceName=" + value.getInterfaceNameString());
+        pw.decreaseIndent();
+    }
+}
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
deleted file mode 100644
index ceae9ba..0000000
--- a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
+++ /dev/null
@@ -1,174 +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.server.net;
-
-import android.content.Context;
-import android.net.INetd;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.ServiceSpecificException;
-import android.system.ErrnoException;
-import android.util.IndentingPrintWriter;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
-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;
-import com.android.net.module.util.Struct.S32;
-
-/**
- * Monitor interface added (without removed) and right interface name and its index to bpf map.
- */
-public class BpfInterfaceMapUpdater {
-    private static final String TAG = BpfInterfaceMapUpdater.class.getSimpleName();
-    // This is current path but may be changed soon.
-    private static final String IFACE_INDEX_NAME_MAP_PATH =
-            "/sys/fs/bpf/netd_shared/map_netd_iface_index_name_map";
-    private final IBpfMap<S32, InterfaceMapValue> mBpfMap;
-    private final INetd mNetd;
-    private final Handler mHandler;
-    private final Dependencies mDeps;
-
-    public BpfInterfaceMapUpdater(Context ctx, Handler handler) {
-        this(ctx, handler, new Dependencies());
-    }
-
-    @VisibleForTesting
-    public BpfInterfaceMapUpdater(Context ctx, Handler handler, Dependencies deps) {
-        mDeps = deps;
-        mBpfMap = deps.getInterfaceMap();
-        mNetd = deps.getINetd(ctx);
-        mHandler = handler;
-    }
-
-    /**
-     * Dependencies of BpfInerfaceMapUpdater, for injection in tests.
-     */
-    @VisibleForTesting
-    public static class Dependencies {
-        /** Create BpfMap for updating interface and index mapping. */
-        public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
-            try {
-                return new BpfMap<>(IFACE_INDEX_NAME_MAP_PATH, BpfMap.BPF_F_RDWR,
-                    S32.class, InterfaceMapValue.class);
-            } catch (ErrnoException e) {
-                Log.e(TAG, "Cannot create interface map: " + e);
-                return null;
-            }
-        }
-
-        /** Get InterfaceParams for giving interface name. */
-        public InterfaceParams getInterfaceParams(String ifaceName) {
-            return InterfaceParams.getByName(ifaceName);
-        }
-
-        /** Get INetd binder object. */
-        public INetd getINetd(Context ctx) {
-            return INetd.Stub.asInterface((IBinder) ctx.getSystemService(Context.NETD_SERVICE));
-        }
-    }
-
-    /**
-     * Start listening interface update event.
-     * Query current interface names before listening.
-     */
-    public void start() {
-        mHandler.post(() -> {
-            if (mBpfMap == null) {
-                Log.wtf(TAG, "Fail to start: Null bpf map");
-                return;
-            }
-
-            try {
-                // TODO: use a NetlinkMonitor and listen for RTM_NEWLINK messages instead.
-                mNetd.registerUnsolicitedEventListener(new InterfaceChangeObserver());
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Unable to register netd UnsolicitedEventListener, " + e);
-            }
-
-            final String[] ifaces;
-            try {
-                // TODO: use a netlink dump to get the current interface list.
-                ifaces = mNetd.interfaceGetList();
-            } catch (RemoteException | ServiceSpecificException e) {
-                Log.wtf(TAG, "Unable to query interface names by netd, " + e);
-                return;
-            }
-
-            for (String ifaceName : ifaces) {
-                addInterface(ifaceName);
-            }
-        });
-    }
-
-    private void addInterface(String ifaceName) {
-        final InterfaceParams iface = mDeps.getInterfaceParams(ifaceName);
-        if (iface == null) {
-            Log.e(TAG, "Unable to get InterfaceParams for " + ifaceName);
-            return;
-        }
-
-        try {
-            mBpfMap.updateEntry(new S32(iface.index), new InterfaceMapValue(ifaceName));
-        } catch (ErrnoException e) {
-            Log.e(TAG, "Unable to update entry for " + ifaceName + ", " + e);
-        }
-    }
-
-    private class InterfaceChangeObserver extends BaseNetdUnsolicitedEventListener {
-        @Override
-        public void onInterfaceAdded(String ifName) {
-            mHandler.post(() -> addInterface(ifName));
-        }
-    }
-
-    /** get interface name by interface index from bpf map */
-    public String getIfNameByIndex(final int index) {
-        try {
-            final InterfaceMapValue value = mBpfMap.getValue(new S32(index));
-            if (value == null) {
-                Log.e(TAG, "No if name entry for index " + index);
-                return null;
-            }
-            return value.getInterfaceNameString();
-        } catch (ErrnoException e) {
-            Log.e(TAG, "Failed to get entry for index " + index + ": " + e);
-            return null;
-        }
-    }
-
-    /**
-     * Dump BPF map
-     *
-     * @param pw print writer
-     */
-    public void dump(final IndentingPrintWriter pw) {
-        pw.println("BPF map status:");
-        pw.increaseIndent();
-        BpfDump.dumpMapStatus(mBpfMap, pw, "IfaceIndexNameMap", IFACE_INDEX_NAME_MAP_PATH);
-        pw.decreaseIndent();
-        pw.println("BPF map content:");
-        pw.increaseIndent();
-        BpfDump.dumpMap(mBpfMap, pw, "IfaceIndexNameMap",
-                (key, value) -> "ifaceIndex=" + key.val
-                        + " ifaceName=" + value.getInterfaceNameString());
-        pw.decreaseIndent();
-    }
-}
diff --git a/service-t/src/com/android/server/net/NetworkStatsEventLogger.java b/service-t/src/com/android/server/net/NetworkStatsEventLogger.java
new file mode 100644
index 0000000..679837a
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsEventLogger.java
@@ -0,0 +1,152 @@
+/*
+ * 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.net;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper class for NetworkStatsService to log events.
+ *
+ * @hide
+ */
+public class NetworkStatsEventLogger {
+    static final int POLL_REASON_DUMPSYS = 0;
+    static final int POLL_REASON_FORCE_UPDATE = 1;
+    static final int POLL_REASON_GLOBAL_ALERT = 2;
+    static final int POLL_REASON_NETWORK_STATUS_CHANGED = 3;
+    static final int POLL_REASON_OPEN_SESSION = 4;
+    static final int POLL_REASON_PERIODIC = 5;
+    static final int POLL_REASON_RAT_CHANGED = 6;
+    static final int POLL_REASON_REG_CALLBACK = 7;
+    static final int POLL_REASON_REMOVE_UIDS = 8;
+    static final int POLL_REASON_UPSTREAM_CHANGED = 9;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "POLL_REASON_" }, value = {
+            POLL_REASON_DUMPSYS,
+            POLL_REASON_FORCE_UPDATE,
+            POLL_REASON_GLOBAL_ALERT,
+            POLL_REASON_NETWORK_STATUS_CHANGED,
+            POLL_REASON_OPEN_SESSION,
+            POLL_REASON_PERIODIC,
+            POLL_REASON_RAT_CHANGED,
+            POLL_REASON_REMOVE_UIDS,
+            POLL_REASON_REG_CALLBACK,
+            POLL_REASON_UPSTREAM_CHANGED
+    })
+    public @interface PollReason {
+    }
+    static final int MAX_POLL_REASON = POLL_REASON_UPSTREAM_CHANGED;
+
+    @VisibleForTesting(visibility = PRIVATE)
+    public static final int MAX_EVENTS_LOGS = 50;
+    private final LocalLog mEventChanges = new LocalLog(MAX_EVENTS_LOGS);
+    private final int[] mPollEventCounts = new int[MAX_POLL_REASON + 1];
+
+    /**
+     * Log a poll event.
+     *
+     * @param flags Flags used when polling. See NetworkStatsService#FLAG_PERSIST_*.
+     * @param event The event of polling to be logged.
+     */
+    public void logPollEvent(int flags, @NonNull PollEvent event) {
+        mEventChanges.log("Poll(flags=" + flags + ", " + event + ")");
+        mPollEventCounts[event.reason]++;
+    }
+
+    /**
+     * Print poll counts per reason into the given stream.
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    public void dumpPollCountsPerReason(@NonNull IndentingPrintWriter pw) {
+        pw.println("Poll counts per reason:");
+        pw.increaseIndent();
+        for (int i = 0; i <= MAX_POLL_REASON; i++) {
+            pw.println(PollEvent.pollReasonNameOf(i) + ": " + mPollEventCounts[i]);
+        }
+        pw.decreaseIndent();
+        pw.println();
+    }
+
+    /**
+     * Print recent poll events into the given stream.
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    public void dumpRecentPollEvents(@NonNull IndentingPrintWriter pw) {
+        pw.println("Recent poll events:");
+        pw.increaseIndent();
+        mEventChanges.reverseDump(pw);
+        pw.decreaseIndent();
+        pw.println();
+    }
+
+    /**
+     * Print the object's state into the given stream.
+     */
+    public void dump(@NonNull IndentingPrintWriter pw) {
+        dumpPollCountsPerReason(pw);
+        dumpRecentPollEvents(pw);
+    }
+
+    public static class PollEvent {
+        public final int reason;
+
+        public PollEvent(@PollReason int reason) {
+            if (reason < 0 || reason > MAX_POLL_REASON) {
+                throw new IllegalArgumentException("Unsupported poll reason: " + reason);
+            }
+            this.reason = reason;
+        }
+
+        @Override
+        public String toString() {
+            return "PollEvent{" + "reason=" + pollReasonNameOf(reason) + "}";
+        }
+
+        /**
+         * Get the name of the given reason.
+         *
+         * If the reason does not have a String representation, returns its integer representation.
+         */
+        @NonNull
+        public static String pollReasonNameOf(@PollReason int reason) {
+            switch (reason) {
+                case POLL_REASON_DUMPSYS:                   return "DUMPSYS";
+                case POLL_REASON_FORCE_UPDATE:              return "FORCE_UPDATE";
+                case POLL_REASON_GLOBAL_ALERT:              return "GLOBAL_ALERT";
+                case POLL_REASON_NETWORK_STATUS_CHANGED:    return "NETWORK_STATUS_CHANGED";
+                case POLL_REASON_OPEN_SESSION:              return "OPEN_SESSION";
+                case POLL_REASON_PERIODIC:                  return "PERIODIC";
+                case POLL_REASON_RAT_CHANGED:               return "RAT_CHANGED";
+                case POLL_REASON_REMOVE_UIDS:               return "REMOVE_UIDS";
+                case POLL_REASON_REG_CALLBACK:              return "REG_CALLBACK";
+                case POLL_REASON_UPSTREAM_CHANGED:          return "UPSTREAM_CHANGED";
+                default:                                    return Integer.toString(reason);
+            }
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsObservers.java b/service-t/src/com/android/server/net/NetworkStatsObservers.java
index 1cd670a..21cf351 100644
--- a/service-t/src/com/android/server/net/NetworkStatsObservers.java
+++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java
@@ -142,6 +142,11 @@
 
     @VisibleForTesting
     protected Looper getHandlerLooperLocked() {
+        // TODO: Currently, callbacks are dispatched on this thread if the caller register
+        //  callback without supplying a Handler. To ensure that the service handler thread
+        //  is not blocked by client code, the observers must create their own thread. Once
+        //  all callbacks are dispatched outside of the handler thread, the service handler
+        //  thread can be used here.
         HandlerThread handlerThread = new HandlerThread(TAG);
         handlerThread.start();
         return handlerThread.getLooper();
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index 3da1585..8ee8591 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -22,6 +22,7 @@
 import static android.text.format.DateUtils.YEAR_IN_MILLIS;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.net.NetworkIdentitySet;
 import android.net.NetworkStats;
 import android.net.NetworkStats.NonMonotonicObserver;
@@ -32,17 +33,20 @@
 import android.net.TrafficStats;
 import android.os.Binder;
 import android.os.DropBoxManager;
+import android.os.SystemClock;
 import android.service.NetworkStatsRecorderProto;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.FileRotator;
+import com.android.metrics.NetworkStatsMetricsLogger;
 import com.android.net.module.util.NetworkStatsUtils;
 
 import libcore.io.IoUtils;
 
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -79,6 +83,7 @@
     private final long mBucketDuration;
     private final boolean mOnlyTags;
     private final boolean mWipeOnError;
+    private final boolean mUseFastDataInput;
 
     private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
     private NetworkStats mLastSnapshot;
@@ -89,6 +94,9 @@
     private final CombiningRewriter mPendingRewriter;
 
     private WeakReference<NetworkStatsCollection> mComplete;
+    private final NetworkStatsMetricsLogger mMetricsLogger = new NetworkStatsMetricsLogger();
+    @Nullable
+    private final File mStatsDir;
 
     /**
      * Non-persisted recorder, with only one bucket. Used by {@link NetworkStatsObservers}.
@@ -104,11 +112,13 @@
         mBucketDuration = YEAR_IN_MILLIS;
         mOnlyTags = false;
         mWipeOnError = true;
+        mUseFastDataInput = false;
 
         mPending = null;
         mSinceBoot = new NetworkStatsCollection(mBucketDuration);
 
         mPendingRewriter = null;
+        mStatsDir = null;
     }
 
     /**
@@ -116,7 +126,7 @@
      */
     public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
             DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags,
-            boolean wipeOnError) {
+            boolean wipeOnError, boolean useFastDataInput, @Nullable File statsDir) {
         mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
         mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
         mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
@@ -125,11 +135,13 @@
         mBucketDuration = bucketDuration;
         mOnlyTags = onlyTags;
         mWipeOnError = wipeOnError;
+        mUseFastDataInput = useFastDataInput;
 
         mPending = new NetworkStatsCollection(bucketDuration);
         mSinceBoot = new NetworkStatsCollection(bucketDuration);
 
         mPendingRewriter = new CombiningRewriter(mPending);
+        mStatsDir = statsDir;
     }
 
     public void setPersistThreshold(long thresholdBytes) {
@@ -179,8 +191,16 @@
         Objects.requireNonNull(mRotator, "missing FileRotator");
         NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
         if (res == null) {
+            final long readStart = SystemClock.elapsedRealtime();
             res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE);
             mComplete = new WeakReference<NetworkStatsCollection>(res);
+            final long readEnd = SystemClock.elapsedRealtime();
+            // For legacy recorders which are used for data integrity check, which
+            // have wipeOnError flag unset, skip reporting metrics.
+            if (mWipeOnError) {
+                mMetricsLogger.logRecorderFileReading(mCookie, (int) (readEnd - readStart),
+                        mStatsDir, res, mUseFastDataInput);
+            }
         }
         return res;
     }
@@ -195,8 +215,12 @@
     }
 
     private NetworkStatsCollection loadLocked(long start, long end) {
-        if (LOGD) Log.d(TAG, "loadLocked() reading from disk for " + mCookie);
-        final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration);
+        if (LOGD) {
+            Log.d(TAG, "loadLocked() reading from disk for " + mCookie
+                    + " useFastDataInput: " + mUseFastDataInput);
+        }
+        final NetworkStatsCollection res =
+                new NetworkStatsCollection(mBucketDuration, mUseFastDataInput);
         try {
             mRotator.readMatching(res, start, end);
             res.recordCollection(mPending);
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index f977a27..114cf2e 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;
@@ -46,15 +47,22 @@
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkStatsHistory.FIELD_ALL;
 import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_TEST;
 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;
@@ -62,8 +70,21 @@
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 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_RAT_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REMOVE_UIDS;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_UPSTREAM_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.PollEvent;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -71,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;
@@ -82,7 +104,6 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
-import android.net.ConnectivityManager;
 import android.net.DataUsageRequest;
 import android.net.INetd;
 import android.net.INetworkStatsService;
@@ -279,6 +300,19 @@
     static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts";
     static final String NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME = "import.successes";
     static final String NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME = "import.fallbacks";
+    static final String CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER =
+            "enable_network_stats_event_logger";
+
+    static final String NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS =
+            "netstats_fastdatainput_target_attempts";
+    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;
 
     private final Context mContext;
     private final NetworkStatsFactory mStatsFactory;
@@ -303,6 +337,8 @@
     private PersistentInt mImportLegacyAttemptsCounter = null;
     private PersistentInt mImportLegacySuccessesCounter = null;
     private PersistentInt mImportLegacyFallbacksCounter = null;
+    private PersistentInt mFastDataInputSuccessesCounter = null;
+    private PersistentInt mFastDataInputFallbacksCounter = null;
 
     @VisibleForTesting
     public static final String ACTION_NETWORK_STATS_POLL =
@@ -433,12 +469,22 @@
 
     private long mLastStatsSessionPoll;
 
+    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";
+    private final boolean mAlwaysUseTrafficStatsRateLimitCache;
+    private final int mTrafficStatsRateLimitCacheExpiryDuration;
+    private final int mTrafficStatsRateLimitCacheMaxEntries;
+
     private final Object mOpenSessionCallsLock = new Object();
 
     /**
      * Map from key {@code OpenSessionKey} to count of opened sessions. This is for recording
      * the caller of open session and it is only for debugging.
      */
+    // TODO: Move to NetworkStatsEventLogger to centralize event logging.
     @GuardedBy("mOpenSessionCallsLock")
     private final HashMap<OpenSessionKey, Integer> mOpenSessionCallsPerCaller = new HashMap<>();
 
@@ -454,11 +500,13 @@
     private final LocationPermissionChecker mLocationPermissionChecker;
 
     @NonNull
-    private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
+    private final BpfInterfaceMapHelper mInterfaceMapHelper;
 
     @Nullable
     private final SkDestroyListener mSkDestroyListener;
 
+    private static final int MAX_SOCKET_DESTROY_LISTENER_LOGS = 20;
+
     private static @NonNull Clock getDefaultClock() {
         return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
                 Clock.systemUTC());
@@ -470,9 +518,10 @@
      */
     private static class OpenSessionKey {
         public final int uid;
+        @Nullable
         public final String packageName;
 
-        OpenSessionKey(int uid, @NonNull String packageName) {
+        OpenSessionKey(int uid, @Nullable String packageName) {
             this.uid = uid;
             this.packageName = packageName;
         }
@@ -511,19 +560,21 @@
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case MSG_PERFORM_POLL: {
-                    performPoll(FLAG_PERSIST_ALL);
+                    performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent((int) msg.obj));
                     break;
                 }
                 case MSG_NOTIFY_NETWORK_STATUS: {
-                    // If no cached states, ignore.
-                    if (mLastNetworkStateSnapshots == null) break;
-                    // TODO (b/181642673): Protect mDefaultNetworks from concurrent accessing.
-                    handleNotifyNetworkStatus(
-                            mDefaultNetworks, mLastNetworkStateSnapshots, mActiveIface);
+                    synchronized (mStatsLock) {
+                        // If no cached states, ignore.
+                        if (mLastNetworkStateSnapshots == null) break;
+                        handleNotifyNetworkStatus(mDefaultNetworks, mLastNetworkStateSnapshots,
+                                mActiveIface, maybeCreatePollEvent((int) msg.obj));
+                    }
                     break;
                 }
                 case MSG_PERFORM_POLL_REGISTER_ALERT: {
-                    performPoll(FLAG_PERSIST_NETWORK);
+                    performPoll(FLAG_PERSIST_NETWORK,
+                            maybeCreatePollEvent(POLL_REASON_GLOBAL_ALERT));
                     registerGlobalAlert();
                     break;
                 }
@@ -601,26 +652,41 @@
         mContentObserver = mDeps.makeContentObserver(mHandler, mSettings,
                 mNetworkStatsSubscriptionsMonitor);
         mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext);
-        mInterfaceMapUpdater = mDeps.makeBpfInterfaceMapUpdater(mContext, mHandler);
-        mInterfaceMapUpdater.start();
+        mInterfaceMapHelper = mDeps.makeBpfInterfaceMapHelper();
         mUidCounterSetMap = mDeps.getUidCounterSetMap();
         mCookieTagMap = mDeps.getCookieTagMap();
         mStatsMapA = mDeps.getStatsMapA();
         mStatsMapB = mDeps.getStatsMapB();
         mAppUidStatsMap = mDeps.getAppUidStatsMap();
         mIfaceStatsMap = mDeps.getIfaceStatsMap();
+        // To prevent any possible races, the flag is not allowed to change until rebooting.
+        mSupportEventLogger = mDeps.supportEventLogger(mContext);
+        if (mSupportEventLogger) {
+            mEventLogger = new NetworkStatsEventLogger();
+        } else {
+            mEventLogger = null;
+        }
+
+        mAlwaysUseTrafficStatsRateLimitCache =
+                mDeps.alwaysUseTrafficStatsRateLimitCache(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
         // NetworkStatsService starts Java SkDestroyListener (new code).
         final BpfNetMaps bpfNetMaps = mDeps.makeBpfNetMaps(mContext);
-        if (bpfNetMaps.isSkDestroyListenerRunning()) {
-            mSkDestroyListener = null;
-        } else {
-            mSkDestroyListener = mDeps.makeSkDestroyListener(mCookieTagMap, mHandler);
-            mHandler.post(mSkDestroyListener::start);
-        }
+        mSkDestroyListener = mDeps.makeSkDestroyListener(mCookieTagMap, mHandler);
+        mHandler.post(mSkDestroyListener::start);
     }
 
     /**
@@ -667,13 +733,31 @@
          * 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);
         }
 
         /**
+         * Get the count of using FastDataInput target attempts.
+         */
+        public int getUseFastDataInputTargetAttempts() {
+            return getDeviceConfigPropertyInt(
+                    DeviceConfig.NAMESPACE_TETHERING,
+                    NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS, 0);
+        }
+
+        /**
+         * Compare two {@link NetworkStatsCollection} instances and returning a human-readable
+         * string description of difference for debugging purpose.
+         */
+        public String compareStats(@NonNull NetworkStatsCollection a,
+                                   @NonNull NetworkStatsCollection b, boolean allowKeyChange) {
+            return NetworkStatsCollection.compareStats(a, b, allowKeyChange);
+        }
+
+        /**
          * Create a persistent counter for given directory and name.
          */
         public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name)
@@ -750,18 +834,16 @@
             return new LocationPermissionChecker(context);
         }
 
-        /** Create BpfInterfaceMapUpdater to update bpf interface map. */
+        /** Create BpfInterfaceMapHelper to update bpf interface map. */
         @NonNull
-        public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
-                @NonNull Context ctx, @NonNull Handler handler) {
-            return new BpfInterfaceMapUpdater(ctx, handler);
+        public BpfInterfaceMapHelper makeBpfInterfaceMapHelper() {
+            return new BpfInterfaceMapHelper();
         }
 
         /** Get counter sets map for each UID. */
         public IBpfMap<S32, U8> getUidCounterSetMap() {
             try {
-                return new BpfMap<S32, U8>(UID_COUNTERSET_MAP_PATH, BpfMap.BPF_F_RDWR,
-                        S32.class, U8.class);
+                return new BpfMap<>(UID_COUNTERSET_MAP_PATH, S32.class, U8.class);
             } catch (ErrnoException e) {
                 Log.wtf(TAG, "Cannot open uid counter set map: " + e);
                 return null;
@@ -771,8 +853,8 @@
         /** Gets the cookie tag map */
         public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
             try {
-                return new BpfMap<CookieTagMapKey, CookieTagMapValue>(COOKIE_TAG_MAP_PATH,
-                        BpfMap.BPF_F_RDWR, CookieTagMapKey.class, CookieTagMapValue.class);
+                return new BpfMap<>(COOKIE_TAG_MAP_PATH,
+                        CookieTagMapKey.class, CookieTagMapValue.class);
             } catch (ErrnoException e) {
                 Log.wtf(TAG, "Cannot open cookie tag map: " + e);
                 return null;
@@ -782,8 +864,7 @@
         /** Gets stats map A */
         public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
             try {
-                return new BpfMap<StatsMapKey, StatsMapValue>(STATS_MAP_A_PATH,
-                        BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class);
+                return new BpfMap<>(STATS_MAP_A_PATH, StatsMapKey.class, StatsMapValue.class);
             } catch (ErrnoException e) {
                 Log.wtf(TAG, "Cannot open stats map A: " + e);
                 return null;
@@ -793,8 +874,7 @@
         /** Gets stats map B */
         public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
             try {
-                return new BpfMap<StatsMapKey, StatsMapValue>(STATS_MAP_B_PATH,
-                        BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class);
+                return new BpfMap<>(STATS_MAP_B_PATH, StatsMapKey.class, StatsMapValue.class);
             } catch (ErrnoException e) {
                 Log.wtf(TAG, "Cannot open stats map B: " + e);
                 return null;
@@ -804,8 +884,8 @@
         /** Gets the uid stats map */
         public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
             try {
-                return new BpfMap<UidStatsMapKey, StatsMapValue>(APP_UID_STATS_MAP_PATH,
-                        BpfMap.BPF_F_RDWR, UidStatsMapKey.class, StatsMapValue.class);
+                return new BpfMap<>(APP_UID_STATS_MAP_PATH,
+                        UidStatsMapKey.class, StatsMapValue.class);
             } catch (ErrnoException e) {
                 Log.wtf(TAG, "Cannot open app uid stats map: " + e);
                 return null;
@@ -815,8 +895,7 @@
         /** Gets interface stats map */
         public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
             try {
-                return new BpfMap<S32, StatsMapValue>(IFACE_STATS_MAP_PATH,
-                        BpfMap.BPF_F_RDWR, S32.class, StatsMapValue.class);
+                return new BpfMap<>(IFACE_STATS_MAP_PATH, S32.class, StatsMapValue.class);
             } catch (ErrnoException e) {
                 throw new IllegalStateException("Failed to open interface stats map", e);
             }
@@ -835,7 +914,93 @@
         /** Create a new SkDestroyListener. */
         public SkDestroyListener makeSkDestroyListener(
                 IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
-            return new SkDestroyListener(cookieTagMap, handler, new SharedLog(TAG));
+            return new SkDestroyListener(
+                    cookieTagMap, handler, new SharedLog(MAX_SOCKET_DESTROY_LISTENER_LOGS, TAG));
+        }
+
+        /**
+         * Get whether event logger feature is supported.
+         */
+        public boolean supportEventLogger(Context ctx) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                    ctx, CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER);
+        }
+
+        /**
+         * 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);
         }
     }
 
@@ -863,13 +1028,7 @@
         synchronized (mStatsLock) {
             mSystemReady = true;
 
-            // create data recorders along with historical rotators
-            mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
-                    true /* wipeOnError */);
-            mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
-                    true /* wipeOnError */);
-            mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
-                    mStatsDir, true /* wipeOnError */);
+            makeRecordersLocked();
 
             updatePersistThresholdsLocked();
 
@@ -934,13 +1093,106 @@
 
     private NetworkStatsRecorder buildRecorder(
             String prefix, NetworkStatsSettings.Config config, boolean includeTags,
-            File baseDir, boolean wipeOnError) {
+            File baseDir, boolean wipeOnError, boolean useFastDataInput) {
         final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
                 Context.DROPBOX_SERVICE);
         return new NetworkStatsRecorder(new FileRotator(
                 baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
                 mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags,
-                wipeOnError);
+                wipeOnError, useFastDataInput, baseDir);
+    }
+
+    @GuardedBy("mStatsLock")
+    private void makeRecordersLocked() {
+        boolean useFastDataInput = true;
+        try {
+            mFastDataInputSuccessesCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+                    NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME);
+            mFastDataInputFallbacksCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+                    NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME);
+        } catch (IOException e) {
+            Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
+            useFastDataInput = false;
+        }
+
+        final int targetAttempts = mDeps.getUseFastDataInputTargetAttempts();
+        int successes = 0;
+        int fallbacks = 0;
+        try {
+            successes = mFastDataInputSuccessesCounter.get();
+            // Fallbacks counter would be set to non-zero value to indicate the reading was
+            // not successful.
+            fallbacks = mFastDataInputFallbacksCounter.get();
+        } catch (IOException e) {
+            Log.wtf(TAG, "Failed to read counters, skip.", e);
+            useFastDataInput = false;
+        }
+
+        final boolean doComparison;
+        if (useFastDataInput) {
+            // Use FastDataInput if it needs to be evaluated or at least one success.
+            doComparison = targetAttempts > successes + fallbacks;
+            // Set target attempt to -1 as the kill switch to disable the feature.
+            useFastDataInput = targetAttempts >= 0 && (doComparison || successes > 0);
+        } else {
+            // useFastDataInput is false due to previous failures.
+            doComparison = false;
+        }
+
+        // create data recorders along with historical rotators.
+        // Don't wipe on error if comparison is needed.
+        mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+                !doComparison /* wipeOnError */, useFastDataInput);
+        mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+                !doComparison /* wipeOnError */, useFastDataInput);
+        mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
+                mStatsDir, !doComparison /* wipeOnError */, useFastDataInput);
+
+        if (!doComparison) return;
+
+        final MigrationInfo[] migrations = new MigrationInfo[]{
+                new MigrationInfo(mXtRecorder),
+                new MigrationInfo(mUidRecorder),
+                new MigrationInfo(mUidTagRecorder)
+        };
+        // Set wipeOnError flag false so the recorder won't damage persistent data if reads
+        // failed and calling deleteAll.
+        final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
+                buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+                        false /* wipeOnError */, false /* useFastDataInput */),
+                buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+                        false /* wipeOnError */, false /* useFastDataInput */),
+                buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, mStatsDir,
+                        false /* wipeOnError */, false /* useFastDataInput */)};
+        boolean success = true;
+        for (int i = 0; i < migrations.length; i++) {
+            try {
+                migrations[i].collection = migrations[i].recorder.getOrLoadCompleteLocked();
+            } catch (Throwable t) {
+                Log.wtf(TAG, "Failed to load collection, skip.", t);
+                success = false;
+                break;
+            }
+            if (!compareImportedToLegacyStats(migrations[i], legacyRecorders[i],
+                    false /* allowKeyChange */)) {
+                success = false;
+                break;
+            }
+        }
+
+        try {
+            if (success) {
+                mFastDataInputSuccessesCounter.set(successes + 1);
+            } else {
+                // Fallback.
+                mXtRecorder = legacyRecorders[0];
+                mUidRecorder = legacyRecorders[1];
+                mUidTagRecorder = legacyRecorders[2];
+                mFastDataInputFallbacksCounter.set(fallbacks + 1);
+            }
+        } catch (IOException e) {
+            Log.wtf(TAG, "Failed to update counters. success = " + success, e);
+        }
     }
 
     @GuardedBy("mStatsLock")
@@ -1039,7 +1291,7 @@
                 new NetworkStatsSettings.Config(HOUR_IN_MILLIS,
                 15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS);
         final NetworkStatsRecorder devRecorder = buildRecorder(PREFIX_DEV, devConfig,
-                false, mStatsDir, true /* wipeOnError */);
+                false, mStatsDir, true /* wipeOnError */, false /* useFastDataInput */);
         final MigrationInfo[] migrations = new MigrationInfo[]{
                 new MigrationInfo(devRecorder), new MigrationInfo(mXtRecorder),
                 new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
@@ -1056,11 +1308,11 @@
             legacyRecorders = new NetworkStatsRecorder[]{
                 null /* dev Recorder */,
                 buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir,
-                        false /* wipeOnError */),
+                        false /* wipeOnError */, false /* useFastDataInput */),
                 buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir,
-                        false /* wipeOnError */),
+                        false /* wipeOnError */, false /* useFastDataInput */),
                 buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir,
-                        false /* wipeOnError */)};
+                        false /* wipeOnError */, false /* useFastDataInput */)};
         } else {
             legacyRecorders = null;
         }
@@ -1091,7 +1343,8 @@
 
                 if (runComparison) {
                     final boolean success =
-                            compareImportedToLegacyStats(migration, legacyRecorders[i]);
+                            compareImportedToLegacyStats(migration, legacyRecorders[i],
+                                    true /* allowKeyChange */);
                     if (!success && !dryRunImportOnly) {
                         tryIncrementLegacyFallbacksCounter();
                     }
@@ -1214,7 +1467,7 @@
      * does not match or throw with exceptions.
      */
     private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration,
-            @Nullable NetworkStatsRecorder legacyRecorder) {
+            @Nullable NetworkStatsRecorder legacyRecorder, boolean allowKeyChange) {
         final NetworkStatsCollection legacyStats;
         // Skip the recorder that doesn't need to be compared.
         if (legacyRecorder == null) return true;
@@ -1229,7 +1482,8 @@
 
         // The result of comparison is only for logging.
         try {
-            final String error = compareStats(migration.collection, legacyStats);
+            final String error = mDeps.compareStats(migration.collection, legacyStats,
+                    allowKeyChange);
             if (error != null) {
                 Log.wtf(TAG, "Unexpected comparison result for recorder "
                         + legacyRecorder.getCookie() + ": " + error);
@@ -1243,93 +1497,6 @@
         return true;
     }
 
-    private static String str(NetworkStatsCollection.Key key) {
-        StringBuilder sb = new StringBuilder()
-                .append(key.ident.toString())
-                .append(" uid=").append(key.uid);
-        if (key.set != SET_FOREGROUND) {
-            sb.append(" set=").append(key.set);
-        }
-        if (key.tag != 0) {
-            sb.append(" tag=").append(key.tag);
-        }
-        return sb.toString();
-    }
-
-    // The importer will modify some keys when importing them.
-    // In order to keep the comparison code simple, add such special cases here and simply
-    // ignore them. This should not impact fidelity much because the start/end checks and the total
-    // bytes check still need to pass.
-    private static boolean couldKeyChangeOnImport(NetworkStatsCollection.Key key) {
-        if (key.ident.isEmpty()) return false;
-        final NetworkIdentity firstIdent = key.ident.iterator().next();
-
-        // Non-mobile network with non-empty RAT type.
-        // This combination is invalid and the NetworkIdentity.Builder will throw if it is passed
-        // in, but it looks like it was previously possible to persist it to disk. The importer sets
-        // the RAT type to NETWORK_TYPE_ALL.
-        if (firstIdent.getType() != ConnectivityManager.TYPE_MOBILE
-                && firstIdent.getRatType() != NetworkTemplate.NETWORK_TYPE_ALL) {
-            return true;
-        }
-
-        return false;
-    }
-
-    @Nullable
-    private static String compareStats(
-            NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
-        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
-                migrated.getEntries();
-        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
-
-        final ArraySet<NetworkStatsCollection.Key> unmatchedLegKeys =
-                new ArraySet<>(legEntries.keySet());
-
-        for (NetworkStatsCollection.Key legKey : legEntries.keySet()) {
-            final NetworkStatsHistory legHistory = legEntries.get(legKey);
-            final NetworkStatsHistory migHistory = migEntries.get(legKey);
-
-            if (migHistory == null && couldKeyChangeOnImport(legKey)) {
-                unmatchedLegKeys.remove(legKey);
-                continue;
-            }
-
-            if (migHistory == null) {
-                return "Missing migrated history for legacy key " + str(legKey)
-                        + ", legacy history was " + legHistory;
-            }
-            if (!migHistory.isSameAs(legHistory)) {
-                return "Difference in history for key " + legKey + "; legacy history " + legHistory
-                        + ", migrated history " + migHistory;
-            }
-            unmatchedLegKeys.remove(legKey);
-        }
-
-        if (!unmatchedLegKeys.isEmpty()) {
-            final NetworkStatsHistory first = legEntries.get(unmatchedLegKeys.valueAt(0));
-            return "Found unmatched legacy keys: count=" + unmatchedLegKeys.size()
-                    + ", first unmatched collection " + first;
-        }
-
-        if (migrated.getStartMillis() != legacy.getStartMillis()
-                || migrated.getEndMillis() != legacy.getEndMillis()) {
-            return "Start / end of the collections "
-                    + migrated.getStartMillis() + "/" + legacy.getStartMillis() + " and "
-                    + migrated.getEndMillis() + "/" + legacy.getEndMillis()
-                    + " don't match";
-        }
-
-        if (migrated.getTotalBytes() != legacy.getTotalBytes()) {
-            return "Total bytes " + migrated.getTotalBytes() + " and " + legacy.getTotalBytes()
-                    + " don't match for collections with start/end "
-                    + migrated.getStartMillis()
-                    + "/" + legacy.getStartMillis();
-        }
-
-        return null;
-    }
-
     @GuardedBy("mStatsLock")
     @NonNull
     private NetworkStatsCollection readPlatformCollectionForRecorder(
@@ -1377,7 +1544,11 @@
     }
 
     @Override
-    public INetworkStatsSession openSessionForUsageStats(int flags, String callingPackage) {
+    public INetworkStatsSession openSessionForUsageStats(
+            int flags, @NonNull String callingPackage) {
+        Objects.requireNonNull(callingPackage);
+        PermissionUtils.enforcePackageNameMatchesUid(
+                mContext, Binder.getCallingUid(), callingPackage);
         return openSessionInternal(flags, callingPackage);
     }
 
@@ -1406,9 +1577,9 @@
         return now - lastCallTime < POLL_RATE_LIMIT_MS;
     }
 
-    private int restrictFlagsForCaller(int flags, @NonNull String callingPackage) {
+    private int restrictFlagsForCaller(int flags, @Nullable String callingPackage) {
         // All non-privileged callers are not allowed to turn off POLL_ON_OPEN.
-        final boolean isPrivileged = PermissionUtils.checkAnyPermissionOf(mContext,
+        final boolean isPrivileged = PermissionUtils.hasAnyPermissionOf(mContext,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
                 android.Manifest.permission.NETWORK_STACK);
         if (!isPrivileged) {
@@ -1423,13 +1594,14 @@
         return flags;
     }
 
-    private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) {
+    private INetworkStatsSession openSessionInternal(
+            final int flags, @Nullable final String callingPackage) {
         final int restrictedFlags = restrictFlagsForCaller(flags, callingPackage);
         if ((restrictedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN
                 | NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
             final long ident = Binder.clearCallingIdentity();
             try {
-                performPoll(FLAG_PERSIST_ALL);
+                performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_OPEN_SESSION));
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -1440,6 +1612,7 @@
 
         return new INetworkStatsSession.Stub() {
             private final int mCallingUid = Binder.getCallingUid();
+            @Nullable
             private final String mCallingPackage = callingPackage;
             private final @NetworkStatsAccess.Level int mAccessLevel = checkAccessLevel(
                     callingPackage);
@@ -1578,11 +1751,13 @@
     }
 
     private void enforceTemplatePermissions(@NonNull NetworkTemplate template,
-            @NonNull String callingPackage) {
+            @Nullable String callingPackage) {
         // For a template with wifi network keys, it is possible for a malicious
         // client to track the user locations via querying data usage. Thus, enforce
         // fine location permission check.
-        if (!template.getWifiNetworkKeys().isEmpty()) {
+        // For a template with MATCH_TEST, since the wifi network key is just a placeholder
+        // to identify a specific test network, it is not related to track user location.
+        if (!template.getWifiNetworkKeys().isEmpty() && template.getMatchRule() != MATCH_TEST) {
             final boolean canAccessFineLocation = mLocationPermissionChecker
                     .checkCallersLocationPermission(callingPackage,
                     null /* featureId */,
@@ -1597,7 +1772,7 @@
         }
     }
 
-    private @NetworkStatsAccess.Level int checkAccessLevel(String callingPackage) {
+    private @NetworkStatsAccess.Level int checkAccessLevel(@Nullable String callingPackage) {
         return NetworkStatsAccess.checkAccessLevel(
                 mContext, Binder.getCallingPid(), Binder.getCallingUid(), callingPackage);
     }
@@ -1722,6 +1897,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);
@@ -1741,8 +1918,7 @@
             // information. This is because no caller needs this information for now, and it
             // makes it easier to change the implementation later by using the histories in the
             // recorder.
-            stats.clearInterfaces();
-            return stats;
+            return stats.withoutInterfaces();
         } catch (RemoteException e) {
             Log.wtf(TAG, "Error compiling UID stats", e);
             return new NetworkStats(0L, 0);
@@ -1824,7 +2000,8 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
-            handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface);
+            handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface,
+                    maybeCreatePollEvent(POLL_REASON_NETWORK_STATUS_CHANGED));
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -1841,7 +2018,8 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
-            performPoll(FLAG_PERSIST_ALL);
+            // TODO: Log callstack for system server callers.
+            performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_FORCE_UPDATE));
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -1887,6 +2065,7 @@
 
         final int callingPid = Binder.getCallingPid();
         final int callingUid = Binder.getCallingUid();
+        PermissionUtils.enforcePackageNameMatchesUid(mContext, callingUid, callingPackage);
         @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage);
         DataUsageRequest normalizedRequest;
         final long token = Binder.clearCallingIdentity();
@@ -1898,7 +2077,8 @@
         }
 
         // Create baseline stats
-        mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL));
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL,
+                POLL_REASON_REG_CALLBACK));
 
         return normalizedRequest;
    }
@@ -1922,36 +2102,111 @@
         if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
             return UNSUPPORTED;
         }
-        return 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);
-        long nativeIfaceStats = nativeGetIfaceStat(iface, type);
-        if (nativeIfaceStats == -1) {
-            return nativeIfaceStats;
-        } 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.
-            return nativeIfaceStats + getProviderIfaceStats(iface, type);
+        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 TYPE_RX_BYTES:
+                return entry.rxBytes;
+            case TYPE_RX_PACKETS:
+                return entry.rxPackets;
+            case TYPE_TX_BYTES:
+                return entry.txBytes;
+            case TYPE_TX_PACKETS:
+                return entry.txPackets;
+            default:
+                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) {
-        long nativeTotalStats = nativeGetTotalStat(type);
-        if (nativeTotalStats == -1) {
-            return nativeTotalStats;
-        } else {
-            // Refer to comment in getIfaceStats
-            return nativeTotalStats + getProviderIfaceStats(IFACE_ALL, type);
+        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);
     }
 
-    private long getProviderIfaceStats(@Nullable String iface, int type) {
+    @Override
+    public void clearTrafficStatsRateLimitCaches() {
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext, NETWORK_SETTINGS);
+        mTrafficStatsUidCache.clear();
+        mTrafficStatsIfaceCache.clear();
+        mTrafficStatsTotalCache.clear();
+    }
+
+    private NetworkStats.Entry getProviderIfaceStats(@Nullable String iface) {
         final NetworkStats providerSnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE);
         final HashSet<String> limitIfaces;
         if (iface == IFACE_ALL) {
@@ -1960,19 +2215,7 @@
             limitIfaces = new HashSet<>();
             limitIfaces.add(iface);
         }
-        final NetworkStats.Entry entry = providerSnapshot.getTotal(null, limitIfaces);
-        switch (type) {
-            case TrafficStats.TYPE_RX_BYTES:
-                return entry.rxBytes;
-            case TrafficStats.TYPE_RX_PACKETS:
-                return entry.rxPackets;
-            case TrafficStats.TYPE_TX_BYTES:
-                return entry.txBytes;
-            case TrafficStats.TYPE_TX_PACKETS:
-                return entry.txPackets;
-            default:
-                return 0;
-        }
+        return providerSnapshot.getTotal(null, limitIfaces);
     }
 
     /**
@@ -1995,7 +2238,8 @@
             new TetheringManager.TetheringEventCallback() {
                 @Override
                 public void onUpstreamChanged(@Nullable Network network) {
-                    performPoll(FLAG_PERSIST_NETWORK);
+                    performPoll(FLAG_PERSIST_NETWORK,
+                            maybeCreatePollEvent(POLL_REASON_UPSTREAM_CHANGED));
                 }
             };
 
@@ -2004,7 +2248,7 @@
         public void onReceive(Context context, Intent intent) {
             // on background handler thread, and verified UPDATE_DEVICE_STATS
             // permission above.
-            performPoll(FLAG_PERSIST_ALL);
+            performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_PERIODIC));
 
             // verify that we're watching global alert
             registerGlobalAlert();
@@ -2068,19 +2312,20 @@
     public void handleOnCollapsedRatTypeChanged() {
         // Protect service from frequently updating. Remove pending messages if any.
         mHandler.removeMessages(MSG_NOTIFY_NETWORK_STATUS);
-        mHandler.sendMessageDelayed(
-                mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS), mSettings.getPollDelay());
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS,
+                        POLL_REASON_RAT_CHANGED), mSettings.getPollDelay());
     }
 
     private void handleNotifyNetworkStatus(
             Network[] defaultNetworks,
             NetworkStateSnapshot[] snapshots,
-            String activeIface) {
+            String activeIface,
+            @Nullable PollEvent event) {
         synchronized (mStatsLock) {
             mWakeLock.acquire();
             try {
                 mActiveIface = activeIface;
-                handleNotifyNetworkStatusLocked(defaultNetworks, snapshots);
+                handleNotifyNetworkStatusLocked(defaultNetworks, snapshots, event);
             } finally {
                 mWakeLock.release();
             }
@@ -2094,7 +2339,7 @@
      */
     @GuardedBy("mStatsLock")
     private void handleNotifyNetworkStatusLocked(@NonNull Network[] defaultNetworks,
-            @NonNull NetworkStateSnapshot[] snapshots) {
+            @NonNull NetworkStateSnapshot[] snapshots, @Nullable PollEvent event) {
         if (!mSystemReady) return;
         if (LOGV) Log.v(TAG, "handleNotifyNetworkStatusLocked()");
 
@@ -2104,7 +2349,7 @@
 
         // poll, but only persist network stats to keep codepath fast. UID stats
         // will be persisted during next alarm poll event.
-        performPollLocked(FLAG_PERSIST_NETWORK);
+        performPollLocked(FLAG_PERSIST_NETWORK, event);
 
         // Rebuild active interfaces based on connected networks
         mActiveIfaces.clear();
@@ -2119,7 +2364,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());
@@ -2140,6 +2387,7 @@
             // both total usage and UID details.
             final String baseIface = snapshot.getLinkProperties().getInterfaceName();
             if (baseIface != null) {
+                nativeRegisterIface(baseIface);
                 findOrCreateNetworkIdentitySet(mActiveIfaces, baseIface).add(ident);
                 findOrCreateNetworkIdentitySet(mActiveUidIfaces, baseIface).add(ident);
 
@@ -2161,7 +2409,7 @@
                             .setDefaultNetwork(true)
                             .setOemManaged(ident.getOemManaged())
                             .setSubId(ident.getSubId()).build();
-                    final String ifaceVt = IFACE_VT + getSubIdForMobile(snapshot);
+                    final String ifaceVt = IFACE_VT + getSubIdForCellularOrSatellite(snapshot);
                     findOrCreateNetworkIdentitySet(mActiveIfaces, ifaceVt).add(vtIdent);
                     findOrCreateNetworkIdentitySet(mActiveUidIfaces, ifaceVt).add(vtIdent);
                 }
@@ -2211,6 +2459,7 @@
                 // baseIface has been handled, so ignore it.
                 if (TextUtils.equals(baseIface, iface)) continue;
                 if (iface != null) {
+                    nativeRegisterIface(iface);
                     findOrCreateNetworkIdentitySet(mActiveIfaces, iface).add(ident);
                     findOrCreateNetworkIdentitySet(mActiveUidIfaces, iface).add(ident);
                     if (isMobile) {
@@ -2229,9 +2478,15 @@
         mMobileIfaces = mobileIfaces.toArray(new String[0]);
     }
 
-    private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {
-        if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
-            throw new IllegalArgumentException("Mobile state need capability TRANSPORT_CELLULAR");
+    private static int getSubIdForCellularOrSatellite(@NonNull NetworkStateSnapshot state) {
+        if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+                // Both cellular and satellite are 2 different network transport at Mobile using
+                // same telephony network specifier. So adding satellite transport to consider
+                // for, when satellite network is active at mobile.
+                && !state.getNetworkCapabilities().hasTransport(
+                NetworkCapabilities.TRANSPORT_SATELLITE)) {
+            throw new IllegalArgumentException(
+                    "Mobile state need capability TRANSPORT_CELLULAR or TRANSPORT_SATELLITE");
         }
 
         final NetworkSpecifier spec = state.getNetworkCapabilities().getNetworkSpecifier();
@@ -2244,12 +2499,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;
         }
 
@@ -2321,12 +2578,12 @@
         }
     }
 
-    private void performPoll(int flags) {
+    private void performPoll(int flags, @Nullable PollEvent event) {
         synchronized (mStatsLock) {
             mWakeLock.acquire();
 
             try {
-                performPollLocked(flags);
+                performPollLocked(flags, event);
             } finally {
                 mWakeLock.release();
             }
@@ -2338,11 +2595,15 @@
      * {@link NetworkStatsHistory}.
      */
     @GuardedBy("mStatsLock")
-    private void performPollLocked(int flags) {
+    private void performPollLocked(int flags, @Nullable PollEvent event) {
         if (!mSystemReady) return;
         if (LOGV) Log.v(TAG, "performPollLocked(flags=0x" + Integer.toHexString(flags) + ")");
         Trace.traceBegin(TRACE_TAG_NETWORK, "performPollLocked");
 
+        if (mSupportEventLogger) {
+            mEventLogger.logPollEvent(flags, event);
+        }
+
         final boolean persistNetwork = (flags & FLAG_PERSIST_NETWORK) != 0;
         final boolean persistUid = (flags & FLAG_PERSIST_UID) != 0;
         final boolean persistForce = (flags & FLAG_PERSIST_FORCE) != 0;
@@ -2542,7 +2803,7 @@
         if (LOGV) Log.v(TAG, "removeUidsLocked() for UIDs " + Arrays.toString(uids));
 
         // Perform one last poll before removing
-        performPollLocked(FLAG_PERSIST_ALL);
+        performPollLocked(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_REMOVE_UIDS));
 
         mUidRecorder.removeUidsLocked(uids);
         mUidTagRecorder.removeUidsLocked(uids);
@@ -2592,7 +2853,7 @@
 
     @Override
     protected void dump(FileDescriptor fd, PrintWriter rawWriter, String[] args) {
-        if (!PermissionUtils.checkDumpPermission(mContext, TAG, rawWriter)) return;
+        if (!PermissionUtils.hasDumpPermission(mContext, TAG, rawWriter)) return;
 
         long duration = DateUtils.DAY_IN_MILLIS;
         final HashSet<String> argSet = new HashSet<String>();
@@ -2625,7 +2886,8 @@
             }
 
             if (poll) {
-                performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE);
+                performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE,
+                        maybeCreatePollEvent(POLL_REASON_DUMPSYS));
                 pw.println("Forced poll");
                 return;
             }
@@ -2685,6 +2947,25 @@
                     pw.println("(failed to dump platform legacy stats import counters)");
                 }
             }
+            pw.println(CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER + ": " + mSupportEventLogger);
+            pw.print(NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS,
+                    mDeps.getUseFastDataInputTargetAttempts());
+            pw.println();
+            try {
+                pw.print("FastDataInput successes", mFastDataInputSuccessesCounter.get());
+                pw.println();
+                pw.print("FastDataInput fallbacks", mFastDataInputFallbacksCounter.get());
+                pw.println();
+            } 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();
 
@@ -2742,6 +3023,10 @@
             pw.decreaseIndent();
             pw.println();
 
+            if (mSupportEventLogger) {
+                mEventLogger.dump(pw);
+            }
+
             pw.println("Stats Providers:");
             pw.increaseIndent();
             invokeForAllStatsProviderCallbacks((cb) -> {
@@ -2795,9 +3080,9 @@
             }
 
             pw.println();
-            pw.println("InterfaceMapUpdater:");
+            pw.println("InterfaceMapHelper:");
             pw.increaseIndent();
-            mInterfaceMapUpdater.dump(pw);
+            mInterfaceMapHelper.dump(pw);
             pw.decreaseIndent();
 
             pw.println();
@@ -2819,6 +3104,12 @@
             dumpStatsMapLocked(mStatsMapB, pw, "mStatsMapB");
             dumpIfaceStatsMapLocked(pw);
             pw.decreaseIndent();
+
+            pw.println();
+            pw.println("SkDestroyListener logs:");
+            pw.increaseIndent();
+            mSkDestroyListener.dump(pw);
+            pw.decreaseIndent();
         }
     }
 
@@ -2938,7 +3229,7 @@
         BpfDump.dumpMap(statsMap, pw, mapName,
                 "ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets",
                 (key, value) -> {
-                    final String ifName = mInterfaceMapUpdater.getIfNameByIndex(key.ifaceIndex);
+                    final String ifName = mInterfaceMapHelper.getIfNameByIndex(key.ifaceIndex);
                     return key.ifaceIndex + " "
                             + (ifName != null ? ifName : "unknown") + " "
                             + "0x" + Long.toHexString(key.tag) + " "
@@ -2956,7 +3247,7 @@
         BpfDump.dumpMap(mIfaceStatsMap, pw, "mIfaceStatsMap",
                 "ifaceIndex ifaceName rxBytes rxPackets txBytes txPackets",
                 (key, value) -> {
-                    final String ifName = mInterfaceMapUpdater.getIfNameByIndex(key.val);
+                    final String ifName = mInterfaceMapHelper.getIfNameByIndex(key.val);
                     return key.val + " "
                             + (ifName != null ? ifName : "unknown") + " "
                             + value.rxBytes + " "
@@ -3211,6 +3502,22 @@
         }
     }
 
+    private final boolean mSupportEventLogger;
+    @GuardedBy("mStatsLock")
+    @Nullable
+    private final NetworkStatsEventLogger mEventLogger;
+
+    /**
+     * Create a PollEvent instance if the feature is enabled.
+     */
+    @Nullable
+    public PollEvent maybeCreatePollEvent(@NetworkStatsEventLogger.PollReason int reason) {
+        if (mSupportEventLogger) {
+            return new PollEvent(reason);
+        }
+        return null;
+    }
+
     private class DropBoxNonMonotonicObserver implements NonMonotonicObserver<String> {
         @Override
         public void foundNonMonotonic(NetworkStats left, int leftIndex, NetworkStats right,
@@ -3246,7 +3553,8 @@
      * Default external settings that read from
      * {@link android.provider.Settings.Global}.
      */
-    private static class DefaultNetworkStatsSettings implements NetworkStatsSettings {
+    @VisibleForTesting(visibility = PRIVATE)
+    static class DefaultNetworkStatsSettings implements NetworkStatsSettings {
         DefaultNetworkStatsSettings() {}
 
         @Override
@@ -3299,9 +3607,14 @@
         }
     }
 
-    private static native long nativeGetTotalStat(int type);
-    private static native long nativeGetIfaceStat(String iface, int type);
-    private static native long nativeGetUidStat(int uid, int type);
+    // TODO: Read stats by using BpfNetMapsReader.
+    private static native void nativeRegisterIface(String iface);
+    @Nullable
+    private static native NetworkStats.Entry nativeGetTotalStat();
+    @Nullable
+    private static native NetworkStats.Entry nativeGetIfaceStat(String iface);
+    @Nullable
+    private static native NetworkStats.Entry nativeGetUidStat(int uid);
 
     /** Initializes and registers the Perfetto Network Trace data source */
     public static native void nativeInitNetworkTracing();
diff --git a/service-t/src/com/android/server/net/SkDestroyListener.java b/service-t/src/com/android/server/net/SkDestroyListener.java
index 7b68f89..a6cc2b5 100644
--- a/service-t/src/com/android/server/net/SkDestroyListener.java
+++ b/service-t/src/com/android/server/net/SkDestroyListener.java
@@ -30,6 +30,8 @@
 import com.android.net.module.util.netlink.NetlinkMessage;
 import com.android.net.module.util.netlink.StructInetDiagSockId;
 
+import java.io.PrintWriter;
+
 /**
  * Monitor socket destroy and delete entry from cookie tag bpf map.
  */
@@ -72,4 +74,11 @@
             mLog.e("Failed to delete CookieTagMap entry for " + sockId.cookie  + ": " + e);
         }
     }
+
+    /**
+     * Dump the contents of SkDestroyListener log.
+     */
+    public void dump(PrintWriter pw) {
+        mLog.reverseDump(pw);
+    }
 }
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
new file mode 100644
index 0000000..ca97d07
--- /dev/null
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -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.server.net;
+
+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.Objects;
+import java.util.function.Supplier;
+
+/**
+ * A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
+ * with an adjustable expiry duration to manage data freshness.
+ */
+class TrafficStatsRateLimitCache {
+    private final Clock mClock;
+    private final long mExpiryDurationMs;
+
+    /**
+     * Constructs a new {@link TrafficStatsRateLimitCache} with the specified expiry duration.
+     *
+     * @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, int maxSize) {
+        mClock = clock;
+        mExpiryDurationMs = expiryDurationMs;
+        mMap = new LruCache<>(maxSize);
+    }
+
+    private static class TrafficStatsCacheKey {
+        @Nullable
+        public final String iface;
+        public final int uid;
+
+        TrafficStatsCacheKey(@Nullable String iface, int uid) {
+            this.iface = iface;
+            this.uid = uid;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof TrafficStatsCacheKey)) return false;
+            TrafficStatsCacheKey that = (TrafficStatsCacheKey) o;
+            return uid == that.uid && Objects.equals(iface, that.iface);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(iface, uid);
+        }
+    }
+
+    private static class TrafficStatsCacheValue {
+        public final long timestamp;
+        @NonNull
+        public final NetworkStats.Entry entry;
+
+        TrafficStatsCacheValue(long timestamp, NetworkStats.Entry entry) {
+            this.timestamp = timestamp;
+            this.entry = entry;
+        }
+    }
+
+    @GuardedBy("mMap")
+    private final LruCache<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap;
+
+    /**
+     * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
+     *
+     * @param iface The interface name to include in the cache key. Null if not applicable.
+     * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+     * @return The cached {@link NetworkStats.Entry}, or null if not found or expired.
+     */
+    @Nullable
+    NetworkStats.Entry get(String iface, int uid) {
+        final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
+        synchronized (mMap) { // Synchronize for thread-safety
+            final TrafficStatsCacheValue value = mMap.get(key);
+            if (value != null && !isExpired(value.timestamp)) {
+                return value.entry;
+            } else {
+                mMap.remove(key); // Remove expired entries
+                return null;
+            }
+        }
+    }
+
+    /**
+     * 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.
+     * @param uid   The UID to include in the cache key. {@code UID_ALL} if not applicable.
+     * @param entry The {@link NetworkStats.Entry} to store in the cache.
+     */
+    void put(String iface, int uid, @NonNull final NetworkStats.Entry entry) {
+        Objects.requireNonNull(entry);
+        final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
+        synchronized (mMap) { // Synchronize for thread-safety
+            mMap.put(key, new TrafficStatsCacheValue(mClock.millis(), entry));
+        }
+    }
+
+    /**
+     * Clear the cache.
+     */
+    void clear() {
+        synchronized (mMap) {
+            mMap.evictAll();
+        }
+    }
+
+    private boolean isExpired(long timestamp) {
+        return mClock.millis() > timestamp + mExpiryDurationMs;
+    }
+}
diff --git a/service/Android.bp b/service/Android.bp
index e1376a1..1a0e045 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -15,10 +15,17 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+service_remoteauth_pre_jarjar_lib = "service-remoteauth-pre-jarjar"
+
+// The above variables may have different values
+// depending on the branch, and this comment helps
+// separate them from the rest of the file to avoid merge conflicts
+
 aidl_interface {
     name: "connectivity_native_aidl_interface",
     local_include_dir: "binder",
@@ -104,8 +111,8 @@
     ],
     srcs: [
         ":services.connectivity-netstats-jni-sources",
-        "jni/com_android_server_BpfNetMaps.cpp",
         "jni/com_android_server_connectivity_ClatCoordinator.cpp",
+        "jni/com_android_server_ServiceManagerWrapper.cpp",
         "jni/com_android_server_TestNetworkService.cpp",
         "jni/onload.cpp",
     ],
@@ -118,11 +125,11 @@
         "libmodules-utils-build",
         "libnetjniutils",
         "libnet_utils_device_common_bpfjni",
-        "libtraffic_controller",
         "netd_aidl_interface-lateststable-ndk",
     ],
     shared_libs: [
         "libbase",
+        "libbinder_ndk",
         "libcutils",
         "libnetdutils",
         "liblog",
@@ -172,6 +179,8 @@
         "unsupportedappusage",
         "ServiceConnectivityResources",
         "framework-statsd",
+        "framework-permission",
+        "framework-permission-s",
     ],
     static_libs: [
         // Do not add libs here if they are already included
@@ -179,13 +188,9 @@
         "androidx.annotation_annotation",
         "connectivity-net-module-utils-bpf",
         "connectivity_native_aidl_interface-lateststable-java",
-        "dnsresolver_aidl_interface-V11-java",
+        "dnsresolver_aidl_interface-V15-java",
         "modules-utils-shell-command-handler",
-        "net-utils-device-common",
-        "net-utils-device-common-bpf",
-        "net-utils-device-common-ip",
-        "net-utils-device-common-netlink",
-        "net-utils-services-common",
+        "net-utils-service-connectivity",
         "netd-client",
         "networkstack-client",
         "PlatformProperties",
@@ -195,10 +200,15 @@
     apex_available: [
         "com.android.tethering",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+
+    },
     visibility: [
         "//packages/modules/Connectivity/service-t",
         "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Connectivity/thread/service:__subpackages__",
+        "//packages/modules/Connectivity/thread/tests:__subpackages__",
     ],
 }
 
@@ -218,6 +228,7 @@
     ],
     lint: {
         strict_updatability_linting: true,
+
     },
 }
 
@@ -236,6 +247,8 @@
         "service-connectivity-pre-jarjar",
         "service-connectivity-tiramisu-pre-jarjar",
         "service-nearby-pre-jarjar",
+        service_remoteauth_pre_jarjar_lib,
+        "service-thread-pre-jarjar",
     ],
     // The below libraries are not actually needed to build since no source is compiled
     // (only combining prebuilt static_libs), but they are necessary so that R8 has the right
@@ -251,6 +264,8 @@
         "framework-tethering.impl",
         "framework-wifi",
         "libprotobuf-java-nano",
+        "framework-permission",
+        "framework-permission-s",
     ],
     jarjar_rules: ":connectivity-jarjar-rules",
     apex_available: [
@@ -259,9 +274,6 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
-    lint: {
-        strict_updatability_linting: true,
-    },
 }
 
 // A special library created strictly for use by the tests as they need the
@@ -303,6 +315,8 @@
         ":framework-connectivity-jarjar-rules",
         ":service-connectivity-jarjar-gen",
         ":service-nearby-jarjar-gen",
+        ":service-remoteauth-jarjar-gen",
+        ":service-thread-jarjar-gen",
     ],
     out: ["connectivity-jarjar-rules.txt"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
@@ -354,6 +368,42 @@
     visibility: ["//visibility:private"],
 }
 
+java_genrule {
+    name: "service-remoteauth-jarjar-gen",
+    tool_files: [
+        ":" + service_remoteauth_pre_jarjar_lib + "{.jar}",
+        "jarjar-excludes.txt",
+    ],
+    tools: [
+        "jarjar-rules-generator",
+    ],
+    out: ["service_remoteauth_jarjar_rules.txt"],
+    cmd: "$(location jarjar-rules-generator) " +
+        "$(location :" + service_remoteauth_pre_jarjar_lib + "{.jar}) " +
+        "--prefix com.android.server.remoteauth " +
+        "--excludes $(location jarjar-excludes.txt) " +
+        "--output $(out)",
+    visibility: ["//visibility:private"],
+}
+
+java_genrule {
+    name: "service-thread-jarjar-gen",
+    tool_files: [
+        ":service-thread-pre-jarjar{.jar}",
+        "jarjar-excludes.txt",
+    ],
+    tools: [
+        "jarjar-rules-generator",
+    ],
+    out: ["service_thread_jarjar_rules.txt"],
+    cmd: "$(location jarjar-rules-generator) " +
+        "$(location :service-thread-pre-jarjar{.jar}) " +
+        "--prefix com.android.server.thread " +
+        "--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/Android.bp b/service/ServiceConnectivityResources/Android.bp
index 2260596..2621256 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -16,6 +16,7 @@
 
 // APK to hold all the wifi overlayable resources.
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/service/ServiceConnectivityResources/OWNERS b/service/ServiceConnectivityResources/OWNERS
new file mode 100644
index 0000000..df41ff2
--- /dev/null
+++ b/service/ServiceConnectivityResources/OWNERS
@@ -0,0 +1,2 @@
+per-file res/values/config_thread.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
+per-file res/values/overlayable.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/service/ServiceConnectivityResources/res/values-af/strings.xml b/service/ServiceConnectivityResources/res/values-af/strings.xml
index 086c6e3..a0f927e 100644
--- a/service/ServiceConnectivityResources/res/values-af/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-af/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Meld by netwerk aan"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Geen internet nie"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Jou <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>-data is dalk op. Tik vir opsies."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Jou data is dalk op. Tik vir opsies."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> het geen internettoegang nie"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tik vir opsies"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Selnetwerk het nie internettoegang nie"</string>
diff --git a/service/ServiceConnectivityResources/res/values-am/strings.xml b/service/ServiceConnectivityResources/res/values-am/strings.xml
index 886b353..b9ce7f0 100644
--- a/service/ServiceConnectivityResources/res/values-am/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-am/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ወደ አውታረ መረብ በመለያ ይግቡ"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"በይነመረብ የለም"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ከ<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> ውሂብ ጨርሰው ሊሆን ይችላል። ለአማራጮች መታ ያድርጉ።"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ውሂብ ጨርሰው ሊሆን ይችላል። ለአማራጮች መታ ያድርጉ።"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ምንም የበይነ መረብ መዳረሻ የለም"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ለአማራጮች መታ ያድርጉ"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"የተንቀሳቃሽ ስልክ አውታረ መረብ የበይነመረብ መዳረሻ የለውም"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ar/strings.xml b/service/ServiceConnectivityResources/res/values-ar/strings.xml
index 07d9c2e..92dd9a1 100644
--- a/service/ServiceConnectivityResources/res/values-ar/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ar/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"تسجيل الدخول إلى الشبكة"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"لا يتوفّر اتصال بالإنترنت"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"قد تكون البيانات التي يوفِّرها \"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>\" نفدت. انقر لعرض الخيارات."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"قد تكون البيانات نفدت. انقر لعرض الخيارات."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"لا يتوفّر في <xliff:g id="NETWORK_SSID">%1$s</xliff:g> إمكانية الاتصال بالإنترنت."</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"انقر للحصول على الخيارات."</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"شبكة الجوّال هذه غير متصلة بالإنترنت"</string>
diff --git a/service/ServiceConnectivityResources/res/values-as/strings.xml b/service/ServiceConnectivityResources/res/values-as/strings.xml
index e753cb3..a3c2c28 100644
--- a/service/ServiceConnectivityResources/res/values-as/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-as/strings.xml
@@ -18,10 +18,13 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ছিষ্টেম সংযোগৰ উৎস"</string>
-    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ৱাই-ফাই নেটৱৰ্কত ছাইন ইন কৰক"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi নেটৱৰ্কত ছাইন ইন কৰক"</string>
     <string name="network_available_sign_in" msgid="2622520134876355561">"নেটৱৰ্কত ছাইন ইন কৰক"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"ইণ্টাৰনেট নাই"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"আপোনাৰ <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>ৰ ডেটা হয়তো শেষ হৈছে। বিকল্পৰ বাবে টিপক।"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"আপোনাৰ ডেটা হয়তো শেষ হৈছে। বিকল্পৰ বাবে টিপক।"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ৰ ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"অধিক বিকল্পৰ বাবে টিপক"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"ম’বাইল নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
diff --git a/service/ServiceConnectivityResources/res/values-az/strings.xml b/service/ServiceConnectivityResources/res/values-az/strings.xml
index f33a3e3..ab6e0fb 100644
--- a/service/ServiceConnectivityResources/res/values-az/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-az/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Şəbəkəyə daxil olun"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"İnternet yoxdur"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> datası bitmiş ola bilər. Seçimlər üçün toxunun."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Data bitmiş ola bilər. Seçimlər üçün toxunun."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> üçün internet girişi əlçatan deyil"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Seçimlər üçün tıklayın"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobil şəbəkənin internetə girişi yoxdur"</string>
diff --git a/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml b/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml
index 7398e7c..5bbf143 100644
--- a/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Prijavite se na mrežu"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Nema interneta"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Možda ste potrošili <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> podatke. Dodirnite za opcije."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Možda ste potrošili podatke. Dodirnite za opcije."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Dodirnite za opcije"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilna mreža nema pristup internetu"</string>
diff --git a/service/ServiceConnectivityResources/res/values-be/strings.xml b/service/ServiceConnectivityResources/res/values-be/strings.xml
index 3459cc7..30a81b3 100644
--- a/service/ServiceConnectivityResources/res/values-be/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-be/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Увайдзіце ў сетку"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Няма падключэння да інтэрнэту"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Магчыма, скончыўся трафік, выдзелены аператарам \"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>\". Націсніце, каб паглядзець даступныя дзеянні."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Магчыма, скончыўся трафік. Націсніце, каб паглядзець даступныя дзеянні."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> не мае доступу ў інтэрнэт"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Дакраніцеся, каб убачыць параметры"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Мабільная сетка не мае доступу ў інтэрнэт"</string>
diff --git a/service/ServiceConnectivityResources/res/values-bg/strings.xml b/service/ServiceConnectivityResources/res/values-bg/strings.xml
index b4ae618..d52ee22 100644
--- a/service/ServiceConnectivityResources/res/values-bg/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-bg/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Вход в мрежата"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Няма интернет"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Възможно е да сте изчерпали данните си от <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Докоснете за опции."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Възможно е да сте изчерпали данните си. Докоснете за опции."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> няма достъп до интернет"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Докоснете за опции"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Мобилната мрежа няма достъп до интернет"</string>
diff --git a/service/ServiceConnectivityResources/res/values-bn/strings.xml b/service/ServiceConnectivityResources/res/values-bn/strings.xml
index 3b32973..9a68bcd 100644
--- a/service/ServiceConnectivityResources/res/values-bn/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-bn/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"নেটওয়ার্কে সাইন-ইন করুন"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"ইন্টারনেট কানেকশন নেই"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"আপনার <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>-এর ডেটা হয়ত শেষ হয়ে গেছে। বিকল্প পাওয়ার জন্য ট্যাপ করুন।"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"আপনার ডেটা হয়ত শেষ হয়ে গেছে। বিকল্প পাওয়ার জন্য ট্যাপ করুন।"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-এর ইন্টারনেটে অ্যাক্সেস নেই"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"বিকল্পগুলির জন্য আলতো চাপুন"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"মোবাইল নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই"</string>
diff --git a/service/ServiceConnectivityResources/res/values-bs/strings.xml b/service/ServiceConnectivityResources/res/values-bs/strings.xml
index 0bc0a7c..ae401fb 100644
--- a/service/ServiceConnectivityResources/res/values-bs/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-bs/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Prijava na mrežu"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Nema internetske veze"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Možda ste iskoristili prijenos podataka na mobilnoj mreži <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Dodirnite za opcije."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Možda ste iskoristili prijenos podataka na mobilnoj mreži. Dodirnite za opcije."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Mreža <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Dodirnite za opcije"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilna mreža nema pristup internetu"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ca/strings.xml b/service/ServiceConnectivityResources/res/values-ca/strings.xml
index 22b9dbd..325b7ea 100644
--- a/service/ServiceConnectivityResources/res/values-ca/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ca/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Inicia la sessió a la xarxa"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Sense connexió a Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Pot ser que t\'hagis quedat sense dades de <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Toca per veure les opcions."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Pot ser que t\'hagis quedat sense dades. Toca per veure les opcions."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no té accés a Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toca per veure les opcions"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"La xarxa mòbil no té accés a Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-cs/strings.xml b/service/ServiceConnectivityResources/res/values-cs/strings.xml
index ccf21ee..a785c10 100644
--- a/service/ServiceConnectivityResources/res/values-cs/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-cs/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Přihlásit se k síti"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Nejste připojeni k internetu"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Možná vám došla data od poskytovatele <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Klepnutím zobrazíte možnosti."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Možná vám došla data. Klepnutím zobrazíte možnosti."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Síť <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nemá přístup k internetu"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Klepnutím zobrazíte možnosti"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilní síť nemá přístup k internetu"</string>
diff --git a/service/ServiceConnectivityResources/res/values-da/strings.xml b/service/ServiceConnectivityResources/res/values-da/strings.xml
index a33143e..9d7b0fe 100644
--- a/service/ServiceConnectivityResources/res/values-da/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-da/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Log ind på netværk"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Intet internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Du er muligvis løbet tør for data fra <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Tryk for at se valgmuligheder."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Du er muligvis løbet tør for data. Tryk for at se valgmuligheder."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internetforbindelse"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tryk for at se valgmuligheder"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilnetværket har ingen internetadgang"</string>
diff --git a/service/ServiceConnectivityResources/res/values-de/strings.xml b/service/ServiceConnectivityResources/res/values-de/strings.xml
index 96cc7d2..f58efb0 100644
--- a/service/ServiceConnectivityResources/res/values-de/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-de/strings.xml
@@ -22,11 +22,14 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Im Netzwerk anmelden"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Kein Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Möglicherweise sind deine mobilen Daten von <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> aufgebraucht. Tippe für Optionen."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Möglicherweise sind deine mobilen Daten aufgebraucht. Tippe für Optionen."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> hat keinen Internetzugriff"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Für Optionen tippen"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiles Netzwerk hat keinen Internetzugriff"</string>
     <string name="other_networks_no_internet" msgid="5693932964749676542">"Netzwerk hat keinen Internetzugriff"</string>
-    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Auf den privaten DNS-Server kann nicht zugegriffen werden"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Auf den Server des privaten DNS kann nicht zugegriffen werden"</string>
     <string name="network_partial_connectivity" msgid="5549503845834993258">"Schlechte Verbindung mit <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
     <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tippen, um die Verbindung trotzdem herzustellen"</string>
     <string name="network_switch_metered" msgid="5016937523571166319">"Zu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> gewechselt"</string>
diff --git a/service/ServiceConnectivityResources/res/values-el/strings.xml b/service/ServiceConnectivityResources/res/values-el/strings.xml
index b5f319d..c8eebc7 100644
--- a/service/ServiceConnectivityResources/res/values-el/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-el/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Σύνδεση στο δίκτυο"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Χωρίς σύνδεση στο διαδίκτυο"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Ενδέχεται να έχουν εξαντληθεί τα δεδομένα σας από τον πάροχο <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Πατήστε για να δείτε τις επιλογές."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Ενδέχεται να έχουν εξαντληθεί τα δεδομένα σας. Πατήστε για να δείτε τις επιλογές."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Η εφαρμογή <xliff:g id="NETWORK_SSID">%1$s</xliff:g> δεν έχει πρόσβαση στο διαδίκτυο"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Πατήστε για να δείτε τις επιλογές"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Το δίκτυο κινητής τηλεφωνίας δεν έχει πρόσβαση στο διαδίκτυο."</string>
diff --git a/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml b/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml
index c490cf8..37a5ec0 100644
--- a/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Sign in to network"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"No Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"You may be out of data from <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Tap for options."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"You may be out of data. Tap for options."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tap for options"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobile network has no Internet access"</string>
diff --git a/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml b/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml
index 9827f4e..836369e 100644
--- a/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Sign in to network"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"No internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"You may be out of data from <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Tap for options."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"You may be out of data. Tap for options."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no internet access"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tap for options"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobile network has no internet access"</string>
diff --git a/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml b/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml
index c490cf8..37a5ec0 100644
--- a/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Sign in to network"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"No Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"You may be out of data from <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Tap for options."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"You may be out of data. Tap for options."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tap for options"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobile network has no Internet access"</string>
diff --git a/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml b/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml
index c490cf8..37a5ec0 100644
--- a/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Sign in to network"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"No Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"You may be out of data from <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Tap for options."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"You may be out of data. Tap for options."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tap for options"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobile network has no Internet access"</string>
diff --git a/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml b/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml
index 67c3659..258e570 100644
--- a/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‏‎‎‏‎‏‎‎‎‏‎‎‎‎‎‎‏‎‏‎‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‎‏‎‏‏‎‏‏‏‏‏‎‏‎‎‏‎Sign in to network‎‏‎‎‏‎"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‏‏‏‏‎‎‎‏‏‏‏‎‎‏‏‏‏‎‏‏‎‎‏‏‏‏‏‏‎‎‏‏‎‎‏‎‏‎‏‎‏‎‏‏‏‏‎‎‎‎‎‏‏‏‎‎‏‎No internet‎‏‎‎‏‎"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‏‏‏‎‏‎‎‏‎‎‏‎‎‎‎‎‎‎‏‏‎‏‏‎‎‏‎‎‏‎‎‎‏‎‎‏‏‎‎‏‏‏‎‎‎‏‎‎‏‎‎‎‎‎‎You may be out of data from ‎‏‎‎‏‏‎<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>‎‏‎‎‏‏‏‎. Tap for options.‎‏‎‎‏‎"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‎‏‎‎‏‏‎‏‎‎‏‎‎‎‎‎‏‎‏‏‏‏‏‏‎‏‎‎‏‏‏‎‎‎‏‏‎‎‎‎‎‎‏‎‏‏‏‏‎‎‏‏‎‎‎‎‏‎You may be out of data. Tap for options.‎‏‎‎‏‎"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‎‏‏‎‏‎‎‎‎‎‏‎‎‎‏‏‎‎‎‎‎‏‏‏‎‎‎‎‏‎‏‎‎‏‎‎‎‎‏‏‏‎‎‏‎‏‎‎‏‏‎‏‎‎‏‏‎‎‏‎‎‏‏‎<xliff:g id="NETWORK_SSID">%1$s</xliff:g>‎‏‎‎‏‏‏‎ has no internet access‎‏‎‎‏‎"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‎‎‎‏‏‏‏‏‎‎‏‎‎‏‏‏‏‎‏‏‏‏‎‏‏‎‏‎‏‎‎‏‏‎‏‏‎‏‎‎‏‎‎‏‎‏‎‏‏‎‎‎‏‏‎‏‎‎Tap for options‎‏‎‎‏‎"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‏‏‏‎‏‎‎‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‎‏‏‏‎‎‏‎‎‏‎‏‎‏‎‎‏‎‎‎‎‏‎‎‏‎‏‎‏‎‎Mobile network has no internet access‎‏‎‎‏‎"</string>
diff --git a/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
index b24dee0..3471243 100644
--- a/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Acceder a la red"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Sin Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Es posible que no tengas más datos de <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Presiona para ver las opciones."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Es posible que no tengas más datos. Presiona para ver las opciones."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no tiene acceso a Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Presiona para ver opciones"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"La red móvil no tiene acceso a Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-es/strings.xml b/service/ServiceConnectivityResources/res/values-es/strings.xml
index f4a7e3d..e8401ed 100644
--- a/service/ServiceConnectivityResources/res/values-es/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-es/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Iniciar sesión en la red"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Sin Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Es posible que ya no tengas datos de <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Toca para ver las opciones."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Es posible que ya no tengas datos. Toca para ver las opciones."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no tiene acceso a Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toca para ver opciones"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"La red móvil no tiene acceso a Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-et/strings.xml b/service/ServiceConnectivityResources/res/values-et/strings.xml
index cf997b3..1c36b0f 100644
--- a/service/ServiceConnectivityResources/res/values-et/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-et/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Võrku sisselogimine"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Internetiühendus puudub"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Teie andmesidemaht operaatorilt <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> võib olla otsas. Puudutage valikute nägemiseks."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Andmesidemaht võib olla otsas. Puudutage valikute nägemiseks."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Võrgul <xliff:g id="NETWORK_SSID">%1$s</xliff:g> puudub Interneti-ühendus"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Puudutage valikute nägemiseks"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiilsidevõrgul puudub Interneti-ühendus"</string>
diff --git a/service/ServiceConnectivityResources/res/values-eu/strings.xml b/service/ServiceConnectivityResources/res/values-eu/strings.xml
index 13f9eb4..81d8ddb 100644
--- a/service/ServiceConnectivityResources/res/values-eu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-eu/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Hasi saioa sarean"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Ez zaude Internetera konektatuta"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Agian agortu egin dituzu <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> operadorearen planean sartzen zaizkizun datuak. Sakatu hau zer aukera dituzun ikusteko."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Datuak agortuko zitzaizkizun, agian. Sakatu hau zer aukera dituzun ikusteko."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Ezin da konektatu Internetera <xliff:g id="NETWORK_SSID">%1$s</xliff:g> sarearen bidez"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Sakatu aukerak ikusteko"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Sare mugikorra ezin da konektatu Internetera"</string>
diff --git a/service/ServiceConnectivityResources/res/values-fa/strings.xml b/service/ServiceConnectivityResources/res/values-fa/strings.xml
index 296ce8e..02c60df 100644
--- a/service/ServiceConnectivityResources/res/values-fa/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-fa/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ورود به سیستم شبکه"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"اتصال اینترنت وجود ندارد"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ممکن است داده <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> تمام شده باشد. برای گزینه‌ها ضربه بزنید."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ممکن است داده شما تمام شده باشد. برای گزینه‌ها ضربه بزنید."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> به اینترنت دسترسی ندارد"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"برای گزینه‌ها ضربه بزنید"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"شبکه تلفن همراه به اینترنت دسترسی ندارد"</string>
diff --git a/service/ServiceConnectivityResources/res/values-fi/strings.xml b/service/ServiceConnectivityResources/res/values-fi/strings.xml
index 07d2907..9c700d4 100644
--- a/service/ServiceConnectivityResources/res/values-fi/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-fi/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Kirjaudu verkkoon"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Ei internetyhteyttä"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Data (<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>) on ehkä lopussa. Näytä vaihtoehdot napauttamalla."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Datasi on ehkä lopussa. Näytä vaihtoehdot napauttamalla."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ei ole yhteydessä internetiin"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Näytä vaihtoehdot napauttamalla."</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiiliverkko ei ole yhteydessä internetiin"</string>
diff --git a/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml b/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml
index 7d5b366..2b7f031 100644
--- a/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Connectez-vous au réseau"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Aucune connexion Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Votre forfait de données avec <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> est peut-être épuisé. Touchez pour afficher les options."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Votre forfait de données est peut-être épuisé. Touchez pour afficher les options."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Le réseau <xliff:g id="NETWORK_SSID">%1$s</xliff:g> n\'offre aucun accès à Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Touchez pour afficher les options"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Le réseau cellulaire n\'offre aucun accès à Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-fr/strings.xml b/service/ServiceConnectivityResources/res/values-fr/strings.xml
index 2331d9b..fd179af 100644
--- a/service/ServiceConnectivityResources/res/values-fr/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-fr/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Se connecter au réseau"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Aucun accès à Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Vous avez peut-être consommé l\'intégralité de votre forfait de données de <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Appuyez pour voir les options disponibles."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Vous avez peut-être consommé l\'intégralité de votre forfait de données. Appuyez pour voir les options disponibles."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Aucune connexion à Internet pour <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Appuyez ici pour afficher des options."</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Le réseau mobile ne dispose d\'aucun accès à Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-gl/strings.xml b/service/ServiceConnectivityResources/res/values-gl/strings.xml
index f46f84b..1276730 100644
--- a/service/ServiceConnectivityResources/res/values-gl/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-gl/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Inicia sesión na rede"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Sen conexión a Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Pode que non che queden datos de <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Toca para ver as opcións."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Pode que non che queden datos. Toca para ver as opcións."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> non ten acceso a Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toca para ver opcións."</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"A rede de telefonía móbil non ten acceso a Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-gu/strings.xml b/service/ServiceConnectivityResources/res/values-gu/strings.xml
index ec9ecd3..cc0bca0 100644
--- a/service/ServiceConnectivityResources/res/values-gu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-gu/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"નેટવર્ક પર સાઇન ઇન કરો"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"કોઈ ઇન્ટરનેટ કનેક્શન નથી"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"તમારી પાસે <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>નો ડેટા બાકી ન હોય તેવું બની શકે છે. વિકલ્પો માટે ટૅપ કરો."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"તમારી પાસે ડેટા બાકી ન હોય તેવું બની શકે છે. વિકલ્પો માટે ટૅપ કરો."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"વિકલ્પો માટે ટૅપ કરો"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"મોબાઇલ નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
diff --git a/service/ServiceConnectivityResources/res/values-hi/strings.xml b/service/ServiceConnectivityResources/res/values-hi/strings.xml
index 6e3bc6b..4826a3c 100644
--- a/service/ServiceConnectivityResources/res/values-hi/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-hi/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"नेटवर्क में साइन इन करें"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"इंटरनेट कनेक्शन नहीं है"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"शायद आपके डिवाइस में <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> का डेटा खत्म हो गया है. विकल्पों के लिए टैप करें."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"शायद आपके डिवाइस में डेटा खत्म हो गया है. विकल्पों के लिए टैप करें."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> का इंटरनेट नहीं चल रहा है"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"विकल्पों के लिए टैप करें"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"मोबाइल नेटवर्क पर इंटरनेट ऐक्सेस नहीं है"</string>
diff --git a/service/ServiceConnectivityResources/res/values-hr/strings.xml b/service/ServiceConnectivityResources/res/values-hr/strings.xml
index 6a6de4c..ace8c81 100644
--- a/service/ServiceConnectivityResources/res/values-hr/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-hr/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Prijava na mrežu"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Nema interneta"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Možda ste potrošili podatkovni promet od operatera <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Dodirnite za opcije."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Možda ste potrošili podatkovni promet. Dodirnite za opcije."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Dodirnite za opcije"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilna mreža nema pristup internetu"</string>
diff --git a/service/ServiceConnectivityResources/res/values-hu/strings.xml b/service/ServiceConnectivityResources/res/values-hu/strings.xml
index 1d39d30..ec96193 100644
--- a/service/ServiceConnectivityResources/res/values-hu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-hu/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Bejelentkezés a hálózatba"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Nincs internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Előfordulhat, hogy elfogyott az adatkerete a szolgáltatónál (<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>). Koppintson a lehetőségek megjelenítéséhez."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Előfordulhat, hogy elfogyott az adatkerete. Koppintson a lehetőségek megjelenítéséhez."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"A(z) <xliff:g id="NETWORK_SSID">%1$s</xliff:g> hálózaton nincs internet-hozzáférés"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Koppintson a beállítások megjelenítéséhez"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"A mobilhálózaton nincs internet-hozzáférés"</string>
diff --git a/service/ServiceConnectivityResources/res/values-hy/strings.xml b/service/ServiceConnectivityResources/res/values-hy/strings.xml
index 99386d4..8d82899 100644
--- a/service/ServiceConnectivityResources/res/values-hy/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-hy/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Մուտք գործեք ցանց"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Կապ չկա"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Հնարավոր է՝ <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> օպերատորի ձեր սահմանաչափը սպառվել է։ Հպեք՝ տարբերակները տեսնելու համար։"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Հնարավոր է՝ ձեր սահմանաչափը սպառվել է։ Հպեք՝ տարբերակները տեսնելու համար։"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ցանցը չունի մուտք ինտերնետին"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Հպեք՝ ընտրանքները տեսնելու համար"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Բջջային ցանցը չի ապահովում ինտերնետ կապ"</string>
diff --git a/service/ServiceConnectivityResources/res/values-in/strings.xml b/service/ServiceConnectivityResources/res/values-in/strings.xml
index f47d257..41226a2 100644
--- a/service/ServiceConnectivityResources/res/values-in/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-in/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Login ke jaringan"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Tidak ada internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Data <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> Anda mungkin sudah habis. Ketuk untuk melihat opsi."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Data seluler Anda mungkin sudah habis. Ketuk untuk melihat opsi."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tidak memiliki akses internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Ketuk untuk melihat opsi"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Jaringan seluler tidak memiliki akses internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-is/strings.xml b/service/ServiceConnectivityResources/res/values-is/strings.xml
index eeba231..423413e 100644
--- a/service/ServiceConnectivityResources/res/values-is/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-is/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Skrá inn á net"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Engin nettenging"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Gagnamagnið þitt frá <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> gæti verið búið. Ýttu til að sjá valkosti."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Gagnamagnið þitt gæti verið búið. Ýttu til að sjá valkosti."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> er ekki með internetaðgang"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Ýttu til að sjá valkosti"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Farsímakerfið er ekki tengt við internetið"</string>
diff --git a/service/ServiceConnectivityResources/res/values-it/strings.xml b/service/ServiceConnectivityResources/res/values-it/strings.xml
index ec3ff8c..62fc74e 100644
--- a/service/ServiceConnectivityResources/res/values-it/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-it/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Accedi alla rete"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Nessuna connessione a internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"I dati di <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> potrebbero essere esauriti. Tocca per le opzioni."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Potresti avere esaurito i dati. Tocca per le opzioni."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> non ha accesso a Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tocca per le opzioni"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"La rete mobile non ha accesso a Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-iw/strings.xml b/service/ServiceConnectivityResources/res/values-iw/strings.xml
index d123ebb..b5b4071 100644
--- a/service/ServiceConnectivityResources/res/values-iw/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-iw/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"היכנס לרשת"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"אין אינטרנט"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"יכול להיות שחבילת הגלישה שלך אצל <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> נגמרה. אפשר להקיש כדי להציג את האפשרויות."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"יכול להיות שחבילת הגלישה נגמרה. אפשר להקיש כדי להציג את האפשרויות."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"ל-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> אין גישה לאינטרנט"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"הקש לקבלת אפשרויות"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"לרשת הסלולרית אין גישה לאינטרנט"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ja/strings.xml b/service/ServiceConnectivityResources/res/values-ja/strings.xml
index 7bb6f85..0021efa 100644
--- a/service/ServiceConnectivityResources/res/values-ja/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ja/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ネットワークにログインしてください"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"インターネットに接続されていません"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> の使用可能なデータ量が不足している可能性があります。タップするとオプションが表示されます。"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"使用可能なデータ量が不足している可能性があります。タップするとオプションが表示されます。"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> はインターネットにアクセスできません"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"タップしてその他のオプションを表示"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"モバイル ネットワークがインターネットに接続されていません"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ka/strings.xml b/service/ServiceConnectivityResources/res/values-ka/strings.xml
index f42c567..b05d59b 100644
--- a/service/ServiceConnectivityResources/res/values-ka/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ka/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ქსელში შესვლა"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"ინტერნეტი არ არის"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"შესაძლოა, დაგიმთავრდათ მობილური ინტერნეტი <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>-სგან. შეეხეთ ვარიანტების სანახავად."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"შესაძლოა, მობილური ინტერნეტი დაგიმთავრდათ. შეეხეთ ვარიანტების სანახავად."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-ს არ აქვს ინტერნეტზე წვდომა"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"შეეხეთ ვარიანტების სანახავად"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"მობილურ ქსელს არ აქვს ინტერნეტზე წვდომა"</string>
diff --git a/service/ServiceConnectivityResources/res/values-kk/strings.xml b/service/ServiceConnectivityResources/res/values-kk/strings.xml
index efe23b6..099e3a6 100644
--- a/service/ServiceConnectivityResources/res/values-kk/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-kk/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Желіге кіру"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Интернет жоқ"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> трафигі таусылған болуы мүмкін. Опцияларды көру үшін түртіңіз."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Трафик таусылған болуы мүмкін. Опцияларды көру үшін түртіңіз."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> желісінің интернетті пайдалану мүмкіндігі шектеулі."</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Опциялар үшін түртіңіз"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Мобильдік желі интернетке қосылмаған."</string>
diff --git a/service/ServiceConnectivityResources/res/values-km/strings.xml b/service/ServiceConnectivityResources/res/values-km/strings.xml
index fa06c5b..5ca4f00 100644
--- a/service/ServiceConnectivityResources/res/values-km/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-km/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ចូលទៅបណ្តាញ"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"គ្មាន​អ៊ីនធឺណិតទេ"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"អ្នកប្រហែលអស់ទិន្នន័យពី <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> ហើយ។ ចុចដើម្បីទទួលបានជម្រើស។"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"អ្នកប្រហែលអស់ទិន្នន័យហើយ។ ចុចដើម្បីទទួលបានជម្រើស។"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> មិនមាន​ការតភ្ជាប់អ៊ីនធឺណិត​ទេ"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ប៉ះសម្រាប់ជម្រើស"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"បណ្ដាញ​ទូរសព្ទ​ចល័ត​មិនមានការតភ្ជាប់​អ៊ីនធឺណិតទេ"</string>
diff --git a/service/ServiceConnectivityResources/res/values-kn/strings.xml b/service/ServiceConnectivityResources/res/values-kn/strings.xml
index 8046d0e..044aff6 100644
--- a/service/ServiceConnectivityResources/res/values-kn/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-kn/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ನೆಟ್‌ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"ಇಂಟರ್ನೆಟ್ ಇಲ್ಲ"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> ನ ನಿಮ್ಮ ಡೇಟಾ ಮುಗಿದಿರಬಹುದು. ಆಯ್ಕೆಗಳಿಗಾಗಿ ಟ್ಯಾಪ್ ಮಾಡಿ."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ನಿಮ್ಮ ಡೇಟಾ ಮುಗಿದಿರಬಹುದು. ಆಯ್ಕೆಗಳಿಗಾಗಿ ಟ್ಯಾಪ್ ಮಾಡಿ."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ಆಯ್ಕೆಗಳಿಗೆ ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"ಮೊಬೈಲ್ ನೆಟ್‌ವರ್ಕ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ko/strings.xml b/service/ServiceConnectivityResources/res/values-ko/strings.xml
index eef59a9..1d1e989 100644
--- a/service/ServiceConnectivityResources/res/values-ko/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ko/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"네트워크에 로그인"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"인터넷 연결 없음"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>에서 제공하는 데이터를 모두 소진했을 수 있습니다. 탭하여 옵션을 확인하세요."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"데이터를 모두 소진했을 수 있습니다. 탭하여 옵션을 확인하세요."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>이(가) 인터넷에 액세스할 수 없습니다."</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"탭하여 옵션 보기"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"모바일 네트워크에 인터넷이 연결되어 있지 않습니다."</string>
diff --git a/service/ServiceConnectivityResources/res/values-ky/strings.xml b/service/ServiceConnectivityResources/res/values-ky/strings.xml
index 0027c8a..398531a 100644
--- a/service/ServiceConnectivityResources/res/values-ky/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ky/strings.xml
@@ -22,8 +22,11 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Тармакка кирүү"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Интернет жок"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> трафиги түгөнгөн окшойт. Параметрлерди ачуу үчүн таптаңыз."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Трафик түгөнгөн окшойт. Параметрлерди ачуу үчүн таптаңыз."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> Интернетке туташуусу жок"</string>
-    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Параметрлерди ачуу үчүн таптап коюңуз"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Параметрлерди ачуу үчүн тийип коюңуз"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Мобилдик Интернет жок"</string>
     <string name="other_networks_no_internet" msgid="5693932964749676542">"Тармактын Интернет жок"</string>
     <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Жеке DNS сервери жеткиликсиз"</string>
diff --git a/service/ServiceConnectivityResources/res/values-lo/strings.xml b/service/ServiceConnectivityResources/res/values-lo/strings.xml
index 64419f9..2967441 100644
--- a/service/ServiceConnectivityResources/res/values-lo/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-lo/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ລົງຊື່ເຂົ້າເຄືອຂ່າຍ"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"ບໍ່ມີອິນເຕີເນັດ"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ອິນເຕີເນັດຂອງທ່ານຈາກເຄືອຂ່າຍ <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> ອາດໝົດ. ແຕະເພື່ອເບິ່ງຕົວເລືອກ."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ອິນເຕີເນັດຂອງທ່ານອາດໝົດ. ແຕະເພື່ອເບິ່ງຕົວເລືອກ."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ແຕະເພື່ອເບິ່ງຕົວເລືອກ"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"ເຄືອຂ່າຍມືຖືບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້"</string>
diff --git a/service/ServiceConnectivityResources/res/values-lt/strings.xml b/service/ServiceConnectivityResources/res/values-lt/strings.xml
index f73f142..1f2569d 100644
--- a/service/ServiceConnectivityResources/res/values-lt/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-lt/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Prisijungti prie tinklo"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Nėra interneto ryšio"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Gali būti, kad nebeturite „<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>“ duomenų. Palieskite, kad būtų rodomos parinktys."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Gali būti, kad nebeturite duomenų. Palieskite, kad būtų rodomos parinktys."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"„<xliff:g id="NETWORK_SSID">%1$s</xliff:g>“ negali pasiekti interneto"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Palieskite, kad būtų rodomos parinktys."</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiliojo ryšio tinkle nėra prieigos prie interneto"</string>
diff --git a/service/ServiceConnectivityResources/res/values-lv/strings.xml b/service/ServiceConnectivityResources/res/values-lv/strings.xml
index ce063a5..837a21f 100644
--- a/service/ServiceConnectivityResources/res/values-lv/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-lv/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Pierakstīšanās tīklā"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Nav interneta"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Iespējams, ir sasniegts <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> datu limits. Pieskarieties, lai skatītu iespējas."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Iespējams, ir sasniegts datu limits. Pieskarieties, lai skatītu iespējas."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Tīklā <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nav piekļuves internetam"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Pieskarieties, lai skatītu opcijas."</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilajā tīklā nav piekļuves internetam."</string>
diff --git a/service/ServiceConnectivityResources/res/values-mk/strings.xml b/service/ServiceConnectivityResources/res/values-mk/strings.xml
index fb105e0..6fa1563 100644
--- a/service/ServiceConnectivityResources/res/values-mk/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-mk/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Најавете се на мрежа"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Нема интернет"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Можеби се потрошил мобилниот интернет од <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Допрете за опции."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Можеби се потрошил мобилниот интернет. Допрете за опции."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нема интернет-пристап"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Допрете за опции"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Мобилната мрежа нема интернет-пристап"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ml/strings.xml b/service/ServiceConnectivityResources/res/values-ml/strings.xml
index 9a51238..0797902 100644
--- a/service/ServiceConnectivityResources/res/values-ml/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ml/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"ഇന്റർനെറ്റ് ഇല്ല"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"നിങ്ങളുടെ <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> എന്നതിലെ ഡാറ്റ തീർന്നിട്ടുണ്ടാകാം. ഓപ്ഷനുകൾക്ക് ടാപ്പ് ചെയ്യുക."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"നിങ്ങളുടെ ഡാറ്റ തീർന്നിട്ടുണ്ടാകാം. ഓപ്ഷനുകൾക്ക് ടാപ്പ് ചെയ്യുക."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> എന്നതിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ഓപ്ഷനുകൾക്ക് ടാപ്പുചെയ്യുക"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"മൊബെെൽ നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
diff --git a/service/ServiceConnectivityResources/res/values-mn/strings.xml b/service/ServiceConnectivityResources/res/values-mn/strings.xml
index 8372533..9af035b 100644
--- a/service/ServiceConnectivityResources/res/values-mn/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-mn/strings.xml
@@ -22,9 +22,12 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Сүлжээнд нэвтэрнэ үү"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Интернэт байхгүй"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Таны <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>-н дата дууссан байж магадгүй. Сонголтыг харахын тулд товшино уу."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Таны дата дууссан байж магадгүй. Сонголтыг харахын тулд товшино уу."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-д интернэтийн хандалт алга"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Сонголт хийхийн тулд товшино уу"</string>
-    <string name="mobile_no_internet" msgid="4087718456753201450">"Мобайл сүлжээнд интернэт хандалт байхгүй байна"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Хөдөлгөөнт холбооны сүлжээнд интернэт хандалт байхгүй байна"</string>
     <string name="other_networks_no_internet" msgid="5693932964749676542">"Сүлжээнд интернэт хандалт байхгүй байна"</string>
     <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Хувийн DNS серверт хандах боломжгүй байна"</string>
     <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> зарим үйлчилгээнд хандах боломжгүй байна"</string>
diff --git a/service/ServiceConnectivityResources/res/values-mr/strings.xml b/service/ServiceConnectivityResources/res/values-mr/strings.xml
index 658b19b..4797ff1 100644
--- a/service/ServiceConnectivityResources/res/values-mr/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-mr/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"नेटवर्कवर साइन इन करा"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"इंटरनेट नाही"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"तुमचा <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> वरील डेटा संपला असेल. पर्यायांसाठी टॅप करा."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"तुमचा डेटा संपला असेल. पर्यायांसाठी टॅप करा."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ला इंटरनेट अ‍ॅक्सेस नाही"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"पर्यायांसाठी टॅप करा"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"मोबाइल नेटवर्कला इंटरनेट ॲक्सेस नाही"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ms/strings.xml b/service/ServiceConnectivityResources/res/values-ms/strings.xml
index 84b242c..de38c9c 100644
--- a/service/ServiceConnectivityResources/res/values-ms/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ms/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Log masuk ke rangkaian"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Tiada Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Anda mungkin kehabisan data daripada <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Ketik untuk melihat pilihan."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Anda mungkin kehabisan data. Ketik untuk melihat pilihan."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiada akses Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Ketik untuk mendapatkan pilihan"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Rangkaian mudah alih tiada akses Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-my/strings.xml b/service/ServiceConnectivityResources/res/values-my/strings.xml
index 6832263..fce3d58 100644
--- a/service/ServiceConnectivityResources/res/values-my/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-my/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ကွန်ယက်သို့ လက်မှတ်ထိုးဝင်ရန်"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"အင်တာနက် မရှိပါ"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> ထံမှ ဒေတာကုန်သွားခြင်း ဖြစ်နိုင်သည်။ ရွေးစရာများကြည့်ရန် တို့ပါ။"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ဒေတာကုန်သွားခြင်း ဖြစ်နိုင်သည်။ ရွေးစရာများကြည့်ရန် တို့ပါ။"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"အခြားရွေးချယ်စရာများကိုကြည့်ရန် တို့ပါ"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"မိုဘိုင်းကွန်ရက်တွင် အင်တာနက်ချိတ်ဆက်မှု မရှိပါ"</string>
diff --git a/service/ServiceConnectivityResources/res/values-nb/strings.xml b/service/ServiceConnectivityResources/res/values-nb/strings.xml
index fff6530..33c30b8 100644
--- a/service/ServiceConnectivityResources/res/values-nb/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-nb/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Logg på nettverk"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Ingen internettilkobling"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Du har kanskje gått tom for data fra <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Trykk for å se alternativer."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Du har kanskje gått tom for data. Trykk for å se alternativer."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internettilkobling"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Trykk for å få alternativer"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilnettverket har ingen internettilgang"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ne/strings.xml b/service/ServiceConnectivityResources/res/values-ne/strings.xml
index 2eaf162..a2f9997 100644
--- a/service/ServiceConnectivityResources/res/values-ne/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ne/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"सञ्जालमा साइन इन गर्नुहोस्"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"इन्टरनेट छैन"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> को मोबाइल डेटा सकियो होला। विकल्पहरू हेर्न ट्याप गर्नुहोस्।"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"मोबाइल डेटा सकियो होला। विकल्पहरू हेर्न ट्याप गर्नुहोस्।"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> को इन्टरनेटमाथि पहुँच छैन"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"विकल्पहरूका लागि ट्याप गर्नुहोस्"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"मोबाइल नेटवर्कको इन्टरनेटमाथि पहुँच छैन"</string>
diff --git a/service/ServiceConnectivityResources/res/values-nl/strings.xml b/service/ServiceConnectivityResources/res/values-nl/strings.xml
index 394c552..116e255 100644
--- a/service/ServiceConnectivityResources/res/values-nl/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-nl/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Inloggen bij netwerk"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Geen internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Je hebt misschien geen data meer van <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Tik voor opties."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Je hebt misschien geen data meer. Tik voor opties."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> heeft geen internettoegang"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tik voor opties"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiel netwerk heeft geen internettoegang"</string>
diff --git a/service/ServiceConnectivityResources/res/values-or/strings.xml b/service/ServiceConnectivityResources/res/values-or/strings.xml
index 49a773a..d15d42e 100644
--- a/service/ServiceConnectivityResources/res/values-or/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-or/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ନେଟ୍‌ୱର୍କରେ ସାଇନ୍‍ ଇନ୍‍ କରନ୍ତୁ"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"ଇଣ୍ଟରନେଟ ନାହିଁ"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ଆପଣଙ୍କ <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> ଡାଟା ଶେଷ ହୋଇଥାଇପାରେ। ବିକଳ୍ପଗୁଡ଼ିକ ପାଇଁ ଟାପ କରନ୍ତୁ।"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ଆପଣଙ୍କ ଡାଟା ଶେଷ ହୋଇଥାଇପାରେ। ବିକଳ୍ପଗୁଡ଼ିକ ପାଇଁ ଟାପ କରନ୍ତୁ।"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ର ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ବିକଳ୍ପ ପାଇଁ ଟାପ୍‍ କରନ୍ତୁ"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"ମୋବାଇଲ୍ ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
diff --git a/service/ServiceConnectivityResources/res/values-pa/strings.xml b/service/ServiceConnectivityResources/res/values-pa/strings.xml
index 9f71cac..103d094 100644
--- a/service/ServiceConnectivityResources/res/values-pa/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-pa/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"ਇੰਟਰਨੈੱਟ ਨਹੀਂ ਹੈ"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ਸ਼ਾਇਦ ਤੁਹਾਡੇ <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> ਦਾ ਡਾਟਾ ਖਤਮ ਹੋ ਗਿਆ ਹੈ। ਵਿਕਲਪਾਂ ਲਈ ਟੈਪ ਕਰੋ।"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ਸ਼ਾਇਦ ਤੁਹਾਡਾ ਡਾਟਾ ਖਤਮ ਹੋ ਗਿਆ ਹੈ। ਵਿਕਲਪਾਂ ਲਈ ਟੈਪ ਕਰੋ।"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ਵਿਕਲਪਾਂ ਲਈ ਟੈਪ ਕਰੋ"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"ਮੋਬਾਈਲ ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
diff --git a/service/ServiceConnectivityResources/res/values-pl/strings.xml b/service/ServiceConnectivityResources/res/values-pl/strings.xml
index cc84e29..bafa57a 100644
--- a/service/ServiceConnectivityResources/res/values-pl/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-pl/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Zaloguj się do sieci"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Brak internetu"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Prawdopodobnie wyczerpał się pakiet danych z: <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Kliknij, aby wyświetlić opcje."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Prawdopodobnie wyczerpał się pakiet danych. Kliknij, aby wyświetlić opcje."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nie ma dostępu do internetu"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Kliknij, by wyświetlić opcje"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Sieć komórkowa nie ma dostępu do internetu"</string>
diff --git a/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml b/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml
index 3c15a76..a2d5ad2 100644
--- a/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Fazer login na rede"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Sem Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Os dados da operadora <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> podem ter acabado. Toque para conferir suas opções."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Os dados podem ter acabado. Toque para conferir suas opções."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toque para ver opções"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"A rede móvel não tem acesso à Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml b/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml
index 48dde75..05934f3 100644
--- a/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Início de sessão na rede"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Sem Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"É possível que já não tenha dados da <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Toque para aceder às opções."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"É possível que já não tenha dados. Toque para aceder às opções."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toque para obter mais opções"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"A rede móvel não tem acesso à Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-pt/strings.xml b/service/ServiceConnectivityResources/res/values-pt/strings.xml
index 3c15a76..a2d5ad2 100644
--- a/service/ServiceConnectivityResources/res/values-pt/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-pt/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Fazer login na rede"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Sem Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Os dados da operadora <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> podem ter acabado. Toque para conferir suas opções."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Os dados podem ter acabado. Toque para conferir suas opções."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toque para ver opções"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"A rede móvel não tem acesso à Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ro/strings.xml b/service/ServiceConnectivityResources/res/values-ro/strings.xml
index bf4479a..082bba8 100644
--- a/service/ServiceConnectivityResources/res/values-ro/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ro/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Conectează-te la rețea"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Fără conexiune la internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Este posibil să fi epuizat datele de la <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Atinge pentru opțiuni."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Este posibil să fi epuizat datele. Atinge pentru opțiuni."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nu are acces la internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Atinge pentru opțiuni"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Rețeaua mobilă nu are acces la internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ru/strings.xml b/service/ServiceConnectivityResources/res/values-ru/strings.xml
index 2e074ed..3c5b7dd 100644
--- a/service/ServiceConnectivityResources/res/values-ru/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ru/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Регистрация в сети"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Нет интернета"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Возможно, закончился трафик по тарифу оператора \"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>\". Нажмите, чтобы посмотреть варианты дальнейших действий."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Возможно, у вас закончился трафик. Нажмите, чтобы посмотреть варианты дальнейших действий."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Сеть \"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>\" не подключена к Интернету"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Нажмите, чтобы показать варианты."</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Мобильная сеть не подключена к Интернету"</string>
diff --git a/service/ServiceConnectivityResources/res/values-si/strings.xml b/service/ServiceConnectivityResources/res/values-si/strings.xml
index a4f720a..70e0252 100644
--- a/service/ServiceConnectivityResources/res/values-si/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-si/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ජාලයට පුරනය වන්න"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"අන්තර්ජාලය නැත"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ඔබට <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> වෙතින් දත්ත අවසන් විය හැක. විකල්ප සඳහා තට්ටු කරන්න."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ඔබට දත්ත අවසන් විය හැක. විකල්ප සඳහා තට්ටු කරන්න."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> හට අන්තර්ජාල ප්‍රවේශය නැත"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"විකල්ප සඳහා තට්ටු කරන්න"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"ජංගම ජාලවලට අන්තර්ජාල ප්‍රවේශය නැත"</string>
diff --git a/service/ServiceConnectivityResources/res/values-sk/strings.xml b/service/ServiceConnectivityResources/res/values-sk/strings.xml
index 432b670..58b6aab 100644
--- a/service/ServiceConnectivityResources/res/values-sk/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sk/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Prihlásenie do siete"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Žiadny internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Možno vám došli dáta od operátora <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Klepnutím si zobrazíte možnosti."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Možno vám došli dáta. Klepnutím si zobrazíte možnosti."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nemá prístup k internetu"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Klepnutím získate možnosti"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilná sieť nemá prístup k internetu"</string>
diff --git a/service/ServiceConnectivityResources/res/values-sl/strings.xml b/service/ServiceConnectivityResources/res/values-sl/strings.xml
index b727614..1e28ee8 100644
--- a/service/ServiceConnectivityResources/res/values-sl/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sl/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Prijava v omrežje"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Ni internetne povezave"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Morda ste že porabili zakupljeno količino prenosa podatkov v mobilnem omrežju <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Dotaknite se za možnosti."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Morda ste že porabili zakupljeno količino prenosa podatkov v mobilnem omrežju. Dotaknite se za možnosti."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Omrežje <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nima dostopa do interneta"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Dotaknite se za možnosti"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilno omrežje nima dostopa do interneta"</string>
diff --git a/service/ServiceConnectivityResources/res/values-sq/strings.xml b/service/ServiceConnectivityResources/res/values-sq/strings.xml
index 85bd84f..48ac926 100644
--- a/service/ServiceConnectivityResources/res/values-sq/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sq/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Identifikohu në rrjet"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Nuk ka internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Mund të të kenë mbaruar të dhënat nga <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Trokit për opsionet."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Mund të të kenë mbaruar të dhënat. Trokit për opsionet."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nuk ka qasje në internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Trokit për opsionet"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Rrjeti celular nuk ka qasje në internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-sr/strings.xml b/service/ServiceConnectivityResources/res/values-sr/strings.xml
index 928dc79..7bf1bb3 100644
--- a/service/ServiceConnectivityResources/res/values-sr/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sr/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Пријавите се на мрежу"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Нема интернета"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Можда сте потрошили <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> податке. Додирните за опције."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Можда сте потрошили податке. Додирните за опције."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нема приступ интернету"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Додирните за опције"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Мобилна мрежа нема приступ интернету"</string>
diff --git a/service/ServiceConnectivityResources/res/values-sv/strings.xml b/service/ServiceConnectivityResources/res/values-sv/strings.xml
index d714124..61d49e7 100644
--- a/service/ServiceConnectivityResources/res/values-sv/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sv/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Logga in på nätverket"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Inget internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Din data från <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> kan ha tagit slut. Tryck för alternativ."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Din data kan ha tagit slut. Tryck för alternativ."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internetanslutning"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tryck för alternativ"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilnätverket har ingen internetanslutning"</string>
diff --git a/service/ServiceConnectivityResources/res/values-sw/strings.xml b/service/ServiceConnectivityResources/res/values-sw/strings.xml
index 15d6cab..29ec013 100644
--- a/service/ServiceConnectivityResources/res/values-sw/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sw/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Ingia katika mtandao"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Hakuna intaneti"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Huenda data ya <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> imeisha. Gusa ili upate chaguo."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Huenda data yako imeisha. Gusa ili upate chaguo."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> haina uwezo wa kufikia intaneti"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Gusa ili upate chaguo"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mtandao wa simu hauna uwezo wa kufikia intaneti"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ta/strings.xml b/service/ServiceConnectivityResources/res/values-ta/strings.xml
index 9850a35..3bb82bb 100644
--- a/service/ServiceConnectivityResources/res/values-ta/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ta/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"நெட்வொர்க்கில் உள்நுழையவும்"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"இணைய இணைப்பு இல்லை"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> டேட்டா தீர்ந்திருக்கக்கூடும். விருப்பங்களுக்கு தட்டவும்."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"உங்கள் டேட்டா தீர்ந்திருக்கக்கூடும். விருப்பங்களுக்கு தட்டவும்."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"விருப்பங்களுக்கு, தட்டவும்"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"மொபைல் நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
diff --git a/service/ServiceConnectivityResources/res/values-te/strings.xml b/service/ServiceConnectivityResources/res/values-te/strings.xml
index f7182a8..42e010c 100644
--- a/service/ServiceConnectivityResources/res/values-te/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-te/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"ఇంటర్నెట్ లేదు"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"మీ <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> డేటా అయిపోయి ఉండవచ్చు. ఆప్షన్‌ల కోసం ట్యాప్ చేయండి."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"మీ డేటా అయిపోయి ఉండవచ్చు. ఆప్షన్‌ల కోసం ట్యాప్ చేయండి."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>కి ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ఎంపికల కోసం నొక్కండి"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"మొబైల్ నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
diff --git a/service/ServiceConnectivityResources/res/values-th/strings.xml b/service/ServiceConnectivityResources/res/values-th/strings.xml
index 7049309..fe99257 100644
--- a/service/ServiceConnectivityResources/res/values-th/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-th/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"ลงชื่อเข้าใช้เครือข่าย"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"ไม่มีอินเทอร์เน็ต"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"คุณอาจถึงขีดจำกัดการใช้งานอินเทอร์เน็ตของ <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> แล้ว แตะเพื่อดูตัวเลือก"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"คุณอาจใช้งานอินเทอร์เน็ตถึงขีดจำกัดแล้ว แตะเพื่อดูตัวเลือก"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> เข้าถึงอินเทอร์เน็ตไม่ได้"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"แตะเพื่อดูตัวเลือก"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"เครือข่ายมือถือไม่มีการเข้าถึงอินเทอร์เน็ต"</string>
diff --git a/service/ServiceConnectivityResources/res/values-tl/strings.xml b/service/ServiceConnectivityResources/res/values-tl/strings.xml
index c866fd4..de42455 100644
--- a/service/ServiceConnectivityResources/res/values-tl/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-tl/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Mag-sign in sa network"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Walang internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Posibleng ubos na ang data mo sa <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. I-tap para sa mga opsyon."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Posibleng naubusan ka ng data. I-tap para sa mga opsyon."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Walang access sa internet ang <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"I-tap para sa mga opsyon"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Walang access sa internet ang mobile network"</string>
diff --git a/service/ServiceConnectivityResources/res/values-tr/strings.xml b/service/ServiceConnectivityResources/res/values-tr/strings.xml
index c4930a8..c65b210 100644
--- a/service/ServiceConnectivityResources/res/values-tr/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-tr/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Ağda oturum açın"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"İnternet bağlantısı yok"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> veri paketiniz tükenmiş olabilir. Seçenekler için dokunun."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Veri paketiniz tükenmiş olabilir. Seçenekler için dokunun."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ağının internet bağlantısı yok"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Seçenekler için dokunun"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobil ağın internet bağlantısı yok"</string>
diff --git a/service/ServiceConnectivityResources/res/values-uk/strings.xml b/service/ServiceConnectivityResources/res/values-uk/strings.xml
index 8811263..5950a24 100644
--- a/service/ServiceConnectivityResources/res/values-uk/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-uk/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Вхід у мережу"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Немає Інтернету"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Можливо, обсяг мобільного трафіку від <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> вичерпано. Торкніться, щоб відкрити опції."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Можливо, обсяг мобільного трафіку вичерпано. Торкніться, щоб відкрити опції."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"Мережа <xliff:g id="NETWORK_SSID">%1$s</xliff:g> не має доступу до Інтернету"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Торкніться, щоб відкрити опції"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Мобільна мережа не має доступу до Інтернету"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ur/strings.xml b/service/ServiceConnectivityResources/res/values-ur/strings.xml
index 8f9656c..6af95b0 100644
--- a/service/ServiceConnectivityResources/res/values-ur/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ur/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"نیٹ ورک میں سائن ان کریں"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"انٹرنیٹ نہیں ہے"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ہو سکتا ہے کہ آپ کے <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> کا ڈیٹا ختم ہو جائے۔ اختیارات کے لیے تھپتھپائیں۔"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ہو سکتا ہے کہ آپ کا ڈیٹا ختم ہو جائے۔ اختیارات کے لیے تھپتھپائیں۔"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"اختیارات کیلئے تھپتھپائیں"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"موبائل نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
diff --git a/service/ServiceConnectivityResources/res/values-uz/strings.xml b/service/ServiceConnectivityResources/res/values-uz/strings.xml
index d7285ad..29bc99e 100644
--- a/service/ServiceConnectivityResources/res/values-uz/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-uz/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Tarmoqqa kirish"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Internet yoʻq"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> tarifida mobil trafik qolmagan boʻlishi mumkin. Yechim olish uchun bosing."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Internet trafik qolmagan boʻlishi mumkin. Yechim olish uchun bosing."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nomli tarmoqda internetga ruxsati yoʻq"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Variantlarni ko‘rsatish uchun bosing"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobil tarmoq internetga ulanmagan"</string>
diff --git a/service/ServiceConnectivityResources/res/values-vi/strings.xml b/service/ServiceConnectivityResources/res/values-vi/strings.xml
index 239fb81..42d168e 100644
--- a/service/ServiceConnectivityResources/res/values-vi/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-vi/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Đăng nhập vào mạng"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Không có kết nối Internet"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Có thể bạn đã dùng hết dữ liệu của <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Hãy nhấn để xem các lựa chọn."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Có thể bạn đã dùng hết dữ liệu. Hãy nhấn để xem các lựa chọn."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> không có quyền truy cập Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Nhấn để biết tùy chọn"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mạng di động không có quyền truy cập Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml
index e318c0b..07271bf 100644
--- a/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"登录到网络"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"未连接到互联网"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> 提供的数据流量可能已用尽。点按即可查看选项。"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"您的数据流量可能已用尽。点按即可查看选项。"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 无法访问互联网"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"点按即可查看相关选项"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"此移动网络无法访问互联网"</string>
diff --git a/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml
index af3dccd..92c605b 100644
--- a/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"登入網絡"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"沒有互聯網連線"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"你的「<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>」數據可能已用完。輕按即可查看選項。"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"你的數據可能已用完。輕按即可查看選項。"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>未有連接至互聯網"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"輕按即可查看選項"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"流動網絡並未連接互聯網"</string>
diff --git a/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml
index 6441707..81f9ddb 100644
--- a/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"登入網路"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"沒有網際網路連線"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> 提供的數據流量可能已用盡。輕觸即可查看選項。"</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"數據流量可能已用盡。輕觸即可查看選項。"</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 沒有網際網路連線"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"輕觸即可查看選項"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"這個行動網路沒有網際網路連線"</string>
diff --git a/service/ServiceConnectivityResources/res/values-zu/strings.xml b/service/ServiceConnectivityResources/res/values-zu/strings.xml
index b59f0d1..0338484 100644
--- a/service/ServiceConnectivityResources/res/values-zu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-zu/strings.xml
@@ -22,6 +22,9 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Ngena ngemvume kunethiwekhi"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
+    <string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Ayikho i-inthanethi"</string>
+    <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Kungenzeka uphelelwe idatha evela ku-<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g>. Thepha ukuze uthole okungakhethwa kukho."</string>
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Kungenzeka uphelelwe yidatha. Thepha ukuze uthole okungakhethwa kukho."</string>
     <string name="wifi_no_internet" msgid="1326348603404555475">"I-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ayinakho ukufinyelela kwe-inthanethi"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Thepha ukuze uthole izinketho"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Inethiwekhi yeselula ayinakho ukufinyelela kwe-inthanethi"</string>
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 22d9b01..2d3647a 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -128,6 +128,13 @@
     <string-array translatable="false" name="config_networkNotifySwitches">
     </string-array>
 
+    <!-- An array of priorities of service types for services to be offloaded via
+         NsdManager#registerOffloadEngine.
+         Format is [priority int]:[service type], for example: "0:_testservice._tcp"
+    -->
+    <string-array translatable="false" name="config_nsdOffloadServicesPriority">
+    </string-array>
+
     <!-- Whether to use an ongoing notification for signing in to captive portals, instead of a
          notification that can be dismissed. -->
     <bool name="config_ongoingSignInNotification">false</bool>
@@ -135,10 +142,17 @@
     <!-- Whether to cancel network notifications automatically when tapped -->
     <bool name="config_autoCancelNetworkNotifications">true</bool>
 
-    <!-- When no internet or partial connectivity is detected on a network, and a high priority
-         (heads up) notification would be shown due to the network being explicitly selected,
-         directly show the dialog that would normally be shown when tapping the notification
-         instead of showing the notification. -->
+    <!-- Configuration to let OEMs customize what to do when :
+         • Partial connectivity is detected on the network
+         • No internet is detected on the network, and
+           - the network was explicitly selected
+           - the system is configured to actively prefer bad wifi (see config_activelyPreferBadWifi)
+         The default behavior (false) is to post a notification with a PendingIntent so
+         the user is informed and can act if they wish.
+         Making this true instead will have the system fire the intent immediately instead
+         of showing a notification. OEMs who do this should have some intent receiver
+         listening to the intent and take the action they prefer (e.g. show a dialog,
+         show a customized notification etc).  -->
     <bool name="config_notifyNoInternetAsDialogWhenHighPriority">false</bool>
 
     <!-- When showing notifications indicating partial connectivity, display the same notifications
@@ -187,8 +201,11 @@
         -->
     </string-array>
 
-    <!-- Regex of wired ethernet ifaces -->
-    <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
+    <!-- Regex of wired ethernet ifaces. Network interfaces that match this regex will be tracked
+         by ethernet service.
+         If set to "*", ethernet service uses "(eth|usb)\\d+" on Android V+ and eth\\d+ on
+         Android T and U. -->
+    <string translatable="false" name="config_ethernet_iface_regex">*</string>
 
     <!-- Ignores Wi-Fi validation failures after roam.
     If validation fails on a Wi-Fi network after a roam to a new BSSID,
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
new file mode 100644
index 0000000..4027038
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -0,0 +1,74 @@
+<?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.
+-->
+
+<!-- These resources are around just to allow their values to be customized
+     for different hardware and product builds for Thread Network. All
+	 configuration names should use the "config_thread" prefix.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Sets to {@code true} to enable Thread on the device by default. Note this is the default
+    value, the actual Thread enabled state can be changed by the {@link
+    ThreadNetworkController#setEnabled} API.
+    -->
+    <bool name="config_thread_default_enabled">true</bool>
+
+    <!-- Whether to use location APIs in the algorithm to determine country code or not.
+    If disabled, will use other sources (telephony, wifi, etc) to determine device location for
+    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 4c85e8c..fbaae05 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -31,6 +31,7 @@
             <item type="integer" name="config_networkWakeupPacketMask"/>
             <item type="integer" name="config_networkNotifySwitchType"/>
             <item type="array" name="config_networkNotifySwitches"/>
+            <item type="array" name="config_nsdOffloadServicesPriority"/>
             <item type="bool" name="config_ongoingSignInNotification"/>
             <item type="bool" name="config_autoCancelNetworkNotifications"/>
             <item type="bool" name="config_notifyNoInternetAsDialogWhenHighPriority"/>
@@ -43,6 +44,14 @@
             <item type="string" name="config_ethernet_iface_regex"/>
             <item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" />
             <item type="integer" name="config_netstats_validate_import" />
+
+            <!-- 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/ServiceConnectivityResources/res/values/strings.xml b/service/ServiceConnectivityResources/res/values/strings.xml
index b2fa5f5..246155e 100644
--- a/service/ServiceConnectivityResources/res/values/strings.xml
+++ b/service/ServiceConnectivityResources/res/values/strings.xml
@@ -29,6 +29,15 @@
     <!-- A notification is shown when a captive portal network is detected.  This is the notification's message. -->
     <string name="network_available_sign_in_detailed"><xliff:g id="network_ssid">%1$s</xliff:g></string>
 
+    <!-- A notification is shown when the system detected no internet access on a mobile network, possibly because the user is out of data, and a webpage is available to get Internet access (possibly by topping up or getting a subscription).  This is the notification's title. -->
+    <string name="mobile_network_available_no_internet">No internet</string>
+
+    <!-- A notification is shown when the system detected no internet access on a mobile network, possibly because the user is out of data, and a webpage is available to get Internet access (possibly by topping up or getting a subscription).  This is the notification's message. -->
+    <string name="mobile_network_available_no_internet_detailed">You may be out of data from <xliff:g id="network_carrier" example="Android Mobile">%1$s</xliff:g>. Tap for options.</string>
+
+    <!-- A notification is shown when the system detected no internet access on a mobile network, possibly because the user is out of data, and a webpage is available to get Internet access (possibly by topping up or getting a subscription).  This is the notification's message when the carrier is unknown. -->
+    <string name="mobile_network_available_no_internet_detailed_unknown_carrier">You may be out of data. Tap for options.</string>
+
     <!-- A notification is shown when the user connects to a Wi-Fi network and the system detects that that network has no Internet access. This is the notification's title. -->
     <string name="wifi_no_internet"><xliff:g id="network_ssid" example="GoogleGuest">%1$s</xliff:g> has no internet access</string>
 
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
deleted file mode 100644
index 77cffda..0000000
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ /dev/null
@@ -1,243 +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.
- */
-
-#define LOG_TAG "TrafficControllerJni"
-
-#include "TrafficController.h"
-
-#include "netd.h"
-
-#include <jni.h>
-#include <log/log.h>
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedUtfChars.h>
-#include <nativehelper/ScopedPrimitiveArray.h>
-#include <netjniutils/netjniutils.h>
-#include <net/if.h>
-#include <private/android_filesystem_config.h>
-#include <unistd.h>
-#include <vector>
-
-
-using android::net::TrafficController;
-using android::netdutils::Status;
-
-using UidOwnerMatchType::PENALTY_BOX_MATCH;
-using UidOwnerMatchType::HAPPY_BOX_MATCH;
-
-static android::net::TrafficController mTc;
-
-namespace android {
-
-#define CHECK_LOG(status) \
-  do { \
-    if (!isOk(status)) \
-      ALOGE("%s failed, error code = %d", __func__, status.code()); \
-  } while (0)
-
-static void native_init(JNIEnv* env, jclass clazz, jboolean startSkDestroyListener) {
-  Status status = mTc.start(startSkDestroyListener);
-  CHECK_LOG(status);
-  if (!isOk(status)) {
-    uid_t uid = getuid();
-    ALOGE("BpfNetMaps jni init failure as uid=%d", uid);
-    // TODO: Fix tests to not use this jni lib, so we can unconditionally abort()
-    if (uid == AID_SYSTEM || uid == AID_NETWORK_STACK) abort();
-  }
-}
-
-static jint native_addNaughtyApp(JNIEnv* env, jobject self, jint uid) {
-  const uint32_t appUids = static_cast<uint32_t>(abs(uid));
-  Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
-      TrafficController::IptOp::IptOpInsert);
-  CHECK_LOG(status);
-  return (jint)status.code();
-}
-
-static jint native_removeNaughtyApp(JNIEnv* env, jobject self, jint uid) {
-  const uint32_t appUids = static_cast<uint32_t>(abs(uid));
-  Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
-      TrafficController::IptOp::IptOpDelete);
-  CHECK_LOG(status);
-  return (jint)status.code();
-}
-
-static jint native_addNiceApp(JNIEnv* env, jobject self, jint uid) {
-  const uint32_t appUids = static_cast<uint32_t>(abs(uid));
-  Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
-      TrafficController::IptOp::IptOpInsert);
-  CHECK_LOG(status);
-  return (jint)status.code();
-}
-
-static jint native_removeNiceApp(JNIEnv* env, jobject self, jint uid) {
-  const uint32_t appUids = static_cast<uint32_t>(abs(uid));
-  Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
-      TrafficController::IptOp::IptOpDelete);
-  CHECK_LOG(status);
-  return (jint)status.code();
-}
-
-static jint native_setChildChain(JNIEnv* env, jobject self, jint childChain, jboolean enable) {
-  auto chain = static_cast<ChildChain>(childChain);
-  int res = mTc.toggleUidOwnerMap(chain, enable);
-  if (res) ALOGE("%s failed, error code = %d", __func__, res);
-  return (jint)res;
-}
-
-static jint native_replaceUidChain(JNIEnv* env, jobject self, jstring name, jboolean isAllowlist,
-                                   jintArray jUids) {
-    const ScopedUtfChars chainNameUtf8(env, name);
-    if (chainNameUtf8.c_str() == nullptr) return -EINVAL;
-    const std::string chainName(chainNameUtf8.c_str());
-
-    ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) return -EINVAL;
-
-    size_t size = uids.size();
-    static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
-    std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
-    int res = mTc.replaceUidOwnerMap(chainName, isAllowlist, data);
-    if (res) ALOGE("%s failed, error code = %d", __func__, res);
-    return (jint)res;
-}
-
-static jint native_setUidRule(JNIEnv* env, jobject self, jint childChain, jint uid,
-                              jint firewallRule) {
-    auto chain = static_cast<ChildChain>(childChain);
-    auto rule = static_cast<FirewallRule>(firewallRule);
-    FirewallType fType = mTc.getFirewallType(chain);
-
-    int res = mTc.changeUidOwnerRule(chain, uid, rule, fType);
-    if (res) ALOGE("%s failed, error code = %d", __func__, res);
-    return (jint)res;
-}
-
-static jint native_addUidInterfaceRules(JNIEnv* env, jobject self, jstring ifName,
-                                        jintArray jUids) {
-    // Null ifName is a wildcard to allow apps to receive packets on all interfaces and ifIndex is
-    // set to 0.
-    int ifIndex = 0;
-    if (ifName != nullptr) {
-        const ScopedUtfChars ifNameUtf8(env, ifName);
-        const std::string interfaceName(ifNameUtf8.c_str());
-        ifIndex = if_nametoindex(interfaceName.c_str());
-    }
-
-    ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) return -EINVAL;
-
-    size_t size = uids.size();
-    static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
-    std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
-    Status status = mTc.addUidInterfaceRules(ifIndex, data);
-    CHECK_LOG(status);
-    return (jint)status.code();
-}
-
-static jint native_removeUidInterfaceRules(JNIEnv* env, jobject self, jintArray jUids) {
-    ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) return -EINVAL;
-
-    size_t size = uids.size();
-    static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
-    std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
-    Status status = mTc.removeUidInterfaceRules(data);
-    CHECK_LOG(status);
-    return (jint)status.code();
-}
-
-static jint native_updateUidLockdownRule(JNIEnv* env, jobject self, jint uid, jboolean add) {
-    Status status = mTc.updateUidLockdownRule(uid, add);
-    CHECK_LOG(status);
-    return (jint)status.code();
-}
-
-static jint native_swapActiveStatsMap(JNIEnv* env, jobject self) {
-    Status status = mTc.swapActiveStatsMap();
-    CHECK_LOG(status);
-    return (jint)status.code();
-}
-
-static void native_setPermissionForUids(JNIEnv* env, jobject self, jint permission,
-                                        jintArray jUids) {
-    ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) return;
-
-    size_t size = uids.size();
-    static_assert(sizeof(*(uids.get())) == sizeof(uid_t));
-    std::vector<uid_t> data ((uid_t *)&uids[0], (uid_t*)&uids[size]);
-    mTc.setPermissionForUids(permission, data);
-}
-
-static void native_dump(JNIEnv* env, jobject self, jobject javaFd, jboolean verbose) {
-    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
-    if (fd < 0) {
-        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
-        return;
-    }
-    mTc.dump(fd, verbose);
-}
-
-static jint native_synchronizeKernelRCU(JNIEnv* env, jobject self) {
-    return -bpf::synchronizeKernelRCU();
-}
-
-/*
- * JNI registration.
- */
-// clang-format off
-static const JNINativeMethod gMethods[] = {
-    /* name, signature, funcPtr */
-    {"native_init", "(Z)V",
-    (void*)native_init},
-    {"native_addNaughtyApp", "(I)I",
-    (void*)native_addNaughtyApp},
-    {"native_removeNaughtyApp", "(I)I",
-    (void*)native_removeNaughtyApp},
-    {"native_addNiceApp", "(I)I",
-    (void*)native_addNiceApp},
-    {"native_removeNiceApp", "(I)I",
-    (void*)native_removeNiceApp},
-    {"native_setChildChain", "(IZ)I",
-    (void*)native_setChildChain},
-    {"native_replaceUidChain", "(Ljava/lang/String;Z[I)I",
-    (void*)native_replaceUidChain},
-    {"native_setUidRule", "(III)I",
-    (void*)native_setUidRule},
-    {"native_addUidInterfaceRules", "(Ljava/lang/String;[I)I",
-    (void*)native_addUidInterfaceRules},
-    {"native_removeUidInterfaceRules", "([I)I",
-    (void*)native_removeUidInterfaceRules},
-    {"native_updateUidLockdownRule", "(IZ)I",
-    (void*)native_updateUidLockdownRule},
-    {"native_swapActiveStatsMap", "()I",
-    (void*)native_swapActiveStatsMap},
-    {"native_setPermissionForUids", "(I[I)V",
-    (void*)native_setPermissionForUids},
-    {"native_dump", "(Ljava/io/FileDescriptor;Z)V",
-    (void*)native_dump},
-    {"native_synchronizeKernelRCU", "()I",
-    (void*)native_synchronizeKernelRCU},
-};
-// clang-format on
-
-int register_com_android_server_BpfNetMaps(JNIEnv* env) {
-    return jniRegisterNativeMethods(env, "android/net/connectivity/com/android/server/BpfNetMaps",
-                                    gMethods, NELEM(gMethods));
-}
-
-}; // namespace android
diff --git a/service/jni/com_android_server_ServiceManagerWrapper.cpp b/service/jni/com_android_server_ServiceManagerWrapper.cpp
new file mode 100644
index 0000000..0e32726
--- /dev/null
+++ b/service/jni/com_android_server_ServiceManagerWrapper.cpp
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+#include <android/binder_ibinder_jni.h>
+#include <android/binder_manager.h>
+#include <jni.h>
+#include "nativehelper/JNIHelp.h"
+#include <nativehelper/ScopedUtfChars.h>
+#include <private/android_filesystem_config.h>
+
+namespace android {
+static jobject com_android_server_ServiceManagerWrapper_waitForService(
+        JNIEnv* env, jobject clazz, jstring serviceName) {
+    ScopedUtfChars name(env, serviceName);
+
+// AServiceManager_waitForService is available on only 31+, but it's still safe for Thread
+// service because it's enabled on only 34+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunguarded-availability"
+    return AIBinder_toJavaBinder(env, AServiceManager_waitForService(name.c_str()));
+#pragma clang diagnostic pop
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"nativeWaitForService",
+         "(Ljava/lang/String;)Landroid/os/IBinder;",
+         (void*)com_android_server_ServiceManagerWrapper_waitForService},
+};
+
+int register_com_android_server_ServiceManagerWrapper(JNIEnv* env) {
+    return jniRegisterNativeMethods(env,
+            "android/net/connectivity/com/android/server/ServiceManagerWrapper",
+            gMethods, NELEM(gMethods));
+}
+
+};  // namespace android
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 059b716..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);
@@ -90,11 +91,6 @@
 
 #undef ALOGF
 
-bool isGsiImage() {
-    // this implementation matches 2 other places in the codebase (same function name too)
-    return !access("/system/system_ext/etc/init/init.gsi.rc", F_OK);
-}
-
 static const char* kClatdDir = "/apex/com.android.tethering/bin/for-system";
 static const char* kClatdBin = "/apex/com.android.tethering/bin/for-system/clatd";
 
@@ -118,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
@@ -135,14 +132,6 @@
 
 #undef V2
 
-    // HACK: Some old vendor kernels lack ~5.10 backport of 'bpffs selinux genfscon' support.
-    // This is *NOT* supported, but let's allow, at least for now, U+ GSI to boot on them.
-    // (without this hack pixel5 R vendor + U gsi breaks)
-    if (isGsiImage() && !bpf::isAtLeastKernelVersion(5, 10, 0)) {
-        ALOGE("GSI with *BAD* pre-5.10 kernel lacking bpffs selinux genfscon support.");
-        return;
-    }
-
     if (fatal) abort();
 }
 
@@ -485,11 +474,15 @@
 static constexpr int WAITPID_ATTEMPTS = 50;
 static constexpr int WAITPID_RETRY_INTERVAL_US = 100000;
 
-static void stopClatdProcess(int pid) {
-    int err = kill(pid, SIGTERM);
-    if (err) {
-        err = errno;
+static void com_android_server_connectivity_ClatCoordinator_stopClatd(JNIEnv* env, jclass clazz,
+                                                                      jint pid) {
+    if (pid <= 0) {
+        jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", "Invalid pid");
+        return;
     }
+
+    int err = kill(pid, SIGTERM);
+    if (err) err = errno;
     if (err == ESRCH) {
         ALOGE("clatd child process %d unexpectedly disappeared", pid);
         return;
@@ -518,23 +511,6 @@
     }
 }
 
-static void com_android_server_connectivity_ClatCoordinator_stopClatd(JNIEnv* env, jclass clazz,
-                                                                      jstring iface, jstring pfx96,
-                                                                      jstring v4, jstring v6,
-                                                                      jint pid) {
-    ScopedUtfChars ifaceStr(env, iface);
-    ScopedUtfChars pfx96Str(env, pfx96);
-    ScopedUtfChars v4Str(env, v4);
-    ScopedUtfChars v6Str(env, v6);
-
-    if (pid <= 0) {
-        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid pid");
-        return;
-    }
-
-    stopClatdProcess(pid);
-}
-
 static jlong com_android_server_connectivity_ClatCoordinator_getSocketCookie(
         JNIEnv* env, jclass clazz, jobject sockJavaFd) {
     int sockFd = netjniutils::GetNativeFileDescriptor(env, sockJavaFd);
@@ -579,8 +555,7 @@
          "(Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/lang/"
          "String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
          (void*)com_android_server_connectivity_ClatCoordinator_startClatd},
-        {"native_stopClatd",
-         "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V",
+        {"native_stopClatd", "(I)V",
          (void*)com_android_server_connectivity_ClatCoordinator_stopClatd},
         {"native_getSocketCookie", "(Ljava/io/FileDescriptor;)J",
          (void*)com_android_server_connectivity_ClatCoordinator_getSocketCookie},
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index 3d15d43..bb70d4f 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -23,9 +23,9 @@
 
 int register_com_android_server_TestNetworkService(JNIEnv* env);
 int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env);
-int register_com_android_server_BpfNetMaps(JNIEnv* env);
 int register_android_server_net_NetworkStatsFactory(JNIEnv* env);
 int register_android_server_net_NetworkStatsService(JNIEnv* env);
+int register_com_android_server_ServiceManagerWrapper(JNIEnv* env);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
     JNIEnv *env;
@@ -38,15 +38,15 @@
         return JNI_ERR;
     }
 
-    if (register_com_android_server_connectivity_ClatCoordinator(env) < 0) {
-        return JNI_ERR;
-    }
-
-    if (register_com_android_server_BpfNetMaps(env) < 0) {
+    if (register_com_android_server_ServiceManagerWrapper(env) < 0) {
         return JNI_ERR;
     }
 
     if (android::modules::sdklevel::IsAtLeastT()) {
+        if (register_com_android_server_connectivity_ClatCoordinator(env) < 0) {
+            return JNI_ERR;
+        }
+
         if (register_android_server_net_NetworkStatsFactory(env) < 0) {
             return JNI_ERR;
         }
diff --git a/service/libconnectivity/Android.bp b/service/libconnectivity/Android.bp
index e063af7..3a72134 100644
--- a/service/libconnectivity/Android.bp
+++ b/service/libconnectivity/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
diff --git a/service/libconnectivity/include/connectivity_native.h b/service/libconnectivity/include/connectivity_native.h
index 5a2509a..f4676a9 100644
--- a/service/libconnectivity/include/connectivity_native.h
+++ b/service/libconnectivity/include/connectivity_native.h
@@ -78,7 +78,7 @@
  * @param count Pointer to the size of the ports array; the value will be set to the total number of
  *              blocked ports, which may be larger than the ports array that was filled.
  */
-int AConnectivityNative_getPortsBlockedForBind(in_port_t *ports, size_t *count)
+int AConnectivityNative_getPortsBlockedForBind(in_port_t* _Nonnull ports, size_t* _Nonnull count)
     __INTRODUCED_IN(__ANDROID_API_U__);
 
 __END_DECLS
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
index 5149e6d..b09589c 100644
--- a/service/lint-baseline.xml
+++ b/service/lint-baseline.xml
@@ -1,5 +1,71 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `BpfBitmap`"
+        errorLine1="                return new BpfBitmap(BLOCKED_PORTS_MAP_PATH);"
+        errorLine2="                       ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+            line="61"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `set`"
+        errorLine1="            mBpfBlockedPortsMap.set(port);"
+        errorLine2="                                ~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+            line="96"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `unset`"
+        errorLine1="            mBpfBlockedPortsMap.unset(port);"
+        errorLine2="                                ~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+            line="107"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `clear`"
+        errorLine1="            mBpfBlockedPortsMap.clear();"
+        errorLine2="                                ~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+            line="118"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `get`"
+        errorLine1="                if (mBpfBlockedPortsMap.get(i)) portMap.add(i);"
+        errorLine2="                                        ~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+            line="131"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportNetworkInterfaceForTransports`"
+        errorLine1="            batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);"
+        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1447"
+            column="26"/>
+    </issue>
 
     <issue
         id="NewApi"
@@ -8,23 +74,562 @@
         errorLine2="                     ~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="1358"
+            line="1458"
             column="22"/>
     </issue>
 
     <issue
         id="NewApi"
+        message="Call requires API level 33 (current min is 30): `getProgramId`"
+        errorLine1="            return BpfUtils.getProgramId(attachType);"
+        errorLine2="                            ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1572"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+        errorLine1="        mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);"
+        errorLine2="                                                   ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1740"
+            column="52"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#registerNetworkPolicyCallback`"
+        errorLine1="        mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1753"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Cast to `UidFrozenStateChangedCallback` requires API level 34 (current min is 30)"
+        errorLine1="                    new UidFrozenStateChangedCallback() {"
+        errorLine2="                    ^">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1888"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 34 (current min is 30): `android.app.ActivityManager.UidFrozenStateChangedCallback`"
+        errorLine1="                    new UidFrozenStateChangedCallback() {"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1888"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 34 (current min is 30): `android.app.ActivityManager#registerUidFrozenStateChangedCallback`"
+        errorLine1="            activityManager.registerUidFrozenStateChangedCallback("
+        errorLine2="                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="1907"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidNetworkingBlocked`"
+        errorLine1="            return mPolicyManager.isUidNetworkingBlocked(uid, metered);"
+        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2162"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getRestrictBackgroundStatus`"
+        errorLine1="            return mPolicyManager.getRestrictBackgroundStatus(callerUid);"
+        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2947"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+        errorLine1="            final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());"
+        errorLine2="                                                                                ~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2963"
+            column="81"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getLinkProperties`"
+        errorLine1="                        snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+        errorLine2="                                 ~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2966"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetworkCapabilities`"
+        errorLine1="                        snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+        errorLine2="                                                               ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2966"
+            column="64"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+        errorLine1="                        snapshot.getNetwork(), snapshot.getSubscriberId()));"
+        errorLine2="                                 ~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2967"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getSubscriberId`"
+        errorLine1="                        snapshot.getNetwork(), snapshot.getSubscriberId()));"
+        errorLine2="                                                        ~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2967"
+            column="57"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager.NetworkPolicyCallback`"
+        errorLine1="    private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {"
+        errorLine2="                                                              ~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="3210"
+            column="63"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `dump`"
+        errorLine1="            mBpfNetMaps.dump(pw, fd, verbose);"
+        errorLine2="                        ~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="4155"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+        errorLine1="        if (!Build.isDebuggable()) {"
+        errorLine2="                   ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="5721"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+        errorLine1="                 mContext.getSystemService(NetworkPolicyManager.class);"
+        errorLine2="                                           ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="6174"
+            column="44"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getMultipathPreference`"
+        errorLine1="            networkPreference = netPolicyManager.getMultipathPreference(network);"
+        errorLine2="                                                 ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="6179"
+            column="50"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `new android.net.UnderlyingNetworkInfo`"
+        errorLine1="        return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),"
+        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="6819"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidRestrictedOnMeteredNetworks`"
+        errorLine1="            if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="7822"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+        errorLine1="            if (Build.isDebuggable()) {"
+        errorLine2="                      ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="9943"
+            column="23"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 31 (current min is 30): `android.app.usage.NetworkStatsManager#notifyNetworkStatus`"
         errorLine1="            mStatsManager.notifyNetworkStatus(getDefaultNetworks(),"
         errorLine2="                          ~~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="9938"
+            line="10909"
             column="27"/>
     </issue>
 
     <issue
         id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="                IoUtils.closeQuietly(pfd);"
+        errorLine2="                        ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="10962"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="                IoUtils.closeQuietly(pfd);"
+        errorLine2="                        ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="10979"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager`"
+        errorLine1="        NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);"
+        errorLine2="                                                                ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="11035"
+            column="65"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager#getWatchlistConfigHash`"
+        errorLine1="        return nwm.getWatchlistConfigHash();"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="11041"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `getProgramId`"
+        errorLine1="                        final int ret = BpfUtils.getProgramId(type);"
+        errorLine2="                                                 ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="11180"
+            column="50"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportMobileRadioPowerState`"
+        errorLine1="                    bs.reportMobileRadioPowerState(isActive, uid);"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="12254"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportWifiRadioPowerState`"
+        errorLine1="                    bs.reportWifiRadioPowerState(isActive, uid);"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="12257"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `addNiceApp`"
+        errorLine1="                mBpfNetMaps.addNiceApp(uid);"
+        errorLine2="                            ~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13079"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `removeNiceApp`"
+        errorLine1="                mBpfNetMaps.removeNiceApp(uid);"
+        errorLine2="                            ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13081"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `addNaughtyApp`"
+        errorLine1="                mBpfNetMaps.addNaughtyApp(uid);"
+        errorLine2="                            ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13094"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `removeNaughtyApp`"
+        errorLine1="                mBpfNetMaps.removeNaughtyApp(uid);"
+        errorLine2="                            ~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13096"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+        errorLine1="            final int uid = uh.getUid(appId);"
+        errorLine2="                               ~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13112"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `setUidRule`"
+        errorLine1="            mBpfNetMaps.setUidRule(chain, uid, firewallRule);"
+        errorLine2="                        ~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13130"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `setChildChain`"
+        errorLine1="            mBpfNetMaps.setChildChain(chain, enable);"
+        errorLine2="                        ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13195"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `isChainEnabled`"
+        errorLine1="        return mBpfNetMaps.isChainEnabled(chain);"
+        errorLine2="                           ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13213"
+            column="28"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 33 (current min is 30): `replaceUidChain`"
+        errorLine1="        mBpfNetMaps.replaceUidChain(chain, uids);"
+        errorLine2="                    ~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="13220"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `BpfMap`"
+        errorLine1="        mBpfDscpIpv4Policies = new BpfMap&lt;Struct.S32, DscpPolicyValue&gt;(IPV4_POLICY_MAP_PATH,"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="88"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `BpfMap`"
+        errorLine1="        mBpfDscpIpv6Policies = new BpfMap&lt;Struct.S32, DscpPolicyValue&gt;(IPV6_POLICY_MAP_PATH,"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="90"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `insertOrReplaceEntry`"
+        errorLine1="                mBpfDscpIpv4Policies.insertOrReplaceEntry("
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="183"
+            column="38"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `insertOrReplaceEntry`"
+        errorLine1="                mBpfDscpIpv6Policies.insertOrReplaceEntry("
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="194"
+            column="38"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `replaceEntry`"
+        errorLine1="            mBpfDscpIpv4Policies.replaceEntry(new Struct.S32(index), DscpPolicyValue.NONE);"
+        errorLine2="                                 ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="261"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `replaceEntry`"
+        errorLine1="            mBpfDscpIpv6Policies.replaceEntry(new Struct.S32(index), DscpPolicyValue.NONE);"
+        errorLine2="                                 ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+            line="262"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+        errorLine1='            InetAddress.parseNumericAddress("::").getAddress();'
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyValue.java"
+            line="99"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `new android.net.NetworkStateSnapshot`"
+        errorLine1="            return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java"
+            line="1353"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+        errorLine1="            IoUtils.closeQuietly(mFileDescriptor);"
+        errorLine2="                    ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java"
+            line="570"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 31 (current min is 30): `android.os.Build.VERSION#DEVICE_INITIAL_SDK_INT`"
+        errorLine1="            return Build.VERSION.DEVICE_INITIAL_SDK_INT;"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+            line="212"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+        errorLine1="        for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {"
+        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+            line="396"
+            column="51"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+        errorLine1="        for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {"
+        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+            line="404"
+            column="51"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 31 (current min is 30): `android.content.pm.ApplicationInfo#isOem`"
         errorLine1="        return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();"
         errorLine2="                                             ~~~~~">
@@ -58,441 +663,34 @@
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getMultipathPreference`"
-        errorLine1="            networkPreference = netPolicyManager.getMultipathPreference(network);"
-        errorLine2="                                                 ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="5498"
-            column="50"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getRestrictBackgroundStatus`"
-        errorLine1="            return mPolicyManager.getRestrictBackgroundStatus(callerUid);"
-        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2565"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidNetworkingBlocked`"
-        errorLine1="            return mPolicyManager.isUidNetworkingBlocked(uid, metered);"
-        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="1914"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidRestrictedOnMeteredNetworks`"
-        errorLine1="            if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {"
-        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="7094"
-            column="32"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#registerNetworkPolicyCallback`"
-        errorLine1="        mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="1567"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getLinkProperties`"
-        errorLine1="                        snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
-        errorLine2="                                 ~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2584"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetworkCapabilities`"
-        errorLine1="                        snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
-        errorLine2="                                                               ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2584"
-            column="64"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
-        errorLine1="                        snapshot.getNetwork(), snapshot.getSubscriberId()));"
-        errorLine2="                                 ~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2585"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
-        errorLine1="            final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());"
-        errorLine2="                                                                                ~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2581"
-            column="81"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getSubscriberId`"
-        errorLine1="                        snapshot.getNetwork(), snapshot.getSubscriberId()));"
-        errorLine2="                                                        ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2585"
-            column="57"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager#getWatchlistConfigHash`"
-        errorLine1="        return nwm.getWatchlistConfigHash();"
-        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="10060"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#addPacProxyInstalledListener`"
-        errorLine1="        mPacProxyManager.addPacProxyInstalledListener("
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
-            line="111"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
-        errorLine1="                        () -&gt; mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));"
-        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
-            line="208"
-            column="48"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
-        errorLine1="        mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
-            line="252"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportMobileRadioPowerState`"
-        errorLine1="                    bs.reportMobileRadioPowerState(isActive, NO_UID);"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="11006"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportNetworkInterfaceForTransports`"
-        errorLine1="            batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="1347"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportWifiRadioPowerState`"
-        errorLine1="                    bs.reportWifiRadioPowerState(isActive, NO_UID);"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="11009"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
-        errorLine1="            if (Build.isDebuggable()) {"
-        errorLine2="                      ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="9074"
-            column="23"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
-        errorLine1="        if (!Build.isDebuggable()) {"
-        errorLine2="                   ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="5039"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
-        errorLine1="        for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {"
-        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
-            line="396"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
-        errorLine1="        for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {"
-        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
-            line="404"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
         message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
         errorLine1="                    final int uid = handle.getUid(appId);"
         errorLine2="                                           ~~~~~~">
         <location
             file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
-            line="1069"
+            line="1070"
             column="44"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
-        errorLine1="                tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);"
-        errorLine2="                                    ~~~~~~~~~~~~~">
+        message="Call requires API level 33 (current min is 30): `updateUidLockdownRule`"
+        errorLine1="            mBpfNetMaps.updateUidLockdownRule(uid, add);"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~">
         <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="285"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
-        errorLine1="                tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);"
-        errorLine2="                                    ~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="287"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
-        errorLine1="            tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
-        errorLine2="                                ~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="265"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
-        errorLine1="            tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
-        errorLine2="                                ~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="262"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
-        errorLine1="        final int result = Os.ioctlInt(fd, SIOCINQ);"
-        errorLine2="                              ~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="392"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
-        errorLine1="        final int result = Os.ioctlInt(fd, SIOCOUTQ);"
-        errorLine2="                              ~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
-            line="402"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
-        errorLine1='            InetAddress.parseNumericAddress("::").getAddress();'
-        errorLine2="                        ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyValue.java"
-            line="99"
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+            line="1123"
             column="25"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
-        errorLine1='    private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");'
-        errorLine2="                                                                ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ClatCoordinator.java"
-            line="89"
-            column="65"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="                IoUtils.closeQuietly(pfd);"
-        errorLine2="                        ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="9991"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="                IoUtils.closeQuietly(pfd);"
-        errorLine2="                        ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="10008"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
-        errorLine1="            IoUtils.closeQuietly(mFileDescriptor);"
-        errorLine2="                    ~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java"
-            line="481"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `new android.net.NetworkStateSnapshot`"
-        errorLine1="            return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),"
-        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java"
-            line="1269"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `new android.net.UnderlyingNetworkInfo`"
-        errorLine1="        return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),"
-        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="6123"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager.NetworkPolicyCallback`"
-        errorLine1="    private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {"
-        errorLine2="                                                              ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="2827"
-            column="63"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
-        errorLine1="                 mContext.getSystemService(NetworkPolicyManager.class);"
-        errorLine2="                                           ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="5493"
-            column="44"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
-        errorLine1="        mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);"
-        errorLine2="                                                   ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="1554"
-            column="52"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager`"
-        errorLine1="        NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);"
-        errorLine2="                                                                ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
-            line="10054"
-            column="65"/>
-    </issue>
-
-    <issue
-        id="NewApi"
         message="Class requires API level 31 (current min is 30): `android.net.PacProxyManager.PacProxyInstalledListener`"
         errorLine1="    private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener {"
         errorLine2="                                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
-            line="90"
+            line="92"
             column="56"/>
     </issue>
 
@@ -503,8 +701,107 @@
         errorLine2="                                                    ~~~~~~~~~~~~~~~">
         <location
             file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
-            line="108"
+            line="111"
             column="53"/>
     </issue>
 
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#addPacProxyInstalledListener`"
+        errorLine1="            mPacProxyManager.addPacProxyInstalledListener("
+        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+            line="115"
+            column="30"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+        errorLine1="                        () -&gt; mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));"
+        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+            line="213"
+            column="48"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+        errorLine1="            mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);"
+        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+            line="259"
+            column="30"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+        errorLine1="            tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+        errorLine2="                                ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="269"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+        errorLine1="            tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+        errorLine2="                                ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="272"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+        errorLine1="                tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);"
+        errorLine2="                                    ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="292"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+        errorLine1="                tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);"
+        errorLine2="                                    ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="294"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+        errorLine1="        final int result = Os.ioctlInt(fd, SIOCINQ);"
+        errorLine2="                              ~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="401"
+            column="31"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+        errorLine1="        final int result = Os.ioctlInt(fd, SIOCOUTQ);"
+        errorLine2="                              ~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+            line="411"
+            column="31"/>
+    </issue>
+
 </issues>
\ No newline at end of file
diff --git a/service/native/Android.bp b/service/native/Android.bp
deleted file mode 100644
index 697fcbd..0000000
--- a/service/native/Android.bp
+++ /dev/null
@@ -1,84 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-cc_library {
-    name: "libtraffic_controller",
-    defaults: ["netd_defaults"],
-    srcs: [
-        "TrafficController.cpp",
-    ],
-    header_libs: [
-        "bpf_connectivity_headers",
-    ],
-    static_libs: [
-        // TrafficController would use the constants of INetd so that add
-        // netd_aidl_interface-lateststable-ndk.
-        "netd_aidl_interface-lateststable-ndk",
-    ],
-    shared_libs: [
-        // TODO: Find a good way to remove libbase.
-        "libbase",
-        "libcutils",
-        "libnetdutils",
-        "libutils",
-        "liblog",
-    ],
-    export_include_dirs: ["include"],
-    sanitize: {
-        cfi: true,
-    },
-    apex_available: [
-        "com.android.tethering",
-    ],
-    min_sdk_version: "30",
-}
-
-cc_test {
-    name: "traffic_controller_unit_test",
-    test_suites: ["general-tests", "mts-tethering"],
-    test_config_template: ":net_native_test_config_template",
-    require_root: true,
-    local_include_dirs: ["include"],
-    header_libs: [
-        "bpf_connectivity_headers",
-    ],
-    srcs: [
-        "TrafficControllerTest.cpp",
-    ],
-    static_libs: [
-        "libbase",
-        "libgmock",
-        "liblog",
-        "libnetdutils",
-        "libtraffic_controller",
-        "libutils",
-        "libnetd_updatable",
-        "netd_aidl_interface-lateststable-ndk",
-    ],
-    compile_multilib: "both",
-    multilib: {
-        lib32: {
-            suffix: "32",
-        },
-        lib64: {
-            suffix: "64",
-        },
-    },
-}
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
deleted file mode 100644
index 8f6df21..0000000
--- a/service/native/TrafficController.cpp
+++ /dev/null
@@ -1,629 +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.
- */
-
-#define LOG_TAG "TrafficController"
-#include <inttypes.h>
-#include <linux/if_ether.h>
-#include <linux/in.h>
-#include <linux/inet_diag.h>
-#include <linux/netlink.h>
-#include <linux/sock_diag.h>
-#include <linux/unistd.h>
-#include <net/if.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <sys/utsname.h>
-#include <sys/wait.h>
-#include <map>
-#include <mutex>
-#include <unordered_set>
-#include <vector>
-
-#include <android-base/stringprintf.h>
-#include <android-base/strings.h>
-#include <android-base/unique_fd.h>
-#include <netdutils/StatusOr.h>
-#include <netdutils/Syscalls.h>
-#include <netdutils/UidConstants.h>
-#include <netdutils/Utils.h>
-#include <private/android_filesystem_config.h>
-
-#include "TrafficController.h"
-#include "bpf/BpfMap.h"
-#include "netdutils/DumpWriter.h"
-
-namespace android {
-namespace net {
-
-using base::StringPrintf;
-using base::unique_fd;
-using bpf::BpfMap;
-using bpf::synchronizeKernelRCU;
-using netdutils::DumpWriter;
-using netdutils::NetlinkListener;
-using netdutils::NetlinkListenerInterface;
-using netdutils::ScopedIndent;
-using netdutils::Slice;
-using netdutils::sSyscalls;
-using netdutils::Status;
-using netdutils::statusFromErrno;
-using netdutils::StatusOr;
-
-constexpr int kSockDiagMsgType = SOCK_DIAG_BY_FAMILY;
-constexpr int kSockDiagDoneMsgType = NLMSG_DONE;
-
-const char* TrafficController::LOCAL_DOZABLE = "fw_dozable";
-const char* TrafficController::LOCAL_STANDBY = "fw_standby";
-const char* TrafficController::LOCAL_POWERSAVE = "fw_powersave";
-const char* TrafficController::LOCAL_RESTRICTED = "fw_restricted";
-const char* TrafficController::LOCAL_LOW_POWER_STANDBY = "fw_low_power_standby";
-const char* TrafficController::LOCAL_OEM_DENY_1 = "fw_oem_deny_1";
-const char* TrafficController::LOCAL_OEM_DENY_2 = "fw_oem_deny_2";
-const char* TrafficController::LOCAL_OEM_DENY_3 = "fw_oem_deny_3";
-
-static_assert(BPF_PERMISSION_INTERNET == INetd::PERMISSION_INTERNET,
-              "Mismatch between BPF and AIDL permissions: PERMISSION_INTERNET");
-static_assert(BPF_PERMISSION_UPDATE_DEVICE_STATS == INetd::PERMISSION_UPDATE_DEVICE_STATS,
-              "Mismatch between BPF and AIDL permissions: PERMISSION_UPDATE_DEVICE_STATS");
-
-#define FLAG_MSG_TRANS(result, flag, value) \
-    do {                                    \
-        if ((value) & (flag)) {             \
-            (result).append(" " #flag);     \
-            (value) &= ~(flag);             \
-        }                                   \
-    } while (0)
-
-const std::string uidMatchTypeToString(uint32_t match) {
-    std::string matchType;
-    FLAG_MSG_TRANS(matchType, HAPPY_BOX_MATCH, match);
-    FLAG_MSG_TRANS(matchType, PENALTY_BOX_MATCH, match);
-    FLAG_MSG_TRANS(matchType, DOZABLE_MATCH, match);
-    FLAG_MSG_TRANS(matchType, STANDBY_MATCH, match);
-    FLAG_MSG_TRANS(matchType, POWERSAVE_MATCH, match);
-    FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
-    FLAG_MSG_TRANS(matchType, LOW_POWER_STANDBY_MATCH, match);
-    FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
-    FLAG_MSG_TRANS(matchType, LOCKDOWN_VPN_MATCH, match);
-    FLAG_MSG_TRANS(matchType, OEM_DENY_1_MATCH, match);
-    FLAG_MSG_TRANS(matchType, OEM_DENY_2_MATCH, match);
-    FLAG_MSG_TRANS(matchType, OEM_DENY_3_MATCH, match);
-    if (match) {
-        return StringPrintf("Unknown match: %u", match);
-    }
-    return matchType;
-}
-
-const std::string UidPermissionTypeToString(int permission) {
-    if (permission == INetd::PERMISSION_NONE) {
-        return "PERMISSION_NONE";
-    }
-    if (permission == INetd::PERMISSION_UNINSTALLED) {
-        // This should never appear in the map, complain loudly if it does.
-        return "PERMISSION_UNINSTALLED error!";
-    }
-    std::string permissionType;
-    FLAG_MSG_TRANS(permissionType, BPF_PERMISSION_INTERNET, permission);
-    FLAG_MSG_TRANS(permissionType, BPF_PERMISSION_UPDATE_DEVICE_STATS, permission);
-    if (permission) {
-        return StringPrintf("Unknown permission: %u", permission);
-    }
-    return permissionType;
-}
-
-StatusOr<std::unique_ptr<NetlinkListenerInterface>> TrafficController::makeSkDestroyListener() {
-    const auto& sys = sSyscalls.get();
-    ASSIGN_OR_RETURN(auto event, sys.eventfd(0, EFD_CLOEXEC));
-    const int domain = AF_NETLINK;
-    const int type = SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK;
-    const int protocol = NETLINK_INET_DIAG;
-    ASSIGN_OR_RETURN(auto sock, sys.socket(domain, type, protocol));
-
-    // TODO: if too many sockets are closed too quickly, we can overflow the socket buffer, and
-    // some entries in mCookieTagMap will not be freed. In order to fix this we would need to
-    // periodically dump all sockets and remove the tag entries for sockets that have been closed.
-    // For now, set a large-enough buffer that we can close hundreds of sockets without getting
-    // ENOBUFS and leaking mCookieTagMap entries.
-    int rcvbuf = 512 * 1024;
-    auto ret = sys.setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
-    if (!ret.ok()) {
-        ALOGW("Failed to set SkDestroyListener buffer size to %d: %s", rcvbuf, ret.msg().c_str());
-    }
-
-    sockaddr_nl addr = {
-        .nl_family = AF_NETLINK,
-        .nl_groups = 1 << (SKNLGRP_INET_TCP_DESTROY - 1) | 1 << (SKNLGRP_INET_UDP_DESTROY - 1) |
-                     1 << (SKNLGRP_INET6_TCP_DESTROY - 1) | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1)};
-    RETURN_IF_NOT_OK(sys.bind(sock, addr));
-
-    const sockaddr_nl kernel = {.nl_family = AF_NETLINK};
-    RETURN_IF_NOT_OK(sys.connect(sock, kernel));
-
-    std::unique_ptr<NetlinkListenerInterface> listener =
-            std::make_unique<NetlinkListener>(std::move(event), std::move(sock), "SkDestroyListen");
-
-    return listener;
-}
-
-Status TrafficController::initMaps() {
-    std::lock_guard guard(mMutex);
-
-    RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
-    RETURN_IF_NOT_OK(mUidCounterSetMap.init(UID_COUNTERSET_MAP_PATH));
-    RETURN_IF_NOT_OK(mAppUidStatsMap.init(APP_UID_STATS_MAP_PATH));
-    RETURN_IF_NOT_OK(mStatsMapA.init(STATS_MAP_A_PATH));
-    RETURN_IF_NOT_OK(mStatsMapB.init(STATS_MAP_B_PATH));
-    RETURN_IF_NOT_OK(mIfaceIndexNameMap.init(IFACE_INDEX_NAME_MAP_PATH));
-    RETURN_IF_NOT_OK(mIfaceStatsMap.init(IFACE_STATS_MAP_PATH));
-
-    RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
-
-    RETURN_IF_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
-    RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
-    ALOGI("%s successfully", __func__);
-
-    return netdutils::status::ok;
-}
-
-Status TrafficController::start(bool startSkDestroyListener) {
-    RETURN_IF_NOT_OK(initMaps());
-
-    if (!startSkDestroyListener) {
-        return netdutils::status::ok;
-    }
-
-    auto result = makeSkDestroyListener();
-    if (!isOk(result)) {
-        ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
-    } else {
-        mSkDestroyListener = std::move(result.value());
-    }
-    // Rx handler extracts nfgenmsg looks up and invokes registered dispatch function.
-    const auto rxHandler = [this](const nlmsghdr&, const Slice msg) {
-        std::lock_guard guard(mMutex);
-        inet_diag_msg diagmsg = {};
-        if (extract(msg, diagmsg) < sizeof(inet_diag_msg)) {
-            ALOGE("Unrecognized netlink message: %s", toString(msg).c_str());
-            return;
-        }
-        uint64_t sock_cookie = static_cast<uint64_t>(diagmsg.id.idiag_cookie[0]) |
-                               (static_cast<uint64_t>(diagmsg.id.idiag_cookie[1]) << 32);
-
-        Status s = mCookieTagMap.deleteValue(sock_cookie);
-        if (!isOk(s) && s.code() != ENOENT) {
-            ALOGE("Failed to delete cookie %" PRIx64 ": %s", sock_cookie, toString(s).c_str());
-            return;
-        }
-    };
-    expectOk(mSkDestroyListener->subscribe(kSockDiagMsgType, rxHandler));
-
-    // In case multiple netlink message comes in as a stream, we need to handle the rxDone message
-    // properly.
-    const auto rxDoneHandler = [](const nlmsghdr&, const Slice msg) {
-        // Ignore NLMSG_DONE  messages
-        inet_diag_msg diagmsg = {};
-        extract(msg, diagmsg);
-    };
-    expectOk(mSkDestroyListener->subscribe(kSockDiagDoneMsgType, rxDoneHandler));
-
-    return netdutils::status::ok;
-}
-
-Status TrafficController::updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
-                                              FirewallType type) {
-    std::lock_guard guard(mMutex);
-    if ((rule == ALLOW && type == ALLOWLIST) || (rule == DENY && type == DENYLIST)) {
-        RETURN_IF_NOT_OK(addRule(uid, match));
-    } else if ((rule == ALLOW && type == DENYLIST) || (rule == DENY && type == ALLOWLIST)) {
-        RETURN_IF_NOT_OK(removeRule(uid, match));
-    } else {
-        //Cannot happen.
-        return statusFromErrno(EINVAL, "");
-    }
-    return netdutils::status::ok;
-}
-
-Status TrafficController::removeRule(uint32_t uid, UidOwnerMatchType match) {
-    auto oldMatch = mUidOwnerMap.readValue(uid);
-    if (oldMatch.ok()) {
-        UidOwnerValue newMatch = {
-                .iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
-                .rule = oldMatch.value().rule & ~match,
-        };
-        if (newMatch.rule == 0) {
-            RETURN_IF_NOT_OK(mUidOwnerMap.deleteValue(uid));
-        } else {
-            RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
-        }
-    } else {
-        return statusFromErrno(ENOENT, StringPrintf("uid: %u does not exist in map", uid));
-    }
-    return netdutils::status::ok;
-}
-
-Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
-    if (match != IIF_MATCH && iif != 0) {
-        return statusFromErrno(EINVAL, "Non-interface match must have zero interface index");
-    }
-    auto oldMatch = mUidOwnerMap.readValue(uid);
-    if (oldMatch.ok()) {
-        UidOwnerValue newMatch = {
-                .iif = (match == IIF_MATCH) ? iif : oldMatch.value().iif,
-                .rule = oldMatch.value().rule | match,
-        };
-        RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
-    } else {
-        UidOwnerValue newMatch = {
-                .iif = iif,
-                .rule = match,
-        };
-        RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
-    }
-    return netdutils::status::ok;
-}
-
-Status TrafficController::updateUidOwnerMap(const uint32_t uid,
-                                            UidOwnerMatchType matchType, IptOp op) {
-    std::lock_guard guard(mMutex);
-    if (op == IptOpDelete) {
-        RETURN_IF_NOT_OK(removeRule(uid, matchType));
-    } else if (op == IptOpInsert) {
-        RETURN_IF_NOT_OK(addRule(uid, matchType));
-    } else {
-        // Cannot happen.
-        return statusFromErrno(EINVAL, StringPrintf("invalid IptOp: %d, %d", op, matchType));
-    }
-    return netdutils::status::ok;
-}
-
-FirewallType TrafficController::getFirewallType(ChildChain chain) {
-    switch (chain) {
-        case DOZABLE:
-            return ALLOWLIST;
-        case STANDBY:
-            return DENYLIST;
-        case POWERSAVE:
-            return ALLOWLIST;
-        case RESTRICTED:
-            return ALLOWLIST;
-        case LOW_POWER_STANDBY:
-            return ALLOWLIST;
-        case OEM_DENY_1:
-            return DENYLIST;
-        case OEM_DENY_2:
-            return DENYLIST;
-        case OEM_DENY_3:
-            return DENYLIST;
-        case NONE:
-        default:
-            return DENYLIST;
-    }
-}
-
-int TrafficController::changeUidOwnerRule(ChildChain chain, uid_t uid, FirewallRule rule,
-                                          FirewallType type) {
-    Status res;
-    switch (chain) {
-        case DOZABLE:
-            res = updateOwnerMapEntry(DOZABLE_MATCH, uid, rule, type);
-            break;
-        case STANDBY:
-            res = updateOwnerMapEntry(STANDBY_MATCH, uid, rule, type);
-            break;
-        case POWERSAVE:
-            res = updateOwnerMapEntry(POWERSAVE_MATCH, uid, rule, type);
-            break;
-        case RESTRICTED:
-            res = updateOwnerMapEntry(RESTRICTED_MATCH, uid, rule, type);
-            break;
-        case LOW_POWER_STANDBY:
-            res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
-            break;
-        case OEM_DENY_1:
-            res = updateOwnerMapEntry(OEM_DENY_1_MATCH, uid, rule, type);
-            break;
-        case OEM_DENY_2:
-            res = updateOwnerMapEntry(OEM_DENY_2_MATCH, uid, rule, type);
-            break;
-        case OEM_DENY_3:
-            res = updateOwnerMapEntry(OEM_DENY_3_MATCH, uid, rule, type);
-            break;
-        case NONE:
-        default:
-            ALOGW("Unknown child chain: %d", chain);
-            return -EINVAL;
-    }
-    if (!isOk(res)) {
-        ALOGE("change uid(%u) rule of %d failed: %s, rule: %d, type: %d", uid, chain,
-              res.msg().c_str(), rule, type);
-        return -res.code();
-    }
-    return 0;
-}
-
-Status TrafficController::replaceRulesInMap(const UidOwnerMatchType match,
-                                            const std::vector<int32_t>& uids) {
-    std::lock_guard guard(mMutex);
-    std::set<int32_t> uidSet(uids.begin(), uids.end());
-    std::vector<uint32_t> uidsToDelete;
-    auto getUidsToDelete = [&uidsToDelete, &uidSet](const uint32_t& key,
-                                                    const BpfMap<uint32_t, UidOwnerValue>&) {
-        if (uidSet.find((int32_t) key) == uidSet.end()) {
-            uidsToDelete.push_back(key);
-        }
-        return base::Result<void>();
-    };
-    RETURN_IF_NOT_OK(mUidOwnerMap.iterate(getUidsToDelete));
-
-    for(auto uid : uidsToDelete) {
-        RETURN_IF_NOT_OK(removeRule(uid, match));
-    }
-
-    for (auto uid : uids) {
-        RETURN_IF_NOT_OK(addRule(uid, match));
-    }
-    return netdutils::status::ok;
-}
-
-Status TrafficController::addUidInterfaceRules(const int iif,
-                                               const std::vector<int32_t>& uidsToAdd) {
-    std::lock_guard guard(mMutex);
-
-    for (auto uid : uidsToAdd) {
-        netdutils::Status result = addRule(uid, IIF_MATCH, iif);
-        if (!isOk(result)) {
-            ALOGW("addRule failed(%d): uid=%d iif=%d", result.code(), uid, iif);
-        }
-    }
-    return netdutils::status::ok;
-}
-
-Status TrafficController::removeUidInterfaceRules(const std::vector<int32_t>& uidsToDelete) {
-    std::lock_guard guard(mMutex);
-
-    for (auto uid : uidsToDelete) {
-        netdutils::Status result = removeRule(uid, IIF_MATCH);
-        if (!isOk(result)) {
-            ALOGW("removeRule failed(%d): uid=%d", result.code(), uid);
-        }
-    }
-    return netdutils::status::ok;
-}
-
-Status TrafficController::updateUidLockdownRule(const uid_t uid, const bool add) {
-    std::lock_guard guard(mMutex);
-
-    netdutils::Status result = add ? addRule(uid, LOCKDOWN_VPN_MATCH)
-                               : removeRule(uid, LOCKDOWN_VPN_MATCH);
-    if (!isOk(result)) {
-        ALOGW("%s Lockdown rule failed(%d): uid=%d",
-              (add ? "add": "remove"), result.code(), uid);
-    }
-    return result;
-}
-
-int TrafficController::replaceUidOwnerMap(const std::string& name, bool isAllowlist __unused,
-                                          const std::vector<int32_t>& uids) {
-    // FirewallRule rule = isAllowlist ? ALLOW : DENY;
-    // FirewallType type = isAllowlist ? ALLOWLIST : DENYLIST;
-    Status res;
-    if (!name.compare(LOCAL_DOZABLE)) {
-        res = replaceRulesInMap(DOZABLE_MATCH, uids);
-    } else if (!name.compare(LOCAL_STANDBY)) {
-        res = replaceRulesInMap(STANDBY_MATCH, uids);
-    } else if (!name.compare(LOCAL_POWERSAVE)) {
-        res = replaceRulesInMap(POWERSAVE_MATCH, uids);
-    } else if (!name.compare(LOCAL_RESTRICTED)) {
-        res = replaceRulesInMap(RESTRICTED_MATCH, uids);
-    } else if (!name.compare(LOCAL_LOW_POWER_STANDBY)) {
-        res = replaceRulesInMap(LOW_POWER_STANDBY_MATCH, uids);
-    } else if (!name.compare(LOCAL_OEM_DENY_1)) {
-        res = replaceRulesInMap(OEM_DENY_1_MATCH, uids);
-    } else if (!name.compare(LOCAL_OEM_DENY_2)) {
-        res = replaceRulesInMap(OEM_DENY_2_MATCH, uids);
-    } else if (!name.compare(LOCAL_OEM_DENY_3)) {
-        res = replaceRulesInMap(OEM_DENY_3_MATCH, uids);
-    } else {
-        ALOGE("unknown chain name: %s", name.c_str());
-        return -EINVAL;
-    }
-    if (!isOk(res)) {
-        ALOGE("Failed to clean up chain: %s: %s", name.c_str(), res.msg().c_str());
-        return -res.code();
-    }
-    return 0;
-}
-
-int TrafficController::toggleUidOwnerMap(ChildChain chain, bool enable) {
-    std::lock_guard guard(mMutex);
-    uint32_t key = UID_RULES_CONFIGURATION_KEY;
-    auto oldConfigure = mConfigurationMap.readValue(key);
-    if (!oldConfigure.ok()) {
-        ALOGE("Cannot read the old configuration from map: %s",
-              oldConfigure.error().message().c_str());
-        return -oldConfigure.error().code();
-    }
-    uint32_t match;
-    switch (chain) {
-        case DOZABLE:
-            match = DOZABLE_MATCH;
-            break;
-        case STANDBY:
-            match = STANDBY_MATCH;
-            break;
-        case POWERSAVE:
-            match = POWERSAVE_MATCH;
-            break;
-        case RESTRICTED:
-            match = RESTRICTED_MATCH;
-            break;
-        case LOW_POWER_STANDBY:
-            match = LOW_POWER_STANDBY_MATCH;
-            break;
-        case OEM_DENY_1:
-            match = OEM_DENY_1_MATCH;
-            break;
-        case OEM_DENY_2:
-            match = OEM_DENY_2_MATCH;
-            break;
-        case OEM_DENY_3:
-            match = OEM_DENY_3_MATCH;
-            break;
-        default:
-            return -EINVAL;
-    }
-    BpfConfig newConfiguration =
-            enable ? (oldConfigure.value() | match) : (oldConfigure.value() & ~match);
-    Status res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
-    if (!isOk(res)) {
-        ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
-    }
-    return -res.code();
-}
-
-Status TrafficController::swapActiveStatsMap() {
-    std::lock_guard guard(mMutex);
-
-    uint32_t key = CURRENT_STATS_MAP_CONFIGURATION_KEY;
-    auto oldConfigure = mConfigurationMap.readValue(key);
-    if (!oldConfigure.ok()) {
-        ALOGE("Cannot read the old configuration from map: %s",
-              oldConfigure.error().message().c_str());
-        return Status(oldConfigure.error().code(), oldConfigure.error().message());
-    }
-
-    // Write to the configuration map to inform the kernel eBPF program to switch
-    // from using one map to the other. Use flag BPF_EXIST here since the map should
-    // be already populated in initMaps.
-    uint32_t newConfigure = (oldConfigure.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
-    auto res = mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, newConfigure,
-                                            BPF_EXIST);
-    if (!res.ok()) {
-        ALOGE("Failed to toggle the stats map: %s", strerror(res.error().code()));
-        return res;
-    }
-    // After changing the config, we need to make sure all the current running
-    // eBPF programs are finished and all the CPUs are aware of this config change
-    // before we modify the old map. So we do a special hack here to wait for
-    // the kernel to do a synchronize_rcu(). Once the kernel called
-    // synchronize_rcu(), the config we just updated will be available to all cores
-    // and the next eBPF programs triggered inside the kernel will use the new
-    // map configuration. So once this function returns we can safely modify the
-    // old stats map without concerning about race between the kernel and
-    // userspace.
-    int ret = synchronizeKernelRCU();
-    if (ret) {
-        ALOGE("map swap synchronize_rcu() ended with failure: %s", strerror(-ret));
-        return statusFromErrno(-ret, "map swap synchronize_rcu() failed");
-    }
-    return netdutils::status::ok;
-}
-
-void TrafficController::setPermissionForUids(int permission, const std::vector<uid_t>& uids) {
-    std::lock_guard guard(mMutex);
-    if (permission == INetd::PERMISSION_UNINSTALLED) {
-        for (uid_t uid : uids) {
-            // Clean up all permission information for the related uid if all the
-            // packages related to it are uninstalled.
-            mPrivilegedUser.erase(uid);
-            Status ret = mUidPermissionMap.deleteValue(uid);
-            if (!isOk(ret) && ret.code() != ENOENT) {
-                ALOGE("Failed to clean up the permission for %u: %s", uid, strerror(ret.code()));
-            }
-        }
-        return;
-    }
-
-    bool privileged = (permission & INetd::PERMISSION_UPDATE_DEVICE_STATS);
-
-    for (uid_t uid : uids) {
-        if (privileged) {
-            mPrivilegedUser.insert(uid);
-        } else {
-            mPrivilegedUser.erase(uid);
-        }
-
-        // The map stores all the permissions that the UID has, except if the only permission
-        // the UID has is the INTERNET permission, then the UID should not appear in the map.
-        if (permission != INetd::PERMISSION_INTERNET) {
-            Status ret = mUidPermissionMap.writeValue(uid, permission, BPF_ANY);
-            if (!isOk(ret)) {
-                ALOGE("Failed to set permission: %s of uid(%u) to permission map: %s",
-                      UidPermissionTypeToString(permission).c_str(), uid, strerror(ret.code()));
-            }
-        } else {
-            Status ret = mUidPermissionMap.deleteValue(uid);
-            if (!isOk(ret) && ret.code() != ENOENT) {
-                ALOGE("Failed to remove uid %u from permission map: %s", uid, strerror(ret.code()));
-            }
-        }
-    }
-}
-
-std::string getMapStatus(const base::unique_fd& map_fd, const char* path) {
-    if (map_fd.get() < 0) {
-        return StringPrintf("map fd lost");
-    }
-    if (access(path, F_OK) != 0) {
-        return StringPrintf("map not pinned to location: %s", path);
-    }
-    return StringPrintf("OK");
-}
-
-// NOLINTNEXTLINE(google-runtime-references): grandfathered pass by non-const reference
-void dumpBpfMap(const std::string& mapName, DumpWriter& dw, const std::string& header) {
-    dw.blankline();
-    dw.println("%s:", mapName.c_str());
-    if (!header.empty()) {
-        dw.println(header);
-    }
-}
-
-void TrafficController::dump(int fd, bool verbose __unused) {
-    std::lock_guard guard(mMutex);
-    DumpWriter dw(fd);
-
-    ScopedIndent indentTop(dw);
-    dw.println("TrafficController");
-
-    ScopedIndent indentPreBpfModule(dw);
-
-    dw.blankline();
-    dw.println("mCookieTagMap status: %s",
-               getMapStatus(mCookieTagMap.getMap(), COOKIE_TAG_MAP_PATH).c_str());
-    dw.println("mUidCounterSetMap status: %s",
-               getMapStatus(mUidCounterSetMap.getMap(), UID_COUNTERSET_MAP_PATH).c_str());
-    dw.println("mAppUidStatsMap status: %s",
-               getMapStatus(mAppUidStatsMap.getMap(), APP_UID_STATS_MAP_PATH).c_str());
-    dw.println("mStatsMapA status: %s",
-               getMapStatus(mStatsMapA.getMap(), STATS_MAP_A_PATH).c_str());
-    dw.println("mStatsMapB status: %s",
-               getMapStatus(mStatsMapB.getMap(), STATS_MAP_B_PATH).c_str());
-    dw.println("mIfaceIndexNameMap status: %s",
-               getMapStatus(mIfaceIndexNameMap.getMap(), IFACE_INDEX_NAME_MAP_PATH).c_str());
-    dw.println("mIfaceStatsMap status: %s",
-               getMapStatus(mIfaceStatsMap.getMap(), IFACE_STATS_MAP_PATH).c_str());
-    dw.println("mConfigurationMap status: %s",
-               getMapStatus(mConfigurationMap.getMap(), CONFIGURATION_MAP_PATH).c_str());
-    dw.println("mUidOwnerMap status: %s",
-               getMapStatus(mUidOwnerMap.getMap(), UID_OWNER_MAP_PATH).c_str());
-}
-
-}  // namespace net
-}  // namespace android
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
deleted file mode 100644
index 57f32af..0000000
--- a/service/native/TrafficControllerTest.cpp
+++ /dev/null
@@ -1,925 +0,0 @@
-/*
- * Copyright 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.
- *
- * TrafficControllerTest.cpp - unit tests for TrafficController.cpp
- */
-
-#include <cstdint>
-#include <string>
-#include <vector>
-
-#include <fcntl.h>
-#include <inttypes.h>
-#include <linux/inet_diag.h>
-#include <linux/sock_diag.h>
-#include <sys/socket.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#include <gtest/gtest.h>
-
-#include <android-base/file.h>
-#include <android-base/logging.h>
-#include <android-base/stringprintf.h>
-#include <android-base/strings.h>
-#include <binder/Status.h>
-
-#include <netdutils/MockSyscalls.h>
-
-#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
-#include "TrafficController.h"
-#include "bpf/BpfUtils.h"
-#include "NetdUpdatablePublic.h"
-
-using namespace android::bpf;  // NOLINT(google-build-using-namespace): grandfathered
-
-namespace android {
-namespace net {
-
-using android::netdutils::Status;
-using base::Result;
-using netdutils::isOk;
-using netdutils::statusFromErrno;
-
-constexpr int TEST_MAP_SIZE = 10;
-constexpr uid_t TEST_UID = 10086;
-constexpr uid_t TEST_UID2 = 54321;
-constexpr uid_t TEST_UID3 = 98765;
-constexpr uint32_t TEST_TAG = 42;
-constexpr uint32_t TEST_COUNTERSET = 1;
-constexpr int TEST_IFINDEX = 999;
-constexpr int RXPACKETS = 1;
-constexpr int RXBYTES = 100;
-constexpr int TXPACKETS = 0;
-constexpr int TXBYTES = 0;
-
-#define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
-#define ASSERT_INVALID(x) ASSERT_FALSE((x).isValid())
-
-class TrafficControllerTest : public ::testing::Test {
-  protected:
-    TrafficControllerTest() {}
-    TrafficController mTc;
-    BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
-    BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
-    BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
-    BpfMap<StatsKey, StatsValue> mFakeStatsMapB;  // makeTrafficControllerMapsInvalid only
-    BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap; ;  // makeTrafficControllerMapsInvalid only
-    BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
-    BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
-    BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
-    BpfMap<uint32_t, uint8_t> mFakeUidCounterSetMap;
-    BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
-
-    void SetUp() {
-        std::lock_guard guard(mTc.mMutex);
-        ASSERT_EQ(0, setrlimitForTest());
-
-        mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
-        ASSERT_VALID(mFakeCookieTagMap);
-
-        mFakeAppUidStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
-        ASSERT_VALID(mFakeAppUidStatsMap);
-
-        mFakeStatsMapA.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
-        ASSERT_VALID(mFakeStatsMapA);
-
-        mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
-        ASSERT_VALID(mFakeConfigurationMap);
-
-        mFakeUidOwnerMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
-        ASSERT_VALID(mFakeUidOwnerMap);
-        mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
-        ASSERT_VALID(mFakeUidPermissionMap);
-
-        mFakeUidCounterSetMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
-        ASSERT_VALID(mFakeUidCounterSetMap);
-
-        mFakeIfaceIndexNameMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
-        ASSERT_VALID(mFakeIfaceIndexNameMap);
-
-        mTc.mCookieTagMap = mFakeCookieTagMap;
-        ASSERT_VALID(mTc.mCookieTagMap);
-        mTc.mAppUidStatsMap = mFakeAppUidStatsMap;
-        ASSERT_VALID(mTc.mAppUidStatsMap);
-        mTc.mStatsMapA = mFakeStatsMapA;
-        ASSERT_VALID(mTc.mStatsMapA);
-        mTc.mConfigurationMap = mFakeConfigurationMap;
-        ASSERT_VALID(mTc.mConfigurationMap);
-
-        // Always write to stats map A by default.
-        static_assert(SELECT_MAP_A == 0);
-
-        mTc.mUidOwnerMap = mFakeUidOwnerMap;
-        ASSERT_VALID(mTc.mUidOwnerMap);
-        mTc.mUidPermissionMap = mFakeUidPermissionMap;
-        ASSERT_VALID(mTc.mUidPermissionMap);
-        mTc.mPrivilegedUser.clear();
-
-        mTc.mUidCounterSetMap = mFakeUidCounterSetMap;
-        ASSERT_VALID(mTc.mUidCounterSetMap);
-
-        mTc.mIfaceIndexNameMap = mFakeIfaceIndexNameMap;
-        ASSERT_VALID(mTc.mIfaceIndexNameMap);
-    }
-
-    void populateFakeStats(uint64_t cookie, uint32_t uid, uint32_t tag, StatsKey* key) {
-        UidTagValue cookieMapkey = {.uid = (uint32_t)uid, .tag = tag};
-        EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, cookieMapkey, BPF_ANY));
-        *key = {.uid = uid, .tag = tag, .counterSet = TEST_COUNTERSET, .ifaceIndex = TEST_IFINDEX};
-        StatsValue statsMapValue = {.rxPackets = RXPACKETS, .rxBytes = RXBYTES,
-                                    .txPackets = TXPACKETS, .txBytes = TXBYTES};
-        EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
-        EXPECT_RESULT_OK(mFakeAppUidStatsMap.writeValue(uid, statsMapValue, BPF_ANY));
-        // put tag information back to statsKey
-        key->tag = tag;
-    }
-
-    void populateFakeCounterSet(uint32_t uid, uint32_t counterSet) {
-        EXPECT_RESULT_OK(mFakeUidCounterSetMap.writeValue(uid, counterSet, BPF_ANY));
-    }
-
-    void populateFakeIfaceIndexName(const char* name, uint32_t ifaceIndex) {
-        if (name == nullptr || ifaceIndex <= 0) return;
-
-        IfaceValue iface;
-        strlcpy(iface.name, name, sizeof(IfaceValue));
-        EXPECT_RESULT_OK(mFakeIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY));
-    }
-
-    void checkUidOwnerRuleForChain(ChildChain chain, UidOwnerMatchType match) {
-        uint32_t uid = TEST_UID;
-        EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, DENYLIST));
-        Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
-        EXPECT_RESULT_OK(value);
-        EXPECT_TRUE(value.value().rule & match);
-
-        uid = TEST_UID2;
-        EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, ALLOW, ALLOWLIST));
-        value = mFakeUidOwnerMap.readValue(uid);
-        EXPECT_RESULT_OK(value);
-        EXPECT_TRUE(value.value().rule & match);
-
-        EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, ALLOWLIST));
-        value = mFakeUidOwnerMap.readValue(uid);
-        EXPECT_FALSE(value.ok());
-        EXPECT_EQ(ENOENT, value.error().code());
-
-        uid = TEST_UID;
-        EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, ALLOW, DENYLIST));
-        value = mFakeUidOwnerMap.readValue(uid);
-        EXPECT_FALSE(value.ok());
-        EXPECT_EQ(ENOENT, value.error().code());
-
-        uid = TEST_UID3;
-        EXPECT_EQ(-ENOENT, mTc.changeUidOwnerRule(chain, uid, ALLOW, DENYLIST));
-        value = mFakeUidOwnerMap.readValue(uid);
-        EXPECT_FALSE(value.ok());
-        EXPECT_EQ(ENOENT, value.error().code());
-    }
-
-    void checkEachUidValue(const std::vector<int32_t>& uids, UidOwnerMatchType match) {
-        for (uint32_t uid : uids) {
-            Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
-            EXPECT_RESULT_OK(value);
-            EXPECT_TRUE(value.value().rule & match);
-        }
-        std::set<uint32_t> uidSet(uids.begin(), uids.end());
-        const auto checkNoOtherUid = [&uidSet](const int32_t& key,
-                                               const BpfMap<uint32_t, UidOwnerValue>&) {
-            EXPECT_NE(uidSet.end(), uidSet.find(key));
-            return Result<void>();
-        };
-        EXPECT_RESULT_OK(mFakeUidOwnerMap.iterate(checkNoOtherUid));
-    }
-
-    void checkUidMapReplace(const std::string& name, const std::vector<int32_t>& uids,
-                            UidOwnerMatchType match) {
-        bool isAllowlist = true;
-        EXPECT_EQ(0, mTc.replaceUidOwnerMap(name, isAllowlist, uids));
-        checkEachUidValue(uids, match);
-
-        isAllowlist = false;
-        EXPECT_EQ(0, mTc.replaceUidOwnerMap(name, isAllowlist, uids));
-        checkEachUidValue(uids, match);
-    }
-
-    void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint32_t expectedRule,
-                                 uint32_t expectedIif) {
-        for (uint32_t uid : appUids) {
-            Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
-            EXPECT_RESULT_OK(value);
-            EXPECT_EQ(expectedRule, value.value().rule)
-                    << "Expected rule for UID " << uid << " to be " << expectedRule << ", but was "
-                    << value.value().rule;
-            EXPECT_EQ(expectedIif, value.value().iif)
-                    << "Expected iif for UID " << uid << " to be " << expectedIif << ", but was "
-                    << value.value().iif;
-        }
-    }
-
-    template <class Key, class Value>
-    void expectMapEmpty(BpfMap<Key, Value>& map) {
-        auto isEmpty = map.isEmpty();
-        EXPECT_RESULT_OK(isEmpty);
-        EXPECT_TRUE(isEmpty.value());
-    }
-
-    void expectUidPermissionMapValues(const std::vector<uid_t>& appUids, uint8_t expectedValue) {
-        for (uid_t uid : appUids) {
-            Result<uint8_t> value = mFakeUidPermissionMap.readValue(uid);
-            EXPECT_RESULT_OK(value);
-            EXPECT_EQ(expectedValue, value.value())
-                    << "Expected value for UID " << uid << " to be " << expectedValue
-                    << ", but was " << value.value();
-        }
-    }
-
-    void expectPrivilegedUserSet(const std::vector<uid_t>& appUids) {
-        std::lock_guard guard(mTc.mMutex);
-        EXPECT_EQ(appUids.size(), mTc.mPrivilegedUser.size());
-        for (uid_t uid : appUids) {
-            EXPECT_NE(mTc.mPrivilegedUser.end(), mTc.mPrivilegedUser.find(uid));
-        }
-    }
-
-    void expectPrivilegedUserSetEmpty() {
-        std::lock_guard guard(mTc.mMutex);
-        EXPECT_TRUE(mTc.mPrivilegedUser.empty());
-    }
-
-    Status updateUidOwnerMaps(const std::vector<uint32_t>& appUids,
-                              UidOwnerMatchType matchType, TrafficController::IptOp op) {
-        Status ret(0);
-        for (auto uid : appUids) {
-        ret = mTc.updateUidOwnerMap(uid, matchType, op);
-           if(!isOk(ret)) break;
-        }
-        return ret;
-    }
-
-    Status dump(bool verbose, std::vector<std::string>& outputLines) {
-      if (!outputLines.empty()) return statusFromErrno(EUCLEAN, "Output buffer is not empty");
-
-      android::base::unique_fd localFd, remoteFd;
-      if (!Pipe(&localFd, &remoteFd)) return statusFromErrno(errno, "Failed on pipe");
-
-      // dump() blocks until another thread has consumed all its output.
-      std::thread dumpThread =
-          std::thread([this, remoteFd{std::move(remoteFd)}, verbose]() {
-            mTc.dump(remoteFd, verbose);
-          });
-
-      std::string dumpContent;
-      if (!android::base::ReadFdToString(localFd.get(), &dumpContent)) {
-        return statusFromErrno(errno, "Failed to read dump results from fd");
-      }
-      dumpThread.join();
-
-      std::stringstream dumpStream(std::move(dumpContent));
-      std::string line;
-      while (std::getline(dumpStream, line)) {
-        outputLines.push_back(line);
-      }
-
-      return netdutils::status::ok;
-    }
-
-    // Strings in the |expect| must exist in dump results in order. But no need to be consecutive.
-    bool expectDumpsysContains(std::vector<std::string>& expect) {
-        if (expect.empty()) return false;
-
-        std::vector<std::string> output;
-        Status result = dump(true, output);
-        if (!isOk(result)) {
-            GTEST_LOG_(ERROR) << "TrafficController dump failed: " << netdutils::toString(result);
-            return false;
-        }
-
-        int matched = 0;
-        auto it = expect.begin();
-        for (const auto& line : output) {
-            if (it == expect.end()) break;
-            if (std::string::npos != line.find(*it)) {
-                matched++;
-                ++it;
-            }
-        }
-
-        if (matched != expect.size()) {
-            // dump results for debugging
-            for (const auto& o : output) LOG(INFO) << "output: " << o;
-            for (const auto& e : expect) LOG(INFO) << "expect: " << e;
-            return false;
-        }
-        return true;
-    }
-
-    // Once called, the maps of TrafficController can't recover to valid maps which initialized
-    // in SetUp().
-    void makeTrafficControllerMapsInvalid() {
-        constexpr char INVALID_PATH[] = "invalid";
-
-        mFakeCookieTagMap.init(INVALID_PATH);
-        mTc.mCookieTagMap = mFakeCookieTagMap;
-        ASSERT_INVALID(mTc.mCookieTagMap);
-
-        mFakeAppUidStatsMap.init(INVALID_PATH);
-        mTc.mAppUidStatsMap = mFakeAppUidStatsMap;
-        ASSERT_INVALID(mTc.mAppUidStatsMap);
-
-        mFakeStatsMapA.init(INVALID_PATH);
-        mTc.mStatsMapA = mFakeStatsMapA;
-        ASSERT_INVALID(mTc.mStatsMapA);
-
-        mFakeStatsMapB.init(INVALID_PATH);
-        mTc.mStatsMapB = mFakeStatsMapB;
-        ASSERT_INVALID(mTc.mStatsMapB);
-
-        mFakeIfaceStatsMap.init(INVALID_PATH);
-        mTc.mIfaceStatsMap = mFakeIfaceStatsMap;
-        ASSERT_INVALID(mTc.mIfaceStatsMap);
-
-        mFakeConfigurationMap.init(INVALID_PATH);
-        mTc.mConfigurationMap = mFakeConfigurationMap;
-        ASSERT_INVALID(mTc.mConfigurationMap);
-
-        mFakeUidOwnerMap.init(INVALID_PATH);
-        mTc.mUidOwnerMap = mFakeUidOwnerMap;
-        ASSERT_INVALID(mTc.mUidOwnerMap);
-
-        mFakeUidPermissionMap.init(INVALID_PATH);
-        mTc.mUidPermissionMap = mFakeUidPermissionMap;
-        ASSERT_INVALID(mTc.mUidPermissionMap);
-
-        mFakeUidCounterSetMap.init(INVALID_PATH);
-        mTc.mUidCounterSetMap = mFakeUidCounterSetMap;
-        ASSERT_INVALID(mTc.mUidCounterSetMap);
-
-        mFakeIfaceIndexNameMap.init(INVALID_PATH);
-        mTc.mIfaceIndexNameMap = mFakeIfaceIndexNameMap;
-        ASSERT_INVALID(mTc.mIfaceIndexNameMap);
-    }
-};
-
-TEST_F(TrafficControllerTest, TestUpdateOwnerMapEntry) {
-    uint32_t uid = TEST_UID;
-    ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, DENY, DENYLIST)));
-    Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
-    ASSERT_RESULT_OK(value);
-    ASSERT_TRUE(value.value().rule & STANDBY_MATCH);
-
-    ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(DOZABLE_MATCH, uid, ALLOW, ALLOWLIST)));
-    value = mFakeUidOwnerMap.readValue(uid);
-    ASSERT_RESULT_OK(value);
-    ASSERT_TRUE(value.value().rule & DOZABLE_MATCH);
-
-    ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(DOZABLE_MATCH, uid, DENY, ALLOWLIST)));
-    value = mFakeUidOwnerMap.readValue(uid);
-    ASSERT_RESULT_OK(value);
-    ASSERT_FALSE(value.value().rule & DOZABLE_MATCH);
-
-    ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, ALLOW, DENYLIST)));
-    ASSERT_FALSE(mFakeUidOwnerMap.readValue(uid).ok());
-
-    uid = TEST_UID2;
-    ASSERT_FALSE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, ALLOW, DENYLIST)));
-    ASSERT_FALSE(mFakeUidOwnerMap.readValue(uid).ok());
-}
-
-TEST_F(TrafficControllerTest, TestChangeUidOwnerRule) {
-    checkUidOwnerRuleForChain(DOZABLE, DOZABLE_MATCH);
-    checkUidOwnerRuleForChain(STANDBY, STANDBY_MATCH);
-    checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
-    checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
-    checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
-    checkUidOwnerRuleForChain(OEM_DENY_1, OEM_DENY_1_MATCH);
-    checkUidOwnerRuleForChain(OEM_DENY_2, OEM_DENY_2_MATCH);
-    checkUidOwnerRuleForChain(OEM_DENY_3, OEM_DENY_3_MATCH);
-    ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
-    ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
-}
-
-TEST_F(TrafficControllerTest, TestReplaceUidOwnerMap) {
-    std::vector<int32_t> uids = {TEST_UID, TEST_UID2, TEST_UID3};
-    checkUidMapReplace("fw_dozable", uids, DOZABLE_MATCH);
-    checkUidMapReplace("fw_standby", uids, STANDBY_MATCH);
-    checkUidMapReplace("fw_powersave", uids, POWERSAVE_MATCH);
-    checkUidMapReplace("fw_restricted", uids, RESTRICTED_MATCH);
-    checkUidMapReplace("fw_low_power_standby", uids, LOW_POWER_STANDBY_MATCH);
-    checkUidMapReplace("fw_oem_deny_1", uids, OEM_DENY_1_MATCH);
-    checkUidMapReplace("fw_oem_deny_2", uids, OEM_DENY_2_MATCH);
-    checkUidMapReplace("fw_oem_deny_3", uids, OEM_DENY_3_MATCH);
-    ASSERT_EQ(-EINVAL, mTc.replaceUidOwnerMap("unknow", true, uids));
-}
-
-TEST_F(TrafficControllerTest, TestReplaceSameChain) {
-    std::vector<int32_t> uids = {TEST_UID, TEST_UID2, TEST_UID3};
-    checkUidMapReplace("fw_dozable", uids, DOZABLE_MATCH);
-    std::vector<int32_t> newUids = {TEST_UID2, TEST_UID3};
-    checkUidMapReplace("fw_dozable", newUids, DOZABLE_MATCH);
-}
-
-TEST_F(TrafficControllerTest, TestDenylistUidMatch) {
-    std::vector<uint32_t> appUids = {1000, 1001, 10012};
-    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
-                                        TrafficController::IptOpInsert)));
-    expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
-    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
-                                        TrafficController::IptOpDelete)));
-    expectMapEmpty(mFakeUidOwnerMap);
-}
-
-TEST_F(TrafficControllerTest, TestAllowlistUidMatch) {
-    std::vector<uint32_t> appUids = {1000, 1001, 10012};
-    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpInsert)));
-    expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
-    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpDelete)));
-    expectMapEmpty(mFakeUidOwnerMap);
-}
-
-TEST_F(TrafficControllerTest, TestReplaceMatchUid) {
-    std::vector<uint32_t> appUids = {1000, 1001, 10012};
-    // Add appUids to the denylist and expect that their values are all PENALTY_BOX_MATCH.
-    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
-                                        TrafficController::IptOpInsert)));
-    expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
-
-    // Add the same UIDs to the allowlist and expect that we get PENALTY_BOX_MATCH |
-    // HAPPY_BOX_MATCH.
-    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpInsert)));
-    expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH | PENALTY_BOX_MATCH, 0);
-
-    // Remove the same UIDs from the allowlist and check the PENALTY_BOX_MATCH is still there.
-    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpDelete)));
-    expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
-
-    // Remove the same UIDs from the denylist and check the map is empty.
-    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
-                                        TrafficController::IptOpDelete)));
-    ASSERT_FALSE(mFakeUidOwnerMap.getFirstKey().ok());
-}
-
-TEST_F(TrafficControllerTest, TestDeleteWrongMatchSilentlyFails) {
-    std::vector<uint32_t> appUids = {1000, 1001, 10012};
-    // If the uid does not exist in the map, trying to delete a rule about it will fail.
-    ASSERT_FALSE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH,
-                                         TrafficController::IptOpDelete)));
-    expectMapEmpty(mFakeUidOwnerMap);
-
-    // Add denylist rules for appUids.
-    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH,
-                                        TrafficController::IptOpInsert)));
-    expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
-
-    // Delete (non-existent) denylist rules for appUids, and check that this silently does
-    // nothing if the uid is in the map but does not have denylist match. This is required because
-    // NetworkManagementService will try to remove a uid from denylist after adding it to the
-    // allowlist and if the remove fails it will not update the uid status.
-    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
-                                        TrafficController::IptOpDelete)));
-    expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
-}
-
-TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRules) {
-    int iif0 = 15;
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif0, {1000, 1001})));
-    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
-
-    // Add some non-overlapping new uids. They should coexist with existing rules
-    int iif1 = 16;
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {2000, 2001})));
-    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
-    expectUidOwnerMapValues({2000, 2001}, IIF_MATCH, iif1);
-
-    // Overwrite some existing uids
-    int iif2 = 17;
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif2, {1000, 2000})));
-    expectUidOwnerMapValues({1001}, IIF_MATCH, iif0);
-    expectUidOwnerMapValues({2001}, IIF_MATCH, iif1);
-    expectUidOwnerMapValues({1000, 2000}, IIF_MATCH, iif2);
-}
-
-TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRules) {
-    int iif0 = 15;
-    int iif1 = 16;
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif0, {1000, 1001})));
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {2000, 2001})));
-    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
-    expectUidOwnerMapValues({2000, 2001}, IIF_MATCH, iif1);
-
-    // Rmove some uids
-    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001, 2001})));
-    expectUidOwnerMapValues({1000}, IIF_MATCH, iif0);
-    expectUidOwnerMapValues({2000}, IIF_MATCH, iif1);
-    checkEachUidValue({1000, 2000}, IIF_MATCH);  // Make sure there are only two uids remaining
-
-    // Remove non-existent uids shouldn't fail
-    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({2000, 3000})));
-    expectUidOwnerMapValues({1000}, IIF_MATCH, iif0);
-    checkEachUidValue({1000}, IIF_MATCH);  // Make sure there are only one uid remaining
-
-    // Remove everything
-    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
-    expectMapEmpty(mFakeUidOwnerMap);
-}
-
-TEST_F(TrafficControllerTest, TestUpdateUidLockdownRule) {
-    // Add Lockdown rules
-    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, true /* add */)));
-    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, true /* add */)));
-    expectUidOwnerMapValues({1000, 1001}, LOCKDOWN_VPN_MATCH, 0);
-
-    // Remove one of Lockdown rules
-    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, false /* add */)));
-    expectUidOwnerMapValues({1001}, LOCKDOWN_VPN_MATCH, 0);
-
-    // Remove remaining Lockdown rule
-    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, false /* add */)));
-    expectMapEmpty(mFakeUidOwnerMap);
-}
-
-TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithExistingMatches) {
-    // Set up existing PENALTY_BOX_MATCH rules
-    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000, 1001, 10012}, PENALTY_BOX_MATCH,
-                                        TrafficController::IptOpInsert)));
-    expectUidOwnerMapValues({1000, 1001, 10012}, PENALTY_BOX_MATCH, 0);
-
-    // Add some partially-overlapping uid owner rules and check result
-    int iif1 = 32;
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {10012, 10013, 10014})));
-    expectUidOwnerMapValues({1000, 1001}, PENALTY_BOX_MATCH, 0);
-    expectUidOwnerMapValues({10012}, PENALTY_BOX_MATCH | IIF_MATCH, iif1);
-    expectUidOwnerMapValues({10013, 10014}, IIF_MATCH, iif1);
-
-    // Removing some PENALTY_BOX_MATCH rules should not change uid interface rule
-    ASSERT_TRUE(isOk(updateUidOwnerMaps({1001, 10012}, PENALTY_BOX_MATCH,
-                                        TrafficController::IptOpDelete)));
-    expectUidOwnerMapValues({1000}, PENALTY_BOX_MATCH, 0);
-    expectUidOwnerMapValues({10012, 10013, 10014}, IIF_MATCH, iif1);
-
-    // Remove all uid interface rules
-    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({10012, 10013, 10014})));
-    expectUidOwnerMapValues({1000}, PENALTY_BOX_MATCH, 0);
-    // Make sure these are the only uids left
-    checkEachUidValue({1000}, PENALTY_BOX_MATCH);
-}
-
-TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithNewMatches) {
-    int iif1 = 56;
-    // Set up existing uid interface rules
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {10001, 10002})));
-    expectUidOwnerMapValues({10001, 10002}, IIF_MATCH, iif1);
-
-    // Add some partially-overlapping doze rules
-    EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_dozable", true, {10002, 10003}));
-    expectUidOwnerMapValues({10001}, IIF_MATCH, iif1);
-    expectUidOwnerMapValues({10002}, DOZABLE_MATCH | IIF_MATCH, iif1);
-    expectUidOwnerMapValues({10003}, DOZABLE_MATCH, 0);
-
-    // Introduce a third rule type (powersave) on various existing UIDs
-    EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_powersave", true, {10000, 10001, 10002, 10003}));
-    expectUidOwnerMapValues({10000}, POWERSAVE_MATCH, 0);
-    expectUidOwnerMapValues({10001}, POWERSAVE_MATCH | IIF_MATCH, iif1);
-    expectUidOwnerMapValues({10002}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif1);
-    expectUidOwnerMapValues({10003}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
-
-    // Remove all doze rules
-    EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_dozable", true, {}));
-    expectUidOwnerMapValues({10000}, POWERSAVE_MATCH, 0);
-    expectUidOwnerMapValues({10001}, POWERSAVE_MATCH | IIF_MATCH, iif1);
-    expectUidOwnerMapValues({10002}, POWERSAVE_MATCH | IIF_MATCH, iif1);
-    expectUidOwnerMapValues({10003}, POWERSAVE_MATCH, 0);
-
-    // Remove all powersave rules, expect ownerMap to only have uid interface rules left
-    EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_powersave", true, {}));
-    expectUidOwnerMapValues({10001, 10002}, IIF_MATCH, iif1);
-    // Make sure these are the only uids left
-    checkEachUidValue({10001, 10002}, IIF_MATCH);
-}
-
-TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRulesWithWildcard) {
-    // iif=0 is a wildcard
-    int iif = 0;
-    // Add interface rule with wildcard to uids
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
-    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
-}
-
-TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRulesWithWildcard) {
-    // iif=0 is a wildcard
-    int iif = 0;
-    // Add interface rule with wildcard to two uids
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
-    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
-
-    // Remove interface rule from one of the uids
-    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
-    expectUidOwnerMapValues({1001}, IIF_MATCH, iif);
-    checkEachUidValue({1001}, IIF_MATCH);
-
-    // Remove interface rule from the remaining uid
-    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001})));
-    expectMapEmpty(mFakeUidOwnerMap);
-}
-
-TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndExistingMatches) {
-    // Set up existing DOZABLE_MATCH and POWERSAVE_MATCH rule
-    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
-                                        TrafficController::IptOpInsert)));
-    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
-                                        TrafficController::IptOpInsert)));
-
-    // iif=0 is a wildcard
-    int iif = 0;
-    // Add interface rule with wildcard to the existing uid
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
-    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
-
-    // Remove interface rule with wildcard from the existing uid
-    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
-    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
-}
-
-TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndNewMatches) {
-    // iif=0 is a wildcard
-    int iif = 0;
-    // Set up existing interface rule with wildcard
-    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
-
-    // Add DOZABLE_MATCH and POWERSAVE_MATCH rule to the existing uid
-    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
-                                        TrafficController::IptOpInsert)));
-    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
-                                        TrafficController::IptOpInsert)));
-    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
-
-    // Remove DOZABLE_MATCH and POWERSAVE_MATCH rule from the existing uid
-    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
-                                        TrafficController::IptOpDelete)));
-    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
-                                        TrafficController::IptOpDelete)));
-    expectUidOwnerMapValues({1000}, IIF_MATCH, iif);
-}
-
-TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
-    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
-    mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, appUids);
-    expectMapEmpty(mFakeUidPermissionMap);
-    expectPrivilegedUserSetEmpty();
-}
-
-TEST_F(TrafficControllerTest, TestRevokeInternetPermission) {
-    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
-    mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
-    expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
-}
-
-TEST_F(TrafficControllerTest, TestPermissionUninstalled) {
-    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
-    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
-    expectUidPermissionMapValues(appUids, INetd::PERMISSION_UPDATE_DEVICE_STATS);
-    expectPrivilegedUserSet(appUids);
-
-    std::vector<uid_t> uidToRemove = {TEST_UID};
-    mTc.setPermissionForUids(INetd::PERMISSION_UNINSTALLED, uidToRemove);
-
-    std::vector<uid_t> uidRemain = {TEST_UID3, TEST_UID2};
-    expectUidPermissionMapValues(uidRemain, INetd::PERMISSION_UPDATE_DEVICE_STATS);
-    expectPrivilegedUserSet(uidRemain);
-
-    mTc.setPermissionForUids(INetd::PERMISSION_UNINSTALLED, uidRemain);
-    expectMapEmpty(mFakeUidPermissionMap);
-    expectPrivilegedUserSetEmpty();
-}
-
-TEST_F(TrafficControllerTest, TestGrantUpdateStatsPermission) {
-    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
-    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
-    expectUidPermissionMapValues(appUids, INetd::PERMISSION_UPDATE_DEVICE_STATS);
-    expectPrivilegedUserSet(appUids);
-
-    mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
-    expectPrivilegedUserSetEmpty();
-    expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
-}
-
-TEST_F(TrafficControllerTest, TestRevokeUpdateStatsPermission) {
-    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
-    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
-    expectPrivilegedUserSet(appUids);
-
-    std::vector<uid_t> uidToRemove = {TEST_UID};
-    mTc.setPermissionForUids(INetd::PERMISSION_NONE, uidToRemove);
-
-    std::vector<uid_t> uidRemain = {TEST_UID3, TEST_UID2};
-    expectPrivilegedUserSet(uidRemain);
-
-    mTc.setPermissionForUids(INetd::PERMISSION_NONE, uidRemain);
-    expectPrivilegedUserSetEmpty();
-}
-
-TEST_F(TrafficControllerTest, TestGrantWrongPermission) {
-    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
-    mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
-    expectPrivilegedUserSetEmpty();
-    expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
-}
-
-TEST_F(TrafficControllerTest, TestGrantDuplicatePermissionSlientlyFail) {
-    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
-    mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, appUids);
-    expectMapEmpty(mFakeUidPermissionMap);
-
-    std::vector<uid_t> uidToAdd = {TEST_UID};
-    mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, uidToAdd);
-
-    expectPrivilegedUserSetEmpty();
-
-    mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
-    expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
-
-    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
-    expectPrivilegedUserSet(appUids);
-
-    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, uidToAdd);
-    expectPrivilegedUserSet(appUids);
-
-    mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
-    expectPrivilegedUserSetEmpty();
-}
-
-TEST_F(TrafficControllerTest, getFirewallType) {
-    static const struct TestConfig {
-        ChildChain childChain;
-        FirewallType firewallType;
-    } testConfigs[] = {
-            // clang-format off
-            {NONE, DENYLIST},
-            {DOZABLE, ALLOWLIST},
-            {STANDBY, DENYLIST},
-            {POWERSAVE, ALLOWLIST},
-            {RESTRICTED, ALLOWLIST},
-            {LOW_POWER_STANDBY, ALLOWLIST},
-            {OEM_DENY_1, DENYLIST},
-            {OEM_DENY_2, DENYLIST},
-            {OEM_DENY_3, DENYLIST},
-            {INVALID_CHAIN, DENYLIST},
-            // clang-format on
-    };
-
-    for (const auto& config : testConfigs) {
-        SCOPED_TRACE(fmt::format("testConfig: [{}, {}]", config.childChain, config.firewallType));
-        EXPECT_EQ(config.firewallType, mTc.getFirewallType(config.childChain));
-    }
-}
-
-constexpr uint32_t SOCK_CLOSE_WAIT_US = 30 * 1000;
-constexpr uint32_t ENOBUFS_POLL_WAIT_US = 10 * 1000;
-
-using android::base::Error;
-using android::base::Result;
-using android::bpf::BpfMap;
-
-// This test set up a SkDestroyListener that is running parallel with the production
-// SkDestroyListener. The test will create thousands of sockets and tag them on the
-// production cookieUidTagMap and close them in a short time. When the number of
-// sockets get closed exceeds the buffer size, it will start to return ENOBUFF
-// error. The error will be ignored by the production SkDestroyListener and the
-// test will clean up the tags in tearDown if there is any remains.
-
-// TODO: Instead of test the ENOBUFF error, we can test the production
-// SkDestroyListener to see if it failed to delete a tagged socket when ENOBUFF
-// triggered.
-class NetlinkListenerTest : public testing::Test {
-  protected:
-    NetlinkListenerTest() {}
-    BpfMap<uint64_t, UidTagValue> mCookieTagMap;
-
-    void SetUp() {
-        mCookieTagMap.init(COOKIE_TAG_MAP_PATH);
-        ASSERT_TRUE(mCookieTagMap.isValid());
-    }
-
-    void TearDown() {
-        const auto deleteTestCookieEntries = [](const uint64_t& key, const UidTagValue& value,
-                                                BpfMap<uint64_t, UidTagValue>& map) {
-            if ((value.uid == TEST_UID) && (value.tag == TEST_TAG)) {
-                Result<void> res = map.deleteValue(key);
-                if (res.ok() || (res.error().code() == ENOENT)) {
-                    return Result<void>();
-                }
-                ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s", key,
-                      strerror(res.error().code()));
-            }
-            // Move forward to next cookie in the map.
-            return Result<void>();
-        };
-        EXPECT_RESULT_OK(mCookieTagMap.iterateWithValue(deleteTestCookieEntries));
-    }
-
-    Result<void> checkNoGarbageTagsExist() {
-        const auto checkGarbageTags = [](const uint64_t&, const UidTagValue& value,
-                                         const BpfMap<uint64_t, UidTagValue>&) -> Result<void> {
-            if ((TEST_UID == value.uid) && (TEST_TAG == value.tag)) {
-                return Error(EUCLEAN) << "Closed socket is not untagged";
-            }
-            return {};
-        };
-        return mCookieTagMap.iterateWithValue(checkGarbageTags);
-    }
-
-    bool checkMassiveSocketDestroy(int totalNumber, bool expectError) {
-        std::unique_ptr<android::netdutils::NetlinkListenerInterface> skDestroyListener;
-        auto result = android::net::TrafficController::makeSkDestroyListener();
-        if (!isOk(result)) {
-            ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
-        } else {
-            skDestroyListener = std::move(result.value());
-        }
-        int rxErrorCount = 0;
-        // Rx handler extracts nfgenmsg looks up and invokes registered dispatch function.
-        const auto rxErrorHandler = [&rxErrorCount](const int, const int) { rxErrorCount++; };
-        skDestroyListener->registerSkErrorHandler(rxErrorHandler);
-        int fds[totalNumber];
-        for (int i = 0; i < totalNumber; i++) {
-            fds[i] = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
-            // The likely reason for a failure is running out of available file descriptors.
-            EXPECT_LE(0, fds[i]) << i << " of " << totalNumber;
-            if (fds[i] < 0) {
-                // EXPECT_LE already failed above, so test case is a failure, but we don't
-                // want potentially tens of thousands of extra failures creating and then
-                // closing all these fds cluttering up the logs.
-                totalNumber = i;
-                break;
-            };
-            libnetd_updatable_tagSocket(fds[i], TEST_TAG, TEST_UID, 1000);
-        }
-
-        // TODO: Use a separate thread that has its own fd table so we can
-        // close sockets even faster simply by terminating that thread.
-        for (int i = 0; i < totalNumber; i++) {
-            EXPECT_EQ(0, close(fds[i]));
-        }
-        // wait a bit for netlink listener to handle all the messages.
-        usleep(SOCK_CLOSE_WAIT_US);
-        if (expectError) {
-            // If ENOBUFS triggered, check it only called into the handler once, ie.
-            // that the netlink handler is not spinning.
-            int currentErrorCount = rxErrorCount;
-            // 0 error count is acceptable because the system has chances to close all sockets
-            // normally.
-            EXPECT_LE(0, rxErrorCount);
-            if (!rxErrorCount) return true;
-
-            usleep(ENOBUFS_POLL_WAIT_US);
-            EXPECT_EQ(currentErrorCount, rxErrorCount);
-        } else {
-            EXPECT_RESULT_OK(checkNoGarbageTagsExist());
-            EXPECT_EQ(0, rxErrorCount);
-        }
-        return false;
-    }
-};
-
-TEST_F(NetlinkListenerTest, TestAllSocketUntagged) {
-    checkMassiveSocketDestroy(10, false);
-    checkMassiveSocketDestroy(100, false);
-}
-
-// Disabled because flaky on blueline-userdebug; this test relies on the main thread
-// winning a race against the NetlinkListener::run() thread. There's no way to ensure
-// things will be scheduled the same way across all architectures and test environments.
-TEST_F(NetlinkListenerTest, DISABLED_TestSkDestroyError) {
-    bool needRetry = false;
-    int retryCount = 0;
-    do {
-        needRetry = checkMassiveSocketDestroy(32500, true);
-        if (needRetry) retryCount++;
-    } while (needRetry && retryCount < 3);
-    // Should review test if it can always close all sockets correctly.
-    EXPECT_GT(3, retryCount);
-}
-
-
-}  // namespace net
-}  // namespace android
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
deleted file mode 100644
index 03f449a..0000000
--- a/service/native/include/Common.h
+++ /dev/null
@@ -1,46 +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.
- */
-
-#pragma once
-// TODO: deduplicate with the constants in NetdConstants.h.
-#include <aidl/android/net/INetd.h>
-#include "clat_mark.h"
-
-using aidl::android::net::INetd;
-
-static_assert(INetd::CLAT_MARK == CLAT_MARK, "must be 0xDEADC1A7");
-
-enum FirewallRule { ALLOW = INetd::FIREWALL_RULE_ALLOW, DENY = INetd::FIREWALL_RULE_DENY };
-
-// ALLOWLIST means the firewall denies all by default, uids must be explicitly ALLOWed
-// DENYLIST means the firewall allows all by default, uids must be explicitly DENYed
-
-enum FirewallType { ALLOWLIST = INetd::FIREWALL_ALLOWLIST, DENYLIST = INetd::FIREWALL_DENYLIST };
-
-// LINT.IfChange(firewall_chain)
-enum ChildChain {
-    NONE = 0,
-    DOZABLE = 1,
-    STANDBY = 2,
-    POWERSAVE = 3,
-    RESTRICTED = 4,
-    LOW_POWER_STANDBY = 5,
-    OEM_DENY_1 = 7,
-    OEM_DENY_2 = 8,
-    OEM_DENY_3 = 9,
-    INVALID_CHAIN
-};
-// LINT.ThenChange(packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java)
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
deleted file mode 100644
index cb6c836..0000000
--- a/service/native/include/TrafficController.h
+++ /dev/null
@@ -1,190 +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.
- */
-
-#pragma once
-
-#include <set>
-#include <Common.h>
-
-#include "android-base/thread_annotations.h"
-#include "bpf/BpfMap.h"
-#include "netd.h"
-#include "netdutils/DumpWriter.h"
-#include "netdutils/NetlinkListener.h"
-#include "netdutils/StatusOr.h"
-
-namespace android {
-namespace net {
-
-using netdutils::StatusOr;
-
-class TrafficController {
-  public:
-    static constexpr char DUMP_KEYWORD[] = "trafficcontroller";
-
-    /*
-     * Initialize the whole controller
-     */
-    netdutils::Status start(bool startSkDestroyListener);
-
-    /*
-     * Swap the stats map config from current active stats map to the idle one.
-     */
-    netdutils::Status swapActiveStatsMap() EXCLUDES(mMutex);
-
-    int changeUidOwnerRule(ChildChain chain, const uid_t uid, FirewallRule rule, FirewallType type);
-
-    int removeUidOwnerRule(const uid_t uid);
-
-    int replaceUidOwnerMap(const std::string& name, bool isAllowlist,
-                           const std::vector<int32_t>& uids);
-
-    enum IptOp { IptOpInsert, IptOpDelete };
-
-    netdutils::Status updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
-                                          FirewallType type) EXCLUDES(mMutex);
-
-    void dump(int fd, bool verbose) EXCLUDES(mMutex);
-
-    netdutils::Status replaceRulesInMap(UidOwnerMatchType match, const std::vector<int32_t>& uids)
-            EXCLUDES(mMutex);
-
-    netdutils::Status addUidInterfaceRules(const int ifIndex, const std::vector<int32_t>& uids)
-            EXCLUDES(mMutex);
-    netdutils::Status removeUidInterfaceRules(const std::vector<int32_t>& uids) EXCLUDES(mMutex);
-
-    netdutils::Status updateUidLockdownRule(const uid_t uid, const bool add) EXCLUDES(mMutex);
-
-    netdutils::Status updateUidOwnerMap(const uint32_t uid,
-                                        UidOwnerMatchType matchType, IptOp op) EXCLUDES(mMutex);
-
-    int toggleUidOwnerMap(ChildChain chain, bool enable) EXCLUDES(mMutex);
-
-    static netdutils::StatusOr<std::unique_ptr<netdutils::NetlinkListenerInterface>>
-    makeSkDestroyListener();
-
-    void setPermissionForUids(int permission, const std::vector<uid_t>& uids) EXCLUDES(mMutex);
-
-    FirewallType getFirewallType(ChildChain);
-
-    static const char* LOCAL_DOZABLE;
-    static const char* LOCAL_STANDBY;
-    static const char* LOCAL_POWERSAVE;
-    static const char* LOCAL_RESTRICTED;
-    static const char* LOCAL_LOW_POWER_STANDBY;
-    static const char* LOCAL_OEM_DENY_1;
-    static const char* LOCAL_OEM_DENY_2;
-    static const char* LOCAL_OEM_DENY_3;
-
-  private:
-    /*
-     * mCookieTagMap: Store the corresponding tag and uid for a specific socket.
-     * DO NOT hold any locks when modifying this map, otherwise when the untag
-     * operation is waiting for a lock hold by other process and there are more
-     * sockets being closed than can fit in the socket buffer of the netlink socket
-     * that receives them, then the kernel will drop some of these sockets and we
-     * won't delete their tags.
-     * Map Key: uint64_t socket cookie
-     * Map Value: UidTagValue, contains a uint32 uid and a uint32 tag.
-     */
-    bpf::BpfMap<uint64_t, UidTagValue> mCookieTagMap GUARDED_BY(mMutex);
-
-    /*
-     * mUidCounterSetMap: Store the counterSet of a specific uid.
-     * Map Key: uint32 uid.
-     * Map Value: uint32 counterSet specifies if the traffic is a background
-     * or foreground traffic.
-     */
-    bpf::BpfMap<uint32_t, uint8_t> mUidCounterSetMap GUARDED_BY(mMutex);
-
-    /*
-     * mAppUidStatsMap: Store the total traffic stats for a uid regardless of
-     * tag, counterSet and iface. The stats is used by TrafficStats.getUidStats
-     * API to return persistent stats for a specific uid since device boot.
-     */
-    bpf::BpfMap<uint32_t, StatsValue> mAppUidStatsMap;
-
-    /*
-     * mStatsMapA/mStatsMapB: Store the traffic statistics for a specific
-     * combination of uid, tag, iface and counterSet. These two maps contain
-     * both tagged and untagged traffic.
-     * Map Key: StatsKey contains the uid, tag, counterSet and ifaceIndex
-     * information.
-     * Map Value: Stats, contains packet count and byte count of each
-     * transport protocol on egress and ingress direction.
-     */
-    bpf::BpfMap<StatsKey, StatsValue> mStatsMapA GUARDED_BY(mMutex);
-
-    bpf::BpfMap<StatsKey, StatsValue> mStatsMapB GUARDED_BY(mMutex);
-
-    /*
-     * mIfaceIndexNameMap: Store the index name pair of each interface show up
-     * on the device since boot. The interface index is used by the eBPF program
-     * to correctly match the iface name when receiving a packet.
-     */
-    bpf::BpfMap<uint32_t, IfaceValue> mIfaceIndexNameMap;
-
-    /*
-     * mIfaceStataMap: Store per iface traffic stats gathered from xt_bpf
-     * filter.
-     */
-    bpf::BpfMap<uint32_t, StatsValue> mIfaceStatsMap;
-
-    /*
-     * mConfigurationMap: Store the current network policy about uid filtering
-     * and the current stats map in use. There are two configuration entries in
-     * the map right now:
-     * - Entry with UID_RULES_CONFIGURATION_KEY:
-     *    Store the configuration for the current uid rules. It indicates the device
-     *    is in doze/powersave/standby/restricted/low power standby/oem deny mode.
-     * - Entry with CURRENT_STATS_MAP_CONFIGURATION_KEY:
-     *    Stores the current live stats map that kernel program is writing to.
-     *    Userspace can do scraping and cleaning job on the other one depending on the
-     *    current configs.
-     */
-    bpf::BpfMap<uint32_t, uint32_t> mConfigurationMap GUARDED_BY(mMutex);
-
-    /*
-     * mUidOwnerMap: Store uids that are used for bandwidth control uid match.
-     */
-    bpf::BpfMap<uint32_t, UidOwnerValue> mUidOwnerMap GUARDED_BY(mMutex);
-
-    /*
-     * mUidOwnerMap: Store uids that are used for INTERNET permission check.
-     */
-    bpf::BpfMap<uint32_t, uint8_t> mUidPermissionMap GUARDED_BY(mMutex);
-
-    std::unique_ptr<netdutils::NetlinkListenerInterface> mSkDestroyListener;
-
-    netdutils::Status removeRule(uint32_t uid, UidOwnerMatchType match) REQUIRES(mMutex);
-
-    netdutils::Status addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif = 0)
-            REQUIRES(mMutex);
-
-    std::mutex mMutex;
-
-    netdutils::Status initMaps() EXCLUDES(mMutex);
-
-    // Keep track of uids that have permission UPDATE_DEVICE_STATS so we don't
-    // need to call back to system server for permission check.
-    std::set<uid_t> mPrivilegedUser GUARDED_BY(mMutex);
-
-    // For testing
-    friend class TrafficControllerTest;
-};
-
-}  // namespace net
-}  // namespace android
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 996706e..6c1c2c4 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -38,16 +39,23 @@
 cc_test {
     name: "libclat_test",
     defaults: ["netd_defaults"],
-    test_suites: ["general-tests", "mts-tethering"],
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
     test_config_template: ":net_native_test_config_template",
     srcs: [
         "clatutils_test.cpp",
     ],
+    header_libs: [
+        "bpf_connectivity_headers",
+    ],
     static_libs: [
         "libbase",
         "libclat",
         "libip_checksum",
         "libnetd_test_tun_interface",
+        "netd_aidl_interface-lateststable-ndk",
     ],
     shared_libs: [
         "liblog",
diff --git a/service/native/libs/libclat/clatutils_test.cpp b/service/native/libs/libclat/clatutils_test.cpp
index f4f97db..cf6492f 100644
--- a/service/native/libs/libclat/clatutils_test.cpp
+++ b/service/native/libs/libclat/clatutils_test.cpp
@@ -26,6 +26,10 @@
 #include "checksum.h"
 }
 
+#include <aidl/android/net/INetd.h>
+#include "clat_mark.h"
+static_assert(aidl::android::net::INetd::CLAT_MARK == CLAT_MARK, "must be 0xDEADC1A7");
+
 // Default translation parameters.
 static const char kIPv4LocalAddr[] = "192.0.0.4";
 
diff --git a/service/proguard.flags b/service/proguard.flags
index cf25f05..ed9a65f 100644
--- a/service/proguard.flags
+++ b/service/proguard.flags
@@ -15,3 +15,7 @@
     static final % EVENT_*;
 }
 
+# b/313539492 Keep the onLocalNetworkInfoChanged method in classes extending Connectivity.NetworkCallback.
+-keepclassmembers class * extends **android.net.ConnectivityManager$NetworkCallback {
+    public void onLocalNetworkInfoChanged(**android.net.Network, **android.net.LocalNetworkInfo);
+}
diff --git a/service/src/com/android/metrics/ConnectivitySampleMetricsHelper.java b/service/src/com/android/metrics/ConnectivitySampleMetricsHelper.java
new file mode 100644
index 0000000..93d1d5d
--- /dev/null
+++ b/service/src/com/android/metrics/ConnectivitySampleMetricsHelper.java
@@ -0,0 +1,74 @@
+/*
+ * 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.metrics;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import android.util.StatsEvent;
+
+import com.android.modules.utils.HandlerExecutor;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * A class to register, sample and send connectivity state metrics.
+ */
+public class ConnectivitySampleMetricsHelper implements StatsManager.StatsPullAtomCallback {
+    private static final String TAG = ConnectivitySampleMetricsHelper.class.getSimpleName();
+
+    final Supplier<StatsEvent> mDelegate;
+
+    /**
+     * Start collecting metrics.
+     * @param context some context to get services
+     * @param connectivityServiceHandler the connectivity service handler
+     * @param atomTag the tag to collect metrics from
+     * @param delegate a method returning data when called on the handler thread
+     */
+    // Unfortunately it seems essentially impossible to unit test this method. The only thing
+    // to test is that there is a call to setPullAtomCallback, but StatsManager is final and
+    // can't be mocked without mockito-extended. Using mockito-extended in FrameworksNetTests
+    // would have a very large impact on performance, while splitting the unit test for this
+    // class in a separate target would make testing very hard to manage. Therefore, there
+    // can unfortunately be no unit tests for this method, but at least it is very simple.
+    public static void start(@NonNull final Context context,
+            @NonNull final Handler connectivityServiceHandler,
+            final int atomTag,
+            @NonNull final Supplier<StatsEvent> delegate) {
+        final ConnectivitySampleMetricsHelper metrics =
+                new ConnectivitySampleMetricsHelper(delegate);
+        final StatsManager mgr = context.getSystemService(StatsManager.class);
+        if (null == mgr) return; // No metrics for you
+        mgr.setPullAtomCallback(atomTag, null /* metadata */,
+                new HandlerExecutor(connectivityServiceHandler), metrics);
+    }
+
+    public ConnectivitySampleMetricsHelper(@NonNull final Supplier<StatsEvent> delegate) {
+        mDelegate = delegate;
+    }
+
+    @Override
+    public int onPullAtom(final int atomTag, final List<StatsEvent> data) {
+        Log.d(TAG, "Sampling data for atom : " + atomTag);
+        data.add(mDelegate.get());
+        return StatsManager.PULL_SUCCESS;
+    }
+}
diff --git a/service/src/com/android/metrics/stats.proto b/service/src/com/android/metrics/stats.proto
index 006d20a..ecc0377 100644
--- a/service/src/com/android/metrics/stats.proto
+++ b/service/src/com/android/metrics/stats.proto
@@ -61,6 +61,21 @@
 
   // Record query service count before unregistered service
   optional int32 replied_requests_count = 11;
+
+  // Record sent query count before stopped discovery
+  optional int32 sent_query_count = 12;
+
+  // Record sent packet count before unregistered service
+  optional int32 sent_packet_count = 13;
+
+  // Record number of conflict during probing
+  optional int32 conflict_during_probing_count = 14;
+
+  // Record number of conflict after probing
+  optional int32 conflict_after_probing_count = 15;
+
+  // The random number between 0 ~ 999 for sampling
+  optional int32 random_number = 16;
 }
 
 /**
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index ec168dd..44868b2d 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -16,14 +16,24 @@
 
 package com.android.server;
 
-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_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.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
+import static android.net.BpfNetMapsConstants.COOKIE_TAG_MAP_PATH;
+import static android.net.BpfNetMapsConstants.CURRENT_STATS_MAP_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED;
+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.IIF_MATCH;
+import static android.net.BpfNetMapsConstants.INGRESS_DISCARD_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_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;
@@ -39,10 +49,13 @@
 
 import android.app.StatsManager;
 import android.content.Context;
+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.provider.DeviceConfig;
+import android.os.UserHandle;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.ArraySet;
@@ -51,22 +64,27 @@
 import android.util.Pair;
 import android.util.StatsEvent;
 
+import androidx.annotation.RequiresApi;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.BackgroundThread;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.BpfMap;
-import com.android.net.module.util.DeviceConfigUtils;
 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;
 import com.android.net.module.util.Struct.U8;
 import com.android.net.module.util.bpf.CookieTagMapKey;
 import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.net.module.util.bpf.IngressDiscardKey;
+import com.android.net.module.util.bpf.IngressDiscardValue;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.net.InetAddress;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
@@ -78,9 +96,8 @@
  * {@hide}
  */
 public class BpfNetMaps {
-    private static final boolean PRE_T = !SdkLevel.isAtLeastT();
     static {
-        if (!PRE_T) {
+        if (SdkLevel.isAtLeastT()) {
             System.loadLibrary("service-connectivity");
         }
     }
@@ -91,10 +108,6 @@
     // Use legacy netd for releases before T.
     private static boolean sInitialized = false;
 
-    private static Boolean sEnableJavaBpfMap = null;
-    private static final String BPF_NET_MAPS_ENABLE_JAVA_BPF_MAP =
-            "bpf_net_maps_enable_java_bpf_map";
-
     // Lock for sConfigurationMap entry for UID_RULES_CONFIGURATION_KEY.
     // This entry is not accessed by others.
     // BpfNetMaps acquires this lock while sequence of read, modify, and write.
@@ -105,16 +118,6 @@
     // BpfNetMaps is an only writer of this entry.
     private static final Object sCurrentStatsMapConfigLock = new Object();
 
-    private static final String CONFIGURATION_MAP_PATH =
-            "/sys/fs/bpf/netd_shared/map_netd_configuration_map";
-    private static final String UID_OWNER_MAP_PATH =
-            "/sys/fs/bpf/netd_shared/map_netd_uid_owner_map";
-    private static final String UID_PERMISSION_MAP_PATH =
-            "/sys/fs/bpf/netd_shared/map_netd_uid_permission_map";
-    private static final String COOKIE_TAG_MAP_PATH =
-            "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
-    private static final S32 UID_RULES_CONFIGURATION_KEY = new S32(0);
-    private static final S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new S32(1);
     private static final long UID_RULES_DEFAULT_CONFIGURATION = 0;
     private static final long STATS_SELECT_MAP_A = 0;
     private static final long STATS_SELECT_MAP_B = 1;
@@ -124,49 +127,14 @@
     private static IBpfMap<S32, UidOwnerValue> sUidOwnerMap = null;
     private static IBpfMap<S32, U8> sUidPermissionMap = null;
     private static IBpfMap<CookieTagMapKey, CookieTagMapValue> sCookieTagMap = null;
-
-    // LINT.IfChange(match_type)
-    @VisibleForTesting public static final long NO_MATCH = 0;
-    @VisibleForTesting public static final long HAPPY_BOX_MATCH = (1 << 0);
-    @VisibleForTesting public static final long PENALTY_BOX_MATCH = (1 << 1);
-    @VisibleForTesting public static final long DOZABLE_MATCH = (1 << 2);
-    @VisibleForTesting public static final long STANDBY_MATCH = (1 << 3);
-    @VisibleForTesting public static final long POWERSAVE_MATCH = (1 << 4);
-    @VisibleForTesting public static final long RESTRICTED_MATCH = (1 << 5);
-    @VisibleForTesting public static final long LOW_POWER_STANDBY_MATCH = (1 << 6);
-    @VisibleForTesting public static final long IIF_MATCH = (1 << 7);
-    @VisibleForTesting public static final long LOCKDOWN_VPN_MATCH = (1 << 8);
-    @VisibleForTesting public static final long OEM_DENY_1_MATCH = (1 << 9);
-    @VisibleForTesting public static final long OEM_DENY_2_MATCH = (1 << 10);
-    @VisibleForTesting public static final long OEM_DENY_3_MATCH = (1 << 11);
-    // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/netd.h)
+    // TODO: Add BOOL class and replace U8?
+    private static IBpfMap<S32, U8> sDataSaverEnabledMap = null;
+    private static IBpfMap<IngressDiscardKey, IngressDiscardValue> sIngressDiscardMap = null;
 
     private static final List<Pair<Integer, String>> PERMISSION_LIST = Arrays.asList(
             Pair.create(PERMISSION_INTERNET, "PERMISSION_INTERNET"),
             Pair.create(PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
     );
-    private 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(DOZABLE_MATCH, "DOZABLE_MATCH"),
-            Pair.create(STANDBY_MATCH, "STANDBY_MATCH"),
-            Pair.create(POWERSAVE_MATCH, "POWERSAVE_MATCH"),
-            Pair.create(RESTRICTED_MATCH, "RESTRICTED_MATCH"),
-            Pair.create(LOW_POWER_STANDBY_MATCH, "LOW_POWER_STANDBY_MATCH"),
-            Pair.create(IIF_MATCH, "IIF_MATCH"),
-            Pair.create(LOCKDOWN_VPN_MATCH, "LOCKDOWN_VPN_MATCH"),
-            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")
-    );
-
-    /**
-     * Set sEnableJavaBpfMap for test.
-     */
-    @VisibleForTesting
-    public static void setEnableJavaBpfMapForTest(boolean enable) {
-        sEnableJavaBpfMap = enable;
-    }
 
     /**
      * Set configurationMap for test.
@@ -201,42 +169,85 @@
         sCookieTagMap = cookieTagMap;
     }
 
+    /**
+     * Set dataSaverEnabledMap for test.
+     */
+    @VisibleForTesting
+    public static void setDataSaverEnabledMapForTest(IBpfMap<S32, U8> dataSaverEnabledMap) {
+        sDataSaverEnabledMap = dataSaverEnabledMap;
+    }
+
+    /**
+     * Set ingressDiscardMap for test.
+     */
+    @VisibleForTesting
+    public static void setIngressDiscardMapForTest(
+            IBpfMap<IngressDiscardKey, IngressDiscardValue> ingressDiscardMap) {
+        sIngressDiscardMap = ingressDiscardMap;
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U32> getConfigurationMap() {
         try {
-            return new BpfMap<>(
-                    CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U32.class);
+            return SingleWriterBpfMap.getSingleton(
+                    CONFIGURATION_MAP_PATH, S32.class, U32.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open netd configuration map", e);
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
         try {
-            return new BpfMap<>(
-                    UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, UidOwnerValue.class);
+            return SingleWriterBpfMap.getSingleton(
+                    UID_OWNER_MAP_PATH, S32.class, UidOwnerValue.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open uid owner map", e);
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U8> getUidPermissionMap() {
         try {
-            return new BpfMap<>(
-                    UID_PERMISSION_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U8.class);
+            return SingleWriterBpfMap.getSingleton(
+                    UID_PERMISSION_MAP_PATH, S32.class, U8.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open uid permission map", e);
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
         try {
-            return new BpfMap<>(COOKIE_TAG_MAP_PATH, BpfMap.BPF_F_RDWR,
+            // 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) {
             throw new IllegalStateException("Cannot open cookie tag map", e);
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private static IBpfMap<S32, U8> getDataSaverEnabledMap() {
+        try {
+            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);
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private static IBpfMap<IngressDiscardKey, IngressDiscardValue> getIngressDiscardMap() {
+        try {
+            return SingleWriterBpfMap.getSingleton(INGRESS_DISCARD_MAP_PATH,
+                    IngressDiscardKey.class, IngressDiscardValue.class);
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Cannot open ingress discard map", e);
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static void initBpfMaps() {
         if (sConfigurationMap == null) {
             sConfigurationMap = getConfigurationMap();
@@ -270,31 +281,37 @@
         if (sCookieTagMap == null) {
             sCookieTagMap = getCookieTagMap();
         }
+
+        if (sDataSaverEnabledMap == null) {
+            sDataSaverEnabledMap = getDataSaverEnabledMap();
+        }
+        try {
+            sDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Failed to initialize data saver configuration", e);
+        }
+
+        if (sIngressDiscardMap == null) {
+            sIngressDiscardMap = getIngressDiscardMap();
+        }
+        try {
+            sIngressDiscardMap.clear();
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Failed to initialize ingress discard map", e);
+        }
     }
 
     /**
      * Initializes the class if it is not already initialized. This method will open maps but not
      * cause any other effects. This method may be called multiple times on any thread.
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static synchronized void ensureInitialized(final Context context) {
         if (sInitialized) return;
-        if (sEnableJavaBpfMap == null) {
-            sEnableJavaBpfMap = SdkLevel.isAtLeastU() ||
-                    DeviceConfigUtils.isFeatureEnabled(context,
-                            DeviceConfig.NAMESPACE_TETHERING, BPF_NET_MAPS_ENABLE_JAVA_BPF_MAP,
-                            DeviceConfigUtils.TETHERING_MODULE_NAME, false /* defaultValue */);
-        }
-        Log.d(TAG, "BpfNetMaps is initialized with sEnableJavaBpfMap=" + sEnableJavaBpfMap);
-
         initBpfMaps();
-        native_init(!sEnableJavaBpfMap /* startSkDestroyListener */);
         sInitialized = true;
     }
 
-    public boolean isSkDestroyListenerRunning() {
-        return !sEnableJavaBpfMap;
-    }
-
     /**
      * Dependencies of BpfNetMaps, for injection in tests.
      */
@@ -308,10 +325,23 @@
         }
 
         /**
-         * Call synchronize_rcu()
+         * Get interface name
          */
+        public String getIfName(final int ifIndex) {
+            return Os.if_indextoname(ifIndex);
+        }
+
+        /**
+         * Synchronously call in to kernel to synchronize_rcu()
+         */
+        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
         public int synchronizeKernelRCU() {
-            return native_synchronizeKernelRCU();
+            try {
+                BpfMap.synchronizeKernelRCU();
+            } catch (ErrnoException e) {
+                return -e.errno;
+            }
+            return 0;
         }
 
         /**
@@ -322,20 +352,14 @@
             return ConnectivityStatsLog.buildStatsEvent(NETWORK_BPF_MAP_INFO, cookieTagMapSize,
                     uidOwnerMapSize, uidPermissionMapSize);
         }
-
-        /**
-         * Call native_dump
-         */
-        public void nativeDump(final FileDescriptor fd, final boolean verbose) {
-            native_dump(fd, verbose);
-        }
     }
 
     /** Constructor used after T that doesn't need to use netd anymore. */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public BpfNetMaps(final Context context) {
         this(context, null);
 
-        if (PRE_T) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
+        if (!SdkLevel.isAtLeastT()) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
     }
 
     public BpfNetMaps(final Context context, final INetd netd) {
@@ -344,63 +368,13 @@
 
     @VisibleForTesting
     public BpfNetMaps(final Context context, final INetd netd, final Dependencies deps) {
-        if (!PRE_T) {
+        if (SdkLevel.isAtLeastT()) {
             ensureInitialized(context);
         }
         mNetd = netd;
         mDeps = deps;
     }
 
-    /**
-     * Get corresponding match from firewall chain.
-     */
-    @VisibleForTesting
-    public long getMatchByFirewallChain(final int chain) {
-        switch (chain) {
-            case FIREWALL_CHAIN_DOZABLE:
-                return DOZABLE_MATCH;
-            case FIREWALL_CHAIN_STANDBY:
-                return STANDBY_MATCH;
-            case FIREWALL_CHAIN_POWERSAVE:
-                return POWERSAVE_MATCH;
-            case FIREWALL_CHAIN_RESTRICTED:
-                return RESTRICTED_MATCH;
-            case FIREWALL_CHAIN_LOW_POWER_STANDBY:
-                return LOW_POWER_STANDBY_MATCH;
-            case FIREWALL_CHAIN_OEM_DENY_1:
-                return OEM_DENY_1_MATCH;
-            case FIREWALL_CHAIN_OEM_DENY_2:
-                return OEM_DENY_2_MATCH;
-            case FIREWALL_CHAIN_OEM_DENY_3:
-                return OEM_DENY_3_MATCH;
-            default:
-                throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
-        }
-    }
-
-    /**
-     * Get if the chain is allow list or not.
-     *
-     * ALLOWLIST means the firewall denies all by default, uids must be explicitly allowed
-     * DENYLIST means the firewall allows all by default, uids must be explicitly denyed
-     */
-    public boolean isFirewallAllowList(final int chain) {
-        switch (chain) {
-            case FIREWALL_CHAIN_DOZABLE:
-            case FIREWALL_CHAIN_POWERSAVE:
-            case FIREWALL_CHAIN_RESTRICTED:
-            case FIREWALL_CHAIN_LOW_POWER_STANDBY:
-                return true;
-            case FIREWALL_CHAIN_STANDBY:
-            case FIREWALL_CHAIN_OEM_DENY_1:
-            case FIREWALL_CHAIN_OEM_DENY_2:
-            case FIREWALL_CHAIN_OEM_DENY_3:
-                return false;
-            default:
-                throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
-        }
-    }
-
     private void maybeThrow(final int err, final String msg) {
         if (err != 0) {
             throw new ServiceSpecificException(err, msg + ": " + Os.strerror(err));
@@ -408,7 +382,7 @@
     }
 
     private void throwIfPreT(final String msg) {
-        if (PRE_T) {
+        if (!SdkLevel.isAtLeastT()) {
             throw new UnsupportedOperationException(msg);
         }
     }
@@ -475,78 +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.
-     */
-    public void addNaughtyApp(final int uid) {
-        throwIfPreT("addNaughtyApp is not available on pre-T devices");
-
-        if (sEnableJavaBpfMap) {
-            addRule(uid, PENALTY_BOX_MATCH, "addNaughtyApp");
-        } else {
-            final int err = native_addNaughtyApp(uid);
-            maybeThrow(err, "Unable to add naughty app");
-        }
-    }
-
-    /**
-     * 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.
-     */
-    public void removeNaughtyApp(final int uid) {
-        throwIfPreT("removeNaughtyApp is not available on pre-T devices");
-
-        if (sEnableJavaBpfMap) {
-            removeRule(uid, PENALTY_BOX_MATCH, "removeNaughtyApp");
-        } else {
-            final int err = native_removeNaughtyApp(uid);
-            maybeThrow(err, "Unable to remove naughty app");
-        }
-    }
-
-    /**
-     * 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.
-     */
-    public void addNiceApp(final int uid) {
-        throwIfPreT("addNiceApp is not available on pre-T devices");
-
-        if (sEnableJavaBpfMap) {
-            addRule(uid, HAPPY_BOX_MATCH, "addNiceApp");
-        } else {
-            final int err = native_addNiceApp(uid);
-            maybeThrow(err, "Unable to add nice app");
-        }
-    }
-
-    /**
-     * 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.
-     */
-    public void removeNiceApp(final int uid) {
-        throwIfPreT("removeNiceApp is not available on pre-T devices");
-
-        if (sEnableJavaBpfMap) {
-            removeRule(uid, HAPPY_BOX_MATCH, "removeNiceApp");
-        } else {
-            final int err = native_removeNiceApp(uid);
-            maybeThrow(err, "Unable to remove nice app");
-        }
-    }
-
-    /**
      * Set target firewall child chain
      *
      * @param childChain target chain to enable
@@ -555,24 +457,20 @@
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public void setChildChain(final int childChain, final boolean enable) {
         throwIfPreT("setChildChain is not available on pre-T devices");
 
-        if (sEnableJavaBpfMap) {
-            final long match = getMatchByFirewallChain(childChain);
-            try {
-                synchronized (sUidRulesConfigBpfMapLock) {
-                    final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
-                    final long newConfig = enable ? (config.val | match) : (config.val & ~match);
-                    sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(newConfig));
-                }
-            } catch (ErrnoException e) {
-                throw new ServiceSpecificException(e.errno,
-                        "Unable to set child chain: " + Os.strerror(e.errno));
+        final long match = getMatchByFirewallChain(childChain);
+        try {
+            synchronized (sUidRulesConfigBpfMapLock) {
+                final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+                final long newConfig = enable ? (config.val | match) : (config.val & ~match);
+                sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(newConfig));
             }
-        } else {
-            final int err = native_setChildChain(childChain, enable);
-            maybeThrow(err, "Unable to set child chain");
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    "Unable to set child chain: " + Os.strerror(e.errno));
         }
     }
 
@@ -585,22 +483,15 @@
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
+    @Deprecated
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public boolean isChainEnabled(final int childChain) {
-        throwIfPreT("isChainEnabled is not available on pre-T devices");
-
-        final long match = getMatchByFirewallChain(childChain);
-        try {
-            final U32 config = sConfigurationMap.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));
-        }
+        return BpfNetMapsUtils.isChainEnabled(sConfigurationMap, childChain);
     }
 
     private Set<Integer> asSet(final int[] uids) {
         final Set<Integer> uidSet = new ArraySet<>();
-        for (final int uid: uids) {
+        for (final int uid : uids) {
             uidSet.add(uid);
         }
         return uidSet;
@@ -615,78 +506,42 @@
      * @throws UnsupportedOperationException if called on pre-T devices.
      * @throws IllegalArgumentException if {@code chain} is not a valid chain.
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public void replaceUidChain(final int chain, final int[] uids) {
         throwIfPreT("replaceUidChain is not available on pre-T devices");
 
-        if (sEnableJavaBpfMap) {
-            final long match;
-            try {
-                match = getMatchByFirewallChain(chain);
-            } catch (ServiceSpecificException e) {
-                // Throws IllegalArgumentException to keep the behavior of
-                // ConnectivityManager#replaceFirewallChain API
-                throw new IllegalArgumentException("Invalid firewall chain: " + chain);
-            }
-            final Set<Integer> uidSet = asSet(uids);
-            final Set<Integer> uidSetToRemoveRule = new ArraySet<>();
-            try {
-                synchronized (sUidOwnerMap) {
-                    sUidOwnerMap.forEach((uid, config) -> {
-                        // config could be null if there is a concurrent entry deletion.
-                        // http://b/220084230. But sUidOwnerMap update must be done while holding a
-                        // lock, so this should not happen.
-                        if (config == null) {
-                            Log.wtf(TAG, "sUidOwnerMap entry was deleted while holding a lock");
-                        } else if (!uidSet.contains((int) uid.val) && (config.rule & match) != 0) {
-                            uidSetToRemoveRule.add((int) uid.val);
-                        }
-                    });
+        final long match;
+        try {
+            match = getMatchByFirewallChain(chain);
+        } catch (ServiceSpecificException e) {
+            // Throws IllegalArgumentException to keep the behavior of
+            // ConnectivityManager#replaceFirewallChain API
+            throw new IllegalArgumentException("Invalid firewall chain: " + chain);
+        }
+        final Set<Integer> uidSet = asSet(uids);
+        final Set<Integer> uidSetToRemoveRule = new ArraySet<>();
+        try {
+            synchronized (sUidOwnerMap) {
+                sUidOwnerMap.forEach((uid, config) -> {
+                    // config could be null if there is a concurrent entry deletion.
+                    // http://b/220084230. But sUidOwnerMap update must be done while holding a
+                    // lock, so this should not happen.
+                    if (config == null) {
+                        Log.wtf(TAG, "sUidOwnerMap entry was deleted while holding a lock");
+                    } else if (!uidSet.contains((int) uid.val) && (config.rule & match) != 0) {
+                        uidSetToRemoveRule.add((int) uid.val);
+                    }
+                });
 
-                    for (final int uid : uidSetToRemoveRule) {
-                        removeRule(uid, match, "replaceUidChain");
-                    }
-                    for (final int uid : uids) {
-                        addRule(uid, match, "replaceUidChain");
-                    }
+                for (final int uid : uidSetToRemoveRule) {
+                    removeRule(uid, match, "replaceUidChain");
                 }
-            } catch (ErrnoException | ServiceSpecificException e) {
-                Log.e(TAG, "replaceUidChain failed: " + e);
+                for (final int uid : uids) {
+                    addRule(uid, match, "replaceUidChain");
+                }
             }
-        } else {
-            final int err;
-            switch (chain) {
-                case FIREWALL_CHAIN_DOZABLE:
-                    err = native_replaceUidChain("fw_dozable", true /* isAllowList */, uids);
-                    break;
-                case FIREWALL_CHAIN_STANDBY:
-                    err = native_replaceUidChain("fw_standby", false /* isAllowList */, uids);
-                    break;
-                case FIREWALL_CHAIN_POWERSAVE:
-                    err = native_replaceUidChain("fw_powersave", true /* isAllowList */, uids);
-                    break;
-                case FIREWALL_CHAIN_RESTRICTED:
-                    err = native_replaceUidChain("fw_restricted", true /* isAllowList */, uids);
-                    break;
-                case FIREWALL_CHAIN_LOW_POWER_STANDBY:
-                    err = native_replaceUidChain(
-                            "fw_low_power_standby", true /* isAllowList */, uids);
-                    break;
-                case FIREWALL_CHAIN_OEM_DENY_1:
-                    err = native_replaceUidChain("fw_oem_deny_1", false /* isAllowList */, uids);
-                    break;
-                case FIREWALL_CHAIN_OEM_DENY_2:
-                    err = native_replaceUidChain("fw_oem_deny_2", false /* isAllowList */, uids);
-                    break;
-                case FIREWALL_CHAIN_OEM_DENY_3:
-                    err = native_replaceUidChain("fw_oem_deny_3", false /* isAllowList */, uids);
-                    break;
-                default:
-                    throw new IllegalArgumentException("replaceFirewallChain with invalid chain: "
-                            + chain);
-            }
-            if (err != 0) {
-                Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
-            }
+        } catch (ErrnoException | ServiceSpecificException e) {
+            Log.e(TAG, "replaceUidChain failed: " + e);
         }
     }
 
@@ -699,23 +554,19 @@
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public void setUidRule(final int childChain, final int uid, final int firewallRule) {
         throwIfPreT("setUidRule is not available on pre-T devices");
 
-        if (sEnableJavaBpfMap) {
-            final long match = getMatchByFirewallChain(childChain);
-            final boolean isAllowList = isFirewallAllowList(childChain);
-            final boolean add = (firewallRule == FIREWALL_RULE_ALLOW && isAllowList)
-                    || (firewallRule == FIREWALL_RULE_DENY && !isAllowList);
+        final long match = getMatchByFirewallChain(childChain);
+        final boolean isAllowList = isFirewallAllowList(childChain);
+        final boolean add = (firewallRule == FIREWALL_RULE_ALLOW && isAllowList)
+                || (firewallRule == FIREWALL_RULE_DENY && !isAllowList);
 
-            if (add) {
-                addRule(uid, match, "setUidRule");
-            } else {
-                removeRule(uid, match, "setUidRule");
-            }
+        if (add) {
+            addRule(uid, match, "setUidRule");
         } else {
-            final int err = native_setUidRule(childChain, uid, firewallRule);
-            maybeThrow(err, "Unable to set uid rule");
+            removeRule(uid, match, "setUidRule");
         }
     }
 
@@ -729,21 +580,12 @@
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public int getUidRule(final int childChain, final int uid) {
-        throwIfPreT("isUidChainEnabled is not available on pre-T devices");
-
-        final long match = getMatchByFirewallChain(childChain);
-        final boolean isAllowList = isFirewallAllowList(childChain);
-        try {
-            final UidOwnerValue uidMatch = sUidOwnerMap.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 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<>();
@@ -772,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)) {
@@ -793,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)) {
@@ -821,34 +665,29 @@
      *                                  cause of the failure.
      */
     public void addUidInterfaceRules(final String ifName, final int[] uids) throws RemoteException {
-        if (PRE_T) {
+        if (!SdkLevel.isAtLeastT()) {
             mNetd.firewallAddUidInterfaceRules(ifName, uids);
             return;
         }
 
-        if (sEnableJavaBpfMap) {
-            // Null ifName is a wildcard to allow apps to receive packets on all interfaces and
-            // ifIndex is set to 0.
-            final int ifIndex;
-            if (ifName == null) {
-                ifIndex = 0;
-            } else {
-                ifIndex = mDeps.getIfIndex(ifName);
-                if (ifIndex == 0) {
-                    throw new ServiceSpecificException(ENODEV,
-                            "Failed to get index of interface " + ifName);
-                }
-            }
-            for (final int uid : uids) {
-                try {
-                    addRule(uid, IIF_MATCH, ifIndex, "addUidInterfaceRules");
-                } catch (ServiceSpecificException e) {
-                    Log.e(TAG, "addRule failed uid=" + uid + " ifName=" + ifName + ", " + e);
-                }
-            }
+        // Null ifName is a wildcard to allow apps to receive packets on all interfaces and
+        // ifIndex is set to 0.
+        final int ifIndex;
+        if (ifName == null) {
+            ifIndex = 0;
         } else {
-            final int err = native_addUidInterfaceRules(ifName, uids);
-            maybeThrow(err, "Unable to add uid interface rules");
+            ifIndex = mDeps.getIfIndex(ifName);
+            if (ifIndex == 0) {
+                throw new ServiceSpecificException(ENODEV,
+                        "Failed to get index of interface " + ifName);
+            }
+        }
+        for (final int uid : uids) {
+            try {
+                addRule(uid, IIF_MATCH, ifIndex, "addUidInterfaceRules");
+            } catch (ServiceSpecificException e) {
+                Log.e(TAG, "addRule failed uid=" + uid + " ifName=" + ifName + ", " + e);
+            }
         }
     }
 
@@ -864,22 +703,17 @@
      *                                  cause of the failure.
      */
     public void removeUidInterfaceRules(final int[] uids) throws RemoteException {
-        if (PRE_T) {
+        if (!SdkLevel.isAtLeastT()) {
             mNetd.firewallRemoveUidInterfaceRules(uids);
             return;
         }
 
-        if (sEnableJavaBpfMap) {
-            for (final int uid : uids) {
-                try {
-                    removeRule(uid, IIF_MATCH, "removeUidInterfaceRules");
-                } catch (ServiceSpecificException e) {
-                    Log.e(TAG, "removeRule failed uid=" + uid + ", " + e);
-                }
+        for (final int uid : uids) {
+            try {
+                removeRule(uid, IIF_MATCH, "removeUidInterfaceRules");
+            } catch (ServiceSpecificException e) {
+                Log.e(TAG, "removeRule failed uid=" + uid + ", " + e);
             }
-        } else {
-            final int err = native_removeUidInterfaceRules(uids);
-            maybeThrow(err, "Unable to remove uid interface rules");
         }
     }
 
@@ -891,18 +725,14 @@
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public void updateUidLockdownRule(final int uid, final boolean add) {
         throwIfPreT("updateUidLockdownRule is not available on pre-T devices");
 
-        if (sEnableJavaBpfMap) {
-            if (add) {
-                addRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
-            } else {
-                removeRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
-            }
+        if (add) {
+            addRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
         } else {
-            final int err = native_updateUidLockdownRule(uid, add);
-            maybeThrow(err, "Unable to update lockdown rule");
+            removeRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
         }
     }
 
@@ -913,36 +743,32 @@
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public void swapActiveStatsMap() {
         throwIfPreT("swapActiveStatsMap is not available on pre-T devices");
 
-        if (sEnableJavaBpfMap) {
-            try {
-                synchronized (sCurrentStatsMapConfigLock) {
-                    final long config = sConfigurationMap.getValue(
-                            CURRENT_STATS_MAP_CONFIGURATION_KEY).val;
-                    final long newConfig = (config == STATS_SELECT_MAP_A)
-                            ? STATS_SELECT_MAP_B : STATS_SELECT_MAP_A;
-                    sConfigurationMap.updateEntry(CURRENT_STATS_MAP_CONFIGURATION_KEY,
-                            new U32(newConfig));
-                }
-            } catch (ErrnoException e) {
-                throw new ServiceSpecificException(e.errno, "Failed to swap active stats map");
+        try {
+            synchronized (sCurrentStatsMapConfigLock) {
+                final long config = sConfigurationMap.getValue(
+                        CURRENT_STATS_MAP_CONFIGURATION_KEY).val;
+                final long newConfig = (config == STATS_SELECT_MAP_A)
+                        ? STATS_SELECT_MAP_B : STATS_SELECT_MAP_A;
+                sConfigurationMap.updateEntry(CURRENT_STATS_MAP_CONFIGURATION_KEY,
+                        new U32(newConfig));
             }
-
-            // After changing the config, it's needed to make sure all the current running eBPF
-            // programs are finished and all the CPUs are aware of this config change before the old
-            // map is modified. So special hack is needed here to wait for the kernel to do a
-            // synchronize_rcu(). Once the kernel called synchronize_rcu(), the updated config will
-            // be available to all cores and the next eBPF programs triggered inside the kernel will
-            // use the new map configuration. So once this function returns it is safe to modify the
-            // old stats map without concerning about race between the kernel and userspace.
-            final int err = mDeps.synchronizeKernelRCU();
-            maybeThrow(err, "synchronizeKernelRCU failed");
-        } else {
-            final int err = native_swapActiveStatsMap();
-            maybeThrow(err, "Unable to swap active stats map");
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno, "Failed to swap active stats map");
         }
+
+        // After changing the config, it's needed to make sure all the current running eBPF
+        // programs are finished and all the CPUs are aware of this config change before the old
+        // map is modified. So special hack is needed here to wait for the kernel to do a
+        // synchronize_rcu(). Once the kernel called synchronize_rcu(), the updated config will
+        // be available to all cores and the next eBPF programs triggered inside the kernel will
+        // use the new map configuration. So once this function returns it is safe to modify the
+        // old stats map without concerning about race between the kernel and userspace.
+        final int err = mDeps.synchronizeKernelRCU();
+        maybeThrow(err, "synchronizeKernelRCU failed");
     }
 
     /**
@@ -956,38 +782,160 @@
      * @throws RemoteException when netd has crashed.
      */
     public void setNetPermForUids(final int permissions, final int[] uids) throws RemoteException {
-        if (PRE_T) {
+        if (!SdkLevel.isAtLeastT()) {
             mNetd.trafficSetNetPermForUids(permissions, uids);
             return;
         }
 
-        if (sEnableJavaBpfMap) {
-            // Remove the entry if package is uninstalled or uid has only INTERNET permission.
-            if (permissions == PERMISSION_UNINSTALLED || permissions == PERMISSION_INTERNET) {
-                for (final int uid : uids) {
-                    try {
-                        sUidPermissionMap.deleteEntry(new S32(uid));
-                    } catch (ErrnoException e) {
-                        Log.e(TAG, "Failed to remove uid " + uid + " from permission map: " + e);
-                    }
-                }
-                return;
-            }
-
+        // Remove the entry if package is uninstalled or uid has only INTERNET permission.
+        if (permissions == PERMISSION_UNINSTALLED || permissions == PERMISSION_INTERNET) {
             for (final int uid : uids) {
                 try {
-                    sUidPermissionMap.updateEntry(new S32(uid), new U8((short) permissions));
+                    sUidPermissionMap.deleteEntry(new S32(uid));
                 } catch (ErrnoException e) {
-                    Log.e(TAG, "Failed to set permission "
-                            + permissions + " to uid " + uid + ": " + e);
+                    Log.e(TAG, "Failed to remove uid " + uid + " from permission map: " + e);
                 }
             }
-        } else {
-            native_setPermissionForUids(permissions, uids);
+            return;
+        }
+
+        for (final int uid : uids) {
+            try {
+                sUidPermissionMap.updateEntry(new S32(uid), new U8((short) permissions));
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Failed to set permission "
+                        + permissions + " to uid " + uid + ": " + e);
+            }
         }
     }
 
+    /**
+     * 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.
+     * @throws UnsupportedOperationException if called on pre-T devices.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public void setDataSaverEnabled(boolean enable) {
+        throwIfPreT("setDataSaverEnabled is not available on pre-T devices");
+
+        try {
+            final short config = enable ? DATA_SAVER_ENABLED : DATA_SAVER_DISABLED;
+            sDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(config));
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno, "Unable to set data saver: "
+                    + Os.strerror(e.errno));
+        }
+    }
+
+    /**
+     * Set ingress discard rule
+     *
+     * @param address target address to set the ingress discard rule
+     * @param iface allowed interface
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public void setIngressDiscardRule(final InetAddress address, final String iface) {
+        throwIfPreT("setIngressDiscardRule is not available on pre-T devices");
+        final int ifIndex = mDeps.getIfIndex(iface);
+        if (ifIndex == 0) {
+            Log.e(TAG, "Failed to get if index, skip setting ingress discard rule for " + address
+                    + "(" + iface + ")");
+            return;
+        }
+        try {
+            sIngressDiscardMap.updateEntry(new IngressDiscardKey(address),
+                    new IngressDiscardValue(ifIndex, ifIndex));
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to set ingress discard rule for " + address + "("
+                    + iface + "), " + e);
+        }
+    }
+
+    /**
+     * Remove ingress discard rule
+     *
+     * @param address target address to remove the ingress discard rule
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public void removeIngressDiscardRule(final InetAddress address) {
+        throwIfPreT("removeIngressDiscardRule is not available on pre-T devices");
+        try {
+            sIngressDiscardMap.deleteEntry(new IngressDiscardKey(address));
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to remove ingress discard rule for " + address + ", " + e);
+        }
+    }
+
+    /**
+     * 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) {
         throwIfPreT("setPullAtomCallback is not available on pre-T devices");
 
@@ -1048,26 +996,7 @@
         return sj.toString();
     }
 
-    private String matchToString(long matchMask) {
-        if (matchMask == NO_MATCH) {
-            return "NO_MATCH";
-        }
-
-        final StringJoiner sj = new StringJoiner(" ");
-        for (Pair<Long, String> match: MATCH_LIST) {
-            final long matchFlag = match.first;
-            final String matchName = match.second;
-            if ((matchMask & matchFlag) != 0) {
-                sj.add(matchName);
-                matchMask &= ~matchFlag;
-            }
-        }
-        if (matchMask != 0) {
-            sj.add("UNKNOWN_MATCH(" + matchMask + ")");
-        }
-        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;
@@ -1088,6 +1017,15 @@
         }
     }
 
+    private void dumpDataSaverConfig(final IndentingPrintWriter pw) {
+        try {
+            final short config = sDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val;
+            // Any non-zero value converted from short to boolean is true by convention.
+            pw.println("sDataSaverEnabledMap: " + (config != DATA_SAVER_DISABLED));
+        } catch (ErrnoException e) {
+            pw.println("Failed to read data saver configuration: " + e);
+        }
+    }
     /**
      * Dump BPF maps
      *
@@ -1097,17 +1035,18 @@
      * @throws IOException when file descriptor is invalid.
      * @throws ServiceSpecificException when the method is called on an unsupported device.
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public void dump(final IndentingPrintWriter pw, final FileDescriptor fd, boolean verbose)
             throws IOException, ServiceSpecificException {
-        if (PRE_T) {
+        if (!SdkLevel.isAtLeastT()) {
             throw new ServiceSpecificException(
                     EOPNOTSUPP, "dumpsys connectivity trafficcontroller dump not available on pre-T"
                     + " devices, use dumpsys netd trafficcontroller instead.");
         }
-        mDeps.nativeDump(fd, verbose);
+
+        pw.println("TrafficController");  // required by CTS testDumpBpfNetMaps
 
         pw.println();
-        pw.println("sEnableJavaBpfMap: " + sEnableJavaBpfMap);
         if (verbose) {
             pw.println();
             pw.println("BPF map content:");
@@ -1136,23 +1075,12 @@
                     });
             BpfDump.dumpMap(sUidPermissionMap, pw, "sUidPermissionMap",
                     (uid, permission) -> uid.val + " " + permissionToString(permission.val));
+            BpfDump.dumpMap(sIngressDiscardMap, pw, "sIngressDiscardMap",
+                    (key, value) -> "[" + key.dstAddr + "]: "
+                            + value.iif1 + "(" + mDeps.getIfName(value.iif1) + "), "
+                            + value.iif2 + "(" + mDeps.getIfName(value.iif2) + ")");
+            dumpDataSaverConfig(pw);
             pw.decreaseIndent();
         }
     }
-
-    private static native void native_init(boolean startSkDestroyListener);
-    private native int native_addNaughtyApp(int uid);
-    private native int native_removeNaughtyApp(int uid);
-    private native int native_addNiceApp(int uid);
-    private native int native_removeNiceApp(int uid);
-    private native int native_setChildChain(int childChain, boolean enable);
-    private native int native_replaceUidChain(String name, boolean isAllowlist, int[] uids);
-    private native int native_setUidRule(int childChain, int uid, int firewallRule);
-    private native int native_addUidInterfaceRules(String ifName, int[] uids);
-    private native int native_removeUidInterfaceRules(int[] uids);
-    private native int native_updateUidLockdownRule(int uid, boolean add);
-    private native int native_swapActiveStatsMap();
-    private native void native_setPermissionForUids(int permissions, int[] uids);
-    private static native void native_dump(FileDescriptor fd, boolean verbose);
-    private static native int native_synchronizeKernelRCU();
 }
diff --git a/service/src/com/android/server/CallbackQueue.java b/service/src/com/android/server/CallbackQueue.java
new file mode 100644
index 0000000..060a984
--- /dev/null
+++ b/service/src/com/android/server/CallbackQueue.java
@@ -0,0 +1,126 @@
+/*
+ * 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);
+ *     [...]
+ *     queue.shrinkToLength();
+ *     storedCallbacks = queue.getBackingArray();
+ * </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 e6bc501..4d4dacf 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -19,10 +19,13 @@
 import static android.Manifest.permission.RECEIVE_DATA_ACTIVITY_CHANGE;
 import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
 import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
+import static android.content.pm.PackageManager.FEATURE_LEANBACK;
 import static android.content.pm.PackageManager.FEATURE_WATCH;
 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;
@@ -31,14 +34,29 @@
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
 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_NONE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NETWORK_RESTRICTED;
+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.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;
@@ -58,15 +76,20 @@
 import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
 import static android.net.ConnectivityManager.getNetworkTypeName;
 import static android.net.ConnectivityManager.isNetworkTypeValid;
+import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
 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;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID;
+import static android.net.MulticastRoutingConfig.FORWARD_NONE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
 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;
@@ -77,6 +100,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
 import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
@@ -91,25 +115,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.provider.DeviceConfig.NAMESPACE_TETHERING;
 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.DeviceConfigUtils.TETHERING_MODULE_NAME;
+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.checkAnyPermissionOf;
 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 java.util.Map.Entry;
+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.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
+import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
+import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
 
 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;
@@ -129,6 +173,7 @@
 import android.content.pm.PackageManager;
 import android.content.res.XmlResourceParser;
 import android.database.ContentObserver;
+import android.net.BpfNetMapsUtils;
 import android.net.CaptivePortal;
 import android.net.CaptivePortalData;
 import android.net.ConnectionInfo;
@@ -159,7 +204,10 @@
 import android.net.IpMemoryStore;
 import android.net.IpPrefix;
 import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
+import android.net.LocalNetworkInfo;
 import android.net.MatchAllNetworkSpecifier;
+import android.net.MulticastRoutingConfig;
 import android.net.NativeNetworkConfig;
 import android.net.NativeNetworkType;
 import android.net.NattSocketKeepalive;
@@ -191,7 +239,6 @@
 import android.net.QosSocketFilter;
 import android.net.QosSocketInfo;
 import android.net.RouteInfo;
-import android.net.RouteInfoParcel;
 import android.net.SocketKeepalive;
 import android.net.TetheringManager;
 import android.net.TransportInfo;
@@ -201,7 +248,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;
@@ -238,8 +284,12 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.stats.connectivity.MeteredState;
+import android.stats.connectivity.RequestType;
+import android.stats.connectivity.ValidatedState;
 import android.sysprop.NetworkProperties;
 import android.system.ErrnoException;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -250,27 +300,39 @@
 import android.util.Range;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
-
-import androidx.annotation.RequiresApi;
+import android.util.StatsEvent;
 
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.MessageUtils;
+import com.android.metrics.ConnectionDurationForTransports;
+import com.android.metrics.ConnectionDurationPerTransports;
+import com.android.metrics.ConnectivitySampleMetricsHelper;
+import com.android.metrics.ConnectivityStateSample;
+import com.android.metrics.NetworkCountForTransports;
+import com.android.metrics.NetworkCountPerTransports;
+import com.android.metrics.NetworkDescription;
+import com.android.metrics.NetworkList;
+import com.android.metrics.NetworkRequestCount;
+import com.android.metrics.RequestCountForType;
 import com.android.modules.utils.BasicShellCommandHandler;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
 import com.android.net.module.util.BinderUtils;
 import com.android.net.module.util.BitUtils;
+import com.android.net.module.util.BpfUtils;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
 import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
 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;
@@ -294,6 +356,7 @@
 import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.LingerMonitor;
 import com.android.server.connectivity.MockableSystemProperties;
+import com.android.server.connectivity.MulticastRoutingCoordinatorService;
 import com.android.server.connectivity.MultinetworkPolicyTracker;
 import com.android.server.connectivity.NetworkAgentInfo;
 import com.android.server.connectivity.NetworkDiagnostics;
@@ -302,10 +365,12 @@
 import com.android.server.connectivity.NetworkOffer;
 import com.android.server.connectivity.NetworkPreferenceList;
 import com.android.server.connectivity.NetworkRanker;
+import com.android.server.connectivity.NetworkRequestStateStatsMetrics;
 import com.android.server.connectivity.PermissionMonitor;
 import com.android.server.connectivity.ProfileNetworkPreferenceInfo;
 import com.android.server.connectivity.ProxyTracker;
 import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.SatelliteAccessController;
 import com.android.server.connectivity.UidRangeUtils;
 import com.android.server.connectivity.VpnNetworkPreferenceInfo;
 import com.android.server.connectivity.wear.CompanionDeviceManagerProxyService;
@@ -319,7 +384,6 @@
 import java.io.InterruptedIOException;
 import java.io.PrintWriter;
 import java.io.Writer;
-import java.lang.IllegalArgumentException;
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -335,13 +399,17 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.StringJoiner;
 import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 /**
  * @hide
@@ -355,6 +423,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);
@@ -391,7 +461,7 @@
     // Timeout in case the "actively prefer bad wifi" feature is on
     private static final int ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS = 20 * 1000;
     // Timeout in case the "actively prefer bad wifi" feature is off
-    private static final int DONT_ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS = 8 * 1000;
+    private static final int DEFAULT_EVALUATION_TIMEOUT_MS = 8 * 1000;
 
     // Default to 30s linger time-out, and 5s for nascent network. Modifiable only for testing.
     private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
@@ -436,9 +506,39 @@
 
     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();
 
@@ -469,6 +569,8 @@
     @GuardedBy("mTNSLock")
     private TestNetworkService mTNS;
     private final CompanionDeviceManagerProxyService mCdmps;
+    private final MulticastRoutingCoordinatorService mMulticastRoutingCoordinatorService;
+    private final RoutingCoordinatorService mRoutingCoordinatorService;
 
     private final Object mTNSLock = new Object();
 
@@ -504,6 +606,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.
@@ -529,6 +633,10 @@
     // See {@link ConnectivitySettingsManager#setMobileDataPreferredUids}
     @VisibleForTesting
     static final int PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED = 30;
+    // Order of setting satellite network preference fallback when default message application
+    // with role_sms role and android.permission.SATELLITE_COMMUNICATION permission detected
+    @VisibleForTesting
+    static final int PREFERENCE_ORDER_SATELLITE_FALLBACK = 40;
     // Preference order that signifies the network shouldn't be set as a default network for
     // the UIDs, only give them access to it. TODO : replace this with a boolean
     // in NativeUidRangeConfig
@@ -734,11 +842,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
@@ -799,6 +906,18 @@
     private static final int EVENT_UID_FROZEN_STATE_CHANGED = 61;
 
     /**
+     * Event to update firewall socket destroy reasons for uids.
+     * obj = List of Pair(uid, socketDestroyReasons)
+     */
+    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
      * should be shown.
      */
@@ -887,6 +1006,7 @@
     private final QosCallbackTracker mQosCallbackTracker;
     private final NetworkNotificationManager mNotifier;
     private final LingerMonitor mLingerMonitor;
+    private final SatelliteAccessController mSatelliteAccessController;
 
     // sequence number of NetworkRequests
     private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID;
@@ -913,6 +1033,8 @@
 
     private final IpConnectivityLog mMetricsLog;
 
+    @Nullable private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+
     @GuardedBy("mBandwidthRequests")
     private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10);
 
@@ -936,6 +1058,26 @@
     private final Map<String, ApplicationSelfCertifiedNetworkCapabilities>
             mSelfCertifiedCapabilityCache = new HashMap<>();
 
+    // Flag to enable the feature of closing frozen app sockets.
+    private final boolean mDestroyFrozenSockets;
+
+    // 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.
+    // 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.
      *
@@ -1232,16 +1374,24 @@
         private static final String PRIORITY_ARG = "--dump-priority";
         private static final String PRIORITY_ARG_HIGH = "HIGH";
         private static final String PRIORITY_ARG_NORMAL = "NORMAL";
+        private static final int DUMPSYS_DEFAULT_TIMEOUT_MS = 10_000;
 
         LocalPriorityDump() {}
 
         private void dumpHigh(FileDescriptor fd, PrintWriter pw) {
-            doDump(fd, pw, new String[] {DIAG_ARG});
-            doDump(fd, pw, new String[] {SHORT_ARG});
+            if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> {
+                doDump(fd, pw, new String[]{DIAG_ARG});
+                doDump(fd, pw, new String[]{SHORT_ARG});
+            }, DUMPSYS_DEFAULT_TIMEOUT_MS)) {
+                pw.println("dumpHigh timeout");
+            }
         }
 
         private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) {
-            doDump(fd, pw, args);
+            if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, pw, args),
+                    DUMPSYS_DEFAULT_TIMEOUT_MS)) {
+                pw.println("dumpNormal timeout");
+            }
         }
 
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
@@ -1294,6 +1444,10 @@
             return SdkLevel.isAtLeastU();
         }
 
+        public boolean isAtLeastV() {
+            return SdkLevel.isAtLeastV();
+        }
+
         /**
          * Get system properties to use in ConnectivityService.
          */
@@ -1311,8 +1465,8 @@
         /**
          * Create a HandlerThread to use in ConnectivityService.
          */
-        public HandlerThread makeHandlerThread() {
-            return new HandlerThread("ConnectivityServiceThread");
+        public HandlerThread makeHandlerThread(@NonNull final String tag) {
+            return new HandlerThread(tag);
         }
 
         /**
@@ -1369,6 +1523,30 @@
             return new AutomaticOnOffKeepaliveTracker(c, h);
         }
 
+        public MulticastRoutingCoordinatorService makeMulticastRoutingCoordinatorService(
+                    @NonNull Handler h) {
+            try {
+                return new MulticastRoutingCoordinatorService(h);
+            } catch (UnsupportedOperationException e) {
+                // Multicast routing is not supported by the kernel
+                Log.i(TAG, "Skipping unsupported MulticastRoutingCoordinatorService");
+                return null;
+            }
+        }
+
+        /**
+         * @see NetworkRequestStateStatsMetrics
+         */
+        public NetworkRequestStateStatsMetrics makeNetworkRequestStateStatsMetrics(
+                Context context) {
+            // We currently have network requests metric for Watch devices only
+            if (context.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
+                return new NetworkRequestStateStatsMetrics();
+            } else {
+                return null;
+            }
+        }
+
         /**
          * @see BatteryStatsManager
          */
@@ -1405,20 +1583,43 @@
          */
         @Nullable
         public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
-                @NonNull final Context context, @NonNull final TelephonyManager tm) {
+                @NonNull final Context context,
+                @NonNull final TelephonyManager tm,
+                boolean requestRestrictedWifiEnabled,
+                @NonNull BiConsumer<Integer, Integer> listener,
+                @NonNull final Handler connectivityServiceHandler) {
             if (isAtLeastT()) {
-                return new CarrierPrivilegeAuthenticator(context, tm);
+                return new CarrierPrivilegeAuthenticator(context, tm, requestRestrictedWifiEnabled,
+                        listener, connectivityServiceHandler);
             } else {
                 return null;
             }
         }
 
         /**
-         * @see DeviceConfigUtils#isFeatureEnabled
+         * @see SatelliteAccessController
+         */
+        @Nullable
+        public SatelliteAccessController makeSatelliteAccessController(
+                @NonNull final Context context,
+                Consumer<Set<Integer>> updateSatelliteNetworkFallbackUidCallback,
+                @NonNull final Handler connectivityServiceInternalHandler) {
+            return new SatelliteAccessController(context, updateSatelliteNetworkFallbackUidCallback,
+                    connectivityServiceInternalHandler);
+        }
+
+        /**
+         * @see DeviceConfigUtils#isTetheringFeatureEnabled
          */
         public boolean isFeatureEnabled(Context context, String name) {
-            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING, name,
-                    TETHERING_MODULE_NAME, false /* defaultValue */);
+            return DeviceConfigUtils.isTetheringFeatureEnabled(context, name);
+        }
+
+        /**
+         * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut
+         */
+        public boolean isFeatureNotChickenedOut(Context context, String name) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name);
         }
 
         /**
@@ -1433,6 +1634,7 @@
         /**
          * @see ClatCoordinator
          */
+        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
         public ClatCoordinator getClatCoordinator(INetd netd) {
             return new ClatCoordinator(
                 new ClatCoordinator.Dependencies() {
@@ -1490,6 +1692,14 @@
         }
 
         /**
+         * Get BPF program Id from CGROUP. See {@link BpfUtils#getProgramId}.
+         */
+        public int getBpfProgramId(final int attachType)
+                throws IOException {
+            return BpfUtils.getProgramId(attachType);
+        }
+
+        /**
          * Wraps {@link BroadcastOptionsShimImpl#newInstance(BroadcastOptions)}
          */
         // TODO: when available in all active branches:
@@ -1587,11 +1797,12 @@
                 new RequestInfoPerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1);
 
         mMetricsLog = logger;
+        mNetworkRequestStateStatsMetrics = mDeps.makeNetworkRequestStateStatsMetrics(mContext);
         final NetworkRequest defaultInternetRequest = createDefaultRequest();
         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);
@@ -1640,7 +1851,7 @@
 
         mNetd = netd;
         mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd);
-        mHandlerThread = mDeps.makeHandlerThread();
+        mHandlerThread = mDeps.makeHandlerThread("ConnectivityServiceThread");
         mPermissionMonitor =
                 new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mHandlerThread);
         mHandlerThread.start();
@@ -1660,13 +1871,37 @@
         mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
         mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
         mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext);
-        mCarrierPrivilegeAuthenticator =
-                mDeps.makeCarrierPrivilegeAuthenticator(mContext, mTelephonyManager);
+        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::handleUidCarrierPrivilegesLost, mHandler);
+
+        if (mDeps.isAtLeastU()
+                && mDeps
+                .isFeatureNotChickenedOut(mContext, ALLOW_SATALLITE_NETWORK_FALLBACK)) {
+            mSatelliteAccessController = mDeps.makeSatelliteAccessController(
+                    mContext, this::updateSatelliteNetworkPreferenceUids, mHandler);
+        } else {
+            mSatelliteAccessController = null;
+        }
 
         // 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);
@@ -1704,7 +1939,25 @@
         mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
                 null /* broadcastPermission */, mHandler);
 
-        mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mHandler, mNetd);
+        // This is needed for pre-V devices to propagate the data saver status
+        // to the BPF map. This isn't supported before Android T because BPF maps are
+        // unsupported, and it's also unnecessary on Android V and later versions,
+        // as the platform code handles data saver bit updates. Additionally, checking
+        // the initial data saver status here is superfluous because the intent won't
+        // be sent until the system is ready.
+        if (mDeps.isAtLeastT() && !mDeps.isAtLeastV()) {
+            final IntentFilter dataSaverIntentFilter =
+                    new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED);
+            mUserAllContext.registerReceiver(mDataSaverReceiver, dataSaverIntentFilter,
+                    null /* broadcastPermission */, mHandler);
+        }
+
+        // TrackMultiNetworkActivities feature should be enabled by trunk stable flag.
+        // But reading the trunk stable flags from mainline modules is not supported yet.
+        // So enabling this feature on V+ release.
+        mTrackMultiNetworkActivities = mDeps.isAtLeastV();
+        mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, mHandler,
+                mTrackMultiNetworkActivities);
 
         final NetdCallback netdCallback = new NetdCallback();
         try {
@@ -1746,7 +1999,7 @@
         mNoServiceNetwork = new NetworkAgentInfo(null,
                 new Network(INetd.UNREACHABLE_NET_ID),
                 new NetworkInfo(TYPE_NONE, 0, "", ""),
-                new LinkProperties(), new NetworkCapabilities(),
+                new LinkProperties(), new NetworkCapabilities(), null /* localNetworkConfig */,
                 new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
                 new NetworkAgentConfig(), this, null, null, 0, INVALID_UID,
                 mLingerDelayMs, mQosCallbackTracker, mDeps);
@@ -1773,8 +2026,16 @@
             mCdmps = null;
         }
 
-        if (mDeps.isAtLeastU()
-                && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION)) {
+        mRoutingCoordinatorService = new RoutingCoordinatorService(netd);
+        mMulticastRoutingCoordinatorService =
+                mDeps.makeMulticastRoutingCoordinatorService(mHandler);
+
+        mDestroyFrozenSockets = mDeps.isAtLeastV() || (mDeps.isAtLeastU()
+                && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION));
+        mDelayDestroySockets = mDeps.isFeatureNotChickenedOut(context, DELAY_DESTROY_SOCKETS);
+        mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
+                mContext, ALLOW_SYSUI_CONNECTIVITY_REPORTS);
+        if (mDestroyFrozenSockets || mQueueCallbacksForFrozenApps) {
             final UidFrozenStateChangedCallback frozenStateChangedCallback =
                     new UidFrozenStateChangedCallback() {
                 @Override
@@ -1798,6 +2059,8 @@
             activityManager.registerUidFrozenStateChangedCallback(
                     (Runnable r) -> r.run(), frozenStateChangedCallback);
         }
+        mIngressToVpnAddressFiltering = mDeps.isAtLeastT()
+                && mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
     }
 
     /**
@@ -1900,6 +2163,18 @@
                 new Pair<>(network, proxyInfo)).sendToTarget();
     }
 
+    /**
+     * Called when satellite network fallback uids at {@link SatelliteAccessController}
+     * cache was updated based on {@link
+     * android.app.role.OnRoleHoldersChangedListener#onRoleHoldersChanged(String, UserHandle)},
+     * to create multilayer request with preference order
+     * {@link #PREFERENCE_ORDER_SATELLITE_FALLBACK} there on.
+     *
+     */
+    private void updateSatelliteNetworkPreferenceUids(Set<Integer> satelliteNetworkFallbackUids) {
+        handleSetSatelliteNetworkPreference(satelliteNetworkFallbackUids);
+    }
+
     private void handleAlwaysOnNetworkRequest(
             NetworkRequest networkRequest, String settingName, boolean defaultValue) {
         final boolean enable = toBool(Settings.Global.getInt(
@@ -1917,7 +2192,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);
@@ -2033,6 +2308,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.
      */
@@ -2046,7 +2326,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);
         }
@@ -2124,7 +2414,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;
     }
@@ -2332,6 +2622,134 @@
         return out;
     }
 
+    // Because StatsEvent is not usable in tests (everything inside it is hidden), this
+    // method is used to convert a ConnectivityStateSample into a StatsEvent, so that tests
+    // can call sampleConnectivityState and make the checks on it.
+    @NonNull
+    private StatsEvent sampleConnectivityStateToStatsEvent() {
+        final ConnectivityStateSample sample = sampleConnectivityState();
+        return ConnectivityStatsLog.buildStatsEvent(
+                ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE,
+                sample.getNetworkCountPerTransports().toByteArray(),
+                sample.getConnectionDurationPerTransports().toByteArray(),
+                sample.getNetworkRequestCount().toByteArray(),
+                sample.getNetworks().toByteArray());
+    }
+
+    /**
+     * Gather and return a snapshot of the current connectivity state, to be used as a sample.
+     *
+     * This is used for metrics. These snapshots will be sampled and constitute a base for
+     * statistics about connectivity state of devices.
+     */
+    @VisibleForTesting
+    @NonNull
+    public ConnectivityStateSample sampleConnectivityState() {
+        ensureRunningOnConnectivityServiceThread();
+        final ConnectivityStateSample.Builder builder = ConnectivityStateSample.newBuilder();
+        builder.setNetworkCountPerTransports(sampleNetworkCount(mNetworkAgentInfos));
+        builder.setConnectionDurationPerTransports(sampleConnectionDuration(mNetworkAgentInfos));
+        builder.setNetworkRequestCount(sampleNetworkRequestCount(mNetworkRequests.values()));
+        builder.setNetworks(sampleNetworks(mNetworkAgentInfos));
+        return builder.build();
+    }
+
+    private static NetworkCountPerTransports sampleNetworkCount(
+            @NonNull final ArraySet<NetworkAgentInfo> nais) {
+        final SparseIntArray countPerTransports = new SparseIntArray();
+        for (final NetworkAgentInfo nai : nais) {
+            int transports = (int) nai.networkCapabilities.getTransportTypesInternal();
+            countPerTransports.put(transports, 1 + countPerTransports.get(transports, 0));
+        }
+        final NetworkCountPerTransports.Builder builder = NetworkCountPerTransports.newBuilder();
+        for (int i = countPerTransports.size() - 1; i >= 0; --i) {
+            final NetworkCountForTransports.Builder c = NetworkCountForTransports.newBuilder();
+            c.setTransportTypes(countPerTransports.keyAt(i));
+            c.setNetworkCount(countPerTransports.valueAt(i));
+            builder.addNetworkCountForTransports(c);
+        }
+        return builder.build();
+    }
+
+    private static ConnectionDurationPerTransports sampleConnectionDuration(
+            @NonNull final ArraySet<NetworkAgentInfo> nais) {
+        final ConnectionDurationPerTransports.Builder builder =
+                ConnectionDurationPerTransports.newBuilder();
+        for (final NetworkAgentInfo nai : nais) {
+            final ConnectionDurationForTransports.Builder c =
+                    ConnectionDurationForTransports.newBuilder();
+            c.setTransportTypes((int) nai.networkCapabilities.getTransportTypesInternal());
+            final long durationMillis = SystemClock.elapsedRealtime() - nai.getConnectedTime();
+            final long millisPerSecond = TimeUnit.SECONDS.toMillis(1);
+            // Add millisPerSecond/2 to round up or down to the nearest value
+            c.setDurationSec((int) ((durationMillis + millisPerSecond / 2) / millisPerSecond));
+            builder.addConnectionDurationForTransports(c);
+        }
+        return builder.build();
+    }
+
+    private static NetworkRequestCount sampleNetworkRequestCount(
+            @NonNull final Collection<NetworkRequestInfo> nris) {
+        final NetworkRequestCount.Builder builder = NetworkRequestCount.newBuilder();
+        final SparseIntArray countPerType = new SparseIntArray();
+        for (final NetworkRequestInfo nri : nris) {
+            final int type;
+            if (Process.SYSTEM_UID == nri.mAsUid) {
+                // The request is filed "as" the system, so it's the system on its own behalf.
+                type = RequestType.RT_SYSTEM.getNumber();
+            } else if (Process.SYSTEM_UID == nri.mUid) {
+                // The request is filed by the system as some other app, so it's the system on
+                // behalf of an app.
+                type = RequestType.RT_SYSTEM_ON_BEHALF_OF_APP.getNumber();
+            } else {
+                // 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) + 1);
+        }
+        for (int i = countPerType.size() - 1; i >= 0; --i) {
+            final RequestCountForType.Builder r = RequestCountForType.newBuilder();
+            r.setRequestType(RequestType.forNumber(countPerType.keyAt(i)));
+            r.setRequestCount(countPerType.valueAt(i));
+            builder.addRequestCountForType(r);
+        }
+        return builder.build();
+    }
+
+    private static NetworkList sampleNetworks(@NonNull final ArraySet<NetworkAgentInfo> nais) {
+        final NetworkList.Builder builder = NetworkList.newBuilder();
+        for (final NetworkAgentInfo nai : nais) {
+            final NetworkCapabilities nc = nai.networkCapabilities;
+            final NetworkDescription.Builder d = NetworkDescription.newBuilder();
+            d.setTransportTypes((int) nc.getTransportTypesInternal());
+            final MeteredState meteredState;
+            if (nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)) {
+                meteredState = MeteredState.METERED_TEMPORARILY_UNMETERED;
+            } else if (nc.hasCapability(NET_CAPABILITY_NOT_METERED)) {
+                meteredState = MeteredState.METERED_NO;
+            } else {
+                meteredState = MeteredState.METERED_YES;
+            }
+            d.setMeteredState(meteredState);
+            final ValidatedState validatedState;
+            if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
+                validatedState = ValidatedState.VS_PORTAL;
+            } else if (nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY)) {
+                validatedState = ValidatedState.VS_PARTIAL;
+            } else if (nc.hasCapability(NET_CAPABILITY_VALIDATED)) {
+                validatedState = ValidatedState.VS_VALID;
+            } else {
+                validatedState = ValidatedState.VS_INVALID;
+            }
+            d.setValidatedState(validatedState);
+            d.setScorePolicies(nai.getScore().getPoliciesInternal());
+            d.setCapabilities(nc.getCapabilitiesInternal());
+            d.setEnterpriseId(nc.getEnterpriseIdsInternal());
+            builder.addNetworkDescription(d);
+        }
+        return builder.build();
+    }
+
     @Override
     public boolean isNetworkSupported(int networkType) {
         enforceAccessPermission();
@@ -2391,7 +2809,7 @@
         Objects.requireNonNull(packageName);
         Objects.requireNonNull(lp);
         enforceNetworkStackOrSettingsPermission();
-        if (!checkAccessPermission(-1 /* pid */, uid)) {
+        if (!hasAccessPermission(-1 /* pid */, uid)) {
             return null;
         }
         return linkPropertiesRestrictedForCallerPermissions(lp, -1 /* callerPid */, uid);
@@ -2427,7 +2845,7 @@
         Objects.requireNonNull(nc);
         Objects.requireNonNull(packageName);
         enforceNetworkStackOrSettingsPermission();
-        if (!checkAccessPermission(-1 /* pid */, uid)) {
+        if (!hasAccessPermission(-1 /* pid */, uid)) {
             return null;
         }
         return createWithLocationInfoSanitizedIfNecessaryWhenParceled(
@@ -2438,11 +2856,18 @@
 
     private void redactUnderlyingNetworksForCapabilities(NetworkCapabilities nc, int pid, int uid) {
         if (nc.getUnderlyingNetworks() != null
-                && !checkNetworkFactoryOrSettingsPermission(pid, uid)) {
+                && !hasNetworkFactoryOrSettingsPermission(pid, uid)) {
             nc.setUnderlyingNetworks(null);
         }
     }
 
+    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);
+    }
+
     @VisibleForTesting
     NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions(
             NetworkCapabilities nc, int callerPid, int callerUid) {
@@ -2452,22 +2877,20 @@
         // it happens for some reason (e.g. the package is uninstalled while CS is trying to
         // send the callback) it would crash the system server with NPE.
         final NetworkCapabilities newNc = new NetworkCapabilities(nc);
-        if (!checkSettingsPermission(callerPid, callerUid)) {
+        if (!hasSettingsPermission(callerPid, callerUid)) {
             newNc.setUids(null);
             newNc.setSSID(null);
         }
         if (newNc.getNetworkSpecifier() != null) {
             newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact());
         }
-        if (!checkAnyPermissionOf(mContext, callerPid, callerUid,
+        if (!hasAnyPermissionOf(mContext, callerPid, callerUid,
                 android.Manifest.permission.NETWORK_STACK,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)) {
             newNc.setAdministratorUids(new int[0]);
         }
-        if (!checkAnyPermissionOf(mContext,
-                callerPid, callerUid, android.Manifest.permission.NETWORK_FACTORY)) {
+        if (!canSeeAllowedUids(callerPid, callerUid, newNc.getOwnerUid())) {
             newNc.setAllowedUids(new ArraySet<>());
-            newNc.setSubscriptionIds(Collections.emptySet());
         }
         redactUnderlyingNetworksForCapabilities(newNc, callerPid, callerUid);
 
@@ -2528,11 +2951,12 @@
          * Returns whether the app holds local mac address permission or not (might return cached
          * result if the permission was already checked before).
          */
+        @CheckResult
         public boolean hasLocalMacAddressPermission() {
             if (mHasLocalMacAddressPermission == null) {
                 // If there is no cached result, perform the check now.
-                mHasLocalMacAddressPermission =
-                        checkLocalMacAddressPermission(mCallingPid, mCallingUid);
+                mHasLocalMacAddressPermission = ConnectivityService.this
+                        .hasLocalMacAddressPermission(mCallingPid, mCallingUid);
             }
             return mHasLocalMacAddressPermission;
         }
@@ -2541,10 +2965,12 @@
          * Returns whether the app holds settings permission or not (might return cached
          * result if the permission was already checked before).
          */
+        @CheckResult
         public boolean hasSettingsPermission() {
             if (mHasSettingsPermission == null) {
                 // If there is no cached result, perform the check now.
-                mHasSettingsPermission = checkSettingsPermission(mCallingPid, mCallingUid);
+                mHasSettingsPermission =
+                        ConnectivityService.this.hasSettingsPermission(mCallingPid, mCallingUid);
             }
             return mHasSettingsPermission;
         }
@@ -2648,7 +3074,7 @@
             return new LinkProperties(lp);
         }
 
-        if (checkSettingsPermission(callerPid, callerUid)) {
+        if (hasSettingsPermission(callerPid, callerUid)) {
             return new LinkProperties(lp, true /* parcelSensitiveFields */);
         }
 
@@ -2664,7 +3090,7 @@
             int callerUid, String callerPackageName) {
         // There is no need to track the effective UID of the request here. If the caller
         // lacks the settings permission, the effective UID is the same as the calling ID.
-        if (!checkSettingsPermission()) {
+        if (!hasSettingsPermission()) {
             // Unprivileged apps can only pass in null or their own UID.
             if (nc.getUids() == null) {
                 // If the caller passes in null, the callback will also match networks that do not
@@ -2692,6 +3118,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();
@@ -2775,26 +3218,6 @@
         return false;
     }
 
-    private int getAppUid(final String app, final UserHandle user) {
-        final PackageManager pm =
-                mContext.createContextAsUser(user, 0 /* flags */).getPackageManager();
-        final long token = Binder.clearCallingIdentity();
-        try {
-            return pm.getPackageUid(app, 0 /* flags */);
-        } catch (PackageManager.NameNotFoundException e) {
-            return -1;
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    private void verifyCallingUidAndPackage(String packageName, int callingUid) {
-        final UserHandle user = UserHandle.getUserHandleForUid(callingUid);
-        if (getAppUid(packageName, user) != callingUid) {
-            throw new SecurityException(packageName + " does not belong to uid " + callingUid);
-        }
-    }
-
     /**
      * Ensure that a network route exists to deliver traffic to the specified
      * host via the specified network interface.
@@ -2810,7 +3233,8 @@
         if (disallowedBecauseSystemCaller()) {
             return false;
         }
-        verifyCallingUidAndPackage(callingPackageName, mDeps.getCallingUid());
+        PermissionUtils.enforcePackageNameMatchesUid(
+                mContext, mDeps.getCallingUid(), callingPackageName);
         enforceChangePermission(callingPackageName, callingAttributionTag);
         if (mProtectedNetworks.contains(networkType)) {
             enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
@@ -2964,14 +3388,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
+    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 {
@@ -2984,29 +3432,229 @@
         }
     }
 
-    private void handleFrozenUids(int[] uids, int[] frozenStates) {
-        final ArraySet<Range<Integer>> ranges = new ArraySet<>();
-
-        for (int i = 0; i < uids.length; i++) {
-            if (frozenStates[i] == UID_FROZEN_STATE_FROZEN) {
-                Integer uidAsInteger = Integer.valueOf(uids[i]);
-                ranges.add(new Range(uidAsInteger, uidAsInteger));
-            }
+    /**
+     * Check if the cell network is idle.
+     * @return true if the cell network state is idle
+     *         false if the cell network state is active or unknown
+     */
+    private boolean isCellNetworkIdle() {
+        final NetworkAgentInfo defaultNai = getDefaultNetwork();
+        if (defaultNai == null
+                || !defaultNai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
+            // mNetworkActivityTracker only tracks the activity of the default network. So if the
+            // cell network is not the default network, cell network state is unknown.
+            // TODO(b/279380356): Track cell network state when the cell is not the default network
+            return false;
         }
 
-        if (!ranges.isEmpty()) {
-            final Set<Integer> exemptUids = new ArraySet<>();
-            try {
-                mDeps.destroyLiveTcpSockets(ranges, exemptUids);
-            } catch (Exception e) {
-                loge("Exception in socket destroy: " + e);
+        return !mNetworkActivityTracker.isDefaultNetworkActive();
+    }
+
+    private boolean shouldTrackFirewallDestroySocketReasons() {
+        return mDeps.isAtLeastV();
+    }
+
+    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 {
+            final int newDestroyReasons = destroyReasons & ~reason;
+            if (newDestroyReasons == DESTROY_SOCKET_REASON_NONE) {
+                mDestroySocketPendingUids.delete(uid);
+            } else {
+                mDestroySocketPendingUids.put(uid, newDestroyReasons);
             }
         }
     }
 
+    private void handleFrozenUids(int[] uids, int[] frozenStates) {
+        ensureRunningOnConnectivityServiceThread();
+        handleDestroyFrozenSockets(uids, frozenStates);
+        // TODO: handle freezing NetworkCallbacks
+    }
+
+    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 handleUpdateFirewallDestroySocketReasons(
+            List<Pair<Integer, Integer>> reasonsList) {
+        if (!shouldTrackFirewallDestroySocketReasons()) {
+            Log.wtf(TAG, "handleUpdateFirewallDestroySocketReasons is called unexpectedly");
+            return;
+        }
+        ensureRunningOnConnectivityServiceThread();
+
+        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);
+        }
+
+        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) {
+        mNetworkActivityTracker.handleReportNetworkActivity(params);
+
+        final boolean isCellNetworkActivity;
+        if (mTrackMultiNetworkActivities) {
+            final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(params.label);
+            // nai could be null if netd receives a netlink message and calls the network
+            // activity change callback after the network is unregistered from ConnectivityService.
+            isCellNetworkActivity = nai != null
+                    && nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
+        } else {
+            isCellNetworkActivity = params.label == TRANSPORT_CELLULAR;
+        }
+
+        if (mDelayDestroySockets && params.isActive && isCellNetworkActivity) {
+            destroyPendingSockets();
+        }
+    }
+
+    /**
+     * 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 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) {
+            destroyPendingSockets();
+        }
+    }
+
+    private void dumpDestroySockets(IndentingPrintWriter pw) {
+        pw.println("DestroySockets:");
+        pw.increaseIndent();
+        pw.print("mDestroyFrozenSockets="); pw.println(mDestroyFrozenSockets);
+        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();
+        try {
+            pw.print("CGROUP_INET_INGRESS: ");
+            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");
+        }
+        pw.decreaseIndent();
+    }
+
     @VisibleForTesting
     static final String KEY_DESTROY_FROZEN_SOCKETS_VERSION = "destroy_frozen_sockets_version";
 
+    @VisibleForTesting
+    public static final String ALLOW_SYSUI_CONNECTIVITY_REPORTS =
+            "allow_sysui_connectivity_reports";
+
+    public static final String ALLOW_SATALLITE_NETWORK_FALLBACK =
+            "allow_satallite_network_fallback";
+
     private void enforceInternetPermission() {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.INTERNET,
@@ -3019,7 +3667,8 @@
                 "ConnectivityService");
     }
 
-    private boolean checkAccessPermission(int pid, int uid) {
+    @CheckResult
+    private boolean hasAccessPermission(int pid, int uid) {
         return mContext.checkPermission(android.Manifest.permission.ACCESS_NETWORK_STATE, pid, uid)
                 == PERMISSION_GRANTED;
     }
@@ -3105,7 +3754,8 @@
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
 
-    private boolean checkNetworkFactoryOrSettingsPermission(int pid, int uid) {
+    @CheckResult
+    private boolean hasNetworkFactoryOrSettingsPermission(int pid, int uid) {
         return PERMISSION_GRANTED == mContext.checkPermission(
                 android.Manifest.permission.NETWORK_FACTORY, pid, uid)
                 || PERMISSION_GRANTED == mContext.checkPermission(
@@ -3115,13 +3765,14 @@
                 || UserHandle.getAppId(uid) == Process.BLUETOOTH_UID;
     }
 
-    private boolean checkSettingsPermission() {
-        return PermissionUtils.checkAnyPermissionOf(mContext,
-                android.Manifest.permission.NETWORK_SETTINGS,
+    @CheckResult
+    private boolean hasSettingsPermission() {
+        return hasAnyPermissionOf(mContext, android.Manifest.permission.NETWORK_SETTINGS,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
 
-    private boolean checkSettingsPermission(int pid, int uid) {
+    @CheckResult
+    private boolean hasSettingsPermission(int pid, int uid) {
         return PERMISSION_GRANTED == mContext.checkPermission(
                 android.Manifest.permission.NETWORK_SETTINGS, pid, uid)
                 || PERMISSION_GRANTED == mContext.checkPermission(
@@ -3158,28 +3809,36 @@
                 "ConnectivityService");
     }
 
-    private boolean checkNetworkStackPermission() {
-        return PermissionUtils.checkAnyPermissionOf(mContext,
-                android.Manifest.permission.NETWORK_STACK,
+    @CheckResult
+    private boolean hasNetworkStackPermission() {
+        return hasAnyPermissionOf(mContext, android.Manifest.permission.NETWORK_STACK,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
 
-    private boolean checkNetworkStackPermission(int pid, int uid) {
-        return checkAnyPermissionOf(mContext, pid, uid,
-                android.Manifest.permission.NETWORK_STACK,
+    @CheckResult
+    private boolean hasNetworkStackPermission(int pid, int uid) {
+        return hasAnyPermissionOf(mContext, pid, uid, android.Manifest.permission.NETWORK_STACK,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
 
-    private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) {
-        return checkAnyPermissionOf(mContext, pid, uid,
+    @CheckResult
+    private boolean hasSystemBarServicePermission(int pid, int uid) {
+        return hasAnyPermissionOf(mContext, pid, uid,
+                android.Manifest.permission.STATUS_BAR_SERVICE);
+    }
+
+    @CheckResult
+    private boolean hasNetworkSignalStrengthWakeupPermission(int pid, int uid) {
+        return hasAnyPermissionOf(mContext, pid, uid,
                 android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
                 android.Manifest.permission.NETWORK_SETTINGS);
     }
 
-    private boolean checkConnectivityRestrictedNetworksPermission(int callingUid,
+    @CheckResult
+    private boolean hasConnectivityRestrictedNetworksPermission(int callingUid,
             boolean checkUidsAllowedList) {
-        if (PermissionUtils.checkAnyPermissionOf(mContext,
+        if (hasAnyPermissionOf(mContext,
                 android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)) {
             return true;
         }
@@ -3187,8 +3846,7 @@
         // fallback to ConnectivityInternalPermission
         // TODO: Remove this fallback check after all apps have declared
         //  CONNECTIVITY_USE_RESTRICTED_NETWORKS.
-        if (PermissionUtils.checkAnyPermissionOf(mContext,
-                android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
+        if (hasAnyPermissionOf(mContext, android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
             return true;
         }
 
@@ -3202,7 +3860,7 @@
 
     private void enforceConnectivityRestrictedNetworksPermission(boolean checkUidsAllowedList) {
         final int callingUid = mDeps.getCallingUid();
-        if (!checkConnectivityRestrictedNetworksPermission(callingUid, checkUidsAllowedList)) {
+        if (!hasConnectivityRestrictedNetworksPermission(callingUid, checkUidsAllowedList)) {
             throw new SecurityException("ConnectivityService: user " + callingUid
                     + " has no permission to access restricted network.");
         }
@@ -3212,7 +3870,8 @@
         mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
     }
 
-    private boolean checkLocalMacAddressPermission(int pid, int uid) {
+    @CheckResult
+    private boolean hasLocalMacAddressPermission(int pid, int uid) {
         return PERMISSION_GRANTED == mContext.checkPermission(
                 Manifest.permission.LOCAL_MAC_ADDRESS, pid, uid);
     }
@@ -3362,10 +4021,20 @@
             updateMobileDataPreferredUids();
         }
 
+        if (mSatelliteAccessController != null) {
+            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);
         }
+        ConnectivitySampleMetricsHelper.start(mContext, mHandler,
+                CONNECTIVITY_STATE_SAMPLE, this::sampleConnectivityStateToStatsEvent);
         // Wait PermissionMonitor to finish the permission update. Then MultipathPolicyTracker won't
         // have permission problem. While CV#block() is unbounded in time and can in principle block
         // forever, this replaces a synchronous call to PermissionMonitor#startMonitoring, which
@@ -3504,12 +4173,13 @@
     @Override
     protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
             @Nullable String[] args) {
-        if (!checkDumpPermission(mContext, TAG, writer)) return;
+        if (!hasDumpPermission(mContext, TAG, writer)) return;
 
         mPriorityDumper.dump(fd, writer, args);
     }
 
-    private boolean checkDumpPermission(Context context, String tag, PrintWriter pw) {
+    @CheckResult
+    private boolean hasDumpPermission(Context context, String tag, PrintWriter pw) {
         if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
                 != PackageManager.PERMISSION_GRANTED) {
             pw.println("Permission Denial: can't dump " + tag + " from from pid="
@@ -3537,6 +4207,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:");
@@ -3610,6 +4286,28 @@
         dumpAvoidBadWifiSettings(pw);
 
         pw.println();
+        dumpDestroySockets(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();
+            mCarrierPrivilegeAuthenticator.dump(pw);
+        }
+
+        pw.println();
 
         if (!CollectionUtils.contains(args, SHORT_ARG)) {
             pw.println();
@@ -3669,6 +4367,13 @@
         pw.increaseIndent();
         mNetworkActivityTracker.dump(pw);
         pw.decreaseIndent();
+
+        pw.println();
+        pw.println("Multicast routing supported: " +
+                (mMulticastRoutingCoordinatorService != null));
+
+        pw.println();
+        pw.println("Background firewall chain enabled: " + mBackgroundFirewallChainEnabled);
     }
 
     private void dumpNetworks(IndentingPrintWriter pw) {
@@ -3778,6 +4483,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
@@ -3859,7 +4573,14 @@
 
             switch (msg.what) {
                 case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: {
-                    nai.setDeclaredCapabilities((NetworkCapabilities) arg.second);
+                    final NetworkCapabilities proposed = (NetworkCapabilities) arg.second;
+                    if (!nai.respectsNcStructuralConstraints(proposed)) {
+                        Log.wtf(TAG, "Agent " + nai + " violates nc structural constraints : "
+                                + nai.networkCapabilities + " -> " + proposed);
+                        disconnectAndDestroyNetwork(nai);
+                        return;
+                    }
+                    nai.setDeclaredCapabilities(proposed);
                     final NetworkCapabilities sanitized =
                             nai.getDeclaredCapabilitiesSanitized(mCarrierPrivilegeAuthenticator);
                     maybeUpdateWifiRoamTimestamp(nai, sanitized);
@@ -3877,6 +4598,11 @@
                     updateNetworkInfo(nai, info);
                     break;
                 }
+                case NetworkAgent.EVENT_LOCAL_NETWORK_CONFIG_CHANGED: {
+                    final LocalNetworkConfig config = (LocalNetworkConfig) arg.second;
+                    handleUpdateLocalNetworkConfig(nai, nai.localNetworkConfig, config);
+                    break;
+                }
                 case NetworkAgent.EVENT_NETWORK_SCORE_CHANGED: {
                     updateNetworkScore(nai, (NetworkScore) arg.second);
                     break;
@@ -4143,7 +4869,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();
@@ -4170,7 +4904,7 @@
                 updateCapabilitiesForNetwork(nai);
             } else if (portalChanged) {
                 if (portal && ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID
-                        == getCaptivePortalMode()) {
+                        == getCaptivePortalMode(nai)) {
                     if (DBG) log("Avoiding captive portal network: " + nai.toShortString());
                     nai.onPreventAutomaticReconnect();
                     teardownUnneededNetwork(nai);
@@ -4206,7 +4940,13 @@
             }
         }
 
-        private int getCaptivePortalMode() {
+        private int getCaptivePortalMode(@NonNull NetworkAgentInfo nai) {
+            if (nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) &&
+                    mContext.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
+                // Do not avoid captive portal when network is wear proxy.
+                return ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT;
+            }
+
             return Settings.Global.getInt(mContext.getContentResolver(),
                     ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE,
                     ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT);
@@ -4420,7 +5160,7 @@
         // If the Private DNS mode is opportunistic, reprogram the DNS servers
         // in order to restart a validation pass from within netd.
         final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();
-        if (cfg.useTls && TextUtils.isEmpty(cfg.hostname)) {
+        if (cfg.inOpportunisticMode()) {
             updateDnses(nai.linkProperties, null, nai.network.getNetId());
         }
     }
@@ -4628,12 +5368,28 @@
         if (wasDefault) {
             mDefaultInetConditionPublished = 0;
         }
+        if (mTrackMultiNetworkActivities) {
+            // If trackMultiNetworkActivities is disabled, ActivityTracker removes idleTimer when
+            // the network becomes no longer the default network.
+            mNetworkActivityTracker.removeDataActivityTracking(nai);
+        }
         notifyIfacesChangedForNetworkStats();
+        // If this was a local network forwarded to some upstream, or if some local network was
+        // forwarded to this nai, then disable forwarding rules now.
+        maybeDisableForwardRulesForDisconnectingNai(nai, true /* sendCallbacks */);
+        // If this is a local network with an upstream selector, remove the associated network
+        // request.
+        if (nai.isLocalNetwork()) {
+            final NetworkRequest selector = nai.localNetworkConfig.getUpstreamSelector();
+            if (null != selector) {
+                handleRemoveNetworkRequest(mNetworkRequests.get(selector));
+            }
+        }
         // TODO - we shouldn't send CALLBACK_LOST to requests that can be satisfied
         // 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);
@@ -4670,12 +5426,8 @@
                 }
 
                 if (mDefaultRequest == nri) {
-                    // TODO : make battery stats aware that since 2013 multiple interfaces may be
-                    //  active at the same time. For now keep calling this with the default
-                    //  network, because while incorrect this is the closest to the old (also
-                    //  incorrect) behavior.
-                    mNetworkActivityTracker.updateDataActivityTracking(
-                            null /* newNetwork */, nai);
+                    mNetworkActivityTracker.updateDefaultNetwork(null /* newNetwork */, nai);
+                    maybeDestroyPendingSockets(null /* newNetwork */, nai);
                     ensureNetworkTransitionWakelock(nai.toShortString());
                 }
             }
@@ -4741,6 +5493,57 @@
         mNetIdManager.releaseNetId(nai.network.getNetId());
     }
 
+    private void maybeDisableForwardRulesForDisconnectingNai(
+            @NonNull final NetworkAgentInfo disconnecting, final boolean sendCallbacks) {
+        // Step 1 : maybe this network was the upstream for one or more local networks.
+        for (final NetworkAgentInfo local : mNetworkAgentInfos) {
+            if (!local.isLocalNetwork()) continue;
+            final NetworkRequest selector = local.localNetworkConfig.getUpstreamSelector();
+            if (null == selector) continue;
+            final NetworkRequestInfo nri = mNetworkRequests.get(selector);
+            // null == nri can happen while disconnecting a network, because destroyNetwork() is
+            // called after removing all associated NRIs from mNetworkRequests.
+            if (null == nri) continue;
+            final NetworkAgentInfo satisfier = nri.getSatisfier();
+            if (disconnecting != satisfier) continue;
+            removeLocalNetworkUpstream(local, disconnecting);
+            // Set the satisfier to null immediately so that the LOCAL_NETWORK_CHANGED callback
+            // correctly contains null as an upstream.
+            if (sendCallbacks) {
+                nri.setSatisfier(null, null);
+                notifyNetworkCallbacks(local, CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+            }
+        }
+
+        // Step 2 : maybe this is a local network that had an upstream.
+        if (!disconnecting.isLocalNetwork()) return;
+        final NetworkRequest selector = disconnecting.localNetworkConfig.getUpstreamSelector();
+        if (null == selector) return;
+        final NetworkRequestInfo nri = mNetworkRequests.get(selector);
+        // As above null == nri can happen while disconnecting a network, because destroyNetwork()
+        // is called after removing all associated NRIs from mNetworkRequests.
+        if (null == nri) return;
+        final NetworkAgentInfo satisfier = nri.getSatisfier();
+        if (null == satisfier) return;
+        removeLocalNetworkUpstream(disconnecting, satisfier);
+    }
+
+    private void removeLocalNetworkUpstream(@NonNull final NetworkAgentInfo localAgent,
+            @NonNull final NetworkAgentInfo upstream) {
+        try {
+            final String localNetworkInterfaceName = localAgent.linkProperties.getInterfaceName();
+            final String upstreamNetworkInterfaceName = upstream.linkProperties.getInterfaceName();
+            mRoutingCoordinatorService.removeInterfaceForward(
+                    localNetworkInterfaceName,
+                    upstreamNetworkInterfaceName);
+            disableMulticastRouting(localNetworkInterfaceName, upstreamNetworkInterfaceName);
+        } catch (RemoteException e) {
+            loge("Couldn't remove interface forward for "
+                    + localAgent.linkProperties.getInterfaceName() + " to "
+                    + upstream.linkProperties.getInterfaceName() + " while disconnecting");
+        }
+    }
+
     private boolean createNativeNetwork(@NonNull NetworkAgentInfo nai) {
         try {
             // This should never fail.  Specifying an already in use NetID will cause failure.
@@ -4755,7 +5558,9 @@
                         !nai.networkAgentConfig.allowBypass /* secure */,
                         getVpnType(nai), nai.networkAgentConfig.excludeLocalRouteVpn);
             } else {
-                config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
+                config = new NativeNetworkConfig(nai.network.getNetId(),
+                        nai.isLocalNetwork() ? NativeNetworkType.PHYSICAL_LOCAL
+                                : NativeNetworkType.PHYSICAL,
                         getNetworkPermission(nai.networkCapabilities),
                         false /* secure */,
                         VpnManager.TYPE_VPN_NONE,
@@ -4763,8 +5568,8 @@
             }
             mNetd.networkCreate(config);
             mDnsResolver.createNetworkCache(nai.network.getNetId());
-            mDnsManager.updateTransportsForNetwork(nai.network.getNetId(),
-                    nai.networkCapabilities.getTransportTypes());
+            mDnsManager.updateCapabilitiesForNetwork(nai.network.getNetId(),
+                    nai.networkCapabilities);
             return true;
         } catch (RemoteException | ServiceSpecificException e) {
             loge("Error creating network " + nai.toShortString() + ": " + e.getMessage());
@@ -4776,6 +5581,12 @@
         if (mDscpPolicyTracker != null) {
             mDscpPolicyTracker.removeAllDscpPolicies(nai, false);
         }
+        // Remove any forwarding rules to and from the interface for this network, since
+        // the interface is going to go away. Don't send the callbacks however ; if the network
+        // 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) {
@@ -4831,12 +5642,19 @@
     private boolean hasCarrierPrivilegeForNetworkCaps(final int callingUid,
             @NonNull final NetworkCapabilities caps) {
         if (mCarrierPrivilegeAuthenticator != null) {
-            return mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+            return mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                     callingUid, caps);
         }
         return false;
     }
 
+    private int getSubscriptionIdFromNetworkCaps(@NonNull final NetworkCapabilities caps) {
+        if (mCarrierPrivilegeAuthenticator != null) {
+            return mCarrierPrivilegeAuthenticator.getSubIdFromNetworkCapabilities(caps);
+        }
+        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    }
+
     private void handleRegisterNetworkRequestWithIntent(@NonNull final Message msg) {
         final NetworkRequestInfo nri = (NetworkRequestInfo) (msg.obj);
         // handleRegisterNetworkRequestWithIntent() doesn't apply to multilayer requests.
@@ -4873,6 +5691,8 @@
                             updateSignalStrengthThresholds(network, "REGISTER", req);
                         }
                     }
+                } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+                    mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req);
                 }
             }
 
@@ -4883,6 +5703,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()) {
@@ -4962,7 +5788,14 @@
 
     private boolean isNetworkPotentialSatisfier(
             @NonNull final NetworkAgentInfo candidate, @NonNull final NetworkRequestInfo nri) {
-        // listen requests won't keep up a network satisfying it. If this is not a multilayer
+        // While destroyed network sometimes satisfy requests (including occasionally newly
+        // satisfying requests), *potential* satisfiers are networks that might beat a current
+        // champion if they validate. As such, a destroyed network is never a potential satisfier,
+        // because it's never a good idea to keep a destroyed network in case it validates.
+        // For example, declaring it a potential satisfier would keep an unvalidated destroyed
+        // candidate after it's been replaced by another unvalidated network.
+        if (candidate.isDestroyed()) return false;
+        // Listen requests won't keep up a network satisfying it. If this is not a multilayer
         // request, return immediately. For multilayer requests, check to see if any of the
         // multilayer requests may have a potential satisfier.
         if (!nri.isMultilayerRequest() && (nri.mRequests.get(0).isListen()
@@ -4980,8 +5813,12 @@
             if (req.isListen() || req.isListenForBest()) {
                 continue;
             }
-            // If this Network is already the best Network for a request, or if
-            // there is hope for it to become one if it validated, then it is needed.
+            // If there is hope for this network might validate and subsequently become the best
+            // network for that request, then it is needed. Note that this network can't already
+            // be the best for this request, or it would be the current satisfier, and therefore
+            // there would be no need to call this method to find out if it is a *potential*
+            // satisfier ("unneeded", the only caller, only calls this if this network currently
+            // satisfies no request).
             if (candidate.satisfies(req)) {
                 // As soon as a network is found that satisfies a request, return. Specifically for
                 // multilayer requests, returning as soon as a NetworkAgentInfo satisfies a request
@@ -5049,8 +5886,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,
@@ -5066,11 +5902,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)) {
@@ -5079,6 +5920,8 @@
             }
             if (req.isListen()) {
                 removeListenRequestFromNetworks(req);
+            } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+                mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(req);
             }
         }
         nri.unlinkDeathRecipient();
@@ -5103,7 +5946,9 @@
             }
         }
 
-        nri.mPerUidCounter.decrementCount(nri.mUid);
+        if (untrackUids) {
+            maybeUntrackUidAndClearBlockedReasons(nri);
+        }
         mNetworkRequestInfoLogs.log("RELEASE " + nri);
         checkNrisConsistency(nri);
 
@@ -5125,12 +5970,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);
         }
     }
 
@@ -5207,7 +6057,7 @@
     }
 
     private RequestInfoPerUidCounter getRequestCounter(NetworkRequestInfo nri) {
-        return checkAnyPermissionOf(mContext,
+        return hasAnyPermissionOf(mContext,
                 nri.mPid, nri.mUid, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
                 ? mSystemNetworkRequestCounter : mNetworkRequestCounter;
     }
@@ -5431,7 +6281,15 @@
             if (nm == null) return;
 
             if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) {
-                checkNetworkStackPermission();
+                // 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());
             }
         }
@@ -5461,7 +6319,7 @@
      * @see MultinetworkPolicyTracker#getAvoidBadWifi()
      */
     public boolean shouldAvoidBadWifi() {
-        if (!checkNetworkStackPermission()) {
+        if (!hasNetworkStackPermission()) {
             throw new SecurityException("avoidBadWifi requires NETWORK_STACK permission");
         }
         return avoidBadWifi();
@@ -5862,8 +6720,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);
@@ -5881,7 +6739,8 @@
                     break;
                 }
                 case EVENT_REPORT_NETWORK_ACTIVITY:
-                    mNetworkActivityTracker.handleReportNetworkActivity();
+                    final NetworkActivityParams arg = (NetworkActivityParams) msg.obj;
+                    handleReportNetworkActivity(arg);
                     break;
                 case EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED:
                     handleMobileDataPreferredUidsChanged();
@@ -5911,6 +6770,12 @@
                     UidFrozenStateChangedArgs args = (UidFrozenStateChangedArgs) msg.obj;
                     handleFrozenUids(args.mUids, args.mFrozenStates);
                     break;
+                case EVENT_UPDATE_FIREWALL_DESTROY_SOCKET_REASONS:
+                    handleUpdateFirewallDestroySocketReasons((List) msg.obj);
+                    break;
+                case EVENT_CLEAR_FIREWALL_DESTROY_SOCKET_REASONS:
+                    handleClearFirewallDestroySocketReasons(msg.arg1);
+                    break;
             }
         }
     }
@@ -6193,7 +7058,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);
         }
     }
 
@@ -6600,6 +7465,32 @@
         }
     };
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private final BroadcastReceiver mDataSaverReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (mDeps.isAtLeastV()) {
+                throw new IllegalStateException(
+                        "data saver status should be updated from platform");
+            }
+            ensureRunningOnConnectivityServiceThread();
+            switch (intent.getAction()) {
+                case ACTION_RESTRICT_BACKGROUND_CHANGED:
+                    // If the uid is present in the deny list, the API will consistently
+                    // return ENABLED. To retrieve the global switch status, the system
+                    // uid is chosen because it will never be included in the deny list.
+                    final int dataSaverForSystemUid =
+                            mPolicyManager.getRestrictBackgroundStatus(Process.SYSTEM_UID);
+                    final boolean isDataSaverEnabled = (dataSaverForSystemUid
+                            != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
+                    mBpfNetMaps.setDataSaverEnabled(isDataSaverEnabled);
+                    break;
+                default:
+                    Log.wtf(TAG, "received unexpected intent: " + intent.getAction());
+            }
+        }
+    };
+
     private final HashMap<Messenger, NetworkProviderInfo> mNetworkProviderInfos = new HashMap<>();
     private final HashMap<NetworkRequest, NetworkRequestInfo> mNetworkRequests = new HashMap<>();
 
@@ -6701,9 +7592,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
@@ -6750,7 +7648,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}.
@@ -6758,21 +7655,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);
@@ -6784,10 +7682,10 @@
             mAsUid = asUid;
             mPendingIntent = null;
             mPerUidCounter = getRequestCounter(this);
-            mPerUidCounter.incrementCountOrThrow(mUid);
             mCallbackFlags = callbackFlags;
             mCallingAttributionTag = callingAttributionTag;
             mPreferenceOrder = PREFERENCE_ORDER_INVALID;
+            mDeclaredMethodsFlags = declaredMethodsFlags;
             linkDeathRecipient();
         }
 
@@ -6824,10 +7722,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();
         }
 
@@ -6880,6 +7779,11 @@
             }
         }
 
+        boolean isCallbackOverridden(int callbackId) {
+            return !mUseDeclaredMethodsForCallbacksEnabled
+                    || (mDeclaredMethodsFlags & (1 << callbackId)) != 0;
+        }
+
         boolean hasHigherOrderThan(@NonNull final NetworkRequestInfo target) {
             // Compare two preference orders.
             return mPreferenceOrder < target.mPreferenceOrder;
@@ -6914,10 +7818,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 {
@@ -6954,20 +7902,16 @@
     // specific SSID/SignalStrength, or the calling app has permission to do so.
     private void ensureSufficientPermissionsForRequest(NetworkCapabilities nc,
             int callerPid, int callerUid, String callerPackageName) {
-        if (null != nc.getSsid() && !checkSettingsPermission(callerPid, callerUid)) {
+        if (null != nc.getSsid() && !hasSettingsPermission(callerPid, callerUid)) {
             throw new SecurityException("Insufficient permissions to request a specific SSID");
         }
 
         if (nc.hasSignalStrength()
-                && !checkNetworkSignalStrengthWakeupPermission(callerPid, callerUid)) {
+                && !hasNetworkSignalStrengthWakeupPermission(callerPid, callerUid)) {
             throw new SecurityException(
                     "Insufficient permissions to request a specific signal strength");
         }
         mAppOpsManager.checkPackage(callerUid, callerPackageName);
-
-        if (!nc.getSubscriptionIds().isEmpty()) {
-            enforceNetworkFactoryPermission();
-        }
     }
 
     private int[] getSignalStrengthThresholds(@NonNull final NetworkAgentInfo nai) {
@@ -7056,8 +8000,22 @@
     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) {
-        if (legacyType != TYPE_NONE && !checkNetworkStackPermission()) {
+            @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)) {
                 throw new SecurityException("Insufficient permissions to specify legacy type");
@@ -7101,10 +8059,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);
@@ -7131,13 +8091,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
@@ -7149,7 +8102,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);
@@ -7173,7 +8132,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(
@@ -7182,7 +8141,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(
@@ -7191,7 +8151,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;
         }
@@ -7203,7 +8163,7 @@
     }
 
     private void enforceRequestCapabilitiesDeclaration(@NonNull final String callerPackageName,
-            @NonNull final NetworkCapabilities networkCapabilities) {
+            @NonNull final NetworkCapabilities networkCapabilities, int callingUid) {
         // This check is added to fix the linter error for "current min is 30", which is not going
         // to happen because Connectivity service always run in S+.
         if (!mDeps.isAtLeastS()) {
@@ -7217,7 +8177,9 @@
                 applicationNetworkCapabilities = mSelfCertifiedCapabilityCache.get(
                         callerPackageName);
                 if (applicationNetworkCapabilities == null) {
-                    final PackageManager packageManager = mContext.getPackageManager();
+                    final PackageManager packageManager =
+                            mContext.createContextAsUser(UserHandle.getUserHandleForUid(
+                                    callingUid), 0 /* flags */).getPackageManager();
                     final PackageManager.Property networkSliceProperty = packageManager.getProperty(
                             ConstantsShim.PROPERTY_SELF_CERTIFIED_NETWORK_CAPABILITIES,
                             callerPackageName
@@ -7245,19 +8207,34 @@
         applicationNetworkCapabilities.enforceSelfCertifiedNetworkCapabilitiesDeclared(
                 networkCapabilities);
     }
+
+    private boolean canRequestRestrictedNetworkDueToCarrierPrivileges(
+            NetworkCapabilities networkCapabilities, int callingUid) {
+        if (mRequestRestrictedWifiEnabled) {
+            // For U+ devices, callers with carrier privilege could request restricted networks
+            // with CBS capabilities, or any restricted WiFi networks.
+            return ((networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+                || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+                && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities));
+        } else {
+            // For T+ devices, callers with carrier privilege could request with CBS
+            // capabilities.
+            return (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+                && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities));
+        }
+    }
     private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities,
             String callingPackageName, String callingAttributionTag, final int callingUid) {
         if (shouldCheckCapabilitiesDeclaration(networkCapabilities, callingUid,
                 callingPackageName)) {
-            enforceRequestCapabilitiesDeclaration(callingPackageName, networkCapabilities);
+            enforceRequestCapabilitiesDeclaration(callingPackageName, networkCapabilities,
+                    callingUid);
         }
-        if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
-            // For T+ devices, callers with carrier privilege could request with CBS capabilities.
-            if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
-                    && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities)) {
-                return;
+        if (!networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+            if (!canRequestRestrictedNetworkDueToCarrierPrivileges(
+                    networkCapabilities, callingUid)) {
+                enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
             }
-            enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
         } else {
             enforceChangePermission(callingPackageName, callingAttributionTag);
         }
@@ -7302,14 +8279,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);
         }
     }
 
@@ -7326,16 +8302,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;
     }
 
@@ -7374,11 +8349,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();
@@ -7387,7 +8439,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.
@@ -7400,10 +8452,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;
     }
 
@@ -7420,7 +8472,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);
@@ -7428,8 +8480,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. */
@@ -7782,6 +8833,7 @@
                 }
             }
         }
+        if (!highestPriorityNri.isBeingSatisfied()) return null;
         return highestPriorityNri.getSatisfier();
     }
 
@@ -7804,6 +8856,18 @@
     }
 
     /**
+     * Returns whether local agents are supported on this device.
+     *
+     * Local agents are supported from U on TVs, and from V on all devices.
+     */
+    @VisibleForTesting
+    public boolean areLocalAgentsSupported() {
+        final PackageManager pm = mContext.getPackageManager();
+        // Local agents are supported starting on U on TVs and on V on everything else.
+        return mDeps.isAtLeastV() || (mDeps.isAtLeastU() && pm.hasSystemFeature(FEATURE_LEANBACK));
+    }
+
+    /**
      * Register a new agent with ConnectivityService to handle a network.
      *
      * @param na a reference for ConnectivityService to contact the agent asynchronously.
@@ -7814,13 +8878,18 @@
      * @param networkCapabilities the initial capabilites of this network. They can be updated
      *         later : see {@link #updateCapabilities}.
      * @param initialScore the initial score of the network. See {@link NetworkAgentInfo#getScore}.
+     * @param localNetworkConfig config about this local network, or null if not a local network
      * @param networkAgentConfig metadata about the network. This is never updated.
      * @param providerId the ID of the provider owning this NetworkAgent.
      * @return the network created for this agent.
      */
-    public Network registerNetworkAgent(INetworkAgent na, NetworkInfo networkInfo,
-            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
-            @NonNull NetworkScore initialScore, NetworkAgentConfig networkAgentConfig,
+    public Network registerNetworkAgent(INetworkAgent na,
+            NetworkInfo networkInfo,
+            LinkProperties linkProperties,
+            NetworkCapabilities networkCapabilities,
+            @NonNull NetworkScore initialScore,
+            @Nullable LocalNetworkConfig localNetworkConfig,
+            NetworkAgentConfig networkAgentConfig,
             int providerId) {
         Objects.requireNonNull(networkInfo, "networkInfo must not be null");
         Objects.requireNonNull(linkProperties, "linkProperties must not be null");
@@ -7832,12 +8901,26 @@
         } else {
             enforceNetworkFactoryPermission();
         }
+        final boolean hasLocalCap =
+                networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK);
+        if (hasLocalCap && !areLocalAgentsSupported()) {
+            // Before U, netd doesn't support PHYSICAL_LOCAL networks so this can't work.
+            throw new IllegalArgumentException("Local agents are not supported in this version");
+        }
+        final boolean hasLocalNetworkConfig = null != localNetworkConfig;
+        if (hasLocalCap != hasLocalNetworkConfig) {
+            throw new IllegalArgumentException(null != localNetworkConfig
+                    ? "Only local network agents can have a LocalNetworkConfig"
+                    : "Local network agents must have a LocalNetworkConfig"
+            );
+        }
 
         final int uid = mDeps.getCallingUid();
         final long token = Binder.clearCallingIdentity();
         try {
             return registerNetworkAgentInternal(na, networkInfo, linkProperties,
-                    networkCapabilities, initialScore, networkAgentConfig, providerId, uid);
+                    networkCapabilities, initialScore, networkAgentConfig, localNetworkConfig,
+                    providerId, uid);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -7845,7 +8928,8 @@
 
     private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
             LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
-            NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
+            NetworkScore currentScore, NetworkAgentConfig networkAgentConfig,
+            @Nullable LocalNetworkConfig localNetworkConfig, int providerId,
             int uid) {
 
         // Make a copy of the passed NI, LP, NC as the caller may hold a reference to them
@@ -7853,6 +8937,7 @@
         final NetworkInfo niCopy = new NetworkInfo(networkInfo);
         final NetworkCapabilities ncCopy = new NetworkCapabilities(networkCapabilities);
         final LinkProperties lpCopy = new LinkProperties(linkProperties);
+        // No need to copy |localNetworkConfiguration| as it is immutable.
 
         // At this point the capabilities/properties are untrusted and unverified, e.g. checks that
         // the capabilities' access UIDs comply with security limitations. They will be sanitized
@@ -7860,9 +8945,9 @@
         // because some of the checks must happen on the handler thread.
         final NetworkAgentInfo nai = new NetworkAgentInfo(na,
                 new Network(mNetIdManager.reserveNetId()), niCopy, lpCopy, ncCopy,
-                currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
-                this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
-                mQosCallbackTracker, mDeps);
+                localNetworkConfig, currentScore, mContext, mTrackerHandler,
+                new NetworkAgentConfig(networkAgentConfig), this, mNetd, mDnsResolver, providerId,
+                uid, mLingerDelayMs, mQosCallbackTracker, mDeps);
 
         final String extraInfo = niCopy.getExtraInfo();
         final String name = TextUtils.isEmpty(extraInfo)
@@ -7899,6 +8984,9 @@
             e.rethrowAsRuntimeException();
         }
 
+        if (nai.isLocalNetwork()) {
+            handleUpdateLocalNetworkConfig(nai, null /* oldConfig */, nai.localNetworkConfig);
+        }
         nai.notifyRegistered();
         NetworkInfo networkInfo = nai.networkInfo;
         updateNetworkInfo(nai, networkInfo);
@@ -8014,6 +9102,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()) {
@@ -8062,7 +9152,7 @@
             }
             networkAgent.networkMonitor().notifyLinkPropertiesChanged(
                     new LinkProperties(newLp, true /* parcelSensitiveFields */));
-            notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED);
+            notifyNetworkCallbacks(networkAgent, CALLBACK_IP_CHANGED);
         }
 
         mKeepaliveTracker.handleCheckKeepalivesStillValid(networkAgent);
@@ -8172,7 +9262,7 @@
             for (final String iface : interfaceDiff.added) {
                 try {
                     if (DBG) log("Adding iface " + iface + " to network " + netId);
-                    mNetd.networkAddInterface(netId, iface);
+                    mRoutingCoordinatorService.addInterfaceToNetwork(netId, iface);
                     wakeupModifyInterface(iface, nai, true);
                     mDeps.reportNetworkInterfaceForTransports(mContext, iface,
                             nai.networkCapabilities.getTransportTypes());
@@ -8185,45 +9275,13 @@
             try {
                 if (DBG) log("Removing iface " + iface + " from network " + netId);
                 wakeupModifyInterface(iface, nai, false);
-                mNetd.networkRemoveInterface(netId, iface);
+                mRoutingCoordinatorService.removeInterfaceFromNetwork(netId, iface);
             } catch (Exception e) {
                 loge("Exception removing interface: " + e);
             }
         }
     }
 
-    // TODO: move to frameworks/libs/net.
-    private RouteInfoParcel convertRouteInfo(RouteInfo route) {
-        final String nextHop;
-
-        switch (route.getType()) {
-            case RouteInfo.RTN_UNICAST:
-                if (route.hasGateway()) {
-                    nextHop = route.getGateway().getHostAddress();
-                } else {
-                    nextHop = INetd.NEXTHOP_NONE;
-                }
-                break;
-            case RouteInfo.RTN_UNREACHABLE:
-                nextHop = INetd.NEXTHOP_UNREACHABLE;
-                break;
-            case RouteInfo.RTN_THROW:
-                nextHop = INetd.NEXTHOP_THROW;
-                break;
-            default:
-                nextHop = INetd.NEXTHOP_NONE;
-                break;
-        }
-
-        final RouteInfoParcel rip = new RouteInfoParcel();
-        rip.ifName = route.getInterface();
-        rip.destination = route.getDestination().toString();
-        rip.nextHop = nextHop;
-        rip.mtu = route.getMtu();
-
-        return rip;
-    }
-
     /**
      * Have netd update routes from oldLp to newLp.
      * @return true if routes changed between oldLp and newLp
@@ -8244,10 +9302,10 @@
             if (route.hasGateway()) continue;
             if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId);
             try {
-                mNetd.networkAddRouteParcel(netId, convertRouteInfo(route));
+                mRoutingCoordinatorService.addRoute(netId, route);
             } catch (Exception e) {
                 if ((route.getDestination().getAddress() instanceof Inet4Address) || VDBG) {
-                    loge("Exception in networkAddRouteParcel for non-gateway: " + e);
+                    loge("Exception in addRoute for non-gateway: " + e);
                 }
             }
         }
@@ -8255,10 +9313,10 @@
             if (!route.hasGateway()) continue;
             if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId);
             try {
-                mNetd.networkAddRouteParcel(netId, convertRouteInfo(route));
+                mRoutingCoordinatorService.addRoute(netId, route);
             } catch (Exception e) {
                 if ((route.getGateway() instanceof Inet4Address) || VDBG) {
-                    loge("Exception in networkAddRouteParcel for gateway: " + e);
+                    loge("Exception in addRoute for gateway: " + e);
                 }
             }
         }
@@ -8266,18 +9324,18 @@
         for (RouteInfo route : routeDiff.removed) {
             if (VDBG || DDBG) log("Removing Route [" + route + "] from network " + netId);
             try {
-                mNetd.networkRemoveRouteParcel(netId, convertRouteInfo(route));
+                mRoutingCoordinatorService.removeRoute(netId, route);
             } catch (Exception e) {
-                loge("Exception in networkRemoveRouteParcel: " + e);
+                loge("Exception in removeRoute: " + e);
             }
         }
 
         for (RouteInfo route : routeDiff.updated) {
             if (VDBG || DDBG) log("Updating Route [" + route + "] from network " + netId);
             try {
-                mNetd.networkUpdateRouteParcel(netId, convertRouteInfo(route));
+                mRoutingCoordinatorService.updateRoute(netId, route);
             } catch (Exception e) {
-                loge("Exception in networkUpdateRouteParcel: " + e);
+                loge("Exception in updateRoute: " + e);
             }
         }
         return !routeDiff.added.isEmpty() || !routeDiff.removed.isEmpty()
@@ -8337,6 +9395,87 @@
         }
     }
 
+    /**
+     * 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 VPN interface and not used by any other
+     *      interfaces even non-VPN ones
+     * 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) {
+            if (!agent.isVPN() || agent.isDestroyed()) {
+                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(
@@ -8509,8 +9648,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
@@ -8519,6 +9657,43 @@
         }
     }
 
+    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
+        //   2. The requestor uid matches the uid with the callback
+        //   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.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                    && nr.getRequestorUid() == uid
+                    && getSubscriptionIdFromNetworkCaps(nr.networkCapabilities) == subId
+                    && !hasConnectivityRestrictedNetworksPermission(uid, true)) {
+                declareNetworkRequestUnfulfillable(nr);
+            }
+        }
+
+        // A NetworkAgent's allowedUids may need to be updated if the app has lost
+        // carrier config
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+            if (nai.networkCapabilities.getAllowedUidsNoCopy().contains(uid)
+                    && getSubscriptionIdFromNetworkCaps(nai.networkCapabilities) == subId) {
+                final NetworkCapabilities nc = new NetworkCapabilities(nai.networkCapabilities);
+                NetworkAgentInfo.restrictCapabilitiesFromNetworkAgent(
+                        nc,
+                        uid,
+                        false /* hasAutomotiveFeature (irrelevant) */,
+                        mDeps,
+                        mCarrierPrivilegeAuthenticator);
+                updateCapabilities(nai.getScore(), nai, nc);
+            }
+        }
+    }
+
     /**
      * Update the NetworkCapabilities for {@code nai} to {@code nc}. Specifically:
      *
@@ -8560,7 +9735,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);
 
@@ -8584,9 +9759,8 @@
         // This network might have been underlying another network. Propagate its capabilities.
         propagateUnderlyingNetworkCapabilities(nai.network);
 
-        if (!newNc.equalsTransportTypes(prevNc)) {
-            mDnsManager.updateTransportsForNetwork(
-                    nai.network.getNetId(), newNc.getTransportTypes());
+        if (meteredChanged || !newNc.equalsTransportTypes(prevNc)) {
+            mDnsManager.updateCapabilitiesForNetwork(nai.network.getNetId(), newNc);
         }
 
         maybeSendProxyBroadcast(nai, prevNc, newNc);
@@ -8597,6 +9771,146 @@
         updateCapabilities(nai.getScore(), nai, nai.networkCapabilities);
     }
 
+    private void maybeApplyMulticastRoutingConfig(@NonNull final NetworkAgentInfo nai,
+            final LocalNetworkConfig oldConfig,
+            final LocalNetworkConfig newConfig) {
+        final MulticastRoutingConfig oldUpstreamConfig =
+                oldConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+                        oldConfig.getUpstreamMulticastRoutingConfig();
+        final MulticastRoutingConfig oldDownstreamConfig =
+                oldConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+                        oldConfig.getDownstreamMulticastRoutingConfig();
+        final MulticastRoutingConfig newUpstreamConfig =
+                newConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+                        newConfig.getUpstreamMulticastRoutingConfig();
+        final MulticastRoutingConfig newDownstreamConfig =
+                newConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+                        newConfig.getDownstreamMulticastRoutingConfig();
+
+        if (oldUpstreamConfig.equals(newUpstreamConfig) &&
+            oldDownstreamConfig.equals(newDownstreamConfig)) {
+            return;
+        }
+
+        final String downstreamNetworkName = nai.linkProperties.getInterfaceName();
+        final LocalNetworkInfo lni = localNetworkInfoForNai(nai);
+        final Network upstreamNetwork = lni.getUpstreamNetwork();
+
+        if (upstreamNetwork != null) {
+            final String upstreamNetworkName =
+                    getLinkProperties(upstreamNetwork).getInterfaceName();
+            applyMulticastRoutingConfig(downstreamNetworkName, upstreamNetworkName, newConfig);
+        }
+    }
+
+    private void applyMulticastRoutingConfig(@NonNull String localNetworkInterfaceName,
+            @NonNull String upstreamNetworkInterfaceName,
+            @NonNull final LocalNetworkConfig config) {
+        if (mMulticastRoutingCoordinatorService == null) {
+            if (config.getDownstreamMulticastRoutingConfig().getForwardingMode() != FORWARD_NONE ||
+                config.getUpstreamMulticastRoutingConfig().getForwardingMode() != FORWARD_NONE) {
+                loge("Multicast routing is not supported, failed to configure " + config
+                        + " for " + localNetworkInterfaceName + " to "
+                        +  upstreamNetworkInterfaceName);
+            }
+            return;
+        }
+
+        mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig(localNetworkInterfaceName,
+                upstreamNetworkInterfaceName, config.getUpstreamMulticastRoutingConfig());
+        mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig
+                (upstreamNetworkInterfaceName, localNetworkInterfaceName,
+                        config.getDownstreamMulticastRoutingConfig());
+    }
+
+    private void disableMulticastRouting(@NonNull String localNetworkInterfaceName,
+            @NonNull String upstreamNetworkInterfaceName) {
+        if (mMulticastRoutingCoordinatorService == null) {
+            return;
+        }
+
+        mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig(localNetworkInterfaceName,
+                upstreamNetworkInterfaceName, MulticastRoutingConfig.CONFIG_FORWARD_NONE);
+        mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig
+                (upstreamNetworkInterfaceName, localNetworkInterfaceName,
+                        MulticastRoutingConfig.CONFIG_FORWARD_NONE);
+    }
+
+    // oldConfig is null iff this is the original registration of the local network config
+    private void handleUpdateLocalNetworkConfig(@NonNull final NetworkAgentInfo nai,
+            @Nullable final LocalNetworkConfig oldConfig,
+            @NonNull final LocalNetworkConfig newConfig) {
+        if (!nai.isLocalNetwork()) {
+            Log.wtf(TAG, "Ignoring update of a local network info on non-local network " + nai);
+            return;
+        }
+
+        if (VDBG) {
+            Log.v(TAG, "Update local network config " + nai.network.netId + " : " + newConfig);
+        }
+        final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
+        configBuilder.setUpstreamMulticastRoutingConfig(
+                newConfig.getUpstreamMulticastRoutingConfig());
+        configBuilder.setDownstreamMulticastRoutingConfig(
+                newConfig.getDownstreamMulticastRoutingConfig());
+
+        final NetworkRequest oldRequest =
+                (null == oldConfig) ? null : oldConfig.getUpstreamSelector();
+        final NetworkCapabilities oldCaps =
+                (null == oldRequest) ? null : oldRequest.networkCapabilities;
+        final NetworkRequestInfo oldNri =
+                null == oldRequest ? null : mNetworkRequests.get(oldRequest);
+        final NetworkAgentInfo oldSatisfier =
+                null == oldNri ? null : oldNri.getSatisfier();
+        final NetworkRequest newRequest = newConfig.getUpstreamSelector();
+        final NetworkCapabilities newCaps =
+                (null == newRequest) ? null : newRequest.networkCapabilities;
+        final boolean requestUpdated = !Objects.equals(newCaps, oldCaps);
+        if (null != oldRequest && requestUpdated) {
+            handleRemoveNetworkRequest(mNetworkRequests.get(oldRequest));
+            if (null == newRequest && null != oldSatisfier) {
+                // If there is an old satisfier, but no new request, then remove the old upstream.
+                removeLocalNetworkUpstream(nai, oldSatisfier);
+                nai.localNetworkConfig = configBuilder.build();
+                // When there is a new request, the rematch sees the new request and sends the
+                // LOCAL_NETWORK_INFO_CHANGED callbacks accordingly.
+                // 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,
+                        CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+                return;
+            }
+        }
+        if (null != newRequest && requestUpdated) {
+            // File the new request if :
+            //  - it has changed (requestUpdated), or
+            //  - it's the first time this local info (null == oldConfig)
+            // is updated and the request has not been filed yet.
+            // Requests for local info are always LISTEN_FOR_BEST, because they have at most one
+            // upstream (the best) but never request it to be brought up.
+            final NetworkRequest nr = new NetworkRequest(newCaps, ConnectivityManager.TYPE_NONE,
+                    nextNetworkRequestId(), LISTEN_FOR_BEST);
+            configBuilder.setUpstreamSelector(nr);
+            final NetworkRequestInfo nri = new NetworkRequestInfo(
+                    nai.creatorUid, nr, null /* messenger */, null /* binder */,
+                    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);
+            }
+            nai.localNetworkConfig = configBuilder.build();
+            // handleRegisterNetworkRequest causes a rematch. The rematch must happen after
+            // nai.localNetworkConfig is set, since it will base its callbacks on the old
+            // satisfier and the new request.
+            handleRegisterNetworkRequest(nri);
+        } else {
+            configBuilder.setUpstreamSelector(oldRequest);
+            nai.localNetworkConfig = configBuilder.build();
+        }
+        maybeApplyMulticastRoutingConfig(nai, oldConfig, newConfig);
+    }
+
     /**
      * Returns the interface which requires VPN isolation (ingress interface filtering).
      *
@@ -8828,7 +10142,6 @@
         final ArraySet<Integer> toAdd = new ArraySet<>(newUids);
         toRemove.removeAll(newUids);
         toAdd.removeAll(prevUids);
-
         try {
             if (!toAdd.isEmpty()) {
                 mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
@@ -8870,7 +10183,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
@@ -8925,6 +10238,33 @@
         releasePendingNetworkRequestWithDelay(pendingIntent);
     }
 
+    @Nullable
+    private LocalNetworkInfo localNetworkInfoForNai(@NonNull final NetworkAgentInfo nai) {
+        if (!nai.isLocalNetwork()) return null;
+        final Network upstream;
+        final NetworkRequest selector = nai.localNetworkConfig.getUpstreamSelector();
+        if (null == selector) {
+            upstream = null;
+        } else {
+            final NetworkRequestInfo upstreamNri = mNetworkRequests.get(selector);
+            final NetworkAgentInfo satisfier = upstreamNri.getSatisfier();
+            upstream = (null == satisfier) ? null : satisfier.network;
+        }
+        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.
@@ -8937,19 +10277,19 @@
             // are Type.LISTEN, but should not have NetworkCallbacks invoked.
             return;
         }
-        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);
+        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(
@@ -8960,15 +10300,13 @@
                 putParcelable(bundle, nc);
                 putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions(
                         networkAgent.linkProperties, nri.mPid, nri.mUid));
-                // For this notification, arg1 contains the blocked status.
-                msg.arg1 = arg1;
+                // The local network info is often null, so can't use the static putParcelable
+                // method here.
+                bundle.putParcelable(LocalNetworkInfo.class.getSimpleName(),
+                        localNetworkInfoForNai(networkAgent));
                 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(
@@ -8981,28 +10319,44 @@
                                 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 CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
+                if (!networkAgent.isLocalNetwork()) {
+                    Log.wtf(TAG, "Callback for local info for a non-local network");
+                    return;
+                }
+                putParcelable(bundle, localNetworkInfoForNai(networkAgent));
                 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());
         }
     }
 
@@ -9122,7 +10476,8 @@
         if (oldDefaultNetwork != null) {
             mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork);
         }
-        mNetworkActivityTracker.updateDataActivityTracking(newDefaultNetwork, oldDefaultNetwork);
+        mNetworkActivityTracker.updateDefaultNetwork(newDefaultNetwork, oldDefaultNetwork);
+        maybeDestroyPendingSockets(newDefaultNetwork, oldDefaultNetwork);
         mProxyTracker.setDefaultProxy(null != newDefaultNetwork
                 ? newDefaultNetwork.linkProperties.getHttpProxy() : null);
         resetHttpProxyForNonDefaultNetwork(oldDefaultNetwork);
@@ -9251,7 +10606,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);
     }
 
@@ -9264,7 +10619,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);
             }
         }
     }
@@ -9375,7 +10730,8 @@
             if (VDBG) log("rematch for " + newSatisfier.toShortString());
             if (null != previousRequest && null != previousSatisfier) {
                 if (VDBG || DDBG) {
-                    log("   accepting network in place of " + previousSatisfier.toShortString());
+                    log("   accepting network in place of " + previousSatisfier.toShortString()
+                            + " for " + newRequest);
                 }
                 previousSatisfier.removeRequest(previousRequest.requestId);
                 if (canSupportGracefulNetworkSwitch(previousSatisfier, newSatisfier)
@@ -9394,7 +10750,7 @@
                     previousSatisfier.lingerRequest(previousRequest.requestId, now);
                 }
             } else {
-                if (VDBG || DDBG) log("   accepting network in place of null");
+                if (VDBG || DDBG) log("   accepting network in place of null for " + newRequest);
             }
 
             // To prevent constantly CPU wake up for nascent timer, if a network comes up
@@ -9510,6 +10866,14 @@
         }
     }
 
+    private boolean hasSameInterfaceName(@Nullable final NetworkAgentInfo nai1,
+            @Nullable final NetworkAgentInfo nai2) {
+        if (null == nai1) return null == nai2;
+        if (null == nai2) return false;
+        return nai1.linkProperties.getInterfaceName()
+                .equals(nai2.linkProperties.getInterfaceName());
+    }
+
     private void applyNetworkReassignment(@NonNull final NetworkReassignment changes,
             final long now) {
         final Collection<NetworkAgentInfo> nais = mNetworkAgentInfos;
@@ -9535,6 +10899,50 @@
         // Process default network changes if applicable.
         processDefaultNetworkChanges(changes);
 
+        // Update forwarding rules for the upstreams of local networks. Do this before sending
+        // onAvailable so that by the time onAvailable is sent the forwarding rules are set up.
+        // Don't send CALLBACK_LOCAL_NETWORK_INFO_CHANGED yet though : they should be sent after
+        // onAvailable so clients know what network the change is about. Store such changes in
+        // an array that's only allocated if necessary (because it's almost never necessary).
+        ArrayList<NetworkAgentInfo> localInfoChangedAgents = null;
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+            if (!nai.isLocalNetwork()) continue;
+            final NetworkRequest nr = nai.localNetworkConfig.getUpstreamSelector();
+            if (null == nr) continue; // No upstream for this local network
+            final NetworkRequestInfo nri = mNetworkRequests.get(nr);
+            final NetworkReassignment.RequestReassignment change = changes.getReassignment(nri);
+            if (null == change) continue; // No change in upstreams for this network
+            final String fromIface = nai.linkProperties.getInterfaceName();
+            if (!hasSameInterfaceName(change.mOldNetwork, change.mNewNetwork)
+                    || change.mOldNetwork.isDestroyed()) {
+                // There can be a change with the same interface name if the new network is the
+                // replacement for the old network that was unregisteredAfterReplacement.
+                try {
+                    if (null != change.mOldNetwork) {
+                        mRoutingCoordinatorService.removeInterfaceForward(fromIface,
+                                change.mOldNetwork.linkProperties.getInterfaceName());
+                        disableMulticastRouting(fromIface,
+                                change.mOldNetwork.linkProperties.getInterfaceName());
+                    }
+                    // If the new upstream is already destroyed, there is no point in setting up
+                    // a forward (in fact, it might forward to the interface for some new network !)
+                    // Later when the upstream disconnects CS will try to remove the forward, which
+                    // is ignored with a benign log by RoutingCoordinatorService.
+                    if (null != change.mNewNetwork && !change.mNewNetwork.isDestroyed()) {
+                        mRoutingCoordinatorService.addInterfaceForward(fromIface,
+                                change.mNewNetwork.linkProperties.getInterfaceName());
+                        applyMulticastRoutingConfig(fromIface,
+                                change.mNewNetwork.linkProperties.getInterfaceName(),
+                                nai.localNetworkConfig);
+                    }
+                } catch (final RemoteException e) {
+                    loge("Can't update forwarding rules", e);
+                }
+            }
+            if (null == localInfoChangedAgents) localInfoChangedAgents = new ArrayList<>();
+            localInfoChangedAgents.add(nai);
+        }
+
         // Notify requested networks are available after the default net is switched, but
         // before LegacyTypeTracker sends legacy broadcasts
         for (final NetworkReassignment.RequestReassignment event :
@@ -9543,7 +10951,7 @@
                 notifyNetworkAvailable(event.mNewNetwork, event.mNetworkRequestInfo);
             } else {
                 callCallbackForRequest(event.mNetworkRequestInfo, event.mOldNetwork,
-                        ConnectivityManager.CALLBACK_LOST, 0);
+                        CALLBACK_LOST, 0);
             }
         }
 
@@ -9583,6 +10991,14 @@
             notifyNetworkLosing(nai, now);
         }
 
+        // Send LOCAL_NETWORK_INFO_CHANGED callbacks now that onAvailable and onLost have been sent.
+        if (null != localInfoChangedAgents) {
+            for (final NetworkAgentInfo nai : localInfoChangedAgents) {
+                notifyNetworkCallbacks(nai,
+                        CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+            }
+        }
+
         updateLegacyTypeTrackerAndVpnLockdownForRematch(changes, nais);
 
         // Tear down all unneeded networks.
@@ -9622,7 +11038,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(
@@ -9907,7 +11323,7 @@
             // If a rate limit has been configured and is applicable to this network (network
             // provides internet connectivity), apply it. The tc police filter cannot be attached
             // before the clsact qdisc is added which happens as part of updateLinkProperties ->
-            // updateInterfaces -> INetd#networkAddInterface.
+            // updateInterfaces -> RoutingCoordinatorManager#addInterfaceToNetwork
             // Note: in case of a system server crash, the NetworkController constructor in netd
             // (called when netd starts up) deletes the clsact qdisc of all interfaces.
             if (canNetworkBeRateLimited(networkAgent) && mIngressRateLimit >= 0) {
@@ -9939,10 +11355,25 @@
                 networkAgent.networkMonitor().notifyNetworkConnected(params.linkProperties,
                         params.networkCapabilities);
             }
-            final long delay = !avoidBadWifi() && activelyPreferBadWifi()
-                    ? ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS
-                    : DONT_ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS;
-            scheduleEvaluationTimeout(networkAgent.network, delay);
+            final long evaluationDelay;
+            if (!networkAgent.networkCapabilities.hasSingleTransport(TRANSPORT_WIFI)) {
+                // If the network is anything other than pure wifi, use the default timeout.
+                evaluationDelay = DEFAULT_EVALUATION_TIMEOUT_MS;
+            } else if (networkAgent.networkAgentConfig.isExplicitlySelected()) {
+                // If the network is explicitly selected, use the default timeout because it's
+                // shorter and the user is likely staring at the screen expecting it to validate
+                // right away.
+                evaluationDelay = DEFAULT_EVALUATION_TIMEOUT_MS;
+            } else if (avoidBadWifi() || !activelyPreferBadWifi()) {
+                // If avoiding bad wifi, or if not avoiding but also not preferring bad wifi
+                evaluationDelay = DEFAULT_EVALUATION_TIMEOUT_MS;
+            } else {
+                // It's wifi, automatically connected, and bad wifi is preferred : use the
+                // longer timeout to avoid the device switching to captive portals with bad
+                // signal or very slow response.
+                evaluationDelay = ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS;
+            }
+            scheduleEvaluationTimeout(networkAgent.network, evaluationDelay);
 
             // Whether a particular NetworkRequest listen should cause signal strength thresholds to
             // be communicated to a particular NetworkAgent depends only on the network's immutable,
@@ -9963,11 +11394,20 @@
                     SystemClock.elapsedRealtime(), mNascentDelayMs);
             networkAgent.setInactive();
 
+            if (mTrackMultiNetworkActivities) {
+                // Start tracking activity of this network.
+                // This must be called before rematchAllNetworksAndRequests since the network
+                // should be tracked when the network becomes the default network.
+                // This method does not trigger any callbacks or broadcasts. Callbacks or broadcasts
+                // can be triggered later if this network becomes the default network.
+                mNetworkActivityTracker.setupDataActivityTracking(networkAgent);
+            }
+
             // Consider network even though it is not yet validated.
             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()) {
@@ -10000,31 +11440,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);
@@ -10059,10 +11516,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);
             }
         }
@@ -10079,8 +11538,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;
             }
@@ -10088,7 +11548,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);
                 }
             }
@@ -10362,6 +11822,27 @@
     }
 
     private class ShellCmd extends BasicShellCommandHandler {
+
+        private Boolean parseBooleanArgument(final String arg) {
+            if ("true".equals(arg)) {
+                return true;
+            } else if ("false".equals(arg)) {
+                return false;
+            } else {
+                getOutPrintWriter().println("Invalid boolean argument: " + arg);
+                return null;
+            }
+        }
+
+        private Integer parseIntegerArgument(final String arg) {
+            try {
+                return Integer.valueOf(arg);
+            } catch (NumberFormatException ne) {
+                getOutPrintWriter().println("Invalid integer argument: " + arg);
+                return null;
+            }
+        }
+
         @Override
         public int onCommand(String cmd) {
             if (cmd == null) {
@@ -10390,6 +11871,94 @@
                             onHelp();
                             return -1;
                         }
+                    case "set-chain3-enabled": {
+                        final Boolean enabled = parseBooleanArgument(getNextArg());
+                        if (null == enabled) {
+                            onHelp();
+                            return -1;
+                        }
+                        Log.i(TAG, (enabled ? "En" : "Dis") + "abled FIREWALL_CHAIN_OEM_DENY_3");
+                        setFirewallChainEnabled(ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3,
+                                enabled);
+                        return 0;
+                    }
+                    case "get-chain3-enabled": {
+                        final boolean chainEnabled = getFirewallChainEnabled(
+                                ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3);
+                        pw.println("chain:" + (chainEnabled ? "enabled" : "disabled"));
+                        return 0;
+                    }
+                    case "set-package-networking-enabled": {
+                        final Boolean enabled = parseBooleanArgument(getNextArg());
+                        final String packageName = getNextArg();
+                        if (null == enabled || null == packageName) {
+                            onHelp();
+                            return -1;
+                        }
+                        // Throws NameNotFound if the package doesn't exist.
+                        final int appId = setPackageFirewallRule(
+                                ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3,
+                                packageName, enabled ? FIREWALL_RULE_DEFAULT : FIREWALL_RULE_DENY);
+                        final String msg = (enabled ? "Enabled" : "Disabled")
+                                + " networking for " + packageName + ", appId " + appId;
+                        Log.i(TAG, msg);
+                        pw.println(msg);
+                        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);
+                        if (FIREWALL_RULE_ALLOW == rule || FIREWALL_RULE_DEFAULT == rule) {
+                            pw.println(packageName + ":" + "allow");
+                        } else if (FIREWALL_RULE_DENY == rule) {
+                            pw.println(packageName + ":" + "deny");
+                        } else {
+                            throw new IllegalStateException("Unknown rule " + rule + " for package "
+                                    + packageName);
+                        }
+                        return 0;
+                    }
+                    case "set-background-networking-enabled-for-uid": {
+                        final Integer uid = parseIntegerArgument(getNextArg());
+                        final Boolean enabled = parseBooleanArgument(getNextArg());
+                        if (null == enabled || null == uid) {
+                            onHelp();
+                            return -1;
+                        }
+                        final int rule = enabled ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DEFAULT;
+                        setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, rule);
+                        final String msg = (enabled ? "Enabled" : "Disabled")
+                                + " background networking for  uid " + uid;
+                        Log.i(TAG, msg);
+                        pw.println(msg);
+                        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();
+                            return -1;
+                        }
+                        final int rule = getUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid);
+                        if (FIREWALL_RULE_ALLOW == rule) {
+                            pw.println(uid + ": allow");
+                        } else if (FIREWALL_RULE_DENY == rule  || FIREWALL_RULE_DEFAULT == rule) {
+                            pw.println(uid + ": deny");
+                        } else {
+                            throw new IllegalStateException(
+                                    "Unknown rule " + rule + " for uid " + uid);
+                        }
+                        return 0;
+                    }
                     case "reevaluate":
                         // Usage : adb shell cmd connectivity reevaluate <netId>
                         // If netId is omitted, then reevaluate the default network
@@ -10411,6 +11980,17 @@
                         Log.d(TAG, "Reevaluating network " + nai.network);
                         reportNetworkConnectivity(nai.network, !nai.isValidated());
                         return 0;
+                    case "bpf-get-cgroup-program-id": {
+                        // Usage : adb shell cmd connectivity bpf-get-cgroup-program-id <type>
+                        // Get cgroup bpf program Id for the given type. See BpfUtils#getProgramId
+                        // for more detail.
+                        // If type can't be parsed, this throws NumberFormatException, which
+                        // is passed back to adb who prints it.
+                        final int type = Integer.parseInt(getNextArg());
+                        final int ret = BpfUtils.getProgramId(type);
+                        pw.println(ret);
+                        return 0;
+                    }
                     default:
                         return handleDefaultCommands(cmd);
                 }
@@ -10430,6 +12010,19 @@
             pw.println("    Turn airplane mode on or off.");
             pw.println("  airplane-mode");
             pw.println("    Get airplane mode.");
+            pw.println("  set-chain3-enabled [true|false]");
+            pw.println("    Enable or disable FIREWALL_CHAIN_OEM_DENY_3 for debugging.");
+            pw.println("  get-chain3-enabled");
+            pw.println("    Returns whether FIREWALL_CHAIN_OEM_DENY_3 is enabled.");
+            pw.println("  set-package-networking-enabled [true|false] [package name]");
+            pw.println("    Set the deny bit in FIREWALL_CHAIN_OEM_DENY_3 to package. This has\n"
+                    + "    no effect if the chain is disabled.");
+            pw.println("  get-package-networking-enabled [package name]");
+            pw.println("    Get the deny bit in FIREWALL_CHAIN_OEM_DENY_3 for package.");
+            pw.println("  set-background-networking-enabled-for-uid [uid] [true|false]");
+            pw.println("    Set the allow bit in FIREWALL_CHAIN_BACKGROUND for the given uid.");
+            pw.println("  get-background-networking-enabled-for-uid [uid]");
+            pw.println("    Get the allow bit in FIREWALL_CHAIN_BACKGROUND for the given uid.");
         }
     }
 
@@ -10470,7 +12063,7 @@
 
         // Connection owner UIDs are visible only to the network stack and to the VpnService-based
         // VPN, if any, that applies to the UID that owns the connection.
-        if (checkNetworkStackPermission()) return uid;
+        if (hasNetworkStackPermission()) return uid;
 
         final NetworkAgentInfo vpn = getVpnForUid(uid);
         if (vpn == null || getVpnType(vpn) != VpnManager.TYPE_VPN_SERVICE
@@ -10730,7 +12323,7 @@
             if (report == null) {
                 continue;
             }
-            if (!checkConnectivityDiagnosticsPermissions(
+            if (!hasConnectivityDiagnosticsPermissions(
                     nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
                 continue;
             }
@@ -10893,7 +12486,7 @@
                 continue;
             }
 
-            if (!checkConnectivityDiagnosticsPermissions(
+            if (!hasConnectivityDiagnosticsPermissions(
                     nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
                 continue;
             }
@@ -10937,10 +12530,15 @@
         return false;
     }
 
+    @CheckResult
     @VisibleForTesting
-    boolean checkConnectivityDiagnosticsPermissions(
+    boolean hasConnectivityDiagnosticsPermissions(
             int callbackPid, int callbackUid, NetworkAgentInfo nai, String callbackPackageName) {
-        if (checkNetworkStackPermission(callbackPid, callbackUid)) {
+        if (hasNetworkStackPermission(callbackPid, callbackUid)) {
+            return true;
+        }
+        if (mAllowSysUiConnectivityReports
+                && hasSystemBarServicePermission(callbackPid, callbackUid)) {
             return true;
         }
 
@@ -10974,7 +12572,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(
@@ -10986,6 +12584,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);
 
@@ -11066,46 +12665,83 @@
         notifyDataStallSuspected(p, network.getNetId());
     }
 
+    /**
+     * Class to hold the information for network activity change event from idle timers
+     * {@link NetdCallback#onInterfaceClassActivityChanged(boolean, int, long, int)}
+     */
+    private static final class NetworkActivityParams {
+        public final boolean isActive;
+        // If TrackMultiNetworkActivities is enabled, idleTimer label is netid.
+        // If TrackMultiNetworkActivities is disabled, idleTimer label is transport type.
+        public final int label;
+        public final long timestampNs;
+        // Uid represents the uid that was responsible for waking the radio.
+        // -1 for no uid and uid is -1 if isActive is false.
+        public final int uid;
+
+        NetworkActivityParams(boolean isActive, int label, long timestampNs, int uid) {
+            this.isActive = isActive;
+            this.label = label;
+            this.timestampNs = timestampNs;
+            this.uid = uid;
+        }
+    }
+
     private class NetdCallback extends BaseNetdUnsolicitedEventListener {
         @Override
-        public void onInterfaceClassActivityChanged(boolean isActive, int transportType,
+        public void onInterfaceClassActivityChanged(boolean isActive, int label,
                 long timestampNs, int uid) {
-            mNetworkActivityTracker.setAndReportNetworkActive(isActive, transportType, timestampNs);
+            mHandler.sendMessage(mHandler.obtainMessage(EVENT_REPORT_NETWORK_ACTIVITY,
+                    new NetworkActivityParams(isActive, label, timestampNs, uid)));
         }
 
         @Override
         public void onInterfaceLinkStateChanged(@NonNull String iface, boolean up) {
-            for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-                nai.clatd.interfaceLinkStateChanged(iface, up);
-            }
+            mHandler.post(() -> {
+                for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+                    nai.clatd.handleInterfaceLinkStateChanged(iface, up);
+                }
+            });
         }
 
         @Override
         public void onInterfaceRemoved(@NonNull String iface) {
-            for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-                nai.clatd.interfaceRemoved(iface);
-            }
+            mHandler.post(() -> {
+                for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+                    nai.clatd.handleInterfaceRemoved(iface);
+                }
+            });
         }
     }
 
+    private final boolean mTrackMultiNetworkActivities;
     private final LegacyNetworkActivityTracker mNetworkActivityTracker;
 
     /**
      * Class used for updating network activity tracking with netd and notify network activity
      * changes.
      */
-    private static final class LegacyNetworkActivityTracker {
+    @VisibleForTesting
+    public static final class LegacyNetworkActivityTracker {
         private static final int NO_UID = -1;
         private final Context mContext;
         private final INetd mNetd;
+        private final Handler mHandler;
         private final RemoteCallbackList<INetworkActivityListener> mNetworkActivityListeners =
                 new RemoteCallbackList<>();
         // Indicate the current system default network activity is active or not.
-        @GuardedBy("mActiveIdleTimers")
-        private boolean mNetworkActive;
-        @GuardedBy("mActiveIdleTimers")
-        private final ArrayMap<String, IdleTimerParams> mActiveIdleTimers = new ArrayMap<>();
-        private final Handler mHandler;
+        // This needs to be volatile to allow non handler threads to read this value without lock.
+        // If there is no default network, default network is considered active to keep the existing
+        // behavior. Initial value is used until first connect to the default network.
+        private volatile boolean mIsDefaultNetworkActive = true;
+        private Network mDefaultNetwork;
+        // Key is netId. Value is configured idle timer information.
+        private final SparseArray<IdleTimerParams> mActiveIdleTimers = new SparseArray<>();
+        private final boolean mTrackMultiNetworkActivities;
+        // Store netIds of Wi-Fi networks whose idletimers report that they are active
+        private final Set<Integer> mActiveWifiNetworks = new ArraySet<>();
+        // Store netIds of cellular networks whose idletimers report that they are active
+        private final Set<Integer> mActiveCellularNetworks = new ArraySet<>();
 
         private static class IdleTimerParams {
             public final int timeout;
@@ -11117,34 +12753,115 @@
             }
         }
 
-        LegacyNetworkActivityTracker(@NonNull Context context, @NonNull Handler handler,
-                @NonNull INetd netd) {
+        LegacyNetworkActivityTracker(@NonNull Context context, @NonNull INetd netd,
+                @NonNull Handler handler, boolean trackMultiNetworkActivities) {
             mContext = context;
             mNetd = netd;
             mHandler = handler;
+            mTrackMultiNetworkActivities = trackMultiNetworkActivities;
         }
 
-        public void setAndReportNetworkActive(boolean active, int transportType, long tsNanos) {
-            sendDataActivityBroadcast(transportTypeToLegacyType(transportType), active, tsNanos);
-            synchronized (mActiveIdleTimers) {
-                mNetworkActive = active;
-                // If there are no idle timers, it means that system is not monitoring
-                // activity, so the system default network for those default network
-                // unspecified apps is always considered active.
-                //
-                // TODO: If the mActiveIdleTimers is empty, netd will actually not send
-                // any network activity change event. Whenever this event is received,
-                // the mActiveIdleTimers should be always not empty. The legacy behavior
-                // is no-op. Remove to refer to mNetworkActive only.
-                if (mNetworkActive || mActiveIdleTimers.isEmpty()) {
-                    mHandler.sendMessage(mHandler.obtainMessage(EVENT_REPORT_NETWORK_ACTIVITY));
-                }
+        private void ensureRunningOnConnectivityServiceThread() {
+            if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+                throw new IllegalStateException("Not running on ConnectivityService thread: "
+                                + Thread.currentThread().getName());
             }
         }
 
-        // The network activity should only be updated from ConnectivityService handler thread
-        // when mActiveIdleTimers lock is held.
-        @GuardedBy("mActiveIdleTimers")
+        /**
+         * Update network activity and call BatteryStats to update radio power state if the
+         * mobile or Wi-Fi activity is changed.
+         * LegacyNetworkActivityTracker considers the mobile network is active if at least one
+         * mobile network is active since BatteryStatsService only maintains a single power state
+         * for the mobile network.
+         * The Wi-Fi network is also the same.
+         *
+         * {@link #setupDataActivityTracking} and {@link #removeDataActivityTracking} use
+         * TRANSPORT_CELLULAR as the transportType argument if the network has both cell and Wi-Fi
+         * transports.
+         */
+        private void maybeUpdateRadioPowerState(final int netId, final int transportType,
+                final boolean isActive, final int uid) {
+            if (transportType != TRANSPORT_WIFI && transportType != TRANSPORT_CELLULAR) {
+                Log.e(TAG, "Unexpected transportType in maybeUpdateRadioPowerState: "
+                        + transportType);
+                return;
+            }
+            final Set<Integer> activeNetworks = transportType == TRANSPORT_WIFI
+                    ? mActiveWifiNetworks : mActiveCellularNetworks;
+
+            final boolean wasEmpty = activeNetworks.isEmpty();
+            if (isActive) {
+                activeNetworks.add(netId);
+            } else {
+                activeNetworks.remove(netId);
+            }
+
+            if (wasEmpty != activeNetworks.isEmpty()) {
+                updateRadioPowerState(isActive, transportType, uid);
+            }
+        }
+
+        private void handleDefaultNetworkActivity(final int transportType,
+                final boolean isActive, final long timestampNs) {
+            mIsDefaultNetworkActive = isActive;
+            sendDataActivityBroadcast(transportTypeToLegacyType(transportType),
+                    isActive, timestampNs);
+            if (isActive) {
+                reportNetworkActive();
+            }
+        }
+
+        private void handleReportNetworkActivityWithNetIdLabel(
+                NetworkActivityParams activityParams) {
+            final int netId = activityParams.label;
+            final IdleTimerParams idleTimerParams = mActiveIdleTimers.get(netId);
+            if (idleTimerParams == null) {
+                // This network activity change is not tracked anymore
+                // This can happen if netd callback post activity change event message but idle
+                // timer is removed before processing this message.
+                return;
+            }
+            // TODO: if a network changes transports, storing the transport type in the
+            // IdleTimerParams is not correct. Consider getting it from the network's
+            // NetworkCapabilities instead.
+            final int transportType = idleTimerParams.transportType;
+            maybeUpdateRadioPowerState(netId, transportType,
+                    activityParams.isActive, activityParams.uid);
+
+            if (mDefaultNetwork == null || mDefaultNetwork.netId != netId) {
+                // This activity change is not for the default network.
+                return;
+            }
+
+            handleDefaultNetworkActivity(transportType, activityParams.isActive,
+                    activityParams.timestampNs);
+        }
+
+        private void handleReportNetworkActivityWithTransportTypeLabel(
+                NetworkActivityParams activityParams) {
+            if (mActiveIdleTimers.size() == 0) {
+                // This activity change is not for the current default network.
+                // This can happen if netd callback post activity change event message but
+                // the default network is lost before processing this message.
+                return;
+            }
+            handleDefaultNetworkActivity(activityParams.label, activityParams.isActive,
+                    activityParams.timestampNs);
+        }
+
+        /**
+         * Handle network activity change
+         */
+        public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
+            ensureRunningOnConnectivityServiceThread();
+            if (mTrackMultiNetworkActivities) {
+                handleReportNetworkActivityWithNetIdLabel(activityParams);
+            } else {
+                handleReportNetworkActivityWithTransportTypeLabel(activityParams);
+            }
+        }
+
         private void reportNetworkActive() {
             final int length = mNetworkActivityListeners.beginBroadcast();
             if (DDBG) log("reportNetworkActive, notify " + length + " listeners");
@@ -11161,13 +12878,6 @@
             }
         }
 
-        @GuardedBy("mActiveIdleTimers")
-        public void handleReportNetworkActivity() {
-            synchronized (mActiveIdleTimers) {
-                reportNetworkActive();
-            }
-        }
-
         // This is deprecated and only to support legacy use cases.
         private int transportTypeToLegacyType(int type) {
             switch (type) {
@@ -11205,18 +12915,49 @@
         }
 
         /**
+         * Get idle timer label
+         */
+        @VisibleForTesting
+        public static int getIdleTimerLabel(final boolean trackMultiNetworkActivities,
+                final int netId, final int transportType) {
+            return trackMultiNetworkActivities ? netId : transportType;
+        }
+
+        private boolean maybeCreateIdleTimer(
+                String iface, int netId, int timeout, int transportType) {
+            if (timeout <= 0 || iface == null) return false;
+            try {
+                final String label = Integer.toString(getIdleTimerLabel(
+                        mTrackMultiNetworkActivities, netId, transportType));
+                mNetd.idletimerAddInterface(iface, timeout, label);
+                mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, transportType));
+                return true;
+            } catch (Exception e) {
+                loge("Exception in createIdleTimer", e);
+                return false;
+            }
+        }
+
+        /**
          * Setup data activity tracking for the given network.
          *
          * Every {@code setupDataActivityTracking} should be paired with a
          * {@link #removeDataActivityTracking} for cleanup.
+         *
+         * @return true if the idleTimer is added to the network, false otherwise
          */
-        private void setupDataActivityTracking(NetworkAgentInfo networkAgent) {
+        private boolean setupDataActivityTracking(NetworkAgentInfo networkAgent) {
+            ensureRunningOnConnectivityServiceThread();
             final String iface = networkAgent.linkProperties.getInterfaceName();
+            final int netId = networkAgent.network().netId;
 
             final int timeout;
             final int type;
 
-            if (networkAgent.networkCapabilities.hasTransport(
+            if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+                // Do not track VPN network.
+                return false;
+            } else if (networkAgent.networkCapabilities.hasTransport(
                     NetworkCapabilities.TRANSPORT_CELLULAR)) {
                 timeout = Settings.Global.getInt(mContext.getContentResolver(),
                         ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_MOBILE,
@@ -11229,38 +12970,35 @@
                         15);
                 type = NetworkCapabilities.TRANSPORT_WIFI;
             } else {
-                return; // do not track any other networks
+                return false; // do not track any other networks
             }
 
-            updateRadioPowerState(true /* isActive */, type);
-
-            if (timeout > 0 && iface != null) {
-                try {
-                    synchronized (mActiveIdleTimers) {
-                        // Networks start up.
-                        mNetworkActive = true;
-                        mActiveIdleTimers.put(iface, new IdleTimerParams(timeout, type));
-                        mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type));
-                        reportNetworkActive();
-                    }
-                } catch (Exception e) {
-                    // You shall not crash!
-                    loge("Exception in setupDataActivityTracking " + e);
-                }
+            final boolean hasIdleTimer = maybeCreateIdleTimer(iface, netId, timeout, type);
+            if (hasIdleTimer || !mTrackMultiNetworkActivities) {
+                // If trackMultiNetwork is disabled, NetworkActivityTracker updates radio power
+                // state in all cases. If trackMultiNetwork is enabled, it updates radio power
+                // state only about a network that has an idletimer.
+                maybeUpdateRadioPowerState(netId, type, true /* isActive */, NO_UID);
             }
+            return hasIdleTimer;
         }
 
         /**
          * Remove data activity tracking when network disconnects.
          */
-        private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+        public void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+            ensureRunningOnConnectivityServiceThread();
             final String iface = networkAgent.linkProperties.getInterfaceName();
+            final int netId = networkAgent.network().netId;
             final NetworkCapabilities caps = networkAgent.networkCapabilities;
 
             if (iface == null) return;
 
             final int type;
-            if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+            if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+                // Do not track VPN network.
+                return;
+            } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
                 type = NetworkCapabilities.TRANSPORT_CELLULAR;
             } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
                 type = NetworkCapabilities.TRANSPORT_WIFI;
@@ -11269,40 +13007,70 @@
             }
 
             try {
-                updateRadioPowerState(false /* isActive */, type);
-                synchronized (mActiveIdleTimers) {
-                    final IdleTimerParams params = mActiveIdleTimers.remove(iface);
-                    // The call fails silently if no idle timer setup for this interface
-                    mNetd.idletimerRemoveInterface(iface, params.timeout,
-                            Integer.toString(params.transportType));
+                maybeUpdateRadioPowerState(netId, type, false /* isActive */, NO_UID);
+                final IdleTimerParams params = mActiveIdleTimers.get(netId);
+                if (params == null) {
+                    // IdleTimer is not added if the configured timeout is 0 or negative value
+                    return;
                 }
+                mActiveIdleTimers.remove(netId);
+                final String label = Integer.toString(getIdleTimerLabel(
+                        mTrackMultiNetworkActivities, netId, params.transportType));
+                        // The call fails silently if no idle timer setup for this interface
+                mNetd.idletimerRemoveInterface(iface, params.timeout, label);
             } catch (Exception e) {
                 // You shall not crash!
                 loge("Exception in removeDataActivityTracking " + e);
             }
         }
 
-        /**
-         * Update data activity tracking when network state is updated.
-         */
-        public void updateDataActivityTracking(NetworkAgentInfo newNetwork,
-                NetworkAgentInfo oldNetwork) {
-            if (newNetwork != null) {
-                setupDataActivityTracking(newNetwork);
+        private void updateDefaultNetworkActivity(NetworkAgentInfo defaultNetwork,
+                boolean hasIdleTimer) {
+            if (defaultNetwork != null) {
+                mDefaultNetwork = defaultNetwork.network();
+                mIsDefaultNetworkActive = true;
+                // If only the default network is tracked, callbacks are called only when the
+                // network has the idle timer.
+                if (mTrackMultiNetworkActivities || hasIdleTimer) {
+                    reportNetworkActive();
+                }
+            } else {
+                mDefaultNetwork = null;
+                // If there is no default network, default network is considered active to keep the
+                // existing behavior.
+                mIsDefaultNetworkActive = true;
             }
-            if (oldNetwork != null) {
+        }
+
+        /**
+         * Update the default network this class tracks the activity of.
+         */
+        public void updateDefaultNetwork(NetworkAgentInfo newNetwork,
+                NetworkAgentInfo oldNetwork) {
+            ensureRunningOnConnectivityServiceThread();
+            // If TrackMultiNetworkActivities is enabled, devices add idleTimer when the network is
+            // first connected and remove when the network is disconnected.
+            // If TrackMultiNetworkActivities is disabled, devices add idleTimer when the network
+            // becomes the default network and remove when the network becomes no longer the default
+            // network.
+            boolean hasIdleTimer = false;
+            if (!mTrackMultiNetworkActivities && newNetwork != null) {
+                hasIdleTimer = setupDataActivityTracking(newNetwork);
+            }
+            updateDefaultNetworkActivity(newNetwork, hasIdleTimer);
+            if (!mTrackMultiNetworkActivities && oldNetwork != null) {
                 removeDataActivityTracking(oldNetwork);
             }
         }
 
-        private void updateRadioPowerState(boolean isActive, int transportType) {
+        private void updateRadioPowerState(boolean isActive, int transportType, int uid) {
             final BatteryStatsManager bs = mContext.getSystemService(BatteryStatsManager.class);
             switch (transportType) {
                 case NetworkCapabilities.TRANSPORT_CELLULAR:
-                    bs.reportMobileRadioPowerState(isActive, NO_UID);
+                    bs.reportMobileRadioPowerState(isActive, uid);
                     break;
                 case NetworkCapabilities.TRANSPORT_WIFI:
-                    bs.reportWifiRadioPowerState(isActive, NO_UID);
+                    bs.reportWifiRadioPowerState(isActive, uid);
                     break;
                 default:
                     logw("Untracked transport type:" + transportType);
@@ -11310,15 +13078,7 @@
         }
 
         public boolean isDefaultNetworkActive() {
-            synchronized (mActiveIdleTimers) {
-                // If there are no idle timers, it means that system is not monitoring activity,
-                // so the default network is always considered active.
-                //
-                // TODO : Distinguish between the cases where mActiveIdleTimers is empty because
-                // tracking is disabled (negative idle timer value configured), or no active default
-                // network. In the latter case, this reports active but it should report inactive.
-                return mNetworkActive || mActiveIdleTimers.isEmpty();
-            }
+            return mIsDefaultNetworkActive;
         }
 
         public void registerNetworkActivityListener(@NonNull INetworkActivityListener l) {
@@ -11330,15 +13090,26 @@
         }
 
         public void dump(IndentingPrintWriter pw) {
-            synchronized (mActiveIdleTimers) {
-                pw.print("mNetworkActive="); pw.println(mNetworkActive);
-                pw.println("Idle timers:");
-                for (HashMap.Entry<String, IdleTimerParams> ent : mActiveIdleTimers.entrySet()) {
-                    pw.print("  "); pw.print(ent.getKey()); pw.println(":");
-                    final IdleTimerParams params = ent.getValue();
+            pw.print("mTrackMultiNetworkActivities="); pw.println(mTrackMultiNetworkActivities);
+            pw.print("mIsDefaultNetworkActive="); pw.println(mIsDefaultNetworkActive);
+            pw.print("mDefaultNetwork="); pw.println(mDefaultNetwork);
+            pw.println("Idle timers:");
+            try {
+                for (int i = 0; i < mActiveIdleTimers.size(); i++) {
+                    pw.print("  "); pw.print(mActiveIdleTimers.keyAt(i)); pw.println(":");
+                    final IdleTimerParams params = mActiveIdleTimers.valueAt(i);
                     pw.print("    timeout="); pw.print(params.timeout);
                     pw.print(" type="); pw.println(params.transportType);
                 }
+                pw.println("WiFi active networks: " + mActiveWifiNetworks);
+                pw.println("Cellular active networks: " + mActiveCellularNetworks);
+            } catch (Exception e) {
+                // mActiveIdleTimers, mActiveWifiNetworks, and mActiveCellularNetworks should only
+                // be accessed from handler thread, except dump(). As dump() is never called in
+                // normal usage, it would be needlessly expensive to lock the collection only for
+                // its benefit. Also, they are not expected to be updated frequently.
+                // So catching the exception and logging.
+                pw.println("Failed to dump NetworkActivityTracker: " + e);
             }
         }
     }
@@ -11409,8 +13180,8 @@
         if (um.isManagedProfile(profile.getIdentifier())) {
             return true;
         }
-        if (mDeps.isAtLeastT() && dpm.getDeviceOwner() != null) return true;
-        return false;
+
+        return mDeps.isAtLeastT() && dpm.getDeviceOwnerComponentOnAnyUser() != null;
     }
 
     /**
@@ -11634,16 +13405,27 @@
 
     @VisibleForTesting
     @NonNull
-    ArraySet<NetworkRequestInfo> createNrisFromMobileDataPreferredUids(
-            @NonNull final Set<Integer> uids) {
+    ArraySet<NetworkRequestInfo> createNrisForPreferenceOrder(@NonNull final Set<Integer> uids,
+            @NonNull final List<NetworkRequest> requests,
+            final int preferenceOrder) {
         final ArraySet<NetworkRequestInfo> nris = new ArraySet<>();
         if (uids.size() == 0) {
             // Should not create NetworkRequestInfo if no preferences. Without uid range in
             // NetworkRequestInfo, makeDefaultForApps() would treat it as a illegal NRI.
-            if (DBG) log("Don't create NetworkRequestInfo because no preferences");
             return nris;
         }
 
+        final Set<UidRange> ranges = new ArraySet<>();
+        for (final int uid : uids) {
+            ranges.add(new UidRange(uid, uid));
+        }
+        setNetworkRequestUids(requests, ranges);
+        nris.add(new NetworkRequestInfo(Process.myUid(), requests, preferenceOrder));
+        return nris;
+    }
+
+    ArraySet<NetworkRequestInfo> createNrisFromMobileDataPreferredUids(
+            @NonNull final Set<Integer> uids) {
         final List<NetworkRequest> requests = new ArrayList<>();
         // The NRI should be comprised of two layers:
         // - The request for the mobile network preferred.
@@ -11652,14 +13434,29 @@
                 TRANSPORT_CELLULAR, NetworkRequest.Type.REQUEST));
         requests.add(createDefaultInternetRequestForTransport(
                 TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
-        final Set<UidRange> ranges = new ArraySet<>();
-        for (final int uid : uids) {
-            ranges.add(new UidRange(uid, uid));
-        }
-        setNetworkRequestUids(requests, ranges);
-        nris.add(new NetworkRequestInfo(Process.myUid(), requests,
-                PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED));
-        return nris;
+        return createNrisForPreferenceOrder(uids, requests, PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED
+        );
+    }
+
+    ArraySet<NetworkRequestInfo> createMultiLayerNrisFromSatelliteNetworkFallbackUids(
+            @NonNull final Set<Integer> uids) {
+        final List<NetworkRequest> requests = new ArrayList<>();
+
+        // request: track default(unrestricted internet network)
+        requests.add(createDefaultInternetRequestForTransport(
+                TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
+
+        // 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));
+
+        return createNrisForPreferenceOrder(uids, requests, PREFERENCE_ORDER_SATELLITE_FALLBACK);
     }
 
     private void handleMobileDataPreferredUidsChanged() {
@@ -11671,6 +13468,16 @@
         rematchAllNetworksAndRequests();
     }
 
+    private void handleSetSatelliteNetworkPreference(
+            @NonNull final Set<Integer> satelliteNetworkPreferredUids) {
+        removeDefaultNetworkRequestsForPreference(PREFERENCE_ORDER_SATELLITE_FALLBACK);
+        addPerAppDefaultNetworkRequests(
+                createMultiLayerNrisFromSatelliteNetworkFallbackUids(satelliteNetworkPreferredUids)
+        );
+        // Finally, rematch.
+        rematchAllNetworksAndRequests();
+    }
+
     private void handleIngressRateLimitChanged() {
         final long oldIngressRateLimit = mIngressRateLimit;
         mIngressRateLimit = ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond(
@@ -11873,7 +13680,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);
@@ -12094,40 +13914,59 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     @Override
-    public void updateMeteredNetworkAllowList(final int uid, final boolean add) {
+    public void setDataSaverEnabled(final boolean enable) {
         enforceNetworkStackOrSettingsPermission();
-
         try {
-            if (add) {
-                mBpfNetMaps.addNiceApp(uid);
-            } else {
-                mBpfNetMaps.removeNiceApp(uid);
+            final boolean ret = mNetd.bandwidthEnableDataSaver(enable);
+            if (!ret) {
+                throw new IllegalStateException("Error when changing iptables: " + enable);
             }
-        } catch (ServiceSpecificException e) {
+        } catch (RemoteException e) {
+            // Lack of permission or binder errors.
             throw new IllegalStateException(e);
         }
+
+        synchronized (mBlockedStatusTrackingUids) {
+            try {
+                mBpfNetMaps.setDataSaverEnabled(enable);
+            } catch (ServiceSpecificException | UnsupportedOperationException e) {
+                Log.e(TAG, "Failed to set data saver " + enable + " : " + e);
+                return;
+            }
+
+            if (shouldTrackUidsForBlockedStatusCallbacks()) {
+                updateTrackingUidsBlockedReasons();
+            }
+        }
     }
 
-    @Override
-    public void updateMeteredNetworkDenyList(final int uid, final boolean add) {
-        enforceNetworkStackOrSettingsPermission();
-
-        try {
-            if (add) {
-                mBpfNetMaps.addNaughtyApp(uid);
-            } else {
-                mBpfNetMaps.removeNaughtyApp(uid);
-            }
-        } catch (ServiceSpecificException e) {
-            throw new IllegalStateException(e);
+    private int setPackageFirewallRule(final int chain, final String packageName, final int rule)
+            throws PackageManager.NameNotFoundException {
+        final PackageManager pm = mContext.getPackageManager();
+        final int appId = UserHandle.getAppId(pm.getPackageUid(packageName, 0 /* flags */));
+        if (appId < Process.FIRST_APPLICATION_UID) {
+            throw new RuntimeException("Can't set package firewall rule for system app "
+                    + packageName + " with appId " + appId);
         }
+        for (final UserHandle uh : mUserManager.getUserHandles(false /* excludeDying */)) {
+            final int uid = uh.getUid(appId);
+            setUidFirewallRule(chain, uid, rule);
+        }
+        return appId;
     }
 
     @Override
     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);
 
@@ -12135,13 +13974,32 @@
             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();
+        final int appId = UserHandle.getAppId(pm.getPackageUid(packageName, 0 /* flags */));
+        return getUidFirewallRule(chain, appId);
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     @Override
     public int getUidFirewallRule(final int chain, final int uid) {
         enforceNetworkStackOrSettingsPermission();
@@ -12155,12 +14013,16 @@
             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:
             case ConnectivityManager.FIREWALL_CHAIN_POWERSAVE:
             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:
@@ -12171,31 +14033,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 {
-        if (mBpfNetMaps.isFirewallAllowList(chain)) {
+        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) {
@@ -12207,10 +14109,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);
     }
 
@@ -12218,7 +14168,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
@@ -12226,4 +14206,22 @@
         enforceNetworkStackPermission(mContext);
         return mCdmps;
     }
+
+    @Override
+    public IBinder getRoutingCoordinatorService() {
+        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/NetIdManager.java b/service/src/com/android/server/NetIdManager.java
index 61925c8..27b6b9b 100644
--- a/service/src/com/android/server/NetIdManager.java
+++ b/service/src/com/android/server/NetIdManager.java
@@ -27,6 +27,16 @@
  * Class used to reserve and release net IDs.
  *
  * <p>Instances of this class are thread-safe.
+ *
+ * NetIds are currently 16 bits long and consume 16 bits in the fwmark.
+ * The reason they are large is that applications might get confused if the netId counter
+ * wraps - for example, Network#equals would return true for a current network
+ * and a long-disconnected network.
+ * We could in theory fix this by splitting the identifier in two, e.g., a 24-bit generation
+ * counter and an 8-bit netId. Java Network objects would be constructed from the full 32-bit
+ * number, but only the 8-bit number would be used by netd and the fwmark.
+ * We'd have to fix all code that assumes that it can take a netId or a mark and construct
+ * a Network object from it.
  */
 public class NetIdManager {
     // Sequence number for Networks; keep in sync with system/netd/NetworkController.cpp
diff --git a/service/src/com/android/server/ServiceManagerWrapper.java b/service/src/com/android/server/ServiceManagerWrapper.java
new file mode 100644
index 0000000..6d99f33
--- /dev/null
+++ b/service/src/com/android/server/ServiceManagerWrapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.RequiresApi;
+import android.os.IBinder;
+import android.os.Build;
+import android.os.ServiceManager;
+
+/** Provides a way to access {@link ServiceManager#waitForService} API. */
+@RequiresApi(Build.VERSION_CODES.S)
+public final class ServiceManagerWrapper {
+    static {
+        System.loadLibrary("service-connectivity");
+    }
+
+    private ServiceManagerWrapper() {}
+
+    /**
+     * Returns the specified service from the service manager.
+     *
+     * If the service is not running, service manager will attempt to start it, and this function
+     * will wait for it to be ready.
+     *
+     * @return {@code null} only if there are permission problems or fatal errors
+     */
+    public static IBinder waitForService(String serviceName) {
+        return nativeWaitForService(serviceName);
+    }
+
+    private static native IBinder nativeWaitForService(String serviceName);
+}
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/UidOwnerValue.java b/service/src/com/android/server/UidOwnerValue.java
deleted file mode 100644
index d6c0e0d..0000000
--- a/service/src/com/android/server/UidOwnerValue.java
+++ /dev/null
@@ -1,35 +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.server;
-
-import com.android.net.module.util.Struct;
-
-/** Value type for per uid traffic control configuration map  */
-public class UidOwnerValue extends Struct {
-    // Allowed interface index. Only applicable if IIF_MATCH is set in the rule bitmask below.
-    @Field(order = 0, type = Type.S32)
-    public final int iif;
-
-    // A bitmask of match type.
-    @Field(order = 1, type = Type.U32)
-    public final long rule;
-
-    public UidOwnerValue(final int iif, final long rule) {
-        this.iif = iif;
-        this.rule = rule;
-    }
-}
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index fa7b404..31108fc 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -20,15 +20,11 @@
 import static android.net.SocketKeepalive.MIN_INTERVAL_SEC;
 import static android.net.SocketKeepalive.SUCCESS;
 import static android.net.SocketKeepalive.SUCCESS_PAUSED;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.SOL_SOCKET;
 import static android.system.OsConstants.SO_SNDTIMEO;
 
-import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
 import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
 
 import android.annotation.IntDef;
@@ -89,11 +85,12 @@
  */
 public class AutomaticOnOffKeepaliveTracker {
     private static final String TAG = "AutomaticOnOffKeepaliveTracker";
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
     private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
     private static final long LOW_TCP_POLLING_INTERVAL_MS = 1_000L;
     private static final int ADJUST_TCP_POLLING_DELAY_MS = 2000;
-    private static final String AUTOMATIC_ON_OFF_KEEPALIVE_VERSION =
-            "automatic_on_off_keepalive_version";
+    private static final String AUTOMATIC_ON_OFF_KEEPALIVE_DISABLE_FLAG =
+            "automatic_on_off_keepalive_disable_flag";
     public static final long METRICS_COLLECTION_DURATION_MS = 24 * 60 * 60 * 1_000L;
 
     // ConnectivityService parses message constants from itself and AutomaticOnOffKeepaliveTracker
@@ -216,30 +213,39 @@
             this.mKi = Objects.requireNonNull(ki);
             mCallback = ki.mCallback;
             mUnderpinnedNetwork = underpinnedNetwork;
-            if (autoOnOff && mDependencies.isFeatureEnabled(AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
-                    true /* defaultEnabled */)) {
+            // Reading DeviceConfig will check if the calling uid and calling package name are the
+            // same. Clear calling identity to align the calling uid and package
+            final boolean enabled = BinderUtils.withCleanCallingIdentity(
+                    () -> mDependencies.isTetheringFeatureNotChickenedOut(
+                            AUTOMATIC_ON_OFF_KEEPALIVE_DISABLE_FLAG));
+            if (autoOnOff && enabled) {
                 mAutomaticOnOffState = STATE_ENABLED;
                 if (null == ki.mFd) {
                     throw new IllegalArgumentException("fd can't be null with automatic "
                             + "on/off keepalives");
                 }
-                try {
-                    mFd = Os.dup(ki.mFd);
-                } catch (ErrnoException e) {
-                    Log.e(TAG, "Cannot dup fd: ", e);
-                    throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
-                }
                 mAlarmListener = () -> mConnectivityServiceHandler.obtainMessage(
                         CMD_MONITOR_AUTOMATIC_KEEPALIVE, mCallback.asBinder())
                         .sendToTarget();
             } else {
                 mAutomaticOnOffState = STATE_ALWAYS_ON;
-                // A null fd is acceptable in KeepaliveInfo for backward compatibility of
-                // PacketKeepalive API, but it must never happen with automatic keepalives.
-                // TODO : remove mFd from KeepaliveInfo or from this class.
-                mFd = ki.mFd;
                 mAlarmListener = null;
             }
+
+            // A null fd is acceptable in KeepaliveInfo for backward compatibility of
+            // PacketKeepalive API, but it must never happen with automatic keepalives.
+            // TODO : remove mFd from KeepaliveInfo.
+            mFd = dupFd(ki.mFd);
+        }
+
+        private FileDescriptor dupFd(FileDescriptor fd) throws InvalidSocketException {
+            try {
+                if (fd == null) return null;
+                return Os.dup(fd);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot dup fd: ", e);
+                throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+            }
         }
 
         @VisibleForTesting
@@ -287,6 +293,18 @@
             }
         }
 
+        /**
+         * Construct a new AutomaticOnOffKeepalive from existing AutomaticOnOffKeepalive with a
+         * new KeepaliveInfo.
+         */
+        public AutomaticOnOffKeepalive withKeepaliveInfo(KeepaliveTracker.KeepaliveInfo ki)
+                throws InvalidSocketException {
+            return new AutomaticOnOffKeepalive(
+                    ki,
+                    mAutomaticOnOffState != STATE_ALWAYS_ON /* autoOnOff */,
+                    mUnderpinnedNetwork);
+        }
+
         @Override
         public String toString() {
             return "AutomaticOnOffKeepalive [ "
@@ -316,12 +334,18 @@
 
         final long time = mDependencies.getElapsedRealtime();
         mMetricsWriteTimeBase = time % METRICS_COLLECTION_DURATION_MS;
-        final long triggerAtMillis = mMetricsWriteTimeBase + METRICS_COLLECTION_DURATION_MS;
-        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, TAG,
-                this::writeMetricsAndRescheduleAlarm, handler);
+        if (mKeepaliveStatsTracker.isEnabled()) {
+            final long triggerAtMillis = mMetricsWriteTimeBase + METRICS_COLLECTION_DURATION_MS;
+            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, TAG,
+                    this::writeMetricsAndRescheduleAlarm, handler);
+        }
     }
 
     private void writeMetricsAndRescheduleAlarm() {
+        // If the metrics is disabled, skip writing and scheduling the next alarm.
+        if (!mKeepaliveStatsTracker.isEnabled()) {
+            return;
+        }
         mKeepaliveStatsTracker.writeAndResetMetrics();
 
         final long time = mDependencies.getElapsedRealtime();
@@ -466,13 +490,25 @@
      * The message is expected to contain a KeepaliveTracker.KeepaliveInfo.
      */
     public void handleStartKeepalive(Message message) {
-        final AutomaticOnOffKeepalive autoKi = (AutomaticOnOffKeepalive) message.obj;
-        final int error = mKeepaliveTracker.handleStartKeepalive(autoKi.mKi);
+        final AutomaticOnOffKeepalive target = (AutomaticOnOffKeepalive) message.obj;
+        final Pair<Integer, KeepaliveTracker.KeepaliveInfo> res =
+                mKeepaliveTracker.handleStartKeepalive(target.mKi);
+        final int error = res.first;
         if (error != SUCCESS) {
-            mEventLog.log("Failed to start keepalive " + autoKi.mCallback + " on "
-                    + autoKi.getNetwork() + " with error " + error);
+            mEventLog.log("Failed to start keepalive " + target.mCallback + " on "
+                    + target.getNetwork() + " with error " + error);
             return;
         }
+        // Generate a new auto ki with the started keepalive info.
+        final AutomaticOnOffKeepalive autoKi;
+        try {
+            autoKi = target.withKeepaliveInfo(res.second);
+            target.close();
+        } catch (InvalidSocketException e) {
+            Log.wtf(TAG, "Fail to create AutomaticOnOffKeepalive", e);
+            return;
+        }
+
         mEventLog.log("Start keepalive " + autoKi.mCallback + " on " + autoKi.getNetwork());
         mKeepaliveStatsTracker.onStartKeepalive(
                 autoKi.getNetwork(),
@@ -502,14 +538,19 @@
      * @return SUCCESS if the keepalive is successfully starting and the error reason otherwise.
      */
     private int handleResumeKeepalive(@NonNull final KeepaliveTracker.KeepaliveInfo ki) {
-        final int error = mKeepaliveTracker.handleStartKeepalive(ki);
+        final Pair<Integer, KeepaliveTracker.KeepaliveInfo> res =
+                mKeepaliveTracker.handleStartKeepalive(ki);
+        final KeepaliveTracker.KeepaliveInfo startedKi = res.second;
+        final int error = res.first;
         if (error != SUCCESS) {
-            mEventLog.log("Failed to resume keepalive " + ki.mCallback + " on " + ki.mNai
-                    + " with error " + error);
+            mEventLog.log("Failed to resume keepalive " + startedKi.mCallback + " on "
+                    + startedKi.mNai + " with error " + error);
             return error;
         }
-        mKeepaliveStatsTracker.onResumeKeepalive(ki.getNai().network(), ki.getSlot());
-        mEventLog.log("Resumed successfully keepalive " + ki.mCallback + " on " + ki.mNai);
+
+        mKeepaliveStatsTracker.onResumeKeepalive(startedKi.getNai().network(), startedKi.getSlot());
+        mEventLog.log("Resumed successfully keepalive " + startedKi.mCallback
+                + " on " + startedKi.mNai);
 
         return SUCCESS;
     }
@@ -649,11 +690,17 @@
 
     /**
      * Dump AutomaticOnOffKeepaliveTracker state.
+     * This should be only be called in ConnectivityService handler thread.
      */
     public void dump(IndentingPrintWriter pw) {
+        ensureRunningOnHandlerThread();
         mKeepaliveTracker.dump(pw);
-        final boolean featureEnabled = mDependencies.isFeatureEnabled(
-                AUTOMATIC_ON_OFF_KEEPALIVE_VERSION, true /* defaultEnabled */);
+        // Reading DeviceConfig will check if the calling uid and calling package name are the same.
+        // Clear calling identity to align the calling uid and package so that it won't fail if cts
+        // would like to call dump()
+        final boolean featureEnabled = BinderUtils.withCleanCallingIdentity(
+                () -> mDependencies.isTetheringFeatureNotChickenedOut(
+                        AUTOMATIC_ON_OFF_KEEPALIVE_DISABLE_FLAG));
         pw.println("AutomaticOnOff enabled: " + featureEnabled);
         pw.increaseIndent();
         for (AutomaticOnOffKeepalive autoKi : mAutomaticOnOffKeepalives) {
@@ -665,6 +712,9 @@
         pw.increaseIndent();
         mEventLog.reverseDump(pw);
         pw.decreaseIndent();
+
+        pw.println();
+        mKeepaliveStatsTracker.dump(pw);
     }
 
     /**
@@ -719,7 +769,8 @@
     }
 
     private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
-            int networkMask) throws ErrnoException, InterruptedIOException {
+            int networkMask)
+            throws ErrnoException, InterruptedIOException {
         ensureRunningOnHandlerThread();
         // Build SocketDiag messages and cache it.
         if (mSockDiagMsg.get(family) == null) {
@@ -737,22 +788,18 @@
 
             try {
                 while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
-                    final int startPos = bytes.position();
+                    // NetlinkMessage.parse() will move the byte buffer position.
+                    // TODO: Parse dst address information to filter socket.
+                    final NetlinkMessage nlMsg = NetlinkMessage.parse(
+                            bytes, OsConstants.NETLINK_INET_DIAG);
+                    if (!(nlMsg instanceof InetDiagMessage)) {
+                        if (DBG) Log.e(TAG, "Not a SOCK_DIAG_BY_FAMILY msg");
+                        return false;
+                    }
 
-                    final int nlmsgLen = bytes.getInt();
-                    final int nlmsgType = bytes.getShort();
-                    if (isEndOfMessageOrError(nlmsgType)) return false;
-                    // TODO: Parse InetDiagMessage to get uid and dst address information to filter
-                    //  socket via NetlinkMessage.parse.
-
-                    // Skip the header to move to data part.
-                    bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
-
-                    if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
-                        if (Log.isLoggable(TAG, Log.DEBUG)) {
-                            bytes.position(startPos);
-                            final InetDiagMessage diagMsg = (InetDiagMessage) NetlinkMessage.parse(
-                                    bytes, OsConstants.NETLINK_INET_DIAG);
+                    final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
+                    if (isTargetTcpSocket(diagMsg, networkMark, networkMask)) {
+                        if (DBG) {
                             Log.d(TAG, String.format("Found open TCP connection by uid %d to %s"
                                             + " cookie %d",
                                     diagMsg.inetDiagMsg.idiag_uid,
@@ -777,26 +824,20 @@
         return false;
     }
 
-    private boolean isEndOfMessageOrError(int nlmsgType) {
-        return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
-    }
-
-    private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
-            int networkMask) {
-        final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
+    private boolean isTargetTcpSocket(@NonNull InetDiagMessage diagMsg,
+            int networkMark, int networkMask) {
+        final int mark = readSocketDataAndReturnMark(diagMsg);
         return (mark & networkMask) == networkMark;
     }
 
-    private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
-        final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
+    private int readSocketDataAndReturnMark(@NonNull InetDiagMessage diagMsg) {
         int mark = NetlinkUtils.INIT_MARK_VALUE;
         // Get socket mark
-        // TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
-        //  data.
-        while (bytes.position() < nextMsgOffset) {
-            final StructNlAttr nlattr = StructNlAttr.parse(bytes);
-            if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
-                mark = nlattr.getValueAsInteger();
+        for (StructNlAttr attr : diagMsg.nlAttrs) {
+            if (attr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
+                // The netlink attributes should contain only one INET_DIAG_MARK for each socket.
+                mark = attr.getValueAsInteger();
+                break;
             }
         }
         return mark;
@@ -848,7 +889,7 @@
         public FileDescriptor createConnectedNetlinkSocket()
                 throws ErrnoException, SocketException {
             final FileDescriptor fd = NetlinkUtils.createNetLinkInetDiagSocket();
-            NetlinkUtils.connectSocketToNetlink(fd);
+            NetlinkUtils.connectToKernel(fd);
             Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO,
                     StructTimeval.fromMillis(IO_TIMEOUT_MS));
             return fd;
@@ -921,20 +962,13 @@
         }
 
         /**
-         * Find out if a feature is enabled from DeviceConfig.
+         * Find out if a feature is not disabled from DeviceConfig.
          *
          * @param name The name of the property to look up.
-         * @param defaultEnabled whether to consider the feature enabled in the absence of
-         *                       the flag. This MUST be a statically-known constant.
          * @return whether the feature is enabled
          */
-        public boolean isFeatureEnabled(@NonNull final String name, final boolean defaultEnabled) {
-            // Reading DeviceConfig will check if the calling uid and calling package name are the
-            // same. Clear calling identity to align the calling uid and package so that it won't
-            // fail if cts would like to do the dump()
-            return BinderUtils.withCleanCallingIdentity(() ->
-                    DeviceConfigUtils.isFeatureEnabled(mContext, NAMESPACE_TETHERING, name,
-                    DeviceConfigUtils.TETHERING_MODULE_NAME, defaultEnabled));
+        public boolean isTetheringFeatureNotChickenedOut(@NonNull final String name) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(mContext, name);
         }
 
         /**
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index 4325763..f5fa4fb 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -16,10 +16,13 @@
 
 package com.android.server.connectivity;
 
-import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import static com.android.server.connectivity.ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -29,16 +32,22 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkSpecifier;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.TransportInfo;
+import android.net.wifi.WifiInfo;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.Log;
+import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.modules.utils.HandlerExecutor;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.networkstack.apishim.TelephonyManagerShimImpl;
 import com.android.networkstack.apishim.common.TelephonyManagerShim;
 import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim;
@@ -47,6 +56,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
 
 /**
  * Tracks the uid of the carrier privileged app that provides the carrier config.
@@ -54,7 +64,7 @@
  * carrier privileged app that provides the carrier config
  * @hide
  */
-public class CarrierPrivilegeAuthenticator extends BroadcastReceiver {
+public class CarrierPrivilegeAuthenticator {
     private static final String TAG = CarrierPrivilegeAuthenticator.class.getSimpleName();
     private static final boolean DBG = true;
 
@@ -63,100 +73,187 @@
     private final TelephonyManagerShim mTelephonyManagerShim;
     private final TelephonyManager mTelephonyManager;
     @GuardedBy("mLock")
-    private int[] mCarrierServiceUid;
+    private final SparseArray<CarrierServiceUidWithSubId> mCarrierServiceUidWithSubId =
+            new SparseArray<>(2 /* initialCapacity */);
     @GuardedBy("mLock")
     private int mModemCount = 0;
     private final Object mLock = new Object();
-    private final HandlerThread mThread;
     private final Handler mHandler;
     @NonNull
-    private final List<CarrierPrivilegesListenerShim> mCarrierPrivilegesChangedListeners =
-            new ArrayList<>();
+    private final List<PrivilegeListener> mCarrierPrivilegesChangedListeners = new ArrayList<>();
+    private final boolean mUseCallbacksForServiceChanged;
+    private final boolean mRequestRestrictedWifiEnabled;
+    @NonNull
+    private final BiConsumer<Integer, Integer> mListener;
 
     public CarrierPrivilegeAuthenticator(@NonNull final Context c,
+            @NonNull final Dependencies deps,
             @NonNull final TelephonyManager t,
-            @NonNull final TelephonyManagerShimImpl telephonyManagerShim) {
+            @NonNull final TelephonyManagerShim telephonyManagerShim,
+            final boolean requestRestrictedWifiEnabled,
+            @NonNull BiConsumer<Integer, Integer> listener,
+            @NonNull final Handler connectivityServiceHandler) {
         mContext = c;
         mTelephonyManager = t;
         mTelephonyManagerShim = telephonyManagerShim;
-        mThread = new HandlerThread(TAG);
-        mThread.start();
-        mHandler = new Handler(mThread.getLooper()) {};
-        synchronized (mLock) {
-            mModemCount = mTelephonyManager.getActiveModemCount();
-            registerForCarrierChanges();
-            updateCarrierServiceUid();
+        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);
+        // 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);
+    }
+
+    /**
+     * Start CarrierPrivilegeAuthenticator
+     */
+    public void start() {
+        if (mRequestRestrictedWifiEnabled) {
+            registerSimConfigChangedReceiver();
+            mHandler.post(this::simConfigChanged);
         }
     }
 
     public CarrierPrivilegeAuthenticator(@NonNull final Context c,
-            @NonNull final TelephonyManager t) {
-        mContext = c;
-        mTelephonyManager = t;
-        mTelephonyManagerShim = TelephonyManagerShimImpl.newInstance(mTelephonyManager);
-        mThread = new HandlerThread(TAG);
-        mThread.start();
-        mHandler = new Handler(mThread.getLooper()) {};
+            @NonNull final TelephonyManager t, final boolean requestRestrictedWifiEnabled,
+            @NonNull BiConsumer<Integer, Integer> listener,
+            @NonNull final Handler connectivityServiceHandler) {
+        this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t),
+                requestRestrictedWifiEnabled, listener, connectivityServiceHandler);
+    }
+
+    public static class Dependencies {
+        /**
+         * Create a HandlerThread to use in CarrierPrivilegeAuthenticator.
+         */
+        public HandlerThread makeHandlerThread() {
+            return new HandlerThread(TAG);
+        }
+
+        /**
+         * @see DeviceConfigUtils#isTetheringFeatureEnabled
+         */
+        public boolean isFeatureEnabled(Context context, String name) {
+            return DeviceConfigUtils.isTetheringFeatureEnabled(context, name);
+        }
+    }
+
+    private void simConfigChanged() {
+        //  If mRequestRestrictedWifiEnabled is false, constructor calls simConfigChanged
+        if (mRequestRestrictedWifiEnabled) {
+            ensureRunningOnHandlerThread();
+        }
         synchronized (mLock) {
+            unregisterCarrierPrivilegesListeners();
             mModemCount = mTelephonyManager.getActiveModemCount();
-            registerForCarrierChanges();
+            registerCarrierPrivilegesListeners(mModemCount);
+            if (!mUseCallbacksForServiceChanged) updateCarrierServiceUid();
+        }
+    }
+
+    private static class CarrierServiceUidWithSubId {
+        final int mUid;
+        final int mSubId;
+
+        CarrierServiceUidWithSubId(int uid, int subId) {
+            mUid = uid;
+            mSubId = subId;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof CarrierServiceUidWithSubId)) {
+                return false;
+            }
+            CarrierServiceUidWithSubId compare = (CarrierServiceUidWithSubId) obj;
+            return (mUid == compare.mUid && mSubId == compare.mSubId);
+        }
+
+        @Override
+        public int hashCode() {
+            return mUid * 31 + mSubId;
+        }
+    }
+    private class PrivilegeListener implements CarrierPrivilegesListenerShim {
+        public final int mLogicalSlot;
+
+        PrivilegeListener(final int logicalSlot) {
+            mLogicalSlot = logicalSlot;
+        }
+
+        @Override
+        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
+            // with the onSubscriptionsChangedListener and broadcasts.
             updateCarrierServiceUid();
         }
-    }
 
-    /**
-     * Broadcast receiver for ACTION_MULTI_SIM_CONFIG_CHANGED
-     *
-     * <p>The broadcast receiver is registered with mHandler
-     */
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        switch (intent.getAction()) {
-            case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED:
-                handleActionMultiSimConfigChanged(context, intent);
-                break;
-            default:
-                Log.d(TAG, "Unknown intent received with action: " + intent.getAction());
+        @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
+                // with the onSubscriptionsChangedListener and broadcasts.
+                updateCarrierServiceUid();
+                return;
+            }
+            synchronized (mLock) {
+                CarrierServiceUidWithSubId oldPair =
+                        mCarrierServiceUidWithSubId.get(mLogicalSlot);
+                int subId = getSubId(mLogicalSlot);
+                mCarrierServiceUidWithSubId.put(
+                        mLogicalSlot,
+                        new CarrierServiceUidWithSubId(carrierServiceUid, subId));
+                if (oldPair != null
+                        && oldPair.mUid != Process.INVALID_UID
+                        && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                        && !oldPair.equals(mCarrierServiceUidWithSubId.get(mLogicalSlot))) {
+                    mListener.accept(oldPair.mUid, oldPair.mSubId);
+                }
+            }
         }
     }
 
-    private void handleActionMultiSimConfigChanged(Context context, Intent intent) {
-        unregisterCarrierPrivilegesListeners();
-        synchronized (mLock) {
-            mModemCount = mTelephonyManager.getActiveModemCount();
-        }
-        registerCarrierPrivilegesListeners();
-        updateCarrierServiceUid();
-    }
-
-    private void registerForCarrierChanges() {
-        final IntentFilter filter = new IntentFilter();
-        filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
-        mContext.registerReceiver(this, filter, null, mHandler);
-        registerCarrierPrivilegesListeners();
-    }
-
-    private void registerCarrierPrivilegesListeners() {
+    private void registerCarrierPrivilegesListeners(final int modemCount) {
         final HandlerExecutor executor = new HandlerExecutor(mHandler);
-        int modemCount;
-        synchronized (mLock) {
-            modemCount = mModemCount;
-        }
         try {
             for (int i = 0; i < modemCount; i++) {
-                CarrierPrivilegesListenerShim carrierPrivilegesListener =
-                        new CarrierPrivilegesListenerShim() {
-                            @Override
-                            public void onCarrierPrivilegesChanged(
-                                    @NonNull List<String> privilegedPackageNames,
-                                    @NonNull int[] privilegedUids) {
-                                // Re-trigger the synchronous check (which is also very cheap due
-                                // to caching in CarrierPrivilegesTracker). This allows consistency
-                                // with the onSubscriptionsChangedListener and broadcasts.
-                                updateCarrierServiceUid();
-                            }
-                        };
-                addCarrierPrivilegesListener(i, executor, carrierPrivilegesListener);
+                PrivilegeListener carrierPrivilegesListener = new PrivilegeListener(i);
+                addCarrierPrivilegesListener(executor, carrierPrivilegesListener);
                 mCarrierPrivilegesChangedListeners.add(carrierPrivilegesListener);
             }
         } catch (IllegalArgumentException e) {
@@ -164,24 +261,20 @@
         }
     }
 
-    private void addCarrierPrivilegesListener(int logicalSlotIndex, Executor executor,
-            CarrierPrivilegesListenerShim listener) {
-        try {
-            mTelephonyManagerShim.addCarrierPrivilegesListener(
-                    logicalSlotIndex, executor, listener);
-        } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
-            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
-            Log.e(TAG, "addCarrierPrivilegesListener API is not available");
+    @GuardedBy("mLock")
+    private void unregisterCarrierPrivilegesListeners() {
+        for (PrivilegeListener carrierPrivilegesListener : mCarrierPrivilegesChangedListeners) {
+            removeCarrierPrivilegesListener(carrierPrivilegesListener);
+            CarrierServiceUidWithSubId oldPair =
+                    mCarrierServiceUidWithSubId.get(carrierPrivilegesListener.mLogicalSlot);
+            mCarrierServiceUidWithSubId.remove(carrierPrivilegesListener.mLogicalSlot);
+            if (oldPair != null
+                    && oldPair.mUid != Process.INVALID_UID
+                    && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                mListener.accept(oldPair.mUid, oldPair.mSubId);
+            }
         }
-    }
-
-    private void removeCarrierPrivilegesListener(CarrierPrivilegesListenerShim listener) {
-        try {
-            mTelephonyManagerShim.removeCarrierPrivilegesListener(listener);
-        } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
-            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
-            Log.e(TAG, "removeCarrierPrivilegesListener API is not available");
-        }
+        mCarrierPrivilegesChangedListeners.clear();
     }
 
     private String getCarrierServicePackageNameForLogicalSlot(int logicalSlotIndex) {
@@ -195,25 +288,18 @@
         return null;
     }
 
-    private void unregisterCarrierPrivilegesListeners() {
-        for (CarrierPrivilegesListenerShim carrierPrivilegesListener :
-                mCarrierPrivilegesChangedListeners) {
-            removeCarrierPrivilegesListener(carrierPrivilegesListener);
-        }
-        mCarrierPrivilegesChangedListeners.clear();
-    }
-
     /**
      * Check if a UID is the carrier service app of the subscription ID in the provided capabilities
      *
      * This returns whether the passed UID is the carrier service package for the subscription ID
      * stored in the telephony network specifier in the passed network capabilities.
-     * If the capabilities don't code for a cellular network, or if they don't have the
+     * If the capabilities don't code for a cellular or Wi-Fi network, or if they don't have the
      * subscription ID in their specifier, this returns false.
      *
-     * This method can be used to check that a network request for {@link NET_CAPABILITY_CBS} is
-     * allowed for the UID of a caller, which must hold carrier privilege and provide the carrier
-     * config.
+     * This method can be used to check that a network request that requires the UID to be
+     * the carrier service UID is indeed called by such a UID. An example of such a network could
+     * be a network with the  {@link android.net.NetworkCapabilities#NET_CAPABILITY_CBS}
+     * capability.
      * It can also be used to check that a factory is entitled to grant access to a given network
      * to a given UID on grounds that it is the carrier service package.
      *
@@ -221,47 +307,101 @@
      * @param networkCapabilities the network capabilities for which carrier privilege is checked.
      * @return true if uid provides the relevant carrier config else false.
      */
-    public boolean hasCarrierPrivilegeForNetworkCapabilities(int callingUid,
+    public boolean isCarrierServiceUidForNetworkCapabilities(int callingUid,
             @NonNull NetworkCapabilities networkCapabilities) {
-        if (callingUid == Process.INVALID_UID) return false;
-        if (!networkCapabilities.hasSingleTransport(TRANSPORT_CELLULAR)) return false;
-        final int subId = getSubIdFromNetworkSpecifier(networkCapabilities.getNetworkSpecifier());
-        if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) return false;
+        if (callingUid == Process.INVALID_UID) {
+            return false;
+        }
+        int subId = getSubIdFromNetworkCapabilities(networkCapabilities);
+        if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) {
+            return false;
+        }
         return callingUid == getCarrierServiceUidForSubId(subId);
     }
 
+    /**
+     * Extract the SubscriptionId from the NetworkCapabilities.
+     *
+     * @param networkCapabilities the network capabilities which may contains the SubscriptionId.
+     * @return the SubscriptionId.
+     */
+    public int getSubIdFromNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
+        int subId;
+        if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_CELLULAR)) {
+            subId = getSubIdFromTelephonySpecifier(networkCapabilities.getNetworkSpecifier());
+        } else if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_WIFI)) {
+            subId = getSubIdFromWifiTransportInfo(networkCapabilities.getTransportInfo());
+        } else {
+            subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+        }
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                && mRequestRestrictedWifiEnabled
+                && networkCapabilities.getSubscriptionIds().size() == 1) {
+            subId = networkCapabilities.getSubscriptionIds().toArray(new Integer[0])[0];
+        }
+
+        if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                && !networkCapabilities.getSubscriptionIds().contains(subId)) {
+            // Ideally, the code above should just use networkCapabilities.getSubscriptionIds()
+            // for simplicity and future-proofing. However, this is not the historical behavior,
+            // and there is no enforcement that they do not differ, so log a terrible failure if
+            // they do not match to gain confidence this never happens.
+            // TODO : when there is confidence that this never happens, rewrite the code above
+            // with NetworkCapabilities#getSubscriptionIds.
+            Log.wtf(TAG, "NetworkCapabilities subIds are inconsistent between "
+                    + "specifier/transportInfo and mSubIds : " + networkCapabilities);
+        }
+        return subId;
+    }
+
+    @VisibleForTesting
+    protected int getSubId(int slotIndex) {
+        if (SdkLevel.isAtLeastU()) {
+            return SubscriptionManager.getSubscriptionId(slotIndex);
+        } else {
+            SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+            int[] subIds = sm.getSubscriptionIds(slotIndex);
+            if (subIds != null && subIds.length > 0) {
+                return subIds[0];
+            }
+            return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+        }
+    }
+
     @VisibleForTesting
     void updateCarrierServiceUid() {
         synchronized (mLock) {
-            mCarrierServiceUid = new int[mModemCount];
+            SparseArray<CarrierServiceUidWithSubId> copy = mCarrierServiceUidWithSubId.clone();
+            mCarrierServiceUidWithSubId.clear();
             for (int i = 0; i < mModemCount; i++) {
-                mCarrierServiceUid[i] = getCarrierServicePackageUidForSlot(i);
+                int subId = getSubId(i);
+                mCarrierServiceUidWithSubId.put(
+                        i,
+                        new CarrierServiceUidWithSubId(
+                                getCarrierServicePackageUidForSlot(i), subId));
+            }
+            for (int i = 0; i < copy.size(); ++i) {
+                CarrierServiceUidWithSubId oldPair = copy.valueAt(i);
+                CarrierServiceUidWithSubId newPair = mCarrierServiceUidWithSubId.get(copy.keyAt(i));
+                if (oldPair.mUid != Process.INVALID_UID
+                        && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                        && !oldPair.equals(newPair)) {
+                    mListener.accept(oldPair.mUid, oldPair.mSubId);
+                }
             }
         }
     }
 
     @VisibleForTesting
     int getCarrierServiceUidForSubId(int subId) {
-        final int slotId = getSlotIndex(subId);
         synchronized (mLock) {
-            if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX && slotId < mModemCount) {
-                return mCarrierServiceUid[slotId];
+            for (int i = 0; i < mCarrierServiceUidWithSubId.size(); ++i) {
+                if (mCarrierServiceUidWithSubId.valueAt(i).mSubId == subId) {
+                    return mCarrierServiceUidWithSubId.valueAt(i).mUid;
+                }
             }
+            return Process.INVALID_UID;
         }
-        return Process.INVALID_UID;
-    }
-
-    @VisibleForTesting
-    protected int getSlotIndex(int subId) {
-        return SubscriptionManager.getSlotIndex(subId);
-    }
-
-    @VisibleForTesting
-    int getSubIdFromNetworkSpecifier(NetworkSpecifier specifier) {
-        if (specifier instanceof TelephonyNetworkSpecifier) {
-            return ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
-        }
-        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     }
 
     @VisibleForTesting
@@ -288,4 +428,61 @@
     int getCarrierServicePackageUidForSlot(int slotId) {
         return getUidForPackage(getCarrierServicePackageNameForLogicalSlot(slotId));
     }
+
+    @VisibleForTesting
+    int getSubIdFromTelephonySpecifier(@Nullable final NetworkSpecifier specifier) {
+        if (specifier instanceof TelephonyNetworkSpecifier) {
+            return ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+        }
+        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    }
+
+    int getSubIdFromWifiTransportInfo(@Nullable final TransportInfo info) {
+        if (info instanceof WifiInfo) {
+            return ((WifiInfo) info).getSubscriptionId();
+        }
+        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    }
+
+    // Helper methods to avoid having to deal with UnsupportedApiLevelException.
+    private void addCarrierPrivilegesListener(@NonNull final Executor executor,
+            @NonNull final PrivilegeListener listener) {
+        try {
+            mTelephonyManagerShim.addCarrierPrivilegesListener(listener.mLogicalSlot, executor,
+                    listener);
+        } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
+            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
+            Log.e(TAG, "addCarrierPrivilegesListener API is not available");
+        }
+    }
+
+    private void removeCarrierPrivilegesListener(PrivilegeListener listener) {
+        try {
+            mTelephonyManagerShim.removeCarrierPrivilegesListener(listener);
+        } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
+            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
+            Log.e(TAG, "removeCarrierPrivilegesListener API is not available");
+        }
+    }
+
+    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);
+        synchronized (mLock) {
+            for (int i = 0; i < mCarrierServiceUidWithSubId.size(); ++i) {
+                final int logicalSlot = mCarrierServiceUidWithSubId.keyAt(i);
+                final int serviceUid = mCarrierServiceUidWithSubId.valueAt(i).mUid;
+                final int subId = mCarrierServiceUidWithSubId.valueAt(i).mSubId;
+                pw.println("Logical slot = " + logicalSlot + " : uid = " + serviceUid
+                        + " : subId = " + subId);
+            }
+        }
+    }
 }
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 5d04632..e808746 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -30,15 +30,18 @@
 import android.net.InetAddresses;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpPrefix;
+import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.system.ErrnoException;
 import android.util.Log;
 
+import androidx.annotation.RequiresApi;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.modules.utils.build.SdkLevel;
+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;
@@ -63,6 +66,7 @@
  *
  * {@hide}
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class ClatCoordinator {
     private static final String TAG = ClatCoordinator.class.getSimpleName();
 
@@ -75,7 +79,7 @@
     @VisibleForTesting
     static final int MTU_DELTA = 28;
     @VisibleForTesting
-    static final int CLAT_MAX_MTU = 65536;
+    static final int CLAT_MAX_MTU = 1500 + MTU_DELTA;
 
     // This must match the interface prefix in clatd.c.
     private static final String CLAT_PREFIX = "v4-";
@@ -237,9 +241,8 @@
         /**
          * Stop clatd.
          */
-        public void stopClatd(String iface, String pfx96, String v4, String v6, int pid)
-                throws IOException {
-            native_stopClatd(iface, pfx96, v4, v6, pid);
+        public void stopClatd(int pid) throws IOException {
+            native_stopClatd(pid);
         }
 
         /**
@@ -252,14 +255,10 @@
         /** Get ingress6 BPF map. */
         @Nullable
         public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
-            // Pre-T devices don't use ClatCoordinator to access clat map. Since Nat464Xlat
-            // initializes a ClatCoordinator object to avoid redundant null pointer check
-            // while using, ignore the BPF map initialization on pre-T devices.
-            // TODO: probably don't initialize ClatCoordinator object on pre-T devices.
-            if (!SdkLevel.isAtLeastT()) return null;
             try {
+                // written from clatd.c
                 return new BpfMap<>(CLAT_INGRESS6_MAP_PATH,
-                    BpfMap.BPF_F_RDWR, ClatIngress6Key.class, ClatIngress6Value.class);
+                       ClatIngress6Key.class, ClatIngress6Value.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create ingress6 map: " + e);
                 return null;
@@ -269,14 +268,10 @@
         /** Get egress4 BPF map. */
         @Nullable
         public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
-            // Pre-T devices don't use ClatCoordinator to access clat map. Since Nat464Xlat
-            // initializes a ClatCoordinator object to avoid redundant null pointer check
-            // while using, ignore the BPF map initialization on pre-T devices.
-            // TODO: probably don't initialize ClatCoordinator object on pre-T devices.
-            if (!SdkLevel.isAtLeastT()) return null;
             try {
+                // written from clatd.c
                 return new BpfMap<>(CLAT_EGRESS4_MAP_PATH,
-                    BpfMap.BPF_F_RDWR, ClatEgress4Key.class, ClatEgress4Value.class);
+                       ClatEgress4Key.class, ClatEgress4Value.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create egress4 map: " + e);
                 return null;
@@ -286,14 +281,10 @@
         /** Get cookie tag map */
         @Nullable
         public IBpfMap<CookieTagMapKey, CookieTagMapValue> getBpfCookieTagMap() {
-            // Pre-T devices don't use ClatCoordinator to access clat map. Since Nat464Xlat
-            // initializes a ClatCoordinator object to avoid redundant null pointer check
-            // while using, ignore the BPF map initialization on pre-T devices.
-            // TODO: probably don't initialize ClatCoordinator object on pre-T devices.
-            if (!SdkLevel.isAtLeastT()) return null;
             try {
+                // also read and written from other locations
                 return new BpfMap<>(COOKIE_TAG_MAP_PATH,
-                        BpfMap.BPF_F_RDWR, CookieTagMapKey.class, CookieTagMapValue.class);
+                       CookieTagMapKey.class, CookieTagMapValue.class);
             } catch (ErrnoException e) {
                 Log.wtf(TAG, "Cannot open cookie tag map: " + e);
                 return null;
@@ -412,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;
 
@@ -533,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");
@@ -613,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.
@@ -656,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, "ipv4 mtu is " + 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) {
@@ -843,9 +755,7 @@
         Log.i(TAG, "Stopping clatd pid=" + mClatdTracker.pid + " on " + mClatdTracker.iface);
 
         maybeStopBpf(mClatdTracker);
-        mDeps.stopClatd(mClatdTracker.iface, mClatdTracker.pfx96.getHostAddress(),
-                mClatdTracker.v4.getHostAddress(), mClatdTracker.v6.getHostAddress(),
-                mClatdTracker.pid);
+        mDeps.stopClatd(mClatdTracker.pid);
         untagSocket(mClatdTracker.cookie);
 
         Log.i(TAG, "clatd on " + mClatdTracker.iface + " stopped");
@@ -862,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) {
@@ -885,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) {
@@ -899,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.
@@ -944,7 +878,6 @@
     private static native int native_startClatd(FileDescriptor tunfd, FileDescriptor readsock6,
             FileDescriptor writesock6, String iface, String pfx96, String v4, String v6)
             throws IOException;
-    private static native void native_stopClatd(String iface, String pfx96, String v4, String v6,
-            int pid) throws IOException;
+    private static native void native_stopClatd(int pid) throws IOException;
     private static native long native_getSocketCookie(FileDescriptor sock) throws IOException;
 }
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 9039a14..df87316 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -33,6 +33,25 @@
     public static final String NO_REMATCH_ALL_REQUESTS_ON_REGISTER =
             "no_rematch_all_requests_on_register";
 
+    public static final String CARRIER_SERVICE_CHANGED_USE_CALLBACK =
+            "carrier_service_changed_use_callback_version";
+
+    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 c1ba40e..917ad4d 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -16,9 +16,6 @@
 
 package com.android.server.connectivity;
 
-import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND;
-import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -26,16 +23,15 @@
 import android.os.Binder;
 import android.os.Process;
 import android.os.ServiceSpecificException;
+import android.os.UserHandle;
 import android.system.ErrnoException;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.BpfBitmap;
-import com.android.net.module.util.BpfUtils;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.PermissionUtils;
 
-import java.io.IOException;
 import java.util.ArrayList;
 
 /**
@@ -45,11 +41,7 @@
     public static final String SERVICE_NAME = "connectivity_native";
 
     private static final String TAG = ConnectivityNativeService.class.getSimpleName();
-    private static final String CGROUP_PATH = "/sys/fs/cgroup";
-    private static final String V4_PROG_PATH =
-            "/sys/fs/bpf/net_shared/prog_block_bind4_block_port";
-    private static final String V6_PROG_PATH =
-            "/sys/fs/bpf/net_shared/prog_block_bind6_block_port";
+
     private static final String BLOCKED_PORTS_MAP_PATH =
             "/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
 
@@ -76,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);
     }
 
@@ -95,7 +87,6 @@
     protected ConnectivityNativeService(final Context context, @NonNull Dependencies deps) {
         mContext = context;
         mBpfBlockedPortsMap = deps.getBlockPortsMap();
-        attachProgram();
     }
 
     @Override
@@ -155,23 +146,4 @@
     public String getInterfaceHash() {
         return this.HASH;
     }
-
-    /**
-     * Attach BPF program
-     */
-    private void attachProgram() {
-        try {
-            BpfUtils.attachProgram(BPF_CGROUP_INET4_BIND, V4_PROG_PATH, CGROUP_PATH, 0);
-        } catch (IOException e) {
-            throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET4_BIND: "
-                    + e);
-        }
-        try {
-            BpfUtils.attachProgram(BPF_CGROUP_INET6_BIND, V6_PROG_PATH, CGROUP_PATH, 0);
-        } catch (IOException e) {
-            throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET6_BIND: "
-                    + e);
-        }
-        Log.d(TAG, "Attached BPF_CGROUP_INET4_BIND and BPF_CGROUP_INET6_BIND programs");
-    }
 }
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index 1493cae..ac02229 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -38,6 +38,7 @@
 import android.net.InetAddresses;
 import android.net.LinkProperties;
 import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.net.ResolverParamsParcel;
 import android.net.Uri;
 import android.net.shared.PrivateDnsConfig;
@@ -251,7 +252,7 @@
     // TODO: Replace the Map with SparseArrays.
     private final Map<Integer, PrivateDnsValidationStatuses> mPrivateDnsValidationMap;
     private final Map<Integer, LinkProperties> mLinkPropertiesMap;
-    private final Map<Integer, int[]> mTransportsMap;
+    private final Map<Integer, NetworkCapabilities> mNetworkCapabilitiesMap;
 
     private int mSampleValidity;
     private int mSuccessThreshold;
@@ -265,7 +266,7 @@
         mPrivateDnsMap = new ConcurrentHashMap<>();
         mPrivateDnsValidationMap = new HashMap<>();
         mLinkPropertiesMap = new HashMap<>();
-        mTransportsMap = new HashMap<>();
+        mNetworkCapabilitiesMap = new HashMap<>();
 
         // TODO: Create and register ContentObservers to track every setting
         // used herein, posting messages to respond to changes.
@@ -278,7 +279,7 @@
     public void removeNetwork(Network network) {
         mPrivateDnsMap.remove(network.getNetId());
         mPrivateDnsValidationMap.remove(network.getNetId());
-        mTransportsMap.remove(network.getNetId());
+        mNetworkCapabilitiesMap.remove(network.getNetId());
         mLinkPropertiesMap.remove(network.getNetId());
     }
 
@@ -301,7 +302,7 @@
         final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
                 PRIVATE_DNS_OFF);
 
-        final boolean useTls = privateDnsCfg.useTls;
+        final boolean useTls = privateDnsCfg.mode != PRIVATE_DNS_MODE_OFF;
         final PrivateDnsValidationStatuses statuses =
                 useTls ? mPrivateDnsValidationMap.get(netId) : null;
         final boolean validated = (null != statuses) && statuses.hasValidatedServer();
@@ -325,13 +326,17 @@
     }
 
     /**
-     * When creating a new network or transport types are changed in a specific network,
-     * transport types are always saved to a hashMap before update dns config.
-     * When destroying network, the specific network will be removed from the hashMap.
-     * The hashMap is always accessed on the same thread.
+     * Update {@link NetworkCapabilities} stored in this instance.
+     *
+     * In order to ensure that the resolver has access to necessary information when other events
+     * occur, capabilities are always saved to a hashMap before updating the DNS configuration
+     * whenever a new network is created, transport types are modified, or metered capabilities are
+     * altered for a network. When a network is destroyed, the corresponding entry is removed from
+     * the hashMap. To prevent concurrency issues, the hashMap should always be accessed from the
+     * same thread.
      */
-    public void updateTransportsForNetwork(int netId, @NonNull int[] transportTypes) {
-        mTransportsMap.put(netId, transportTypes);
+    public void updateCapabilitiesForNetwork(int netId, @NonNull final NetworkCapabilities nc) {
+        mNetworkCapabilitiesMap.put(netId, nc);
         sendDnsConfigurationForNetwork(netId);
     }
 
@@ -351,8 +356,8 @@
      */
     public void sendDnsConfigurationForNetwork(int netId) {
         final LinkProperties lp = mLinkPropertiesMap.get(netId);
-        final int[] transportTypes = mTransportsMap.get(netId);
-        if (lp == null || transportTypes == null) return;
+        final NetworkCapabilities nc = mNetworkCapabilitiesMap.get(netId);
+        if (lp == null || nc == null) return;
         updateParametersSettings();
         final ResolverParamsParcel paramsParcel = new ResolverParamsParcel();
 
@@ -365,7 +370,7 @@
         // networks like IMS.
         final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
                 PRIVATE_DNS_OFF);
-        final boolean useTls = privateDnsCfg.useTls;
+        final boolean useTls = privateDnsCfg.mode != PRIVATE_DNS_MODE_OFF;
         final boolean strictMode = privateDnsCfg.inStrictMode();
 
         paramsParcel.netId = netId;
@@ -383,7 +388,9 @@
                               .collect(Collectors.toList()))
                 : useTls ? paramsParcel.servers  // Opportunistic
                 : new String[0];            // Off
-        paramsParcel.transportTypes = transportTypes;
+        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) {
@@ -397,12 +404,14 @@
         }
 
         Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
-                + "%d, %d, %s, %s)", paramsParcel.netId, Arrays.toString(paramsParcel.servers),
-                Arrays.toString(paramsParcel.domains), paramsParcel.sampleValiditySeconds,
-                paramsParcel.successThreshold, paramsParcel.minSamples,
-                paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
+                + "%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.tlsServers),
+                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 8d566b6..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;
@@ -85,10 +87,10 @@
     public DscpPolicyTracker() throws ErrnoException {
         mAttachedIfaces = new HashSet<String>();
         mIfaceIndexToPolicyIdBpfMapIndex = new HashMap<Integer, SparseIntArray>();
-        mBpfDscpIpv4Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,
-                BpfMap.BPF_F_RDWR, Struct.S32.class, DscpPolicyValue.class);
-        mBpfDscpIpv6Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
-                BpfMap.BPF_F_RDWR, Struct.S32.class, DscpPolicyValue.class);
+        mBpfDscpIpv4Policies = new BpfMap<>(IPV4_POLICY_MAP_PATH,
+                Struct.S32.class, DscpPolicyValue.class);
+        mBpfDscpIpv6Policies = new BpfMap<>(IPV6_POLICY_MAP_PATH,
+                Struct.S32.class, DscpPolicyValue.class);
     }
 
     private boolean isUnusedIndex(int index) {
@@ -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/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
index 87ae0c9..648f3bf 100644
--- a/service/src/com/android/server/connectivity/FullScore.java
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -124,7 +124,7 @@
             new Class[]{FullScore.class, NetworkScore.class}, new String[]{"POLICY_"});
 
     @VisibleForTesting
-    static @NonNull String policyNameOf(final int policy) {
+    public static @NonNull String policyNameOf(final int policy) {
         final String name = sMessageNames.get(policy);
         if (name == null) {
             // Don't throw here because name might be null due to proguard stripping out the
@@ -304,6 +304,18 @@
     }
 
     /**
+     * Gets the policies as an long. Internal callers only.
+     *
+     * DO NOT USE if not immediately collapsing back into a scalar. Instead, use
+     * {@link #hasPolicy}.
+     * @return the internal, version-dependent int representing the policies.
+     * @hide
+     */
+    public long getPoliciesInternal() {
+        return mPolicies;
+    }
+
+    /**
      * @return whether this score has a particular policy.
      */
     @VisibleForTesting
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index d59d526..21dbb45 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -29,15 +29,19 @@
 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;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 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;
@@ -45,6 +49,7 @@
 import com.android.metrics.KeepaliveLifetimeForCarrier;
 import com.android.metrics.KeepaliveLifetimePerCarrier;
 import com.android.modules.utils.BackgroundThread;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.server.ConnectivityStatsLog;
 
@@ -55,6 +60,8 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Tracks carrier and duration metrics of automatic on/off keepalives.
@@ -62,9 +69,18 @@
  * <p>This class follows AutomaticOnOffKeepaliveTracker closely and its on*Keepalive methods needs
  * to be called in a timely manner to keep the metrics accurate. It is also not thread-safe and all
  * public methods must be called by the same thread, namely the ConnectivityService handler thread.
+ *
+ * <p>In the case that the keepalive state becomes out of sync with the hardware, the tracker will
+ * be disabled. e.g. Calling onStartKeepalive on a given network, slot pair twice without calling
+ * onStopKeepalive is unexpected and will disable the tracker.
  */
 public class KeepaliveStatsTracker {
     private static final String TAG = KeepaliveStatsTracker.class.getSimpleName();
+    private static final int INVALID_KEEPALIVE_ID = -1;
+    // 2 hour acceptable deviation in metrics collection duration time to account for the 1 hour
+    // window of AlarmManager.
+    private static final long MAX_EXPECTED_DURATION_MS =
+            AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 2 * 60 * 60 * 1_000L;
 
     @NonNull private final Handler mConnectivityServiceHandler;
     @NonNull private final Dependencies mDependencies;
@@ -75,6 +91,11 @@
     // Updates are received from the ACTION_DEFAULT_SUBSCRIPTION_CHANGED broadcast.
     private int mCachedDefaultSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
+    // Boolean to track whether the KeepaliveStatsTracker is enabled.
+    // Use a final AtomicBoolean to ensure initialization is seen on the handler thread.
+    // Repeated fields in metrics are only supported on T+ so this is enabled only on T+.
+    private final AtomicBoolean mEnabled = new AtomicBoolean(SdkLevel.isAtLeastT());
+
     // Class to store network information, lifetime durations and active state of a keepalive.
     private static final class KeepaliveStats {
         // The carrier ID for a keepalive, or TelephonyManager.UNKNOWN_CARRIER_ID(-1) if not set.
@@ -184,17 +205,21 @@
     // Map of keepalives identified by the id from getKeepaliveId to their stats information.
     private final SparseArray<KeepaliveStats> mKeepaliveStatsPerId = new SparseArray<>();
 
-    // Generate a unique integer using a given network's netId and the slot number.
+    // Generate and return a unique integer using a given network's netId and the slot number.
     // This is possible because netId is a 16 bit integer, so an integer with the first 16 bits as
     // the netId and the last 16 bits as the slot number can be created. This allows slot numbers to
     // be up to 2^16.
+    // Returns INVALID_KEEPALIVE_ID if the netId or slot is not as expected above.
     private int getKeepaliveId(@NonNull Network network, int slot) {
         final int netId = network.getNetId();
+        // Since there is no enforcement that a Network's netId is valid check for it here.
         if (netId < 0 || netId >= (1 << 16)) {
-            throw new IllegalArgumentException("Unexpected netId value: " + netId);
+            disableTracker("Unexpected netId value: " + netId);
+            return INVALID_KEEPALIVE_ID;
         }
         if (slot < 0 || slot >= (1 << 16)) {
-            throw new IllegalArgumentException("Unexpected slot value: " + slot);
+            disableTracker("Unexpected slot value: " + slot);
+            return INVALID_KEEPALIVE_ID;
         }
 
         return (netId << 16) + slot;
@@ -251,35 +276,65 @@
         public long getElapsedRealtime() {
             return SystemClock.elapsedRealtime();
         }
+
+        /**
+         * Writes a DAILY_KEEPALIVE_INFO_REPORTED to ConnectivityStatsLog.
+         *
+         * @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,
+                    dailyKeepaliveInfoReported.getDurationPerNumOfKeepalive().toByteArray(),
+                    dailyKeepaliveInfoReported.getKeepaliveLifetimePerCarrier().toByteArray(),
+                    dailyKeepaliveInfoReported.getKeepaliveRequests(),
+                    dailyKeepaliveInfoReported.getAutomaticKeepaliveRequests(),
+                    dailyKeepaliveInfoReported.getDistinctUserCount(),
+                    CollectionUtils.toIntArray(dailyKeepaliveInfoReported.getUidList()));
+        }
     }
 
     public KeepaliveStatsTracker(@NonNull Context context, @NonNull Handler handler) {
         this(context, handler, new Dependencies());
     }
 
+    private final Context mContext;
+    private final SubscriptionManager mSubscriptionManager;
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mCachedDefaultSubscriptionId =
+                    intent.getIntExtra(
+                            SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
+                            SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        }
+    };
+
+    private final CompletableFuture<OnSubscriptionsChangedListener> mListenerFuture =
+            new CompletableFuture<>();
+
     @VisibleForTesting
     public KeepaliveStatsTracker(
             @NonNull Context context,
             @NonNull Handler handler,
             @NonNull Dependencies dependencies) {
-        Objects.requireNonNull(context);
+        mContext = Objects.requireNonNull(context);
         mDependencies = Objects.requireNonNull(dependencies);
         mConnectivityServiceHandler = Objects.requireNonNull(handler);
 
-        final SubscriptionManager subscriptionManager =
+        mSubscriptionManager =
                 Objects.requireNonNull(context.getSystemService(SubscriptionManager.class));
 
         mLastUpdateDurationsTimestamp = mDependencies.getElapsedRealtime();
+
+        if (!isEnabled()) {
+            return;
+        }
+
         context.registerReceiver(
-                new BroadcastReceiver() {
-                    @Override
-                    public void onReceive(Context context, Intent intent) {
-                        mCachedDefaultSubscriptionId =
-                                intent.getIntExtra(
-                                        SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
-                                        SubscriptionManager.INVALID_SUBSCRIPTION_ID);
-                    }
-                },
+                mBroadcastReceiver,
                 new IntentFilter(SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED),
                 /* broadcastPermission= */ null,
                 mConnectivityServiceHandler);
@@ -289,38 +344,41 @@
         // this will throw. Therefore, post a runnable that creates it there.
         // When the callback is called on the BackgroundThread, post a message on the CS handler
         // thread to update the caches, which can only be touched there.
-        BackgroundThread.getHandler().post(() ->
-                subscriptionManager.addOnSubscriptionsChangedListener(
-                        r -> r.run(), new OnSubscriptionsChangedListener() {
-                            @Override
-                            public void onSubscriptionsChanged() {
-                                final List<SubscriptionInfo> activeSubInfoList =
-                                        subscriptionManager.getActiveSubscriptionInfoList();
-                                // A null subInfo list here indicates the current state is unknown
-                                // but not necessarily empty, simply ignore it. Another call to the
-                                // listener will be invoked in the future.
-                                if (activeSubInfoList == null) return;
-                                mConnectivityServiceHandler.post(() -> {
-                                    mCachedCarrierIdPerSubId.clear();
+        BackgroundThread.getHandler().post(() -> {
+            final OnSubscriptionsChangedListener listener =
+                    new OnSubscriptionsChangedListener() {
+                        @Override
+                        public void onSubscriptionsChanged() {
+                            final List<SubscriptionInfo> activeSubInfoList =
+                                    mSubscriptionManager.getActiveSubscriptionInfoList();
+                            // A null subInfo list here indicates the current state is unknown
+                            // but not necessarily empty, simply ignore it. Another call to the
+                            // listener will be invoked in the future.
+                            if (activeSubInfoList == null) return;
+                            mConnectivityServiceHandler.post(() -> {
+                                mCachedCarrierIdPerSubId.clear();
 
-                                    for (final SubscriptionInfo subInfo : activeSubInfoList) {
-                                        mCachedCarrierIdPerSubId.put(subInfo.getSubscriptionId(),
-                                                subInfo.getCarrierId());
-                                    }
-                                });
-                            }
-                        }));
+                                for (final SubscriptionInfo subInfo : activeSubInfoList) {
+                                    mCachedCarrierIdPerSubId.put(subInfo.getSubscriptionId(),
+                                            subInfo.getCarrierId());
+                                }
+                            });
+                        }
+                    };
+            mListenerFuture.complete(listener);
+            mSubscriptionManager.addOnSubscriptionsChangedListener(r -> r.run(), listener);
+        });
     }
 
     /** Ensures the list of duration metrics is large enough for number of registered keepalives. */
     private void ensureDurationPerNumOfKeepaliveSize() {
         if (mNumActiveKeepalive < 0 || mNumRegisteredKeepalive < 0) {
-            throw new IllegalStateException(
-                    "Number of active or registered keepalives is negative");
+            disableTracker("Number of active or registered keepalives is negative");
+            return;
         }
         if (mNumActiveKeepalive > mNumRegisteredKeepalive) {
-            throw new IllegalStateException(
-                    "Number of active keepalives greater than registered keepalives");
+            disableTracker("Number of active keepalives greater than registered keepalives");
+            return;
         }
 
         while (mDurationPerNumOfKeepalive.size() <= mNumRegisteredKeepalive) {
@@ -409,10 +467,12 @@
             int appUid,
             boolean isAutoKeepalive) {
         ensureRunningOnHandlerThread();
+        if (!isEnabled()) return;
         final int keepaliveId = getKeepaliveId(network, slot);
+        if (keepaliveId == INVALID_KEEPALIVE_ID) return;
         if (mKeepaliveStatsPerId.contains(keepaliveId)) {
-            throw new IllegalArgumentException(
-                    "Attempt to start keepalive stats on a known network, slot pair");
+            disableTracker("Attempt to start keepalive stats on a known network, slot pair");
+            return;
         }
 
         mNumKeepaliveRequests++;
@@ -440,13 +500,11 @@
     /**
      * Inform the KeepaliveStatsTracker that the keepalive with the given network, slot pair has
      * updated its active state to keepaliveActive.
-     *
-     * @return the KeepaliveStats associated with the network, slot pair or null if it is unknown.
      */
-    private @NonNull KeepaliveStats onKeepaliveActive(
+    private void onKeepaliveActive(
             @NonNull Network network, int slot, boolean keepaliveActive) {
         final long timeNow = mDependencies.getElapsedRealtime();
-        return onKeepaliveActive(network, slot, keepaliveActive, timeNow);
+        onKeepaliveActive(network, slot, keepaliveActive, timeNow);
     }
 
     /**
@@ -457,45 +515,53 @@
      * @param slot the slot number of the keepalive
      * @param keepaliveActive the new active state of the keepalive
      * @param timeNow a timestamp obtained using Dependencies.getElapsedRealtime
-     * @return the KeepaliveStats associated with the network, slot pair or null if it is unknown.
      */
-    private @NonNull KeepaliveStats onKeepaliveActive(
+    private void onKeepaliveActive(
             @NonNull Network network, int slot, boolean keepaliveActive, long timeNow) {
-        ensureRunningOnHandlerThread();
-
         final int keepaliveId = getKeepaliveId(network, slot);
-        if (!mKeepaliveStatsPerId.contains(keepaliveId)) {
-            throw new IllegalArgumentException(
-                    "Attempt to set active keepalive on an unknown network, slot pair");
+        if (keepaliveId == INVALID_KEEPALIVE_ID) return;
+
+        final KeepaliveStats keepaliveStats = mKeepaliveStatsPerId.get(keepaliveId, null);
+
+        if (keepaliveStats == null) {
+            disableTracker("Attempt to set active keepalive on an unknown network, slot pair");
+            return;
         }
         updateDurationsPerNumOfKeepalive(timeNow);
 
-        final KeepaliveStats keepaliveStats = mKeepaliveStatsPerId.get(keepaliveId);
         if (keepaliveActive != keepaliveStats.isKeepaliveActive()) {
             mNumActiveKeepalive += keepaliveActive ? 1 : -1;
         }
 
         keepaliveStats.updateLifetimeStatsAndSetActive(timeNow, keepaliveActive);
-        return keepaliveStats;
     }
 
     /** Inform the KeepaliveStatsTracker a keepalive has just been paused. */
     public void onPauseKeepalive(@NonNull Network network, int slot) {
+        ensureRunningOnHandlerThread();
+        if (!isEnabled()) return;
         onKeepaliveActive(network, slot, /* keepaliveActive= */ false);
     }
 
     /** Inform the KeepaliveStatsTracker a keepalive has just been resumed. */
     public void onResumeKeepalive(@NonNull Network network, int slot) {
+        ensureRunningOnHandlerThread();
+        if (!isEnabled()) return;
         onKeepaliveActive(network, slot, /* keepaliveActive= */ true);
     }
 
     /** Inform the KeepaliveStatsTracker a keepalive has just been stopped. */
     public void onStopKeepalive(@NonNull Network network, int slot) {
+        ensureRunningOnHandlerThread();
+        if (!isEnabled()) return;
+
         final int keepaliveId = getKeepaliveId(network, slot);
+        if (keepaliveId == INVALID_KEEPALIVE_ID) return;
         final long timeNow = mDependencies.getElapsedRealtime();
 
-        final KeepaliveStats keepaliveStats =
-                onKeepaliveActive(network, slot, /* keepaliveActive= */ false, timeNow);
+        onKeepaliveActive(network, slot, /* keepaliveActive= */ false, timeNow);
+        final KeepaliveStats keepaliveStats = mKeepaliveStatsPerId.get(keepaliveId, null);
+        if (keepaliveStats == null) return;
 
         mNumRegisteredKeepalive--;
 
@@ -634,18 +700,82 @@
         return metrics;
     }
 
-    /** Writes the stored metrics to ConnectivityStatsLog and resets.  */
+    private void disableTracker(String msg) {
+        if (!mEnabled.compareAndSet(/* expectedValue= */ true, /* newValue= */ false)) {
+            // already disabled
+            return;
+        }
+        Log.wtf(TAG, msg + ". Disabling KeepaliveStatsTracker");
+        mContext.unregisterReceiver(mBroadcastReceiver);
+        // The returned future is ignored since it is void and the is never completed exceptionally.
+        final CompletableFuture<Void> unused = mListenerFuture.thenAcceptAsync(
+                listener -> mSubscriptionManager.removeOnSubscriptionsChangedListener(listener),
+                BackgroundThread.getExecutor());
+    }
+
+    /** Whether this tracker is enabled. This method is thread safe. */
+    public boolean isEnabled() {
+        return mEnabled.get();
+    }
+
+    /**
+     * Checks the DailykeepaliveInfoReported for the following:
+     * 1. total active durations/lifetimes <= total registered durations/lifetimes.
+     * 2. Total time in Durations == total time in Carrier lifetime stats
+     * 3. The total elapsed real time spent is within expectations.
+     */
+    @VisibleForTesting
+    public boolean allMetricsExpected(DailykeepaliveInfoReported dailyKeepaliveInfoReported) {
+        int totalRegistered = 0;
+        int totalActiveDurations = 0;
+        int totalTimeSpent = 0;
+        for (DurationForNumOfKeepalive durationForNumOfKeepalive: dailyKeepaliveInfoReported
+                .getDurationPerNumOfKeepalive().getDurationForNumOfKeepaliveList()) {
+            final int n = durationForNumOfKeepalive.getNumOfKeepalive();
+            totalRegistered += durationForNumOfKeepalive.getKeepaliveRegisteredDurationsMsec() * n;
+            totalActiveDurations += durationForNumOfKeepalive.getKeepaliveActiveDurationsMsec() * n;
+            totalTimeSpent += durationForNumOfKeepalive.getKeepaliveRegisteredDurationsMsec();
+        }
+        int totalLifetimes = 0;
+        int totalActiveLifetimes = 0;
+        for (KeepaliveLifetimeForCarrier keepaliveLifetimeForCarrier: dailyKeepaliveInfoReported
+                .getKeepaliveLifetimePerCarrier().getKeepaliveLifetimeForCarrierList()) {
+            totalLifetimes += keepaliveLifetimeForCarrier.getLifetimeMsec();
+            totalActiveLifetimes += keepaliveLifetimeForCarrier.getActiveLifetimeMsec();
+        }
+        return totalActiveDurations <= totalRegistered && totalActiveLifetimes <= totalLifetimes
+                && totalLifetimes == totalRegistered && totalActiveLifetimes == totalActiveDurations
+                && totalTimeSpent <= MAX_EXPECTED_DURATION_MS;
+    }
+
+    /** Writes the stored metrics to ConnectivityStatsLog and resets. */
     public void writeAndResetMetrics() {
         ensureRunningOnHandlerThread();
+        // Keepalive stats use repeated atoms, which are only supported on T+. If written to statsd
+        // on S- they will bootloop the system, so they must not be sent on S-. See b/289471411.
+        if (!SdkLevel.isAtLeastT()) {
+            Log.d(TAG, "KeepaliveStatsTracker is disabled before T, skipping write");
+            return;
+        }
+        if (!isEnabled()) {
+            Log.d(TAG, "KeepaliveStatsTracker is disabled, skipping write");
+            return;
+        }
+
         final DailykeepaliveInfoReported dailyKeepaliveInfoReported = buildAndResetMetrics();
-        ConnectivityStatsLog.write(
-                ConnectivityStatsLog.DAILY_KEEPALIVE_INFO_REPORTED,
-                dailyKeepaliveInfoReported.getDurationPerNumOfKeepalive().toByteArray(),
-                dailyKeepaliveInfoReported.getKeepaliveLifetimePerCarrier().toByteArray(),
-                dailyKeepaliveInfoReported.getKeepaliveRequests(),
-                dailyKeepaliveInfoReported.getAutomaticKeepaliveRequests(),
-                dailyKeepaliveInfoReported.getDistinctUserCount(),
-                CollectionUtils.toIntArray(dailyKeepaliveInfoReported.getUidList()));
+        if (!allMetricsExpected(dailyKeepaliveInfoReported)) {
+            Log.wtf(TAG, "Unexpected metrics values: " + dailyKeepaliveInfoReported.toString());
+        }
+        mDependencies.writeStats(dailyKeepaliveInfoReported);
+    }
+
+    /** Dump KeepaliveStatsTracker state. */
+    public void dump(IndentingPrintWriter pw) {
+        ensureRunningOnHandlerThread();
+        pw.println("KeepaliveStatsTracker enabled: " + isEnabled());
+        pw.increaseIndent();
+        pw.println(buildKeepaliveMetrics().toString());
+        pw.decreaseIndent();
     }
 
     private void ensureRunningOnHandlerThread() {
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
index cbf091b..a51f09f 100644
--- a/service/src/com/android/server/connectivity/KeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java
@@ -34,6 +34,8 @@
 import static android.net.SocketKeepalive.SUCCESS;
 import static android.net.SocketKeepalive.SUCCESS_PAUSED;
 
+import static com.android.net.module.util.FeatureVersions.FEATURE_CLAT_ADDRESS_TRANSLATE;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -54,14 +56,18 @@
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.Log;
+import android.util.Pair;
 
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.HexDump;
 import com.android.net.module.util.IpUtils;
 
 import java.io.FileDescriptor;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
@@ -83,6 +89,9 @@
 
     public static final String PERMISSION = android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
 
+    private static final String CONFIG_DISABLE_CLAT_ADDRESS_TRANSLATE =
+            "disable_clat_address_translate";
+
     /** Keeps track of keepalive requests. */
     private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives =
             new HashMap<> ();
@@ -104,19 +113,21 @@
     // Allowed unprivileged keepalive slots per uid. Caller's permission will be enforced if
     // the number of remaining keepalive slots is less than or equal to the threshold.
     private final int mAllowedUnprivilegedSlotsForUid;
-
+    private final Dependencies mDependencies;
     public KeepaliveTracker(Context context, Handler handler) {
-        this(context, handler, new TcpKeepaliveController(handler));
+        this(context, handler, new TcpKeepaliveController(handler), new Dependencies());
     }
 
     @VisibleForTesting
-    KeepaliveTracker(Context context, Handler handler, TcpKeepaliveController tcpController) {
+    public KeepaliveTracker(Context context, Handler handler, TcpKeepaliveController tcpController,
+            Dependencies deps) {
         mTcpController = tcpController;
         mContext = context;
+        mDependencies = deps;
 
-        mSupportedKeepalives = KeepaliveResourceUtil.getSupportedKeepalives(context);
+        mSupportedKeepalives = mDependencies.getSupportedKeepalives(mContext);
 
-        final ConnectivityResources res = new ConnectivityResources(mContext);
+        final ConnectivityResources res = mDependencies.createConnectivityResources(mContext);
         mReservedPrivilegedSlots = res.get().getInteger(
                 R.integer.config_reservedPrivilegedKeepaliveSlots);
         mAllowedUnprivilegedSlotsForUid = res.get().getInteger(
@@ -290,11 +301,15 @@
 
         private int checkSourceAddress() {
             // Check that we have the source address.
-            for (InetAddress address : mNai.linkProperties.getAddresses()) {
+            for (InetAddress address : mNai.linkProperties.getAllAddresses()) {
                 if (address.equals(mPacket.getSrcAddress())) {
                     return SUCCESS;
                 }
             }
+            // Or the address is the clat source address.
+            if (mPacket.getSrcAddress().equals(mNai.getClatv6SrcAddress())) {
+                return SUCCESS;
+            }
             return ERROR_INVALID_IP_ADDRESS;
         }
 
@@ -477,6 +492,15 @@
             return new KeepaliveInfo(mCallback, mNai, mPacket, mPid, mUid, mInterval, mType,
                     fd, mSlot, true /* resumed */);
         }
+
+        /**
+         * Construct a new KeepaliveInfo from existing KeepaliveInfo with a new KeepalivePacketData.
+         */
+        public KeepaliveInfo withPacketData(@NonNull KeepalivePacketData packet)
+                throws InvalidSocketException {
+            return new KeepaliveInfo(mCallback, mNai, packet, mPid, mUid, mInterval, mType,
+                    mFd, mSlot, mResumed);
+        }
     }
 
     void notifyErrorCallback(ISocketKeepaliveCallback cb, int error) {
@@ -510,15 +534,51 @@
      * Handle start keepalives with the message.
      *
      * @param ki the keepalive to start.
-     * @return SUCCESS if the keepalive is successfully starting and the error reason otherwise.
+     * @return Pair of (SUCCESS if the keepalive is successfully starting and the error reason
+     *         otherwise, the started KeepaliveInfo object)
      */
-    public int handleStartKeepalive(KeepaliveInfo ki) {
-        NetworkAgentInfo nai = ki.getNai();
+    public Pair<Integer, KeepaliveInfo> handleStartKeepalive(KeepaliveInfo ki) {
+        final KeepaliveInfo newKi;
+        try {
+            newKi = handleUpdateKeepaliveForClat(ki);
+        } catch (InvalidSocketException | InvalidPacketException e) {
+            Log.e(TAG, "Fail to construct keepalive packet");
+            notifyErrorCallback(ki.mCallback, ERROR_INVALID_IP_ADDRESS);
+            // Fail to create new keepalive packet for clat. Return the original keepalive info.
+            return new Pair<>(ERROR_INVALID_IP_ADDRESS, ki);
+        }
+
+        final NetworkAgentInfo nai = newKi.getNai();
         // If this was a paused keepalive, then reuse the same slot that was kept for it. Otherwise,
         // use the first free slot for this network agent.
-        final int slot = NO_KEEPALIVE != ki.mSlot ? ki.mSlot : findFirstFreeSlot(nai);
-        mKeepalives.get(nai).put(slot, ki);
-        return ki.start(slot);
+        final int slot = NO_KEEPALIVE != newKi.mSlot ? newKi.mSlot : findFirstFreeSlot(nai);
+        mKeepalives.get(nai).put(slot, newKi);
+
+        return new Pair<>(newKi.start(slot), newKi);
+    }
+
+    private KeepaliveInfo handleUpdateKeepaliveForClat(KeepaliveInfo ki)
+            throws InvalidSocketException, InvalidPacketException {
+        if (!mDependencies.isAddressTranslationEnabled(mContext)) return ki;
+
+        // Translation applies to only NAT-T keepalive
+        if (ki.mType != KeepaliveInfo.TYPE_NATT) return ki;
+        // Only try to translate address if the packet source address is the clat's source address.
+        if (!ki.mPacket.getSrcAddress().equals(ki.getNai().getClatv4SrcAddress())) return ki;
+
+        final InetAddress dstAddr = ki.mPacket.getDstAddress();
+        // Do not perform translation for a v6 dst address.
+        if (!(dstAddr instanceof Inet4Address)) return ki;
+
+        final Inet6Address address = ki.getNai().translateV4toClatV6((Inet4Address) dstAddr);
+
+        if (address == null) return ki;
+
+        final int srcPort = ki.mPacket.getSrcPort();
+        final KeepaliveInfo newInfo = ki.withPacketData(NattKeepalivePacketData.nattKeepalivePacket(
+                ki.getNai().getClatv6SrcAddress(), srcPort, address, NATT_PORT));
+        Log.d(TAG, "Src is clat v4 address. Convert from " + ki + " to " + newInfo);
+        return newInfo;
     }
 
     public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) {
@@ -526,8 +586,12 @@
         if (networkKeepalives != null) {
             final ArrayList<KeepaliveInfo> kalist = new ArrayList(networkKeepalives.values());
             for (KeepaliveInfo ki : kalist) {
-                // Check if keepalive is already stopped
+                // If the keepalive is paused, then it is already stopped with the hardware and so
+                // continue. Note that to send the appropriate stop reason callback,
+                // AutomaticOnOffKeepaliveTracker will call finalizePausedKeepalive which will also
+                // finally remove this keepalive slot from the array.
                 if (ki.mStopReason == SUCCESS_PAUSED) continue;
+
                 ki.stop(reason);
                 // Clean up keepalives since the network agent is disconnected and unable to pass
                 // back asynchronous result of stop().
@@ -748,6 +812,7 @@
             srcAddress = InetAddresses.parseNumericAddress(srcAddrString);
             dstAddress = InetAddresses.parseNumericAddress(dstAddrString);
         } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Fail to construct address", e);
             notifyErrorCallback(cb, ERROR_INVALID_IP_ADDRESS);
             return null;
         }
@@ -757,6 +822,7 @@
             packet = NattKeepalivePacketData.nattKeepalivePacket(
                     srcAddress, srcPort, dstAddress, NATT_PORT);
         } catch (InvalidPacketException e) {
+            Log.e(TAG, "Fail to construct keepalive packet", e);
             notifyErrorCallback(cb, e.getError());
             return null;
         }
@@ -889,4 +955,46 @@
         }
         pw.decreaseIndent();
     }
+
+    /**
+     * Dependencies class for testing.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * Read supported keepalive count for each transport type from overlay resource. This should
+         * be used to create a local variable store of resource customization, and set as the
+         * input for {@link getSupportedKeepalivesForNetworkCapabilities}.
+         *
+         * @param context The context to read resource from.
+         * @return An array of supported keepalive count for each transport type.
+         */
+        @NonNull
+        public int[] getSupportedKeepalives(@NonNull Context context) {
+            return KeepaliveResourceUtil.getSupportedKeepalives(context);
+        }
+
+        /**
+         * Create a new {@link ConnectivityResources}.
+         */
+        @NonNull
+        public ConnectivityResources createConnectivityResources(@NonNull Context context) {
+            return new ConnectivityResources(context);
+        }
+
+        /**
+         * Return if keepalive address translation with clat feature is supported or not.
+         *
+         * This is controlled by both isFeatureSupported() and isFeatureEnabled(). The
+         * isFeatureSupported() checks whether device contains the minimal required module
+         * version for FEATURE_CLAT_ADDRESS_TRANSLATE. The isTetheringFeatureForceDisabled()
+         * checks the DeviceConfig flag that can be updated via DeviceConfig push to control
+         * the overall feature.
+         */
+        public boolean isAddressTranslationEnabled(@NonNull Context context) {
+            return DeviceConfigUtils.isFeatureSupported(context, FEATURE_CLAT_ADDRESS_TRANSLATE)
+                    && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context,
+                            CONFIG_DISABLE_CLAT_ADDRESS_TRANSLATE);
+        }
+    }
 }
diff --git a/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
new file mode 100644
index 0000000..af4aee5
--- /dev/null
+++ b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
@@ -0,0 +1,842 @@
+/*
+ * 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.connectivity;
+
+import static android.net.MulticastRoutingConfig.FORWARD_NONE;
+import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
+import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.EADDRINUSE;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_IPV6;
+import static android.system.OsConstants.SOCK_CLOEXEC;
+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;
+import android.net.NetworkUtils;
+import android.os.Handler;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+import com.android.net.module.util.PacketReader;
+import com.android.net.module.util.SocketUtils;
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.RtNetlinkRouteMessage;
+import com.android.net.module.util.structs.StructMf6cctl;
+import com.android.net.module.util.structs.StructMif6ctl;
+import com.android.net.module.util.structs.StructMrt6Msg;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Class to coordinate multicast routing between network interfaces.
+ *
+ * <p>Supports IPv6 multicast routing.
+ *
+ * <p>Note that usage of this class is not thread-safe. All public methods must be called from the
+ * same thread that the handler from {@code dependencies.getHandler} is associated.
+ */
+public class MulticastRoutingCoordinatorService {
+    private static final String TAG = MulticastRoutingCoordinatorService.class.getSimpleName();
+    private static final int ICMP6_FILTER = 1;
+    private static final int MRT6_INIT = 200;
+    private static final int MRT6_ADD_MIF = 202;
+    private static final int MRT6_DEL_MIF = 203;
+    private static final int MRT6_ADD_MFC = 204;
+    private static final int MRT6_DEL_MFC = 205;
+    private static final int ONE = 1;
+
+    private final Dependencies mDependencies;
+
+    private final Handler mHandler;
+    private final MulticastNocacheUpcallListener mMulticastNoCacheUpcallListener;
+    @NonNull private final FileDescriptor mMulticastRoutingFd; // For multicast routing config
+    @NonNull private final MulticastSocket mMulticastSocket; // For join group and leave group
+
+    @VisibleForTesting public static final int MFC_INACTIVE_CHECK_INTERVAL_MS = 60_000;
+    @VisibleForTesting public static final int MFC_INACTIVE_TIMEOUT_MS = 300_000;
+    @VisibleForTesting public static final int MFC_MAX_NUMBER_OF_ENTRIES = 1_000;
+
+    // The kernel supports max 32 virtual interfaces per multicast routing table.
+    private static final int MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES = 32;
+
+    /** Tracks if checking for inactive MFC has been scheduled */
+    private boolean mMfcPollingScheduled = false;
+
+    /** Mapping from multicast virtual interface index to interface name */
+    private SparseArray<String> mVirtualInterfaces =
+            new SparseArray<>(MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES);
+    /** Mapping from physical interface index to interface name */
+    private SparseArray<String> mInterfaces =
+            new SparseArray<>(MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES);
+
+    /** Mapping of iif to PerInterfaceMulticastRoutingConfig */
+    private Map<String, PerInterfaceMulticastRoutingConfig> mMulticastRoutingConfigs =
+            new HashMap<String, PerInterfaceMulticastRoutingConfig>();
+
+    private static final class PerInterfaceMulticastRoutingConfig {
+        // mapping of oif name to MulticastRoutingConfig
+        public Map<String, MulticastRoutingConfig> oifConfigs =
+                new HashMap<String, MulticastRoutingConfig>();
+    }
+
+    /** Tracks the MFCs added to kernel. Using LinkedHashMap to keep the added order, so
+    // when the number of MFCs reaches the max limit then the earliest added one is removed. */
+    private LinkedHashMap<MfcKey, MfcValue> mMfcs = new LinkedHashMap<>();
+
+    public MulticastRoutingCoordinatorService(Handler h) {
+        this(h, new Dependencies());
+    }
+
+    @VisibleForTesting
+    /* @throws UnsupportedOperationException if multicast routing is not supported */
+    public MulticastRoutingCoordinatorService(Handler h, Dependencies dependencies) {
+        mDependencies = dependencies;
+        mMulticastRoutingFd = mDependencies.createMulticastRoutingSocket();
+        mMulticastSocket = mDependencies.createMulticastSocket();
+        mHandler = h;
+        mMulticastNoCacheUpcallListener =
+                new MulticastNocacheUpcallListener(mHandler, mMulticastRoutingFd);
+        mHandler.post(() -> mMulticastNoCacheUpcallListener.start());
+    }
+
+    private void checkOnHandlerThread() {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            throw new IllegalStateException(
+                    "Not running on ConnectivityService thread (" + mHandler.getLooper() + ") : "
+                            + Looper.myLooper());
+        }
+    }
+
+    private Integer getInterfaceIndex(String ifName) {
+        int mapIndex = getIndexForValue(mInterfaces, ifName);
+        if (mapIndex < 0) return null;
+        return mInterfaces.keyAt(mapIndex);
+    }
+
+    /**
+     * Apply multicast routing configuration
+     *
+     * @param iifName name of the incoming interface
+     * @param oifName name of the outgoing interface
+     * @param newConfig the multicast routing configuration to be applied from iif to oif
+     * @throws MulticastRoutingException when failed to apply the config
+     */
+    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
+            if (!maybeAddAndTrackInterface(iifName) || !maybeAddAndTrackInterface(oifName)) {
+                Log.e(
+                        TAG,
+                        "Failed to apply multicast routing config from "
+                                + iifName
+                                + " to "
+                                + oifName);
+                return;
+            }
+        }
+
+        final MulticastRoutingConfig oldConfig = getMulticastRoutingConfig(iifName, oifName);
+
+        if (oldConfig.equals(newConfig)) return;
+
+        int oldMode = oldConfig.getForwardingMode();
+        int newMode = newConfig.getForwardingMode();
+        Integer iifIndex = getInterfaceIndex(iifName);
+        if (iifIndex == null) {
+            // This cannot happen unless the new config has FORWARD_NONE but is not the same
+            // as the old config. This is not possible in current code.
+            Log.wtf(TAG, "Adding multicast configuration on null interface?");
+            return;
+        }
+
+        // When new addresses are added to FORWARD_SELECTED mode, join these multicast groups
+        // on their upstream interface, so upstream multicast routers know about the subscription.
+        // When addresses are removed from FORWARD_SELECTED mode, leave the multicast groups.
+        final Set<Inet6Address> oldListeningAddresses =
+                (oldMode == FORWARD_SELECTED)
+                        ? oldConfig.getListeningAddresses()
+                        : new ArraySet<>();
+        final Set<Inet6Address> newListeningAddresses =
+                (newMode == FORWARD_SELECTED)
+                        ? newConfig.getListeningAddresses()
+                        : new ArraySet<>();
+        final CompareResult<Inet6Address> addressDiff =
+                new CompareResult<>(oldListeningAddresses, newListeningAddresses);
+        joinGroups(iifIndex, addressDiff.added);
+        leaveGroups(iifIndex, addressDiff.removed);
+
+        setMulticastRoutingConfig(iifName, oifName, newConfig);
+        Log.d(
+                TAG,
+                "Applied multicast routing config for iif "
+                        + iifName
+                        + " to oif "
+                        + oifName
+                        + " with Config "
+                        + newConfig);
+
+        // Update existing MFCs to make sure they align with the updated configuration
+        updateMfcs();
+
+        if (newConfig.getForwardingMode() == FORWARD_NONE) {
+            if (!hasActiveMulticastConfig(iifName)) {
+                removeInterfaceFromMulticastRouting(iifName);
+            }
+            if (!hasActiveMulticastConfig(oifName)) {
+                removeInterfaceFromMulticastRouting(oifName);
+            }
+        }
+    }
+
+    /**
+     * Removes an network interface from multicast routing.
+     *
+     * <p>Remove the network interface from multicast configs and remove it from the list of
+     * multicast routing interfaces in the kernel
+     *
+     * @param ifName name of the interface that should be removed
+     */
+    @VisibleForTesting
+    public void removeInterfaceFromMulticastRouting(final String ifName) {
+        checkOnHandlerThread();
+        final Integer virtualIndex = getVirtualInterfaceIndex(ifName);
+        if (virtualIndex == null) return;
+
+        updateMfcs();
+        mInterfaces.removeAt(getIndexForValue(mInterfaces, ifName));
+        mVirtualInterfaces.remove(virtualIndex);
+        try {
+            mDependencies.setsockoptMrt6DelMif(mMulticastRoutingFd, virtualIndex);
+            Log.d(TAG, "Removed mifi " + virtualIndex + " from MIF");
+        } catch (ErrnoException e) {
+            Log.e(TAG, "failed to remove multicast virtual interface" + virtualIndex, e);
+        }
+    }
+
+    /**
+     * 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) {
+            Log.e(TAG, "Can't allocate new multicast virtual interface");
+            return -1;
+        }
+        for (int i = 0; i < mVirtualInterfaces.size(); i++) {
+            if (!mVirtualInterfaces.contains(i)) {
+                return i;
+            }
+        }
+        return mVirtualInterfaces.size();
+    }
+
+    @VisibleForTesting
+    public Integer getVirtualInterfaceIndex(String ifName) {
+        int mapIndex = getIndexForValue(mVirtualInterfaces, ifName);
+        if (mapIndex < 0) return null;
+        return mVirtualInterfaces.keyAt(mapIndex);
+    }
+
+    private Integer getVirtualInterfaceIndex(int physicalIndex) {
+        String ifName = mInterfaces.get(physicalIndex);
+        if (ifName == null) {
+            // This is only used to match MFCs from kernel to MFCs we know about.
+            // Unknown MFCs should be ignored.
+            return null;
+        }
+        return getVirtualInterfaceIndex(ifName);
+    }
+
+    private String getInterfaceName(int virtualIndex) {
+        return mVirtualInterfaces.get(virtualIndex);
+    }
+
+    /**
+     * 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 (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,
+                            (short) 0 /* mif6c_flags */,
+                            (short) 1 /* vifc_threshold */,
+                            ifIndex,
+                            0 /* vifc_rate_limit */);
+        try {
+            mDependencies.setsockoptMrt6AddMif(mMulticastRoutingFd, mif6ctl);
+            Log.d(TAG, "Added mifi " + nextVirtualIndex + " to MIF");
+        } catch (ErrnoException e) {
+            Log.e(TAG, "failed to add multicast virtual interface", e);
+            return false;
+        }
+        mVirtualInterfaces.put(nextVirtualIndex, ifName);
+        mInterfaces.put(ifIndex, ifName);
+        return true;
+    }
+
+    @VisibleForTesting
+    public MulticastRoutingConfig getMulticastRoutingConfig(String iifName, String oifName) {
+        PerInterfaceMulticastRoutingConfig configs = mMulticastRoutingConfigs.get(iifName);
+        final MulticastRoutingConfig defaultConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+        if (configs == null) {
+            return defaultConfig;
+        } else {
+            return configs.oifConfigs.getOrDefault(oifName, defaultConfig);
+        }
+    }
+
+    private void setMulticastRoutingConfig(
+            final String iifName, final String oifName, final MulticastRoutingConfig config) {
+        checkOnHandlerThread();
+        PerInterfaceMulticastRoutingConfig iifConfig = mMulticastRoutingConfigs.get(iifName);
+
+        if (config.getForwardingMode() == FORWARD_NONE) {
+            if (iifConfig != null) {
+                iifConfig.oifConfigs.remove(oifName);
+            }
+            if (iifConfig.oifConfigs.isEmpty()) {
+                mMulticastRoutingConfigs.remove(iifName);
+            }
+            return;
+        }
+
+        if (iifConfig == null) {
+            iifConfig = new PerInterfaceMulticastRoutingConfig();
+            mMulticastRoutingConfigs.put(iifName, iifConfig);
+        }
+        iifConfig.oifConfigs.put(oifName, config);
+    }
+
+    /** Returns whether an interface has multicast routing config */
+    private boolean hasActiveMulticastConfig(final String ifName) {
+        // FORWARD_NONE configs are not saved in the config tables, so
+        // any existing config is an active multicast routing config
+        if (mMulticastRoutingConfigs.containsKey(ifName)) return true;
+        for (var pic : mMulticastRoutingConfigs.values()) {
+            if (pic.oifConfigs.containsKey(ifName)) return true;
+        }
+        return false;
+    }
+
+    /**
+     * A multicast forwarding cache (MFC) entry holds a multicast forwarding route where packet from
+     * incoming interface(iif) with source address(S) to group address (G) are forwarded to outgoing
+     * interfaces(oifs).
+     *
+     * <p>iif, S and G identifies an MFC entry. For example an MFC1 is added: [iif1, S1, G1, oifs1]
+     * Adding another MFC2 of [iif1, S1, G1, oifs2] to the kernel overwrites MFC1.
+     */
+    private static final class MfcKey {
+        public final int mIifVirtualIdx;
+        public final Inet6Address mSrcAddr;
+        public final Inet6Address mDstAddr;
+
+        public MfcKey(int iif, Inet6Address src, Inet6Address dst) {
+            mIifVirtualIdx = iif;
+            mSrcAddr = src;
+            mDstAddr = dst;
+        }
+
+        public boolean equals(Object other) {
+            if (other == this) {
+                return true;
+            } else if (!(other instanceof MfcKey)) {
+                return false;
+            } else {
+                MfcKey otherKey = (MfcKey) other;
+                return mIifVirtualIdx == otherKey.mIifVirtualIdx
+                        && mSrcAddr.equals(otherKey.mSrcAddr)
+                        && mDstAddr.equals(otherKey.mDstAddr);
+            }
+        }
+
+        public int hashCode() {
+            return Objects.hash(mIifVirtualIdx, mSrcAddr, mDstAddr);
+        }
+
+        public String toString() {
+            return "{iifVirtualIndex: "
+                    + Integer.toString(mIifVirtualIdx)
+                    + ", sourceAddress: "
+                    + mSrcAddr.toString()
+                    + ", destinationAddress: "
+                    + mDstAddr.toString()
+                    + "}";
+        }
+    }
+
+    private static final class MfcValue {
+        private Set<Integer> mOifVirtualIndices;
+        // timestamp of when the mfc was last used in the kernel
+        // (e.g. created, or used to forward a packet)
+        private Instant mLastUsedAt;
+
+        public MfcValue(Set<Integer> oifs, Instant timestamp) {
+            mOifVirtualIndices = oifs;
+            mLastUsedAt = timestamp;
+        }
+
+        public boolean hasSameOifsAs(MfcValue other) {
+            return this.mOifVirtualIndices.equals(other.mOifVirtualIndices);
+        }
+
+        public boolean equals(Object other) {
+            if (other == this) {
+                return true;
+            } else if (!(other instanceof MfcValue)) {
+                return false;
+            } else {
+                MfcValue otherValue = (MfcValue) other;
+                return mOifVirtualIndices.equals(otherValue.mOifVirtualIndices)
+                        && mLastUsedAt.equals(otherValue.mLastUsedAt);
+            }
+        }
+
+        public int hashCode() {
+            return Objects.hash(mOifVirtualIndices, mLastUsedAt);
+        }
+
+        public Set<Integer> getOifIndices() {
+            return mOifVirtualIndices;
+        }
+
+        public void setLastUsedAt(Instant timestamp) {
+            mLastUsedAt = timestamp;
+        }
+
+        public Instant getLastUsedAt() {
+            return mLastUsedAt;
+        }
+
+        public String toString() {
+            return "{oifVirtualIdxes: "
+                    + mOifVirtualIndices.toString()
+                    + ", lastUsedAt: "
+                    + mLastUsedAt.toString()
+                    + "}";
+        }
+    }
+
+    /**
+     * Returns the MFC value for the given MFC key according to current multicast routing config. If
+     * the MFC should be removed return null.
+     */
+    private MfcValue computeMfcValue(int iif, Inet6Address dst) {
+        final int dstScope = getGroupAddressScope(dst);
+        Set<Integer> forwardingOifs = new ArraySet<>();
+
+        PerInterfaceMulticastRoutingConfig iifConfig =
+                mMulticastRoutingConfigs.get(getInterfaceName(iif));
+
+        if (iifConfig == null) {
+            // An iif may have been removed from multicast routing, in this
+            // case remove the MFC directly
+            return null;
+        }
+
+        for (var config : iifConfig.oifConfigs.entrySet()) {
+            if ((config.getValue().getForwardingMode() == FORWARD_WITH_MIN_SCOPE
+                            && config.getValue().getMinimumScope() <= dstScope)
+                    || (config.getValue().getForwardingMode() == FORWARD_SELECTED
+                            && config.getValue().getListeningAddresses().contains(dst))) {
+                forwardingOifs.add(getVirtualInterfaceIndex(config.getKey()));
+            }
+        }
+
+        return new MfcValue(forwardingOifs, Instant.now(mDependencies.getClock()));
+    }
+
+    /**
+     * Given the iif, source address and group destination address, add an MFC entry or update the
+     * existing MFC according to the multicast routing config. If such an MFC should not exist,
+     * return null for caller of the function to remove it.
+     *
+     * <p>Note that if a packet has no matching MFC entry in the kernel, kernel creates an
+     * unresolved route and notifies multicast socket with a NOCACHE upcall message. The unresolved
+     * route is kept for no less than 10s. If packets with the same source and destination arrives
+     * before the 10s timeout, they will not be notified. Thus we need to add a 'blocking' MFC which
+     * is an MFC with an empty oif list. When the multicast configs changes, the 'blocking' MFC
+     * will be updated to a 'forwarding' MFC so that corresponding multicast traffic can be
+     * forwarded instantly.
+     *
+     * @return {@code true} if the MFC is updated and no operation is needed from caller.
+     * {@code false} if the MFC should not be added, caller of the function should remove
+     * the MFC if needed.
+     */
+    private boolean addOrUpdateMfc(int vif, Inet6Address src, Inet6Address dst) {
+        checkOnHandlerThread();
+        final MfcKey key = new MfcKey(vif, src, dst);
+        final MfcValue value = mMfcs.get(key);
+        final MfcValue updatedValue = computeMfcValue(vif, dst);
+
+        if (updatedValue == null) {
+            return false;
+        }
+
+        if (value != null && value.hasSameOifsAs(updatedValue)) {
+            // no updates to make
+            return true;
+        }
+
+        final StructMf6cctl mf6cctl =
+                new StructMf6cctl(src, dst, vif, updatedValue.getOifIndices());
+        try {
+            mDependencies.setsockoptMrt6AddMfc(mMulticastRoutingFd, mf6cctl);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "failed to add MFC: " + e);
+            return false;
+        }
+        mMfcs.put(key, updatedValue);
+        String operation = (value == null ? "Added" : "Updated");
+        Log.d(TAG, operation + " MFC key: " + key + " value: " + updatedValue);
+        return true;
+    }
+
+    private void checkMfcsExpiration() {
+        checkOnHandlerThread();
+        // Check if there are inactive MFCs that can be removed
+        refreshMfcInactiveDuration();
+        maybeExpireMfcs();
+        if (mMfcs.size() > 0) {
+            mHandler.postDelayed(() -> checkMfcsExpiration(), MFC_INACTIVE_CHECK_INTERVAL_MS);
+            mMfcPollingScheduled = true;
+        } else {
+            mMfcPollingScheduled = false;
+        }
+    }
+
+    private void checkMfcEntriesLimit() {
+        checkOnHandlerThread();
+        // If the max number of MFC entries is reached, remove the first MFC entry. This can be
+        // any entry, as if this entry is needed again there will be a NOCACHE upcall to add it
+        // back.
+        if (mMfcs.size() == MFC_MAX_NUMBER_OF_ENTRIES) {
+            Log.w(TAG, "Reached max number of MFC entries " + MFC_MAX_NUMBER_OF_ENTRIES);
+            var iter = mMfcs.entrySet().iterator();
+            MfcKey firstMfcKey = iter.next().getKey();
+            removeMfcFromKernel(firstMfcKey);
+            iter.remove();
+        }
+    }
+
+    /**
+     * Reads multicast routes information from the kernel, and update the last used timestamp for
+     * each multicast route save in this class.
+     */
+    private void refreshMfcInactiveDuration() {
+        checkOnHandlerThread();
+        final List<RtNetlinkRouteMessage> multicastRoutes = NetlinkUtils.getIpv6MulticastRoutes();
+
+        for (var route : multicastRoutes) {
+            if (!route.isResolved()) {
+                continue; // Don't handle unresolved mfc, the kernel will recycle in 10s
+            }
+            Integer iif = getVirtualInterfaceIndex(route.getIifIndex());
+            if (iif == null) {
+                Log.e(TAG, "Can't find kernel returned IIF " + route.getIifIndex());
+                return;
+            }
+            final MfcKey key =
+                    new MfcKey(
+                            iif,
+                            (Inet6Address) route.getSource().getAddress(),
+                            (Inet6Address) route.getDestination().getAddress());
+            MfcValue value = mMfcs.get(key);
+            if (value == null) {
+                Log.e(TAG, "Can't find kernel returned MFC " + key);
+                continue;
+            }
+            value.setLastUsedAt(
+                    Instant.now(mDependencies.getClock())
+                            .minusMillis(route.getSinceLastUseMillis()));
+        }
+    }
+
+    /** Remove MFC entry from mMfcs map and the kernel if exists. */
+    private void removeMfcFromKernel(MfcKey key) {
+        checkOnHandlerThread();
+
+        final MfcValue value = mMfcs.get(key);
+        final Set<Integer> oifs = new ArraySet<>();
+        final StructMf6cctl mf6cctl =
+                new StructMf6cctl(key.mSrcAddr, key.mDstAddr, key.mIifVirtualIdx, oifs);
+        try {
+            mDependencies.setsockoptMrt6DelMfc(mMulticastRoutingFd, mf6cctl);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "failed to remove MFC: " + e);
+            return;
+        }
+        Log.d(TAG, "Removed MFC key: " + key + " value: " + value);
+    }
+
+    /**
+     * This is called every MFC_INACTIVE_CHECK_INTERVAL_MS milliseconds to remove any MFC that is
+     * inactive for more than MFC_INACTIVE_TIMEOUT_MS milliseconds.
+     */
+    private void maybeExpireMfcs() {
+        checkOnHandlerThread();
+
+        for (var it = mMfcs.entrySet().iterator(); it.hasNext(); ) {
+            var entry = it.next();
+            if (entry.getValue()
+                    .getLastUsedAt()
+                    .plusMillis(MFC_INACTIVE_TIMEOUT_MS)
+                    .isBefore(Instant.now(mDependencies.getClock()))) {
+                removeMfcFromKernel(entry.getKey());
+                it.remove();
+            }
+        }
+    }
+
+    private void updateMfcs() {
+        checkOnHandlerThread();
+
+        for (Iterator<Map.Entry<MfcKey, MfcValue>> it = mMfcs.entrySet().iterator();
+                it.hasNext(); ) {
+            MfcKey key = it.next().getKey();
+            if (!addOrUpdateMfc(key.mIifVirtualIdx, key.mSrcAddr, key.mDstAddr)) {
+                removeMfcFromKernel(key);
+                it.remove();
+            }
+        }
+
+        refreshMfcInactiveDuration();
+    }
+
+    private void joinGroups(int ifIndex, List<Inet6Address> addresses) {
+        for (Inet6Address address : addresses) {
+            InetSocketAddress socketAddress = new InetSocketAddress(address, 0);
+            try {
+                mMulticastSocket.joinGroup(
+                        socketAddress, mDependencies.getNetworkInterface(ifIndex));
+            } catch (IOException e) {
+                if (e.getCause() instanceof ErrnoException) {
+                    ErrnoException ee = (ErrnoException) e.getCause();
+                    if (ee.errno == EADDRINUSE) {
+                        // The list of added address are calculated from address changes,
+                        // repeated join group is unexpected
+                        Log.e(TAG, "Already joined group" + e);
+                        continue;
+                    }
+                }
+                Log.e(TAG, "failed to join group: " + e);
+            }
+        }
+    }
+
+    private void leaveGroups(int ifIndex, List<Inet6Address> addresses) {
+        for (Inet6Address address : addresses) {
+            InetSocketAddress socketAddress = new InetSocketAddress(address, 0);
+            try {
+                mMulticastSocket.leaveGroup(
+                        socketAddress, mDependencies.getNetworkInterface(ifIndex));
+            } catch (IOException e) {
+                Log.e(TAG, "failed to leave group: " + e);
+            }
+        }
+    }
+
+    private int getGroupAddressScope(Inet6Address address) {
+        return address.getAddress()[1] & 0xf;
+    }
+
+    /**
+     * Handles a NoCache upcall that indicates a multicast packet is received and requires
+     * a multicast forwarding cache to be added.
+     *
+     * A forwarding or blocking MFC is added according to the multicast config.
+     *
+     * The number of MFCs is checked to make sure it doesn't exceed the
+     * {@code MFC_MAX_NUMBER_OF_ENTRIES} limit.
+     */
+    @VisibleForTesting
+    public void handleMulticastNocacheUpcall(final StructMrt6Msg mrt6Msg) {
+        final int iifVid = mrt6Msg.mif;
+
+        // add MFC to forward the packet or add blocking MFC to not forward the packet
+        // If the packet comes from an interface the service doesn't care about, the
+        // addOrUpdateMfc function will return null and not MFC will be added.
+        if (!addOrUpdateMfc(iifVid, mrt6Msg.src, mrt6Msg.dst)) return;
+        // If the list of MFCs is not empty and there is no MFC check scheduled,
+        // schedule one now
+        if (!mMfcPollingScheduled) {
+            mHandler.postDelayed(() -> checkMfcsExpiration(), MFC_INACTIVE_CHECK_INTERVAL_MS);
+            mMfcPollingScheduled = true;
+        }
+
+        checkMfcEntriesLimit();
+    }
+
+    /**
+     * A packet reader that handles the packets sent to the multicast routing socket
+     */
+    private final class MulticastNocacheUpcallListener extends PacketReader {
+        private final FileDescriptor mFd;
+
+        public MulticastNocacheUpcallListener(Handler h, FileDescriptor fd) {
+            super(h);
+            mFd = fd;
+        }
+
+        @Override
+        protected FileDescriptor createFd() {
+            return mFd;
+        }
+
+        @Override
+        protected void handlePacket(byte[] recvbuf, int length) {
+            final ByteBuffer buf = ByteBuffer.wrap(recvbuf);
+            final StructMrt6Msg mrt6Msg = StructMrt6Msg.parse(buf);
+            if (mrt6Msg.msgType != StructMrt6Msg.MRT6MSG_NOCACHE) {
+                return;
+            }
+            handleMulticastNocacheUpcall(mrt6Msg);
+        }
+    }
+
+    /** Dependencies of RoutingCoordinatorService, for test injections. */
+    @VisibleForTesting
+    public static class Dependencies {
+        private final Clock mClock = Clock.system(ZoneId.systemDefault());
+
+        /**
+         * Creates a socket to configure multicast routing in the kernel.
+         *
+         * <p>If the kernel doesn't support multicast routing, then the {@code setsockoptInt} with
+         * {@code MRT6_INIT} method would fail.
+         *
+         * @return the multicast routing socket, or null if it fails to be created/configured.
+         */
+        public FileDescriptor createMulticastRoutingSocket() {
+            FileDescriptor sock = null;
+            byte[] filter = new byte[32]; // filter all ICMPv6 messages
+            try {
+                sock = Os.socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_ICMPV6);
+                Os.setsockoptInt(sock, IPPROTO_IPV6, MRT6_INIT, ONE);
+                NetworkUtils.setsockoptBytes(sock, IPPROTO_ICMPV6, ICMP6_FILTER, filter);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "failed to create multicast socket: " + e);
+                if (sock != null) {
+                    SocketUtils.closeSocketQuietly(sock);
+                }
+                throw new UnsupportedOperationException("Multicast routing is not supported ", e);
+            }
+            Log.i(TAG, "socket created for multicast routing: " + sock);
+            return sock;
+        }
+
+        public MulticastSocket createMulticastSocket() {
+            try {
+                return new MulticastSocket();
+            } catch (IOException e) {
+                Log.wtf(TAG, "Failed to create multicast socket " + e);
+                throw new IllegalStateException(e);
+            }
+        }
+
+        public void setsockoptMrt6AddMif(FileDescriptor fd, StructMif6ctl mif6ctl)
+                throws ErrnoException {
+            final byte[] bytes = mif6ctl.writeToBytes();
+            NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_ADD_MIF, bytes);
+        }
+
+        public void setsockoptMrt6DelMif(FileDescriptor fd, int virtualIfIndex)
+                throws ErrnoException {
+            Os.setsockoptInt(fd, IPPROTO_IPV6, MRT6_DEL_MIF, virtualIfIndex);
+        }
+
+        public void setsockoptMrt6AddMfc(FileDescriptor fd, StructMf6cctl mf6cctl)
+                throws ErrnoException {
+            final byte[] bytes = mf6cctl.writeToBytes();
+            NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_ADD_MFC, bytes);
+        }
+
+        public void setsockoptMrt6DelMfc(FileDescriptor fd, StructMf6cctl mf6cctl)
+                throws ErrnoException {
+            final byte[] bytes = mf6cctl.writeToBytes();
+            NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_DEL_MFC, bytes);
+        }
+
+        /**
+         * 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) {
+            try {
+                return NetworkInterface.getByIndex(physicalIndex);
+            } catch (SocketException e) {
+                return null;
+            }
+        }
+
+        public Clock getClock() {
+            return mClock;
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index 2ac2ad3..a979681 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -44,7 +44,9 @@
 import com.android.server.ConnectivityService;
 
 import java.io.IOException;
+import java.net.Inet4Address;
 import java.net.Inet6Address;
+import java.net.UnknownHostException;
 import java.util.Objects;
 
 /**
@@ -99,11 +101,12 @@
     private IpPrefix mNat64PrefixFromRa;
     private String mBaseIface;
     private String mIface;
-    private Inet6Address mIPv6Address;
+    @VisibleForTesting
+    Inet6Address mIPv6Address;
     private State mState = State.IDLE;
-    private ClatCoordinator mClatCoordinator;
+    private final ClatCoordinator mClatCoordinator;  // non-null iff T+
 
-    private boolean mEnableClatOnCellular;
+    private final boolean mEnableClatOnCellular;
     private boolean mPrefixDiscoveryRunning;
 
     public Nat464Xlat(NetworkAgentInfo nai, INetd netd, IDnsResolver dnsResolver,
@@ -112,7 +115,11 @@
         mNetd = netd;
         mNetwork = nai;
         mEnableClatOnCellular = deps.getCellular464XlatEnabled();
-        mClatCoordinator = deps.getClatCoordinator(mNetd);
+        if (SdkLevel.isAtLeastT()) {
+            mClatCoordinator = deps.getClatCoordinator(mNetd);
+        } else {
+            mClatCoordinator = null;
+        }
     }
 
     /**
@@ -195,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;
@@ -210,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();
@@ -235,6 +242,7 @@
         mNat64PrefixInUse = null;
         mIface = null;
         mBaseIface = null;
+        mIPv6Address = null;
 
         if (!mPrefixDiscoveryRunning) {
             setPrefix64(null);
@@ -475,8 +483,9 @@
 
     /**
      * Adds stacked link on base link and transitions to RUNNING state.
+     * Must be called on the handler thread.
      */
-    private void handleInterfaceLinkStateChanged(String iface, boolean up) {
+    public void handleInterfaceLinkStateChanged(String iface, boolean up) {
         // TODO: if we call start(), then stop(), then start() again, and the
         // interfaceLinkStateChanged notification for the first start is delayed past the first
         // stop, then the code becomes out of sync with system state and will behave incorrectly.
@@ -491,6 +500,7 @@
         // Once this code is converted to StateMachine, it will be possible to use deferMessage to
         // ensure it stays in STARTING state until the interfaceLinkStateChanged notification fires,
         // and possibly use a timeout (or provide some guarantees at the lower layer) to address #1.
+        ensureRunningOnHandlerThread();
         if (!isStarting() || !up || !Objects.equals(mIface, iface)) {
             return;
         }
@@ -511,8 +521,10 @@
 
     /**
      * Removes stacked link on base link and transitions to IDLE state.
+     * Must be called on the handler thread.
      */
-    private void handleInterfaceRemoved(String iface) {
+    public void handleInterfaceRemoved(String iface) {
+        ensureRunningOnHandlerThread();
         if (!Objects.equals(mIface, iface)) {
             return;
         }
@@ -528,12 +540,65 @@
         stop();
     }
 
-    public void interfaceLinkStateChanged(String iface, boolean up) {
-        mNetwork.handler().post(() -> { handleInterfaceLinkStateChanged(iface, up); });
+    /**
+     * Translate the input v4 address to v6 clat address.
+     */
+    @Nullable
+    public Inet6Address translateV4toV6(@NonNull Inet4Address addr) {
+        // Variables in Nat464Xlat should only be accessed from handler thread.
+        ensureRunningOnHandlerThread();
+        if (!isStarted()) return null;
+
+        return convertv4ToClatv6(mNat64PrefixInUse, addr);
     }
 
-    public void interfaceRemoved(String iface) {
-        mNetwork.handler().post(() -> handleInterfaceRemoved(iface));
+    @Nullable
+    private static Inet6Address convertv4ToClatv6(
+            @NonNull IpPrefix prefix, @NonNull Inet4Address addr) {
+        final byte[] v6Addr = new byte[16];
+        // Generate a v6 address from Nat64 prefix. Prefix should be 12 bytes long.
+        System.arraycopy(prefix.getAddress().getAddress(), 0, v6Addr, 0, 12);
+        System.arraycopy(addr.getAddress(), 0, v6Addr, 12, 4);
+
+        try {
+            return (Inet6Address) Inet6Address.getByAddress(v6Addr);
+        } catch (UnknownHostException e) {
+            Log.wtf(TAG, "getByAddress should never throw for a numeric address", e);
+            return null;
+        }
+    }
+
+    /**
+     * Get the generated v6 address of clat.
+     */
+    @Nullable
+    public Inet6Address getClatv6SrcAddress() {
+        // Variables in Nat464Xlat should only be accessed from handler thread.
+        ensureRunningOnHandlerThread();
+
+        return mIPv6Address;
+    }
+
+    /**
+     * Get the generated v4 address of clat.
+     */
+    @Nullable
+    public Inet4Address getClatv4SrcAddress() {
+        // Variables in Nat464Xlat should only be accessed from handler thread.
+        ensureRunningOnHandlerThread();
+        if (!isStarted()) return null;
+
+        final LinkAddress v4Addr = getLinkAddress(mIface);
+        if (v4Addr == null) return null;
+
+        return (Inet4Address) v4Addr.getAddress();
+    }
+
+    private void ensureRunningOnHandlerThread() {
+        if (mNetwork.handler().getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on handler thread: " + Thread.currentThread().getName());
+        }
     }
 
     /**
@@ -557,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/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 85282cb..76993a6 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -17,10 +17,13 @@
 package com.android.server.connectivity;
 
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+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_RESTRICTED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.transportNamesOf;
 
 import android.annotation.NonNull;
@@ -35,6 +38,7 @@
 import android.net.INetworkAgentRegistry;
 import android.net.INetworkMonitor;
 import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
 import android.net.NattKeepalivePacketData;
 import android.net.Network;
 import android.net.NetworkAgent;
@@ -64,10 +68,11 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.WakeupMessage;
-import com.android.modules.utils.build.SdkLevel;
 import com.android.server.ConnectivityService;
 
 import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -172,6 +177,7 @@
     // TODO: make this private with a getter.
     @NonNull public NetworkCapabilities networkCapabilities;
     @NonNull public final NetworkAgentConfig networkAgentConfig;
+    @Nullable public LocalNetworkConfig localNetworkConfig;
 
     // Underlying networks declared by the agent.
     // The networks in this list might be declared by a VPN using setUnderlyingNetworks and are
@@ -425,12 +431,28 @@
     private final boolean mHasAutomotiveFeature;
 
     /**
+     * Checks that a proposed update to the NCs of this NAI satisfies structural constraints.
+     *
+     * Some changes to NetworkCapabilities are structurally not supported by the stack, and
+     * NetworkAgents are absolutely never allowed to try and do them. When one of these is
+     * violated, this method returns false, which has ConnectivityService disconnect the network ;
+     * this is meant to guarantee that no implementor ever tries to do this.
+     */
+    public boolean respectsNcStructuralConstraints(@NonNull final NetworkCapabilities proposedNc) {
+        if (networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                != proposedNc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * Sets the capabilities sent by the agent for later retrieval.
-     *
-     * This method does not sanitize the capabilities ; instead, use
-     * {@link #getDeclaredCapabilitiesSanitized} to retrieve a sanitized
-     * copy of the capabilities as they were passed here.
-     *
+     * <p>
+     * This method does not sanitize the capabilities before storing them ; instead, use
+     * {@link #getDeclaredCapabilitiesSanitized} to retrieve a sanitized copy of the capabilities
+     * as they were passed here.
+     * <p>
      * This method makes a defensive copy to avoid issues where the passed object is later mutated.
      *
      * @param caps the caps sent by the agent
@@ -451,6 +473,8 @@
      * apply to the allowedUids field.
      * They also should not mutate immutable capabilities, although for backward-compatibility
      * this is not enforced and limited to just a log.
+     * Forbidden capabilities also make no sense for networks, so they are disallowed and
+     * will be ignored with a warning.
      *
      * @param carrierPrivilegeAuthenticator the authenticator, to check access UIDs.
      */
@@ -459,14 +483,15 @@
         final NetworkCapabilities nc = new NetworkCapabilities(mDeclaredCapabilitiesUnsanitized);
         if (nc.hasConnectivityManagedCapability()) {
             Log.wtf(TAG, "BUG: " + this + " has CS-managed capability.");
+            nc.removeAllForbiddenCapabilities();
         }
         if (networkCapabilities.getOwnerUid() != nc.getOwnerUid()) {
             Log.e(TAG, toShortString() + ": ignoring attempt to change owner from "
                     + networkCapabilities.getOwnerUid() + " to " + nc.getOwnerUid());
             nc.setOwnerUid(networkCapabilities.getOwnerUid());
         }
-        restrictCapabilitiesFromNetworkAgent(
-                nc, creatorUid, mHasAutomotiveFeature, carrierPrivilegeAuthenticator);
+        restrictCapabilitiesFromNetworkAgent(nc, creatorUid, mHasAutomotiveFeature,
+                mConnServiceDeps, carrierPrivilegeAuthenticator);
         return nc;
     }
 
@@ -596,6 +621,7 @@
     private static final String TAG = ConnectivityService.class.getSimpleName();
     private static final boolean VDBG = false;
     private final ConnectivityService mConnService;
+    private final ConnectivityService.Dependencies mConnServiceDeps;
     private final Context mContext;
     private final Handler mHandler;
     private final QosCallbackTracker mQosCallbackTracker;
@@ -604,6 +630,7 @@
 
     public NetworkAgentInfo(INetworkAgent na, Network net, NetworkInfo info,
             @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+            @Nullable LocalNetworkConfig localNetworkConfig,
             @NonNull NetworkScore score, Context context,
             Handler handler, NetworkAgentConfig config, ConnectivityService connService, INetd netd,
             IDnsResolver dnsResolver, int factorySerialNumber, int creatorUid,
@@ -621,8 +648,10 @@
         networkInfo = info;
         linkProperties = lp;
         networkCapabilities = nc;
+        this.localNetworkConfig = localNetworkConfig;
         networkAgentConfig = config;
         mConnService = connService;
+        mConnServiceDeps = deps;
         setScore(score); // uses members connService, networkCapabilities and networkAgentConfig
         clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
         mContext = context;
@@ -899,6 +928,12 @@
         }
 
         @Override
+        public void sendLocalNetworkConfig(@NonNull final LocalNetworkConfig config) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_LOCAL_NETWORK_CONFIG_CHANGED,
+                    new Pair<>(NetworkAgentInfo.this, config)).sendToTarget();
+        }
+
+        @Override
         public void sendScore(@NonNull final NetworkScore score) {
             mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_SCORE_CHANGED,
                     new Pair<>(NetworkAgentInfo.this, score)).sendToTarget();
@@ -1033,6 +1068,30 @@
     }
 
     /**
+     * Get the generated v6 address of clat.
+     */
+    @Nullable
+    public Inet6Address getClatv6SrcAddress() {
+        return clatd.getClatv6SrcAddress();
+    }
+
+    /**
+     * Get the generated v4 address of clat.
+     */
+    @Nullable
+    public Inet4Address getClatv4SrcAddress() {
+        return clatd.getClatv4SrcAddress();
+    }
+
+    /**
+     * Translate the input v4 address to v6 clat address.
+     */
+    @Nullable
+    public Inet6Address translateV4toClatV6(@NonNull Inet4Address addr) {
+        return clatd.translateV4toV6(addr);
+    }
+
+    /**
      * Get the NetworkMonitorManager in this NetworkAgentInfo.
      *
      * <p>This will be null before {@link #onNetworkMonitorCreated(INetworkMonitor)} is called.
@@ -1079,6 +1138,11 @@
      *         already present.
      */
     public boolean addRequest(NetworkRequest networkRequest) {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on ConnectivityService thread: "
+                            + Thread.currentThread().getName());
+        }
         NetworkRequest existing = mNetworkRequests.get(networkRequest.requestId);
         if (existing == networkRequest) return false;
         if (existing != null) {
@@ -1097,6 +1161,11 @@
      * Remove the specified request from this network.
      */
     public void removeRequest(int requestId) {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on ConnectivityService thread: "
+                            + Thread.currentThread().getName());
+        }
         NetworkRequest existing = mNetworkRequests.get(requestId);
         if (existing == null) return;
         updateRequestCounts(REMOVE, existing);
@@ -1118,6 +1187,11 @@
      * network.
      */
     public NetworkRequest requestAt(int index) {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on ConnectivityService thread: "
+                            + Thread.currentThread().getName());
+        }
         return mNetworkRequests.valueAt(index);
     }
 
@@ -1148,6 +1222,11 @@
      * Returns the number of requests of any type currently satisfied by this network.
      */
     public int numNetworkRequests() {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on ConnectivityService thread: "
+                            + Thread.currentThread().getName());
+        }
         return mNetworkRequests.size();
     }
 
@@ -1181,6 +1260,11 @@
         return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
     }
 
+    /** Whether this network is a local network */
+    public boolean isLocalNetwork() {
+        return networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK);
+    }
+
     /**
      * Whether this network should propagate the capabilities from its underlying networks.
      * Currently only true for VPNs.
@@ -1469,42 +1553,52 @@
      */
     public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
             final int creatorUid, final boolean hasAutomotiveFeature,
+            @NonNull final ConnectivityService.Dependencies deps,
             @Nullable final CarrierPrivilegeAuthenticator authenticator) {
         if (nc.hasTransport(TRANSPORT_TEST)) {
             nc.restrictCapabilitiesForTestNetwork(creatorUid);
         }
-        if (!areAllowedUidsAcceptableFromNetworkAgent(nc, hasAutomotiveFeature, authenticator)) {
+        if (!areAllowedUidsAcceptableFromNetworkAgent(
+                nc, hasAutomotiveFeature, deps, authenticator)) {
             nc.setAllowedUids(new ArraySet<>());
         }
     }
 
     private static boolean areAllowedUidsAcceptableFromNetworkAgent(
             @NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
+            @NonNull final ConnectivityService.Dependencies deps,
             @Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
         // NCs without access UIDs are fine.
         if (!nc.hasAllowedUids()) return true;
         // S and below must never accept access UIDs, even if an agent sends them, because netd
         // didn't support the required feature in S.
-        if (!SdkLevel.isAtLeastT()) return false;
+        if (!deps.isAtLeastT()) return false;
 
         // On a non-restricted network, access UIDs make no sense
         if (nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) return false;
 
-        // If this network has TRANSPORT_TEST, then the caller can do whatever they want to
-        // access UIDs
-        if (nc.hasTransport(TRANSPORT_TEST)) return true;
+        // If this network has TRANSPORT_TEST and nothing else, then the caller can do whatever
+        // they want to access UIDs
+        if (nc.hasSingleTransport(TRANSPORT_TEST)) return true;
 
-        // Factories that make ethernet networks can allow UIDs for automotive devices.
-        if (nc.hasSingleTransport(TRANSPORT_ETHERNET) && hasAutomotiveFeature) {
-            return true;
+        if (nc.hasTransport(TRANSPORT_ETHERNET)) {
+            // Factories that make ethernet networks can allow UIDs for automotive devices.
+            if (hasAutomotiveFeature) return true;
+            // It's also admissible if the ethernet network has TRANSPORT_TEST, as long as it
+            // doesn't have NET_CAPABILITY_INTERNET so it can't become the default network.
+            if (nc.hasTransport(TRANSPORT_TEST) && !nc.hasCapability(NET_CAPABILITY_INTERNET)) {
+                return true;
+            }
+            return false;
         }
 
-        // Factories that make cell networks can allow the UID for the carrier service package.
+        // Factories that make cell/wifi networks can allow the UID for the carrier service package.
         // This can only work in T where there is support for CarrierPrivilegeAuthenticator
         if (null != carrierPrivilegeAuthenticator
-                && nc.hasSingleTransport(TRANSPORT_CELLULAR)
+                && (nc.hasSingleTransportBesidesTest(TRANSPORT_CELLULAR)
+                        || nc.hasSingleTransportBesidesTest(TRANSPORT_WIFI))
                 && (1 == nc.getAllowedUidsNoCopy().size())
-                && (carrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+                && (carrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                         nc.getAllowedUidsNoCopy().valueAt(0), nc))) {
             return true;
         }
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
index 15d0925..3db37e5 100644
--- a/service/src/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -18,13 +18,18 @@
 
 import static android.system.OsConstants.*;
 
+import static com.android.net.module.util.NetworkStackConstants.DNS_OVER_TLS_PORT;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
 import static com.android.net.module.util.NetworkStackConstants.ICMP_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_MTU;
+import static com.android.net.module.util.NetworkStackConstants.IP_MTU;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TargetApi;
 import android.net.InetAddresses;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -33,6 +38,7 @@
 import android.net.TrafficStats;
 import android.net.shared.PrivateDnsConfig;
 import android.net.util.NetworkConstants;
+import android.os.Build;
 import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -211,13 +217,10 @@
             mLinkProperties.addDnsServer(TEST_DNS6);
         }
 
-        final int mtu = mLinkProperties.getMtu();
         for (RouteInfo route : mLinkProperties.getRoutes()) {
             if (route.getType() == RouteInfo.RTN_UNICAST && route.hasGateway()) {
-                InetAddress gateway = route.getGateway();
-                // Use mtu in the route if exists. Otherwise, use the one in the link property.
-                final int routeMtu = route.getMtu();
-                prepareIcmpMeasurements(gateway, (routeMtu > 0) ? routeMtu : mtu);
+                final InetAddress gateway = route.getGateway();
+                prepareIcmpMeasurements(gateway);
                 if (route.isIPv6Default()) {
                     prepareExplicitSourceIcmpMeasurements(gateway);
                 }
@@ -225,7 +228,7 @@
         }
 
         for (InetAddress nameserver : mLinkProperties.getDnsServers()) {
-            prepareIcmpMeasurements(nameserver, mtu);
+            prepareIcmpMeasurements(nameserver);
             prepareDnsMeasurement(nameserver);
 
             // Unlike the DnsResolver which doesn't do certificate validation in opportunistic mode,
@@ -282,24 +285,29 @@
             // calculation.
             if (addr instanceof Inet6Address) {
                 return IPV6_HEADER_LEN + ICMP_HEADER_LEN;
+            } else {
+                return IPV4_HEADER_MIN_LEN + ICMP_HEADER_LEN;
             }
         } catch (UnknownHostException e) {
-            Log.e(TAG, "Create InetAddress fail(" + target + "): " + e);
+            throw new AssertionError("Create InetAddress fail(" + target + ")", e);
         }
-
-        return IPV4_HEADER_MIN_LEN + ICMP_HEADER_LEN;
     }
 
-    private void prepareIcmpMeasurements(@NonNull InetAddress target, int targetNetworkMtu) {
+    private void prepareIcmpMeasurements(@NonNull InetAddress target) {
+        int mtu = getMtuForTarget(target);
+        // If getMtuForTarget fails, it doesn't matter what mtu is used because connect can't
+        // succeed anyway
+        if (mtu <= 0) mtu = mLinkProperties.getMtu();
+        if (mtu <= 0) mtu = ETHER_MTU;
         // Test with different size payload ICMP.
         // 1. Test with 0 payload.
         addPayloadIcmpMeasurement(target, 0);
         final int header = getHeaderLen(target);
         // 2. Test with full size MTU.
-        addPayloadIcmpMeasurement(target, targetNetworkMtu - header);
+        addPayloadIcmpMeasurement(target, mtu - header);
         // 3. If v6, make another measurement with the full v6 min MTU, unless that's what
         //    was done above.
-        if ((target instanceof Inet6Address) && (targetNetworkMtu != IPV6_MIN_MTU)) {
+        if ((target instanceof Inet6Address) && (mtu != IPV6_MIN_MTU)) {
             addPayloadIcmpMeasurement(target, IPV6_MIN_MTU - header);
         }
     }
@@ -318,6 +326,38 @@
         }
     }
 
+    /**
+     * Open a socket to the target address and return the mtu from that socket
+     *
+     * If the MTU can't be obtained for some reason (e.g. the target is unreachable) this will
+     * return -1.
+     *
+     * @param target the destination address
+     * @return the mtu to that destination, or -1
+     */
+    // getsockoptInt is S+, but this service code and only installs on S, so it's safe to ignore
+    // the lint warnings by using @TargetApi.
+    @TargetApi(Build.VERSION_CODES.S)
+    private int getMtuForTarget(InetAddress target) {
+        final int family = target instanceof Inet4Address ? AF_INET : AF_INET6;
+        FileDescriptor socket = null;
+        try {
+            socket = Os.socket(family, SOCK_DGRAM, 0);
+            mNetwork.bindSocket(socket);
+            Os.connect(socket, target, 0);
+            if (family == AF_INET) {
+                return Os.getsockoptInt(socket, IPPROTO_IP, IP_MTU);
+            } else {
+                return Os.getsockoptInt(socket, IPPROTO_IPV6, IPV6_MTU);
+            }
+        } catch (ErrnoException | IOException e) {
+            Log.e(TAG, "Can't get MTU for destination " + target, e);
+            return -1;
+        } finally {
+            IoUtils.closeQuietly(socket);
+        }
+    }
+
     private void prepareExplicitSourceIcmpMeasurements(InetAddress target) {
         for (LinkAddress l : mLinkProperties.getLinkAddresses()) {
             InetAddress source = l.getAddress();
@@ -730,7 +770,6 @@
     private class DnsTlsCheck extends DnsUdpCheck {
         private static final int TCP_CONNECT_TIMEOUT_MS = 2500;
         private static final int TCP_TIMEOUT_MS = 2000;
-        private static final int DNS_TLS_PORT = 853;
         private static final int DNS_HEADER_SIZE = 12;
 
         private final String mHostname;
@@ -769,7 +808,8 @@
             final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits);
 
             mMeasurement.startTime = now();
-            sslSocket.connect(new InetSocketAddress(mTarget, DNS_TLS_PORT), TCP_CONNECT_TIMEOUT_MS);
+            sslSocket.connect(new InetSocketAddress(mTarget, DNS_OVER_TLS_PORT),
+                    TCP_CONNECT_TIMEOUT_MS);
 
             // Synchronous call waiting for the TLS handshake complete.
             sslSocket.startHandshake();
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
index 8b0cb7c..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;
@@ -243,7 +245,7 @@
                     details = r.getString(R.string.network_available_sign_in_detailed, name);
                     break;
                 case TRANSPORT_CELLULAR:
-                    title = r.getString(R.string.network_available_sign_in, 0);
+                    title = r.getString(R.string.mobile_network_available_no_internet);
                     // TODO: Change this to pull from NetworkInfo once a printable
                     // name has been added to it
                     NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier();
@@ -252,8 +254,16 @@
                         subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
                     }
 
-                    details = mTelephonyManager.createForSubscriptionId(subId)
+                    final String operatorName = mTelephonyManager.createForSubscriptionId(subId)
                             .getNetworkOperatorName();
+                    if (TextUtils.isEmpty(operatorName)) {
+                        details = r.getString(R.string
+                                .mobile_network_available_no_internet_detailed_unknown_carrier);
+                    } else {
+                        details = r.getString(
+                                R.string.mobile_network_available_no_internet_detailed,
+                                operatorName);
+                    }
                     break;
                 default:
                     title = r.getString(R.string.network_available_sign_in, 0);
@@ -322,7 +332,8 @@
 
     private boolean maybeNotifyViaDialog(Resources res, NotificationType notifyType,
             PendingIntent intent) {
-        if (notifyType != NotificationType.NO_INTERNET
+        if (notifyType != NotificationType.LOST_INTERNET
+                && notifyType != NotificationType.NO_INTERNET
                 && notifyType != NotificationType.PARTIAL_CONNECTIVITY) {
             return false;
         }
@@ -432,7 +443,8 @@
      * A notification with a higher number will take priority over a notification with a lower
      * number.
      */
-    private static int priority(NotificationType t) {
+    @VisibleForTesting
+    public static int priority(NotificationType t) {
         if (t == null) {
             return 0;
         }
diff --git a/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
new file mode 100644
index 0000000..ab3d315
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
@@ -0,0 +1,108 @@
+/*
+ * 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;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.SystemClock;
+
+import com.android.net.module.util.BitUtils;
+
+
+class NetworkRequestStateInfo {
+    private final NetworkRequest mNetworkRequest;
+    private final long mNetworkRequestReceivedTime;
+
+    private enum NetworkRequestState {
+        RECEIVED,
+        REMOVED
+    }
+    private NetworkRequestState mNetworkRequestState;
+    private int mNetworkRequestDurationMillis;
+    private final Dependencies mDependencies;
+
+    NetworkRequestStateInfo(NetworkRequest networkRequest,
+            Dependencies deps) {
+        mDependencies = deps;
+        mNetworkRequest = networkRequest;
+        mNetworkRequestReceivedTime = mDependencies.getElapsedRealtime();
+        mNetworkRequestDurationMillis = 0;
+        mNetworkRequestState = NetworkRequestState.RECEIVED;
+    }
+
+    NetworkRequestStateInfo(NetworkRequestStateInfo anotherNetworkRequestStateInfo) {
+        mDependencies = anotherNetworkRequestStateInfo.mDependencies;
+        mNetworkRequest = new NetworkRequest(anotherNetworkRequestStateInfo.mNetworkRequest);
+        mNetworkRequestReceivedTime = anotherNetworkRequestStateInfo.mNetworkRequestReceivedTime;
+        mNetworkRequestDurationMillis =
+                anotherNetworkRequestStateInfo.mNetworkRequestDurationMillis;
+        mNetworkRequestState = anotherNetworkRequestStateInfo.mNetworkRequestState;
+    }
+
+    public void setNetworkRequestRemoved() {
+        mNetworkRequestState = NetworkRequestState.REMOVED;
+        mNetworkRequestDurationMillis = (int) (
+                mDependencies.getElapsedRealtime() - mNetworkRequestReceivedTime);
+    }
+
+    public int getNetworkRequestStateStatsType() {
+        if (mNetworkRequestState == NetworkRequestState.RECEIVED) {
+            return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+        } else if (mNetworkRequestState == NetworkRequestState.REMOVED) {
+            return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+        } else {
+            return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+        }
+    }
+
+    public int getRequestId() {
+        return mNetworkRequest.requestId;
+    }
+
+    public int getPackageUid() {
+        return mNetworkRequest.networkCapabilities.getRequestorUid();
+    }
+
+    public int getTransportTypes() {
+        return (int) BitUtils.packBits(mNetworkRequest.networkCapabilities.getTransportTypes());
+    }
+
+    public boolean getNetCapabilityNotMetered() {
+        return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+    }
+
+    public boolean getNetCapabilityInternet() {
+        return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+    }
+
+    public int getNetworkRequestDurationMillis() {
+        return mNetworkRequestDurationMillis;
+    }
+
+    /** Dependency class */
+    public static class Dependencies {
+        // Returns a timestamp with the time base of SystemClock.elapsedRealtime to keep durations
+        // relative to start time and avoid timezone change, including time spent in deep sleep.
+        public long getElapsedRealtime() {
+            return SystemClock.elapsedRealtime();
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
new file mode 100644
index 0000000..1bc654a
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
@@ -0,0 +1,228 @@
+/*
+ * 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;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED;
+
+import android.annotation.NonNull;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+import java.util.ArrayDeque;
+
+/**
+ * A Connectivity Service helper class to push atoms capturing network requests have been received
+ * and removed and its metadata.
+ *
+ * Atom events are logged in the ConnectivityStatsLog. Network request id: network request metadata
+ * hashmap is stored to calculate network request duration when it is removed.
+ *
+ * Note that this class is not thread-safe. The instance of the class needs to be
+ * synchronized in the callers when being used in multiple threads.
+ */
+public class NetworkRequestStateStatsMetrics {
+
+    private static final String TAG = "NetworkRequestStateStatsMetrics";
+    private static final int CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC = 0;
+    private static final int CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC = 1;
+
+    @VisibleForTesting
+    static final int MAX_QUEUED_REQUESTS = 20;
+
+    // Stats logging frequency is limited to 10 ms at least, 500ms are taken as a safely margin
+    // for cases of longer periods of frequent network requests.
+    private static final int ATOM_INTERVAL_MS = 500;
+    private final StatsLoggingHandler mStatsLoggingHandler;
+
+    private final Dependencies mDependencies;
+
+    private final NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+    private final SparseArray<NetworkRequestStateInfo> mNetworkRequestsActive;
+
+    public NetworkRequestStateStatsMetrics() {
+        this(new Dependencies(), new NetworkRequestStateInfo.Dependencies());
+    }
+
+    @VisibleForTesting
+    NetworkRequestStateStatsMetrics(Dependencies deps,
+            NetworkRequestStateInfo.Dependencies nrStateInfoDeps) {
+        mNetworkRequestsActive = new SparseArray<>();
+        mDependencies = deps;
+        mNRStateInfoDeps = nrStateInfoDeps;
+        HandlerThread handlerThread = mDependencies.makeHandlerThread(TAG);
+        handlerThread.start();
+        mStatsLoggingHandler = new StatsLoggingHandler(handlerThread.getLooper());
+    }
+
+    /**
+     * Register network request receive event, push RECEIVE atom
+     *
+     * @param networkRequest network request received
+     */
+    public void onNetworkRequestReceived(NetworkRequest networkRequest) {
+        if (mNetworkRequestsActive.contains(networkRequest.requestId)) {
+            Log.w(TAG, "Received already registered network request, id = "
+                    + networkRequest.requestId);
+        } else {
+            Log.d(TAG, "Registered nr with ID = " + networkRequest.requestId
+                    + ", package_uid = " + networkRequest.networkCapabilities.getRequestorUid());
+            NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+                    networkRequest, mNRStateInfoDeps);
+            mNetworkRequestsActive.put(networkRequest.requestId, networkRequestStateInfo);
+            mStatsLoggingHandler.sendMessage(Message.obtain(
+                    mStatsLoggingHandler,
+                    CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
+                    networkRequestStateInfo));
+        }
+    }
+
+    /**
+     * Register network request remove event, push REMOVE atom
+     *
+     * @param networkRequest network request removed
+     */
+    public void onNetworkRequestRemoved(NetworkRequest networkRequest) {
+        NetworkRequestStateInfo networkRequestStateInfo = mNetworkRequestsActive.get(
+                networkRequest.requestId);
+        if (networkRequestStateInfo == null) {
+            Log.w(TAG, "This NR hasn't been registered. NR id = " + networkRequest.requestId);
+        } else {
+            Log.d(TAG, "Removed nr with ID = " + networkRequest.requestId);
+            mNetworkRequestsActive.remove(networkRequest.requestId);
+            networkRequestStateInfo = new NetworkRequestStateInfo(networkRequestStateInfo);
+            networkRequestStateInfo.setNetworkRequestRemoved();
+            mStatsLoggingHandler.sendMessage(Message.obtain(
+                    mStatsLoggingHandler,
+                    CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
+                    networkRequestStateInfo));
+        }
+    }
+
+    /** Dependency class */
+    public static class Dependencies {
+        /**
+         * Creates a thread with provided tag.
+         *
+         * @param tag for the thread.
+         */
+        public HandlerThread makeHandlerThread(@NonNull final String tag) {
+            return new HandlerThread(tag);
+        }
+
+        /**
+         * @see Handler#sendMessageDelayed(Message, long)
+         */
+        public void sendMessageDelayed(@NonNull Handler handler, int what, long delayMillis) {
+            handler.sendMessageDelayed(Message.obtain(handler, what), delayMillis);
+        }
+
+        /**
+         * Gets number of millis since event.
+         *
+         * @param eventTimeMillis long timestamp in millis when the event occurred.
+         */
+        public long getMillisSinceEvent(long eventTimeMillis) {
+            return SystemClock.elapsedRealtime() - eventTimeMillis;
+        }
+
+        /**
+         * Writes a NETWORK_REQUEST_STATE_CHANGED event to ConnectivityStatsLog.
+         *
+         * @param networkRequestStateInfo NetworkRequestStateInfo containing network request info.
+         */
+        public void writeStats(NetworkRequestStateInfo networkRequestStateInfo) {
+            ConnectivityStatsLog.write(
+                    NETWORK_REQUEST_STATE_CHANGED,
+                    networkRequestStateInfo.getPackageUid(),
+                    networkRequestStateInfo.getTransportTypes(),
+                    networkRequestStateInfo.getNetCapabilityNotMetered(),
+                    networkRequestStateInfo.getNetCapabilityInternet(),
+                    networkRequestStateInfo.getNetworkRequestStateStatsType(),
+                    networkRequestStateInfo.getNetworkRequestDurationMillis());
+        }
+    }
+
+    private class StatsLoggingHandler extends Handler {
+        private static final String TAG = "NetworkRequestsStateStatsLoggingHandler";
+
+        private final ArrayDeque<NetworkRequestStateInfo> mPendingState = new ArrayDeque<>();
+
+        private long mLastLogTime = 0;
+
+        StatsLoggingHandler(Looper looper) {
+            super(looper);
+        }
+
+        private void maybeEnqueueStatsMessage(NetworkRequestStateInfo networkRequestStateInfo) {
+            if (mPendingState.size() < MAX_QUEUED_REQUESTS) {
+                mPendingState.add(networkRequestStateInfo);
+            } else {
+                Log.w(TAG, "Too many network requests received within last " + ATOM_INTERVAL_MS
+                        + " ms, dropping the last network request (id = "
+                        + networkRequestStateInfo.getRequestId() + ") event");
+                return;
+            }
+            if (hasMessages(CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC)) {
+                return;
+            }
+            long millisSinceLastLog = mDependencies.getMillisSinceEvent(mLastLogTime);
+
+            if (millisSinceLastLog >= ATOM_INTERVAL_MS) {
+                sendMessage(
+                        Message.obtain(this, CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC));
+            } else {
+                mDependencies.sendMessageDelayed(
+                        this,
+                        CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC,
+                        ATOM_INTERVAL_MS - millisSinceLastLog);
+            }
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            NetworkRequestStateInfo loggingInfo;
+            switch (msg.what) {
+                case CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC:
+                    maybeEnqueueStatsMessage((NetworkRequestStateInfo) msg.obj);
+                    break;
+                case CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC:
+                    mLastLogTime = SystemClock.elapsedRealtime();
+                    if (!mPendingState.isEmpty()) {
+                        loggingInfo = mPendingState.remove();
+                        mDependencies.writeStats(loggingInfo);
+                        if (!mPendingState.isEmpty()) {
+                            mDependencies.sendMessageDelayed(
+                                    this,
+                                    CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC,
+                                    ATOM_INTERVAL_MS);
+                        }
+                    }
+                    break;
+                default: // fall out
+            }
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index c15f042..beaa174 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -1011,9 +1011,8 @@
      * @param ranges The updated UID ranges under VPN Lockdown. This function does not treat the VPN
      *               app's UID in any special way. The caller is responsible for excluding the VPN
      *               app UID from the passed-in ranges.
-     *               Ranges can have duplications and/or contain the range that is already subject
-     *               to lockdown. However, ranges can not have overlaps with other ranges including
-     *               ranges that are currently subject to lockdown.
+     *               Ranges can have duplications, overlaps, and/or contain the range that is
+     *               already subject to lockdown.
      */
     public synchronized void updateVpnLockdownUidRanges(boolean add, UidRange[] ranges) {
         final Set<UidRange> affectedUidRanges = new HashSet<>();
@@ -1045,8 +1044,10 @@
         // exclude privileged apps from the prohibit routing rules used to implement outgoing packet
         // filtering, privileged apps can still bypass outgoing packet filtering because the
         // prohibit rules observe the protected from VPN bit.
+        // If removing a UID, we ensure it is not present anywhere in the set first.
         for (final int uid: affectedUids) {
-            if (!hasRestrictedNetworksPermission(uid)) {
+            if (!hasRestrictedNetworksPermission(uid)
+                    && (add || !UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid))) {
                 updateLockdownUidRule(uid, add);
             }
         }
diff --git a/service/src/com/android/server/connectivity/ProxyTracker.java b/service/src/com/android/server/connectivity/ProxyTracker.java
index bda4b8f..4415007 100644
--- a/service/src/com/android/server/connectivity/ProxyTracker.java
+++ b/service/src/com/android/server/connectivity/ProxyTracker.java
@@ -86,6 +86,7 @@
 
     private final Handler mConnectivityServiceHandler;
 
+    @Nullable
     private final PacProxyManager mPacProxyManager;
 
     private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener {
@@ -109,9 +110,11 @@
         mConnectivityServiceHandler = connectivityServiceInternalHandler;
         mPacProxyManager = context.getSystemService(PacProxyManager.class);
 
-        PacProxyInstalledListener listener = new PacProxyInstalledListener(pacChangedEvent);
-        mPacProxyManager.addPacProxyInstalledListener(
+        if (mPacProxyManager != null) {
+            PacProxyInstalledListener listener = new PacProxyInstalledListener(pacChangedEvent);
+            mPacProxyManager.addPacProxyInstalledListener(
                 mConnectivityServiceHandler::post, listener);
+        }
     }
 
     // Convert empty ProxyInfo's to null as null-checks are used to determine if proxies are present
@@ -205,7 +208,7 @@
                 mGlobalProxy = proxyProperties;
             }
 
-            if (!TextUtils.isEmpty(pacFileUrl)) {
+            if (!TextUtils.isEmpty(pacFileUrl) && mPacProxyManager != null) {
                 mConnectivityServiceHandler.post(
                         () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));
             }
@@ -251,7 +254,10 @@
         final ProxyInfo defaultProxy = getDefaultProxy();
         final ProxyInfo proxyInfo = null != defaultProxy ?
                 defaultProxy : ProxyInfo.buildDirectProxy("", 0, Collections.emptyList());
-        mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);
+
+        if (mPacProxyManager != null) {
+            mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);
+        }
 
         if (!shouldSendBroadcast(proxyInfo)) {
             return;
@@ -398,7 +404,7 @@
                 // network, so discount this case.
                 if (null == mGlobalProxy && !lp.getHttpProxy().getPacFileUrl()
                         .equals(defaultProxy.getPacFileUrl())) {
-                    throw new IllegalStateException("Unexpected discrepancy between proxy in LP of "
+                    Log.wtf(TAG, "Unexpected discrepancy between proxy in LP of "
                             + "default network and default proxy. The former has a PAC URL of "
                             + lp.getHttpProxy().getPacFileUrl() + " while the latter has "
                             + defaultProxy.getPacFileUrl());
diff --git a/service/src/com/android/server/connectivity/SatelliteAccessController.java b/service/src/com/android/server/connectivity/SatelliteAccessController.java
new file mode 100644
index 0000000..2cdc932
--- /dev/null
+++ b/service/src/com/android/server/connectivity/SatelliteAccessController.java
@@ -0,0 +1,259 @@
+/*
+ * 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;
+
+import android.Manifest;
+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;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Tracks the uid of all the default messaging application which are role_sms role and
+ * satellite_communication permission complaint and requests ConnectivityService to create multi
+ * layer request with satellite internet access support for the default message application.
+ * @hide
+ */
+public class SatelliteAccessController {
+    private static final String TAG = SatelliteAccessController.class.getSimpleName();
+    private final Context mContext;
+    private final Dependencies mDeps;
+    private final DefaultMessageRoleListener mDefaultMessageRoleListener;
+    private final Consumer<Set<Integer>> mCallback;
+    private final Handler mConnectivityServiceHandler;
+
+    // At this sparseArray, Key is userId and values are uids of SMS apps that are allowed
+    // to use satellite network as fallback.
+    private final SparseArray<Set<Integer>> mAllUsersSatelliteNetworkFallbackUidCache =
+            new SparseArray<>();
+
+    /**
+     *  Monitor {@link android.app.role.OnRoleHoldersChangedListener#onRoleHoldersChanged(String,
+     *  UserHandle)},
+     *
+     */
+    private final class DefaultMessageRoleListener
+            implements OnRoleHoldersChangedListener {
+        @Override
+        public void onRoleHoldersChanged(String role, UserHandle userHandle) {
+            if (RoleManager.ROLE_SMS.equals(role)) {
+                Log.i(TAG, "ROLE_SMS Change detected ");
+                onRoleSmsChanged(userHandle);
+            }
+        }
+
+        public void register() {
+            try {
+                mDeps.addOnRoleHoldersChangedListenerAsUser(
+                        mConnectivityServiceHandler::post, this, UserHandle.ALL);
+            } catch (RuntimeException e) {
+                Log.wtf(TAG, "Could not register satellite controller listener due to " + e);
+            }
+        }
+    }
+
+    public SatelliteAccessController(@NonNull final Context c,
+            Consumer<Set<Integer>> callback,
+            @NonNull final Handler connectivityServiceInternalHandler) {
+        this(c, new Dependencies(c), callback, connectivityServiceInternalHandler);
+    }
+
+    public static class Dependencies {
+        private final RoleManager mRoleManager;
+
+        private Dependencies(Context context) {
+            mRoleManager = context.getSystemService(RoleManager.class);
+        }
+
+        /** See {@link RoleManager#getRoleHoldersAsUser(String, UserHandle)} */
+        public List<String> getRoleHoldersAsUser(String roleName, UserHandle userHandle) {
+            return mRoleManager.getRoleHoldersAsUser(roleName, userHandle);
+        }
+
+        /** See {@link RoleManager#addOnRoleHoldersChangedListenerAsUser} */
+        public void addOnRoleHoldersChangedListenerAsUser(@NonNull Executor executor,
+                @NonNull OnRoleHoldersChangedListener listener, UserHandle user) {
+            mRoleManager.addOnRoleHoldersChangedListenerAsUser(executor, listener, user);
+        }
+    }
+
+    @VisibleForTesting
+    SatelliteAccessController(@NonNull final Context c, @NonNull final Dependencies deps,
+            Consumer<Set<Integer>> callback,
+            @NonNull final Handler connectivityServiceInternalHandler) {
+        mContext = c;
+        mDeps = deps;
+        mDefaultMessageRoleListener = new DefaultMessageRoleListener();
+        mCallback = callback;
+        mConnectivityServiceHandler = connectivityServiceInternalHandler;
+    }
+
+    private Set<Integer> updateSatelliteNetworkFallbackUidListCache(List<String> packageNames,
+            @NonNull UserHandle userHandle) {
+        Set<Integer> fallbackUids = new ArraySet<>();
+        PackageManager pm =
+                mContext.createContextAsUser(userHandle, 0).getPackageManager();
+        if (pm != null) {
+            for (String packageName : packageNames) {
+                // Check if SATELLITE_COMMUNICATION permission is enabled for default sms
+                // application package before adding it part of satellite network fallback uid
+                // cache list.
+                if (isSatellitePermissionEnabled(pm, packageName)) {
+                    int uid = getUidForPackage(pm, packageName);
+                    if (uid != Process.INVALID_UID) {
+                        fallbackUids.add(uid);
+                    }
+                }
+            }
+        } else {
+            Log.wtf(TAG, "package manager found null");
+        }
+        return fallbackUids;
+    }
+
+    //Check if satellite communication is enabled for the package
+    private boolean isSatellitePermissionEnabled(PackageManager packageManager,
+            String packageName) {
+        return packageManager.checkPermission(
+                Manifest.permission.SATELLITE_COMMUNICATION, packageName)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    private int getUidForPackage(PackageManager packageManager, String pkgName) {
+        if (pkgName == null) {
+            return Process.INVALID_UID;
+        }
+        try {
+            ApplicationInfo applicationInfo = packageManager.getApplicationInfo(pkgName, 0);
+            return applicationInfo.uid;
+        } catch (PackageManager.NameNotFoundException exception) {
+            Log.e(TAG, "Unable to find uid for package: " + pkgName);
+        }
+        return Process.INVALID_UID;
+    }
+
+    // on Role sms change triggered by OnRoleHoldersChangedListener()
+    private void onRoleSmsChanged(@NonNull UserHandle userHandle) {
+        int userId = userHandle.getIdentifier();
+        if (userId == Process.INVALID_UID) {
+            Log.wtf(TAG, "Invalid User Id");
+            return;
+        }
+
+        //Returns empty list if no package exists
+        final List<String> packageNames =
+                mDeps.getRoleHoldersAsUser(RoleManager.ROLE_SMS, userHandle);
+
+        // Store previous satellite fallback uid available
+        final Set<Integer> prevUidsForUser =
+                mAllUsersSatelliteNetworkFallbackUidCache.get(userId, new ArraySet<>());
+
+        Log.i(TAG, "currentUser : role_sms_packages: " + userId + " : " + packageNames);
+        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
+        // satellite network fallback uid cache list of multiple users as applicable
+        if (newUidsForUser.equals(prevUidsForUser)) {
+            return;
+        }
+
+        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++) {
+            mergedSatelliteNetworkFallbackUidCache.addAll(
+                    mAllUsersSatelliteNetworkFallbackUidCache.valueAt(i));
+        }
+        Log.i(TAG, "merged uid list for multi layer request : "
+                + mergedSatelliteNetworkFallbackUidCache);
+
+        // trigger multiple layer request for satellite network fallback of multi user uids
+        mCallback.accept(mergedSatelliteNetworkFallbackUidCache);
+    }
+
+    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() {
+        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/service/src/com/android/server/connectivity/TcpKeepaliveController.java b/service/src/com/android/server/connectivity/TcpKeepaliveController.java
index 0fd8604..4124e36 100644
--- a/service/src/com/android/server/connectivity/TcpKeepaliveController.java
+++ b/service/src/com/android/server/connectivity/TcpKeepaliveController.java
@@ -34,6 +34,7 @@
 import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
 
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
 import android.net.ISocketKeepaliveCallback;
 import android.net.InvalidPacketException;
 import android.net.NetworkUtils;
@@ -106,6 +107,8 @@
     private static final int TCP_REPAIR_ON = 1;
     // Reference include/uapi/linux/sockios.h
     private static final int SIOCINQ = FIONREAD;
+    // arch specific BSD socket API constant that predates Linux and Android
+    @SuppressLint("NewApi")
     private static final int SIOCOUTQ = TIOCOUTQ;
 
     /**
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
new file mode 100644
index 0000000..14bd5df
--- /dev/null
+++ b/staticlibs/Android.bp
@@ -0,0 +1,665 @@
+// 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.
+
+// 1. The "net-utils-framework-common" library is also compiled into the framework and placed on the
+//    boot classpath. It uses jarjar rules so that anything outside the framework can use this
+//    library directly.
+// 2. The "net-utils-services-common" library is for use by modules and frameworks/base/services.
+//    It does not need to be jarjared because it is not placed on the bootclasspath.
+// 3. The "net-utils-telephony-common-srcs" filegroup is for use specifically by telephony, which
+//    places many of its classes, even non-API service classes, on the boot classpath. Any file that
+//    is added to this filegroup *must* have a corresponding jarjar rule in the telephony jarjar
+//    rules file. Otherwise, it will end up on the boot classpath and other modules will not be able
+//    to provide their own copy.
+
+// Note: all filegroups here must have the right path attribute because otherwise, if they are
+// included in the bootclasspath, they could incorrectly be included in the SDK documentation even
+// though they are not in the current.txt files.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "net-utils-device-common",
+    srcs: [
+        "device/com/android/net/module/util/arp/ArpPacket.java",
+        "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/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",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    target_sdk_version: "30",
+    apex_available: [
+        "//apex_available:anyapex",
+        "//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: [
+        "androidx.annotation_annotation",
+        "framework-annotations-lib",
+        "framework-configinfrastructure",
+        "framework-connectivity.stubs.module_lib",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+java_defaults {
+    name: "lib_mockito_extended",
+    static_libs: [
+        "mockito-target-extended-minus-junit4",
+    ],
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+}
+
+java_library {
+    name: "net-utils-dnspacket-common",
+    srcs: [
+        "framework/**/DnsPacket.java",
+        "framework/**/DnsPacketUtils.java",
+        "framework/**/DnsSvcbPacket.java",
+        "framework/**/DnsSvcbRecord.java",
+        "framework/**/HexDump.java",
+        "framework/**/NetworkStackConstants.java",
+    ],
+    sdk_version: "module_current",
+    visibility: [
+        "//packages/services/Iwlan:__subpackages__",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-annotations-lib",
+        "framework-connectivity.stubs.module_lib",
+    ],
+}
+
+filegroup {
+    name: "net-utils-framework-common-srcs",
+    srcs: ["framework/**/*.java"],
+    path: "framework",
+    visibility: [
+        "//frameworks/base",
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+}
+
+// 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: [
+        "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/TcUtils.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+        "//packages/modules/NetworkStack:__subpackages__",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-connectivity.stubs.module_lib",
+        "net-utils-device-common-struct-base",
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+java_library {
+    name: "net-utils-device-common-struct-base",
+    srcs: [
+        "device/com/android/net/module/util/Struct.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+        "//packages/modules/NetworkStack:__subpackages__",
+    ],
+    static_libs: [
+        "net-utils-framework-common",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-annotations-lib", // Required by InetAddressUtils.java
+        "framework-connectivity.stubs.module_lib",
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+// The net-utils-device-common-struct library requires the callers to contain
+// net-utils-device-common-struct-base.
+java_library {
+    name: "net-utils-device-common-struct",
+    srcs: [
+        "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: [
+        "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,
+        error_checks: ["NewApi"],
+    },
+}
+
+// The net-utils-device-common-netlink library requires the callers to contain
+// net-utils-device-common-struct and net-utils-device-common-struct-base.
+java_library {
+    name: "net-utils-device-common-netlink",
+    srcs: [
+        "device/com/android/net/module/util/netlink/**/*.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+        "//packages/modules/NetworkStack:__subpackages__",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-connectivity.stubs.module_lib",
+        // 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",
+        "//apex_available:platform",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+// The net-utils-device-common-ip library requires the callers to contain
+// 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",
+    srcs: [
+        "device/com/android/net/module/util/ip/*.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+        "//packages/modules/NetworkStack:__subpackages__",
+    ],
+    libs: [
+        "framework-annotations-lib",
+        "framework-connectivity",
+    ],
+    static_libs: [
+        "net-utils-device-common",
+        "net-utils-device-common-netlink",
+        "net-utils-framework-common",
+        "netd-client",
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+java_library {
+    name: "net-utils-framework-common",
+    srcs: [
+        ":net-utils-framework-common-srcs",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-annotations-lib",
+        "framework-connectivity.stubs.module_lib",
+        "framework-connectivity-t.stubs.module_lib",
+        "framework-location.stubs.module_lib",
+    ],
+    jarjar_rules: "jarjar-rules-shared.txt",
+    visibility: [
+        "//cts/tests/tests/net",
+        "//cts/tests/tests/wifi",
+        "//packages/modules/Connectivity/tests/cts/net",
+        "//packages/modules/Connectivity/Tethering",
+        "//frameworks/base/tests:__subpackages__",
+        "//frameworks/opt/net/ike",
+        "//frameworks/opt/telephony",
+        "//frameworks/base/wifi:__subpackages__",
+        "//packages/modules/Connectivity:__subpackages__",
+        "//packages/modules/NetworkStack:__subpackages__",
+        "//packages/modules/CaptivePortalLogin",
+        "//packages/modules/Wifi/framework/tests:__subpackages__",
+        "//packages/apps/Settings",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+    errorprone: {
+        enabled: true,
+        // Error-prone checking only warns of problems when building. To make the build fail with
+        // these errors, list the specific error-prone problems below.
+        javacflags: [
+            "-Xep:NullablePrimitive:ERROR",
+        ],
+    },
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
+}
+
+java_library {
+    name: "net-utils-services-common",
+    srcs: [
+        "device/android/net/NetworkFactory.java",
+        "device/android/net/NetworkFactoryImpl.java",
+        "device/android/net/NetworkFactoryLegacyImpl.java",
+        "device/android/net/NetworkFactoryShim.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    libs: [
+        "framework-annotations-lib",
+        "framework-connectivity",
+        "modules-utils-build_system",
+    ],
+    // TODO: remove "apex_available:platform".
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.btservices",
+        "com.android.tethering",
+        "com.android.wifi",
+    ],
+    visibility: [
+        // TODO: remove after NetworkStatsService moves to the module.
+        "//frameworks/base/services/net",
+        "//packages/modules/Connectivity/service",
+        "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Bluetooth/android/app",
+        "//packages/modules/Wifi/service:__subpackages__",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+java_library {
+    name: "net-utils-device-common-async",
+    srcs: [
+        "device/com/android/net/module/util/async/*.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+    libs: [
+        "framework-annotations-lib",
+    ],
+    static_libs: [
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+java_library {
+    name: "net-utils-device-common-wear",
+    srcs: [
+        "device/com/android/net/module/util/wear/*.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+    libs: [
+        "framework-annotations-lib",
+    ],
+    static_libs: [
+        "net-utils-device-common-async",
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+// Limited set of utilities for use by service-connectivity-mdns-standalone-build-test, to make sure
+// the mDNS code can build with only system APIs.
+// The mDNS code is platform code so it should use framework-annotations-lib, contrary to apps that
+// should use sdk_version: "system_current" and only androidx.annotation_annotation. But this build
+// rule verifies that the mDNS code can be built into apps, if code transformations are applied to
+// the annotations.
+// When using "system_current", framework annotations are not available; they would appear as
+// package-private as they are marked as such in the system_current stubs. So build against
+// core_platform and add the stubs manually in "libs". See http://b/147773144#comment7.
+java_library {
+    name: "net-utils-device-common-mdns-standalone-build-test",
+    // Build against core_platform and add the stub libraries manually in "libs", as annotations
+    // are already included in android_system_stubs_current but package-private, so
+    // "framework-annotations-lib" needs to be manually included before
+    // "android_system_stubs_current" (b/272392042)
+    sdk_version: "core_platform",
+    srcs: [
+        "device/com/android/net/module/util/FdEventsReader.java",
+        "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/HexDump.java",
+        "framework/com/android/net/module/util/LinkPropertiesUtils.java",
+    ],
+    libs: [
+        "framework-annotations-lib",
+        "android_system_stubs_current",
+        "androidx.annotation_annotation",
+    ],
+    visibility: ["//packages/modules/Connectivity/service-t"],
+}
+
+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:__subpackages__",
+        "//packages/modules/NetworkStack:__subpackages__",
+    ],
+    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",
+    jarjar_rules: "jarjar-rules-shared.txt",
+    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",
+    ],
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+    defaults_visibility: [
+        "//visibility:private",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+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"],
+}
+
+java_library {
+    name: "net-utils-tethering",
+    srcs: [
+        ":net-utils-all-srcs",
+        ":framework-connectivity-shared-srcs",
+    ],
+    defaults: ["net-utils-non-bootclasspath-defaults"],
+}
+
+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",
+            ],
+        },
+        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 {
+    name: "net-utils-telephony-common-srcs",
+    srcs: [
+        // Any class here *must* have a corresponding jarjar rule in the telephony build rules.
+        "device/android/net/NetworkFactory.java",
+        "device/android/net/NetworkFactoryImpl.java",
+        "device/android/net/NetworkFactoryLegacyImpl.java",
+        "device/android/net/NetworkFactoryShim.java",
+    ],
+    path: "device",
+    visibility: [
+        "//frameworks/opt/telephony",
+    ],
+}
+
+// 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-framework-wifi-common-srcs",
+    srcs: [
+        "framework/com/android/net/module/util/DnsSdTxtRecord.java",
+        "framework/com/android/net/module/util/Inet4AddressUtils.java",
+        "framework/com/android/net/module/util/InetAddressUtils.java",
+        "framework/com/android/net/module/util/MacAddressUtils.java",
+        "framework/com/android/net/module/util/NetUtils.java",
+    ],
+    path: "framework",
+    visibility: [
+        "//frameworks/base",
+    ],
+}
+
+// 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",
+    ],
+    visibility: [
+        "//frameworks/opt/net/wifi/service",
+        "//packages/modules/Wifi/service",
+    ],
+}
+
+// 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"],
+}
diff --git a/staticlibs/TEST_MAPPING b/staticlibs/TEST_MAPPING
new file mode 100644
index 0000000..b1cb6b5
--- /dev/null
+++ b/staticlibs/TEST_MAPPING
@@ -0,0 +1,28 @@
+{
+  "presubmit": [
+    {
+      "name": "netdutils_test"
+    }
+  ],
+  "imports": [
+    {
+      "path": "frameworks/base/core/java/android/net"
+    },
+    // Below tests already run the library tests as part of their coverage tests
+    {
+      "path": "packages/modules/NetworkStack"
+    },
+    {
+      "path": "packages/modules/CaptivePortalLogin"
+    },
+    {
+      "path": "frameworks/base/packages/Tethering"
+    },
+    {
+      "path": "packages/modules/Wifi/framework"
+    },
+    {
+      "path": "vendor/xts/gts-tests/hostsidetests/networkstack"
+    }
+  ]
+}
diff --git a/staticlibs/client-libs/Android.bp b/staticlibs/client-libs/Android.bp
new file mode 100644
index 0000000..f665584
--- /dev/null
+++ b/staticlibs/client-libs/Android.bp
@@ -0,0 +1,27 @@
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "netd-client",
+    srcs: ["netd/**/*"],
+    sdk_version: "system_current",
+    min_sdk_version: "30",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+        "com.android.wifi",
+    ],
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+        "//frameworks/base/services:__subpackages__",
+        "//frameworks/base/packages:__subpackages__",
+        "//packages/modules/Wifi/service:__subpackages__",
+    ],
+    libs: ["androidx.annotation_annotation"],
+    static_libs: [
+        "netd_aidl_interface-lateststable-java",
+        "netd_event_listener_interface-lateststable-java",
+    ],
+}
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java b/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java
new file mode 100644
index 0000000..4180732
--- /dev/null
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java
@@ -0,0 +1,61 @@
+/*
+ * 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.net.module.util;
+
+import android.net.metrics.INetdEventListener;
+
+/**
+ * Base {@link INetdEventListener} that provides no-op implementations which can
+ * be overridden.
+ */
+public class BaseNetdEventListener extends INetdEventListener.Stub {
+
+    @Override
+    public void onDnsEvent(int netId, int eventType, int returnCode,
+            int latencyMs, String hostname, String[] ipAddresses,
+            int ipAddressesCount, int uid) { }
+
+    @Override
+    public void onPrivateDnsValidationEvent(int netId, String ipAddress,
+            String hostname, boolean validated) { }
+
+    @Override
+    public void onConnectEvent(int netId, int error, int latencyMs,
+            String ipAddr, int port, int uid) { }
+
+    @Override
+    public void onWakeupEvent(String prefix, int uid, int ethertype,
+            int ipNextHeader, byte[] dstHw, String srcIp, String dstIp,
+            int srcPort, int dstPort, long timestampNs) { }
+
+    @Override
+    public void onTcpSocketStatsEvent(int[] networkIds, int[] sentPackets,
+            int[] lostPackets, int[] rttUs, int[] sentAckDiffMs) { }
+
+    @Override
+    public void onNat64PrefixEvent(int netId, boolean added,
+            String prefixString, int prefixLength) { }
+
+    @Override
+    public int getInterfaceVersion() {
+        return INetdEventListener.VERSION;
+    }
+
+    @Override
+    public String getInterfaceHash() {
+        return INetdEventListener.HASH;
+    }
+}
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdUnsolicitedEventListener.java b/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdUnsolicitedEventListener.java
new file mode 100644
index 0000000..526dd8b
--- /dev/null
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdUnsolicitedEventListener.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.net.module.util;
+
+import android.net.INetdUnsolicitedEventListener;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Base {@link INetdUnsolicitedEventListener} that provides no-op implementations which can be
+ * overridden.
+ */
+public class BaseNetdUnsolicitedEventListener extends INetdUnsolicitedEventListener.Stub {
+
+    @Override
+    public void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs,
+            int uid) { }
+
+    @Override
+    public void onQuotaLimitReached(@NonNull String alertName, @NonNull String ifName) { }
+
+    @Override
+    public void onInterfaceDnsServerInfo(@NonNull String ifName, long lifetimeS,
+            @NonNull String[] servers) { }
+
+    @Override
+    public void onInterfaceAddressUpdated(@NonNull String addr, String ifName, int flags,
+            int scope) { }
+
+    @Override
+    public void onInterfaceAddressRemoved(@NonNull String addr, @NonNull String ifName, int flags,
+            int scope) { }
+
+    @Override
+    public void onInterfaceAdded(@NonNull String ifName) { }
+
+    @Override
+    public void onInterfaceRemoved(@NonNull String ifName) { }
+
+    @Override
+    public void onInterfaceChanged(@NonNull String ifName, boolean up) { }
+
+    @Override
+    public void onInterfaceLinkStateChanged(@NonNull String ifName, boolean up) { }
+
+    @Override
+    public void onRouteChanged(boolean updated, @NonNull String route, @NonNull String gateway,
+            @NonNull String ifName) { }
+
+    @Override
+    public void onStrictCleartextDetected(int uid, @NonNull String hex) { }
+
+    @Override
+    public int getInterfaceVersion() {
+        return INetdUnsolicitedEventListener.VERSION;
+    }
+
+    @Override
+    public String getInterfaceHash() {
+        return INetdUnsolicitedEventListener.HASH;
+    }
+}
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
new file mode 100644
index 0000000..d99eedc
--- /dev/null
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -0,0 +1,316 @@
+/*
+ * 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.net.module.util;
+
+import static android.net.INetd.IF_STATE_DOWN;
+import static android.net.INetd.IF_STATE_UP;
+import static android.net.RouteInfo.RTN_THROW;
+import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.system.OsConstants.EBUSY;
+
+import android.annotation.SuppressLint;
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.net.RouteInfo;
+import android.net.RouteInfoParcel;
+import android.net.TetherConfigParcel;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Collection of utilities for netd.
+ */
+public class NetdUtils {
+    private static final String TAG = NetdUtils.class.getSimpleName();
+
+    /** Used to modify the specified route. */
+    public enum ModifyOperation {
+        ADD,
+        REMOVE,
+    }
+
+    /**
+     * Get InterfaceConfigurationParcel from netd.
+     */
+    public static InterfaceConfigurationParcel getInterfaceConfigParcel(@NonNull INetd netd,
+            @NonNull String iface) {
+        try {
+            return netd.interfaceGetCfg(iface);
+        } catch (RemoteException | ServiceSpecificException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private static void validateFlag(String flag) {
+        if (flag.indexOf(' ') >= 0) {
+            throw new IllegalArgumentException("flag contains space: " + flag);
+        }
+    }
+
+    /**
+     * Check whether the InterfaceConfigurationParcel contains the target flag or not.
+     *
+     * @param config The InterfaceConfigurationParcel instance.
+     * @param flag Target flag string to be checked.
+     */
+    public static boolean hasFlag(@NonNull final InterfaceConfigurationParcel config,
+            @NonNull final String flag) {
+        validateFlag(flag);
+        final Set<String> flagList = new HashSet<String>(Arrays.asList(config.flags));
+        return flagList.contains(flag);
+    }
+
+    @VisibleForTesting
+    protected static String[] removeAndAddFlags(@NonNull String[] flags, @NonNull String remove,
+            @NonNull String add) {
+        final ArrayList<String> result = new ArrayList<>();
+        try {
+            // Validate the add flag first, so that the for-loop can be ignore once the format of
+            // add flag is invalid.
+            validateFlag(add);
+            for (String flag : flags) {
+                // Simply ignore both of remove and add flags first, then add the add flag after
+                // exiting the loop to prevent adding the duplicate flag.
+                if (remove.equals(flag) || add.equals(flag)) continue;
+                result.add(flag);
+            }
+            result.add(add);
+            return result.toArray(new String[result.size()]);
+        } catch (IllegalArgumentException iae) {
+            throw new IllegalStateException("Invalid InterfaceConfigurationParcel", iae);
+        }
+    }
+
+    /**
+     * Set interface configuration to netd by passing InterfaceConfigurationParcel.
+     */
+    public static void setInterfaceConfig(INetd netd, InterfaceConfigurationParcel configParcel) {
+        try {
+            netd.interfaceSetCfg(configParcel);
+        } catch (RemoteException | ServiceSpecificException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Set the given interface up.
+     */
+    public static void setInterfaceUp(INetd netd, String iface) {
+        final InterfaceConfigurationParcel configParcel = getInterfaceConfigParcel(netd, iface);
+        configParcel.flags = removeAndAddFlags(configParcel.flags, IF_STATE_DOWN /* remove */,
+                IF_STATE_UP /* add */);
+        setInterfaceConfig(netd, configParcel);
+    }
+
+    /**
+     * Set the given interface down.
+     */
+    public static void setInterfaceDown(INetd netd, String iface) {
+        final InterfaceConfigurationParcel configParcel = getInterfaceConfigParcel(netd, iface);
+        configParcel.flags = removeAndAddFlags(configParcel.flags, IF_STATE_UP /* remove */,
+                IF_STATE_DOWN /* add */);
+        setInterfaceConfig(netd, configParcel);
+    }
+
+    /** Start tethering. */
+    public static void tetherStart(final INetd netd, final boolean usingLegacyDnsProxy,
+            final String[] dhcpRange) throws RemoteException, ServiceSpecificException {
+        final TetherConfigParcel config = new TetherConfigParcel();
+        config.usingLegacyDnsProxy = usingLegacyDnsProxy;
+        config.dhcpRanges = dhcpRange;
+        netd.tetherStartWithConfiguration(config);
+    }
+
+    /** Setup interface for tethering. */
+    public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest)
+            throws RemoteException, ServiceSpecificException {
+        tetherInterface(netd, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
+    }
+
+    /** Setup interface with configurable retries for tethering. */
+    public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest,
+            int maxAttempts, int pollingIntervalMs)
+            throws RemoteException, ServiceSpecificException {
+        netd.tetherInterfaceAdd(iface);
+        networkAddInterface(netd, iface, maxAttempts, pollingIntervalMs);
+        // Activate a route to dest and IPv6 link local.
+        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+                new RouteInfo(dest, null, iface, RTN_UNICAST));
+        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+                new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
+    }
+
+    /**
+     * Retry Netd#networkAddInterface for EBUSY error code.
+     * If the same interface (e.g., wlan0) is in client mode and then switches to tethered mode.
+     * There can be a race where puts the interface into the local network but interface is still
+     * in use in netd because the ConnectivityService thread hasn't processed the disconnect yet.
+     * See b/158269544 for detail.
+     */
+    private static void networkAddInterface(final INetd netd, final String iface,
+            int maxAttempts, int pollingIntervalMs)
+            throws ServiceSpecificException, RemoteException {
+        for (int i = 1; i <= maxAttempts; i++) {
+            try {
+                netd.networkAddInterface(INetd.LOCAL_NET_ID, iface);
+                return;
+            } catch (ServiceSpecificException e) {
+                if (e.errorCode == EBUSY && i < maxAttempts) {
+                    SystemClock.sleep(pollingIntervalMs);
+                    continue;
+                }
+
+                Log.e(TAG, "Retry Netd#networkAddInterface failure: " + e);
+                throw e;
+            }
+        }
+    }
+
+    /** Reset interface for tethering. */
+    public static void untetherInterface(final INetd netd, String iface)
+            throws RemoteException, ServiceSpecificException {
+        try {
+            netd.tetherInterfaceRemove(iface);
+        } finally {
+            netd.networkRemoveInterface(INetd.LOCAL_NET_ID, iface);
+        }
+    }
+
+    /** Add |routes| to local network. */
+    public static void addRoutesToLocalNetwork(final INetd netd, final String iface,
+            final List<RouteInfo> routes) {
+
+        for (RouteInfo route : routes) {
+            if (!route.isDefaultRoute()) {
+                modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID, route);
+            }
+        }
+
+        // IPv6 link local should be activated always.
+        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+                new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
+    }
+
+    /** Remove routes from local network. */
+    public static int removeRoutesFromLocalNetwork(final INetd netd, final List<RouteInfo> routes) {
+        int failures = 0;
+
+        for (RouteInfo route : routes) {
+            try {
+                modifyRoute(netd, ModifyOperation.REMOVE, INetd.LOCAL_NET_ID, route);
+            } catch (IllegalStateException e) {
+                failures++;
+            }
+        }
+
+        return failures;
+    }
+
+    @SuppressLint("NewApi")
+    private static String findNextHop(final RouteInfo route) {
+        final String nextHop;
+        switch (route.getType()) {
+            case RTN_UNICAST:
+                if (route.hasGateway()) {
+                    nextHop = route.getGateway().getHostAddress();
+                } else {
+                    nextHop = INetd.NEXTHOP_NONE;
+                }
+                break;
+            case RTN_UNREACHABLE:
+                nextHop = INetd.NEXTHOP_UNREACHABLE;
+                break;
+            case RTN_THROW:
+                nextHop = INetd.NEXTHOP_THROW;
+                break;
+            default:
+                nextHop = INetd.NEXTHOP_NONE;
+                break;
+        }
+        return nextHop;
+    }
+
+    /** Add or remove |route|. */
+    public static void modifyRoute(final INetd netd, final ModifyOperation op, final int netId,
+            final RouteInfo route) {
+        final String ifName = route.getInterface();
+        final String dst = route.getDestination().toString();
+        final String nextHop = findNextHop(route);
+
+        try {
+            switch(op) {
+                case ADD:
+                    netd.networkAddRoute(netId, ifName, dst, nextHop);
+                    break;
+                case REMOVE:
+                    netd.networkRemoveRoute(netId, ifName, dst, nextHop);
+                    break;
+                default:
+                    throw new IllegalStateException("Unsupported modify operation:" + op);
+            }
+        } catch (RemoteException | ServiceSpecificException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Convert a RouteInfo into a RouteInfoParcel.
+     */
+    public static RouteInfoParcel toRouteInfoParcel(RouteInfo route) {
+        final String nextHop;
+
+        switch (route.getType()) {
+            case RouteInfo.RTN_UNICAST:
+                if (route.hasGateway()) {
+                    nextHop = route.getGateway().getHostAddress();
+                } else {
+                    nextHop = INetd.NEXTHOP_NONE;
+                }
+                break;
+            case RouteInfo.RTN_UNREACHABLE:
+                nextHop = INetd.NEXTHOP_UNREACHABLE;
+                break;
+            case RouteInfo.RTN_THROW:
+                nextHop = INetd.NEXTHOP_THROW;
+                break;
+            default:
+                nextHop = INetd.NEXTHOP_NONE;
+                break;
+        }
+
+        final RouteInfoParcel rip = new RouteInfoParcel();
+        rip.ifName = route.getInterface();
+        rip.destination = route.getDestination().toString();
+        rip.nextHop = nextHop;
+        rip.mtu = route.getMtu();
+
+        return rip;
+    }
+}
diff --git a/staticlibs/client-libs/tests/unit/Android.bp b/staticlibs/client-libs/tests/unit/Android.bp
new file mode 100644
index 0000000..7aafd69
--- /dev/null
+++ b/staticlibs/client-libs/tests/unit/Android.bp
@@ -0,0 +1,45 @@
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "NetdStaticLibTestsLib",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    min_sdk_version: "30",
+    static_libs: [
+        "androidx.test.rules",
+        "mockito-target-extended-minus-junit4",
+        "net-tests-utils-host-device-common",
+        "netd-client",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    visibility: [
+        // Visible for Tethering and NetworkStack integration test and link NetdStaticLibTestsLib
+        // there, so that the tests under client-libs can also be run when running tethering and
+        // NetworkStack MTS.
+        "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
+        "//packages/modules/NetworkStack/tests/integration",
+    ],
+}
+
+android_test {
+    name: "NetdStaticLibTests",
+    certificate: "platform",
+    static_libs: [
+        "NetdStaticLibTestsLib",
+    ],
+    jni_libs: [
+        // For mockito extended
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    test_suites: ["device-tests"],
+}
diff --git a/staticlibs/client-libs/tests/unit/AndroidManifest.xml b/staticlibs/client-libs/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..7a07d3d
--- /dev/null
+++ b/staticlibs/client-libs/tests/unit/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.frameworks.clientlibs.tests">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.clientlibs.tests"
+        android:label="Netd Static Library Tests" />
+</manifest>
diff --git a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
new file mode 100644
index 0000000..5069672
--- /dev/null
+++ b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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.net.module.util;
+
+import static android.net.INetd.LOCAL_NET_ID;
+import static android.system.OsConstants.EBUSY;
+
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+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.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+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.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetdUtilsTest {
+    @Mock private INetd mNetd;
+
+    private static final String IFACE = "TEST_IFACE";
+    private static final IpPrefix TEST_IPPREFIX = new IpPrefix("192.168.42.1/24");
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private void setupFlagsForInterfaceConfiguration(String[] flags) throws Exception {
+        final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel();
+        config.flags = flags;
+        when(mNetd.interfaceGetCfg(eq(IFACE))).thenReturn(config);
+    }
+
+    private void verifyMethodsAndArgumentsOfSetInterface(boolean ifaceUp) throws Exception {
+        final String[] flagsContainDownAndUp = new String[] {"flagA", "down", "flagB", "up"};
+        final String[] flagsForInterfaceDown = new String[] {"flagA", "down", "flagB"};
+        final String[] flagsForInterfaceUp = new String[] {"flagA", "up", "flagB"};
+        final String[] expectedFinalFlags;
+        setupFlagsForInterfaceConfiguration(flagsContainDownAndUp);
+        if (ifaceUp) {
+            // "down" flag will be removed from flagsContainDownAndUp when interface is up. Set
+            // expectedFinalFlags to flagsForInterfaceUp.
+            expectedFinalFlags = flagsForInterfaceUp;
+            NetdUtils.setInterfaceUp(mNetd, IFACE);
+        } else {
+            // "up" flag will be removed from flagsContainDownAndUp when interface is down. Set
+            // expectedFinalFlags to flagsForInterfaceDown.
+            expectedFinalFlags = flagsForInterfaceDown;
+            NetdUtils.setInterfaceDown(mNetd, IFACE);
+        }
+        verify(mNetd).interfaceSetCfg(
+                argThat(config ->
+                        // Check if actual flags are the same as expected flags.
+                        // TODO: Have a function in MiscAsserts to check if two arrays are the same.
+                        CollectionUtils.all(Arrays.asList(expectedFinalFlags),
+                                flag -> Arrays.asList(config.flags).contains(flag))
+                        && CollectionUtils.all(Arrays.asList(config.flags),
+                                flag -> Arrays.asList(expectedFinalFlags).contains(flag))));
+    }
+
+    @Test
+    public void testSetInterfaceUp() throws Exception {
+        verifyMethodsAndArgumentsOfSetInterface(true /* ifaceUp */);
+    }
+
+    @Test
+    public void testSetInterfaceDown() throws Exception {
+        verifyMethodsAndArgumentsOfSetInterface(false /* ifaceUp */);
+    }
+
+    @Test
+    public void testRemoveAndAddFlags() throws Exception {
+        final String[] flags = new String[] {"flagA", "down", "flagB"};
+        // Add an invalid flag and expect to get an IllegalStateException.
+        assertThrows(IllegalStateException.class,
+                () -> NetdUtils.removeAndAddFlags(flags, "down" /* remove */, "u p" /* add */));
+    }
+
+    private void setNetworkAddInterfaceOutcome(final Exception cause, int numLoops)
+            throws Exception {
+        // This cannot be an int because local variables referenced from a lambda expression must
+        // be final or effectively final.
+        final Counter myCounter = new Counter();
+        doAnswer((invocation) -> {
+            myCounter.count();
+            if (myCounter.isCounterReached(numLoops)) {
+                if (cause == null) return null;
+
+                throw cause;
+            }
+
+            throw new ServiceSpecificException(EBUSY);
+        }).when(mNetd).networkAddInterface(LOCAL_NET_ID, IFACE);
+    }
+
+    class Counter {
+        private int mValue = 0;
+
+        private void count() {
+            mValue++;
+        }
+
+        private boolean isCounterReached(int target) {
+            return mValue >= target;
+        }
+    }
+
+    @Test
+    public void testTetherInterfaceSuccessful() throws Exception {
+        // Expect #networkAddInterface successful at first tries.
+        verifyTetherInterfaceSucceeds(1);
+
+        // Expect #networkAddInterface successful after 10 tries.
+        verifyTetherInterfaceSucceeds(10);
+    }
+
+    private void runTetherInterfaceWithServiceSpecificException(int expectedTries,
+            int expectedCode) throws Exception {
+        setNetworkAddInterfaceOutcome(new ServiceSpecificException(expectedCode), expectedTries);
+
+        try {
+            NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+            fail("Expect throw ServiceSpecificException");
+        } catch (ServiceSpecificException e) {
+            assertEquals(e.errorCode, expectedCode);
+        }
+
+        verifyNetworkAddInterfaceFails(expectedTries);
+        reset(mNetd);
+    }
+
+    private void runTetherInterfaceWithRemoteException(int expectedTries) throws Exception {
+        setNetworkAddInterfaceOutcome(new RemoteException(), expectedTries);
+
+        try {
+            NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+            fail("Expect throw RemoteException");
+        } catch (RemoteException e) { }
+
+        verifyNetworkAddInterfaceFails(expectedTries);
+        reset(mNetd);
+    }
+
+    private void verifyNetworkAddInterfaceFails(int expectedTries) throws Exception {
+        verify(mNetd).tetherInterfaceAdd(IFACE);
+        verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
+        verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+        verifyNoMoreInteractions(mNetd);
+    }
+
+    private void verifyTetherInterfaceSucceeds(int expectedTries) throws Exception {
+        setNetworkAddInterfaceOutcome(null, expectedTries);
+
+        NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX);
+        verify(mNetd).tetherInterfaceAdd(IFACE);
+        verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
+        verify(mNetd, times(2)).networkAddRoute(eq(LOCAL_NET_ID), eq(IFACE), any(), any());
+        verifyNoMoreInteractions(mNetd);
+        reset(mNetd);
+    }
+
+    @Test
+    public void testTetherInterfaceFailOnNetworkAddInterface() throws Exception {
+        // Test throwing ServiceSpecificException with EBUSY failure.
+        runTetherInterfaceWithServiceSpecificException(20, EBUSY);
+
+        // Test throwing ServiceSpecificException with unexpectedError.
+        final int unexpectedError = 999;
+        runTetherInterfaceWithServiceSpecificException(1, unexpectedError);
+
+        // Test throwing ServiceSpecificException with unexpectedError after 7 tries.
+        runTetherInterfaceWithServiceSpecificException(7, unexpectedError);
+
+        // Test throwing RemoteException.
+        runTetherInterfaceWithRemoteException(1);
+
+        // Test throwing RemoteException after 3 tries.
+        runTetherInterfaceWithRemoteException(3);
+    }
+
+    @Test
+    public void testNetdUtilsHasFlag() throws Exception {
+        final String[] flags = new String[] {"up", "broadcast", "running", "multicast"};
+        setupFlagsForInterfaceConfiguration(flags);
+
+        // Set interface up.
+        NetdUtils.setInterfaceUp(mNetd, IFACE);
+        final ArgumentCaptor<InterfaceConfigurationParcel> arg =
+                ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
+        verify(mNetd, times(1)).interfaceSetCfg(arg.capture());
+
+        final InterfaceConfigurationParcel p = arg.getValue();
+        assertTrue(NetdUtils.hasFlag(p, "up"));
+        assertTrue(NetdUtils.hasFlag(p, "running"));
+        assertTrue(NetdUtils.hasFlag(p, "broadcast"));
+        assertTrue(NetdUtils.hasFlag(p, "multicast"));
+        assertFalse(NetdUtils.hasFlag(p, "down"));
+    }
+
+    @Test
+    public void testNetdUtilsHasFlag_flagContainsSpace() throws Exception {
+        final String[] flags = new String[] {"up", "broadcast", "running", "multicast"};
+        setupFlagsForInterfaceConfiguration(flags);
+
+        // Set interface up.
+        NetdUtils.setInterfaceUp(mNetd, IFACE);
+        final ArgumentCaptor<InterfaceConfigurationParcel> arg =
+                ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
+        verify(mNetd, times(1)).interfaceSetCfg(arg.capture());
+
+        final InterfaceConfigurationParcel p = arg.getValue();
+        assertThrows(IllegalArgumentException.class, () -> NetdUtils.hasFlag(p, "up "));
+    }
+
+    @Test
+    public void testNetdUtilsHasFlag_UppercaseString() throws Exception {
+        final String[] flags = new String[] {"up", "broadcast", "running", "multicast"};
+        setupFlagsForInterfaceConfiguration(flags);
+
+        // Set interface up.
+        NetdUtils.setInterfaceUp(mNetd, IFACE);
+        final ArgumentCaptor<InterfaceConfigurationParcel> arg =
+                ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
+        verify(mNetd, times(1)).interfaceSetCfg(arg.capture());
+
+        final InterfaceConfigurationParcel p = arg.getValue();
+        assertFalse(NetdUtils.hasFlag(p, "UP"));
+        assertFalse(NetdUtils.hasFlag(p, "BROADCAST"));
+        assertFalse(NetdUtils.hasFlag(p, "RUNNING"));
+        assertFalse(NetdUtils.hasFlag(p, "MULTICAST"));
+    }
+}
diff --git a/staticlibs/device/android/net/NetworkFactory.java b/staticlibs/device/android/net/NetworkFactory.java
new file mode 100644
index 0000000..87f6dee
--- /dev/null
+++ b/staticlibs/device/android/net/NetworkFactory.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * A NetworkFactory is an entity that creates NetworkAgent objects.
+ * The bearers register with ConnectivityService using {@link #register} and
+ * their factory will start receiving scored NetworkRequests.  NetworkRequests
+ * can be filtered 3 ways: by NetworkCapabilities, by score and more complexly by
+ * overridden function.  All of these can be dynamic - changing NetworkCapabilities
+ * or score forces re-evaluation of all current requests.
+ *
+ * If any requests pass the filter some overrideable functions will be called.
+ * If the bearer only cares about very simple start/stopNetwork callbacks, those
+ * functions can be overridden.  If the bearer needs more interaction, it can
+ * override addNetworkRequest and removeNetworkRequest which will give it each
+ * request that passes their current filters.
+ *
+ * This class is mostly a shim which delegates to one of two implementations depending
+ * on the SDK level of the device it's running on.
+ *
+ * @hide
+ **/
+public class NetworkFactory {
+    static final boolean DBG = true;
+    static final boolean VDBG = false;
+
+    final NetworkFactoryShim mImpl;
+
+    private final String LOG_TAG;
+
+    // Ideally the filter argument would be non-null, but null has historically meant to see
+    // no requests and telephony passes null.
+    public NetworkFactory(Looper looper, Context context, String logTag,
+            @Nullable final NetworkCapabilities filter) {
+        LOG_TAG = logTag;
+        if (isAtLeastS()) {
+            mImpl = new NetworkFactoryImpl(this, looper, context, filter);
+        } else {
+            mImpl = new NetworkFactoryLegacyImpl(this, looper, context, filter);
+        }
+    }
+
+    // TODO : these two constants and the method are only used by telephony tests. Replace it in
+    // the tests and remove them and the associated code.
+    public static final int CMD_REQUEST_NETWORK = 1;
+    public static final int CMD_CANCEL_REQUEST = 2;
+    /** Like Handler#obtainMessage */
+    @VisibleForTesting
+    public Message obtainMessage(final int what, final int arg1, final int arg2,
+            final @Nullable Object obj) {
+        return mImpl.obtainMessage(what, arg1, arg2, obj);
+    }
+
+    // Called by BluetoothNetworkFactory
+    public final Looper getLooper() {
+        return mImpl.getLooper();
+    }
+
+    // Refcount for simple mode requests
+    private int mRefCount = 0;
+
+    /* Registers this NetworkFactory with the system. May only be called once per factory. */
+    public void register() {
+        mImpl.register(LOG_TAG);
+    }
+
+    /**
+     * Registers this NetworkFactory with the system ignoring the score filter. This will let
+     * the factory always see all network requests matching its capabilities filter.
+     * May only be called once per factory.
+     */
+    public void registerIgnoringScore() {
+        mImpl.registerIgnoringScore(LOG_TAG);
+    }
+
+    /** Unregisters this NetworkFactory. After this call, the object can no longer be used. */
+    public void terminate() {
+        mImpl.terminate();
+    }
+
+    protected final void reevaluateAllRequests() {
+        mImpl.reevaluateAllRequests();
+    }
+
+    /**
+     * Overridable function to provide complex filtering.
+     * Called for every request every time a new NetworkRequest is seen
+     * and whenever the filterScore or filterNetworkCapabilities change.
+     *
+     * acceptRequest can be overridden to provide complex filter behavior
+     * for the incoming requests
+     *
+     * For output, this class will call {@link #needNetworkFor} and
+     * {@link #releaseNetworkFor} for every request that passes the filters.
+     * If you don't need to see every request, you can leave the base
+     * implementations of those two functions and instead override
+     * {@link #startNetwork} and {@link #stopNetwork}.
+     *
+     * If you want to see every score fluctuation on every request, set
+     * your score filter to a very high number and watch {@link #needNetworkFor}.
+     *
+     * @return {@code true} to accept the request.
+     */
+    public boolean acceptRequest(@NonNull final NetworkRequest request) {
+        return true;
+    }
+
+    /**
+     * Can be called by a factory to release a request as unfulfillable: the request will be
+     * removed, and the caller will get a
+     * {@link ConnectivityManager.NetworkCallback#onUnavailable()} callback after this function
+     * returns.
+     *
+     * Note: this should only be called by factory which KNOWS that it is the ONLY factory which
+     * is able to fulfill this request!
+     */
+    protected void releaseRequestAsUnfulfillableByAnyFactory(NetworkRequest r) {
+        mImpl.releaseRequestAsUnfulfillableByAnyFactory(r);
+    }
+
+    // override to do simple mode (request independent)
+    protected void startNetwork() { }
+    protected void stopNetwork() { }
+
+    // override to do fancier stuff
+    protected void needNetworkFor(@NonNull final NetworkRequest networkRequest) {
+        if (++mRefCount == 1) startNetwork();
+    }
+
+    protected void releaseNetworkFor(@NonNull final NetworkRequest networkRequest) {
+        if (--mRefCount == 0) stopNetwork();
+    }
+
+    /**
+     * @deprecated this method was never part of the API (system or public) and is only added
+     *   for migration of existing clients.
+     */
+    @Deprecated
+    public void setScoreFilter(final int score) {
+        mImpl.setScoreFilter(score);
+    }
+
+    /**
+     * Set a score filter for this factory.
+     *
+     * This should include the transports the factory knows its networks will have, and
+     * an optimistic view of the attributes it may have. This does not commit the factory
+     * to being able to bring up such a network ; it only lets it avoid hearing about
+     * requests that it has no chance of fulfilling.
+     *
+     * @param score the filter
+     */
+    public void setScoreFilter(@NonNull final NetworkScore score) {
+        mImpl.setScoreFilter(score);
+    }
+
+    public void setCapabilityFilter(NetworkCapabilities netCap) {
+        mImpl.setCapabilityFilter(netCap);
+    }
+
+    @VisibleForTesting
+    protected int getRequestCount() {
+        return mImpl.getRequestCount();
+    }
+
+    public int getSerialNumber() {
+        return mImpl.getSerialNumber();
+    }
+
+    public NetworkProvider getProvider() {
+        return mImpl.getProvider();
+    }
+
+    protected void log(String s) {
+        Log.d(LOG_TAG, s);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        mImpl.dump(fd, writer, args);
+    }
+
+    @Override
+    public String toString() {
+        return "{" + LOG_TAG + " " + mImpl.toString() + "}";
+    }
+}
diff --git a/staticlibs/device/android/net/NetworkFactoryImpl.java b/staticlibs/device/android/net/NetworkFactoryImpl.java
new file mode 100644
index 0000000..9c1190c
--- /dev/null
+++ b/staticlibs/device/android/net/NetworkFactoryImpl.java
@@ -0,0 +1,322 @@
+/*
+ * 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 android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.NetworkProvider.NetworkOfferCallback;
+import android.os.Looper;
+import android.os.Message;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * A NetworkFactory is an entity that creates NetworkAgent objects.
+ * The bearers register with ConnectivityService using {@link #register} and
+ * their factory will start receiving scored NetworkRequests.  NetworkRequests
+ * can be filtered 3 ways: by NetworkCapabilities, by score and more complexly by
+ * overridden function.  All of these can be dynamic - changing NetworkCapabilities
+ * or score forces re-evaluation of all current requests.
+ *
+ * If any requests pass the filter some overrideable functions will be called.
+ * If the bearer only cares about very simple start/stopNetwork callbacks, those
+ * functions can be overridden.  If the bearer needs more interaction, it can
+ * override addNetworkRequest and removeNetworkRequest which will give it each
+ * request that passes their current filters.
+ * @hide
+ **/
+// TODO(b/187083878): factor out common code between this and NetworkFactoryLegacyImpl
+class NetworkFactoryImpl extends NetworkFactoryLegacyImpl {
+    private static final boolean DBG = NetworkFactory.DBG;
+    private static final boolean VDBG = NetworkFactory.VDBG;
+
+    // A score that will win against everything, so that score filtering will let all requests
+    // through
+    // TODO : remove this and replace with an API to listen to all requests.
+    @NonNull
+    private static final NetworkScore INVINCIBLE_SCORE =
+            new NetworkScore.Builder().setLegacyInt(1000).build();
+
+    // TODO(b/187082970): Replace CMD_* with Handler.post(() -> { ... }) since all the CMDs do is to
+    //  run the tasks asynchronously on the Handler thread.
+
+    /**
+     * Pass a network request to the bearer.  If the bearer believes it can
+     * satisfy the request it should connect to the network and create a
+     * NetworkAgent.  Once the NetworkAgent is fully functional it will
+     * register itself with ConnectivityService using registerNetworkAgent.
+     * If the bearer cannot immediately satisfy the request (no network,
+     * user disabled the radio, lower-scored network) it should remember
+     * any NetworkRequests it may be able to satisfy in the future.  It may
+     * disregard any that it will never be able to service, for example
+     * those requiring a different bearer.
+     * msg.obj = NetworkRequest
+     */
+    // TODO : this and CANCEL_REQUEST are only used by telephony tests. Replace it in the tests
+    // and remove them and the associated code.
+    private static final int CMD_REQUEST_NETWORK = NetworkFactory.CMD_REQUEST_NETWORK;
+
+    /**
+     * Cancel a network request
+     * msg.obj = NetworkRequest
+     */
+    private static final int CMD_CANCEL_REQUEST = NetworkFactory.CMD_CANCEL_REQUEST;
+
+    /**
+     * Internally used to set our best-guess score.
+     * msg.obj = new score
+     */
+    private static final int CMD_SET_SCORE = 3;
+
+    /**
+     * Internally used to set our current filter for coarse bandwidth changes with
+     * technology changes.
+     * msg.obj = new filter
+     */
+    private static final int CMD_SET_FILTER = 4;
+
+    /**
+     * Internally used to send the network offer associated with this factory.
+     * No arguments, will read from members
+     */
+    private static final int CMD_OFFER_NETWORK = 5;
+
+    /**
+     * Internally used to send the request to listen to all requests.
+     * No arguments, will read from members
+     */
+    private static final int CMD_LISTEN_TO_ALL_REQUESTS = 6;
+
+    private final Map<NetworkRequest, NetworkRequestInfo> mNetworkRequests =
+            new LinkedHashMap<>();
+
+    @NonNull private NetworkScore mScore = new NetworkScore.Builder().setLegacyInt(0).build();
+
+    private final NetworkOfferCallback mRequestCallback = new NetworkOfferCallback() {
+        @Override
+        public void onNetworkNeeded(@NonNull final NetworkRequest request) {
+            handleAddRequest(request);
+        }
+
+        @Override
+        public void onNetworkUnneeded(@NonNull final NetworkRequest request) {
+            handleRemoveRequest(request);
+        }
+    };
+    @NonNull private final Executor mExecutor = command -> post(command);
+
+
+    // Ideally the filter argument would be non-null, but null has historically meant to see
+    // no requests and telephony passes null.
+    NetworkFactoryImpl(NetworkFactory parent, Looper looper, Context context,
+            @Nullable final NetworkCapabilities filter) {
+        super(parent, looper, context,
+                null != filter ? filter :
+                        NetworkCapabilities.Builder.withoutDefaultCapabilities().build());
+    }
+
+    /* Registers this NetworkFactory with the system. May only be called once per factory. */
+    @Override public void register(final String logTag) {
+        register(logTag, false);
+    }
+
+    /**
+     * Registers this NetworkFactory with the system ignoring the score filter. This will let
+     * the factory always see all network requests matching its capabilities filter.
+     * May only be called once per factory.
+     */
+    @Override public void registerIgnoringScore(final String logTag) {
+        register(logTag, true);
+    }
+
+    private void register(final String logTag, final boolean listenToAllRequests) {
+        if (mProvider != null) {
+            throw new IllegalStateException("A NetworkFactory must only be registered once");
+        }
+        if (DBG) mParent.log("Registering NetworkFactory");
+
+        mProvider = new NetworkProvider(mContext, NetworkFactoryImpl.this.getLooper(), logTag) {
+            @Override
+            public void onNetworkRequested(@NonNull NetworkRequest request, int score,
+                    int servingProviderId) {
+                handleAddRequest(request);
+            }
+
+            @Override
+            public void onNetworkRequestWithdrawn(@NonNull NetworkRequest request) {
+                handleRemoveRequest(request);
+            }
+        };
+
+        ((ConnectivityManager) mContext.getSystemService(
+                Context.CONNECTIVITY_SERVICE)).registerNetworkProvider(mProvider);
+
+        // The mScore and mCapabilityFilter members can only be accessed on the handler thread.
+        // TODO : offer a separate API to listen to all requests instead
+        if (listenToAllRequests) {
+            sendMessage(obtainMessage(CMD_LISTEN_TO_ALL_REQUESTS));
+        } else {
+            sendMessage(obtainMessage(CMD_OFFER_NETWORK));
+        }
+    }
+
+    private void handleOfferNetwork(@NonNull final NetworkScore score) {
+        mProvider.registerNetworkOffer(score, mCapabilityFilter, mExecutor, mRequestCallback);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case CMD_REQUEST_NETWORK: {
+                handleAddRequest((NetworkRequest) msg.obj);
+                break;
+            }
+            case CMD_CANCEL_REQUEST: {
+                handleRemoveRequest((NetworkRequest) msg.obj);
+                break;
+            }
+            case CMD_SET_SCORE: {
+                handleSetScore((NetworkScore) msg.obj);
+                break;
+            }
+            case CMD_SET_FILTER: {
+                handleSetFilter((NetworkCapabilities) msg.obj);
+                break;
+            }
+            case CMD_OFFER_NETWORK: {
+                handleOfferNetwork(mScore);
+                break;
+            }
+            case CMD_LISTEN_TO_ALL_REQUESTS: {
+                handleOfferNetwork(INVINCIBLE_SCORE);
+                break;
+            }
+        }
+    }
+
+    private static class NetworkRequestInfo {
+        @NonNull public final NetworkRequest request;
+        public boolean requested; // do we have a request outstanding, limited by score
+
+        NetworkRequestInfo(@NonNull final NetworkRequest request) {
+            this.request = request;
+            this.requested = false;
+        }
+
+        @Override
+        public String toString() {
+            return "{" + request + ", requested=" + requested + "}";
+        }
+    }
+
+    /**
+     * Add a NetworkRequest that the bearer may want to attempt to satisfy.
+     * @see #CMD_REQUEST_NETWORK
+     *
+     * @param request the request to handle.
+     */
+    private void handleAddRequest(@NonNull final NetworkRequest request) {
+        NetworkRequestInfo n = mNetworkRequests.get(request);
+        if (n == null) {
+            if (DBG) mParent.log("got request " + request);
+            n = new NetworkRequestInfo(request);
+            mNetworkRequests.put(n.request, n);
+        } else {
+            if (VDBG) mParent.log("handle existing request " + request);
+        }
+        if (VDBG) mParent.log("  my score=" + mScore + ", my filter=" + mCapabilityFilter);
+
+        if (mParent.acceptRequest(request)) {
+            n.requested = true;
+            mParent.needNetworkFor(request);
+        }
+    }
+
+    private void handleRemoveRequest(NetworkRequest request) {
+        NetworkRequestInfo n = mNetworkRequests.get(request);
+        if (n != null) {
+            mNetworkRequests.remove(request);
+            if (n.requested) mParent.releaseNetworkFor(n.request);
+        }
+    }
+
+    private void handleSetScore(@NonNull final NetworkScore score) {
+        if (mScore.equals(score)) return;
+        mScore = score;
+        mParent.reevaluateAllRequests();
+    }
+
+    private void handleSetFilter(@NonNull final NetworkCapabilities netCap) {
+        if (netCap.equals(mCapabilityFilter)) return;
+        mCapabilityFilter = netCap;
+        mParent.reevaluateAllRequests();
+    }
+
+    @Override public final void reevaluateAllRequests() {
+        if (mProvider == null) return;
+        mProvider.registerNetworkOffer(mScore, mCapabilityFilter, mExecutor, mRequestCallback);
+    }
+
+    /**
+     * @deprecated this method was never part of the API (system or public) and is only added
+     *   for migration of existing clients.
+     */
+    @Deprecated
+    public void setScoreFilter(final int score) {
+        setScoreFilter(new NetworkScore.Builder().setLegacyInt(score).build());
+    }
+
+    /**
+     * Set a score filter for this factory.
+     *
+     * This should include the transports the factory knows its networks will have, and
+     * an optimistic view of the attributes it may have. This does not commit the factory
+     * to being able to bring up such a network ; it only lets it avoid hearing about
+     * requests that it has no chance of fulfilling.
+     *
+     * @param score the filter
+     */
+    @Override public void setScoreFilter(@NonNull final NetworkScore score) {
+        sendMessage(obtainMessage(CMD_SET_SCORE, score));
+    }
+
+    @Override public void setCapabilityFilter(NetworkCapabilities netCap) {
+        sendMessage(obtainMessage(CMD_SET_FILTER, new NetworkCapabilities(netCap)));
+    }
+
+    @Override public int getRequestCount() {
+        return mNetworkRequests.size();
+    }
+
+    @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        writer.println(toString());
+        for (NetworkRequestInfo n : mNetworkRequests.values()) {
+            writer.println("  " + n);
+        }
+    }
+
+    @Override public String toString() {
+        return "providerId=" + (mProvider != null ? mProvider.getProviderId() : "null")
+                + ", ScoreFilter=" + mScore + ", Filter=" + mCapabilityFilter
+                + ", requests=" + mNetworkRequests.size();
+    }
+}
diff --git a/staticlibs/device/android/net/NetworkFactoryLegacyImpl.java b/staticlibs/device/android/net/NetworkFactoryLegacyImpl.java
new file mode 100644
index 0000000..6cba625
--- /dev/null
+++ b/staticlibs/device/android/net/NetworkFactoryLegacyImpl.java
@@ -0,0 +1,397 @@
+/*
+ * 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 android.net;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A NetworkFactory is an entity that creates NetworkAgent objects.
+ * The bearers register with ConnectivityService using {@link #register} and
+ * their factory will start receiving scored NetworkRequests.  NetworkRequests
+ * can be filtered 3 ways: by NetworkCapabilities, by score and more complexly by
+ * overridden function.  All of these can be dynamic - changing NetworkCapabilities
+ * or score forces re-evaluation of all current requests.
+ *
+ * If any requests pass the filter some overrideable functions will be called.
+ * If the bearer only cares about very simple start/stopNetwork callbacks, those
+ * functions can be overridden.  If the bearer needs more interaction, it can
+ * override addNetworkRequest and removeNetworkRequest which will give it each
+ * request that passes their current filters.
+ * @hide
+ **/
+// TODO(b/187083878): factor out common code between this and NetworkFactoryImpl
+class NetworkFactoryLegacyImpl extends Handler
+        implements NetworkFactoryShim {
+    private static final boolean DBG = NetworkFactory.DBG;
+    private static final boolean VDBG = NetworkFactory.VDBG;
+
+    // TODO(b/187082970): Replace CMD_* with Handler.post(() -> { ... }) since all the CMDs do is to
+    //  run the tasks asynchronously on the Handler thread.
+
+    /**
+     * Pass a network request to the bearer.  If the bearer believes it can
+     * satisfy the request it should connect to the network and create a
+     * NetworkAgent.  Once the NetworkAgent is fully functional it will
+     * register itself with ConnectivityService using registerNetworkAgent.
+     * If the bearer cannot immediately satisfy the request (no network,
+     * user disabled the radio, lower-scored network) it should remember
+     * any NetworkRequests it may be able to satisfy in the future.  It may
+     * disregard any that it will never be able to service, for example
+     * those requiring a different bearer.
+     * msg.obj = NetworkRequest
+     * msg.arg1 = score - the score of the network currently satisfying this
+     *            request.  If this bearer knows in advance it cannot
+     *            exceed this score it should not try to connect, holding the request
+     *            for the future.
+     *            Note that subsequent events may give a different (lower
+     *            or higher) score for this request, transmitted to each
+     *            NetworkFactory through additional CMD_REQUEST_NETWORK msgs
+     *            with the same NetworkRequest but an updated score.
+     *            Also, network conditions may change for this bearer
+     *            allowing for a better score in the future.
+     * msg.arg2 = the ID of the NetworkProvider currently responsible for the
+     *            NetworkAgent handling this request, or NetworkProvider.ID_NONE if none.
+     */
+    public static final int CMD_REQUEST_NETWORK = 1;
+
+    /**
+     * Cancel a network request
+     * msg.obj = NetworkRequest
+     */
+    public static final int CMD_CANCEL_REQUEST = 2;
+
+    /**
+     * Internally used to set our best-guess score.
+     * msg.arg1 = new score
+     */
+    private static final int CMD_SET_SCORE = 3;
+
+    /**
+     * Internally used to set our current filter for coarse bandwidth changes with
+     * technology changes.
+     * msg.obj = new filter
+     */
+    private static final int CMD_SET_FILTER = 4;
+
+    final Context mContext;
+    final NetworkFactory mParent;
+
+    private final Map<NetworkRequest, NetworkRequestInfo> mNetworkRequests =
+            new LinkedHashMap<>();
+
+    private int mScore;
+    NetworkCapabilities mCapabilityFilter;
+
+    NetworkProvider mProvider = null;
+
+    NetworkFactoryLegacyImpl(NetworkFactory parent, Looper looper, Context context,
+            NetworkCapabilities filter) {
+        super(looper);
+        mParent = parent;
+        mContext = context;
+        mCapabilityFilter = filter;
+    }
+
+    /* Registers this NetworkFactory with the system. May only be called once per factory. */
+    @Override public void register(final String logTag) {
+        if (mProvider != null) {
+            throw new IllegalStateException("A NetworkFactory must only be registered once");
+        }
+        if (DBG) mParent.log("Registering NetworkFactory");
+
+        mProvider = new NetworkProvider(mContext, NetworkFactoryLegacyImpl.this.getLooper(),
+                logTag) {
+            @Override
+            public void onNetworkRequested(@NonNull NetworkRequest request, int score,
+                    int servingProviderId) {
+                handleAddRequest(request, score, servingProviderId);
+            }
+
+            @Override
+            public void onNetworkRequestWithdrawn(@NonNull NetworkRequest request) {
+                handleRemoveRequest(request);
+            }
+        };
+
+        ((ConnectivityManager) mContext.getSystemService(
+            Context.CONNECTIVITY_SERVICE)).registerNetworkProvider(mProvider);
+    }
+
+    /** Unregisters this NetworkFactory. After this call, the object can no longer be used. */
+    @Override public void terminate() {
+        if (mProvider == null) {
+            throw new IllegalStateException("This NetworkFactory was never registered");
+        }
+        if (DBG) mParent.log("Unregistering NetworkFactory");
+
+        ((ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE))
+            .unregisterNetworkProvider(mProvider);
+
+        // Remove all pending messages, since this object cannot be reused. Any message currently
+        // being processed will continue to run.
+        removeCallbacksAndMessages(null);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case CMD_REQUEST_NETWORK: {
+                handleAddRequest((NetworkRequest) msg.obj, msg.arg1, msg.arg2);
+                break;
+            }
+            case CMD_CANCEL_REQUEST: {
+                handleRemoveRequest((NetworkRequest) msg.obj);
+                break;
+            }
+            case CMD_SET_SCORE: {
+                handleSetScore(msg.arg1);
+                break;
+            }
+            case CMD_SET_FILTER: {
+                handleSetFilter((NetworkCapabilities) msg.obj);
+                break;
+            }
+        }
+    }
+
+    private static class NetworkRequestInfo {
+        public final NetworkRequest request;
+        public int score;
+        public boolean requested; // do we have a request outstanding, limited by score
+        public int providerId;
+
+        NetworkRequestInfo(NetworkRequest request, int score, int providerId) {
+            this.request = request;
+            this.score = score;
+            this.requested = false;
+            this.providerId = providerId;
+        }
+
+        @Override
+        public String toString() {
+            return "{" + request + ", score=" + score + ", requested=" + requested + "}";
+        }
+    }
+
+    /**
+     * Add a NetworkRequest that the bearer may want to attempt to satisfy.
+     * @see #CMD_REQUEST_NETWORK
+     *
+     * @param request the request to handle.
+     * @param score the score of the NetworkAgent currently satisfying this request.
+     * @param servingProviderId the ID of the NetworkProvider that created the NetworkAgent
+     *        currently satisfying this request.
+     */
+    @VisibleForTesting
+    protected void handleAddRequest(NetworkRequest request, int score, int servingProviderId) {
+        NetworkRequestInfo n = mNetworkRequests.get(request);
+        if (n == null) {
+            if (DBG) {
+                mParent.log("got request " + request + " with score " + score
+                        + " and providerId " + servingProviderId);
+            }
+            n = new NetworkRequestInfo(request, score, servingProviderId);
+            mNetworkRequests.put(n.request, n);
+        } else {
+            if (VDBG) {
+                mParent.log("new score " + score + " for existing request " + request
+                        + " and providerId " + servingProviderId);
+            }
+            n.score = score;
+            n.providerId = servingProviderId;
+        }
+        if (VDBG) mParent.log("  my score=" + mScore + ", my filter=" + mCapabilityFilter);
+
+        evalRequest(n);
+    }
+
+    private void handleRemoveRequest(NetworkRequest request) {
+        NetworkRequestInfo n = mNetworkRequests.get(request);
+        if (n != null) {
+            mNetworkRequests.remove(request);
+            if (n.requested) mParent.releaseNetworkFor(n.request);
+        }
+    }
+
+    private void handleSetScore(int score) {
+        mScore = score;
+        evalRequests();
+    }
+
+    private void handleSetFilter(NetworkCapabilities netCap) {
+        mCapabilityFilter = netCap;
+        evalRequests();
+    }
+
+    /**
+     * Overridable function to provide complex filtering.
+     * Called for every request every time a new NetworkRequest is seen
+     * and whenever the filterScore or filterNetworkCapabilities change.
+     *
+     * acceptRequest can be overridden to provide complex filter behavior
+     * for the incoming requests
+     *
+     * For output, this class will call {@link NetworkFactory#needNetworkFor} and
+     * {@link NetworkFactory#releaseNetworkFor} for every request that passes the filters.
+     * If you don't need to see every request, you can leave the base
+     * implementations of those two functions and instead override
+     * {@link NetworkFactory#startNetwork} and {@link NetworkFactory#stopNetwork}.
+     *
+     * If you want to see every score fluctuation on every request, set
+     * your score filter to a very high number and watch {@link NetworkFactory#needNetworkFor}.
+     *
+     * @return {@code true} to accept the request.
+     */
+    public boolean acceptRequest(NetworkRequest request) {
+        return mParent.acceptRequest(request);
+    }
+
+    private void evalRequest(NetworkRequestInfo n) {
+        if (VDBG) {
+            mParent.log("evalRequest");
+            mParent.log(" n.requests = " + n.requested);
+            mParent.log(" n.score = " + n.score);
+            mParent.log(" mScore = " + mScore);
+            mParent.log(" request.providerId = " + n.providerId);
+            mParent.log(" mProvider.id = " + mProvider.getProviderId());
+        }
+        if (shouldNeedNetworkFor(n)) {
+            if (VDBG) mParent.log("  needNetworkFor");
+            mParent.needNetworkFor(n.request);
+            n.requested = true;
+        } else if (shouldReleaseNetworkFor(n)) {
+            if (VDBG) mParent.log("  releaseNetworkFor");
+            mParent.releaseNetworkFor(n.request);
+            n.requested = false;
+        } else {
+            if (VDBG) mParent.log("  done");
+        }
+    }
+
+    private boolean shouldNeedNetworkFor(NetworkRequestInfo n) {
+        // If this request is already tracked, it doesn't qualify for need
+        return !n.requested
+            // If the score of this request is higher or equal to that of this factory and some
+            // other factory is responsible for it, then this factory should not track the request
+            // because it has no hope of satisfying it.
+            && (n.score < mScore || n.providerId == mProvider.getProviderId())
+            // If this factory can't satisfy the capability needs of this request, then it
+            // should not be tracked.
+            && n.request.canBeSatisfiedBy(mCapabilityFilter)
+            // Finally if the concrete implementation of the factory rejects the request, then
+            // don't track it.
+            && acceptRequest(n.request);
+    }
+
+    private boolean shouldReleaseNetworkFor(NetworkRequestInfo n) {
+        // Don't release a request that's not tracked.
+        return n.requested
+            // The request should be released if it can't be satisfied by this factory. That
+            // means either of the following conditions are met :
+            // - Its score is too high to be satisfied by this factory and it's not already
+            //   assigned to the factory
+            // - This factory can't satisfy the capability needs of the request
+            // - The concrete implementation of the factory rejects the request
+            && ((n.score > mScore && n.providerId != mProvider.getProviderId())
+                    || !n.request.canBeSatisfiedBy(mCapabilityFilter)
+                    || !acceptRequest(n.request));
+    }
+
+    private void evalRequests() {
+        for (NetworkRequestInfo n : mNetworkRequests.values()) {
+            evalRequest(n);
+        }
+    }
+
+    /**
+     * Post a command, on this NetworkFactory Handler, to re-evaluate all
+     * outstanding requests. Can be called from a factory implementation.
+     */
+    @Override public void reevaluateAllRequests() {
+        post(this::evalRequests);
+    }
+
+    /**
+     * Can be called by a factory to release a request as unfulfillable: the request will be
+     * removed, and the caller will get a
+     * {@link ConnectivityManager.NetworkCallback#onUnavailable()} callback after this function
+     * returns.
+     *
+     * Note: this should only be called by factory which KNOWS that it is the ONLY factory which
+     * is able to fulfill this request!
+     */
+    @Override public void releaseRequestAsUnfulfillableByAnyFactory(NetworkRequest r) {
+        post(() -> {
+            if (DBG) mParent.log("releaseRequestAsUnfulfillableByAnyFactory: " + r);
+            final NetworkProvider provider = mProvider;
+            if (provider == null) {
+                mParent.log("Ignoring attempt to release unregistered request as unfulfillable");
+                return;
+            }
+            provider.declareNetworkRequestUnfulfillable(r);
+        });
+    }
+
+    @Override public void setScoreFilter(int score) {
+        sendMessage(obtainMessage(CMD_SET_SCORE, score, 0));
+    }
+
+    @Override public void setScoreFilter(@NonNull final NetworkScore score) {
+        setScoreFilter(score.getLegacyInt());
+    }
+
+    @Override public void setCapabilityFilter(NetworkCapabilities netCap) {
+        sendMessage(obtainMessage(CMD_SET_FILTER, new NetworkCapabilities(netCap)));
+    }
+
+    @Override public int getRequestCount() {
+        return mNetworkRequests.size();
+    }
+
+    /* TODO: delete when all callers have migrated to NetworkProvider IDs. */
+    @Override public int getSerialNumber() {
+        return mProvider.getProviderId();
+    }
+
+    @Override public NetworkProvider getProvider() {
+        return mProvider;
+    }
+
+    @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        writer.println(toString());
+        for (NetworkRequestInfo n : mNetworkRequests.values()) {
+            writer.println("  " + n);
+        }
+    }
+
+    @Override public String toString() {
+        return "providerId=" + (mProvider != null ? mProvider.getProviderId() : "null")
+                + ", ScoreFilter=" + mScore + ", Filter=" + mCapabilityFilter
+                + ", requests=" + mNetworkRequests.size();
+    }
+}
diff --git a/staticlibs/device/android/net/NetworkFactoryShim.java b/staticlibs/device/android/net/NetworkFactoryShim.java
new file mode 100644
index 0000000..dfbb5ec
--- /dev/null
+++ b/staticlibs/device/android/net/NetworkFactoryShim.java
@@ -0,0 +1,71 @@
+/*
+ * 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 android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Looper;
+import android.os.Message;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Extract an interface for multiple implementation of {@link NetworkFactory}, depending on the SDK
+ * version.
+ *
+ * Known implementations:
+ * - {@link NetworkFactoryImpl}: For Android S+
+ * - {@link NetworkFactoryLegacyImpl}: For Android R-
+ *
+ * @hide
+ */
+interface NetworkFactoryShim {
+    void register(String logTag);
+
+    default void registerIgnoringScore(String logTag) {
+        throw new UnsupportedOperationException();
+    }
+
+    void terminate();
+
+    void releaseRequestAsUnfulfillableByAnyFactory(NetworkRequest r);
+
+    void reevaluateAllRequests();
+
+    void setScoreFilter(int score);
+
+    void setScoreFilter(@NonNull NetworkScore score);
+
+    void setCapabilityFilter(NetworkCapabilities netCap);
+
+    int getRequestCount();
+
+    int getSerialNumber();
+
+    NetworkProvider getProvider();
+
+    void dump(FileDescriptor fd, PrintWriter writer, String[] args);
+
+    // All impls inherit Handler
+    @VisibleForTesting
+    Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj);
+
+    Looper getLooper();
+}
diff --git a/staticlibs/device/com/android/net/module/util/BpfBitmap.java b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
new file mode 100644
index 0000000..b62a430
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.os.Build;
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+ /**
+ *
+ * Generic bitmap class for use with BPF programs. Corresponds to a BpfMap
+ * array type with key->int and value->uint64_t defined in the bpf program.
+ *
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class BpfBitmap {
+    private BpfMap<Struct.S32, Struct.S64> mBpfMap;
+
+    /**
+     * Create a BpfBitmap map wrapper with "path" of filesystem.
+     *
+     * @param path The path of the BPF map.
+     */
+    public BpfBitmap(@NonNull String path) throws ErrnoException {
+        mBpfMap = new BpfMap<>(path, Struct.S32.class, Struct.S64.class);
+    }
+
+    /**
+     * Retrieves the value from BpfMap for the given key.
+     *
+     * @param key The key in the map corresponding to the value to return.
+     */
+    private long getBpfMapValue(Struct.S32 key) throws ErrnoException  {
+        Struct.S64 curVal = mBpfMap.getValue(key);
+        if (curVal != null) {
+            return curVal.val;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Retrieves the bit for the given index in the bitmap.
+     *
+     * @param index Position in bitmap.
+     */
+    public boolean get(int index) throws ErrnoException  {
+        if (index < 0) return false;
+
+        Struct.S32 key = new Struct.S32(index >> 6);
+        return ((getBpfMapValue(key) >>> (index & 63)) & 1L) != 0;
+    }
+
+    /**
+     * Set the specified index in the bitmap.
+     *
+     * @param index Position to set in bitmap.
+     */
+    public void set(int index) throws ErrnoException {
+        set(index, true);
+    }
+
+    /**
+     * Unset the specified index in the bitmap.
+     *
+     * @param index Position to unset in bitmap.
+     */
+    public void unset(int index) throws ErrnoException {
+        set(index, false);
+    }
+
+    /**
+     * Change the specified index in the bitmap to set value.
+     *
+     * @param index Position to unset in bitmap.
+     * @param set Boolean indicating to set or unset index.
+     */
+    public void set(int index, boolean set) throws ErrnoException {
+        if (index < 0) throw new IllegalArgumentException("Index out of bounds.");
+
+        Struct.S32 key = new Struct.S32(index >> 6);
+        long mask = (1L << (index & 63));
+        long val = getBpfMapValue(key);
+        if (set) val |= mask; else val &= ~mask;
+        mBpfMap.updateEntry(key, new Struct.S64(val));
+    }
+
+    /**
+     * Clears the map. The map may already be empty.
+     *
+     * @throws ErrnoException if updating entry to 0 fails.
+     */
+    public void clear() throws ErrnoException {
+        mBpfMap.forEach((key, value) -> {
+            mBpfMap.updateEntry(key, new Struct.S64(0));
+        });
+    }
+
+    /**
+     * Checks if all bitmap values are 0.
+     */
+    public boolean isEmpty() throws ErrnoException {
+        Struct.S32 key = mBpfMap.getFirstKey();
+        while (key != null) {
+            if (getBpfMapValue(key) != 0) {
+                return false;
+            }
+            key = mBpfMap.getNextKey(key);
+        }
+        return true;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/BpfDump.java b/staticlibs/device/com/android/net/module/util/BpfDump.java
new file mode 100644
index 0000000..4227194
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/BpfDump.java
@@ -0,0 +1,156 @@
+/*
+ * 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.net.module.util;
+
+import static android.system.OsConstants.R_OK;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Base64;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+
+import java.io.PrintWriter;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.function.BiFunction;
+
+/**
+ * The classes and the methods for BPF dump utilization.
+ */
+public class BpfDump {
+    // Using "," as a separator between base64 encoded key and value is safe because base64
+    // characters are [0-9a-zA-Z/=+].
+    private static final String BASE64_DELIMITER = ",";
+
+    /**
+     * Encode BPF key and value into a base64 format string which uses the delimiter ',':
+     * <base64 encoded key>,<base64 encoded value>
+     */
+    public static <K extends Struct, V extends Struct> String toBase64EncodedString(
+            @NonNull final K key, @NonNull final V value) {
+        final byte[] keyBytes = key.writeToBytes();
+        final String keyBase64Str = Base64.encodeToString(keyBytes, Base64.DEFAULT)
+                .replace("\n", "");
+        final byte[] valueBytes = value.writeToBytes();
+        final String valueBase64Str = Base64.encodeToString(valueBytes, Base64.DEFAULT)
+                .replace("\n", "");
+
+        return keyBase64Str + BASE64_DELIMITER + valueBase64Str;
+    }
+
+    /**
+     * Decode Struct from a base64 format string
+     */
+    private static <T extends Struct> T parseStruct(
+            Class<T> structClass, @NonNull String base64Str) {
+        final byte[] bytes = Base64.decode(base64Str, Base64.DEFAULT);
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        return Struct.parse(structClass, byteBuffer);
+    }
+
+    /**
+     * Decode BPF key and value from a base64 format string which uses the delimiter ',':
+     * <base64 encoded key>,<base64 encoded value>
+     */
+    public static <K extends Struct, V extends Struct> Pair<K, V> fromBase64EncodedString(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String base64Str) {
+        String[] keyValueStrs = base64Str.split(BASE64_DELIMITER);
+        if (keyValueStrs.length != 2 /* key + value */) {
+            throw new IllegalArgumentException("Invalid base64Str (" + base64Str + "), base64Str"
+                    + " must contain exactly one delimiter '" + BASE64_DELIMITER + "'");
+        }
+        final K k = parseStruct(keyClass, keyValueStrs[0]);
+        final V v = parseStruct(valueClass, keyValueStrs[1]);
+        return new Pair<>(k, v);
+    }
+
+    /**
+     * 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,
+            PrintWriter pw, String mapName, BiFunction<K, V, String> entryToString) {
+        dumpMap(map, pw, mapName, "" /* header */, entryToString);
+    }
+
+    /**
+     * Dump the BpfMap name, header, and entries
+     */
+    public static <K extends Struct, V extends Struct> void dumpMap(IBpfMap<K, V> map,
+            PrintWriter pw, String mapName, String header, BiFunction<K, V, String> entryToString) {
+        pw.println(mapName + ":");
+        if (!header.isEmpty()) {
+            pw.println("  " + header);
+        }
+        try {
+            map.forEach((key, value) -> {
+                // Value could be null if there is a concurrent entry deletion.
+                // http://b/220084230.
+                if (value != null) {
+                    pw.println("  " + entryToString.apply(key, value));
+                } else {
+                    pw.println("Entry is deleted while dumping, iterating from first entry");
+                }
+            });
+        } catch (ErrnoException e) {
+            pw.println("Map dump end with error: " + Os.strerror(e.errno));
+        }
+    }
+
+    /**
+     * Dump the BpfMap status
+     */
+    public static <K extends Struct, V extends Struct> void dumpMapStatus(IBpfMap<K, V> map,
+            PrintWriter pw, String mapName, String path) {
+        if (map != null) {
+            pw.println(mapName + ": OK");
+            return;
+        }
+        try {
+            Os.access(path, R_OK);
+            pw.println(mapName + ": NULL(map is pinned to " + path + ")");
+        } catch (ErrnoException e) {
+            pw.println(mapName + ": NULL(map is not pinned to " + path + ": "
+                    + Os.strerror(e.errno) + ")");
+        }
+    }
+
+    // TODO: add a helper to dump bpf map content with the map name, the header line
+    // (ex: "BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif"), a lambda that
+    // knows how to dump each line, and the PrintWriter.
+}
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
new file mode 100644
index 0000000..0fbc25d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -0,0 +1,293 @@
+/*
+ * 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 static android.system.OsConstants.EBUSY;
+import static android.system.OsConstants.EEXIST;
+import static android.system.OsConstants.ENOENT;
+
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * BpfMap is a key -> value mapping structure that is designed to maintained the bpf map entries.
+ * This is a wrapper class of in-kernel data structure. The in-kernel data can be read/written by
+ * passing syscalls with map file descriptor.
+ *
+ * @param <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class BpfMap<K extends Struct, V extends Struct> implements IBpfMap<K, V> {
+    static {
+        System.loadLibrary(JniUtil.getJniLibraryName(BpfMap.class.getPackage()));
+    }
+
+    // Following definitions from kernel include/uapi/linux/bpf.h
+    public static final int BPF_F_RDWR = 0;
+    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;
+
+    private static final int BPF_ANY = 0;
+    private static final int BPF_NOEXIST = 1;
+    private static final int BPF_EXIST = 2;
+
+    private final ParcelFileDescriptor mMapFd;
+    private final Class<K> mKeyClass;
+    private final Class<V> mValueClass;
+    private final int mKeySize;
+    private final int mValueSize;
+
+    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 {
+        // Supports up to 1023 byte key and 65535 byte values
+        // Creating a BpfMap with larger keys/values seems like a bad idea any way...
+        keySize &= 1023; // 10-bits
+        valueSize &= 65535; // 16-bits
+        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 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 checkModeExclusivity(fd, mode);
+            // okay, we really haven't opened this before...
+            fd = ParcelFileDescriptor.adoptFd(nativeBpfFdGet(path, mode, keySize, valueSize));
+            sFdCache.put(key, fd);
+            return fd;
+        }
+    }
+
+    /**
+     * Create a BpfMap map wrapper with "path" of filesystem.
+     *
+     * @param flag the access mode, one of BPF_F_RDWR, BPF_F_RDONLY, or BPF_F_WRONLY.
+     * @throws ErrnoException if the BPF map associated with {@code path} cannot be retrieved.
+     * @throws NullPointerException if {@code path} is null.
+     */
+    public BpfMap(@NonNull final String path, final int flag, final Class<K> key,
+            final Class<V> value) throws ErrnoException, NullPointerException {
+        mKeyClass = key;
+        mValueClass = value;
+        mKeySize = Struct.getSize(key);
+        mValueSize = Struct.getSize(value);
+        mMapFd = cachedBpfFdGet(path, flag, mKeySize, mValueSize);
+    }
+
+    /**
+     * Create a R/W BpfMap map wrapper with "path" of filesystem.
+     *
+     * @throws ErrnoException if the BPF map associated with {@code path} cannot be retrieved.
+     * @throws NullPointerException if {@code path} is null.
+     */
+    public BpfMap(@NonNull final String path, final Class<K> key,
+            final Class<V> value) throws ErrnoException, NullPointerException {
+        this(path, BPF_F_RDWR, key, value);
+    }
+
+    /**
+     * Update an existing or create a new key -> value entry in an eBbpf map.
+     * (use insertOrReplaceEntry() if you need to know whether insert or replace happened)
+     */
+    @Override
+    public void updateEntry(K key, V value) throws ErrnoException {
+        nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_ANY);
+    }
+
+    /**
+     * If the key does not exist in the map, insert key -> value entry into eBpf map.
+     * Otherwise IllegalStateException will be thrown.
+     */
+    @Override
+    public void insertEntry(K key, V value)
+            throws ErrnoException, IllegalStateException {
+        try {
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_NOEXIST);
+        } catch (ErrnoException e) {
+            if (e.errno == EEXIST) throw new IllegalStateException(key + " already exists");
+
+            throw e;
+        }
+    }
+
+    /**
+     * If the key already exists in the map, replace its value. Otherwise NoSuchElementException
+     * will be thrown.
+     */
+    @Override
+    public void replaceEntry(K key, V value)
+            throws ErrnoException, NoSuchElementException {
+        try {
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_EXIST);
+        } catch (ErrnoException e) {
+            if (e.errno == ENOENT) throw new NoSuchElementException(key + " not found");
+
+            throw e;
+        }
+    }
+
+    /**
+     * Update an existing or create a new key -> value entry in an eBbpf map.
+     * Returns true if inserted, false if replaced.
+     * (use updateEntry() if you don't care whether insert or replace happened)
+     * Note: see inline comment below if running concurrently with delete operations.
+     */
+    @Override
+    public boolean insertOrReplaceEntry(K key, V value)
+            throws ErrnoException {
+        try {
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_NOEXIST);
+            return true;   /* insert succeeded */
+        } catch (ErrnoException e) {
+            if (e.errno != EEXIST) throw e;
+        }
+        try {
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_EXIST);
+            return false;   /* replace succeeded */
+        } catch (ErrnoException e) {
+            if (e.errno != ENOENT) throw e;
+        }
+        /* If we reach here somebody deleted after our insert attempt and before our replace:
+         * this implies a race happened.  The kernel bpf delete interface only takes a key,
+         * and not the value, so we can safely pretend the replace actually succeeded and
+         * was immediately followed by the other thread's delete, since the delete cannot
+         * observe the potential change to the value.
+         */
+        return false;   /* pretend replace succeeded */
+    }
+
+    /** Remove existing key from eBpf map. Return false if map was not modified. */
+    @Override
+    public boolean deleteEntry(K key) throws ErrnoException {
+        return nativeDeleteMapEntry(mMapFd.getFd(), key.writeToBytes());
+    }
+
+    private K getNextKeyInternal(@Nullable K key) throws ErrnoException {
+        byte[] rawKey = new byte[mKeySize];
+
+        if (!nativeGetNextMapKey(mMapFd.getFd(),
+                                 key == null ? null : key.writeToBytes(),
+                                 rawKey)) return null;
+
+        final ByteBuffer buffer = ByteBuffer.wrap(rawKey);
+        buffer.order(ByteOrder.nativeOrder());
+        return Struct.parse(mKeyClass, buffer);
+    }
+
+    /**
+     * Get the next key of the passed-in key. If the passed-in key is not found, return the first
+     * key. If the passed-in key is the last one, return null.
+     *
+     * TODO: consider allowing null passed-in key.
+     */
+    @Override
+    public K getNextKey(@NonNull K key) throws ErrnoException {
+        Objects.requireNonNull(key);
+        return getNextKeyInternal(key);
+    }
+
+    /** Get the first key of eBpf map. */
+    @Override
+    public K getFirstKey() throws ErrnoException {
+        return getNextKeyInternal(null);
+    }
+
+    /** Check whether a key exists in the map. */
+    @Override
+    public boolean containsKey(@NonNull K key) throws ErrnoException {
+        Objects.requireNonNull(key);
+
+        byte[] rawValue = new byte[mValueSize];
+        return nativeFindMapEntry(mMapFd.getFd(), key.writeToBytes(), rawValue);
+    }
+
+    /** Retrieve a value from the map. Return null if there is no such key. */
+    @Override
+    public V getValue(@NonNull K key) throws ErrnoException {
+        Objects.requireNonNull(key);
+
+        byte[] rawValue = new byte[mValueSize];
+        if (!nativeFindMapEntry(mMapFd.getFd(), key.writeToBytes(), rawValue)) return null;
+
+        final ByteBuffer buffer = ByteBuffer.wrap(rawValue);
+        buffer.order(ByteOrder.nativeOrder());
+        return Struct.parse(mValueClass, buffer);
+    }
+
+    /** Synchronize Kernel RCU */
+    public static void synchronizeKernelRCU() throws ErrnoException {
+        nativeSynchronizeKernelRCU();
+    }
+
+    private static native int nativeBpfFdGet(String path, int mode, int keySize, int valueSize)
+            throws ErrnoException, NullPointerException;
+
+    // Note: the following methods appear to not require the object by virtue of taking the
+    // fd as an int argument, but the hidden reference to this is actually what prevents
+    // the object from being garbage collected (and thus potentially maps closed) prior
+    // to the native code actually running (with a possibly already closed fd).
+
+    private native void nativeWriteToMapEntry(int fd, byte[] key, byte[] value, int flags)
+            throws ErrnoException;
+
+    private native boolean nativeDeleteMapEntry(int fd, byte[] key) throws ErrnoException;
+
+    // If key is found, the operation returns true and the nextKey would reference to the next
+    // element.  If key is not found, the operation returns true and the nextKey would reference to
+    // the first element.  If key is the last element, false is returned.
+    private native boolean nativeGetNextMapKey(int fd, byte[] key, byte[] nextKey)
+            throws ErrnoException;
+
+    private native boolean nativeFindMapEntry(int fd, byte[] key, byte[] value)
+            throws ErrnoException;
+
+    private static native void nativeSynchronizeKernelRCU() throws ErrnoException;
+}
diff --git a/staticlibs/device/com/android/net/module/util/BpfUtils.java b/staticlibs/device/com/android/net/module/util/BpfUtils.java
new file mode 100644
index 0000000..a41eeba
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/BpfUtils.java
@@ -0,0 +1,71 @@
+/*
+ * 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.net.module.util;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.io.IOException;
+
+/**
+ * The classes and the methods for BPF utilization.
+ *
+ * {@hide}
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class BpfUtils {
+    static {
+        System.loadLibrary(JniUtil.getJniLibraryName(BpfUtils.class.getPackage()));
+    }
+
+    // Defined in include/uapi/linux/bpf.h. Only adding the CGROUPS currently being used for now.
+    public static final int BPF_CGROUP_INET_INGRESS = 0;
+    public static final int BPF_CGROUP_INET_EGRESS = 1;
+    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.
+    public static final String CGROUP_PATH = "/sys/fs/cgroup/";
+
+    /**
+     * Get BPF program Id from CGROUP.
+     *
+     * Note: This requires a 4.19 kernel which is only guaranteed on V+.
+     *
+     * @param attachType Bpf attach type. See bpf_attach_type in include/uapi/linux/bpf.h.
+     * @return Positive integer for a Program Id. 0 if no program is attached.
+     * @throws IOException if failed to open the cgroup directory or query bpf program.
+     */
+    public static int getProgramId(int attachType) throws IOException {
+        return native_getProgramIdFromCgroup(attachType, CGROUP_PATH);
+    }
+
+    private static native int native_getProgramIdFromCgroup(int type, String cgroupPath)
+            throws IOException;
+}
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
new file mode 100644
index 0000000..0426ace
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -0,0 +1,433 @@
+/*
+ * 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 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;
+
+import static com.android.net.module.util.FeatureVersions.CONNECTIVITY_MODULE_ID;
+import static com.android.net.module.util.FeatureVersions.MODULE_MASK;
+import static com.android.net.module.util.FeatureVersions.NETWORK_STACK_MODULE_ID;
+import static com.android.net.module.util.FeatureVersions.VERSION_MASK;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.provider.DeviceConfig;
+import android.util.Log;
+
+import androidx.annotation.BoolRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Utilities for modules to query {@link DeviceConfig} and flags.
+ */
+public final class DeviceConfigUtils {
+    private DeviceConfigUtils() {}
+
+    private static final String TAG = DeviceConfigUtils.class.getSimpleName();
+    /**
+     * DO NOT MODIFY: this may be used by multiple modules that will not see the updated value
+     * until they are recompiled, so modifying this constant means that different modules may
+     * be referencing a different tethering module variant, or having a stale reference.
+     */
+    public static final String TETHERING_MODULE_NAME = "com.android.tethering";
+
+    @VisibleForTesting
+    public static final String RESOURCES_APK_INTENT =
+            "com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK";
+    private static final String CONNECTIVITY_RES_PKG_DIR = "/apex/" + TETHERING_MODULE_NAME + "/";
+
+    @VisibleForTesting
+    public static final long DEFAULT_PACKAGE_VERSION = 1000;
+
+    @VisibleForTesting
+    public static void resetPackageVersionCacheForTest() {
+        sPackageVersion = -1;
+        sModuleVersion = -1;
+        sNetworkStackModuleVersion = -1;
+    }
+
+    private static final int FORCE_ENABLE_FEATURE_FLAG_VALUE = 1;
+    private static final int FORCE_DISABLE_FEATURE_FLAG_VALUE = -1;
+
+    private static volatile long sPackageVersion = -1;
+    private static long getPackageVersion(@NonNull final Context context) {
+        // sPackageVersion may be set by another thread just after this check, but querying the
+        // package version several times on rare occasions is fine.
+        if (sPackageVersion >= 0) {
+            return sPackageVersion;
+        }
+        try {
+            final long version = context.getPackageManager().getPackageInfo(
+                    context.getPackageName(), 0).getLongVersionCode();
+            sPackageVersion = version;
+            return version;
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Failed to get package info: " + e);
+            return DEFAULT_PACKAGE_VERSION;
+        }
+    }
+
+    /**
+     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @param defaultValue The value to return if the property does not exist or has no valid value.
+     * @return the corresponding value, or defaultValue if none exists.
+     */
+    @Nullable
+    public static String getDeviceConfigProperty(@NonNull String namespace, @NonNull String name,
+            @Nullable String defaultValue) {
+        String value = DeviceConfig.getProperty(namespace, name);
+        return value != null ? value : defaultValue;
+    }
+
+    /**
+     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @param defaultValue The value to return if the property does not exist or its value is null.
+     * @return the corresponding value, or defaultValue if none exists.
+     */
+    public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name,
+            int defaultValue) {
+        String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */);
+        try {
+            return (value != null) ? Integer.parseInt(value) : defaultValue;
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
+     *
+     * Flags like timeouts should use this method and set an appropriate min/max range: if invalid
+     * values like "0" or "1" are pushed to devices, everything would timeout. The min/max range
+     * protects against this kind of breakage.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @param minimumValue The minimum value of a property.
+     * @param maximumValue The maximum value of a property.
+     * @param defaultValue The value to return if the property does not exist or its value is null.
+     * @return the corresponding value, or defaultValue if none exists or the fetched value is
+     *         not in the provided range.
+     */
+    public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name,
+            int minimumValue, int maximumValue, int defaultValue) {
+        int value = getDeviceConfigPropertyInt(namespace, name, defaultValue);
+        if (value < minimumValue || value > maximumValue) return defaultValue;
+        return value;
+    }
+
+    /**
+     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @param defaultValue The value to return if the property does not exist or its value is null.
+     * @return the corresponding value, or defaultValue if none exists.
+     */
+    public static boolean getDeviceConfigPropertyBoolean(@NonNull String namespace,
+            @NonNull String name, boolean defaultValue) {
+        String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */);
+        return (value != null) ? Boolean.parseBoolean(value) : defaultValue;
+    }
+
+    /**
+     * 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 #isNetworkStackFeatureNotChickenedOut(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 isNetworkStackFeatureEnabled(@NonNull Context context,
+            @NonNull String name) {
+        return isFeatureEnabled(NAMESPACE_CONNECTIVITY, name, false /* defaultEnabled */,
+                () -> getPackageVersion(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 #isTetheringFeatureNotChickenedOut(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 isTetheringFeatureEnabled(@NonNull Context context,
+            @NonNull String name) {
+        return isFeatureEnabled(NAMESPACE_TETHERING, name, false /* defaultEnabled */,
+                () -> 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 */);
+        switch (flagValue) {
+            case 0:
+                return defaultEnabled;
+            case FORCE_DISABLE_FEATURE_FLAG_VALUE:
+                return false;
+            case FORCE_ENABLE_FEATURE_FLAG_VALUE:
+                return true;
+            default:
+                final long packageVersion = packageVersionSupplier.get();
+                return packageVersion >= (long) flagValue;
+        }
+    }
+
+    // Guess the tethering module name based on the package prefix of the connectivity resources
+    // Take the resource package name, cut it before "connectivity" and append "tethering".
+    // Then resolve that package version number with packageManager.
+    // If that fails retry by appending "go.tethering" instead
+    private static long resolveTetheringModuleVersion(@NonNull Context context)
+            throws PackageManager.NameNotFoundException {
+        final String pkgPrefix = resolvePkgPrefix(context);
+        final PackageManager packageManager = context.getPackageManager();
+        try {
+            return packageManager.getPackageInfo(pkgPrefix + "tethering",
+                    PackageManager.MATCH_APEX).getLongVersionCode();
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.d(TAG, "Device is using go modules");
+            // fall through
+        }
+
+        return packageManager.getPackageInfo(pkgPrefix + "go.tethering",
+                PackageManager.MATCH_APEX).getLongVersionCode();
+    }
+
+    private static String resolvePkgPrefix(Context context) {
+        final String connResourcesPackage = getConnectivityResourcesPackageName(context);
+        final int pkgPrefixLen = connResourcesPackage.indexOf("connectivity");
+        if (pkgPrefixLen < 0) {
+            throw new IllegalStateException(
+                    "Invalid connectivity resources package: " + connResourcesPackage);
+        }
+
+        return connResourcesPackage.substring(0, pkgPrefixLen);
+    }
+
+    private static volatile long sModuleVersion = -1;
+    private static long getTetheringModuleVersion(@NonNull Context context) {
+        if (sModuleVersion >= 0) return sModuleVersion;
+
+        try {
+            sModuleVersion = resolveTetheringModuleVersion(context);
+        } catch (PackageManager.NameNotFoundException e) {
+            // It's expected to fail tethering module version resolution on the devices with
+            // flattened apex
+            Log.e(TAG, "Failed to resolve tethering module version: " + e);
+            return DEFAULT_PACKAGE_VERSION;
+        }
+        return sModuleVersion;
+    }
+
+    private static volatile long sNetworkStackModuleVersion = -1;
+
+    /**
+     * Get networkstack module version.
+     */
+    @VisibleForTesting
+    static long getNetworkStackModuleVersion(@NonNull Context context) {
+        if (sNetworkStackModuleVersion >= 0) return sNetworkStackModuleVersion;
+
+        try {
+            sNetworkStackModuleVersion = resolveNetworkStackModuleVersion(context);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.wtf(TAG, "Failed to resolve networkstack module version: " + e);
+            return DEFAULT_PACKAGE_VERSION;
+        }
+        return sNetworkStackModuleVersion;
+    }
+
+    private static long resolveNetworkStackModuleVersion(@NonNull Context context)
+            throws PackageManager.NameNotFoundException {
+        // TODO(b/293975546): Strictly speaking this is the prefix for connectivity and not
+        //  network stack. In practice, it's the same. Read the prefix from network stack instead.
+        final String pkgPrefix = resolvePkgPrefix(context);
+        final PackageManager packageManager = context.getPackageManager();
+        try {
+            return packageManager.getPackageInfo(pkgPrefix + "networkstack",
+                    PackageManager.MATCH_SYSTEM_ONLY).getLongVersionCode();
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.d(TAG, "Device is using go or non-mainline modules");
+            // fall through
+        }
+
+        return packageManager.getPackageInfo(pkgPrefix + "go.networkstack",
+                PackageManager.MATCH_ALL).getLongVersionCode();
+    }
+
+    /**
+     * Check whether one specific feature is supported from the feature Id. The feature Id is
+     * composed by a module package Id and version Id from {@link FeatureVersions}.
+     *
+     * This is useful when a feature required minimal module version supported and cannot function
+     * well with a standalone newer module.
+     * @param context The global context information about an app environment.
+     * @param featureId The feature id that contains required module id and minimal module version
+     * @return true if this feature is supported, or false if not supported.
+     **/
+    public static boolean isFeatureSupported(@NonNull Context context, long featureId) {
+        final long moduleVersion;
+        final long moduleId = featureId & MODULE_MASK;
+        if (moduleId == CONNECTIVITY_MODULE_ID) {
+            moduleVersion = getTetheringModuleVersion(context);
+        } else if (moduleId == NETWORK_STACK_MODULE_ID) {
+            moduleVersion = getNetworkStackModuleVersion(context);
+        } else {
+            throw new IllegalArgumentException("Unknown module " + moduleId);
+        }
+        // Support by default if no module version is available.
+        return moduleVersion == DEFAULT_PACKAGE_VERSION
+                || moduleVersion >= (featureId & VERSION_MASK);
+    }
+
+    /**
+     * Check whether one specific experimental feature in Tethering module from {@link DeviceConfig}
+     * is not disabled.
+     * If the feature is enabled by default and disabled by flag push (kill switch), this method
+     * should be used.
+     * If the feature is disabled by default and enabled by flag push,
+     * {@link #isTetheringFeatureEnabled(Context, String)} should be used.
+     *
+     * @param context The global context information about an app environment.
+     * @param name The name of the property in tethering module to look up.
+     * @return true if this feature is enabled, or false if disabled.
+     */
+    public static boolean isTetheringFeatureNotChickenedOut(@NonNull Context context, String name) {
+        return isFeatureEnabled(NAMESPACE_TETHERING, name, true /* defaultEnabled */,
+                () -> getTetheringModuleVersion(context));
+    }
+
+    /**
+     * Check whether one specific experimental feature in NetworkStack module from
+     * {@link DeviceConfig} is not disabled.
+     * If the feature is enabled by default and disabled by flag push (kill switch), this method
+     * should be used.
+     * If the feature is disabled by default and enabled by flag push,
+     * {@link #isNetworkStackFeatureEnabled(Context, String)} should be used.
+     *
+     * @param context The global context information about an app environment.
+     * @param name The name of the property in NetworkStack module to look up.
+     * @return true if this feature is enabled, or false if disabled.
+     */
+    public static boolean isNetworkStackFeatureNotChickenedOut(
+            @NonNull Context context, String name) {
+        return isFeatureEnabled(NAMESPACE_CONNECTIVITY, name, true /* defaultEnabled */,
+                () -> getPackageVersion(context));
+    }
+
+    /**
+     * Gets boolean config from resources.
+     */
+    public static boolean getResBooleanConfig(@NonNull final Context context,
+            @BoolRes int configResource, final boolean defaultValue) {
+        final Resources res = context.getResources();
+        try {
+            return res.getBoolean(configResource);
+        } catch (Resources.NotFoundException e) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Gets int config from resources.
+     */
+    public static int getResIntegerConfig(@NonNull final Context context,
+            @BoolRes int configResource, final int defaultValue) {
+        final Resources res = context.getResources();
+        try {
+            return res.getInteger(configResource);
+        } catch (Resources.NotFoundException e) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Get the package name of the ServiceConnectivityResources package, used to provide resources
+     * for service-connectivity.
+     */
+    @NonNull
+    public static String getConnectivityResourcesPackageName(@NonNull Context context) {
+        final List<ResolveInfo> pkgs = new ArrayList<>(context.getPackageManager()
+                .queryIntentActivities(new Intent(RESOURCES_APK_INTENT), MATCH_SYSTEM_ONLY));
+        pkgs.removeIf(pkg -> !pkg.activityInfo.applicationInfo.sourceDir.startsWith(
+                CONNECTIVITY_RES_PKG_DIR));
+        if (pkgs.size() > 1) {
+            Log.wtf(TAG, "More than one connectivity resources package found: " + pkgs);
+        }
+        if (pkgs.isEmpty()) {
+            throw new IllegalStateException("No connectivity resource package found");
+        }
+
+        return pkgs.get(0).activityInfo.applicationInfo.packageName;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/DomainUtils.java b/staticlibs/device/com/android/net/module/util/DomainUtils.java
new file mode 100644
index 0000000..b327fd0
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/DomainUtils.java
@@ -0,0 +1,143 @@
+/*
+ * 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.net.module.util;
+
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.DnsPacketUtils.DnsRecordParser;
+
+import java.nio.BufferOverflowException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+
+/**
+ * Utilities for encoding/decoding the domain name or domain search list.
+ *
+ * @hide
+ */
+public final class DomainUtils {
+    private static final String TAG = "DomainUtils";
+    private static final int MAX_OPTION_LEN = 255;
+
+    @NonNull
+    private static String getSubstring(@NonNull final String string, @NonNull final String[] labels,
+            int index) {
+        int beginIndex = 0;
+        for (int i = 0; i < index; i++) {
+            beginIndex += labels[i].length() + 1; // include the dot
+        }
+        return string.substring(beginIndex);
+    }
+
+    /**
+     * Encode the given single domain name to byte array, comply with RFC1035 section-3.1.
+     *
+     * @return null if the given domain string is invalid, otherwise, return a byte array
+     *         wrapping the encoded domain, not including any padded octets, caller should
+     *         pad zero octets at the end if needed.
+     */
+    @Nullable
+    public static byte[] encode(@NonNull final String domain) {
+        if (!DnsRecordParser.isHostName(domain)) return null;
+        return encode(new String[]{ domain }, false /* compression */);
+    }
+
+    /**
+     * Encode the given multiple domain names to byte array, comply with RFC1035 section-3.1
+     * and section 4.1.4 (message compression) if enabled.
+     *
+     * @return Null if encode fails due to BufferOverflowException, otherwise, return a byte
+     *         array wrapping the encoded domains, not including any padded octets, caller
+     *         should pad zero octets at the end if needed. The byte array may be empty if
+     *         the given domain strings are invalid.
+     */
+    @Nullable
+    public static byte[] encode(@NonNull final String[] domains, boolean compression) {
+        try {
+            final ByteBuffer buffer = ByteBuffer.allocate(MAX_OPTION_LEN);
+            final ArrayMap<String, Integer> offsetMap = new ArrayMap<>();
+            for (int i = 0; i < domains.length; i++) {
+                if (!DnsRecordParser.isHostName(domains[i])) {
+                    Log.e(TAG, "Skip invalid domain name " + domains[i]);
+                    continue;
+                }
+                final String[] labels = domains[i].split("\\.");
+                for (int j = 0; j < labels.length; j++) {
+                    if (compression) {
+                        final String suffix = getSubstring(domains[i], labels, j);
+                        if (offsetMap.containsKey(suffix)) {
+                            int offsetOfSuffix = offsetMap.get(suffix);
+                            offsetOfSuffix |= 0xC000;
+                            buffer.putShort((short) offsetOfSuffix);
+                            break; // unnecessary to put the compressed string into map
+                        } else {
+                            offsetMap.put(suffix, buffer.position());
+                        }
+                    }
+                    // encode the domain name string without compression when:
+                    // - compression feature isn't enabled,
+                    // - suffix does not match any string in the map.
+                    final byte[] labelBytes = labels[j].getBytes(StandardCharsets.UTF_8);
+                    buffer.put((byte) labelBytes.length);
+                    buffer.put(labelBytes);
+                    if (j == labels.length - 1) {
+                        // Pad terminate label at the end of last label.
+                        buffer.put((byte) 0);
+                    }
+                }
+            }
+            buffer.flip();
+            final byte[] out = new byte[buffer.limit()];
+            buffer.get(out);
+            return out;
+        } catch (BufferOverflowException e) {
+            Log.e(TAG, "Fail to encode domain name and stop encoding", e);
+            return null;
+        }
+    }
+
+    /**
+     * Decode domain name(s) from the given byteBuffer. Decode follows RFC1035 section 3.1 and
+     * section 4.1.4(message compression).
+     *
+     * @return domain name(s) string array with space separated, or empty string if decode fails.
+     */
+    @NonNull
+    public static ArrayList<String> decode(@NonNull final ByteBuffer buffer, boolean compression) {
+        final ArrayList<String> domainList = new ArrayList<>();
+        while (buffer.remaining() > 0) {
+            try {
+                // TODO: replace the recursion with loop in parseName and don't need to pass in the
+                // maxLabelCount parameter to prevent recursion from overflowing stack.
+                final String domain = DnsRecordParser.parseName(buffer, 0 /* depth */,
+                        15 /* maxLabelCount */, compression);
+                if (!DnsRecordParser.isHostName(domain)) continue;
+                domainList.add(domain);
+            } catch (BufferUnderflowException | DnsPacket.ParseException e) {
+                Log.e(TAG, "Fail to parse domain name and stop parsing", e);
+                break;
+            }
+        }
+        return domainList;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/FdEventsReader.java b/staticlibs/device/com/android/net/module/util/FdEventsReader.java
new file mode 100644
index 0000000..f88883b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/FdEventsReader.java
@@ -0,0 +1,294 @@
+/*
+ * 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.net.module.util;
+
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.util.SocketUtils;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+
+/**
+ * This class encapsulates the mechanics of registering a file descriptor
+ * with a thread's Looper and handling read events (and errors).
+ *
+ * Subclasses MUST implement createFd() and SHOULD override handlePacket(). They MAY override
+ * onStop() and onStart().
+ *
+ * Subclasses can expect a call life-cycle like the following:
+ *
+ *     [1] when a client calls start(), createFd() is called, followed by the onStart() hook if all
+ *         goes well. Implementations may override onStart() for additional initialization.
+ *
+ *     [2] yield, waiting for read event or error notification:
+ *
+ *             [a] readPacket() && handlePacket()
+ *
+ *             [b] if (no error):
+ *                     goto 2
+ *                 else:
+ *                     goto 3
+ *
+ *     [3] when a client calls stop(), the onStop() hook is called (unless already stopped or never
+ *         started). Implementations may override onStop() for additional cleanup.
+ *
+ * The packet receive buffer is recycled on every read call, so subclasses
+ * should make any copies they would like inside their handlePacket()
+ * implementation.
+ *
+ * All public methods MUST only be called from the same thread with which
+ * the Handler constructor argument is associated.
+ *
+ * @param <BufferType> the type of the buffer used to read data.
+ */
+public abstract class FdEventsReader<BufferType> {
+    private static final String TAG = FdEventsReader.class.getSimpleName();
+    private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+    private static final int UNREGISTER_THIS_FD = 0;
+
+    @NonNull
+    private final Handler mHandler;
+    @NonNull
+    private final MessageQueue mQueue;
+    @NonNull
+    private final BufferType mBuffer;
+    @Nullable
+    private FileDescriptor mFd;
+    private long mPacketsReceived;
+
+    protected static void closeFd(FileDescriptor fd) {
+        try {
+            SocketUtils.closeSocket(fd);
+        } catch (IOException ignored) {
+        }
+    }
+
+    protected FdEventsReader(@NonNull Handler h, @NonNull BufferType buffer) {
+        mHandler = h;
+        mQueue = mHandler.getLooper().getQueue();
+        mBuffer = buffer;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    protected MessageQueue getMessageQueue() {
+        return mQueue;
+    }
+
+    /** Start this FdEventsReader. */
+    public boolean start() {
+        if (!onCorrectThread()) {
+            throw new IllegalStateException("start() called from off-thread");
+        }
+
+        return createAndRegisterFd();
+    }
+
+    /** Stop this FdEventsReader and destroy the file descriptor. */
+    public void stop() {
+        if (!onCorrectThread()) {
+            throw new IllegalStateException("stop() called from off-thread");
+        }
+
+        unregisterAndDestroyFd();
+    }
+
+    @NonNull
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    protected abstract int recvBufSize(@NonNull BufferType buffer);
+
+    /** Returns the size of the receive buffer. */
+    public int recvBufSize() {
+        return recvBufSize(mBuffer);
+    }
+
+    /**
+     * Get the number of successful calls to {@link #readPacket(FileDescriptor, Object)}.
+     *
+     * <p>A call was successful if {@link #readPacket(FileDescriptor, Object)} returned a value > 0.
+     */
+    public final long numPacketsReceived() {
+        return mPacketsReceived;
+    }
+
+    /**
+     * Subclasses MUST create the listening socket here, including setting all desired socket
+     * options, interface or address/port binding, etc. The socket MUST be created nonblocking.
+     */
+    @Nullable
+    protected abstract FileDescriptor createFd();
+
+    /**
+     * Implementations MUST return the bytes read or throw an Exception.
+     *
+     * <p>The caller may throw a {@link ErrnoException} with {@link OsConstants#EAGAIN} or
+     * {@link OsConstants#EINTR}, in which case {@link FdEventsReader} will ignore the buffer
+     * contents and respectively wait for further input or retry the read immediately. For all other
+     * exceptions, the {@link FdEventsReader} will be stopped with no more interactions with this
+     * method.
+     */
+    protected abstract int readPacket(@NonNull FileDescriptor fd, @NonNull BufferType buffer)
+            throws Exception;
+
+    /**
+     * Called by the main loop for every packet.  Any desired copies of
+     * |recvbuf| should be made in here, as the underlying byte array is
+     * reused across all reads.
+     */
+    protected void handlePacket(@NonNull BufferType recvbuf, int length) {}
+
+    /**
+     * Called by the subclasses of FdEventsReader, decide whether it should stop reading packet or
+     * just ignore the specific error other than EAGAIN or EINTR.
+     *
+     * @return {@code true} if this FdEventsReader should stop reading from the socket.
+     *         {@code false} if it should continue.
+     */
+    protected boolean handleReadError(@NonNull ErrnoException e) {
+        logError("readPacket error: ", e);
+        return true; // by default, stop reading on any error.
+    }
+
+    /**
+     * Called by the subclasses of FdEventsReader, decide whether it should stop reading from the
+     * socket or process the packet and continue to read upon receiving a zero-length packet.
+     *
+     * @return {@code true} if this FdEventsReader should process the zero-length packet.
+     *         {@code false} if it should stop reading from the socket.
+     */
+    protected boolean shouldProcessZeroLengthPacket() {
+        return false; // by default, stop reading upon receiving zero-length packet.
+    }
+
+    /**
+     * Called by the main loop to log errors.  In some cases |e| may be null.
+     */
+    protected void logError(@NonNull String msg, @Nullable Exception e) {}
+
+    /**
+     * Called by start(), if successful, just prior to returning.
+     */
+    protected void onStart() {}
+
+    /**
+     * Called by stop() just prior to returning.
+     */
+    protected void onStop() {}
+
+    private boolean createAndRegisterFd() {
+        if (mFd != null) return true;
+
+        try {
+            mFd = createFd();
+        } catch (Exception e) {
+            logError("Failed to create socket: ", e);
+            closeFd(mFd);
+            mFd = null;
+        }
+
+        if (mFd == null) return false;
+
+        getMessageQueue().addOnFileDescriptorEventListener(
+                mFd,
+                FD_EVENTS,
+                (fd, events) -> {
+                    // Always call handleInput() so read/recvfrom are given
+                    // a proper chance to encounter a meaningful errno and
+                    // perhaps log a useful error message.
+                    if (!isRunning() || !handleInput()) {
+                        unregisterAndDestroyFd();
+                        return UNREGISTER_THIS_FD;
+                    }
+                    return FD_EVENTS;
+                });
+        onStart();
+        return true;
+    }
+
+    protected boolean isRunning() {
+        return (mFd != null) && mFd.valid();
+    }
+
+    // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error.
+    private boolean handleInput() {
+        while (isRunning()) {
+            final int bytesRead;
+
+            try {
+                bytesRead = readPacket(mFd, mBuffer);
+                if (bytesRead == 0 && !shouldProcessZeroLengthPacket()) {
+                    if (isRunning()) logError("Socket closed, exiting", null);
+                    break;
+                }
+                mPacketsReceived++;
+            } catch (ErrnoException e) {
+                if (e.errno == OsConstants.EAGAIN) {
+                    // We've read everything there is to read this time around.
+                    return true;
+                } else if (e.errno == OsConstants.EINTR) {
+                    continue;
+                } else {
+                    if (!isRunning()) break;
+                    final boolean shouldStop = handleReadError(e);
+                    if (shouldStop) break;
+                    continue;
+                }
+            } catch (Exception e) {
+                if (isRunning()) logError("readPacket error: ", e);
+                break;
+            }
+
+            try {
+                handlePacket(mBuffer, bytesRead);
+            } catch (Exception e) {
+                logError("handlePacket error: ", e);
+                Log.wtf(TAG, "Error handling packet", e);
+            }
+        }
+
+        return false;
+    }
+
+    private void unregisterAndDestroyFd() {
+        if (mFd == null) return;
+
+        getMessageQueue().removeOnFileDescriptorEventListener(mFd);
+        closeFd(mFd);
+        mFd = null;
+        onStop();
+    }
+
+    private boolean onCorrectThread() {
+        return (mHandler.getLooper() == Looper.myLooper());
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/FeatureVersions.java b/staticlibs/device/com/android/net/module/util/FeatureVersions.java
new file mode 100644
index 0000000..d5f8124
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/FeatureVersions.java
@@ -0,0 +1,51 @@
+/*
+ * 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.net.module.util;
+
+/**
+ * Class to centralize feature version control that requires a specific module or a specific
+ * module version.
+ * @hide
+ */
+public class FeatureVersions {
+    /**
+     * This constant is used to do bitwise shift operation to create module ids.
+     * The module version is composed with 9 digits which is placed in the lower 36 bits.
+     */
+    private static final int MODULE_SHIFT = 36;
+    /**
+     * The bitmask to do bitwise-and(i.e. {@code &}) operation to get the module id.
+     */
+    public static final long MODULE_MASK = 0xFF0_0000_0000L;
+    /**
+     * The bitmask to do bitwise-and(i.e. {@code &}) operation to get the module version.
+     */
+    public static final long VERSION_MASK = 0x00F_FFFF_FFFFL;
+    public static final long CONNECTIVITY_MODULE_ID = 0x01L << MODULE_SHIFT;
+    public static final long NETWORK_STACK_MODULE_ID = 0x02L << MODULE_SHIFT;
+    // CLAT_ADDRESS_TRANSLATE is a feature of the network stack, which doesn't throw when system
+    // try to add a NAT-T keepalive packet filter with v6 address, introduced in version
+    // M-2023-Sept on July 3rd, 2023.
+    public static final long FEATURE_CLAT_ADDRESS_TRANSLATE =
+            NETWORK_STACK_MODULE_ID + 34_09_00_000L;
+
+    // IS_UID_NETWORKING_BLOCKED is a feature in ConnectivityManager,
+    // which provides an API to access BPF maps to check whether the networking is blocked
+    // by BPF for the given uid and conditions, introduced in version M-2024-Feb on Nov 6, 2023.
+    public static final long FEATURE_IS_UID_NETWORKING_BLOCKED =
+            CONNECTIVITY_MODULE_ID + 34_14_00_000L;
+}
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..4a81c10
--- /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[] getShrinkedBackingArray() {
+        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/staticlibs/device/com/android/net/module/util/HandlerUtils.java b/staticlibs/device/com/android/net/module/util/HandlerUtils.java
new file mode 100644
index 0000000..c620368
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/HandlerUtils.java
@@ -0,0 +1,105 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Helper class for Handler related utilities.
+ *
+ * @hide
+ */
+public class HandlerUtils {
+    /**
+     * Runs the specified task synchronously for dump method.
+     * <p>
+     * If the current thread is the same as the handler thread, then the runnable
+     * runs immediately without being enqueued.  Otherwise, posts the runnable
+     * to the handler and waits for it to complete before returning.
+     * </p><p>
+     * This method is dangerous!  Improper use can result in deadlocks.
+     * Never call this method while any locks are held or use it in a
+     * possibly re-entrant manner.
+     * </p><p>
+     * This method is made to let dump method access members on the handler thread to
+     * avoid concurrent access problems or races.
+     * </p><p>
+     * If timeout occurs then this method returns <code>false</code> but the runnable
+     * will remain posted on the handler and may already be in progress or
+     * complete at a later time.
+     * </p><p>
+     * When using this method, be sure to use {@link Looper#quitSafely} when
+     * quitting the looper.  Otherwise {@link #runWithScissorsForDump} may hang indefinitely.
+     * (TODO: We should fix this by making MessageQueue aware of blocking runnables.)
+     * </p>
+     *
+     * @param h The target handler.
+     * @param r The Runnable that will be executed synchronously.
+     * @param timeout The timeout in milliseconds, or 0 to not wait at all.
+     *
+     * @return Returns true if the Runnable was successfully executed.
+     *         Returns false on failure, usually because the
+     *         looper processing the message queue is exiting.
+     *
+     * @hide
+     */
+    public static boolean runWithScissorsForDump(@NonNull Handler h, @NonNull Runnable r,
+                                                 long timeout) {
+        if (r == null) {
+            throw new IllegalArgumentException("runnable must not be null");
+        }
+        if (timeout < 0) {
+            throw new IllegalArgumentException("timeout must be non-negative");
+        }
+        if (Looper.myLooper() == h.getLooper()) {
+            r.run();
+            return true;
+        }
+
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        // Don't crash in the handler if something in the runnable throws an exception,
+        // but try to propagate the exception to the caller.
+        AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
+        h.post(() -> {
+            try {
+                r.run();
+            } catch (RuntimeException e) {
+                exceptionRef.set(e);
+            }
+            latch.countDown();
+        });
+
+        try {
+            if (!latch.await(timeout, TimeUnit.MILLISECONDS)) {
+                return false;
+            }
+        } catch (InterruptedException e) {
+            exceptionRef.compareAndSet(null, new IllegalStateException("Thread interrupted", e));
+        }
+
+        final RuntimeException e = exceptionRef.get();
+        if (e != null) throw e;
+        return true;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/IBpfMap.java b/staticlibs/device/com/android/net/module/util/IBpfMap.java
new file mode 100644
index 0000000..ca56830
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/IBpfMap.java
@@ -0,0 +1,110 @@
+/*
+ * 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.net.module.util;
+
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+import java.util.NoSuchElementException;
+
+/**
+ * The interface of BpfMap. This could be used to inject for testing.
+ * So the testing code won't load the JNI and update the entries to kernel.
+ *
+ * @param <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+public interface IBpfMap<K extends Struct, V extends Struct> extends AutoCloseable {
+    /** Update an existing or create a new key -> value entry in an eBbpf map. */
+    void updateEntry(K key, V value) throws ErrnoException;
+
+    /** If the key does not exist in the map, insert key -> value entry into eBpf map. */
+    void insertEntry(K key, V value) throws ErrnoException, IllegalStateException;
+
+    /** If the key already exists in the map, replace its value. */
+    void replaceEntry(K key, V value) throws ErrnoException, NoSuchElementException;
+
+    /**
+     * Update an existing or create a new key -> value entry in an eBbpf map. Returns true if
+     * inserted, false if replaced. (use updateEntry() if you don't care whether insert or replace
+     * happened).
+     */
+    boolean insertOrReplaceEntry(K key, V value) throws ErrnoException;
+
+    /** Remove existing key from eBpf map. Return true if something was deleted. */
+    boolean deleteEntry(K key) throws ErrnoException;
+
+    /** Get the key after the passed-in key. */
+    K getNextKey(@NonNull K key) throws ErrnoException;
+
+    /** Get the first key of the eBpf map. */
+    K getFirstKey() throws ErrnoException;
+
+    /** Returns {@code true} if this map contains no elements. */
+    default boolean isEmpty() throws ErrnoException {
+        return getFirstKey() == null;
+    }
+
+    /** Check whether a key exists in the map. */
+    boolean containsKey(@NonNull K key) throws ErrnoException;
+
+    /** Retrieve a value from the map. */
+    V getValue(@NonNull K key) throws ErrnoException;
+
+    public interface ThrowingBiConsumer<T,U> {
+        void accept(T t, U u) throws ErrnoException;
+    }
+
+    /**
+     * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+     * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
+     * other structural modifications to the map, such as adding entries or deleting other entries.
+     * Otherwise, iteration will result in undefined behaviour.
+     */
+    default public void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException {
+        @Nullable K nextKey = getFirstKey();
+
+        while (nextKey != null) {
+            @NonNull final K curKey = nextKey;
+            @NonNull final V value = getValue(curKey);
+
+            nextKey = getNextKey(curKey);
+            action.accept(curKey, value);
+        }
+    }
+
+    /**
+     * Clears the map. The map may already be empty.
+     *
+     * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
+     *                        or if a non-ENOENT error occurred when deleting a key.
+     */
+    default public void clear() throws ErrnoException {
+        K key = getFirstKey();
+        while (key != null) {
+            deleteEntry(key);  // ignores ENOENT.
+            key = getFirstKey();
+        }
+    }
+
+    /** Close for AutoCloseable. */
+    @Override
+    default void close() throws IOException {
+    };
+}
diff --git a/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl b/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
new file mode 100644
index 0000000..72a4a94
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
@@ -0,0 +1,99 @@
+/*
+ * 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.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
+    *
+    * @param netId the network to add the route to
+    * @param route the route to add
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void addRoute(int netId, in RouteInfo route);
+
+   /**
+    * Remove a route for specific network
+    *
+    * @param netId the network to remove the route from
+    * @param route the route to remove
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void removeRoute(int netId, in RouteInfo route);
+
+    /**
+    * Update a route for specific network
+    *
+    * @param netId the network to update the route for
+    * @param route parcelable with route information
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void updateRoute(int netId, in RouteInfo route);
+
+    /**
+     * Adds an interface to a network. The interface must not be assigned to any network, including
+     * the specified network.
+     *
+     * @param netId the network to add the interface to.
+     * @param iface the name of the interface to add.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void addInterfaceToNetwork(int netId, in String iface);
+
+    /**
+     * Removes an interface from a network. The interface must be assigned to the specified network.
+     *
+     * @param netId the network to remove the interface from.
+     * @param iface the name of the interface to remove.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+     void removeInterfaceFromNetwork(int netId, in String iface);
+
+   /**
+    * Add forwarding ip rule
+    *
+    * @param fromIface interface name to add forwarding ip rule
+    * @param toIface interface name to add forwarding ip rule
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void addInterfaceForward(in String fromIface, in String toIface);
+
+   /**
+    * Remove forwarding ip rule
+    *
+    * @param fromIface interface name to remove forwarding ip rule
+    * @param toIface interface name to remove forwarding ip rule
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void removeInterfaceForward(in String fromIface, in String toIface);
+}
diff --git a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
new file mode 100644
index 0000000..497b8cb
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
@@ -0,0 +1,215 @@
+/*
+ * 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.net.module.util;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.IpUtils.icmpv6Checksum;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+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.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+
+import android.net.MacAddress;
+
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.NaHeader;
+import com.android.net.module.util.structs.NsHeader;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.net.module.util.structs.RsHeader;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Utilities for IPv6 packets.
+ */
+public class Ipv6Utils {
+    /**
+     * Build a generic ICMPv6 packet without Ethernet header.
+     */
+    public static ByteBuffer buildIcmpv6Packet(final Inet6Address srcIp, final Inet6Address dstIp,
+            short type, short code, final ByteBuffer... options) {
+        final int ipv6HeaderLen = Struct.getSize(Ipv6Header.class);
+        final int icmpv6HeaderLen = Struct.getSize(Icmpv6Header.class);
+        int payloadLen = 0;
+        for (ByteBuffer option: options) {
+            payloadLen += option.limit();
+        }
+        final ByteBuffer packet = ByteBuffer.allocate(ipv6HeaderLen + icmpv6HeaderLen + payloadLen);
+        final Ipv6Header ipv6Header =
+                new Ipv6Header((int) 0x60000000 /* version, traffic class, flowlabel */,
+                icmpv6HeaderLen + payloadLen /* payload length */,
+                (byte) IPPROTO_ICMPV6 /* next header */, (byte) 0xff /* hop limit */, srcIp, dstIp);
+        final Icmpv6Header icmpv6Header = new Icmpv6Header(type, code, (short) 0 /* checksum */);
+
+        ipv6Header.writeToByteBuffer(packet);
+        icmpv6Header.writeToByteBuffer(packet);
+        for (ByteBuffer option : options) {
+            packet.put(option);
+            // in case option might be reused by caller, restore the position and
+            // limit of bytebuffer.
+            option.clear();
+        }
+        packet.flip();
+
+        // Populate the ICMPv6 checksum field.
+        packet.putShort(ipv6HeaderLen + 2, icmpv6Checksum(packet, 0 /* ipOffset */,
+                ipv6HeaderLen /* transportOffset */,
+                (short) (icmpv6HeaderLen + payloadLen) /* transportLen */));
+        return packet;
+
+    }
+
+    /**
+     * Build a generic ICMPv6 packet(e.g., packet used in the neighbor discovery protocol).
+     */
+    public static ByteBuffer buildIcmpv6Packet(final MacAddress srcMac, final MacAddress dstMac,
+            final Inet6Address srcIp, final Inet6Address dstIp, short type, short code,
+            final ByteBuffer... options) {
+        final ByteBuffer payload = buildIcmpv6Packet(srcIp, dstIp, type, code, options);
+
+        final int etherHeaderLen = Struct.getSize(EthernetHeader.class);
+        final ByteBuffer packet = ByteBuffer.allocate(etherHeaderLen + payload.limit());
+        final EthernetHeader ethHeader =
+                new EthernetHeader(dstMac, srcMac, (short) ETHER_TYPE_IPV6);
+
+        ethHeader.writeToByteBuffer(packet);
+        packet.put(payload);
+        packet.flip();
+
+        return packet;
+    }
+
+    /**
+     * Build the ICMPv6 packet payload including payload header and specific options.
+     */
+    private static ByteBuffer[] buildIcmpv6Payload(final ByteBuffer payloadHeader,
+            final ByteBuffer... options) {
+        final ByteBuffer[] payload = new ByteBuffer[options.length + 1];
+        payload[0] = payloadHeader;
+        System.arraycopy(options, 0, payload, 1, options.length);
+        return payload;
+    }
+
+    /**
+     * Build an ICMPv6 Router Advertisement packet from the required specified parameters.
+     */
+    public static ByteBuffer buildRaPacket(final MacAddress srcMac, final MacAddress dstMac,
+            final Inet6Address srcIp, final Inet6Address dstIp, final byte flags,
+            final int lifetime, final long reachableTime, final long retransTimer,
+            final ByteBuffer... options) {
+        final RaHeader raHeader = new RaHeader((byte) 0 /* hopLimit, unspecified */,
+                flags, lifetime, reachableTime, retransTimer);
+        final ByteBuffer[] payload = buildIcmpv6Payload(
+                ByteBuffer.wrap(raHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+        return buildIcmpv6Packet(srcMac, dstMac, srcIp, dstIp,
+                (byte) ICMPV6_ROUTER_ADVERTISEMENT /* type */, (byte) 0 /* code */, payload);
+    }
+
+    /**
+     * Build an ICMPv6 Neighbor Advertisement packet from the required specified parameters.
+     */
+    public static ByteBuffer buildNaPacket(final MacAddress srcMac, final MacAddress dstMac,
+            final Inet6Address srcIp, final Inet6Address dstIp, final int flags,
+            final Inet6Address target, final ByteBuffer... options) {
+        final NaHeader naHeader = new NaHeader(flags, target);
+        final ByteBuffer[] payload = buildIcmpv6Payload(
+                ByteBuffer.wrap(naHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+        return buildIcmpv6Packet(srcMac, dstMac, srcIp, dstIp,
+                (byte) ICMPV6_NEIGHBOR_ADVERTISEMENT /* type */, (byte) 0 /* code */, payload);
+    }
+
+    /**
+     * Build an ICMPv6 Neighbor Solicitation packet from the required specified parameters.
+     */
+    public static ByteBuffer buildNsPacket(final MacAddress srcMac, final MacAddress dstMac,
+            final Inet6Address srcIp, final Inet6Address dstIp,
+            final Inet6Address target, final ByteBuffer... options) {
+        final NsHeader nsHeader = new NsHeader(target);
+        final ByteBuffer[] payload = buildIcmpv6Payload(
+                ByteBuffer.wrap(nsHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+        return buildIcmpv6Packet(srcMac, dstMac, srcIp, dstIp,
+                (byte) ICMPV6_NEIGHBOR_SOLICITATION /* type */, (byte) 0 /* code */, payload);
+    }
+
+    /**
+     * Build an ICMPv6 Router Solicitation packet from the required specified parameters.
+     */
+    public static ByteBuffer buildRsPacket(final MacAddress srcMac, final MacAddress dstMac,
+            final Inet6Address srcIp, final Inet6Address dstIp, final ByteBuffer... options) {
+        final RsHeader rsHeader = new RsHeader((int) 0 /* reserved */);
+        final ByteBuffer[] payload = buildIcmpv6Payload(
+                ByteBuffer.wrap(rsHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+        return buildIcmpv6Packet(srcMac, dstMac, srcIp, dstIp,
+                (byte) ICMPV6_ROUTER_SOLICITATION /* type */, (byte) 0 /* code */, payload);
+    }
+
+    /**
+     * Build an ICMPv6 Router Solicitation packet from the required specified parameters without
+     * ethernet header.
+     */
+    public static ByteBuffer buildRsPacket(
+            final Inet6Address srcIp, final Inet6Address dstIp, final ByteBuffer... options) {
+        final RsHeader rsHeader = new RsHeader((int) 0 /* reserved */);
+        final ByteBuffer[] payload =
+                buildIcmpv6Payload(
+                        ByteBuffer.wrap(rsHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+        return buildIcmpv6Packet(
+                srcIp,
+                dstIp,
+                (byte) ICMPV6_ROUTER_SOLICITATION /* type */,
+                (byte) 0 /* code */,
+                payload);
+    }
+
+    /**
+     * Build an ICMPv6 Echo Request packet from the required specified parameters.
+     */
+    public static ByteBuffer buildEchoRequestPacket(final MacAddress srcMac,
+            final MacAddress dstMac, final Inet6Address srcIp, final Inet6Address dstIp) {
+        final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
+        return buildIcmpv6Packet(srcMac, dstMac, srcIp, dstIp,
+                (byte) ICMPV6_ECHO_REQUEST_TYPE /* type */, (byte) 0 /* code */, payload);
+    }
+
+    /**
+     * Build an ICMPv6 Echo Request packet from the required specified parameters without ethernet
+     * header.
+     */
+    public static ByteBuffer buildEchoRequestPacket(final Inet6Address srcIp,
+            final Inet6Address dstIp) {
+        final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
+        return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REQUEST_TYPE /* type */,
+                (byte) 0 /* code */,
+                payload);
+    }
+
+    /** Build an ICMPv6 Echo Reply packet without ethernet header. */
+    public static ByteBuffer buildEchoReplyPacket(
+            final Inet6Address srcIp, final Inet6Address dstIp) {
+        final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
+        return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REPLY_TYPE /* type */,
+                (byte) 0 /* code */, payload);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/JniUtil.java b/staticlibs/device/com/android/net/module/util/JniUtil.java
new file mode 100644
index 0000000..5210a3e
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/JniUtil.java
@@ -0,0 +1,35 @@
+/*
+ * 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.net.module.util;
+
+/**
+ * Utilities for modules to use jni.
+ */
+public final class JniUtil {
+    /**
+     * The method to find jni library accroding to the giving package name.
+     *
+     * The jni library name would be packageName + _jni.so. E.g.
+     * com_android_networkstack_tethering_util_jni for tethering,
+     * com_android_connectivity_util_jni for connectivity.
+     */
+    public static String getJniLibraryName(final Package pkg) {
+        final String libPrefix = pkg.getName().replaceAll("\\.", "_");
+
+        return libPrefix + "_jni";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/NetworkMonitorUtils.java b/staticlibs/device/com/android/net/module/util/NetworkMonitorUtils.java
new file mode 100644
index 0000000..5a4412f
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/NetworkMonitorUtils.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+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_OEM_PAID;
+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_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import android.annotation.NonNull;
+import android.net.NetworkCapabilities;
+import android.os.Build;
+
+/** @hide */
+public class NetworkMonitorUtils {
+    // This class is used by both NetworkMonitor and ConnectivityService, so it cannot use
+    // NetworkStack shims, but at the same time cannot use non-system APIs.
+    // TRANSPORT_TEST is test API as of R (so it is enforced to always be 7 and can't be changed),
+    // and it is being added as a system API in S.
+    // TODO: use NetworkCapabilities.TRANSPORT_TEST once NetworkStack builds against API 31.
+    private static final int TRANSPORT_TEST = 7;
+
+    // This class is used by both NetworkMonitor and ConnectivityService, so it cannot use
+    // NetworkStack shims, but at the same time cannot use non-system APIs.
+    // NET_CAPABILITY_NOT_VCN_MANAGED is system API as of S (so it is enforced to always be 28 and
+    // can't be changed).
+    // TODO: use NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED once NetworkStack builds against
+    //       API 31.
+    public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28;
+
+    // Network conditions broadcast constants
+    public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
+            "android.net.conn.NETWORK_CONDITIONS_MEASURED";
+    public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
+    public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
+    public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
+    public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
+    public static final String EXTRA_CELL_ID = "extra_cellid";
+    public static final String EXTRA_SSID = "extra_ssid";
+    public static final String EXTRA_BSSID = "extra_bssid";
+    /** real time since boot */
+    public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
+    public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
+    public static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
+            "android.permission.ACCESS_NETWORK_CONDITIONS";
+
+    /**
+     * Return whether validation is required for private DNS in strict mode.
+     * @param nc Network capabilities of the network to test.
+     */
+    public static boolean isPrivateDnsValidationRequired(@NonNull final NetworkCapabilities nc) {
+        final boolean isVcnManaged = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+                && !nc.hasCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        final boolean isOemPaid = nc.hasCapability(NET_CAPABILITY_OEM_PAID)
+                && nc.hasCapability(NET_CAPABILITY_TRUSTED);
+        final boolean isDefaultCapable = nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                && nc.hasCapability(NET_CAPABILITY_TRUSTED);
+
+        // TODO: Consider requiring validation for DUN networks.
+        if (nc.hasCapability(NET_CAPABILITY_INTERNET)
+                && (isVcnManaged || isOemPaid || isDefaultCapable)) {
+            return true;
+        }
+
+        // Test networks that also have one of the major transport types are attempting to replicate
+        // that transport on a test interface (for example, test ethernet networks with
+        // EthernetManager#setIncludeTestInterfaces). Run validation on them for realistic tests.
+        // See also comments on EthernetManager#setIncludeTestInterfaces and on TestNetworkManager.
+        if (nc.hasTransport(TRANSPORT_TEST) && nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) && (
+                nc.hasTransport(TRANSPORT_WIFI)
+                || nc.hasTransport(TRANSPORT_CELLULAR)
+                || nc.hasTransport(TRANSPORT_BLUETOOTH)
+                || nc.hasTransport(TRANSPORT_ETHERNET))) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Return whether validation is required for a network.
+     * @param isVpnValidationRequired Whether network validation should be performed for VPN
+     *                                networks.
+     * @param nc Network capabilities of the network to test.
+     */
+    public static boolean isValidationRequired(boolean isDunValidationRequired,
+            boolean isVpnValidationRequired,
+            @NonNull final NetworkCapabilities nc) {
+        if (isDunValidationRequired && nc.hasCapability(NET_CAPABILITY_DUN)) {
+            return true;
+        }
+        if (!nc.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+            return isVpnValidationRequired;
+        }
+        return isPrivateDnsValidationRequired(nc);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/PacketBuilder.java b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
new file mode 100644
index 0000000..a2dbd81
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
@@ -0,0 +1,419 @@
+/*
+ * 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.net.module.util;
+
+import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_IPV6;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+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;
+
+import android.net.MacAddress;
+
+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;
+import com.android.net.module.util.structs.UdpHeader;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+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.
+ *
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                Layer 2 header (EthernetHeader)                | (optional)
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |           Layer 3 header (Ipv4Header, Ipv6Header)             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |           Layer 4 header (TcpHeader, UdpHeader)               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                           Payload                             | (optional)
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * Below is a sample code to build a packet.
+ *
+ * // Initialize builder
+ * final ByteBuffer buf = ByteBuffer.allocate(...);
+ * final PacketBuilder pb = new PacketBuilder(buf);
+ * // Write headers
+ * pb.writeL2Header(...);
+ * pb.writeIpHeader(...);
+ * pb.writeTcpHeader(...);
+ * // Write payload
+ * buf.putInt(...);
+ * buf.putShort(...);
+ * buf.putByte(...);
+ * // Finalize and use the packet
+ * pb.finalizePacket();
+ * sendPacket(buf);
+ */
+public class PacketBuilder {
+    private static final int INVALID_OFFSET = -1;
+
+    private final ByteBuffer mBuffer;
+
+    private int mIpv4HeaderOffset = INVALID_OFFSET;
+    private int mIpv6HeaderOffset = INVALID_OFFSET;
+    private int mTcpHeaderOffset = INVALID_OFFSET;
+    private int mUdpHeaderOffset = INVALID_OFFSET;
+
+    public PacketBuilder(@NonNull ByteBuffer buffer) {
+        mBuffer = buffer;
+    }
+
+    /**
+     * Write an ethernet header.
+     *
+     * @param srcMac source MAC address
+     * @param dstMac destination MAC address
+     * @param etherType ether type
+     */
+    public void writeL2Header(MacAddress srcMac, MacAddress dstMac, short etherType) throws
+            IOException {
+        final EthernetHeader ethHeader = new EthernetHeader(dstMac, srcMac, etherType);
+        try {
+            ethHeader.writeToByteBuffer(mBuffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
+    /**
+     * Write an IPv4 header.
+     * The IP header length and checksum are calculated and written back in #finalizePacket.
+     *
+     * @param tos type of service
+     * @param id the identification
+     * @param flagsAndFragmentOffset flags and fragment offset
+     * @param ttl time to live
+     * @param protocol protocol
+     * @param srcIp source IP address
+     * @param dstIp destination IP address
+     */
+    public void writeIpv4Header(byte tos, short id, short flagsAndFragmentOffset, byte ttl,
+            byte protocol, @NonNull final Inet4Address srcIp, @NonNull final Inet4Address dstIp)
+            throws IOException {
+        mIpv4HeaderOffset = mBuffer.position();
+        final Ipv4Header ipv4Header = new Ipv4Header(tos,
+                (short) 0 /* totalLength, calculate in #finalizePacket */, id,
+                flagsAndFragmentOffset, ttl, protocol,
+                (short) 0 /* checksum, calculate in #finalizePacket */, srcIp, dstIp);
+
+        try {
+            ipv4Header.writeToByteBuffer(mBuffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
+    /**
+     * Write an IPv6 header.
+     * The IP header length is calculated and written back in #finalizePacket.
+     *
+     * @param vtf version, traffic class and flow label
+     * @param nextHeader the transport layer protocol
+     * @param hopLimit hop limit
+     * @param srcIp source IP address
+     * @param dstIp destination IP address
+     */
+    public void writeIpv6Header(int vtf, byte nextHeader, short hopLimit,
+            @NonNull final Inet6Address srcIp, @NonNull final Inet6Address dstIp)
+            throws IOException {
+        mIpv6HeaderOffset = mBuffer.position();
+        final Ipv6Header ipv6Header = new Ipv6Header(vtf,
+                (short) 0 /* payloadLength, calculate in #finalizePacket */, nextHeader,
+                hopLimit, srcIp, dstIp);
+
+        try {
+            ipv6Header.writeToByteBuffer(mBuffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
+    /**
+     * Write a TCP header.
+     * The TCP header checksum is calculated and written back in #finalizePacket.
+     *
+     * @param srcPort source port
+     * @param dstPort destination port
+     * @param seq sequence number
+     * @param ack acknowledgement number
+     * @param tcpFlags tcp flags
+     * @param window window size
+     * @param urgentPointer urgent pointer
+     */
+    public void writeTcpHeader(short srcPort, short dstPort, short seq, short ack,
+            byte tcpFlags, short window, short urgentPointer) throws IOException {
+        mTcpHeaderOffset = mBuffer.position();
+        final TcpHeader tcpHeader = new TcpHeader(srcPort, dstPort, seq, ack,
+                (short) ((short) 0x5000 | ((byte) 0x3f & tcpFlags)) /* dataOffsetAndControlBits,
+                dataOffset is always 5(*4bytes) because options not supported */, window,
+                (short) 0 /* checksum, calculate in #finalizePacket */,
+                urgentPointer);
+
+        try {
+            tcpHeader.writeToByteBuffer(mBuffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
+    /**
+     * Write a UDP header.
+     * The UDP header length and checksum are calculated and written back in #finalizePacket.
+     *
+     * @param srcPort source port
+     * @param dstPort destination port
+     */
+    public void writeUdpHeader(short srcPort, short dstPort) throws IOException {
+        mUdpHeaderOffset = mBuffer.position();
+        final UdpHeader udpHeader = new UdpHeader(srcPort, dstPort,
+                (short) 0 /* length, calculate in #finalizePacket */,
+                (short) 0 /* checksum, calculate in #finalizePacket */);
+
+        try {
+            udpHeader.writeToByteBuffer(mBuffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
+    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.
+     *
+     * Call after writing L4 header (no payload) or 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.
+     */
+    @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.
+            mBuffer.putShort(mIpv4HeaderOffset + IPV4_LENGTH_OFFSET,
+                    (short) (mBuffer.position() - mIpv4HeaderOffset));
+
+            // Populate the IPv4 header checksum field.
+            mBuffer.putShort(mIpv4HeaderOffset + IPV4_CHECKSUM_OFFSET,
+                    ipChecksum(mBuffer, mIpv4HeaderOffset /* headerOffset */));
+        } else if (mIpv6HeaderOffset != INVALID_OFFSET) {
+            ipHeaderOffset = mIpv6HeaderOffset;
+
+            // Populate the IPv6 payloadLength field.
+            // The payload length doesn't include IPv6 header length. See rfc8200 section 3.
+            mBuffer.putShort(mIpv6HeaderOffset + IPV6_LEN_OFFSET,
+                    (short) (mBuffer.position() - mIpv6HeaderOffset - IPV6_HEADER_LEN));
+        } else {
+            throw new IOException("Packet is missing neither IPv4 nor IPv6 header");
+        }
+
+        // [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));
+
+            // Populate the UDP header checksum field.
+            mBuffer.putShort(mUdpHeaderOffset + UDP_CHECKSUM_OFFSET, udpChecksum(mBuffer,
+                    ipHeaderOffset /* ipOffset */, mUdpHeaderOffset /* transportOffset */));
+        } else {
+            throw new IOException("Packet has neither TCP nor UDP header");
+        }
+
+        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;
+    }
+
+    /**
+     * Allocate bytebuffer for building the packet.
+     *
+     * @param hasEther has ethernet header. Set this flag to indicate that the packet has an
+     *        ethernet header.
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} and {@code IPPROTO_IPV6}
+     *        currently supported.
+     * @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} and {@code IPPROTO_UDP}
+     *        currently supported.
+     * @param payloadLen length of the payload.
+     */
+    @NonNull
+    public static ByteBuffer allocate(boolean hasEther, int l3proto, int l4proto, int payloadLen) {
+        if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
+            throw new IllegalArgumentException("Unsupported layer 3 protocol " + l3proto);
+        }
+
+        if (l4proto != IPPROTO_TCP && l4proto != IPPROTO_UDP) {
+            throw new IllegalArgumentException("Unsupported layer 4 protocol " + l4proto);
+        }
+
+        if (payloadLen < 0) {
+            throw new IllegalArgumentException("Invalid payload length " + payloadLen);
+        }
+
+        int packetLen = 0;
+        if (hasEther) packetLen += Struct.getSize(EthernetHeader.class);
+        packetLen += (l3proto == IPPROTO_IP) ? Struct.getSize(Ipv4Header.class)
+                : Struct.getSize(Ipv6Header.class);
+        packetLen += (l4proto == IPPROTO_TCP) ? Struct.getSize(TcpHeader.class)
+                : Struct.getSize(UdpHeader.class);
+        packetLen += payloadLen;
+
+        return ByteBuffer.allocate(packetLen);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/PacketReader.java b/staticlibs/device/com/android/net/module/util/PacketReader.java
new file mode 100644
index 0000000..66c4788
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/PacketReader.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+package com.android.net.module.util;
+
+import static java.lang.Math.max;
+
+import android.os.Handler;
+import android.system.Os;
+
+import java.io.FileDescriptor;
+
+/**
+ * Specialization of {@link FdEventsReader} that reads packets into a byte array.
+ *
+ * TODO: rename this class to something more correctly descriptive (something
+ * like [or less horrible than] FdReadEventsHandler?).
+ */
+public abstract class PacketReader extends FdEventsReader<byte[]> {
+
+    public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024;
+
+    protected PacketReader(Handler h) {
+        this(h, DEFAULT_RECV_BUF_SIZE);
+    }
+
+    protected PacketReader(Handler h, int recvBufSize) {
+        super(h, new byte[max(recvBufSize, DEFAULT_RECV_BUF_SIZE)]);
+    }
+
+    @Override
+    protected final int recvBufSize(byte[] buffer) {
+        return buffer.length;
+    }
+
+    /**
+     * Subclasses MAY override this to change the default read() implementation
+     * in favour of, say, recvfrom().
+     *
+     * Implementations MUST return the bytes read or throw an Exception.
+     */
+    @Override
+    protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception {
+        return Os.read(fd, packetBuffer, 0, packetBuffer.length);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
new file mode 100644
index 0000000..02e3643
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
@@ -0,0 +1,157 @@
+/*
+ * 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.net.module.util;
+
+import android.content.Context;
+import android.net.RouteInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A manager class for talking to the routing coordinator service.
+ *
+ * This class should only be used by the connectivity and tethering module. This is enforced
+ * by the build rules. Do not change build rules to gain access to this class from elsewhere.
+ * @hide
+ */
+public class RoutingCoordinatorManager {
+    @NonNull final Context mContext;
+    @NonNull final IRoutingCoordinator mService;
+
+    public RoutingCoordinatorManager(@NonNull final Context context,
+            @NonNull final IBinder binder) {
+        mContext = context;
+        mService = IRoutingCoordinator.Stub.asInterface(binder);
+    }
+
+    /**
+     * Add a route for specific network
+     *
+     * @param netId the network to add the route to
+     * @param route the route to add
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    public void addRoute(final int netId, final RouteInfo route) {
+        try {
+            mService.addRoute(netId, route);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove a route for specific network
+     *
+     * @param netId the network to remove the route from
+     * @param route the route to remove
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    public void removeRoute(final int netId, final RouteInfo route) {
+        try {
+            mService.removeRoute(netId, route);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Update a route for specific network
+     *
+     * @param netId the network to update the route for
+     * @param route parcelable with route information
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    public void updateRoute(final int netId, final RouteInfo route) {
+        try {
+            mService.updateRoute(netId, route);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Adds an interface to a network. The interface must not be assigned to any network, including
+     * the specified network.
+     *
+     * @param netId the network to add the interface to.
+     * @param iface the name of the interface to add.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    public void addInterfaceToNetwork(final int netId, final String iface) {
+        try {
+            mService.addInterfaceToNetwork(netId, iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Removes an interface from a network. The interface must be assigned to the specified network.
+     *
+     * @param netId the network to remove the interface from.
+     * @param iface the name of the interface to remove.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    public void removeInterfaceFromNetwork(final int netId, final String iface) {
+        try {
+            mService.removeInterfaceFromNetwork(netId, iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Add forwarding ip rule
+     *
+     * @param fromIface interface name to add forwarding ip rule
+     * @param toIface interface name to add forwarding ip rule
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    public void addInterfaceForward(final String fromIface, final String toIface) {
+        try {
+            mService.addInterfaceForward(fromIface, toIface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove forwarding ip rule
+     *
+     * @param fromIface interface name to remove forwarding ip rule
+     * @param toIface interface name to remove forwarding ip rule
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    public void removeInterfaceForward(final String fromIface, final String toIface) {
+        try {
+            mService.removeInterfaceForward(fromIface, toIface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
new file mode 100644
index 0000000..c75b860
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
@@ -0,0 +1,228 @@
+/*
+ * 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.net.module.util;
+
+import static com.android.net.module.util.NetdUtils.toRouteInfoParcel;
+
+import android.annotation.NonNull;
+import android.net.INetd;
+
+import android.net.RouteInfo;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Objects;
+
+/**
+ * Class to coordinate routing across multiple clients.
+ *
+ * At present this is just a wrapper for netd methods, but it will be used to host some more
+ * coordination logic in the near future. It can be used to pull up some of the routing logic
+ * from netd into Java land.
+ *
+ * Note that usage of this class is not thread-safe. Clients are responsible for their own
+ * synchronization.
+ */
+public class RoutingCoordinatorService extends IRoutingCoordinator.Stub {
+    private static final String TAG = RoutingCoordinatorService.class.getSimpleName();
+    private final INetd mNetd;
+
+    public RoutingCoordinatorService(@NonNull INetd netd) {
+        mNetd = netd;
+    }
+
+    /**
+     * Add a route for specific network
+     *
+     * @param netId the network to add the route to
+     * @param route the route to add
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    @Override
+    public void addRoute(final int netId, final RouteInfo route)
+            throws ServiceSpecificException, RemoteException {
+        mNetd.networkAddRouteParcel(netId, toRouteInfoParcel(route));
+    }
+
+    /**
+     * Remove a route for specific network
+     *
+     * @param netId the network to remove the route from
+     * @param route the route to remove
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    @Override
+    public void removeRoute(final int netId, final RouteInfo route)
+            throws ServiceSpecificException, RemoteException {
+        mNetd.networkRemoveRouteParcel(netId, toRouteInfoParcel(route));
+    }
+
+    /**
+     * Update a route for specific network
+     *
+     * @param netId the network to update the route for
+     * @param route parcelable with route information
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    @Override
+    public void updateRoute(final int netId, final RouteInfo route)
+            throws ServiceSpecificException, RemoteException {
+        mNetd.networkUpdateRouteParcel(netId, toRouteInfoParcel(route));
+    }
+
+    /**
+     * Adds an interface to a network. The interface must not be assigned to any network, including
+     * the specified network.
+     *
+     * @param netId the network to add the interface to.
+     * @param iface the name of the interface to add.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    @Override
+    public void addInterfaceToNetwork(final int netId, final String iface)
+            throws ServiceSpecificException, RemoteException {
+        mNetd.networkAddInterface(netId, iface);
+    }
+
+    /**
+     * Removes an interface from a network. The interface must be assigned to the specified network.
+     *
+     * @param netId the network to remove the interface from.
+     * @param iface the name of the interface to remove.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    @Override
+    public void removeInterfaceFromNetwork(final int netId, final String iface)
+            throws ServiceSpecificException, RemoteException {
+        mNetd.networkRemoveInterface(netId, iface);
+    }
+
+    private final Object mIfacesLock = new Object();
+    private static final class ForwardingPair {
+        @NonNull public final String fromIface;
+        @NonNull public final String toIface;
+        ForwardingPair(@NonNull final String fromIface, @NonNull final String toIface) {
+            this.fromIface = fromIface;
+            this.toIface = toIface;
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) return true;
+            if (!(o instanceof ForwardingPair)) return false;
+
+            final ForwardingPair that = (ForwardingPair) o;
+
+            return fromIface.equals(that.fromIface) && toIface.equals(that.toIface);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = fromIface.hashCode();
+            result = 2 * result + toIface.hashCode();
+            return result;
+        }
+    }
+
+    @GuardedBy("mIfacesLock")
+    private final ArraySet<ForwardingPair> mForwardedInterfaces = new ArraySet<>();
+
+    /**
+     * Add forwarding ip rule
+     *
+     * @param fromIface interface name to add forwarding ip rule
+     * @param toIface interface name to add forwarding ip rule
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    public void addInterfaceForward(final String fromIface, final String toIface)
+            throws ServiceSpecificException, RemoteException {
+        Objects.requireNonNull(fromIface);
+        Objects.requireNonNull(toIface);
+        Log.i(TAG, "Adding interface forward " + fromIface + " → " + toIface);
+        synchronized (mIfacesLock) {
+            if (mForwardedInterfaces.size() == 0) {
+                mNetd.ipfwdEnableForwarding("RoutingCoordinator");
+            }
+            final ForwardingPair fwp = new ForwardingPair(fromIface, toIface);
+            if (mForwardedInterfaces.contains(fwp)) {
+                // TODO: remove if no reports are observed from the below log
+                Log.wtf(TAG, "Forward already exists between ifaces "
+                        + fromIface + " → " + toIface);
+            }
+            mForwardedInterfaces.add(fwp);
+            // Enables NAT for v4 and filters packets from unknown interfaces
+            mNetd.tetherAddForward(fromIface, toIface);
+            mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
+        }
+    }
+
+    /**
+     * Remove forwarding ip rule
+     *
+     * @param fromIface interface name to remove forwarding ip rule
+     * @param toIface interface name to remove forwarding ip rule
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    public void removeInterfaceForward(final String fromIface, final String toIface)
+            throws ServiceSpecificException, RemoteException {
+        Objects.requireNonNull(fromIface);
+        Objects.requireNonNull(toIface);
+        Log.i(TAG, "Removing interface forward " + fromIface + " → " + toIface);
+        synchronized (mIfacesLock) {
+            final ForwardingPair fwp = new ForwardingPair(fromIface, toIface);
+            if (!mForwardedInterfaces.contains(fwp)) {
+                // This can happen when an upstream was unregisteredAfterReplacement. The forward
+                // is removed immediately when the upstream is destroyed, but later when the
+                // network actually disconnects CS does not know that and it asks for removal
+                // again.
+                // This can also happen if the network was destroyed before being set as an
+                // upstream, because then CS does not set up the forward rules seeing how the
+                // interface was removed anyway.
+                // Either way, this is benign.
+                Log.i(TAG, "No forward set up between interfaces " + fromIface + " → " + toIface);
+                return;
+            }
+            mForwardedInterfaces.remove(fwp);
+            try {
+                mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
+            } catch (RemoteException | ServiceSpecificException e) {
+                Log.e(TAG, "Exception in ipfwdRemoveInterfaceForward", e);
+            }
+            try {
+                mNetd.tetherRemoveForward(fromIface, toIface);
+            } catch (RemoteException | ServiceSpecificException e) {
+                Log.e(TAG, "Exception in tetherRemoveForward", e);
+            }
+            if (mForwardedInterfaces.size() == 0) {
+                mNetd.ipfwdDisableForwarding("RoutingCoordinator");
+            }
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/SharedLog.java b/staticlibs/device/com/android/net/module/util/SharedLog.java
new file mode 100644
index 0000000..6b12c80
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SharedLog.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.time.LocalDateTime;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.StringJoiner;
+
+
+/**
+ * Class to centralize logging functionality for tethering.
+ *
+ * All access to class methods other than dump() must be on the same thread.
+ *
+ * @hide
+ */
+public class SharedLog {
+    private static final int DEFAULT_MAX_RECORDS = 500;
+    private static final String COMPONENT_DELIMITER = ".";
+
+    private enum Category {
+        NONE,
+        ERROR,
+        MARK,
+        WARN,
+        VERBOSE,
+        TERRIBLE,
+    }
+
+    private final LocalLog mLocalLog;
+    // The tag to use for output to the system log. This is not output to the
+    // LocalLog because that would be redundant.
+    private final String mTag;
+    // The component (or subcomponent) of a system that is sharing this log.
+    // This can grow in depth if components call forSubComponent() to obtain
+    // their SharedLog instance. The tag is not included in the component for
+    // brevity.
+    private final String mComponent;
+
+    public SharedLog(String tag) {
+        this(DEFAULT_MAX_RECORDS, tag);
+    }
+
+    public SharedLog(int maxRecords, String tag) {
+        this(new LocalLog(maxRecords), tag, tag);
+    }
+
+    private SharedLog(LocalLog localLog, String tag, String component) {
+        mLocalLog = localLog;
+        mTag = tag;
+        mComponent = component;
+    }
+
+    public String getTag() {
+        return mTag;
+    }
+
+    /**
+     * Create a SharedLog based on this log with an additional component prefix on each logged line.
+     */
+    public SharedLog forSubComponent(String component) {
+        if (!isRootLogInstance()) {
+            component = mComponent + COMPONENT_DELIMITER + component;
+        }
+        return new SharedLog(mLocalLog, mTag, component);
+    }
+
+    /**
+     * Dump the contents of this log.
+     *
+     * <p>This method may be called on any thread.
+     */
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        mLocalLog.dump(writer);
+    }
+
+    /**
+     * Reverse dump the contents of this log.
+     *
+     * <p>This method may be called on any thread.
+     */
+    public void reverseDump(PrintWriter writer) {
+        mLocalLog.reverseDump(writer);
+    }
+
+    //////
+    // Methods that both log an entry and emit it to the system log.
+    //////
+
+    /**
+     * Log an error due to an exception. This does not include the exception stacktrace.
+     *
+     * <p>The log entry will be also added to the system log.
+     * @see #e(String, Throwable)
+     */
+    public void e(Exception e) {
+        Log.e(mTag, record(Category.ERROR, e.toString()));
+    }
+
+    /**
+     * Log an error message.
+     *
+     * <p>The log entry will be also added to the system log.
+     */
+    public void e(String msg) {
+        Log.e(mTag, record(Category.ERROR, msg));
+    }
+
+    /**
+     * Log an error due to an exception, with the exception stacktrace if provided.
+     *
+     * <p>The error and exception message appear in the shared log, but the stacktrace is only
+     * logged in general log output (logcat). The log entry will be also added to the system log.
+     */
+    public void e(@NonNull String msg, @Nullable Throwable exception) {
+        if (exception == null) {
+            e(msg);
+            return;
+        }
+        Log.e(mTag, record(Category.ERROR, msg + ": " + exception.getMessage()), exception);
+    }
+
+    /**
+     * Log an informational message.
+     *
+     * <p>The log entry will be also added to the system log.
+     */
+    public void i(String msg) {
+        Log.i(mTag, record(Category.NONE, msg));
+    }
+
+    /**
+     * Log a warning message.
+     *
+     * <p>The log entry will be also added to the system log.
+     */
+    public void w(String msg) {
+        Log.w(mTag, record(Category.WARN, msg));
+    }
+
+    /**
+     * Log a verbose message.
+     *
+     * <p>The log entry will be also added to the system log.
+     */
+    public void v(String msg) {
+        Log.v(mTag, record(Category.VERBOSE, msg));
+    }
+
+    /**
+     * Log a terrible failure message.
+     *
+     * <p>The log entry will be also added to the system log and will trigger system reporting
+     * for terrible failures.
+     */
+    public void wtf(String msg) {
+        Log.wtf(mTag, record(Category.TERRIBLE, msg));
+    }
+
+    /**
+     * Log a terrible failure due to an exception, with the exception stacktrace if provided.
+     *
+     * <p>The error and exception message appear in the shared log, but the stacktrace is only
+     * logged in general log output (logcat). The log entry will be also added to the system log
+     * and will trigger system reporting for terrible failures.
+     */
+    public void wtf(@NonNull String msg, @Nullable Throwable exception) {
+        if (exception == null) {
+            e(msg);
+            return;
+        }
+        Log.wtf(mTag, record(Category.TERRIBLE, msg + ": " + exception.getMessage()), exception);
+    }
+
+
+    //////
+    // Methods that only log an entry (and do NOT emit to the system log).
+    //////
+
+    /**
+     * Log a general message to be only included in the in-memory log.
+     *
+     * <p>The log entry will *not* be added to the system log.
+     */
+    public void log(String msg) {
+        record(Category.NONE, msg);
+    }
+
+    /**
+     * Log a general, formatted message to be only included in the in-memory log.
+     *
+     * <p>The log entry will *not* be added to the system log.
+     * @see String#format(String, Object...)
+     */
+    public void logf(String fmt, Object... args) {
+        log(String.format(fmt, args));
+    }
+
+    /**
+     * Log a message with MARK level.
+     *
+     * <p>The log entry will *not* be added to the system log.
+     */
+    public void mark(String msg) {
+        record(Category.MARK, msg);
+    }
+
+    private String record(Category category, String msg) {
+        final String entry = logLine(category, msg);
+        mLocalLog.append(entry);
+        return entry;
+    }
+
+    private String logLine(Category category, String msg) {
+        final StringJoiner sj = new StringJoiner(" ");
+        if (!isRootLogInstance()) sj.add("[" + mComponent + "]");
+        if (category != Category.NONE) sj.add(category.toString());
+        return sj.add(msg).toString();
+    }
+
+    // Check whether this SharedLog instance is nominally the top level in
+    // a potential hierarchy of shared logs (the root of a tree),
+    // or is a subcomponent within the hierarchy.
+    private boolean isRootLogInstance() {
+        return TextUtils.isEmpty(mComponent) || mComponent.equals(mTag);
+    }
+
+    private static final class LocalLog {
+        private final Deque<String> mLog;
+        private final int mMaxLines;
+
+        LocalLog(int maxLines) {
+            mMaxLines = Math.max(0, maxLines);
+            mLog = new ArrayDeque<>(mMaxLines);
+        }
+
+        synchronized void append(String logLine) {
+            if (mMaxLines <= 0) return;
+            while (mLog.size() >= mMaxLines) {
+                mLog.remove();
+            }
+            mLog.add(LocalDateTime.now() + " - " + logLine);
+        }
+
+        /**
+         * Dumps the content of local log to print writer with each log entry
+         *
+         * @param pw printer writer to write into
+         */
+        synchronized void dump(PrintWriter pw) {
+            for (final String s : mLog) {
+                pw.println(s);
+            }
+        }
+
+        synchronized void reverseDump(PrintWriter pw) {
+            final Iterator<String> itr = mLog.descendingIterator();
+            while (itr.hasNext()) {
+                pw.println(itr.next());
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000..51671a6
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SocketUtils.java
@@ -0,0 +1,63 @@
+/*
+ * 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.net.module.util;
+
+import static android.net.util.SocketUtils.closeSocket;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.system.NetlinkSocketAddress;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.SocketAddress;
+
+/**
+ * Collection of utilities to interact with raw sockets.
+ *
+ * This class also provides utilities to interact with {@link android.net.util.SocketUtils}
+ * because it is in the API surface could not be simply move the frameworks/libs/net/
+ * to share with module.
+ *
+ * TODO: deprecate android.net.util.SocketUtils and replace with this class.
+ */
+public class SocketUtils {
+
+    /**
+     * Make a socket address to communicate with netlink.
+     */
+    // 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);
+    }
+
+    /**
+     * Close a socket, ignoring any exception while closing.
+     */
+    public static void closeSocketQuietly(FileDescriptor fd) {
+        try {
+            closeSocket(fd);
+        } catch (IOException ignored) {
+        }
+    }
+
+    private SocketUtils() {}
+}
diff --git a/staticlibs/device/com/android/net/module/util/Struct.java b/staticlibs/device/com/android/net/module/util/Struct.java
new file mode 100644
index 0000000..ff7a711
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/Struct.java
@@ -0,0 +1,790 @@
+/*
+ * 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.net.MacAddress;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.math.BigInteger;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Define a generic class that helps to parse the structured message.
+ *
+ * Example usage:
+ *
+ *    // C-style NduserOption message header definition in the kernel:
+ *    struct nduseroptmsg {
+ *        unsigned char nduseropt_family;
+ *        unsigned char nduseropt_pad1;
+ *        unsigned short nduseropt_opts_len;
+ *        int nduseropt_ifindex;
+ *        __u8 nduseropt_icmp_type;
+ *        __u8 nduseropt_icmp_code;
+ *        unsigned short nduseropt_pad2;
+ *        unsigned int nduseropt_pad3;
+ *    }
+ *
+ *    - Declare a subclass with explicit constructor or not which extends from this class to parse
+ *      NduserOption header from raw bytes array.
+ *
+ *    - Option w/ explicit constructor:
+ *      static class NduserOptHeaderMessage extends Struct {
+ *          @Field(order = 0, type = Type.U8, padding = 1)
+ *          final short family;
+ *          @Field(order = 1, type = Type.U16)
+ *          final int len;
+ *          @Field(order = 2, type = Type.S32)
+ *          final int ifindex;
+ *          @Field(order = 3, type = Type.U8)
+ *          final short type;
+ *          @Field(order = 4, type = Type.U8, padding = 6)
+ *          final short code;
+ *
+ *          NduserOptHeaderMessage(final short family, final int len, final int ifindex,
+ *                  final short type, final short code) {
+ *              this.family = family;
+ *              this.len = len;
+ *              this.ifindex = ifindex;
+ *              this.type = type;
+ *              this.code = code;
+ *          }
+ *      }
+ *
+ *      - Option w/o explicit constructor:
+ *        static class NduserOptHeaderMessage extends Struct {
+ *            @Field(order = 0, type = Type.U8, padding = 1)
+ *            short family;
+ *            @Field(order = 1, type = Type.U16)
+ *            int len;
+ *            @Field(order = 2, type = Type.S32)
+ *            int ifindex;
+ *            @Field(order = 3, type = Type.U8)
+ *            short type;
+ *            @Field(order = 4, type = Type.U8, padding = 6)
+ *            short code;
+ *        }
+ *
+ *    - Parse the target message and refer the members.
+ *      final ByteBuffer buf = ByteBuffer.wrap(RAW_BYTES_ARRAY);
+ *      buf.order(ByteOrder.nativeOrder());
+ *      final NduserOptHeaderMessage nduserHdrMsg = Struct.parse(NduserOptHeaderMessage.class, buf);
+ *      assertEquals(10, nduserHdrMsg.family);
+ */
+public class Struct {
+    public enum Type {
+        U8,          // unsigned byte,  size = 1 byte
+        U16,         // unsigned short, size = 2 bytes
+        U32,         // unsigned int,   size = 4 bytes
+        U63,         // unsigned long(MSB: 0), size = 8 bytes
+        U64,         // unsigned long,  size = 8 bytes
+        S8,          // signed byte,    size = 1 byte
+        S16,         // signed short,   size = 2 bytes
+        S32,         // signed int,     size = 4 bytes
+        S64,         // signed long,    size = 8 bytes
+        UBE16,       // unsigned short in network order, size = 2 bytes
+        UBE32,       // unsigned int in network order,   size = 4 bytes
+        UBE63,       // unsigned long(MSB: 0) in network order, size = 8 bytes
+        UBE64,       // unsigned long in network order,  size = 8 bytes
+        ByteArray,   // byte array with predefined length
+        EUI48,       // IEEE Extended Unique Identifier, a 48-bits long MAC address in network order
+        Ipv4Address, // IPv4 address in network order
+        Ipv6Address, // IPv6 address in network order
+    }
+
+    /**
+     * Indicate that the field marked with this annotation will automatically be managed by this
+     * class (e.g., will be parsed by #parse).
+     *
+     * order:     The placeholder associated with each field, consecutive order starting from zero.
+     * type:      The primitive data type listed in above Type enumeration.
+     * padding:   Padding bytes appear after the field for alignment.
+     * arraysize: The length of byte array.
+     *
+     * Annotation associated with field MUST have order and type properties at least, padding
+     * and arraysize properties depend on the specific usage, if these properties are absent,
+     * then default value 0 will be applied.
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.FIELD)
+    public @interface Field {
+        int order();
+        Type type();
+        int padding() default 0;
+        int arraysize() default 0;
+    }
+
+    /**
+     * Indicates that this field contains a computed value and is ignored for the purposes of Struct
+     * parsing.
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.FIELD)
+    public @interface Computed {}
+
+    private static class FieldInfo {
+        @NonNull
+        public final Field annotation;
+        @NonNull
+        public final java.lang.reflect.Field field;
+
+        FieldInfo(final Field annotation, final java.lang.reflect.Field field) {
+            this.annotation = annotation;
+            this.field = field;
+        }
+    }
+    private static ConcurrentHashMap<Class, FieldInfo[]> sFieldCache = new ConcurrentHashMap();
+
+    private static void checkAnnotationType(final Field annotation, final Class fieldType) {
+        switch (annotation.type()) {
+            case U8:
+            case S16:
+                if (fieldType == Short.TYPE) return;
+                break;
+            case U16:
+            case S32:
+            case UBE16:
+                if (fieldType == Integer.TYPE) return;
+                break;
+            case U32:
+            case U63:
+            case S64:
+            case UBE32:
+            case UBE63:
+                if (fieldType == Long.TYPE) return;
+                break;
+            case U64:
+            case UBE64:
+                if (fieldType == BigInteger.class) return;
+                break;
+            case S8:
+                if (fieldType == Byte.TYPE) return;
+                break;
+            case ByteArray:
+                if (fieldType != byte[].class) break;
+                if (annotation.arraysize() <= 0) {
+                    throw new IllegalArgumentException("Invalid ByteArray size: "
+                            + annotation.arraysize());
+                }
+                return;
+            case EUI48:
+                if (fieldType == MacAddress.class) return;
+                break;
+            case Ipv4Address:
+                if (fieldType == Inet4Address.class) return;
+                break;
+            case Ipv6Address:
+                if (fieldType == Inet6Address.class) return;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown type" + annotation.type());
+        }
+        throw new IllegalArgumentException("Invalid primitive data type: " + fieldType
+                + " for annotation type: " + annotation.type());
+    }
+
+    private static int getFieldLength(final Field annotation) {
+        int length = 0;
+        switch (annotation.type()) {
+            case U8:
+            case S8:
+                length = 1;
+                break;
+            case U16:
+            case S16:
+            case UBE16:
+                length = 2;
+                break;
+            case U32:
+            case S32:
+            case UBE32:
+                length = 4;
+                break;
+            case U63:
+            case U64:
+            case S64:
+            case UBE63:
+            case UBE64:
+                length = 8;
+                break;
+            case ByteArray:
+                length = annotation.arraysize();
+                break;
+            case EUI48:
+                length = 6;
+                break;
+            case Ipv4Address:
+                length = 4;
+                break;
+            case Ipv6Address:
+                length = 16;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown type" + annotation.type());
+        }
+        return length + annotation.padding();
+    }
+
+    private static boolean isStructSubclass(final Class clazz) {
+        return clazz != null && Struct.class.isAssignableFrom(clazz) && Struct.class != clazz;
+    }
+
+    private static int getAnnotationFieldCount(final Class clazz) {
+        int count = 0;
+        for (java.lang.reflect.Field field : clazz.getDeclaredFields()) {
+            if (field.isAnnotationPresent(Field.class)) count++;
+        }
+        return count;
+    }
+
+    private static boolean allFieldsFinal(final FieldInfo[] fields, boolean immutable) {
+        for (FieldInfo fi : fields) {
+            if (Modifier.isFinal(fi.field.getModifiers()) != immutable) return false;
+        }
+        return true;
+    }
+
+    private static boolean hasBothMutableAndImmutableFields(final FieldInfo[] fields) {
+        return !allFieldsFinal(fields, true /* immutable */)
+                && !allFieldsFinal(fields, false /* mutable */);
+    }
+
+    private static boolean matchConstructor(final Constructor cons, final FieldInfo[] fields) {
+        final Class[] paramTypes = cons.getParameterTypes();
+        if (paramTypes.length != fields.length) return false;
+        for (int i = 0; i < paramTypes.length; i++) {
+            if (!paramTypes[i].equals(fields[i].field.getType())) return false;
+        }
+        return true;
+    }
+
+    /**
+     * Read U64/UBE64 type data from ByteBuffer and output a BigInteger instance.
+     *
+     * @param buf The byte buffer to read.
+     * @param type The annotation type.
+     *
+     * The magnitude argument of BigInteger constructor is a byte array in big-endian order.
+     * If BigInteger data is read from the byte buffer in little-endian, reverse the order of
+     * the bytes is required; if BigInteger data is read from the byte buffer in big-endian,
+     * then just keep it as-is.
+     */
+    private static BigInteger readBigInteger(final ByteBuffer buf, final Type type) {
+        final byte[] input = new byte[8];
+        boolean reverseBytes = (type == Type.U64 && buf.order() == ByteOrder.LITTLE_ENDIAN);
+        for (int i = 0; i < 8; i++) {
+            input[reverseBytes ? input.length - 1 - i : i] = buf.get();
+        }
+        return new BigInteger(1, input);
+    }
+
+    /**
+     * Get the last 8 bytes of a byte array. If there are less than 8 bytes,
+     * the first bytes are replaced with zeroes.
+     */
+    private static byte[] getLast8Bytes(final byte[] input) {
+        final byte[] tmp = new byte[8];
+        System.arraycopy(
+                input,
+                Math.max(0, input.length - 8), // srcPos: read at most last 8 bytes
+                tmp,
+                Math.max(0, 8 - input.length), // dstPos: pad output with that many zeroes
+                Math.min(8, input.length));    // length
+        return tmp;
+    }
+
+    /**
+     * Convert U64/UBE64 type data interpreted by BigInteger class to bytes array, output are
+     * always 8 bytes.
+     *
+     * @param bigInteger The number to convert.
+     * @param order Indicate ByteBuffer is read as little-endian or big-endian.
+     * @param type The annotation U64 type.
+     *
+     * BigInteger#toByteArray returns a byte array containing the 2's complement representation
+     * of this BigInteger, in big-endian. If annotation type is U64 and ByteBuffer is read as
+     * little-endian, then reversing the order of the bytes is required.
+     */
+    private static byte[] bigIntegerToU64Bytes(final BigInteger bigInteger, final ByteOrder order,
+            final Type type) {
+        final byte[] bigIntegerBytes = bigInteger.toByteArray();
+        final byte[] output = getLast8Bytes(bigIntegerBytes);
+
+        if (type == Type.U64 && order == ByteOrder.LITTLE_ENDIAN) {
+            for (int i = 0; i < 4; i++) {
+                byte tmp = output[i];
+                output[i] = output[7 - i];
+                output[7 - i] = tmp;
+            }
+        }
+        return output;
+    }
+
+    private static Object getFieldValue(final ByteBuffer buf, final FieldInfo fieldInfo)
+            throws BufferUnderflowException {
+        final Object value;
+        checkAnnotationType(fieldInfo.annotation, fieldInfo.field.getType());
+        switch (fieldInfo.annotation.type()) {
+            case U8:
+                value = (short) (buf.get() & 0xFF);
+                break;
+            case U16:
+                value = (int) (buf.getShort() & 0xFFFF);
+                break;
+            case U32:
+                value = (long) (buf.getInt() & 0xFFFFFFFFL);
+                break;
+            case U64:
+                value = readBigInteger(buf, Type.U64);
+                break;
+            case S8:
+                value = buf.get();
+                break;
+            case S16:
+                value = buf.getShort();
+                break;
+            case S32:
+                value = buf.getInt();
+                break;
+            case U63:
+            case S64:
+                value = buf.getLong();
+                break;
+            case UBE16:
+                if (buf.order() == ByteOrder.LITTLE_ENDIAN) {
+                    value = (int) (Short.reverseBytes(buf.getShort()) & 0xFFFF);
+                } else {
+                    value = (int) (buf.getShort() & 0xFFFF);
+                }
+                break;
+            case UBE32:
+                if (buf.order() == ByteOrder.LITTLE_ENDIAN) {
+                    value = (long) (Integer.reverseBytes(buf.getInt()) & 0xFFFFFFFFL);
+                } else {
+                    value = (long) (buf.getInt() & 0xFFFFFFFFL);
+                }
+                break;
+            case UBE63:
+                if (buf.order() == ByteOrder.LITTLE_ENDIAN) {
+                    value = Long.reverseBytes(buf.getLong());
+                } else {
+                    value = buf.getLong();
+                }
+                break;
+            case UBE64:
+                value = readBigInteger(buf, Type.UBE64);
+                break;
+            case ByteArray:
+                final byte[] array = new byte[fieldInfo.annotation.arraysize()];
+                buf.get(array);
+                value = array;
+                break;
+            case EUI48:
+                final byte[] macAddress = new byte[6];
+                buf.get(macAddress);
+                value = MacAddress.fromBytes(macAddress);
+                break;
+            case Ipv4Address:
+            case Ipv6Address:
+                final boolean isIpv6 = (fieldInfo.annotation.type() == Type.Ipv6Address);
+                final byte[] address = new byte[isIpv6 ? 16 : 4];
+                buf.get(address);
+                try {
+                    if (isIpv6) {
+                        // Using Inet6Address.getByAddress since InetAddress.getByAddress converts
+                        // v4-mapped v6 address to v4 address internally and returns Inet4Address.
+                        value = Inet6Address.getByAddress(
+                                null /* host */, address, -1 /* scope_id */);
+                    } else {
+                        value = InetAddress.getByAddress(address);
+                    }
+                } catch (UnknownHostException e) {
+                    throw new IllegalArgumentException("illegal length of IP address", e);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown type:" + fieldInfo.annotation.type());
+        }
+
+        // Skip the padding data for alignment if any.
+        if (fieldInfo.annotation.padding() > 0) {
+            buf.position(buf.position() + fieldInfo.annotation.padding());
+        }
+        return value;
+    }
+
+    @Nullable
+    private Object getFieldValue(@NonNull java.lang.reflect.Field field) {
+        try {
+            return field.get(this);
+        } catch (IllegalAccessException e) {
+            throw new IllegalStateException("Cannot access field: " + field, e);
+        }
+    }
+
+    private static void putFieldValue(final ByteBuffer output, final FieldInfo fieldInfo,
+            final Object value) throws BufferUnderflowException {
+        switch (fieldInfo.annotation.type()) {
+            case U8:
+                output.put((byte) (((short) value) & 0xFF));
+                break;
+            case U16:
+                output.putShort((short) (((int) value) & 0xFFFF));
+                break;
+            case U32:
+                output.putInt((int) (((long) value) & 0xFFFFFFFFL));
+                break;
+            case U63:
+                output.putLong((long) value);
+                break;
+            case U64:
+                output.put(bigIntegerToU64Bytes((BigInteger) value, output.order(), Type.U64));
+                break;
+            case S8:
+                output.put((byte) value);
+                break;
+            case S16:
+                output.putShort((short) value);
+                break;
+            case S32:
+                output.putInt((int) value);
+                break;
+            case S64:
+                output.putLong((long) value);
+                break;
+            case UBE16:
+                if (output.order() == ByteOrder.LITTLE_ENDIAN) {
+                    output.putShort(Short.reverseBytes((short) (((int) value) & 0xFFFF)));
+                } else {
+                    output.putShort((short) (((int) value) & 0xFFFF));
+                }
+                break;
+            case UBE32:
+                if (output.order() == ByteOrder.LITTLE_ENDIAN) {
+                    output.putInt(Integer.reverseBytes(
+                            (int) (((long) value) & 0xFFFFFFFFL)));
+                } else {
+                    output.putInt((int) (((long) value) & 0xFFFFFFFFL));
+                }
+                break;
+            case UBE63:
+                if (output.order() == ByteOrder.LITTLE_ENDIAN) {
+                    output.putLong(Long.reverseBytes((long) value));
+                } else {
+                    output.putLong((long) value);
+                }
+                break;
+            case UBE64:
+                output.put(bigIntegerToU64Bytes((BigInteger) value, output.order(), Type.UBE64));
+                break;
+            case ByteArray:
+                checkByteArraySize((byte[]) value, fieldInfo);
+                output.put((byte[]) value);
+                break;
+            case EUI48:
+                final byte[] macAddress = ((MacAddress) value).toByteArray();
+                output.put(macAddress);
+                break;
+            case Ipv4Address:
+            case Ipv6Address:
+                final byte[] address = ((InetAddress) value).getAddress();
+                output.put(address);
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown type:" + fieldInfo.annotation.type());
+        }
+
+        // padding zero after field value for alignment.
+        for (int i = 0; i < fieldInfo.annotation.padding(); i++) output.put((byte) 0);
+    }
+
+    private static FieldInfo[] getClassFieldInfo(final Class clazz) {
+        if (!isStructSubclass(clazz)) {
+            throw new IllegalArgumentException(clazz.getName() + " is not a subclass of "
+                    + Struct.class.getName() + ", its superclass is "
+                    + clazz.getSuperclass().getName());
+        }
+
+        final FieldInfo[] cachedAnnotationFields = sFieldCache.get(clazz);
+        if (cachedAnnotationFields != null) {
+            return cachedAnnotationFields;
+        }
+
+        // Since array returned from Class#getDeclaredFields doesn't guarantee the actual order
+        // of field appeared in the class, that is a problem when parsing raw data read from
+        // ByteBuffer. Store the fields appeared by the order() defined in the Field annotation.
+        final FieldInfo[] annotationFields = new FieldInfo[getAnnotationFieldCount(clazz)];
+        for (java.lang.reflect.Field field : clazz.getDeclaredFields()) {
+            if (Modifier.isStatic(field.getModifiers())) continue;
+            if (field.getAnnotation(Computed.class) != null) continue;
+
+            final Field annotation = field.getAnnotation(Field.class);
+            if (annotation == null) {
+                throw new IllegalArgumentException("Field " + field.getName()
+                        + " is missing the " + Field.class.getSimpleName()
+                        + " annotation");
+            }
+            if (annotation.order() < 0 || annotation.order() >= annotationFields.length) {
+                throw new IllegalArgumentException("Annotation order: " + annotation.order()
+                        + " is negative or non-consecutive");
+            }
+            if (annotationFields[annotation.order()] != null) {
+                throw new IllegalArgumentException("Duplicated annotation order: "
+                        + annotation.order());
+            }
+            annotationFields[annotation.order()] = new FieldInfo(annotation, field);
+        }
+        sFieldCache.putIfAbsent(clazz, annotationFields);
+        return annotationFields;
+    }
+
+    /**
+     * Parse raw data from ByteBuffer according to the pre-defined annotation rule and return
+     * the type-variable object which is subclass of Struct class.
+     *
+     * TODO:
+     * 1. Support subclass inheritance.
+     * 2. Introduce annotation processor to enforce the subclass naming schema.
+     */
+    public static <T> T parse(final Class<T> clazz, final ByteBuffer buf) {
+        try {
+            final FieldInfo[] foundFields = getClassFieldInfo(clazz);
+            if (hasBothMutableAndImmutableFields(foundFields)) {
+                throw new IllegalArgumentException("Class has both final and non-final fields");
+            }
+
+            Constructor<?> constructor = null;
+            Constructor<?> defaultConstructor = null;
+            final Constructor<?>[] constructors = clazz.getDeclaredConstructors();
+            for (Constructor cons : constructors) {
+                if (matchConstructor(cons, foundFields)) constructor = cons;
+                if (cons.getParameterTypes().length == 0) defaultConstructor = cons;
+            }
+
+            if (constructor == null && defaultConstructor == null) {
+                throw new IllegalArgumentException("Fail to find available constructor");
+            }
+            if (constructor != null) {
+                final Object[] args = new Object[foundFields.length];
+                for (int i = 0; i < args.length; i++) {
+                    args[i] = getFieldValue(buf, foundFields[i]);
+                }
+                return (T) constructor.newInstance(args);
+            }
+
+            final Object instance = defaultConstructor.newInstance();
+            for (FieldInfo fi : foundFields) {
+                fi.field.set(instance, getFieldValue(buf, fi));
+            }
+            return (T) instance;
+        } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
+            throw new IllegalArgumentException("Fail to create a instance from constructor", e);
+        } catch (BufferUnderflowException e) {
+            throw new IllegalArgumentException("Fail to read raw data from ByteBuffer", e);
+        }
+    }
+
+    private static int getSizeInternal(final FieldInfo[] fieldInfos) {
+        int size = 0;
+        for (FieldInfo fi : fieldInfos) {
+            size += getFieldLength(fi.annotation);
+        }
+        return size;
+    }
+
+    // Check whether the actual size of byte array matches the array size declared in
+    // annotation. For other annotation types, the actual length of field could be always
+    // deduced from annotation correctly.
+    private static void checkByteArraySize(@Nullable final byte[] array,
+            @NonNull final FieldInfo fieldInfo) {
+        Objects.requireNonNull(array, "null byte array for field " + fieldInfo.field.getName());
+        int annotationArraySize = fieldInfo.annotation.arraysize();
+        if (array.length == annotationArraySize) return;
+        throw new IllegalStateException("byte array actual length: "
+                + array.length + " doesn't match the declared array size: " + annotationArraySize);
+    }
+
+    private void writeToByteBufferInternal(final ByteBuffer output, final FieldInfo[] fieldInfos) {
+        for (FieldInfo fi : fieldInfos) {
+            final Object value = getFieldValue(fi.field);
+            try {
+                putFieldValue(output, fi, value);
+            } catch (BufferUnderflowException e) {
+                throw new IllegalArgumentException("Fail to fill raw data to ByteBuffer", e);
+            }
+        }
+    }
+
+    /**
+     * Get the size of Struct subclass object.
+     */
+    public static <T extends Struct> int getSize(final Class<T> clazz) {
+        final FieldInfo[] fieldInfos = getClassFieldInfo(clazz);
+        return getSizeInternal(fieldInfos);
+    }
+
+    /**
+     * Convert the parsed Struct subclass object to ByteBuffer.
+     *
+     * @param output ByteBuffer passed-in from the caller.
+     */
+    public final void writeToByteBuffer(final ByteBuffer output) {
+        final FieldInfo[] fieldInfos = getClassFieldInfo(this.getClass());
+        writeToByteBufferInternal(output, fieldInfos);
+    }
+
+    /**
+     * Convert the parsed Struct subclass object to byte array.
+     *
+     * @param order indicate ByteBuffer is outputted as little-endian or big-endian.
+     */
+    public final byte[] writeToBytes(final ByteOrder order) {
+        final FieldInfo[] fieldInfos = getClassFieldInfo(this.getClass());
+        final byte[] output = new byte[getSizeInternal(fieldInfos)];
+        final ByteBuffer buffer = ByteBuffer.wrap(output);
+        buffer.order(order);
+        writeToByteBufferInternal(buffer, fieldInfos);
+        return output;
+    }
+
+    /** Convert the parsed Struct subclass object to byte array with native order. */
+    public final byte[] writeToBytes() {
+        return writeToBytes(ByteOrder.nativeOrder());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || this.getClass() != obj.getClass()) return false;
+
+        final FieldInfo[] fieldInfos = getClassFieldInfo(this.getClass());
+        for (int i = 0; i < fieldInfos.length; i++) {
+            try {
+                final Object value = fieldInfos[i].field.get(this);
+                final Object otherValue = fieldInfos[i].field.get(obj);
+
+                // Use Objects#deepEquals because the equals method on arrays does not check the
+                // contents of the array. The only difference between Objects#deepEquals and
+                // Objects#equals is that the former will call Arrays#deepEquals when comparing
+                // arrays. In turn, the only difference between Arrays#deepEquals is that it
+                // supports nested arrays. Struct does not currently support these, and if it did,
+                // Objects#deepEquals might be more correct.
+                if (!Objects.deepEquals(value, otherValue)) return false;
+            } catch (IllegalAccessException e) {
+                throw new IllegalStateException("Cannot access field: " + fieldInfos[i].field, e);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        final FieldInfo[] fieldInfos = getClassFieldInfo(this.getClass());
+        final Object[] values = new Object[fieldInfos.length];
+        for (int i = 0; i < fieldInfos.length; i++) {
+            final Object value = getFieldValue(fieldInfos[i].field);
+            // For byte array field, put the hash code generated based on the array content into
+            // the Object array instead of the reference to byte array, which might change and cause
+            // to get a different hash code even with the exact same elements.
+            if (fieldInfos[i].field.getType() == byte[].class) {
+                values[i] = Arrays.hashCode((byte[]) value);
+            } else {
+                values[i] = value;
+            }
+        }
+        return Objects.hash(values);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        final FieldInfo[] fieldInfos = getClassFieldInfo(this.getClass());
+        for (int i = 0; i < fieldInfos.length; i++) {
+            sb.append(fieldInfos[i].field.getName()).append(": ");
+            final Object value = getFieldValue(fieldInfos[i].field);
+            if (value == null) {
+                sb.append("null");
+            } else if (fieldInfos[i].annotation.type() == Type.ByteArray) {
+                sb.append("0x").append(HexDump.toHexString((byte[]) value));
+            } else if (fieldInfos[i].annotation.type() == Type.Ipv4Address
+                    || fieldInfos[i].annotation.type() == Type.Ipv6Address) {
+                sb.append(((InetAddress) value).getHostAddress());
+            } else {
+                sb.append(value.toString());
+            }
+            if (i != fieldInfos.length - 1) sb.append(", ");
+        }
+        return sb.toString();
+    }
+
+    /** A simple Struct which only contains a u8 field. */
+    public static class U8 extends Struct {
+        @Struct.Field(order = 0, type = Struct.Type.U8)
+        public final short val;
+
+        public U8(final short val) {
+            this.val = val;
+        }
+    }
+
+    /** A simple Struct which only contains an s32 field. */
+    public static class S32 extends Struct {
+        @Struct.Field(order = 0, type = Struct.Type.S32)
+        public final int val;
+
+        public S32(final int val) {
+            this.val = val;
+        }
+    }
+
+    /** A simple Struct which only contains a u32 field. */
+    public static class U32 extends Struct {
+        @Struct.Field(order = 0, type = Struct.Type.U32)
+        public final long val;
+
+        public U32(final long val) {
+            this.val = val;
+        }
+    }
+
+    /** A simple Struct which only contains an s64 field. */
+    public static class S64 extends Struct {
+        @Struct.Field(order = 0, type = Struct.Type.S64)
+        public final long val;
+
+        public S64(final long val) {
+            this.val = val;
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/SyncStateMachine.java b/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
new file mode 100644
index 0000000..da184d3
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
@@ -0,0 +1,347 @@
+/**
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Message;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.util.State;
+
+import java.util.ArrayDeque;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * An implementation of a state machine, meant to be called synchronously.
+ *
+ * This class implements a finite state automaton based on the same State
+ * class as StateMachine.
+ * All methods of this class must be called on only one thread.
+ */
+public class SyncStateMachine {
+    @NonNull private final String mName;
+    @NonNull private final Thread mMyThread;
+    private final boolean mDbg;
+    private final ArrayMap<State, StateInfo> mStateInfo = new ArrayMap<>();
+
+    // mCurrentState is the current state. mDestState is the target state that mCurrentState will
+    // transition to. The value of mDestState can be changed when a state processes a message and
+    // calls #transitionTo, but it cannot be changed during the state transition. When the state
+    // transition is complete, mDestState will be set to mCurrentState. Both mCurrentState and
+    // mDestState only be null before state machine starts and must only be touched on mMyThread.
+    @Nullable private State mCurrentState;
+    @Nullable private State mDestState;
+    private final ArrayDeque<Message> mSelfMsgQueue = new ArrayDeque<Message>();
+
+    // MIN_VALUE means not currently processing any message.
+    private int mCurrentlyProcessing = Integer.MIN_VALUE;
+    // Indicates whether automaton can send self message. Self messages can only be sent by
+    // automaton from State#enter, State#exit, or State#processMessage. Calling from outside
+    // of State is not allowed.
+    private boolean mSelfMsgAllowed = false;
+
+    /**
+     * A information class about a state and its parent. Used to maintain the state hierarchy.
+     */
+    public static class StateInfo {
+        /** The state who owns this StateInfo. */
+        public final State state;
+        /** The parent state. */
+        public final State parent;
+        // True when the state has been entered and on the stack.
+        private boolean mActive;
+
+        public StateInfo(@NonNull final State child, @Nullable final State parent) {
+            this.state = child;
+            this.parent = parent;
+        }
+    }
+
+    /**
+     * The constructor.
+     *
+     * @param name of this machine.
+     * @param thread the running thread of this machine. It must either be the thread on which this
+     * constructor is called, or a thread that is not started yet.
+     */
+    public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread) {
+        this(name, thread, false /* debug */);
+    }
+
+    /**
+     * The constructor.
+     *
+     * @param name of this machine.
+     * @param thread the running thread of this machine. It must either be the thread on which this
+     * constructor is called, or a thread that is not started yet.
+     * @param dbg whether to print debug logs.
+     */
+    public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread,
+            final boolean dbg) {
+        mMyThread = thread;
+        // Machine can either be setup from machine thread or before machine thread started.
+        ensureCorrectOrNotStartedThread();
+
+        mName = name;
+        mDbg = dbg;
+    }
+
+    /**
+     * Add all of states to the state machine. Different StateInfos which have same state are not
+     * allowed. In other words, a state can not have multiple parent states. #addAllStates can
+     * only be called once either from mMyThread or before mMyThread started.
+     */
+    public final void addAllStates(@NonNull final List<StateInfo> stateInfos) {
+        ensureCorrectOrNotStartedThread();
+
+        if (mCurrentState != null) {
+            throw new IllegalStateException("State only can be added before started");
+        }
+
+        if (stateInfos.isEmpty()) throw new IllegalStateException("Empty state is not allowed");
+
+        if (!mStateInfo.isEmpty()) throw new IllegalStateException("States are already configured");
+
+        final Set<Class> usedClasses = new ArraySet<>();
+        for (final StateInfo info : stateInfos) {
+            Objects.requireNonNull(info.state);
+            if (!usedClasses.add(info.state.getClass())) {
+                throw new IllegalStateException("Adding the same state multiple times in a state "
+                        + "machine is forbidden because it tends to be confusing; it can be done "
+                        + "with anonymous subclasses but consider carefully whether you want to "
+                        + "use a single state or other alternatives instead.");
+            }
+
+            mStateInfo.put(info.state, info);
+        }
+
+        // Check whether all of parent states indicated from StateInfo are added.
+        for (final StateInfo info : stateInfos) {
+            if (info.parent != null) ensureExistingState(info.parent);
+        }
+    }
+
+    /**
+     * Start the state machine. The initial state can't be child state.
+     *
+     * @param initialState the first state of this machine. The state must be exact state object
+     * setting up by {@link #addAllStates}, not a copy of it.
+     */
+    public final void start(@NonNull final State initialState) {
+        ensureCorrectThread();
+        ensureExistingState(initialState);
+
+        mDestState = initialState;
+        mSelfMsgAllowed = true;
+        performTransitions();
+        mSelfMsgAllowed = false;
+        // If sendSelfMessage was called inside initialState#enter(), mSelfMsgQueue must be
+        // processed.
+        maybeProcessSelfMessageQueue();
+    }
+
+    /**
+     * Process the message synchronously then perform state transition. This method is used
+     * externally to the automaton to request that the automaton process the given message.
+     * 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();
+
+        if (mCurrentlyProcessing != Integer.MIN_VALUE) {
+            throw new IllegalStateException("Message(" + mCurrentlyProcessing
+                    + ") is still being processed");
+        }
+
+        // mCurrentlyProcessing tracks the external message request and it prevents this method to
+        // be called recursively. Once this message is processed and the transitions have been
+        // performed, the automaton will process the self message queue. The messages in the self
+        // message queue are added from within the automaton during processing external message.
+        // mCurrentlyProcessing is still the original external one and it will not prevent self
+        // messages from being processed.
+        mCurrentlyProcessing = what;
+        final Message msg = Message.obtain(null, what, arg1, arg2, obj);
+        currentStateProcessMessageThenPerformTransitions(msg);
+        msg.recycle();
+        maybeProcessSelfMessageQueue();
+
+        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());
+        }
+    }
+
+    private void currentStateProcessMessageThenPerformTransitions(@NonNull final Message msg) {
+        mSelfMsgAllowed = true;
+        StateInfo consideredState = mStateInfo.get(mCurrentState);
+        while (null != consideredState) {
+            // Ideally this should compare with IState.HANDLED, but it is not public field so just
+            // checking whether the return value is true (IState.HANDLED = true).
+            if (consideredState.state.processMessage(msg)) {
+                if (mDbg) {
+                    Log.d(mName, "State " + consideredState.state
+                            + " processed message " + msg.what);
+                }
+                break;
+            }
+            consideredState = mStateInfo.get(consideredState.parent);
+        }
+        if (null == consideredState) {
+            Log.wtf(mName, "Message " + msg.what + " was not handled");
+        }
+
+        performTransitions();
+        mSelfMsgAllowed = false;
+    }
+
+    /**
+     * Send self message during state transition.
+     *
+     * Must only be used inside State processMessage, enter or exit. The typical use case is
+     * something wrong happens during state transition, sending an error message which would be
+     * handled after finishing current state transitions.
+     */
+    public final void sendSelfMessage(int what, int arg1, int arg2, Object obj) {
+        if (!mSelfMsgAllowed) {
+            throw new IllegalStateException("sendSelfMessage can only be called inside "
+                    + "State#enter, State#exit or State#processMessage");
+        }
+
+        mSelfMsgQueue.add(Message.obtain(null, what, arg1, arg2, obj));
+    }
+
+    /**
+     * Transition to destination state. Upon returning from processMessage the automaton will
+     * transition to the given destination state.
+     *
+     * This function can NOT be called inside the State enter and exit function. The transition
+     * target is always defined and can never be changed mid-way of state transition.
+     *
+     * @param destState will be the state to transition to. The state must be the same instance set
+     * up by {@link #addAllStates}, not a copy of it.
+     */
+    public final void transitionTo(@NonNull final State destState) {
+        if (mDbg) Log.d(mName, "transitionTo " + destState);
+        ensureCorrectThread();
+        ensureExistingState(destState);
+
+        if (mDestState == mCurrentState) {
+            mDestState = destState;
+        } else {
+            throw new IllegalStateException("Destination already specified");
+        }
+    }
+
+    private void performTransitions() {
+        // 1. Determine the common ancestor state of current/destination states
+        // 2. Invoke state exit list from current state to common ancestor state.
+        // 3. Invoke state enter list from common ancestor state to destState by going
+        // through mEnterStateStack.
+        if (mDestState == mCurrentState) return;
+
+        final StateInfo commonAncestor = getLastActiveAncestor(mStateInfo.get(mDestState));
+
+        executeExitMethods(commonAncestor, mStateInfo.get(mCurrentState));
+        executeEnterMethods(commonAncestor, mStateInfo.get(mDestState));
+        mCurrentState = mDestState;
+    }
+
+    // Null is the root of all states.
+    private StateInfo getLastActiveAncestor(@Nullable final StateInfo start) {
+        if (null == start || start.mActive) return start;
+
+        return getLastActiveAncestor(mStateInfo.get(start.parent));
+    }
+
+    // Call the exit method from current state to common ancestor state.
+    // Both the commonAncestor and exitingState StateInfo can be null because null is the ancestor
+    // of all states.
+    // For example: When transitioning from state1 to state2, the
+    // executeExitMethods(commonAncestor, exitingState) function will be called twice, once with
+    // null and state1 as the argument, and once with null and null as the argument.
+    //              root
+    //              |   \
+    // current <- state1 state2 -> destination
+    private void executeExitMethods(@Nullable StateInfo commonAncestor,
+            @Nullable StateInfo exitingState) {
+        if (commonAncestor == exitingState) return;
+
+        if (mDbg) Log.d(mName, exitingState.state + " exit()");
+        exitingState.state.exit();
+        exitingState.mActive = false;
+        executeExitMethods(commonAncestor, mStateInfo.get(exitingState.parent));
+    }
+
+    // Call the enter method from common ancestor state to destination state.
+    // Both the commonAncestor and enteringState StateInfo can be null because null is the ancestor
+    // of all states.
+    // For example: When transitioning from state1 to state2, the
+    // executeEnterMethods(commonAncestor, enteringState) function will be called twice, once with
+    // null and state2 as the argument, and once with null and null as the argument.
+    //              root
+    //              |   \
+    // current <- state1 state2 -> destination
+    private void executeEnterMethods(@Nullable StateInfo commonAncestor,
+            @Nullable StateInfo enteringState) {
+        if (enteringState == commonAncestor) return;
+
+        executeEnterMethods(commonAncestor, mStateInfo.get(enteringState.parent));
+        if (mDbg) Log.d(mName, enteringState.state + " enter()");
+        enteringState.state.enter();
+        enteringState.mActive = true;
+    }
+
+    private void ensureCorrectThread() {
+        if (!mMyThread.equals(Thread.currentThread())) {
+            throw new IllegalStateException("Called from wrong thread");
+        }
+    }
+
+    private void ensureCorrectOrNotStartedThread() {
+        if (!mMyThread.isAlive()) return;
+
+        ensureCorrectThread();
+    }
+
+    private void ensureExistingState(@NonNull final State state) {
+        if (!mStateInfo.containsKey(state)) throw new IllegalStateException("Invalid state");
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/TcUtils.java b/staticlibs/device/com/android/net/module/util/TcUtils.java
new file mode 100644
index 0000000..a6b222f
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/TcUtils.java
@@ -0,0 +1,112 @@
+/*
+ * 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.net.module.util;
+
+import java.io.IOException;
+
+/**
+ * Contains mostly tc-related functionality.
+ */
+public class TcUtils {
+    static {
+        System.loadLibrary(JniUtil.getJniLibraryName(TcUtils.class.getPackage()));
+    }
+
+    /**
+     * Checks if the network interface uses an ethernet L2 header.
+     *
+     * @param iface the network interface.
+     * @return true if the interface uses an ethernet L2 header.
+     * @throws IOException
+     */
+    public static native boolean isEthernet(String iface) throws IOException;
+
+    /**
+     * Attach a tc bpf filter.
+     *
+     * Equivalent to the following 'tc' command:
+     * tc filter add dev .. in/egress prio .. protocol ipv6/ip bpf object-pinned
+     * /sys/fs/bpf/... direct-action
+     *
+     * @param ifIndex the network interface index.
+     * @param ingress ingress or egress qdisc.
+     * @param prio
+     * @param proto
+     * @param bpfProgPath
+     * @throws IOException
+     */
+    public static native void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio,
+            short proto, String bpfProgPath) throws IOException;
+
+    /**
+     * Attach a tc police action.
+     *
+     * Attaches a matchall filter to the clsact qdisc with a tc police and tc bpf action attached.
+     * This causes the ingress rate to be limited and exceeding packets to be forwarded to a bpf
+     * program (specified in bpfProgPah) that accounts for the packets before dropping them.
+     *
+     * Equivalent to the following 'tc' command:
+     * tc filter add dev .. ingress prio .. protocol .. matchall \
+     *     action police rate .. burst .. conform-exceed pipe/continue \
+     *     action bpf object-pinned .. \
+     *     drop
+     *
+     * @param ifIndex the network interface index.
+     * @param prio the filter preference.
+     * @param proto protocol.
+     * @param rateInBytesPerSec rate limit in bytes/s.
+     * @param bpfProgPath bpg program that accounts for rate exceeding packets before they are
+     *                    dropped.
+     * @throws IOException
+     */
+    public static native void tcFilterAddDevIngressPolice(int ifIndex, short prio, short proto,
+            int rateInBytesPerSec, String bpfProgPath) throws IOException;
+
+    /**
+     * Delete a tc filter.
+     *
+     * Equivalent to the following 'tc' command:
+     * tc filter del dev .. in/egress prio .. protocol ..
+     *
+     * @param ifIndex the network interface index.
+     * @param ingress ingress or egress qdisc.
+     * @param prio the filter preference.
+     * @param proto protocol.
+     * @throws IOException
+     */
+    public static native void tcFilterDelDev(int ifIndex, boolean ingress, short prio,
+            short proto) throws IOException;
+
+    /**
+     * Add a clsact qdisc.
+     *
+     * Equivalent to the following 'tc' command:
+     * tc qdisc add dev .. clsact
+     *
+     * @param ifIndex the network interface index.
+     * @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/arp/ArpPacket.java b/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
new file mode 100644
index 0000000..bf447d3
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.arp;
+
+import static android.system.OsConstants.ETH_P_ARP;
+import static android.system.OsConstants.ETH_P_IP;
+
+import static com.android.net.module.util.NetworkStackConstants.ARP_ETHER_IPV4_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ARP_HWTYPE_ETHER;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+
+import android.net.MacAddress;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+
+/**
+ * Defines basic data and operations needed to build and parse packets for the
+ * ARP protocol.
+ *
+ * @hide
+ */
+public class ArpPacket {
+    private static final String TAG = "ArpPacket";
+
+    public final MacAddress destination;
+    public final MacAddress source;
+    public final short opCode;
+    public final Inet4Address senderIp;
+    public final Inet4Address targetIp;
+    public final MacAddress senderHwAddress;
+    public final MacAddress targetHwAddress;
+
+    ArpPacket(MacAddress destination, MacAddress source, short opCode, MacAddress senderHwAddress,
+            Inet4Address senderIp, MacAddress targetHwAddress, Inet4Address targetIp) {
+        this.destination = destination;
+        this.source = source;
+        this.opCode = opCode;
+        this.senderHwAddress = senderHwAddress;
+        this.senderIp = senderIp;
+        this.targetHwAddress = targetHwAddress;
+        this.targetIp = targetIp;
+    }
+
+    /**
+     * Build an ARP packet from the required specified parameters.
+     */
+    @VisibleForTesting
+    public static ByteBuffer buildArpPacket(final byte[] dstMac, final byte[] srcMac,
+            final byte[] targetIp, final byte[] targetHwAddress, byte[] senderIp,
+            final short opCode) {
+        final ByteBuffer buf = ByteBuffer.allocate(ARP_ETHER_IPV4_LEN);
+
+        // Ether header
+        buf.put(dstMac);
+        buf.put(srcMac);
+        buf.putShort((short) ETH_P_ARP);
+
+        // ARP header
+        buf.putShort((short) ARP_HWTYPE_ETHER);  // hrd
+        buf.putShort((short) ETH_P_IP);          // pro
+        buf.put((byte) ETHER_ADDR_LEN);          // hln
+        buf.put((byte) IPV4_ADDR_LEN);           // pln
+        buf.putShort(opCode);                    // op
+        buf.put(srcMac);                         // sha
+        buf.put(senderIp);                       // spa
+        buf.put(targetHwAddress);                // tha
+        buf.put(targetIp);                       // tpa
+        buf.flip();
+        return buf;
+    }
+
+    /**
+     * Parse an ARP packet from a ByteBuffer object.
+     */
+    @VisibleForTesting
+    public static ArpPacket parseArpPacket(final byte[] recvbuf, final int length)
+            throws ParseException {
+        try {
+            if (length < ARP_ETHER_IPV4_LEN || recvbuf.length < length) {
+                throw new ParseException("Invalid packet length: " + length);
+            }
+
+            final ByteBuffer buffer = ByteBuffer.wrap(recvbuf, 0, length);
+            byte[] l2dst = new byte[ETHER_ADDR_LEN];
+            byte[] l2src = new byte[ETHER_ADDR_LEN];
+            buffer.get(l2dst);
+            buffer.get(l2src);
+
+            final short etherType = buffer.getShort();
+            if (etherType != ETH_P_ARP) {
+                throw new ParseException("Incorrect Ether Type: " + etherType);
+            }
+
+            final short hwType = buffer.getShort();
+            if (hwType != ARP_HWTYPE_ETHER) {
+                throw new ParseException("Incorrect HW Type: " + hwType);
+            }
+
+            final short protoType = buffer.getShort();
+            if (protoType != ETH_P_IP) {
+                throw new ParseException("Incorrect Protocol Type: " + protoType);
+            }
+
+            final byte hwAddrLength = buffer.get();
+            if (hwAddrLength != ETHER_ADDR_LEN) {
+                throw new ParseException("Incorrect HW address length: " + hwAddrLength);
+            }
+
+            final byte ipAddrLength = buffer.get();
+            if (ipAddrLength != IPV4_ADDR_LEN) {
+                throw new ParseException("Incorrect Protocol address length: " + ipAddrLength);
+            }
+
+            final short opCode = buffer.getShort();
+            if (opCode != ARP_REQUEST && opCode != ARP_REPLY) {
+                throw new ParseException("Incorrect opCode: " + opCode);
+            }
+
+            byte[] senderHwAddress = new byte[ETHER_ADDR_LEN];
+            byte[] senderIp = new byte[IPV4_ADDR_LEN];
+            buffer.get(senderHwAddress);
+            buffer.get(senderIp);
+
+            byte[] targetHwAddress = new byte[ETHER_ADDR_LEN];
+            byte[] targetIp = new byte[IPV4_ADDR_LEN];
+            buffer.get(targetHwAddress);
+            buffer.get(targetIp);
+
+            return new ArpPacket(MacAddress.fromBytes(l2dst),
+                    MacAddress.fromBytes(l2src), opCode,
+                    MacAddress.fromBytes(senderHwAddress),
+                    (Inet4Address) InetAddress.getByAddress(senderIp),
+                    MacAddress.fromBytes(targetHwAddress),
+                    (Inet4Address) InetAddress.getByAddress(targetIp));
+        } catch (IndexOutOfBoundsException e) {
+            throw new ParseException("Invalid index when wrapping a byte array into a buffer");
+        } catch (BufferUnderflowException e) {
+            throw new ParseException("Invalid buffer position");
+        } catch (IllegalArgumentException e) {
+            throw new ParseException("Invalid MAC address representation");
+        } catch (UnknownHostException e) {
+            throw new ParseException("Invalid IP address of Host");
+        }
+    }
+
+    /**
+     * Thrown when parsing ARP packet failed.
+     */
+    public static class ParseException extends Exception {
+        ParseException(String message) {
+            super(message);
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/Assertions.java b/staticlibs/device/com/android/net/module/util/async/Assertions.java
new file mode 100644
index 0000000..ce701d0
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/Assertions.java
@@ -0,0 +1,41 @@
+/*
+ * 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.net.module.util.async;
+
+import android.os.Build;
+
+/**
+ * Implements basic assert functions for runtime error-checking.
+ *
+ * @hide
+ */
+public final class Assertions {
+    public static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
+
+    public static void throwsIfOutOfBounds(int totalLength, int pos, int len) {
+        if (!IS_USER_BUILD && ((totalLength | pos | len) < 0 || pos > totalLength - len)) {
+            throw new ArrayIndexOutOfBoundsException(
+                "length=" + totalLength + "; regionStart=" + pos + "; regionLength=" + len);
+        }
+    }
+
+    public static void throwsIfOutOfBounds(byte[] buffer, int pos, int len) {
+        throwsIfOutOfBounds(buffer != null ? buffer.length : 0, pos, len);
+    }
+
+    private Assertions() {}
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/AsyncFile.java b/staticlibs/device/com/android/net/module/util/async/AsyncFile.java
new file mode 100644
index 0000000..2a3231b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/AsyncFile.java
@@ -0,0 +1,78 @@
+/*
+ * 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.net.module.util.async;
+
+import java.io.IOException;
+
+/**
+ * Represents an EventManager-managed file with Async IO semantics.
+ *
+ * Implements level-based Asyn IO semantics. This means that:
+ *   - onReadReady() callback would keep happening as long as there's any remaining
+ *     data to read, or the user calls enableReadEvents(false)
+ *   - onWriteReady() callback would keep happening as long as there's remaining space
+ *     to write to, or the user calls enableWriteEvents(false)
+ *
+ * All operations except close() must be called on the EventManager thread.
+ *
+ * @hide
+ */
+public interface AsyncFile {
+    /**
+     * Receives notifications when file readability or writeability changes.
+     * @hide
+     */
+    public interface Listener {
+        /** Invoked after the underlying file has been closed. */
+        void onClosed(AsyncFile file);
+
+        /** Invoked while the file has readable data and read notifications are enabled. */
+        void onReadReady(AsyncFile file);
+
+        /** Invoked while the file has writeable space and write notifications are enabled. */
+        void onWriteReady(AsyncFile file);
+    }
+
+    /** Requests this file to be closed. */
+    void close();
+
+    /** Enables or disables onReadReady() events. */
+    void enableReadEvents(boolean enable);
+
+    /** Enables or disables onWriteReady() events. */
+    void enableWriteEvents(boolean enable);
+
+    /** Returns true if the input stream has reached its end, or has been closed. */
+    boolean reachedEndOfFile();
+
+    /**
+     * Reads available data from the given non-blocking file descriptor.
+     *
+     * Returns zero if there's no data to read at this moment.
+     * Returns -1 if the file has reached its end or the input stream has been closed.
+     * Otherwise returns the number of bytes read.
+     */
+    int read(byte[] buffer, int pos, int len) throws IOException;
+
+    /**
+     * Writes data into the given non-blocking file descriptor.
+     *
+     * Returns zero if there's no buffer space to write to at this moment.
+     * Otherwise returns the number of bytes written.
+     */
+    int write(byte[] buffer, int pos, int len) throws IOException;
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/BufferedFile.java b/staticlibs/device/com/android/net/module/util/async/BufferedFile.java
new file mode 100644
index 0000000..bb5736b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/BufferedFile.java
@@ -0,0 +1,292 @@
+/*
+ * 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.net.module.util.async;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Buffers inbound and outbound file data within given strict limits.
+ *
+ * Automatically manages all readability and writeability events in EventManager:
+ *   - When read buffer has more space - asks EventManager to notify on more data
+ *   - When write buffer has more space - asks the user to provide more data
+ *   - When underlying file cannot accept more data - registers EventManager callback
+ *
+ * @hide
+ */
+public final class BufferedFile implements AsyncFile.Listener {
+    /**
+     * Receives notifications when new data or output space is available.
+     * @hide
+     */
+    public interface Listener {
+        /** Invoked after the underlying file has been closed. */
+        void onBufferedFileClosed();
+
+        /** Invoked when there's new data in the inbound buffer. */
+        void onBufferedFileInboundData(int readByteCount);
+
+        /** Notifies on data being flushed from output buffer. */
+        void onBufferedFileOutboundSpace();
+
+        /** Notifies on unrecoverable error in file access. */
+        void onBufferedFileIoError(String message);
+    }
+
+    private final Listener mListener;
+    private final EventManager mEventManager;
+    private AsyncFile mFile;
+
+    private final CircularByteBuffer mInboundBuffer;
+    private final AtomicLong mTotalBytesRead = new AtomicLong();
+    private boolean mIsReadingShutdown;
+
+    private final CircularByteBuffer mOutboundBuffer;
+    private final AtomicLong mTotalBytesWritten = new AtomicLong();
+
+    /** Creates BufferedFile based on the given file descriptor. */
+    public static BufferedFile create(
+            EventManager eventManager,
+            FileHandle fileHandle,
+            Listener listener,
+            int inboundBufferSize,
+            int outboundBufferSize) throws IOException {
+        if (fileHandle == null) {
+            throw new NullPointerException();
+        }
+        BufferedFile file = new BufferedFile(
+            eventManager, listener, inboundBufferSize, outboundBufferSize);
+        file.mFile = eventManager.registerFile(fileHandle, file);
+        return file;
+    }
+
+    private BufferedFile(
+            EventManager eventManager,
+            Listener listener,
+            int inboundBufferSize,
+            int outboundBufferSize) {
+        if (eventManager == null || listener == null) {
+            throw new NullPointerException();
+        }
+        mEventManager = eventManager;
+        mListener = listener;
+
+        mInboundBuffer = new CircularByteBuffer(inboundBufferSize);
+        mOutboundBuffer = new CircularByteBuffer(outboundBufferSize);
+    }
+
+    /** Requests this file to be closed. */
+    public void close() {
+        mFile.close();
+    }
+
+    @Override
+    public void onClosed(AsyncFile file) {
+        mListener.onBufferedFileClosed();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // READ PATH
+    ///////////////////////////////////////////////////////////////////////////
+
+    /** Returns buffer that is automatically filled with inbound data. */
+    public ReadableByteBuffer getInboundBuffer() {
+        return mInboundBuffer;
+    }
+
+    public int getInboundBufferFreeSizeForTest() {
+        return mInboundBuffer.freeSize();
+    }
+
+    /** Permanently disables reading of this file, and clears all buffered data. */
+    public void shutdownReading() {
+        mIsReadingShutdown = true;
+        mInboundBuffer.clear();
+        mFile.enableReadEvents(false);
+    }
+
+    /** Returns true after shutdownReading() has been called. */
+    public boolean isReadingShutdown() {
+        return mIsReadingShutdown;
+    }
+
+    /** Starts or resumes async read operations on this file. */
+    public void continueReading() {
+        if (!mIsReadingShutdown && mInboundBuffer.freeSize() > 0) {
+            mFile.enableReadEvents(true);
+        }
+    }
+
+    @Override
+    public void onReadReady(AsyncFile file) {
+        if (mIsReadingShutdown) {
+            return;
+        }
+
+        int readByteCount;
+        try {
+            readByteCount = bufferInputData();
+        } catch (IOException e) {
+            mListener.onBufferedFileIoError("IOException while reading: " + e.toString());
+            return;
+        }
+
+        if (readByteCount > 0) {
+            mListener.onBufferedFileInboundData(readByteCount);
+        }
+
+        continueReading();
+    }
+
+    private int bufferInputData() throws IOException {
+        int totalReadCount = 0;
+        while (true) {
+            final int maxReadCount = mInboundBuffer.getDirectWriteSize();
+            if (maxReadCount == 0) {
+                mFile.enableReadEvents(false);
+                break;
+            }
+
+            final int bufferOffset = mInboundBuffer.getDirectWritePos();
+            final byte[] buffer = mInboundBuffer.getDirectWriteBuffer();
+
+            final int readCount = mFile.read(buffer, bufferOffset, maxReadCount);
+            if (readCount <= 0) {
+                break;
+            }
+
+            mInboundBuffer.accountForDirectWrite(readCount);
+            totalReadCount += readCount;
+        }
+
+        mTotalBytesRead.addAndGet(totalReadCount);
+        return totalReadCount;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // WRITE PATH
+    ///////////////////////////////////////////////////////////////////////////
+
+    /** Returns the number of bytes currently buffered for output. */
+    public int getOutboundBufferSize() {
+        return mOutboundBuffer.size();
+    }
+
+    /** Returns the number of bytes currently available for buffering for output. */
+    public int getOutboundBufferFreeSize() {
+        return mOutboundBuffer.freeSize();
+    }
+
+    /**
+     * Queues the given data for output.
+     * Throws runtime exception if there is not enough space.
+     */
+    public boolean enqueueOutboundData(byte[] data, int pos, int len) {
+        return enqueueOutboundData(data, pos, len, null, 0, 0);
+    }
+
+    /**
+     * Queues data1, then data2 for output.
+     * Throws runtime exception if there is not enough space.
+     */
+    public boolean enqueueOutboundData(
+            byte[] data1, int pos1, int len1,
+            byte[] buffer2, int pos2, int len2) {
+        Assertions.throwsIfOutOfBounds(data1, pos1, len1);
+        Assertions.throwsIfOutOfBounds(buffer2, pos2, len2);
+
+        final int totalLen = len1 + len2;
+
+        if (totalLen > mOutboundBuffer.freeSize()) {
+            flushOutboundBuffer();
+
+            if (totalLen > mOutboundBuffer.freeSize()) {
+                return false;
+            }
+        }
+
+        mOutboundBuffer.writeBytes(data1, pos1, len1);
+
+        if (buffer2 != null) {
+            mOutboundBuffer.writeBytes(buffer2, pos2, len2);
+        }
+
+        flushOutboundBuffer();
+
+        return true;
+    }
+
+    private void flushOutboundBuffer() {
+        try {
+            while (mOutboundBuffer.getDirectReadSize() > 0) {
+                final int maxReadSize = mOutboundBuffer.getDirectReadSize();
+                final int writeCount = mFile.write(
+                    mOutboundBuffer.getDirectReadBuffer(),
+                    mOutboundBuffer.getDirectReadPos(),
+                    maxReadSize);
+
+                if (writeCount == 0) {
+                    mFile.enableWriteEvents(true);
+                    break;
+                }
+
+                if (writeCount > maxReadSize) {
+                    throw new IllegalArgumentException(
+                        "Write count " + writeCount + " above max " + maxReadSize);
+                }
+
+                mOutboundBuffer.accountForDirectRead(writeCount);
+            }
+        } catch (IOException e) {
+            scheduleOnIoError("IOException while writing: " + e.toString());
+        }
+    }
+
+    private void scheduleOnIoError(String message) {
+        mEventManager.execute(() -> {
+            mListener.onBufferedFileIoError(message);
+        });
+    }
+
+    @Override
+    public void onWriteReady(AsyncFile file) {
+        mFile.enableWriteEvents(false);
+        flushOutboundBuffer();
+        mListener.onBufferedFileOutboundSpace();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("file={");
+        sb.append(mFile);
+        sb.append("}");
+        if (mIsReadingShutdown) {
+            sb.append(", readingShutdown");
+        }
+        sb.append("}, inboundBuffer={");
+        sb.append(mInboundBuffer);
+        sb.append("}, outboundBuffer={");
+        sb.append(mOutboundBuffer);
+        sb.append("}, totalBytesRead=");
+        sb.append(mTotalBytesRead);
+        sb.append(", totalBytesWritten=");
+        sb.append(mTotalBytesWritten);
+        return sb.toString();
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/CircularByteBuffer.java b/staticlibs/device/com/android/net/module/util/async/CircularByteBuffer.java
new file mode 100644
index 0000000..92daa08
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/CircularByteBuffer.java
@@ -0,0 +1,210 @@
+/*
+ * 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.net.module.util.async;
+
+import java.util.Arrays;
+
+/**
+ * Implements a circular read-write byte buffer.
+ *
+ * @hide
+ */
+public final class CircularByteBuffer implements ReadableByteBuffer {
+    private final byte[] mBuffer;
+    private final int mCapacity;
+    private int mReadPos;
+    private int mWritePos;
+    private int mSize;
+
+    public CircularByteBuffer(int capacity) {
+        mCapacity = capacity;
+        mBuffer = new byte[mCapacity];
+    }
+
+    @Override
+    public void clear() {
+        mReadPos = 0;
+        mWritePos = 0;
+        mSize = 0;
+        Arrays.fill(mBuffer, (byte) 0);
+    }
+
+    @Override
+    public int capacity() {
+        return mCapacity;
+    }
+
+    @Override
+    public int size() {
+        return mSize;
+    }
+
+    /** Returns the amount of remaining writeable space in this buffer. */
+    public int freeSize() {
+        return mCapacity - mSize;
+    }
+
+    @Override
+    public byte peek(int offset) {
+        if (offset < 0 || offset >= size()) {
+            throw new IllegalArgumentException("Invalid offset=" + offset + ", size=" + size());
+        }
+
+        return mBuffer[(mReadPos + offset) % mCapacity];
+    }
+
+    @Override
+    public void readBytes(byte[] dst, int dstPos, int dstLen) {
+        if (dst != null) {
+            Assertions.throwsIfOutOfBounds(dst, dstPos, dstLen);
+        }
+        if (dstLen > size()) {
+            throw new IllegalArgumentException("Invalid len=" + dstLen + ", size=" + size());
+        }
+
+        while (dstLen > 0) {
+            final int copyLen = getCopyLen(mReadPos, mWritePos, dstLen);
+            if (dst != null) {
+                System.arraycopy(mBuffer, mReadPos, dst, dstPos, copyLen);
+            }
+            dstPos += copyLen;
+            dstLen -= copyLen;
+            mSize -= copyLen;
+            mReadPos = (mReadPos + copyLen) % mCapacity;
+        }
+
+        if (mSize == 0) {
+            // Reset to the beginning for better contiguous access.
+            mReadPos = 0;
+            mWritePos = 0;
+        }
+    }
+
+    @Override
+    public void peekBytes(int offset, byte[] dst, int dstPos, int dstLen) {
+        Assertions.throwsIfOutOfBounds(dst, dstPos, dstLen);
+        if (offset + dstLen > size()) {
+            throw new IllegalArgumentException("Invalid len=" + dstLen
+                    + ", offset=" + offset + ", size=" + size());
+        }
+
+        int tmpReadPos = (mReadPos + offset) % mCapacity;
+        while (dstLen > 0) {
+            final int copyLen = getCopyLen(tmpReadPos, mWritePos, dstLen);
+            System.arraycopy(mBuffer, tmpReadPos, dst, dstPos, copyLen);
+            dstPos += copyLen;
+            dstLen -= copyLen;
+            tmpReadPos = (tmpReadPos + copyLen) % mCapacity;
+        }
+    }
+
+    @Override
+    public int getDirectReadSize() {
+        if (size() == 0) {
+            return 0;
+        }
+        return (mReadPos < mWritePos ? (mWritePos - mReadPos) : (mCapacity - mReadPos));
+    }
+
+    @Override
+    public int getDirectReadPos() {
+        return mReadPos;
+    }
+
+    @Override
+    public byte[] getDirectReadBuffer() {
+        return mBuffer;
+    }
+
+    @Override
+    public void accountForDirectRead(int len) {
+        if (len < 0 || len > size()) {
+            throw new IllegalArgumentException("Invalid len=" + len + ", size=" + size());
+        }
+
+        mSize -= len;
+        mReadPos = (mReadPos + len) % mCapacity;
+    }
+
+    /** Copies given data to the end of the buffer. */
+    public void writeBytes(byte[] buffer, int pos, int len) {
+        Assertions.throwsIfOutOfBounds(buffer, pos, len);
+        if (len > freeSize()) {
+            throw new IllegalArgumentException("Invalid len=" + len + ", size=" + freeSize());
+        }
+
+        while (len > 0) {
+            final int copyLen = getCopyLen(mWritePos, mReadPos,len);
+            System.arraycopy(buffer, pos, mBuffer, mWritePos, copyLen);
+            pos += copyLen;
+            len -= copyLen;
+            mSize += copyLen;
+            mWritePos = (mWritePos + copyLen) % mCapacity;
+        }
+    }
+
+    private int getCopyLen(int startPos, int endPos, int len) {
+        if (startPos < endPos) {
+            return Math.min(len, endPos - startPos);
+        } else {
+            return Math.min(len, mCapacity - startPos);
+        }
+    }
+
+    /** Returns the amount of contiguous writeable space. */
+    public int getDirectWriteSize() {
+        if (freeSize() == 0) {
+            return 0;  // Return zero in case buffer is full.
+        }
+        return (mWritePos < mReadPos ? (mReadPos - mWritePos) : (mCapacity - mWritePos));
+    }
+
+    /** Returns the position of contiguous writeable space. */
+    public int getDirectWritePos() {
+        return mWritePos;
+    }
+
+    /** Returns the buffer reference for direct write operation. */
+    public byte[] getDirectWriteBuffer() {
+        return mBuffer;
+    }
+
+    /** Must be called after performing a direct write using getDirectWriteBuffer(). */
+    public void accountForDirectWrite(int len) {
+        if (len < 0 || len > freeSize()) {
+            throw new IllegalArgumentException("Invalid len=" + len + ", size=" + freeSize());
+        }
+
+        mSize += len;
+        mWritePos = (mWritePos + len) % mCapacity;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("CircularByteBuffer{c=");
+        sb.append(mCapacity);
+        sb.append(",s=");
+        sb.append(mSize);
+        sb.append(",r=");
+        sb.append(mReadPos);
+        sb.append(",w=");
+        sb.append(mWritePos);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/EventManager.java b/staticlibs/device/com/android/net/module/util/async/EventManager.java
new file mode 100644
index 0000000..4ed4a70
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/EventManager.java
@@ -0,0 +1,75 @@
+/*
+ * 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.net.module.util.async;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+/**
+ * Manages Async IO files and scheduled alarms, and executes all related callbacks
+ * in its own thread.
+ *
+ * All callbacks of AsyncFile, Alarm and EventManager will execute on EventManager's thread.
+ *
+ * Methods of this interface can be called from any thread.
+ *
+ * @hide
+ */
+public interface EventManager extends Executor {
+    /**
+     * Represents a scheduled alarm, allowing caller to attempt to cancel that alarm
+     * before it executes.
+     *
+     * @hide
+     */
+    public interface Alarm {
+        /** @hide */
+        public interface Listener {
+            void onAlarm(Alarm alarm, long elapsedTimeMs);
+            void onAlarmCancelled(Alarm alarm);
+        }
+
+        /**
+         * Attempts to cancel this alarm. Note that this request is inherently
+         * racy if executed close to the alarm's expiration time.
+         */
+        void cancel();
+    }
+
+    /**
+     * Requests EventManager to manage the given file.
+     *
+     * The file descriptors are not cloned, and EventManager takes ownership of all files passed.
+     *
+     * No event callbacks are enabled by this method.
+     */
+    AsyncFile registerFile(FileHandle fileHandle, AsyncFile.Listener listener) throws IOException;
+
+    /**
+     * Schedules Alarm with the given timeout.
+     *
+     * Timeout of zero can be used for immediate execution.
+     */
+    Alarm scheduleAlarm(long timeout, Alarm.Listener callback);
+
+    /** Schedules Runnable for immediate execution. */
+    @Override
+    void execute(Runnable callback);
+
+    /** Throws a runtime exception if the caller is not executing on this EventManager's thread. */
+    void assertInThread();
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/FileHandle.java b/staticlibs/device/com/android/net/module/util/async/FileHandle.java
new file mode 100644
index 0000000..9f7942d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/FileHandle.java
@@ -0,0 +1,74 @@
+/*
+ * 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.net.module.util.async;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.Closeable;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Represents an file descriptor or another way to access a file.
+ *
+ * @hide
+ */
+public final class FileHandle {
+    private final ParcelFileDescriptor mFd;
+    private final Closeable mCloseable;
+    private final InputStream mInputStream;
+    private final OutputStream mOutputStream;
+
+    public static FileHandle fromFileDescriptor(ParcelFileDescriptor fd) {
+        if (fd == null) {
+            throw new NullPointerException();
+        }
+        return new FileHandle(fd, null, null, null);
+    }
+
+    public static FileHandle fromBlockingStream(
+            Closeable closeable, InputStream is, OutputStream os) {
+        if (closeable == null || is == null || os == null) {
+            throw new NullPointerException();
+        }
+        return new FileHandle(null, closeable, is, os);
+    }
+
+    private FileHandle(ParcelFileDescriptor fd, Closeable closeable,
+            InputStream is, OutputStream os) {
+        mFd = fd;
+        mCloseable = closeable;
+        mInputStream = is;
+        mOutputStream = os;
+    }
+
+    ParcelFileDescriptor getFileDescriptor() {
+        return mFd;
+    }
+
+    Closeable getCloseable() {
+        return mCloseable;
+    }
+
+    InputStream getInputStream() {
+        return mInputStream;
+    }
+
+    OutputStream getOutputStream() {
+        return mOutputStream;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/OsAccess.java b/staticlibs/device/com/android/net/module/util/async/OsAccess.java
new file mode 100644
index 0000000..df0ded2
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/OsAccess.java
@@ -0,0 +1,66 @@
+/*
+ * 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.net.module.util.async;
+
+import android.os.ParcelFileDescriptor;
+import android.system.StructPollfd;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Provides access to all relevant OS functions..
+ *
+ * @hide
+ */
+public abstract class OsAccess {
+    /** Closes the given file, suppressing IO exceptions. */
+    public abstract void close(ParcelFileDescriptor fd);
+
+    /** Returns file name for debugging purposes. */
+    public abstract String getFileDebugName(ParcelFileDescriptor fd);
+
+    /** Returns inner FileDescriptor instance. */
+    public abstract FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd);
+
+    /**
+     * Reads available data from the given non-blocking file descriptor.
+     *
+     * Returns zero if there's no data to read at this moment.
+     * Returns -1 if the file has reached its end or the input stream has been closed.
+     * Otherwise returns the number of bytes read.
+     */
+    public abstract int read(FileDescriptor fd, byte[] buffer, int pos, int len)
+            throws IOException;
+
+    /**
+     * Writes data into the given non-blocking file descriptor.
+     *
+     * Returns zero if there's no buffer space to write to at this moment.
+     * Otherwise returns the number of bytes written.
+     */
+    public abstract int write(FileDescriptor fd, byte[] buffer, int pos, int len)
+            throws IOException;
+
+    public abstract long monotonicTimeMillis();
+    public abstract void setNonBlocking(FileDescriptor fd) throws IOException;
+    public abstract ParcelFileDescriptor[] pipe() throws IOException;
+
+    public abstract int poll(StructPollfd[] fds, int timeoutMs) throws IOException;
+    public abstract short getPollInMask();
+    public abstract short getPollOutMask();
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/ReadableByteBuffer.java b/staticlibs/device/com/android/net/module/util/async/ReadableByteBuffer.java
new file mode 100644
index 0000000..7f82404
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/ReadableByteBuffer.java
@@ -0,0 +1,60 @@
+/*
+ * 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.net.module.util.async;
+
+/**
+ * Allows reading from a buffer of bytes. The data can be read and thus removed,
+ * or peeked at without disturbing the buffer state.
+ *
+ * @hide
+ */
+public interface ReadableByteBuffer {
+    /** Returns the size of the buffered data. */
+    int size();
+
+    /**
+     * Returns the maximum amount of the buffered data.
+     *
+     * The caller may use this method in combination with peekBytes()
+     * to estimate when the buffer needs to be emptied using readData().
+     */
+    int capacity();
+
+    /** Clears all buffered data. */
+    void clear();
+
+    /** Returns a single byte at the given offset. */
+    byte peek(int offset);
+
+    /** Copies an array of bytes from the given offset to "dst". */
+    void peekBytes(int offset, byte[] dst, int dstPos, int dstLen);
+
+    /** Reads and removes an array of bytes from the head of the buffer. */
+    void readBytes(byte[] dst, int dstPos, int dstLen);
+
+    /** Returns the amount of contiguous readable data. */
+    int getDirectReadSize();
+
+    /** Returns the position of contiguous readable data. */
+    int getDirectReadPos();
+
+    /** Returns the buffer reference for direct read operation. */
+    byte[] getDirectReadBuffer();
+
+    /** Must be called after performing a direct read using getDirectReadBuffer(). */
+    void accountForDirectRead(int len);
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java b/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java
new file mode 100644
index 0000000..420a544
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java
@@ -0,0 +1,203 @@
+/*
+ * 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.ip;
+
+import static com.android.net.module.util.netlink.ConntrackMessage.DYING_MASK;
+import static com.android.net.module.util.netlink.ConntrackMessage.ESTABLISHED_MASK;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.system.OsConstants;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.ConntrackMessage;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import java.util.Objects;
+
+
+/**
+ * ConntrackMonitor.
+ *
+ * Monitors the netfilter conntrack notifications and presents to callers
+ * ConntrackEvents describing each event.
+ *
+ * @hide
+ */
+public class ConntrackMonitor extends NetlinkMonitor {
+    private static final String TAG = ConntrackMonitor.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false;
+
+    // Reference kernel/uapi/linux/netfilter/nfnetlink_compat.h
+    public static final int NF_NETLINK_CONNTRACK_NEW = 1;
+    public static final int NF_NETLINK_CONNTRACK_UPDATE = 2;
+    public static final int NF_NETLINK_CONNTRACK_DESTROY = 4;
+
+    // The socket receive buffer size in bytes. If too many conntrack messages are sent too
+    // quickly, the conntrack messages can overflow the socket receive buffer. This can happen
+    // if too many connections are disconnected by losing network and so on. Use a large-enough
+    // buffer to avoid the error ENOBUFS while listening to the conntrack messages.
+    private static final int SOCKET_RECV_BUFSIZE = 6 * 1024 * 1024;
+
+    /**
+     * A class for describing parsed netfilter conntrack events.
+     */
+    public static class ConntrackEvent {
+        /**
+         * Conntrack event type.
+         */
+        public final short msgType;
+        /**
+         * Original direction conntrack tuple.
+         */
+        public final ConntrackMessage.Tuple tupleOrig;
+        /**
+         * Reply direction conntrack tuple.
+         */
+        public final ConntrackMessage.Tuple tupleReply;
+        /**
+         * Connection status. A bitmask of ip_conntrack_status enum flags.
+         */
+        public final int status;
+        /**
+         * Conntrack timeout.
+         */
+        public final int timeoutSec;
+
+        public ConntrackEvent(ConntrackMessage msg) {
+            this.msgType = msg.getHeader().nlmsg_type;
+            this.tupleOrig = msg.tupleOrig;
+            this.tupleReply = msg.tupleReply;
+            this.status = msg.status;
+            this.timeoutSec = msg.timeoutSec;
+        }
+
+        @VisibleForTesting
+        public ConntrackEvent(short msgType, ConntrackMessage.Tuple tupleOrig,
+                ConntrackMessage.Tuple tupleReply, int status, int timeoutSec) {
+            this.msgType = msgType;
+            this.tupleOrig = tupleOrig;
+            this.tupleReply = tupleReply;
+            this.status = status;
+            this.timeoutSec = timeoutSec;
+        }
+
+        @Override
+        @VisibleForTesting
+        public boolean equals(Object o) {
+            if (!(o instanceof ConntrackEvent)) return false;
+            ConntrackEvent that = (ConntrackEvent) o;
+            return this.msgType == that.msgType
+                    && Objects.equals(this.tupleOrig, that.tupleOrig)
+                    && Objects.equals(this.tupleReply, that.tupleReply)
+                    && this.status == that.status
+                    && this.timeoutSec == that.timeoutSec;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(msgType, tupleOrig, tupleReply, status, timeoutSec);
+        }
+
+        @Override
+        public String toString() {
+            return "ConntrackEvent{"
+                    + "msg_type{"
+                    + NetlinkConstants.stringForNlMsgType(msgType, OsConstants.NETLINK_NETFILTER)
+                    + "}, "
+                    + "tuple_orig{" + tupleOrig + "}, "
+                    + "tuple_reply{" + tupleReply + "}, "
+                    + "status{"
+                    + status + "(" + ConntrackMessage.stringForIpConntrackStatus(status) + ")"
+                    + "}, "
+                    + "timeout_sec{" + Integer.toUnsignedLong(timeoutSec) + "}"
+                    + "}";
+        }
+
+        /**
+         * Check the established NAT session conntrack message.
+         *
+         * @param msg the conntrack message to check.
+         * @return true if an established NAT message, false if not.
+         */
+        public static boolean isEstablishedNatSession(@NonNull ConntrackMessage msg) {
+            if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_NEW) return false;
+            if (msg.tupleOrig == null) return false;
+            if (msg.tupleReply == null) return false;
+            if (msg.timeoutSec == 0) return false;
+            if ((msg.status & ESTABLISHED_MASK) != ESTABLISHED_MASK) return false;
+
+            return true;
+        }
+
+        /**
+         * Check the dying NAT session conntrack message.
+         * Note that IPCTNL_MSG_CT_DELETE event has no CTA_TIMEOUT attribute.
+         *
+         * @param msg the conntrack message to check.
+         * @return true if a dying NAT message, false if not.
+         */
+        public static boolean isDyingNatSession(@NonNull ConntrackMessage msg) {
+            if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_DELETE) return false;
+            if (msg.tupleOrig == null) return false;
+            if (msg.tupleReply == null) return false;
+            if (msg.timeoutSec != 0) return false;
+            if ((msg.status & DYING_MASK) != DYING_MASK) return false;
+
+            return true;
+        }
+    }
+
+    /**
+     * A callback to caller for conntrack event.
+     */
+    public interface ConntrackEventConsumer {
+        /**
+         * Every conntrack event received on the netlink socket is passed in
+         * here.
+         */
+        void accept(@NonNull ConntrackEvent event);
+    }
+
+    private final ConntrackEventConsumer mConsumer;
+
+    public ConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
+            @NonNull ConntrackEventConsumer cb) {
+        super(h, log, TAG, OsConstants.NETLINK_NETFILTER, NF_NETLINK_CONNTRACK_NEW
+                | NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY, SOCKET_RECV_BUFSIZE);
+        mConsumer = cb;
+    }
+
+    @Override
+    public void processNetlinkMessage(NetlinkMessage nlMsg, final long whenMs) {
+        if (!(nlMsg instanceof ConntrackMessage)) {
+            mLog.e("non-conntrack msg: " + nlMsg);
+            return;
+        }
+
+        final ConntrackMessage conntrackMsg = (ConntrackMessage) nlMsg;
+        if (!(ConntrackEvent.isEstablishedNatSession(conntrackMsg)
+                || ConntrackEvent.isDyingNatSession(conntrackMsg))) {
+            return;
+        }
+
+        mConsumer.accept(new ConntrackEvent(conntrackMsg));
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java b/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java
new file mode 100644
index 0000000..7277fec
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.ip;
+
+import static android.net.INetd.IF_STATE_DOWN;
+import static android.net.INetd.IF_STATE_UP;
+
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.OsConstants;
+
+import com.android.net.module.util.SharedLog;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+/**
+ * Encapsulates the multiple IP configuration operations performed on an interface.
+ *
+ * TODO: refactor/eliminate the redundant ways to set and clear addresses.
+ *
+ * @hide
+ */
+public class InterfaceController {
+    private static final boolean DBG = false;
+
+    private final String mIfName;
+    private final INetd mNetd;
+    private final SharedLog mLog;
+
+    public InterfaceController(String ifname, INetd netd, SharedLog log) {
+        mIfName = ifname;
+        mNetd = netd;
+        mLog = log;
+    }
+
+    /**
+     * Set the IPv4 address and also optionally bring the interface up or down.
+     */
+    public boolean setInterfaceConfiguration(final LinkAddress ipv4Addr,
+            final Boolean setIfaceUp) {
+        if (!(ipv4Addr.getAddress() instanceof Inet4Address)) {
+            throw new IllegalArgumentException("Invalid or mismatched Inet4Address");
+        }
+        // Note: currently netd only support INetd#IF_STATE_UP and #IF_STATE_DOWN.
+        // Other flags would be ignored.
+
+        final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
+        ifConfig.ifName = mIfName;
+        ifConfig.ipv4Addr = ipv4Addr.getAddress().getHostAddress();
+        ifConfig.prefixLength = ipv4Addr.getPrefixLength();
+        // Netd ignores hwaddr in interfaceSetCfg.
+        ifConfig.hwAddr = "";
+        if (setIfaceUp == null) {
+            // Empty array means no change.
+            ifConfig.flags = new String[0];
+        } else {
+            // Netd ignores any flag that's not IF_STATE_UP or IF_STATE_DOWN in interfaceSetCfg.
+            ifConfig.flags = setIfaceUp.booleanValue()
+                    ? new String[] {IF_STATE_UP} : new String[] {IF_STATE_DOWN};
+        }
+        try {
+            mNetd.interfaceSetCfg(ifConfig);
+        } catch (RemoteException | ServiceSpecificException e) {
+            logError("Setting IPv4 address to %s/%d failed: %s",
+                    ifConfig.ipv4Addr, ifConfig.prefixLength, e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Set the IPv4 address of the interface.
+     */
+    public boolean setIPv4Address(final LinkAddress address) {
+        return setInterfaceConfiguration(address, null);
+    }
+
+    /**
+     * Clear the IPv4Address of the interface.
+     */
+    public boolean clearIPv4Address() {
+        return setIPv4Address(new LinkAddress("0.0.0.0/0"));
+    }
+
+    private boolean setEnableIPv6(boolean enabled) {
+        try {
+            mNetd.interfaceSetEnableIPv6(mIfName, enabled);
+        } catch (RemoteException | ServiceSpecificException e) {
+            logError("%s IPv6 failed: %s", (enabled ? "enabling" : "disabling"), e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Enable IPv6 on the interface.
+     */
+    public boolean enableIPv6() {
+        return setEnableIPv6(true);
+    }
+
+    /**
+     * Disable IPv6 on the interface.
+     */
+    public boolean disableIPv6() {
+        return setEnableIPv6(false);
+    }
+
+    /**
+     * Enable or disable IPv6 privacy extensions on the interface.
+     * @param enabled Whether the extensions should be enabled.
+     */
+    public boolean setIPv6PrivacyExtensions(boolean enabled) {
+        try {
+            mNetd.interfaceSetIPv6PrivacyExtensions(mIfName, enabled);
+        } catch (RemoteException | ServiceSpecificException e) {
+            logError("error %s IPv6 privacy extensions: %s",
+                    (enabled ? "enabling" : "disabling"), e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Set IPv6 address generation mode on the interface.
+     *
+     * <p>IPv6 should be disabled before changing the mode.
+     */
+    public boolean setIPv6AddrGenModeIfSupported(int mode) {
+        try {
+            mNetd.setIPv6AddrGenMode(mIfName, mode);
+        } catch (RemoteException e) {
+            logError("Unable to set IPv6 addrgen mode: %s", e);
+            return false;
+        } catch (ServiceSpecificException e) {
+            if (e.errorCode != OsConstants.EOPNOTSUPP) {
+                logError("Unable to set IPv6 addrgen mode: %s", e);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Add an address to the interface.
+     */
+    public boolean addAddress(LinkAddress addr) {
+        return addAddress(addr.getAddress(), addr.getPrefixLength());
+    }
+
+    /**
+     * Add an address to the interface.
+     */
+    public boolean addAddress(InetAddress ip, int prefixLen) {
+        try {
+            mNetd.interfaceAddAddress(mIfName, ip.getHostAddress(), prefixLen);
+        } catch (ServiceSpecificException | RemoteException e) {
+            logError("failed to add %s/%d: %s", ip, prefixLen, e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Remove an address from the interface.
+     */
+    public boolean removeAddress(InetAddress ip, int prefixLen) {
+        try {
+            mNetd.interfaceDelAddress(mIfName, ip.getHostAddress(), prefixLen);
+        } catch (ServiceSpecificException | RemoteException e) {
+            logError("failed to remove %s/%d: %s", ip, prefixLen, e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Remove all addresses from the interface.
+     */
+    public boolean clearAllAddresses() {
+        try {
+            mNetd.interfaceClearAddrs(mIfName);
+        } catch (Exception e) {
+            logError("Failed to clear addresses: %s", e);
+            return false;
+        }
+        return true;
+    }
+
+    private void logError(String fmt, Object... args) {
+        mLog.e(String.format(fmt, args));
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java b/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java
new file mode 100644
index 0000000..e88e7f0
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ip;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForNlMsgType;
+
+import android.annotation.NonNull;
+import android.net.MacAddress;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.RtNetlinkNeighborMessage;
+import com.android.net.module.util.netlink.StructNdMsg;
+
+import java.net.InetAddress;
+import java.util.StringJoiner;
+
+/**
+ * IpNeighborMonitor.
+ *
+ * Monitors the kernel rtnetlink neighbor notifications and presents to callers
+ * NeighborEvents describing each event. Callers can provide a consumer instance
+ * to both filter (e.g. by interface index and IP address) and handle the
+ * generated NeighborEvents.
+ *
+ * @hide
+ */
+public class IpNeighborMonitor extends NetlinkMonitor {
+    private static final String TAG = IpNeighborMonitor.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false;
+
+    /**
+     * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND)
+     * for the given IP address on the specified interface index.
+     *
+     * @return 0 if the request was successfully passed to the kernel; otherwise return
+     *         a non-zero error code.
+     */
+    public static int startKernelNeighborProbe(int ifIndex, InetAddress ip) {
+        final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex;
+        if (DBG) Log.d(TAG, msgSnippet);
+
+        final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage(
+                1, ip, StructNdMsg.NUD_PROBE, ifIndex, null);
+
+        try {
+            NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Error " + msgSnippet + ": " + e);
+            return -e.errno;
+        }
+
+        return 0;
+    }
+
+    /**
+     * An event about a neighbor.
+     */
+    public static class NeighborEvent {
+        public final long elapsedMs;
+        public final short msgType;
+        public final int ifindex;
+        @NonNull
+        public final InetAddress ip;
+        public final short nudState;
+        @NonNull
+        public final MacAddress macAddr;
+
+        public NeighborEvent(long elapsedMs, short msgType, int ifindex, @NonNull InetAddress ip,
+                short nudState, @NonNull MacAddress macAddr) {
+            this.elapsedMs = elapsedMs;
+            this.msgType = msgType;
+            this.ifindex = ifindex;
+            this.ip = ip;
+            this.nudState = nudState;
+            this.macAddr = macAddr;
+        }
+
+        boolean isConnected() {
+            return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateConnected(nudState);
+        }
+
+        public boolean isValid() {
+            return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateValid(nudState);
+        }
+
+        @Override
+        public String toString() {
+            final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
+            return j.add("@" + elapsedMs)
+                    .add(stringForNlMsgType(msgType, NETLINK_ROUTE))
+                    .add("if=" + ifindex)
+                    .add(ip.getHostAddress())
+                    .add(StructNdMsg.stringForNudState(nudState))
+                    .add("[" + macAddr + "]")
+                    .toString();
+        }
+    }
+
+    /**
+     * A client that consumes NeighborEvent instances.
+     * Implement this to listen to neighbor events.
+     */
+    public interface NeighborEventConsumer {
+        // Every neighbor event received on the netlink socket is passed in
+        // here. Subclasses should filter for events of interest.
+        /**
+         * Consume a neighbor event
+         * @param event the event
+         */
+        void accept(NeighborEvent event);
+    }
+
+    private final NeighborEventConsumer mConsumer;
+
+    public IpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb) {
+        super(h, log, TAG, NETLINK_ROUTE, OsConstants.RTMGRP_NEIGH);
+        mConsumer = (cb != null) ? cb : (event) -> { /* discard */ };
+    }
+
+    @Override
+    public void processNetlinkMessage(NetlinkMessage nlMsg, final long whenMs) {
+        if (!(nlMsg instanceof RtNetlinkNeighborMessage)) {
+            mLog.e("non-rtnetlink neighbor msg: " + nlMsg);
+            return;
+        }
+
+        final RtNetlinkNeighborMessage neighMsg = (RtNetlinkNeighborMessage) nlMsg;
+        final short msgType = neighMsg.getHeader().nlmsg_type;
+        final StructNdMsg ndMsg = neighMsg.getNdHeader();
+        if (ndMsg == null) {
+            mLog.e("RtNetlinkNeighborMessage without ND message header!");
+            return;
+        }
+
+        final int ifindex = ndMsg.ndm_ifindex;
+        final InetAddress destination = neighMsg.getDestination();
+        final short nudState =
+                (msgType == RTM_DELNEIGH)
+                ? StructNdMsg.NUD_NONE
+                : ndMsg.ndm_state;
+
+        final NeighborEvent event = new NeighborEvent(
+                whenMs, msgType, ifindex, destination, nudState,
+                getMacAddress(neighMsg.getLinkLayerAddress()));
+
+        if (VDBG) {
+            Log.d(TAG, neighMsg.toString());
+        }
+        if (DBG) {
+            Log.d(TAG, event.toString());
+        }
+
+        mConsumer.accept(event);
+    }
+
+    private static MacAddress getMacAddress(byte[] linkLayerAddress) {
+        if (linkLayerAddress != null) {
+            try {
+                return MacAddress.fromBytes(linkLayerAddress);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "Failed to parse link-layer address: " + hexify(linkLayerAddress));
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java b/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
new file mode 100644
index 0000000..15a4633
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
@@ -0,0 +1,188 @@
+/*
+ * 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.ip;
+
+import static android.system.OsConstants.AF_NETLINK;
+import static android.system.OsConstants.ENOBUFS;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
+
+import static com.android.net.module.util.SocketUtils.closeSocketQuietly;
+import static com.android.net.module.util.SocketUtils.makeNetlinkSocketAddress;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.net.module.util.PacketReader;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.NetlinkErrorMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
+
+import java.io.FileDescriptor;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A simple base class to listen for netlink broadcasts.
+ *
+ * Opens a netlink socket of the given family and binds to the specified groups. Polls the socket
+ * from the event loop of the passed-in {@link Handler}, and calls the subclass-defined
+ * {@link #processNetlinkMessage} method on the handler thread for each netlink message that
+ * arrives. Currently ignores all netlink errors.
+ * @hide
+ */
+public class NetlinkMonitor extends PacketReader {
+    protected final SharedLog mLog;
+    protected final String mTag;
+    private final int mFamily;
+    private final int mBindGroups;
+    private final int mSockRcvbufSize;
+
+    private static final boolean DBG = false;
+
+    // Default socket receive buffer size. This means the specific buffer size is not set.
+    private static final int DEFAULT_SOCKET_RECV_BUFSIZE = -1;
+
+    /**
+     * Constructs a new {@code NetlinkMonitor} instance.
+     *
+     * @param h The Handler on which to poll for messages and on which to call
+     *          {@link #processNetlinkMessage}.
+     * @param log A SharedLog to log to.
+     * @param tag The log tag to use for log messages.
+     * @param family the Netlink socket family to, e.g., {@code NETLINK_ROUTE}.
+     * @param bindGroups the netlink groups to bind to.
+     * @param sockRcvbufSize the specific socket receive buffer size in bytes. -1 means that don't
+     *        set the specific socket receive buffer size in #createFd and use the default value in
+     *        /proc/sys/net/core/rmem_default file. See SO_RCVBUF in man-pages/socket.
+     */
+    public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
+            int family, int bindGroups, int sockRcvbufSize) {
+        super(h, NetlinkUtils.DEFAULT_RECV_BUFSIZE);
+        mLog = log.forSubComponent(tag);
+        mTag = tag;
+        mFamily = family;
+        mBindGroups = bindGroups;
+        mSockRcvbufSize = sockRcvbufSize;
+    }
+
+    public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
+            int family, int bindGroups) {
+        this(h, log, tag, family, bindGroups, DEFAULT_SOCKET_RECV_BUFSIZE);
+    }
+
+    @Override
+    protected FileDescriptor createFd() {
+        FileDescriptor fd = null;
+
+        try {
+            fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_NONBLOCK, mFamily);
+            if (mSockRcvbufSize != DEFAULT_SOCKET_RECV_BUFSIZE) {
+                try {
+                    Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, mSockRcvbufSize);
+                } catch (ErrnoException e) {
+                    Log.wtf(mTag, "Failed to set SO_RCVBUF to " + mSockRcvbufSize, e);
+                }
+            }
+            Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));
+            NetlinkUtils.connectToKernel(fd);
+
+            if (DBG) {
+                final SocketAddress nlAddr = Os.getsockname(fd);
+                Log.d(mTag, "bound to sockaddr_nl{" + nlAddr.toString() + "}");
+            }
+        } catch (ErrnoException | SocketException e) {
+            logError("Failed to create rtnetlink socket", e);
+            closeSocketQuietly(fd);
+            return null;
+        }
+
+        return fd;
+    }
+
+    @Override
+    protected void handlePacket(byte[] recvbuf, int length) {
+        final long whenMs = SystemClock.elapsedRealtime();
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf, 0, length);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        while (byteBuffer.remaining() > 0) {
+            try {
+                final int position = byteBuffer.position();
+                final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer, mFamily);
+                if (nlMsg == null || nlMsg.getHeader() == null) {
+                    byteBuffer.position(position);
+                    mLog.e("unparsable netlink msg: " + hexify(byteBuffer));
+                    break;
+                }
+
+                if (nlMsg instanceof NetlinkErrorMessage) {
+                    mLog.e("netlink error: " + nlMsg);
+                    continue;
+                }
+
+                processNetlinkMessage(nlMsg, whenMs);
+            } catch (Exception e) {
+                mLog.e("Error handling netlink message", e);
+            }
+        }
+    }
+
+    @Override
+    protected void logError(String msg, Exception e) {
+        mLog.e(msg, e);
+    }
+
+    // Ignoring ENOBUFS may miss any important netlink messages, there are some messages which
+    // cannot be recovered by dumping current state once missed since kernel doesn't keep state
+    // for it. In addition, dumping current state will not result in any RTM_DELxxx messages, so
+    // reconstructing current state from a dump will be difficult. However, for those netlink
+    // messages don't cause any state changes, e.g. RTM_NEWLINK with current link state, maybe
+    // it's okay to ignore them, because these netlink messages won't cause any changes on the
+    // LinkProperties. Given the above trade-offs, try to ignore ENOBUFS and that's similar to
+    // what netd does today.
+    //
+    // TODO: log metrics when ENOBUFS occurs, or even force a disconnect, it will help see how
+    // often this error occurs on fields with the associated socket receive buffer size.
+    @Override
+    protected boolean handleReadError(ErrnoException e) {
+        logError("readPacket error: ", e);
+        if (e.errno == ENOBUFS) {
+            Log.wtf(mTag, "Errno: ENOBUFS");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Processes one netlink message. Must be overridden by subclasses.
+     * @param nlMsg the message to process.
+     * @param whenMs the timestamp, as measured by {@link SystemClock#elapsedRealtime}, when the
+     *               message was received.
+     */
+    protected void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) { }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/ConntrackMessage.java b/staticlibs/device/com/android/net/module/util/netlink/ConntrackMessage.java
new file mode 100644
index 0000000..dfed3ef
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/ConntrackMessage.java
@@ -0,0 +1,560 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.net.module.util.netlink.StructNlAttr.findNextAttrOfType;
+import static com.android.net.module.util.netlink.StructNlAttr.makeNestedType;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REPLACE;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import static java.nio.ByteOrder.BIG_ENDIAN;
+
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Objects;
+
+/**
+ * A NetlinkMessage subclass for netlink conntrack messages.
+ *
+ * see also: &lt;linux_src&gt;/include/uapi/linux/netfilter/nfnetlink_conntrack.h
+ *
+ * @hide
+ */
+public class ConntrackMessage extends NetlinkMessage {
+    public static final int STRUCT_SIZE = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
+
+    // enum ctattr_type
+    public static final short CTA_TUPLE_ORIG  = 1;
+    public static final short CTA_TUPLE_REPLY = 2;
+    public static final short CTA_STATUS      = 3;
+    public static final short CTA_TIMEOUT     = 7;
+
+    // enum ctattr_tuple
+    public static final short CTA_TUPLE_IP    = 1;
+    public static final short CTA_TUPLE_PROTO = 2;
+
+    // enum ctattr_ip
+    public static final short CTA_IP_V4_SRC = 1;
+    public static final short CTA_IP_V4_DST = 2;
+
+    // enum ctattr_l4proto
+    public static final short CTA_PROTO_NUM      = 1;
+    public static final short CTA_PROTO_SRC_PORT = 2;
+    public static final short CTA_PROTO_DST_PORT = 3;
+
+    // enum ip_conntrack_status
+    public static final int IPS_EXPECTED      = 0x00000001;
+    public static final int IPS_SEEN_REPLY    = 0x00000002;
+    public static final int IPS_ASSURED       = 0x00000004;
+    public static final int IPS_CONFIRMED     = 0x00000008;
+    public static final int IPS_SRC_NAT       = 0x00000010;
+    public static final int IPS_DST_NAT       = 0x00000020;
+    public static final int IPS_SEQ_ADJUST    = 0x00000040;
+    public static final int IPS_SRC_NAT_DONE  = 0x00000080;
+    public static final int IPS_DST_NAT_DONE  = 0x00000100;
+    public static final int IPS_DYING         = 0x00000200;
+    public static final int IPS_FIXED_TIMEOUT = 0x00000400;
+    public static final int IPS_TEMPLATE      = 0x00000800;
+    public static final int IPS_UNTRACKED     = 0x00001000;
+    public static final int IPS_HELPER        = 0x00002000;
+    public static final int IPS_OFFLOAD       = 0x00004000;
+    public static final int IPS_HW_OFFLOAD    = 0x00008000;
+
+    // ip_conntrack_status mask
+    // Interesting on the NAT conntrack session which has already seen two direction traffic.
+    // TODO: Probably IPS_{SRC, DST}_NAT_DONE are also interesting.
+    public static final int ESTABLISHED_MASK = IPS_CONFIRMED | IPS_ASSURED | IPS_SEEN_REPLY
+            | IPS_SRC_NAT;
+    // Interesting on the established NAT conntrack session which is dying.
+    public static final int DYING_MASK = ESTABLISHED_MASK | IPS_DYING;
+
+    /**
+     * A tuple for the conntrack connection information.
+     *
+     * see also CTA_TUPLE_ORIG and CTA_TUPLE_REPLY.
+     */
+    public static class Tuple {
+        public final Inet4Address srcIp;
+        public final Inet4Address dstIp;
+
+        // Both port and protocol number are unsigned numbers stored in signed integers, and that
+        // callers that want to compare them to integers should either cast those integers, or
+        // convert them to unsigned using Byte.toUnsignedInt() and Short.toUnsignedInt().
+        public final short srcPort;
+        public final short dstPort;
+        public final byte protoNum;
+
+        public Tuple(TupleIpv4 ip, TupleProto proto) {
+            this.srcIp = ip.src;
+            this.dstIp = ip.dst;
+            this.srcPort = proto.srcPort;
+            this.dstPort = proto.dstPort;
+            this.protoNum = proto.protoNum;
+        }
+
+        @Override
+        @VisibleForTesting
+        public boolean equals(Object o) {
+            if (!(o instanceof Tuple)) return false;
+            Tuple that = (Tuple) o;
+            return Objects.equals(this.srcIp, that.srcIp)
+                    && Objects.equals(this.dstIp, that.dstIp)
+                    && this.srcPort == that.srcPort
+                    && this.dstPort == that.dstPort
+                    && this.protoNum == that.protoNum;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(srcIp, dstIp, srcPort, dstPort, protoNum);
+        }
+
+        @Override
+        public String toString() {
+            final String srcIpStr = (srcIp == null) ? "null" : srcIp.getHostAddress();
+            final String dstIpStr = (dstIp == null) ? "null" : dstIp.getHostAddress();
+            final String protoStr = NetlinkConstants.stringForProtocol(protoNum);
+
+            return "Tuple{"
+                    + protoStr + ": "
+                    + srcIpStr + ":" + Short.toUnsignedInt(srcPort) + " -> "
+                    + dstIpStr + ":" + Short.toUnsignedInt(dstPort)
+                    + "}";
+        }
+    }
+
+    /**
+     * A tuple for the conntrack connection address.
+     *
+     * see also CTA_TUPLE_IP.
+     */
+    public static class TupleIpv4 {
+        public final Inet4Address src;
+        public final Inet4Address dst;
+
+        public TupleIpv4(Inet4Address src, Inet4Address dst) {
+            this.src = src;
+            this.dst = dst;
+        }
+    }
+
+    /**
+     * A tuple for the conntrack connection protocol.
+     *
+     * see also CTA_TUPLE_PROTO.
+     */
+    public static class TupleProto {
+        public final byte protoNum;
+        public final short srcPort;
+        public final short dstPort;
+
+        public TupleProto(byte protoNum, short srcPort, short dstPort) {
+            this.protoNum = protoNum;
+            this.srcPort = srcPort;
+            this.dstPort = dstPort;
+        }
+    }
+
+    /**
+     * Create a netlink message to refresh IPv4 conntrack entry timeout.
+     */
+    public static byte[] newIPv4TimeoutUpdateRequest(
+            int proto, Inet4Address src, int sport, Inet4Address dst, int dport, int timeoutSec) {
+        // *** STYLE WARNING ***
+        //
+        // Code below this point uses extra block indentation to highlight the
+        // packing of nested tuple netlink attribute types.
+        final StructNlAttr ctaTupleOrig = new StructNlAttr(CTA_TUPLE_ORIG,
+                new StructNlAttr(CTA_TUPLE_IP,
+                        new StructNlAttr(CTA_IP_V4_SRC, src),
+                        new StructNlAttr(CTA_IP_V4_DST, dst)),
+                new StructNlAttr(CTA_TUPLE_PROTO,
+                        new StructNlAttr(CTA_PROTO_NUM, (byte) proto),
+                        new StructNlAttr(CTA_PROTO_SRC_PORT, (short) sport, BIG_ENDIAN),
+                        new StructNlAttr(CTA_PROTO_DST_PORT, (short) dport, BIG_ENDIAN)));
+
+        final StructNlAttr ctaTimeout = new StructNlAttr(CTA_TIMEOUT, timeoutSec, BIG_ENDIAN);
+
+        final int payloadLength = ctaTupleOrig.getAlignedLength() + ctaTimeout.getAlignedLength();
+        final byte[] bytes = new byte[STRUCT_SIZE + payloadLength];
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        final ConntrackMessage ctmsg = new ConntrackMessage();
+        ctmsg.mHeader.nlmsg_len = bytes.length;
+        ctmsg.mHeader.nlmsg_type = (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8)
+                | NetlinkConstants.IPCTNL_MSG_CT_NEW;
+        ctmsg.mHeader.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_REPLACE;
+        ctmsg.mHeader.nlmsg_seq = 1;
+        ctmsg.pack(byteBuffer);
+
+        ctaTupleOrig.pack(byteBuffer);
+        ctaTimeout.pack(byteBuffer);
+
+        return bytes;
+    }
+
+    /**
+     * Parses a netfilter conntrack message from a {@link ByteBuffer}.
+     *
+     * @param header the netlink message header.
+     * @param byteBuffer The buffer from which to parse the netfilter conntrack message.
+     * @return the parsed netfilter conntrack message, or {@code null} if the netfilter conntrack
+     *         message could not be parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static ConntrackMessage parse(@NonNull StructNlMsgHdr header,
+            @NonNull ByteBuffer byteBuffer) {
+        // Just build the netlink header and netfilter header for now and pretend the whole message
+        // was consumed.
+        // TODO: Parse the conntrack attributes.
+        final StructNfGenMsg nfGenMsg = StructNfGenMsg.parse(byteBuffer);
+        if (nfGenMsg == null) {
+            return null;
+        }
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(CTA_STATUS, byteBuffer);
+        int status = 0;
+        if (nlAttr != null) {
+            status = nlAttr.getValueAsBe32(0);
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(CTA_TIMEOUT, byteBuffer);
+        int timeoutSec = 0;
+        if (nlAttr != null) {
+            timeoutSec = nlAttr.getValueAsBe32(0);
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_ORIG), byteBuffer);
+        Tuple tupleOrig = null;
+        if (nlAttr != null) {
+            tupleOrig = parseTuple(nlAttr.getValueAsByteBuffer());
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_REPLY), byteBuffer);
+        Tuple tupleReply = null;
+        if (nlAttr != null) {
+            tupleReply = parseTuple(nlAttr.getValueAsByteBuffer());
+        }
+
+        // Advance to the end of the message.
+        byteBuffer.position(baseOffset);
+        final int kMinConsumed = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
+        final int kAdditionalSpace = NetlinkConstants.alignedLengthOf(
+                header.nlmsg_len - kMinConsumed);
+        if (byteBuffer.remaining() < kAdditionalSpace) {
+            return null;
+        }
+        byteBuffer.position(baseOffset + kAdditionalSpace);
+
+        return new ConntrackMessage(header, nfGenMsg, tupleOrig, tupleReply, status, timeoutSec);
+    }
+
+    /**
+     * Parses a conntrack tuple from a {@link ByteBuffer}.
+     *
+     * The attribute parsing is interesting on:
+     * - CTA_TUPLE_IP
+     *     CTA_IP_V4_SRC
+     *     CTA_IP_V4_DST
+     * - CTA_TUPLE_PROTO
+     *     CTA_PROTO_NUM
+     *     CTA_PROTO_SRC_PORT
+     *     CTA_PROTO_DST_PORT
+     *
+     * Assume that the minimum size is the sum of CTA_TUPLE_IP (size: 20) and CTA_TUPLE_PROTO
+     * (size: 28). Here is an example for an expected CTA_TUPLE_ORIG message in raw data:
+     * +--------------------------------------------------------------------------------------+
+     * | CTA_TUPLE_ORIG                                                                       |
+     * +--------------------------+-----------------------------------------------------------+
+     * | 1400                     | nla_len = 20                                              |
+     * | 0180                     | nla_type = nested CTA_TUPLE_IP                            |
+     * |     0800 0100 C0A8500C   |     nla_type=CTA_IP_V4_SRC, ip=192.168.80.12              |
+     * |     0800 0200 8C700874   |     nla_type=CTA_IP_V4_DST, ip=140.112.8.116              |
+     * | 1C00                     | nla_len = 28                                              |
+     * | 0280                     | nla_type = nested CTA_TUPLE_PROTO                         |
+     * |     0500 0100 06 000000  |     nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)         |
+     * |     0600 0200 F3F1 0000  |     nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)  |
+     * |     0600 0300 01BB 0000  |     nla_type=CTA_PROTO_DST_PORT, port=433 (big endian)    |
+     * +--------------------------+-----------------------------------------------------------+
+     *
+     * The position of the byte buffer doesn't set to the end when the function returns. It is okay
+     * because the caller ConntrackMessage#parse has passed a copy which is used for this parser
+     * only. Moreover, the parser behavior is the same as other existing netlink struct class
+     * parser. Ex: StructInetDiagMsg#parse.
+     */
+    @Nullable
+    private static Tuple parseTuple(@Nullable ByteBuffer byteBuffer) {
+        if (byteBuffer == null) return null;
+
+        TupleIpv4 tupleIpv4 = null;
+        TupleProto tupleProto = null;
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_IP), byteBuffer);
+        if (nlAttr != null) {
+            tupleIpv4 = parseTupleIpv4(nlAttr.getValueAsByteBuffer());
+        }
+        if (tupleIpv4 == null) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_PROTO), byteBuffer);
+        if (nlAttr != null) {
+            tupleProto = parseTupleProto(nlAttr.getValueAsByteBuffer());
+        }
+        if (tupleProto == null) return null;
+
+        return new Tuple(tupleIpv4, tupleProto);
+    }
+
+    @Nullable
+    private static Inet4Address castToInet4Address(@Nullable InetAddress address) {
+        if (address == null || !(address instanceof Inet4Address)) return null;
+        return (Inet4Address) address;
+    }
+
+    @Nullable
+    private static TupleIpv4 parseTupleIpv4(@Nullable ByteBuffer byteBuffer) {
+        if (byteBuffer == null) return null;
+
+        Inet4Address src = null;
+        Inet4Address dst = null;
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(CTA_IP_V4_SRC, byteBuffer);
+        if (nlAttr != null) {
+            src = castToInet4Address(nlAttr.getValueAsInetAddress());
+        }
+        if (src == null) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(CTA_IP_V4_DST, byteBuffer);
+        if (nlAttr != null) {
+            dst = castToInet4Address(nlAttr.getValueAsInetAddress());
+        }
+        if (dst == null) return null;
+
+        return new TupleIpv4(src, dst);
+    }
+
+    @Nullable
+    private static TupleProto parseTupleProto(@Nullable ByteBuffer byteBuffer) {
+        if (byteBuffer == null) return null;
+
+        byte protoNum = 0;
+        short srcPort = 0;
+        short dstPort = 0;
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(CTA_PROTO_NUM, byteBuffer);
+        if (nlAttr != null) {
+            protoNum = nlAttr.getValueAsByte((byte) 0);
+        }
+        if (!(protoNum == IPPROTO_TCP || protoNum == IPPROTO_UDP)) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(CTA_PROTO_SRC_PORT, byteBuffer);
+        if (nlAttr != null) {
+            srcPort = nlAttr.getValueAsBe16((short) 0);
+        }
+        if (srcPort == 0) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(CTA_PROTO_DST_PORT, byteBuffer);
+        if (nlAttr != null) {
+            dstPort = nlAttr.getValueAsBe16((short) 0);
+        }
+        if (dstPort == 0) return null;
+
+        return new TupleProto(protoNum, srcPort, dstPort);
+    }
+
+    /**
+     * Netfilter header.
+     */
+    public final StructNfGenMsg nfGenMsg;
+    /**
+     * Original direction conntrack tuple.
+     *
+     * The tuple is determined by the parsed attribute value CTA_TUPLE_ORIG, or null if the
+     * tuple could not be parsed successfully (for example, if it was truncated or absent).
+     */
+    @Nullable
+    public final Tuple tupleOrig;
+    /**
+     * Reply direction conntrack tuple.
+     *
+     * The tuple is determined by the parsed attribute value CTA_TUPLE_REPLY, or null if the
+     * tuple could not be parsed successfully (for example, if it was truncated or absent).
+     */
+    @Nullable
+    public final Tuple tupleReply;
+    /**
+     * Connection status. A bitmask of ip_conntrack_status enum flags.
+     *
+     * The status is determined by the parsed attribute value CTA_STATUS, or 0 if the status could
+     * not be parsed successfully (for example, if it was truncated or absent). For the message
+     * from kernel, the valid status is non-zero. For the message from user space, the status may
+     * be 0 (absent).
+     */
+    public final int status;
+    /**
+     * Conntrack timeout.
+     *
+     * The timeout is determined by the parsed attribute value CTA_TIMEOUT, or 0 if the timeout
+     * could not be parsed successfully (for example, if it was truncated or absent). For
+     * IPCTNL_MSG_CT_NEW event, the valid timeout is non-zero. For IPCTNL_MSG_CT_DELETE event, the
+     * timeout is 0 (absent).
+     */
+    public final int timeoutSec;
+
+    private ConntrackMessage() {
+        super(new StructNlMsgHdr());
+        nfGenMsg = new StructNfGenMsg((byte) OsConstants.AF_INET);
+
+        // This constructor is only used by #newIPv4TimeoutUpdateRequest which doesn't use these
+        // data member for packing message. Simply fill them to null or 0.
+        tupleOrig = null;
+        tupleReply = null;
+        status = 0;
+        timeoutSec = 0;
+    }
+
+    private ConntrackMessage(@NonNull StructNlMsgHdr header, @NonNull StructNfGenMsg nfGenMsg,
+            @Nullable Tuple tupleOrig, @Nullable Tuple tupleReply, int status, int timeoutSec) {
+        super(header);
+        this.nfGenMsg = nfGenMsg;
+        this.tupleOrig = tupleOrig;
+        this.tupleReply = tupleReply;
+        this.status = status;
+        this.timeoutSec = timeoutSec;
+    }
+
+    /**
+     * Write a netfilter message to {@link ByteBuffer}.
+     */
+    public void pack(ByteBuffer byteBuffer) {
+        mHeader.pack(byteBuffer);
+        nfGenMsg.pack(byteBuffer);
+    }
+
+    public short getMessageType() {
+        return (short) (getHeader().nlmsg_type & ~(NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8));
+    }
+
+    /**
+     * Convert an ip conntrack status to a string.
+     */
+    public static String stringForIpConntrackStatus(int flags) {
+        final StringBuilder sb = new StringBuilder();
+
+        if ((flags & IPS_EXPECTED) != 0) {
+            sb.append("IPS_EXPECTED");
+        }
+        if ((flags & IPS_SEEN_REPLY) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_SEEN_REPLY");
+        }
+        if ((flags & IPS_ASSURED) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_ASSURED");
+        }
+        if ((flags & IPS_CONFIRMED) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_CONFIRMED");
+        }
+        if ((flags & IPS_SRC_NAT) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_SRC_NAT");
+        }
+        if ((flags & IPS_DST_NAT) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_DST_NAT");
+        }
+        if ((flags & IPS_SEQ_ADJUST) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_SEQ_ADJUST");
+        }
+        if ((flags & IPS_SRC_NAT_DONE) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_SRC_NAT_DONE");
+        }
+        if ((flags & IPS_DST_NAT_DONE) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_DST_NAT_DONE");
+        }
+        if ((flags & IPS_DYING) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_DYING");
+        }
+        if ((flags & IPS_FIXED_TIMEOUT) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_FIXED_TIMEOUT");
+        }
+        if ((flags & IPS_TEMPLATE) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_TEMPLATE");
+        }
+        if ((flags & IPS_UNTRACKED) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_UNTRACKED");
+        }
+        if ((flags & IPS_HELPER) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_HELPER");
+        }
+        if ((flags & IPS_OFFLOAD) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_OFFLOAD");
+        }
+        if ((flags & IPS_HW_OFFLOAD) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_HW_OFFLOAD");
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public String toString() {
+        return "ConntrackMessage{"
+                + "nlmsghdr{"
+                + (mHeader == null ? "" : mHeader.toString(OsConstants.NETLINK_NETFILTER))
+                + "}, "
+                + "nfgenmsg{" + nfGenMsg + "}, "
+                + "tuple_orig{" + tupleOrig + "}, "
+                + "tuple_reply{" + tupleReply + "}, "
+                + "status{" + status + "(" + stringForIpConntrackStatus(status) + ")" + "}, "
+                + "timeout_sec{" + Integer.toUnsignedLong(timeoutSec) + "}"
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
new file mode 100644
index 0000000..fecaa09
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -0,0 +1,496 @@
+/*
+ * 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.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static android.os.Process.INVALID_UID;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.ENOENT;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.NETLINK_INET_DIAG;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DESTROY;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForAddressFamily;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForProtocol;
+import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
+import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
+import static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_RECV_BUFSIZE;
+import static com.android.net.module.util.netlink.NetlinkUtils.TCP_ALIVE_STATE_FILTER;
+import static com.android.net.module.util.netlink.NetlinkUtils.connectToKernel;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import android.net.util.SocketUtils;
+import android.os.Process;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.util.Log;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * A NetlinkMessage subclass for netlink inet_diag messages.
+ *
+ * see also: &lt;linux_src&gt;/include/uapi/linux/inet_diag.h
+ *
+ * @hide
+ */
+public class InetDiagMessage extends NetlinkMessage {
+    public static final String TAG = "InetDiagMessage";
+    private static final int TIMEOUT_MS = 500;
+
+    /**
+     * Construct an inet_diag_req_v2 message. This method will throw
+     * {@link IllegalArgumentException} if local and remote are not both null or both non-null.
+     */
+    public static byte[] inetDiagReqV2(int protocol, InetSocketAddress local,
+            InetSocketAddress remote, int family, short flags) {
+        return inetDiagReqV2(protocol, local, remote, family, flags, 0 /* pad */,
+                0 /* idiagExt */, StructInetDiagReqV2.INET_DIAG_REQ_V2_ALL_STATES);
+    }
+
+    /**
+     * Construct an inet_diag_req_v2 message. This method will throw
+     * {@code IllegalArgumentException} if local and remote are not both null or both non-null.
+     *
+     * @param protocol the request protocol type. This should be set to one of IPPROTO_TCP,
+     *                 IPPROTO_UDP, or IPPROTO_UDPLITE.
+     * @param local local socket address of the target socket. This will be packed into a
+     *              {@link StructInetDiagSockId}. Request to diagnose for all sockets if both of
+     *              local or remote address is null.
+     * @param remote remote socket address of the target socket. This will be packed into a
+     *              {@link StructInetDiagSockId}. Request to diagnose for all sockets if both of
+     *              local or remote address is null.
+     * @param family the ip family of the request message. This should be set to either AF_INET or
+     *               AF_INET6 for IPv4 or IPv6 sockets respectively.
+     * @param flags message flags. See &lt;linux_src&gt;/include/uapi/linux/netlink.h.
+     * @param pad for raw socket protocol specification.
+     * @param idiagExt a set of flags defining what kind of extended information to report.
+     * @param state a bit mask that defines a filter of socket states.
+     *
+     * @return bytes array representation of the message
+     */
+    public static byte[] inetDiagReqV2(int protocol, @Nullable InetSocketAddress local,
+            @Nullable InetSocketAddress remote, int family, short flags, int pad, int idiagExt,
+            int state) throws IllegalArgumentException {
+        // Request for all sockets if no specific socket is requested. Specify the local and remote
+        // socket address information for target request socket.
+        if ((local == null) != (remote == null)) {
+            throw new IllegalArgumentException(
+                    "Local and remote must be both null or both non-null");
+        }
+        final StructInetDiagSockId id = ((local != null && remote != null)
+                ? new StructInetDiagSockId(local, remote) : null);
+        return inetDiagReqV2(protocol, id, family,
+                SOCK_DIAG_BY_FAMILY, flags, pad, idiagExt, state);
+    }
+
+    /**
+     * Construct an inet_diag_req_v2 message.
+     *
+     * @param protocol the request protocol type. This should be set to one of IPPROTO_TCP,
+     *                 IPPROTO_UDP, or IPPROTO_UDPLITE.
+     * @param id inet_diag_sockid. See {@link StructInetDiagSockId}
+     * @param family the ip family of the request message. This should be set to either AF_INET or
+     *               AF_INET6 for IPv4 or IPv6 sockets respectively.
+     * @param type message types.
+     * @param flags message flags. See &lt;linux_src&gt;/include/uapi/linux/netlink.h.
+     * @param pad for raw socket protocol specification.
+     * @param idiagExt a set of flags defining what kind of extended information to report.
+     * @param state a bit mask that defines a filter of socket states.
+     * @return bytes array representation of the message
+     */
+    public static byte[] inetDiagReqV2(int protocol, @Nullable StructInetDiagSockId id, int family,
+            short type, short flags, int pad, int idiagExt, int state) {
+        final byte[] bytes = new byte[StructNlMsgHdr.STRUCT_SIZE + StructInetDiagReqV2.STRUCT_SIZE];
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        final StructNlMsgHdr nlMsgHdr = new StructNlMsgHdr();
+        nlMsgHdr.nlmsg_len = bytes.length;
+        nlMsgHdr.nlmsg_type = type;
+        nlMsgHdr.nlmsg_flags = flags;
+        nlMsgHdr.pack(byteBuffer);
+        final StructInetDiagReqV2 inetDiagReqV2 =
+                new StructInetDiagReqV2(protocol, id, family, pad, idiagExt, state);
+
+        inetDiagReqV2.pack(byteBuffer);
+        return bytes;
+    }
+
+    public StructInetDiagMsg inetDiagMsg;
+    // The netlink attributes.
+    public List<StructNlAttr> nlAttrs = new ArrayList<>();
+    @VisibleForTesting
+    public InetDiagMessage(@NonNull StructNlMsgHdr header) {
+        super(header);
+        inetDiagMsg = new StructInetDiagMsg();
+    }
+
+    /**
+     * Parse an inet_diag_req_v2 message from buffer.
+     */
+    @Nullable
+    public static InetDiagMessage parse(@NonNull StructNlMsgHdr header,
+            @NonNull ByteBuffer byteBuffer) {
+        final InetDiagMessage msg = new InetDiagMessage(header);
+        msg.inetDiagMsg = StructInetDiagMsg.parse(byteBuffer);
+        if (msg.inetDiagMsg == null) {
+            return null;
+        }
+        final int payloadLength = header.nlmsg_len - SOCKDIAG_MSG_HEADER_SIZE;
+        final ByteBuffer payload = byteBuffer.slice();
+        while (payload.position() < payloadLength) {
+            final StructNlAttr attr = StructNlAttr.parse(payload);
+            // Stop parsing for truncated or malformed attribute
+            if (attr == null)  {
+                Log.wtf(TAG, "Got truncated or malformed attribute");
+                return null;
+            }
+
+            msg.nlAttrs.add(attr);
+        }
+
+        return msg;
+    }
+
+    private static void closeSocketQuietly(final FileDescriptor fd) {
+        try {
+            SocketUtils.closeSocket(fd);
+        } catch (IOException ignored) {
+        }
+    }
+
+    private static int lookupUidByFamily(int protocol, InetSocketAddress local,
+                                         InetSocketAddress remote, int family, short flags,
+                                         FileDescriptor fd)
+            throws ErrnoException, InterruptedIOException {
+        byte[] msg = inetDiagReqV2(protocol, local, remote, family, flags);
+        NetlinkUtils.sendMessage(fd, msg, 0, msg.length, TIMEOUT_MS);
+        ByteBuffer response = NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TIMEOUT_MS);
+
+        final NetlinkMessage nlMsg = NetlinkMessage.parse(response, NETLINK_INET_DIAG);
+        if (nlMsg == null) {
+            return INVALID_UID;
+        }
+        final StructNlMsgHdr hdr = nlMsg.getHeader();
+        if (hdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
+            return INVALID_UID;
+        }
+        if (nlMsg instanceof InetDiagMessage) {
+            return ((InetDiagMessage) nlMsg).inetDiagMsg.idiag_uid;
+        }
+        return INVALID_UID;
+    }
+
+    private static final int[] FAMILY = {AF_INET6, AF_INET};
+
+    private static int lookupUid(int protocol, InetSocketAddress local,
+                                 InetSocketAddress remote, FileDescriptor fd)
+            throws ErrnoException, InterruptedIOException {
+        int uid;
+
+        for (int family : FAMILY) {
+            /**
+             * For exact match lookup, swap local and remote for UDP lookups due to kernel
+             * bug which will not be fixed. See aosp/755889 and
+             * https://www.mail-archive.com/netdev@vger.kernel.org/msg248638.html
+             */
+            if (protocol == IPPROTO_UDP) {
+                uid = lookupUidByFamily(protocol, remote, local, family, NLM_F_REQUEST, fd);
+            } else {
+                uid = lookupUidByFamily(protocol, local, remote, family, NLM_F_REQUEST, fd);
+            }
+            if (uid != INVALID_UID) {
+                return uid;
+            }
+        }
+
+        /**
+         * For UDP it's possible for a socket to send packets to arbitrary destinations, even if the
+         * socket is not connected (and even if the socket is connected to a different destination).
+         * If we want this API to work for such packets, then on miss we need to do a second lookup
+         * with only the local address and port filled in.
+         * Always use flags == NLM_F_REQUEST | NLM_F_DUMP for wildcard.
+         */
+        if (protocol == IPPROTO_UDP) {
+            try {
+                InetSocketAddress wildcard = new InetSocketAddress(
+                        Inet6Address.getByName("::"), 0);
+                uid = lookupUidByFamily(protocol, local, wildcard, AF_INET6,
+                        (short) (NLM_F_REQUEST | NLM_F_DUMP), fd);
+                if (uid != INVALID_UID) {
+                    return uid;
+                }
+                wildcard = new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), 0);
+                uid = lookupUidByFamily(protocol, local, wildcard, AF_INET,
+                        (short) (NLM_F_REQUEST | NLM_F_DUMP), fd);
+                if (uid != INVALID_UID) {
+                    return uid;
+                }
+            } catch (UnknownHostException e) {
+                Log.e(TAG, e.toString());
+            }
+        }
+        return INVALID_UID;
+    }
+
+    /**
+     * Use an inet_diag socket to look up the UID associated with the input local and remote
+     * address/port and protocol of a connection.
+     */
+    public static int getConnectionOwnerUid(int protocol, InetSocketAddress local,
+                                            InetSocketAddress remote) {
+        int uid = INVALID_UID;
+        FileDescriptor fd = null;
+        try {
+            fd = NetlinkUtils.netlinkSocketForProto(NETLINK_INET_DIAG, SOCKET_RECV_BUFSIZE);
+            connectToKernel(fd);
+            uid = lookupUid(protocol, local, remote, fd);
+        } catch (ErrnoException | SocketException | IllegalArgumentException
+                | InterruptedIOException e) {
+            Log.e(TAG, e.toString());
+        } finally {
+            closeSocketQuietly(fd);
+        }
+        return uid;
+    }
+
+    /**
+     * Construct an inet_diag_req_v2 message for querying alive TCP sockets from kernel.
+     */
+    public static byte[] buildInetDiagReqForAliveTcpSockets(int family) {
+        return inetDiagReqV2(IPPROTO_TCP,
+                null /* local addr */,
+                null /* remote addr */,
+                family,
+                (short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP) /* flag */,
+                0 /* pad */,
+                1 << NetlinkConstants.INET_DIAG_MEMINFO /* idiagExt */,
+                TCP_ALIVE_STATE_FILTER);
+    }
+
+    private static void sendNetlinkDestroyRequest(FileDescriptor fd, int proto,
+            InetDiagMessage diagMsg) throws InterruptedIOException, ErrnoException {
+        final byte[] destroyMsg = InetDiagMessage.inetDiagReqV2(
+                proto,
+                diagMsg.inetDiagMsg.id,
+                diagMsg.inetDiagMsg.idiag_family,
+                SOCK_DESTROY,
+                (short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_ACK),
+                0 /* pad */,
+                0 /* idiagExt */,
+                1 << diagMsg.inetDiagMsg.idiag_state
+        );
+        NetlinkUtils.sendMessage(fd, destroyMsg, 0, destroyMsg.length, IO_TIMEOUT_MS);
+        NetlinkUtils.receiveNetlinkAck(fd);
+    }
+
+    private static byte [] makeNetlinkDumpRequest(int proto, int states, int family) {
+        return InetDiagMessage.inetDiagReqV2(
+                proto,
+                null /* id */,
+                family,
+                SOCK_DIAG_BY_FAMILY,
+                (short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP),
+                0 /* pad */,
+                0 /* idiagExt */,
+                states);
+    }
+
+    private static int processNetlinkDumpAndDestroySockets(byte[] dumpReq,
+            FileDescriptor destroyFd, int proto, Predicate<InetDiagMessage> filter)
+            throws SocketException, InterruptedIOException, ErrnoException {
+        AtomicInteger destroyedSockets = new AtomicInteger(0);
+        Consumer<InetDiagMessage> handleNlDumpMsg = (diagMsg) -> {
+            if (filter.test(diagMsg)) {
+                try {
+                    sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
+                    destroyedSockets.getAndIncrement();
+                } catch (InterruptedIOException | ErrnoException e) {
+                    if (!(e instanceof ErrnoException
+                            && ((ErrnoException) e).errno == ENOENT)) {
+                        Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
+                    }
+                }
+            }
+        };
+
+        NetlinkUtils.<InetDiagMessage>getAndProcessNetlinkDumpMessages(dumpReq,
+                NETLINK_INET_DIAG, InetDiagMessage.class, handleNlDumpMsg);
+        return destroyedSockets.get();
+    }
+
+    /**
+     * Returns whether the InetDiagMessage is for adb socket or not
+     */
+    @VisibleForTesting
+    public static boolean isAdbSocket(final InetDiagMessage msg) {
+        // This is inaccurate since adb could run with ROOT_UID or other services can run with
+        // SHELL_UID. But this check covers most cases and enough.
+        // Note that getting service.adb.tcp.port system property is prohibited by sepolicy
+        // TODO: skip the socket only if there is a listen socket owned by SHELL_UID with the same
+        // source port as this socket
+        return msg.inetDiagMsg.idiag_uid == Process.SHELL_UID;
+    }
+
+    /**
+     * Returns whether the range contains the uid in the InetDiagMessage or not
+     */
+    @VisibleForTesting
+    public static boolean containsUid(InetDiagMessage msg, Set<Range<Integer>> ranges) {
+        for (final Range<Integer> range: ranges) {
+            if (range.contains(msg.inetDiagMsg.idiag_uid)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isLoopbackAddress(InetAddress addr) {
+        if (addr.isLoopbackAddress()) return true;
+        if (!(addr instanceof Inet6Address)) return false;
+
+        // Following check is for v4-mapped v6 address. StructInetDiagSockId contains v4-mapped v6
+        // address as Inet6Address, See StructInetDiagSockId#parse
+        final byte[] addrBytes = addr.getAddress();
+        for (int i = 0; i < 10; i++) {
+            if (addrBytes[i] != 0) return false;
+        }
+        return addrBytes[10] == (byte) 0xff
+                && addrBytes[11] == (byte) 0xff
+                && addrBytes[12] == 127;
+    }
+
+    /**
+     * Returns whether the socket address in the InetDiagMessage is loopback or not
+     */
+    @VisibleForTesting
+    public static boolean isLoopback(InetDiagMessage msg) {
+        final InetAddress srcAddr = msg.inetDiagMsg.id.locSocketAddress.getAddress();
+        final InetAddress dstAddr = msg.inetDiagMsg.id.remSocketAddress.getAddress();
+        return isLoopbackAddress(srcAddr)
+                || isLoopbackAddress(dstAddr)
+                || srcAddr.equals(dstAddr);
+    }
+
+    private static void destroySockets(int proto, int states, Predicate<InetDiagMessage> filter)
+            throws ErrnoException, SocketException, InterruptedIOException {
+        FileDescriptor destroyFd = null;
+
+        try {
+            destroyFd = NetlinkUtils.createNetLinkInetDiagSocket();
+            connectToKernel(destroyFd);
+
+            for (int family : List.of(AF_INET, AF_INET6)) {
+                byte[] req = makeNetlinkDumpRequest(proto, states, family);
+
+                try {
+                    final int destroyedSockets = processNetlinkDumpAndDestroySockets(
+                            req, destroyFd, proto, filter);
+                    Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+                        + ", proto=" + stringForProtocol(proto)
+                        + ", family=" + stringForAddressFamily(family)
+                        + ", states=" + states);
+                } catch (SocketException | InterruptedIOException | ErrnoException e) {
+                    Log.e(TAG, "Failed to send netlink dump request or receive messages: " + e);
+                    continue;
+                }
+            }
+        } finally {
+            closeSocketQuietly(destroyFd);
+        }
+    }
+
+    /**
+     * Close tcp sockets that match the following condition
+     *  1. TCP status is one of TCP_ESTABLISHED, TCP_SYN_SENT, and TCP_SYN_RECV
+     *  2. Owner uid of socket is not in the exemptUids
+     *  3. Owner uid of socket is in the ranges
+     *  4. Socket is not loopback
+     *  5. Socket is not adb socket
+     *
+     * @param ranges target uid ranges
+     * @param exemptUids uids to skip close socket
+     */
+    public static void destroyLiveTcpSockets(Set<Range<Integer>> ranges, Set<Integer> exemptUids)
+            throws SocketException, InterruptedIOException, ErrnoException {
+        final long startTimeMs = SystemClock.elapsedRealtime();
+        destroySockets(IPPROTO_TCP, TCP_ALIVE_STATE_FILTER,
+                (diagMsg) -> !exemptUids.contains(diagMsg.inetDiagMsg.idiag_uid)
+                        && containsUid(diagMsg, ranges)
+                        && !isLoopback(diagMsg)
+                        && !isAdbSocket(diagMsg));
+        final long durationMs = SystemClock.elapsedRealtime() - startTimeMs;
+        Log.d(TAG, "Destroyed live tcp sockets for uids=" + ranges + " exemptUids=" + exemptUids
+                + " in " + durationMs + "ms");
+    }
+
+    /**
+     * Close tcp sockets that match the following condition
+     *  1. TCP status is one of TCP_ESTABLISHED, TCP_SYN_SENT, and TCP_SYN_RECV
+     *  2. Owner uid of socket is in the targetUids
+     *  3. Socket is not loopback
+     *  4. Socket is not adb socket
+     *
+     * @param ownerUids target uids to close sockets
+     */
+    public static void destroyLiveTcpSocketsByOwnerUids(Set<Integer> ownerUids)
+            throws SocketException, InterruptedIOException, ErrnoException {
+        final long startTimeMs = SystemClock.elapsedRealtime();
+        destroySockets(IPPROTO_TCP, TCP_ALIVE_STATE_FILTER,
+                (diagMsg) -> ownerUids.contains(diagMsg.inetDiagMsg.idiag_uid)
+                        && !isLoopback(diagMsg)
+                        && !isAdbSocket(diagMsg));
+        final long durationMs = SystemClock.elapsedRealtime() - startTimeMs;
+        Log.d(TAG, "Destroyed live tcp sockets for uids=" + ownerUids + " in " + durationMs + "ms");
+    }
+
+    @Override
+    public String toString() {
+        return "InetDiagMessage{ "
+                + "nlmsghdr{"
+                + (mHeader == null ? "" : mHeader.toString(NETLINK_INET_DIAG)) + "}, "
+                + "inet_diag_msg{"
+                + (inetDiagMsg == null ? "" : inetDiagMsg.toString()) + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NdOption.java b/staticlibs/device/com/android/net/module/util/netlink/NdOption.java
new file mode 100644
index 0000000..4f58380
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/NdOption.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Base class for IPv6 neighbour discovery options.
+ */
+public class NdOption {
+    public static final int STRUCT_SIZE = 2;
+
+    /** The option type. */
+    public final byte type;
+    /** The length of the option in 8-byte units. Actually an unsigned 8-bit integer */
+    public final int length;
+
+    /** Constructs a new NdOption. */
+    NdOption(byte type, int length) {
+        this.type = type;
+        this.length = length;
+    }
+
+    /**
+     * Parses a neighbour discovery option.
+     *
+     * Parses (and consumes) the option if it is of a known type. If the option is of an unknown
+     * type, advances the buffer (so the caller can continue parsing if desired) and returns
+     * {@link #UNKNOWN}. If the option claims a length of 0, returns null because parsing cannot
+     * continue.
+     *
+     * No checks are performed on the length other than ensuring it is not 0, so if a caller wants
+     * to deal with options that might overflow the structure that contains them, it must explicitly
+     * set the buffer's limit to the position at which that structure ends.
+     *
+     * @param buf the buffer to parse.
+     * @return a subclass of {@link NdOption}, or {@code null} for an unknown or malformed option.
+     */
+    public static NdOption parse(@NonNull ByteBuffer buf) {
+        if (buf.remaining() < STRUCT_SIZE) return null;
+
+        // Peek the type without advancing the buffer.
+        byte type = buf.get(buf.position());
+        int length = Byte.toUnsignedInt(buf.get(buf.position() + 1));
+        if (length == 0) return null;
+
+        switch (type) {
+            case StructNdOptPref64.TYPE:
+                return StructNdOptPref64.parse(buf);
+
+            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);
+                return UNKNOWN;
+        }
+    }
+
+    void writeToByteBuffer(ByteBuffer buf) {
+        buf.put(type);
+        buf.put((byte) length);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("NdOption(%d, %d)", Byte.toUnsignedInt(type), length);
+    }
+
+    public static final NdOption UNKNOWN = new NdOption((byte) 0, 0);
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
new file mode 100644
index 0000000..586f19d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
@@ -0,0 +1,164 @@
+/*
+ * 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.netlink;
+
+import static android.system.OsConstants.AF_INET6;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A NetlinkMessage subclass for RTM_NEWNDUSEROPT messages.
+ */
+public class NduseroptMessage extends NetlinkMessage {
+    public static final int STRUCT_SIZE = 16;
+
+    static final int NDUSEROPT_SRCADDR = 1;
+
+    /** The address family. Presumably always AF_INET6. */
+    public final byte family;
+    /**
+     * The total length in bytes of the options that follow this structure.
+     * Actually a 16-bit unsigned integer.
+     */
+    public final int opts_len;
+    /** The interface index on which the options were received. */
+    public final int ifindex;
+    /** The ICMP type of the packet that contained the options. */
+    public final byte icmp_type;
+    /** The ICMP code of the packet that contained the options. */
+    public final byte icmp_code;
+
+    /**
+     * ND option that was in this message.
+     * Even though the length field is called "opts_len", the kernel only ever sends one option per
+     * message. It is unlikely that this will ever change as it would break existing userspace code.
+     * But if it does, we can simply update this code, since userspace is typically newer than the
+     * kernel.
+     */
+    @Nullable
+    public final NdOption option;
+
+    /** The IP address that sent the packet containing the option. */
+    public final InetAddress srcaddr;
+
+    @VisibleForTesting
+    public NduseroptMessage(@NonNull final StructNlMsgHdr header, byte family, int optslen,
+            int ifindex, byte icmptype, byte icmpcode, @NonNull final NdOption option,
+            final InetAddress srcaddr) {
+        super(header);
+        this.family = family;
+        this.opts_len = optslen;
+        this.ifindex = ifindex;
+        this.icmp_type = icmptype;
+        this.icmp_code = icmpcode;
+        this.option = option;
+        this.srcaddr = srcaddr;
+    }
+
+    NduseroptMessage(@NonNull StructNlMsgHdr header, @NonNull ByteBuffer buf)
+            throws UnknownHostException {
+        super(header);
+
+        // The structure itself.
+        buf.order(ByteOrder.nativeOrder());  // Restored in the finally clause inside parse().
+        final int start = buf.position();
+        family = buf.get();
+        buf.get();  // Skip 1 byte of padding.
+        opts_len = Short.toUnsignedInt(buf.getShort());
+        ifindex = buf.getInt();
+        icmp_type = buf.get();
+        icmp_code = buf.get();
+        buf.position(buf.position() + 6);  // Skip 6 bytes of padding.
+
+        // The ND option.
+        // Ensure we don't read past opts_len even if the option length is invalid.
+        // Note that this check is not really necessary since if the option length is not valid,
+        // this struct won't be very useful to the caller.
+        //
+        // It's safer to pass the slice of original ByteBuffer to just parse the ND option field,
+        // although parsing ND option might throw exception or return null, it won't break the
+        // original ByteBuffer position.
+        buf.order(ByteOrder.BIG_ENDIAN);
+        try {
+            final ByteBuffer slice = buf.slice();
+            slice.limit(opts_len);
+            option = NdOption.parse(slice);
+        } finally {
+            // Advance buffer position according to opts_len in the header. ND option length might
+            // be incorrect in the malformed packet.
+            int newPosition = start + STRUCT_SIZE + opts_len;
+            if (newPosition >= buf.limit()) {
+                throw new IllegalArgumentException("ND option extends past end of buffer");
+            }
+            buf.position(newPosition);
+        }
+
+        // The source address attribute.
+        StructNlAttr nla = StructNlAttr.parse(buf);
+        if (nla == null || nla.nla_type != NDUSEROPT_SRCADDR || nla.nla_value == null) {
+            throw new IllegalArgumentException("Invalid source address in ND useropt");
+        }
+        if (family == AF_INET6) {
+            // InetAddress.getByAddress only looks at the ifindex if the address type needs one.
+            srcaddr = Inet6Address.getByAddress(null /* hostname */, nla.nla_value, ifindex);
+        } else {
+            srcaddr = InetAddress.getByAddress(nla.nla_value);
+        }
+    }
+
+    /**
+     * Parses a StructNduseroptmsg from a {@link ByteBuffer}.
+     *
+     * @param header the netlink message header.
+     * @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
+     *         (for example, if it was truncated, or if the prefix length code was wrong).
+     */
+    @Nullable
+    public static NduseroptMessage parse(@NonNull StructNlMsgHdr header, @NonNull ByteBuffer buf) {
+        if (buf == null || buf.remaining() < STRUCT_SIZE) return null;
+        ByteOrder oldOrder = buf.order();
+        try {
+            return new NduseroptMessage(header, buf);
+        } catch (IllegalArgumentException | UnknownHostException | 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, so
+            // callers must already handle it.
+            return null;
+        } finally {
+            buf.order(oldOrder);
+        }
+    }
+
+    @Override
+    public String toString() {
+        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(), 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
new file mode 100644
index 0000000..1896de6
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.system.OsConstants;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Various constants and static helper methods for netlink communications.
+ *
+ * Values taken from:
+ *
+ *     include/uapi/linux/netfilter/nfnetlink.h
+ *     include/uapi/linux/netfilter/nfnetlink_conntrack.h
+ *     include/uapi/linux/netlink.h
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class NetlinkConstants {
+    private NetlinkConstants() {}
+
+    public static final int NLA_ALIGNTO = 4;
+    /**
+     * Flag for dumping struct tcp_info.
+     * Corresponding to enum definition in external/strace/linux/inet_diag.h.
+     */
+    public static final int INET_DIAG_MEMINFO = 1;
+
+    public static final int SOCKDIAG_MSG_HEADER_SIZE =
+            StructNlMsgHdr.STRUCT_SIZE + StructInetDiagMsg.STRUCT_SIZE;
+
+    /**
+     * Get the aligned length based on a Short type number.
+     */
+    public static final int alignedLengthOf(short length) {
+        final int intLength = (int) length & 0xffff;
+        return alignedLengthOf(intLength);
+    }
+
+    /**
+     * Get the aligned length based on a Integer type number.
+     */
+    public static final int alignedLengthOf(int length) {
+        if (length <= 0) return 0;
+        return (((length + NLA_ALIGNTO - 1) / NLA_ALIGNTO) * NLA_ALIGNTO);
+    }
+
+    /**
+     * Convert a address family type to a string.
+     */
+    public static String stringForAddressFamily(int family) {
+        if (family == OsConstants.AF_INET) return "AF_INET";
+        if (family == OsConstants.AF_INET6) return "AF_INET6";
+        if (family == OsConstants.AF_NETLINK) return "AF_NETLINK";
+        if (family == OsConstants.AF_UNSPEC) return "AF_UNSPEC";
+        return String.valueOf(family);
+    }
+
+    /**
+     * Convert a protocol type to a string.
+     */
+    public static String stringForProtocol(int protocol) {
+        if (protocol == OsConstants.IPPROTO_TCP) return "IPPROTO_TCP";
+        if (protocol == OsConstants.IPPROTO_UDP) return "IPPROTO_UDP";
+        return String.valueOf(protocol);
+    }
+
+    /**
+     * Convert a byte array to a hexadecimal string.
+     */
+    public static String hexify(byte[] bytes) {
+        if (bytes == null) return "(null)";
+        return toHexString(bytes, 0, bytes.length);
+    }
+
+    /**
+     * Convert a {@link ByteBuffer} to a hexadecimal string.
+     */
+    public static String hexify(ByteBuffer buffer) {
+        if (buffer == null) return "(null)";
+        return toHexString(
+                buffer.array(), buffer.position(), buffer.remaining());
+    }
+
+    // Known values for struct nlmsghdr nlm_type.
+    public static final short NLMSG_NOOP                    = 1;   // Nothing
+    public static final short NLMSG_ERROR                   = 2;   // Error
+    public static final short NLMSG_DONE                    = 3;   // End of a dump
+    public static final short NLMSG_OVERRUN                 = 4;   // Data lost
+    public static final short NLMSG_MAX_RESERVED            = 15;  // Max reserved value
+
+    public static final short RTM_NEWLINK                   = 16;
+    public static final short RTM_DELLINK                   = 17;
+    public static final short RTM_GETLINK                   = 18;
+    public static final short RTM_SETLINK                   = 19;
+    public static final short RTM_NEWADDR                   = 20;
+    public static final short RTM_DELADDR                   = 21;
+    public static final short RTM_GETADDR                   = 22;
+    public static final short RTM_NEWROUTE                  = 24;
+    public static final short RTM_DELROUTE                  = 25;
+    public static final short RTM_GETROUTE                  = 26;
+    public static final short RTM_NEWNEIGH                  = 28;
+    public static final short RTM_DELNEIGH                  = 29;
+    public static final short RTM_GETNEIGH                  = 30;
+    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
+    // low byte operation. See the macro NFNL_SUBSYS_ID and NFNL_MSG_TYPE in
+    // include/uapi/linux/netfilter/nfnetlink.h
+    public static final short NFNL_SUBSYS_CTNETLINK         = 1;
+
+    public static final short IPCTNL_MSG_CT_NEW             = 0;
+    public static final short IPCTNL_MSG_CT_GET             = 1;
+    public static final short IPCTNL_MSG_CT_DELETE          = 2;
+    public static final short IPCTNL_MSG_CT_GET_CTRZERO     = 3;
+    public static final short IPCTNL_MSG_CT_GET_STATS_CPU   = 4;
+    public static final short IPCTNL_MSG_CT_GET_STATS       = 5;
+    public static final short IPCTNL_MSG_CT_GET_DYING       = 6;
+    public static final short IPCTNL_MSG_CT_GET_UNCONFIRMED = 7;
+
+    /* see include/uapi/linux/sock_diag.h */
+    public static final short SOCK_DIAG_BY_FAMILY = 20;
+    public static final short SOCK_DESTROY = 21;
+
+    // Netlink groups.
+    public static final int RTMGRP_LINK = 1;
+    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);
+
+    // Netlink family
+    public static final short RTNL_FAMILY_IP6MR = 129;
+
+    // Device flags.
+    public static final int IFF_UP       = 1 << 0;
+    public static final int IFF_LOWER_UP = 1 << 16;
+
+    // Known values for struct rtmsg rtm_protocol.
+    public static final short RTPROT_KERNEL     = 2;
+    public static final short RTPROT_RA         = 9;
+
+    // Known values for struct rtmsg rtm_scope.
+    public static final short RT_SCOPE_UNIVERSE = 0;
+
+    // Known values for struct rtmsg rtm_type.
+    public static final short RTN_UNICAST       = 1;
+
+    // Known values for struct rtmsg rtm_flags.
+    public static final int RTM_F_CLONED        = 0x200;
+
+    /**
+     * Convert a netlink message type to a string for control message.
+     */
+    @NonNull
+    private static String stringForCtlMsgType(short nlmType) {
+        switch (nlmType) {
+            case NLMSG_NOOP: return "NLMSG_NOOP";
+            case NLMSG_ERROR: return "NLMSG_ERROR";
+            case NLMSG_DONE: return "NLMSG_DONE";
+            case NLMSG_OVERRUN: return "NLMSG_OVERRUN";
+            default: return "unknown control message type: " + String.valueOf(nlmType);
+        }
+    }
+
+    /**
+     * Convert a netlink message type to a string for NETLINK_ROUTE.
+     */
+    @NonNull
+    private static String stringForRtMsgType(short nlmType) {
+        switch (nlmType) {
+            case RTM_NEWLINK: return "RTM_NEWLINK";
+            case RTM_DELLINK: return "RTM_DELLINK";
+            case RTM_GETLINK: return "RTM_GETLINK";
+            case RTM_SETLINK: return "RTM_SETLINK";
+            case RTM_NEWADDR: return "RTM_NEWADDR";
+            case RTM_DELADDR: return "RTM_DELADDR";
+            case RTM_GETADDR: return "RTM_GETADDR";
+            case RTM_NEWROUTE: return "RTM_NEWROUTE";
+            case RTM_DELROUTE: return "RTM_DELROUTE";
+            case RTM_GETROUTE: return "RTM_GETROUTE";
+            case RTM_NEWNEIGH: return "RTM_NEWNEIGH";
+            case RTM_DELNEIGH: return "RTM_DELNEIGH";
+            case RTM_GETNEIGH: return "RTM_GETNEIGH";
+            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);
+        }
+    }
+
+    /**
+     * Convert a netlink message type to a string for NETLINK_INET_DIAG.
+     */
+    @NonNull
+    private static String stringForInetDiagMsgType(short nlmType) {
+        switch (nlmType) {
+            case SOCK_DIAG_BY_FAMILY: return "SOCK_DIAG_BY_FAMILY";
+            default: return "unknown SOCK_DIAG type: " + String.valueOf(nlmType);
+        }
+    }
+
+    /**
+     * Convert a netlink message type to a string for NETLINK_NETFILTER.
+     */
+    @NonNull
+    private static String stringForNfMsgType(short nlmType) {
+        final byte subsysId = (byte) (nlmType >> 8);
+        final byte msgType = (byte) nlmType;
+        switch (subsysId) {
+            case NFNL_SUBSYS_CTNETLINK:
+                switch (msgType) {
+                    case IPCTNL_MSG_CT_NEW: return "IPCTNL_MSG_CT_NEW";
+                    case IPCTNL_MSG_CT_GET: return "IPCTNL_MSG_CT_GET";
+                    case IPCTNL_MSG_CT_DELETE: return "IPCTNL_MSG_CT_DELETE";
+                    case IPCTNL_MSG_CT_GET_CTRZERO: return "IPCTNL_MSG_CT_GET_CTRZERO";
+                    case IPCTNL_MSG_CT_GET_STATS_CPU: return "IPCTNL_MSG_CT_GET_STATS_CPU";
+                    case IPCTNL_MSG_CT_GET_STATS: return "IPCTNL_MSG_CT_GET_STATS";
+                    case IPCTNL_MSG_CT_GET_DYING: return "IPCTNL_MSG_CT_GET_DYING";
+                    case IPCTNL_MSG_CT_GET_UNCONFIRMED: return "IPCTNL_MSG_CT_GET_UNCONFIRMED";
+                }
+                break;
+        }
+        return "unknown NETFILTER type: " + String.valueOf(nlmType);
+    }
+
+    /**
+     * Convert a netlink message type to a string by netlink family.
+     */
+    @NonNull
+    public static String stringForNlMsgType(short nlmType, int nlFamily) {
+        // Reserved control messages. The netlink family is ignored.
+        // See NLMSG_MIN_TYPE in include/uapi/linux/netlink.h.
+        if (nlmType <= NLMSG_MAX_RESERVED) return stringForCtlMsgType(nlmType);
+
+        // Netlink family messages. The netlink family is required. Note that the reason for using
+        // if-statement is that switch-case can't be used because the OsConstants.NETLINK_* are
+        // not constant.
+        if (nlFamily == OsConstants.NETLINK_ROUTE) return stringForRtMsgType(nlmType);
+        if (nlFamily == OsConstants.NETLINK_INET_DIAG) return stringForInetDiagMsgType(nlmType);
+        if (nlFamily == OsConstants.NETLINK_NETFILTER) return stringForNfMsgType(nlmType);
+
+        return "unknown type: " + String.valueOf(nlmType);
+    }
+
+    private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+            'A', 'B', 'C', 'D', 'E', 'F' };
+    /**
+     * Convert a byte array to a hexadecimal string.
+     */
+    public static String toHexString(byte[] array, int offset, int length) {
+        char[] buf = new char[length * 2];
+
+        int bufIndex = 0;
+        for (int i = offset; i < offset + length; i++) {
+            byte b = array[i];
+            buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
+            buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
+        }
+
+        return new String(buf);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkErrorMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkErrorMessage.java
new file mode 100644
index 0000000..4831432
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkErrorMessage.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+
+/**
+ * A NetlinkMessage subclass for netlink error messages.
+ *
+ * @hide
+ */
+public class NetlinkErrorMessage extends NetlinkMessage {
+
+    /**
+     * Parse a netlink error message from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the netlink error message.
+     * @return the parsed netlink error message, or {@code null} if the netlink error message
+     *         could not be parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static NetlinkErrorMessage parse(@NonNull StructNlMsgHdr header,
+            @NonNull ByteBuffer byteBuffer) {
+        final NetlinkErrorMessage errorMsg = new NetlinkErrorMessage(header);
+
+        errorMsg.mNlMsgErr = StructNlMsgErr.parse(byteBuffer);
+        if (errorMsg.mNlMsgErr == null) {
+            return null;
+        }
+
+        return errorMsg;
+    }
+
+    private StructNlMsgErr mNlMsgErr;
+
+    NetlinkErrorMessage(@NonNull StructNlMsgHdr header) {
+        super(header);
+        mNlMsgErr = null;
+    }
+
+    public StructNlMsgErr getNlMsgError() {
+        return mNlMsgErr;
+    }
+
+    @Override
+    public String toString() {
+        return "NetlinkErrorMessage{ "
+                + "nlmsghdr{" + (mHeader == null ? "" : mHeader.toString()) + "}, "
+                + "nlmsgerr{" + (mNlMsgErr == null ? "" : mNlMsgErr.toString()) + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
new file mode 100644
index 0000000..781a04e
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
+
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * NetlinkMessage base class for other, more specific netlink message types.
+ *
+ * Classes that extend NetlinkMessage should:
+ *     - implement a public static parse(StructNlMsgHdr, ByteBuffer) method
+ *     - returning either null (parse errors) or a new object of the subclass
+ *       type (cast-able to NetlinkMessage)
+ *
+ * NetlinkMessage.parse() should be updated to know which nlmsg_type values
+ * correspond with which message subclasses.
+ *
+ * @hide
+ */
+public class NetlinkMessage {
+    private static final String TAG = "NetlinkMessage";
+
+    /**
+     * Parsing netlink messages for reserved control message or specific netlink message. The
+     * netlink family is required for parsing specific netlink message. See man-pages/netlink.
+     */
+    @Nullable
+    public static NetlinkMessage parse(@NonNull ByteBuffer byteBuffer, int nlFamily) {
+        final int startPosition = (byteBuffer != null) ? byteBuffer.position() : -1;
+        final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(byteBuffer);
+        if (nlmsghdr == null) {
+            return null;
+        }
+
+        final int messageLength = NetlinkConstants.alignedLengthOf(nlmsghdr.nlmsg_len);
+        final int payloadLength = messageLength - StructNlMsgHdr.STRUCT_SIZE;
+        if (payloadLength < 0 || payloadLength > byteBuffer.remaining()) {
+            // Malformed message or runt buffer.  Pretend the buffer was consumed.
+            byteBuffer.position(byteBuffer.limit());
+            return null;
+        }
+
+        // Reserved control messages. The netlink family is ignored.
+        // See NLMSG_MIN_TYPE in include/uapi/linux/netlink.h.
+        if (nlmsghdr.nlmsg_type <= NetlinkConstants.NLMSG_MAX_RESERVED) {
+            return parseCtlMessage(nlmsghdr, byteBuffer, payloadLength);
+        }
+
+        // Netlink family messages. The netlink family is required. Note that the reason for using
+        // if-statement is that switch-case can't be used because the OsConstants.NETLINK_* are
+        // not constant.
+        final NetlinkMessage parsed;
+        if (nlFamily == OsConstants.NETLINK_ROUTE) {
+            parsed = parseRtMessage(nlmsghdr, byteBuffer);
+        } else if (nlFamily == OsConstants.NETLINK_INET_DIAG) {
+            parsed = parseInetDiagMessage(nlmsghdr, byteBuffer);
+        } else if (nlFamily == OsConstants.NETLINK_NETFILTER) {
+            parsed = parseNfMessage(nlmsghdr, byteBuffer);
+        } else if (nlFamily == NETLINK_XFRM) {
+            parsed = parseXfrmMessage(nlmsghdr, byteBuffer);
+        } else {
+            parsed = null;
+        }
+
+        // Advance to the end of the message, regardless of whether the parsing code consumed
+        // all of it or not.
+        byteBuffer.position(startPosition + messageLength);
+
+        return parsed;
+    }
+
+    @NonNull
+    protected final StructNlMsgHdr mHeader;
+
+    public NetlinkMessage(@NonNull StructNlMsgHdr nlmsghdr) {
+        mHeader = nlmsghdr;
+    }
+
+    @NonNull
+    public StructNlMsgHdr getHeader() {
+        return mHeader;
+    }
+
+    @Override
+    public String toString() {
+        // The netlink family is not provided to StructNlMsgHdr#toString because NetlinkMessage
+        // doesn't store the information. So the netlink message type can't be transformed into
+        // a string by StructNlMsgHdr#toString and just keep as an integer. The specific message
+        // which inherits NetlinkMessage could override NetlinkMessage#toString and provide the
+        // specific netlink family to StructNlMsgHdr#toString.
+        return "NetlinkMessage{" + mHeader.toString() + "}";
+    }
+
+    @NonNull
+    private static NetlinkMessage parseCtlMessage(@NonNull StructNlMsgHdr nlmsghdr,
+            @NonNull ByteBuffer byteBuffer, int payloadLength) {
+        switch (nlmsghdr.nlmsg_type) {
+            case NetlinkConstants.NLMSG_ERROR:
+                return (NetlinkMessage) NetlinkErrorMessage.parse(nlmsghdr, byteBuffer);
+            default: {
+                // Other netlink control messages. Just parse the header for now,
+                // pretending the whole message was consumed.
+                byteBuffer.position(byteBuffer.position() + payloadLength);
+                return new NetlinkMessage(nlmsghdr);
+            }
+        }
+    }
+
+    @Nullable
+    private static NetlinkMessage parseRtMessage(@NonNull StructNlMsgHdr nlmsghdr,
+            @NonNull ByteBuffer byteBuffer) {
+        switch (nlmsghdr.nlmsg_type) {
+            case NetlinkConstants.RTM_NEWLINK:
+            case NetlinkConstants.RTM_DELLINK:
+                return (NetlinkMessage) RtNetlinkLinkMessage.parse(nlmsghdr, byteBuffer);
+            case NetlinkConstants.RTM_NEWADDR:
+            case NetlinkConstants.RTM_DELADDR:
+                return (NetlinkMessage) RtNetlinkAddressMessage.parse(nlmsghdr, byteBuffer);
+            case NetlinkConstants.RTM_NEWROUTE:
+            case NetlinkConstants.RTM_DELROUTE:
+            case NetlinkConstants.RTM_GETROUTE:
+                return (NetlinkMessage) RtNetlinkRouteMessage.parse(nlmsghdr, byteBuffer);
+            case NetlinkConstants.RTM_NEWNEIGH:
+            case NetlinkConstants.RTM_DELNEIGH:
+            case NetlinkConstants.RTM_GETNEIGH:
+                return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
+            case NetlinkConstants.RTM_NEWNDUSEROPT:
+                return (NetlinkMessage) NduseroptMessage.parse(nlmsghdr, byteBuffer);
+            default: return null;
+        }
+    }
+
+    @Nullable
+    private static NetlinkMessage parseInetDiagMessage(@NonNull StructNlMsgHdr nlmsghdr,
+            @NonNull ByteBuffer byteBuffer) {
+        switch (nlmsghdr.nlmsg_type) {
+            case NetlinkConstants.SOCK_DIAG_BY_FAMILY:
+                return (NetlinkMessage) InetDiagMessage.parse(nlmsghdr, byteBuffer);
+            default: return null;
+        }
+    }
+
+    @Nullable
+    private static NetlinkMessage parseNfMessage(@NonNull StructNlMsgHdr nlmsghdr,
+            @NonNull ByteBuffer byteBuffer) {
+        switch (nlmsghdr.nlmsg_type) {
+            case NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8
+                    | NetlinkConstants.IPCTNL_MSG_CT_NEW:
+            case NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8
+                    | NetlinkConstants.IPCTNL_MSG_CT_DELETE:
+                return (NetlinkMessage) ConntrackMessage.parse(nlmsghdr, byteBuffer);
+            default: return null;
+        }
+    }
+
+    @Nullable
+    private static NetlinkMessage parseXfrmMessage(
+            @NonNull final StructNlMsgHdr nlmsghdr, @NonNull final ByteBuffer byteBuffer) {
+        return (NetlinkMessage) XfrmNetlinkMessage.parseXfrmInternal(nlmsghdr, byteBuffer);
+    }
+
+    /** A convenient method to create a ByteBuffer for encoding a new message */
+    protected static ByteBuffer newNlMsgByteBuffer(int payloadLen) {
+        final int length = StructNlMsgHdr.STRUCT_SIZE + payloadLen;
+        final ByteBuffer byteBuffer = ByteBuffer.allocate(length);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        return byteBuffer;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
new file mode 100644
index 0000000..541a375
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -0,0 +1,472 @@
+/*
+ * 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.net.module.util.netlink;
+
+import static android.net.util.SocketUtils.makeNetlinkSocketAddress;
+import static android.system.OsConstants.AF_NETLINK;
+import static android.system.OsConstants.EIO;
+import static android.system.OsConstants.EPROTO;
+import static android.system.OsConstants.ETIMEDOUT;
+import static android.system.OsConstants.NETLINK_INET_DIAG;
+import static android.system.OsConstants.NETLINK_ROUTE;
+import static android.system.OsConstants.SOCK_CLOEXEC;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
+import static android.system.OsConstants.SO_RCVTIMEO;
+import static android.system.OsConstants.SO_SNDTIMEO;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import android.net.util.SocketUtils;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructTimeval;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Utilities for netlink related class that may not be able to fit into a specific class.
+ * @hide
+ */
+public class NetlinkUtils {
+    private static final String TAG = "NetlinkUtils";
+    /** Corresponds to enum from bionic/libc/include/netinet/tcp.h. */
+    private static final int TCP_ESTABLISHED = 1;
+    private static final int TCP_SYN_SENT = 2;
+    private static final int TCP_SYN_RECV = 3;
+
+    public static final int TCP_ALIVE_STATE_FILTER =
+            (1 << TCP_ESTABLISHED) | (1 << TCP_SYN_SENT) | (1 << TCP_SYN_RECV);
+
+    public static final int UNKNOWN_MARK = 0xffffffff;
+    public static final int NULL_MASK = 0;
+
+    // Initial mark value corresponds to the initValue in system/netd/include/Fwmark.h.
+    public static final int INIT_MARK_VALUE = 0;
+    // Corresponds to enum definition in bionic/libc/kernel/uapi/linux/inet_diag.h
+    public static final int INET_DIAG_INFO = 2;
+    public static final int INET_DIAG_MARK = 15;
+
+    public static final long IO_TIMEOUT_MS = 300L;
+
+    public static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
+    public static final int SOCKET_RECV_BUFSIZE = 64 * 1024;
+    public static final int SOCKET_DUMP_RECV_BUFSIZE = 128 * 1024;
+
+    /**
+     * Return whether the input ByteBuffer contains enough remaining bytes for
+     * {@code StructNlMsgHdr}.
+     */
+    public static boolean enoughBytesRemainForValidNlMsg(@NonNull final ByteBuffer bytes) {
+        return bytes.remaining() >= StructNlMsgHdr.STRUCT_SIZE;
+    }
+
+    /**
+     * Parse netlink error message
+     *
+     * @param bytes byteBuffer to parse netlink error message
+     * @return NetlinkErrorMessage if bytes contains valid NetlinkErrorMessage, else {@code null}
+     */
+    @Nullable
+    private static NetlinkErrorMessage parseNetlinkErrorMessage(ByteBuffer bytes) {
+        final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(bytes);
+        if (nlmsghdr == null || nlmsghdr.nlmsg_type != NetlinkConstants.NLMSG_ERROR) {
+            return null;
+        }
+
+        final int messageLength = NetlinkConstants.alignedLengthOf(nlmsghdr.nlmsg_len);
+        final int payloadLength = messageLength - StructNlMsgHdr.STRUCT_SIZE;
+        if (payloadLength < 0 || payloadLength > bytes.remaining()) {
+            // Malformed message or runt buffer.  Pretend the buffer was consumed.
+            bytes.position(bytes.limit());
+            return null;
+        }
+
+        return NetlinkErrorMessage.parse(nlmsghdr, bytes);
+    }
+
+    /**
+     * Receive netlink ack message and check error
+     *
+     * @param fd fd to read netlink message
+     */
+    public static void receiveNetlinkAck(final FileDescriptor fd)
+            throws InterruptedIOException, ErrnoException {
+        final ByteBuffer bytes = recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
+        // recvMessage() guaranteed to not return null if it did not throw.
+        final NetlinkErrorMessage response = parseNetlinkErrorMessage(bytes);
+        if (response != null && response.getNlMsgError() != null) {
+            final int errno = response.getNlMsgError().error;
+            if (errno != 0) {
+                // TODO: consider ignoring EINVAL (-22), which appears to be
+                // normal when probing a neighbor for which the kernel does
+                // not already have / no longer has a link layer address.
+                Log.e(TAG, "receiveNetlinkAck, errmsg=" + response.toString());
+                // Note: convert kernel errnos (negative) into userspace errnos (positive).
+                throw new ErrnoException(response.toString(), Math.abs(errno));
+            }
+        } else {
+            final String errmsg;
+            if (response == null) {
+                bytes.position(0);
+                errmsg = "raw bytes: " + NetlinkConstants.hexify(bytes);
+            } else {
+                errmsg = response.toString();
+            }
+            Log.e(TAG, "receiveNetlinkAck, errmsg=" + errmsg);
+            throw new ErrnoException(errmsg, EPROTO);
+        }
+    }
+
+    /**
+     * Send one netlink message to kernel via netlink socket.
+     *
+     * @param nlProto netlink protocol type.
+     * @param msg the raw bytes of netlink message to be sent.
+     */
+    public static void sendOneShotKernelMessage(int nlProto, byte[] msg) throws ErrnoException {
+        final String errPrefix = "Error in NetlinkSocket.sendOneShotKernelMessage";
+        final FileDescriptor fd = netlinkSocketForProto(nlProto, SOCKET_RECV_BUFSIZE);
+
+        try {
+            connectToKernel(fd);
+            sendMessage(fd, msg, 0, msg.length, IO_TIMEOUT_MS);
+            receiveNetlinkAck(fd);
+        } catch (InterruptedIOException e) {
+            Log.e(TAG, errPrefix, e);
+            throw new ErrnoException(errPrefix, ETIMEDOUT, e);
+        } catch (SocketException e) {
+            Log.e(TAG, errPrefix, e);
+            throw new ErrnoException(errPrefix, EIO, e);
+        } finally {
+            closeSocketQuietly(fd);
+        }
+    }
+
+    /**
+     * Send an RTM_NEWADDR message to kernel to add or update an IP address.
+     *
+     * @param ifIndex interface index.
+     * @param ip IP address to be added.
+     * @param prefixlen IP address prefix length.
+     * @param flags IP address flags.
+     * @param scope IP address scope.
+     * @param preferred The preferred lifetime of IP address.
+     * @param valid The valid lifetime of IP address.
+     */
+    public static boolean sendRtmNewAddressRequest(int ifIndex, @NonNull final InetAddress ip,
+            short prefixlen, int flags, byte scope, long preferred, long valid) {
+        Objects.requireNonNull(ip, "IP address to be added should not be null.");
+        final byte[] msg = RtNetlinkAddressMessage.newRtmNewAddressMessage(1 /* seqNo*/, ip,
+                prefixlen, flags, scope, ifIndex, preferred, valid);
+        try {
+            NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+            return true;
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Fail to send RTM_NEWADDR to add " + ip.getHostAddress(), e);
+            return false;
+        }
+    }
+
+    /**
+     * Send an RTM_DELADDR message to kernel to delete an IPv6 address.
+     *
+     * @param ifIndex interface index.
+     * @param ip IPv6 address to be deleted.
+     * @param prefixlen IPv6 address prefix length.
+     */
+    public static boolean sendRtmDelAddressRequest(int ifIndex, final Inet6Address ip,
+            short prefixlen) {
+        Objects.requireNonNull(ip, "IPv6 address to be deleted should not be null.");
+        final byte[] msg = RtNetlinkAddressMessage.newRtmDelAddressMessage(1 /* seqNo*/, ip,
+                prefixlen, ifIndex);
+        try {
+            NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+            return true;
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Fail to send RTM_DELADDR to delete " + ip.getHostAddress(), e);
+            return false;
+        }
+    }
+
+    /**
+     * Create netlink socket with the given netlink protocol type and buffersize.
+     *
+     * @param nlProto the netlink protocol
+     * @param bufferSize the receive buffer size to set when the value is not 0
+     *
+     * @return fd the fileDescriptor of the socket.
+     * @throws ErrnoException if the FileDescriptor not connect to be created successfully
+     */
+    public static FileDescriptor netlinkSocketForProto(int nlProto, int bufferSize)
+            throws ErrnoException {
+        final FileDescriptor fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, nlProto);
+        if (bufferSize > 0) {
+            Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, bufferSize);
+        }
+        return fd;
+    }
+
+    /**
+     * Create netlink socket with the given netlink protocol type. Receive buffer size is not set.
+     *
+     * @param nlProto the netlink protocol
+     *
+     * @return fd the fileDescriptor of the socket.
+     * @throws ErrnoException if the FileDescriptor not connect to be created successfully
+     */
+    public static FileDescriptor netlinkSocketForProto(int nlProto)
+            throws ErrnoException {
+        return netlinkSocketForProto(nlProto, 0);
+    }
+
+    /**
+     * Construct a netlink inet_diag socket.
+     */
+    public static FileDescriptor createNetLinkInetDiagSocket() throws ErrnoException {
+        return netlinkSocketForProto(NETLINK_INET_DIAG);
+    }
+
+    /**
+     * Connect the given file descriptor to the Netlink interface to the kernel.
+     *
+     * The fd must be a SOCK_DGRAM socket : create it with {@link #netlinkSocketForProto}
+     *
+     * @throws ErrnoException if the {@code fd} could not connect to kernel successfully
+     * @throws SocketException if there is an error accessing a socket.
+     */
+    public static void connectToKernel(@NonNull FileDescriptor fd)
+            throws ErrnoException, SocketException {
+        Os.connect(fd, makeNetlinkSocketAddress(0, 0));
+    }
+
+    private static void checkTimeout(long timeoutMs) {
+        if (timeoutMs < 0) {
+            throw new IllegalArgumentException("Negative timeouts not permitted");
+        }
+    }
+
+    /**
+     * Wait up to |timeoutMs| (or until underlying socket error) for a
+     * netlink message of at most |bufsize| size.
+     *
+     * Multi-threaded calls with different timeouts will cause unexpected results.
+     */
+    public static ByteBuffer recvMessage(FileDescriptor fd, int bufsize, long timeoutMs)
+            throws ErrnoException, IllegalArgumentException, InterruptedIOException {
+        checkTimeout(timeoutMs);
+
+        Os.setsockoptTimeval(fd, SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(timeoutMs));
+
+        final ByteBuffer byteBuffer = ByteBuffer.allocate(bufsize);
+        final int length = Os.read(fd, byteBuffer);
+        if (length == bufsize) {
+            Log.w(TAG, "maximum read");
+        }
+        byteBuffer.position(0);
+        byteBuffer.limit(length);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        return byteBuffer;
+    }
+
+    /**
+     * Send a message to a peer to which this socket has previously connected.
+     *
+     * This waits at most |timeoutMs| milliseconds for the send to complete, will get the exception
+     * if it times out.
+     */
+    public static int sendMessage(
+            FileDescriptor fd, byte[] bytes, int offset, int count, long timeoutMs)
+            throws ErrnoException, IllegalArgumentException, InterruptedIOException {
+        checkTimeout(timeoutMs);
+        Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(timeoutMs));
+        return Os.write(fd, bytes, offset, count);
+    }
+
+    private static final long CLOCK_TICKS_PER_SECOND = Os.sysconf(OsConstants._SC_CLK_TCK);
+
+    /**
+     * Convert the system time in clock ticks(clock_t type in times(), not in clock()) to
+     * milliseconds. times() clock_t ticks at the kernel's USER_HZ (100) while clock() clock_t
+     * ticks at CLOCKS_PER_SEC (1000000).
+     *
+     * See the NOTES on https://man7.org/linux/man-pages/man2/times.2.html for the difference
+     * of clock_t used in clock() and times().
+     */
+    public static long ticksToMilliSeconds(int intClockTicks) {
+        final long longClockTicks = intClockTicks & 0xffffffffL;
+        return (longClockTicks * 1000) / CLOCK_TICKS_PER_SECOND;
+    }
+
+    private NetlinkUtils() {}
+
+    private static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessagesWithFd(
+            FileDescriptor fd, byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+            Consumer<T> func)
+            throws SocketException, InterruptedIOException, ErrnoException {
+        // connecToKernel throws ErrnoException and SocketException, should be handled by caller
+        connectToKernel(fd);
+
+        // sendMessage throws InterruptedIOException and ErrnoException,
+        // should be handled by caller
+        sendMessage(fd, dumpRequestMessage, 0, dumpRequestMessage.length, IO_TIMEOUT_MS);
+
+        while (true) {
+            // recvMessage throws ErrnoException, InterruptedIOException
+            // should be handled by caller
+            final ByteBuffer buf = recvMessage(
+                    fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
+
+            while (buf.remaining() > 0) {
+                final int position = buf.position();
+                final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, nlFamily);
+                if (nlMsg == null) {
+                    // Move to the position where parse started for error log.
+                    buf.position(position);
+                    Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
+                    break;
+                }
+
+                if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
+                    return;
+                }
+
+                if (!msgClass.isInstance(nlMsg)) {
+                    Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
+                    continue;
+                }
+
+                final T msg = (T) nlMsg;
+                func.accept(msg);
+            }
+        }
+    }
+    /**
+     * Sends a netlink dump request and processes the returned dump messages
+     *
+     * @param <T> extends NetlinkMessage
+     * @param dumpRequestMessage netlink dump request message to be sent
+     * @param nlFamily netlink family
+     * @param msgClass expected class of the netlink message
+     * @param func function defined by caller to handle the dump messages
+     * @throws SocketException when fails to connect socket to kernel
+     * @throws InterruptedIOException when fails to read the dumpFd
+     * @throws ErrnoException when fails to create dump fd, send dump request
+     *                        or receive messages
+     */
+    public static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessages(
+            byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+            Consumer<T> func)
+            throws SocketException, InterruptedIOException, ErrnoException {
+        // Create socket
+        final FileDescriptor fd = netlinkSocketForProto(nlFamily, SOCKET_DUMP_RECV_BUFSIZE);
+        try {
+            getAndProcessNetlinkDumpMessagesWithFd(fd, dumpRequestMessage, nlFamily,
+                    msgClass, func);
+        } finally {
+            closeSocketQuietly(fd);
+        }
+    }
+
+    /**
+     * Construct a RTM_GETROUTE message for dumping multicast IPv6 routes from kernel.
+     */
+    private static byte[] newIpv6MulticastRouteDumpRequest() {
+        final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+        nlmsghdr.nlmsg_type = NetlinkConstants.RTM_GETROUTE;
+        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+        final short shortZero = 0;
+
+        // family must be RTNL_FAMILY_IP6MR to dump IPv6 multicast routes.
+        // dstLen, srcLen, tos and scope must be zero in FIB dump request.
+        // protocol, flags must be 0, and type must be RTN_MULTICAST (if not 0) for multicast
+        // dump request.
+        // table or RTA_TABLE attributes can be used to dump a specific routing table.
+        // RTA_OIF attribute can be used to dump only routes containing given oif.
+        // Here no attributes are set so the kernel can return all multicast routes.
+        final StructRtMsg rtMsg =
+                new StructRtMsg(RTNL_FAMILY_IP6MR /* family */, shortZero /* dstLen */,
+                        shortZero /* srcLen */, shortZero /* tos */, shortZero /* table */,
+                        shortZero /* protocol */, shortZero /* scope */, shortZero /* type */,
+                        0L /* flags */);
+        final RtNetlinkRouteMessage msg =
+            new RtNetlinkRouteMessage(nlmsghdr, rtMsg);
+
+        final int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructRtMsg.STRUCT_SIZE;
+        nlmsghdr.nlmsg_len = spaceRequired;
+        final byte[] bytes = new byte[NetlinkConstants.alignedLengthOf(spaceRequired)];
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        msg.pack(byteBuffer);
+        return bytes;
+     }
+
+    /**
+     * Get the list of IPv6 multicast route messages from kernel.
+     */
+    public static List<RtNetlinkRouteMessage> getIpv6MulticastRoutes() {
+        final byte[] dumpMsg = newIpv6MulticastRouteDumpRequest();
+        List<RtNetlinkRouteMessage> routes = new ArrayList<>();
+        Consumer<RtNetlinkRouteMessage> handleNlDumpMsg = (msg) -> {
+            if (msg.getRtmFamily() == RTNL_FAMILY_IP6MR) {
+                // Sent rtmFamily RTNL_FAMILY_IP6MR in dump request to make sure ipv6
+                // multicast routes are included in netlink reply messages, the kernel
+                // may also reply with other kind of routes, so we filter them out here.
+                routes.add(msg);
+            }
+        };
+        try {
+            NetlinkUtils.<RtNetlinkRouteMessage>getAndProcessNetlinkDumpMessages(
+                    dumpMsg, NETLINK_ROUTE, RtNetlinkRouteMessage.class,
+                    handleNlDumpMsg);
+        } catch (SocketException | InterruptedIOException | ErrnoException e) {
+            Log.e(TAG, "Failed to dump multicast routes");
+            return routes;
+        }
+
+        return routes;
+    }
+
+    private static void closeSocketQuietly(final FileDescriptor fd) {
+        try {
+            SocketUtils.closeSocket(fd);
+        } catch (IOException e) {
+            // Nothing we can do here
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
new file mode 100644
index 0000000..4846df7
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
@@ -0,0 +1,294 @@
+/*
+ * 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.net.module.util.netlink;
+
+import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REPLACE;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.net.module.util.HexDump;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Objects;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink address messages.
+ *
+ * RtNetlinkAddressMessage.parse() must be called with a ByteBuffer that contains exactly one
+ * netlink message.
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class RtNetlinkAddressMessage extends NetlinkMessage {
+    public static final short IFA_ADDRESS        = 1;
+    public static final short IFA_LOCAL          = 2;
+    public static final short IFA_BROADCAST      = 4;
+    public static final short IFA_CACHEINFO      = 6;
+    public static final short IFA_FLAGS          = 8;
+
+    private int mFlags;
+    @NonNull
+    private StructIfaddrMsg mIfaddrmsg;
+    @NonNull
+    private InetAddress mIpAddress;
+    @Nullable
+    private StructIfacacheInfo mIfacacheInfo;
+
+    @VisibleForTesting
+    public RtNetlinkAddressMessage(@NonNull final StructNlMsgHdr header,
+            @NonNull final StructIfaddrMsg ifaddrMsg,
+            @NonNull final InetAddress ipAddress,
+            @Nullable final StructIfacacheInfo structIfacacheInfo,
+            int flags) {
+        super(header);
+        mIfaddrmsg = ifaddrMsg;
+        mIpAddress = ipAddress;
+        mIfacacheInfo = structIfacacheInfo;
+        mFlags = flags;
+    }
+
+    private RtNetlinkAddressMessage(@NonNull StructNlMsgHdr header) {
+        this(header, null, null, null, 0);
+    }
+
+    public int getFlags() {
+        return mFlags;
+    }
+
+    @NonNull
+    public StructIfaddrMsg getIfaddrHeader() {
+        return mIfaddrmsg;
+    }
+
+    @NonNull
+    public InetAddress getIpAddress() {
+        return mIpAddress;
+    }
+
+    @Nullable
+    public StructIfacacheInfo getIfacacheInfo() {
+        return mIfacacheInfo;
+    }
+
+    /**
+     * Parse rtnetlink address message from {@link ByteBuffer}. This method must be called with a
+     * ByteBuffer that contains exactly one netlink message.
+     *
+     * @param header netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+     */
+    @Nullable
+    public static RtNetlinkAddressMessage parse(@NonNull final StructNlMsgHdr header,
+            @NonNull final ByteBuffer byteBuffer) {
+        final RtNetlinkAddressMessage addrMsg = new RtNetlinkAddressMessage(header);
+
+        addrMsg.mIfaddrmsg = StructIfaddrMsg.parse(byteBuffer);
+        if (addrMsg.mIfaddrmsg == null) return null;
+
+        // IFA_ADDRESS
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(IFA_ADDRESS, byteBuffer);
+        if (nlAttr == null) return null;
+        addrMsg.mIpAddress = nlAttr.getValueAsInetAddress();
+        if (addrMsg.mIpAddress == null) return null;
+
+        // IFA_CACHEINFO
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(IFA_CACHEINFO, byteBuffer);
+        if (nlAttr != null) {
+            addrMsg.mIfacacheInfo = StructIfacacheInfo.parse(nlAttr.getValueAsByteBuffer());
+        }
+
+        // The first 8 bits of flags are in the ifaddrmsg.
+        addrMsg.mFlags = addrMsg.mIfaddrmsg.flags;
+        // IFA_FLAGS. All the flags are in the IF_FLAGS attribute. This should always be present,
+        // and will overwrite the flags set above.
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(IFA_FLAGS, byteBuffer);
+        if (nlAttr == null) return null;
+        final Integer value = nlAttr.getValueAsInteger();
+        if (value == null) return null;
+        addrMsg.mFlags = value;
+
+        return addrMsg;
+    }
+
+    /**
+     * Write a rtnetlink address message to {@link ByteBuffer}.
+     */
+    @VisibleForTesting
+    protected void pack(ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer);
+        mIfaddrmsg.pack(byteBuffer);
+
+        final StructNlAttr address = new StructNlAttr(IFA_ADDRESS, mIpAddress);
+        address.pack(byteBuffer);
+
+        if (mIfacacheInfo != null) {
+            final StructNlAttr cacheInfo = new StructNlAttr(IFA_CACHEINFO,
+                    mIfacacheInfo.writeToBytes());
+            cacheInfo.pack(byteBuffer);
+        }
+
+        // If IFA_FLAGS attribute isn't present on the wire at parsing netlink message, it will
+        // still be packed to ByteBuffer even if the flag is 0.
+        final StructNlAttr flags = new StructNlAttr(IFA_FLAGS, mFlags);
+        flags.pack(byteBuffer);
+
+        // Add the required IFA_LOCAL and IFA_BROADCAST attributes for IPv4 addresses. The IFA_LOCAL
+        // attribute represents the local address, which is equivalent to IFA_ADDRESS on a normally
+        // configured broadcast interface, however, for PPP interfaces, IFA_ADDRESS indicates the
+        // destination address and the local address is provided in the IFA_LOCAL attribute. If the
+        // IFA_LOCAL attribute is not present in the RTM_NEWADDR message, the kernel replies with an
+        // error netlink message with invalid parameters. IFA_BROADCAST is also required, otherwise
+        // the broadcast on the interface is 0.0.0.0. See include/uapi/linux/if_addr.h for details.
+        // For IPv6 addresses, the IFA_ADDRESS attribute applies and introduces no ambiguity.
+        if (mIpAddress instanceof Inet4Address) {
+            final StructNlAttr localAddress = new StructNlAttr(IFA_LOCAL, mIpAddress);
+            localAddress.pack(byteBuffer);
+
+            final Inet4Address broadcast =
+                    getBroadcastAddress((Inet4Address) mIpAddress, mIfaddrmsg.prefixLen);
+            final StructNlAttr broadcastAddress = new StructNlAttr(IFA_BROADCAST, broadcast);
+            broadcastAddress.pack(byteBuffer);
+        }
+    }
+
+    /**
+     * A convenience method to create a RTM_NEWADDR message.
+     */
+    public static byte[] newRtmNewAddressMessage(int seqNo, @NonNull final InetAddress ip,
+            short prefixlen, int flags, byte scope, int ifIndex, long preferred, long valid) {
+        Objects.requireNonNull(ip, "IP address to be set via netlink message cannot be null");
+
+        final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+        nlmsghdr.nlmsg_type = NetlinkConstants.RTM_NEWADDR;
+        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_REPLACE | NLM_F_ACK;
+        nlmsghdr.nlmsg_seq = seqNo;
+
+        final RtNetlinkAddressMessage msg = new RtNetlinkAddressMessage(nlmsghdr);
+        final byte family =
+                (byte) ((ip instanceof Inet6Address) ? OsConstants.AF_INET6 : OsConstants.AF_INET);
+        // IFA_FLAGS attribute is always present within this method, just set flags from
+        // ifaddrmsg to 0. kernel will prefer the flags from IFA_FLAGS attribute.
+        msg.mIfaddrmsg =
+                new StructIfaddrMsg(family, prefixlen, (short) 0 /* flags */, scope, ifIndex);
+        msg.mIpAddress = ip;
+        msg.mIfacacheInfo = new StructIfacacheInfo(preferred, valid, 0 /* cstamp */,
+                0 /* tstamp */);
+        msg.mFlags = flags;
+
+        final byte[] bytes = new byte[msg.getRequiredSpace(family)];
+        nlmsghdr.nlmsg_len = bytes.length;
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        msg.pack(byteBuffer);
+        return bytes;
+    }
+
+    /**
+     * A convenience method to create a RTM_DELADDR message.
+     */
+    public static byte[] newRtmDelAddressMessage(int seqNo, @NonNull final InetAddress ip,
+            short prefixlen, int ifIndex) {
+        Objects.requireNonNull(ip, "IP address to be deleted via netlink message cannot be null");
+
+        final int ifaAddrAttrLength = NetlinkConstants.alignedLengthOf(
+                StructNlAttr.NLA_HEADERLEN + ip.getAddress().length);
+        final int length = StructNlMsgHdr.STRUCT_SIZE + StructIfaddrMsg.STRUCT_SIZE
+                + ifaAddrAttrLength;
+        final byte[] bytes = new byte[length];
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+        nlmsghdr.nlmsg_len = length;
+        nlmsghdr.nlmsg_type = NetlinkConstants.RTM_DELADDR;
+        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+        nlmsghdr.nlmsg_seq = seqNo;
+        nlmsghdr.pack(byteBuffer);
+
+        final byte family =
+                (byte) ((ip instanceof Inet6Address) ? OsConstants.AF_INET6 : OsConstants.AF_INET);
+        // Actually kernel ignores scope and flags(only deal with IFA_F_MANAGETEMPADDR, it
+        // indicates that all relevant IPv6 temporary addresses should be deleted as well when
+        // user space intends to delete a global IPv6 address with IFA_F_MANAGETEMPADDR), so
+        // far IFA_F_MANAGETEMPADDR flag isn't used in user space, it's fine to ignore it.
+        // However, we need to add IFA_FLAGS attribute in RTM_DELADDR if flags parsing should
+        // be supported in the future.
+        final StructIfaddrMsg ifaddrmsg = new StructIfaddrMsg(family, prefixlen,
+                (short) 0 /* flags */, (short) 0 /* scope */, ifIndex);
+        ifaddrmsg.pack(byteBuffer);
+
+        final StructNlAttr address = new StructNlAttr(IFA_ADDRESS, ip);
+        address.pack(byteBuffer);
+
+        return bytes;
+    }
+
+    // This function helper gives the required buffer size for IFA_ADDRESS, IFA_CACHEINFO and
+    // IFA_FLAGS attributes encapsulation. However, that's not a mandatory requirement for all
+    // RtNetlinkAddressMessage, e.g. RTM_DELADDR sent from user space to kernel to delete an
+    // IP address only requires IFA_ADDRESS attribute. The caller should check if these attributes
+    // are necessary to carry when constructing a RtNetlinkAddressMessage.
+    private int getRequiredSpace(int family) {
+        int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructIfaddrMsg.STRUCT_SIZE;
+        // IFA_ADDRESS attr
+        spaceRequired += NetlinkConstants.alignedLengthOf(
+                StructNlAttr.NLA_HEADERLEN + mIpAddress.getAddress().length);
+        // IFA_CACHEINFO attr
+        spaceRequired += NetlinkConstants.alignedLengthOf(
+                StructNlAttr.NLA_HEADERLEN + StructIfacacheInfo.STRUCT_SIZE);
+        // IFA_FLAGS "u32" attr
+        spaceRequired += StructNlAttr.NLA_HEADERLEN + 4;
+        if (family == OsConstants.AF_INET) {
+            // IFA_LOCAL attr
+            spaceRequired += NetlinkConstants.alignedLengthOf(
+                    StructNlAttr.NLA_HEADERLEN + mIpAddress.getAddress().length);
+            // IFA_BROADCAST attr
+            spaceRequired += NetlinkConstants.alignedLengthOf(
+                    StructNlAttr.NLA_HEADERLEN + mIpAddress.getAddress().length);
+        }
+        return spaceRequired;
+    }
+
+    @Override
+    public String toString() {
+        return "RtNetlinkAddressMessage{ "
+                + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+                + "Ifaddrmsg{" + mIfaddrmsg.toString() + "}, "
+                + "IP Address{" + mIpAddress.getHostAddress() + "}, "
+                + "IfacacheInfo{" + (mIfacacheInfo == null ? "" : mIfacacheInfo.toString()) + "}, "
+                + "Address Flags{" + HexDump.toHexString(mFlags) + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
new file mode 100644
index 0000000..0c49edc
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -0,0 +1,166 @@
+/*
+ * 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.net.module.util.netlink;
+
+import android.net.MacAddress;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.nio.ByteBuffer;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink link messages.
+ *
+ * RtNetlinkLinkMessage.parse() must be called with a ByteBuffer that contains exactly one netlink
+ * message.
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class RtNetlinkLinkMessage extends NetlinkMessage {
+    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
+    private StructIfinfoMsg mIfinfomsg;
+    @Nullable
+    private MacAddress mHardwareAddress;
+    @Nullable
+    private String mInterfaceName;
+
+    private RtNetlinkLinkMessage(@NonNull StructNlMsgHdr header) {
+        super(header);
+        mIfinfomsg = null;
+        mMtu = 0;
+        mHardwareAddress = null;
+        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;
+    }
+
+    @NonNull
+    public StructIfinfoMsg getIfinfoHeader() {
+        return mIfinfomsg;
+    }
+
+    @Nullable
+    public MacAddress getHardwareAddress() {
+        return mHardwareAddress;
+    }
+
+    @Nullable
+    public String getInterfaceName() {
+        return mInterfaceName;
+    }
+
+    /**
+     * Parse rtnetlink link message from {@link ByteBuffer}. This method must be called with a
+     * ByteBuffer that contains exactly one netlink message.
+     *
+     * @param header netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+     */
+    @Nullable
+    public static RtNetlinkLinkMessage parse(@NonNull final StructNlMsgHdr header,
+            @NonNull final ByteBuffer byteBuffer) {
+        final RtNetlinkLinkMessage linkMsg = new RtNetlinkLinkMessage(header);
+
+        linkMsg.mIfinfomsg = StructIfinfoMsg.parse(byteBuffer);
+        if (linkMsg.mIfinfomsg == null) return null;
+
+        // IFLA_MTU
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(IFLA_MTU, byteBuffer);
+        if (nlAttr != null) {
+            linkMsg.mMtu = nlAttr.getValueAsInt(0 /* default value */);
+        }
+
+        // IFLA_ADDRESS
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(IFLA_ADDRESS, byteBuffer);
+        if (nlAttr != null) {
+            linkMsg.mHardwareAddress = nlAttr.getValueAsMacAddress();
+        }
+
+        // IFLA_IFNAME
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(IFLA_IFNAME, byteBuffer);
+        if (nlAttr != null) {
+            linkMsg.mInterfaceName = nlAttr.getValueAsString();
+        }
+
+        return linkMsg;
+    }
+
+    /**
+     * Write a rtnetlink link message to {@link ByteBuffer}.
+     */
+    @VisibleForTesting
+    protected void pack(ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer);
+        mIfinfomsg.pack(byteBuffer);
+
+        if (mMtu != 0) {
+            final StructNlAttr mtu = new StructNlAttr(IFLA_MTU, mMtu);
+            mtu.pack(byteBuffer);
+        }
+        if (mHardwareAddress != null) {
+            final StructNlAttr hardwareAddress = new StructNlAttr(IFLA_ADDRESS, mHardwareAddress);
+            hardwareAddress.pack(byteBuffer);
+        }
+        if (mInterfaceName != null) {
+            final StructNlAttr ifname = new StructNlAttr(IFLA_IFNAME, mInterfaceName);
+            ifname.pack(byteBuffer);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "RtNetlinkLinkMessage{ "
+                + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+                + "Ifinfomsg{" + mIfinfomsg.toString() + "}, "
+                + "Hardware Address{" + mHardwareAddress + "}, "
+                + "MTU{" + mMtu + "}, "
+                + "Ifname{" + mInterfaceName + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkNeighborMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkNeighborMessage.java
new file mode 100644
index 0000000..4a09015
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkNeighborMessage.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REPLACE;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink neighbor messages.
+ *
+ * see also: &lt;linux_src&gt;/include/uapi/linux/neighbour.h
+ *
+ * @hide
+ */
+public class RtNetlinkNeighborMessage extends NetlinkMessage {
+    public static final short NDA_UNSPEC    = 0;
+    public static final short NDA_DST       = 1;
+    public static final short NDA_LLADDR    = 2;
+    public static final short NDA_CACHEINFO = 3;
+    public static final short NDA_PROBES    = 4;
+    public static final short NDA_VLAN      = 5;
+    public static final short NDA_PORT      = 6;
+    public static final short NDA_VNI       = 7;
+    public static final short NDA_IFINDEX   = 8;
+    public static final short NDA_MASTER    = 9;
+
+    /**
+     * Parse routing socket netlink neighbor message from ByteBuffer.
+     *
+     * @param header netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+     */
+    @Nullable
+    public static RtNetlinkNeighborMessage parse(@NonNull StructNlMsgHdr header,
+            @NonNull ByteBuffer byteBuffer) {
+        final RtNetlinkNeighborMessage neighMsg = new RtNetlinkNeighborMessage(header);
+
+        neighMsg.mNdmsg = StructNdMsg.parse(byteBuffer);
+        if (neighMsg.mNdmsg == null) {
+            return null;
+        }
+
+        // Some of these are message-type dependent, and not always present.
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(NDA_DST, byteBuffer);
+        if (nlAttr != null) {
+            neighMsg.mDestination = nlAttr.getValueAsInetAddress();
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(NDA_LLADDR, byteBuffer);
+        if (nlAttr != null) {
+            neighMsg.mLinkLayerAddr = nlAttr.nla_value;
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(NDA_PROBES, byteBuffer);
+        if (nlAttr != null) {
+            neighMsg.mNumProbes = nlAttr.getValueAsInt(0);
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(NDA_CACHEINFO, byteBuffer);
+        if (nlAttr != null) {
+            neighMsg.mCacheInfo = StructNdaCacheInfo.parse(nlAttr.getValueAsByteBuffer());
+        }
+
+        final int kMinConsumed = StructNlMsgHdr.STRUCT_SIZE + StructNdMsg.STRUCT_SIZE;
+        final int kAdditionalSpace = NetlinkConstants.alignedLengthOf(
+                neighMsg.mHeader.nlmsg_len - kMinConsumed);
+        if (byteBuffer.remaining() < kAdditionalSpace) {
+            byteBuffer.position(byteBuffer.limit());
+        } else {
+            byteBuffer.position(baseOffset + kAdditionalSpace);
+        }
+
+        return neighMsg;
+    }
+
+    /**
+     * A convenience method to create an RTM_GETNEIGH request message.
+     */
+    public static byte[] newGetNeighborsRequest(int seqNo) {
+        final int length = StructNlMsgHdr.STRUCT_SIZE + StructNdMsg.STRUCT_SIZE;
+        final byte[] bytes = new byte[length];
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+        nlmsghdr.nlmsg_len = length;
+        nlmsghdr.nlmsg_type = NetlinkConstants.RTM_GETNEIGH;
+        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+        nlmsghdr.nlmsg_seq = seqNo;
+        nlmsghdr.pack(byteBuffer);
+
+        final StructNdMsg ndmsg = new StructNdMsg();
+        ndmsg.pack(byteBuffer);
+
+        return bytes;
+    }
+
+    /**
+     * A convenience method to create an RTM_NEWNEIGH message, to modify
+     * the kernel's state information for a specific neighbor.
+     */
+    public static byte[] newNewNeighborMessage(
+            int seqNo, InetAddress ip, short nudState, int ifIndex, byte[] llAddr) {
+        final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+        nlmsghdr.nlmsg_type = NetlinkConstants.RTM_NEWNEIGH;
+        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_REPLACE;
+        nlmsghdr.nlmsg_seq = seqNo;
+
+        final RtNetlinkNeighborMessage msg = new RtNetlinkNeighborMessage(nlmsghdr);
+        msg.mNdmsg = new StructNdMsg();
+        msg.mNdmsg.ndm_family =
+                (byte) ((ip instanceof Inet6Address) ? OsConstants.AF_INET6 : OsConstants.AF_INET);
+        msg.mNdmsg.ndm_ifindex = ifIndex;
+        msg.mNdmsg.ndm_state = nudState;
+        msg.mDestination = ip;
+        msg.mLinkLayerAddr = llAddr;  // might be null
+
+        final byte[] bytes = new byte[msg.getRequiredSpace()];
+        nlmsghdr.nlmsg_len = bytes.length;
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        msg.pack(byteBuffer);
+        return bytes;
+    }
+
+    private StructNdMsg mNdmsg;
+    private InetAddress mDestination;
+    private byte[] mLinkLayerAddr;
+    private int mNumProbes;
+    private StructNdaCacheInfo mCacheInfo;
+
+    private RtNetlinkNeighborMessage(@NonNull StructNlMsgHdr header) {
+        super(header);
+        mNdmsg = null;
+        mDestination = null;
+        mLinkLayerAddr = null;
+        mNumProbes = 0;
+        mCacheInfo = null;
+    }
+
+    public StructNdMsg getNdHeader() {
+        return mNdmsg;
+    }
+
+    public InetAddress getDestination() {
+        return mDestination;
+    }
+
+    public byte[] getLinkLayerAddress() {
+        return mLinkLayerAddr;
+    }
+
+    public int getProbes() {
+        return mNumProbes;
+    }
+
+    public StructNdaCacheInfo getCacheInfo() {
+        return mCacheInfo;
+    }
+
+    private int getRequiredSpace() {
+        int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructNdMsg.STRUCT_SIZE;
+        if (mDestination != null) {
+            spaceRequired += NetlinkConstants.alignedLengthOf(
+                    StructNlAttr.NLA_HEADERLEN + mDestination.getAddress().length);
+        }
+        if (mLinkLayerAddr != null) {
+            spaceRequired += NetlinkConstants.alignedLengthOf(
+                    StructNlAttr.NLA_HEADERLEN + mLinkLayerAddr.length);
+        }
+        // Currently we don't write messages with NDA_PROBES nor NDA_CACHEINFO
+        // attributes appended.  Fix later, if necessary.
+        return spaceRequired;
+    }
+
+    private static void packNlAttr(short nlType, byte[] nlValue, ByteBuffer byteBuffer) {
+        final StructNlAttr nlAttr = new StructNlAttr();
+        nlAttr.nla_type = nlType;
+        nlAttr.nla_value = nlValue;
+        nlAttr.nla_len = (short) (StructNlAttr.NLA_HEADERLEN + nlAttr.nla_value.length);
+        nlAttr.pack(byteBuffer);
+    }
+
+    /**
+     * Write a neighbor discovery netlink message to {@link ByteBuffer}.
+     */
+    public void pack(ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer);
+        mNdmsg.pack(byteBuffer);
+
+        if (mDestination != null) {
+            packNlAttr(NDA_DST, mDestination.getAddress(), byteBuffer);
+        }
+        if (mLinkLayerAddr != null) {
+            packNlAttr(NDA_LLADDR, mLinkLayerAddr, byteBuffer);
+        }
+    }
+
+    @Override
+    public String toString() {
+        final String ipLiteral = (mDestination == null) ? "" : mDestination.getHostAddress();
+        return "RtNetlinkNeighborMessage{ "
+                + "nlmsghdr{"
+                + (mHeader == null ? "" : mHeader.toString(OsConstants.NETLINK_ROUTE)) + "}, "
+                + "ndmsg{" + (mNdmsg == null ? "" : mNdmsg.toString()) + "}, "
+                + "destination{" + ipLiteral + "} "
+                + "linklayeraddr{" + NetlinkConstants.hexify(mLinkLayerAddr) + "} "
+                + "probes{" + mNumProbes + "} "
+                + "cacheinfo{" + (mCacheInfo == null ? "" : mCacheInfo.toString()) + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
new file mode 100644
index 0000000..545afea
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -0,0 +1,340 @@
+/*
+ * 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.net.module.util.netlink;
+
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
+
+import android.annotation.SuppressLint;
+import android.net.IpPrefix;
+import android.net.RouteInfo;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink route messages.
+ *
+ * RtNetlinkRouteMessage.parse() must be called with a ByteBuffer that contains exactly one
+ * netlink message.
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class RtNetlinkRouteMessage extends NetlinkMessage {
+    public static final short RTA_DST           = 1;
+    public static final short RTA_SRC           = 2;
+    public static final short RTA_IIF           = 3;
+    public static final short RTA_OIF           = 4;
+    public static final short RTA_GATEWAY       = 5;
+    public static final short RTA_CACHEINFO     = 12;
+    public static final short RTA_EXPIRES       = 23;
+
+    public static final short RTNH_F_UNRESOLVED = 32;   // The multicast route is unresolved
+
+    public static final String TAG = "NetlinkRouteMessage";
+
+    // For multicast routes, whether the route is resolved or unresolved
+    private boolean mIsResolved;
+    // The interface index for incoming interface, this is set for multicast
+    // routes, see common/net/ipv4/ipmr_base.c mr_fill_mroute
+    private int mIifIndex; // Incoming interface of a route, for resolved multicast routes
+    private int mOifIndex;
+    @NonNull
+    private StructRtMsg mRtmsg;
+    @Nullable
+    private IpPrefix mSource; // Source address of a route, for all multicast routes
+    @Nullable
+    private IpPrefix mDestination; // Destination of a route, can be null for RTM_GETROUTE
+    @Nullable
+    private InetAddress mGateway;
+    @Nullable
+    private StructRtaCacheInfo mRtaCacheInfo;
+    private long mSinceLastUseMillis; // Milliseconds since the route was used,
+                                      // for resolved multicast routes
+
+
+    @VisibleForTesting
+    public RtNetlinkRouteMessage(final StructNlMsgHdr header, final StructRtMsg rtMsg,
+            final IpPrefix source, final IpPrefix destination, final InetAddress gateway,
+            int iif, int oif, final StructRtaCacheInfo cacheInfo) {
+        super(header);
+        mRtmsg = rtMsg;
+        mSource = source;
+        mDestination = destination;
+        mGateway = gateway;
+        mIifIndex = iif;
+        mOifIndex = oif;
+        mRtaCacheInfo = cacheInfo;
+        mSinceLastUseMillis = -1;
+    }
+
+    public RtNetlinkRouteMessage(StructNlMsgHdr header, StructRtMsg rtMsg) {
+        this(header, rtMsg, null /* source */, null /* destination */, null /* gateway */,
+                0 /* iif */, 0 /* oif */, null /* cacheInfo */);
+    }
+
+    /**
+     * Returns the rtnetlink family.
+     */
+    public short getRtmFamily() {
+        return mRtmsg.family;
+    }
+
+    /**
+     * Returns if the route is resolved. This is always true for unicast,
+     * and may be false only for multicast routes.
+     */
+    public boolean isResolved() {
+        return mIsResolved;
+    }
+
+    public int getIifIndex() {
+        return mIifIndex;
+    }
+
+    public int getInterfaceIndex() {
+        return mOifIndex;
+    }
+
+    @NonNull
+    public StructRtMsg getRtMsgHeader() {
+        return mRtmsg;
+    }
+
+    @NonNull
+    public IpPrefix getDestination() {
+        return mDestination;
+    }
+
+    /**
+     * Get source address of a route. This is for multicast routes.
+     */
+    @NonNull
+    public IpPrefix getSource() {
+        return mSource;
+    }
+
+    @Nullable
+    public InetAddress getGateway() {
+        return mGateway;
+    }
+
+    @Nullable
+    public StructRtaCacheInfo getRtaCacheInfo() {
+        return mRtaCacheInfo;
+    }
+
+    /**
+     * RTA_EXPIRES attribute returned by kernel to indicate the clock ticks
+     * from the route was last used to now, converted to milliseconds.
+     * This is set for multicast routes.
+     *
+     * Note that this value is not updated with the passage of time. It always
+     * returns the value that was read when the netlink message was parsed.
+     */
+    public long getSinceLastUseMillis() {
+        return mSinceLastUseMillis;
+    }
+
+    /**
+     * Check whether the address families of destination and gateway match rtm_family in
+     * StructRtmsg.
+     *
+     * For example, IPv4-mapped IPv6 addresses as an IPv6 address will be always converted to IPv4
+     * address, that's incorrect when upper layer creates a new {@link RouteInfo} class instance
+     * for IPv6 route with the converted IPv4 gateway.
+     */
+    private static boolean matchRouteAddressFamily(@NonNull final InetAddress address,
+            int family) {
+        return ((address instanceof Inet4Address) && (family == AF_INET))
+                || ((address instanceof Inet6Address) &&
+                        (family == AF_INET6 || family == RTNL_FAMILY_IP6MR));
+    }
+
+    /**
+     * Parse rtnetlink route message from {@link ByteBuffer}. This method must be called with a
+     * ByteBuffer that contains exactly one netlink message.
+     *
+     * @param header netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+     */
+    @SuppressLint("NewApi")
+    @Nullable
+    public static RtNetlinkRouteMessage parse(@NonNull final StructNlMsgHdr header,
+            @NonNull final ByteBuffer byteBuffer) {
+        final StructRtMsg rtmsg = StructRtMsg.parse(byteBuffer);
+        if (rtmsg == null) return null;
+        final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header, rtmsg);
+        int rtmFamily = routeMsg.mRtmsg.family;
+        routeMsg.mIsResolved = ((routeMsg.mRtmsg.flags & RTNH_F_UNRESOLVED) == 0);
+
+        // RTA_DST
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(RTA_DST, byteBuffer);
+        if (nlAttr != null) {
+            final InetAddress destination = nlAttr.getValueAsInetAddress();
+            // If the RTA_DST attribute is malformed, return null.
+            if (destination == null) return null;
+            // If the address family of destination doesn't match rtm_family, return null.
+            if (!matchRouteAddressFamily(destination, rtmFamily)) return null;
+            routeMsg.mDestination = new IpPrefix(destination, routeMsg.mRtmsg.dstLen);
+        } else if (rtmFamily == AF_INET) {
+            routeMsg.mDestination = new IpPrefix(IPV4_ADDR_ANY, 0);
+        } else if (rtmFamily == AF_INET6 || rtmFamily == RTNL_FAMILY_IP6MR) {
+            routeMsg.mDestination = new IpPrefix(IPV6_ADDR_ANY, 0);
+        } else {
+            return null;
+        }
+
+        // RTA_SRC
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_SRC, byteBuffer);
+        if (nlAttr != null) {
+            final InetAddress source = nlAttr.getValueAsInetAddress();
+            // If the RTA_SRC attribute is malformed, return null.
+            if (source == null) return null;
+            // If the address family of destination doesn't match rtm_family, return null.
+            if (!matchRouteAddressFamily(source, rtmFamily)) return null;
+            routeMsg.mSource = new IpPrefix(source, routeMsg.mRtmsg.srcLen);
+        }
+
+        // RTA_GATEWAY
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_GATEWAY, byteBuffer);
+        if (nlAttr != null) {
+            routeMsg.mGateway = nlAttr.getValueAsInetAddress();
+            // If the RTA_GATEWAY attribute is malformed, return null.
+            if (routeMsg.mGateway == null) return null;
+            // If the address family of gateway doesn't match rtm_family, return null.
+            if (!matchRouteAddressFamily(routeMsg.mGateway, rtmFamily)) return null;
+        }
+
+        // RTA_IIF
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_IIF, byteBuffer);
+        if (nlAttr != null) {
+            Integer iifInteger = nlAttr.getValueAsInteger();
+            if (iifInteger == null) {
+                return null;
+            }
+            routeMsg.mIifIndex = iifInteger;
+        }
+
+        // RTA_OIF
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_OIF, byteBuffer);
+        if (nlAttr != null) {
+            // Any callers that deal with interface names are responsible for converting
+            // the interface index to a name themselves. This may not succeed or may be
+            // incorrect, because the interface might have been deleted, or even deleted
+            // and re-added with a different index, since the netlink message was sent.
+            routeMsg.mOifIndex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
+        }
+
+        // RTA_CACHEINFO
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_CACHEINFO, byteBuffer);
+        if (nlAttr != null) {
+            routeMsg.mRtaCacheInfo = StructRtaCacheInfo.parse(nlAttr.getValueAsByteBuffer());
+        }
+
+        // RTA_EXPIRES
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_EXPIRES, byteBuffer);
+        if (nlAttr != null) {
+            final Long sinceLastUseCentis = nlAttr.getValueAsLong();
+            // If the RTA_EXPIRES attribute is malformed, return null.
+            if (sinceLastUseCentis == null) return null;
+            // RTA_EXPIRES returns time in clock ticks of USER_HZ(100), which is centiseconds
+            routeMsg.mSinceLastUseMillis = sinceLastUseCentis * 10;
+        }
+
+        return routeMsg;
+    }
+
+    /**
+     * Write a rtnetlink address message to {@link ByteBuffer}.
+     */
+    public void pack(ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer);
+        mRtmsg.pack(byteBuffer);
+
+        if (mSource != null) {
+            final StructNlAttr source = new StructNlAttr(RTA_SRC, mSource.getAddress());
+            source.pack(byteBuffer);
+        }
+
+        if (mDestination != null) {
+            final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
+            destination.pack(byteBuffer);
+        }
+
+        if (mGateway != null) {
+            final StructNlAttr gateway = new StructNlAttr(RTA_GATEWAY, mGateway.getAddress());
+            gateway.pack(byteBuffer);
+        }
+        if (mIifIndex != 0) {
+            final StructNlAttr iifindex = new StructNlAttr(RTA_IIF, mIifIndex);
+            iifindex.pack(byteBuffer);
+        }
+        if (mOifIndex != 0) {
+            final StructNlAttr oifindex = new StructNlAttr(RTA_OIF, mOifIndex);
+            oifindex.pack(byteBuffer);
+        }
+        if (mRtaCacheInfo != null) {
+            final StructNlAttr cacheInfo = new StructNlAttr(RTA_CACHEINFO,
+                    mRtaCacheInfo.writeToBytes());
+            cacheInfo.pack(byteBuffer);
+        }
+        if (mSinceLastUseMillis >= 0) {
+            final long sinceLastUseCentis = mSinceLastUseMillis / 10;
+            final StructNlAttr expires = new StructNlAttr(RTA_EXPIRES, sinceLastUseCentis);
+            expires.pack(byteBuffer);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "RtNetlinkRouteMessage{ "
+                + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+                + "Rtmsg{" + mRtmsg.toString() + "}, "
+                + (mSource == null ? "" : "source{" + mSource.getAddress().getHostAddress() + "}, ")
+                + (mDestination == null ?
+                        "" : "destination{" + mDestination.getAddress().getHostAddress() + "}, ")
+                + "gateway{" + (mGateway == null ? "" : mGateway.getHostAddress()) + "}, "
+                + (mIifIndex == 0 ? "" : "iifindex{" + mIifIndex + "}, ")
+                + "oifindex{" + mOifIndex + "}, "
+                + "rta_cacheinfo{" + (mRtaCacheInfo == null ? "" : mRtaCacheInfo.toString()) + "} "
+                + (mSinceLastUseMillis < 0 ? "" : "sinceLastUseMillis{" + mSinceLastUseMillis + "}")
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructIfacacheInfo.java b/staticlibs/device/com/android/net/module/util/netlink/StructIfacacheInfo.java
new file mode 100644
index 0000000..360f56d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructIfacacheInfo.java
@@ -0,0 +1,79 @@
+/*
+ * 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.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+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 ifa_cacheinfo
+ *
+ * see also:
+ *
+ *     include/uapi/linux/if_addr.h
+ *
+ * @hide
+ */
+public class StructIfacacheInfo extends Struct {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 16;
+
+    @Field(order = 0, type = Type.U32)
+    public final long preferred;
+    @Field(order = 1, type = Type.U32)
+    public final long valid;
+    @Field(order = 2, type = Type.U32)
+    public final long cstamp; // created timestamp, hundredths of seconds.
+    @Field(order = 3, type = Type.U32)
+    public final long tstamp; // updated timestamp, hundredths of seconds.
+
+    StructIfacacheInfo(long preferred, long valid, long cstamp, long tstamp) {
+        this.preferred = preferred;
+        this.valid = valid;
+        this.cstamp = cstamp;
+        this.tstamp = tstamp;
+    }
+
+    /**
+     * Parse an ifa_cacheinfo struct from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the ifa_cacheinfo.
+     * @return the parsed ifa_cacheinfo struct, or {@code null} if the ifa_cacheinfo struct
+     *         could not be parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructIfacacheInfo parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) return null;
+
+        // The ByteOrder must already have been set to native order.
+        return Struct.parse(StructIfacacheInfo.class, byteBuffer);
+    }
+
+    /**
+     * Write an ifa_cacheinfo struct to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        this.writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java
new file mode 100644
index 0000000..f9781a7
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java
@@ -0,0 +1,82 @@
+/*
+ * 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.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+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 ifaddrmsg
+ *
+ * see also:
+ *
+ *     include/uapi/linux/if_addr.h
+ *
+ * @hide
+ */
+public class StructIfaddrMsg extends Struct {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 8;
+
+    @Field(order = 0, type = Type.U8)
+    public final short family;
+    @Field(order = 1, type = Type.U8)
+    public final short prefixLen;
+    @Field(order = 2, type = Type.U8)
+    public final short flags;
+    @Field(order = 3, type = Type.U8)
+    public final short scope;
+    @Field(order = 4, type = Type.S32)
+    public final int index;
+
+    public StructIfaddrMsg(short family, short prefixLen, short flags, short scope, int index) {
+        this.family = family;
+        this.prefixLen = prefixLen;
+        this.flags = flags;
+        this.scope = scope;
+        this.index = index;
+    }
+
+    /**
+     * Parse an ifaddrmsg struct from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the ifaddrmsg.
+     * @return the parsed ifaddrmsg struct, or {@code null} if the ifaddrmsg struct
+     *         could not be parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructIfaddrMsg parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) return null;
+
+        // The ByteOrder must already have been set to native order.
+        return Struct.parse(StructIfaddrMsg.class, byteBuffer);
+    }
+
+    /**
+     * Write an ifaddrmsg struct to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        this.writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructIfinfoMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructIfinfoMsg.java
new file mode 100644
index 0000000..28eeaea
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructIfinfoMsg.java
@@ -0,0 +1,82 @@
+/*
+ * 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.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+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 ifinfomsg
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class StructIfinfoMsg extends Struct {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 16;
+
+    @Field(order = 0, type = Type.U8, padding = 1)
+    public final short family;
+    @Field(order = 1, type = Type.U16)
+    public final int type;
+    @Field(order = 2, type = Type.S32)
+    public final int index;
+    @Field(order = 3, type = Type.U32)
+    public final long flags;
+    @Field(order = 4, type = Type.U32)
+    public final long change;
+
+    public StructIfinfoMsg(short family, int type, int index, long flags, long change) {
+        this.family = family;
+        this.type = type;
+        this.index = index;
+        this.flags = flags;
+        this.change = change;
+    }
+
+    /**
+     * Parse an ifinfomsg struct from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the ifinfomsg.
+     * @return the parsed ifinfomsg struct, or {@code null} if the ifinfomsg struct
+     *         could not be parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructIfinfoMsg parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) return null;
+
+        // The ByteOrder must already have been set to native order.
+        return Struct.parse(StructIfinfoMsg.class, byteBuffer);
+    }
+
+    /**
+     * Write an ifinfomsg struct to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        this.writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java
new file mode 100644
index 0000000..cbd895d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct inet_diag_msg
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/inet_diag.h
+ *
+ * struct inet_diag_msg {
+ *      __u8    idiag_family;
+ *      __u8    idiag_state;
+ *      __u8    idiag_timer;
+ *      __u8    idiag_retrans;
+ *      struct  inet_diag_sockid id;
+ *      __u32   idiag_expires;
+ *      __u32   idiag_rqueue;
+ *      __u32   idiag_wqueue;
+ *      __u32   idiag_uid;
+ *      __u32   idiag_inode;
+ * };
+ *
+ * @hide
+ */
+public class StructInetDiagMsg {
+    public static final int STRUCT_SIZE = 4 + StructInetDiagSockId.STRUCT_SIZE + 20;
+    public short idiag_family;
+    public short idiag_state;
+    public short idiag_timer;
+    public short idiag_retrans;
+    @NonNull
+    public StructInetDiagSockId id;
+    public long idiag_expires;
+    public long idiag_rqueue;
+    public long idiag_wqueue;
+    // Use int for uid since other code use int for uid and uid fits to int
+    public int idiag_uid;
+    public long idiag_inode;
+
+    private static short unsignedByte(byte b) {
+        return (short) (b & 0xFF);
+    }
+
+    /**
+     * Parse inet diag netlink message from buffer.
+     */
+    @Nullable
+    public static StructInetDiagMsg parse(@NonNull ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) {
+            return null;
+        }
+        StructInetDiagMsg struct = new StructInetDiagMsg();
+        struct.idiag_family = unsignedByte(byteBuffer.get());
+        struct.idiag_state = unsignedByte(byteBuffer.get());
+        struct.idiag_timer = unsignedByte(byteBuffer.get());
+        struct.idiag_retrans = unsignedByte(byteBuffer.get());
+        struct.id = StructInetDiagSockId.parse(byteBuffer, struct.idiag_family);
+        if (struct.id == null) {
+            return null;
+        }
+        struct.idiag_expires = Integer.toUnsignedLong(byteBuffer.getInt());
+        struct.idiag_rqueue = Integer.toUnsignedLong(byteBuffer.getInt());
+        struct.idiag_wqueue = Integer.toUnsignedLong(byteBuffer.getInt());
+        struct.idiag_uid = byteBuffer.getInt();
+        struct.idiag_inode = Integer.toUnsignedLong(byteBuffer.getInt());
+        return struct;
+    }
+
+    @Override
+    public String toString() {
+        return "StructInetDiagMsg{ "
+                + "idiag_family{" + idiag_family + "}, "
+                + "idiag_state{" + idiag_state + "}, "
+                + "idiag_timer{" + idiag_timer + "}, "
+                + "idiag_retrans{" + idiag_retrans + "}, "
+                + "id{" + id + "}, "
+                + "idiag_expires{" + idiag_expires + "}, "
+                + "idiag_rqueue{" + idiag_rqueue + "}, "
+                + "idiag_wqueue{" + idiag_wqueue + "}, "
+                + "idiag_uid{" + idiag_uid + "}, "
+                + "idiag_inode{" + idiag_inode + "}, "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java
new file mode 100644
index 0000000..3b47008
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct inet_diag_req_v2
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/inet_diag.h
+ *
+ *      struct inet_diag_req_v2 {
+ *          __u8    sdiag_family;
+ *          __u8    sdiag_protocol;
+ *          __u8    idiag_ext;
+ *          __u8    pad;
+ *          __u32   idiag_states;
+ *          struct  inet_diag_sockid id;
+ *      };
+ *
+ * @hide
+ */
+public class StructInetDiagReqV2 {
+    public static final int STRUCT_SIZE = 8 + StructInetDiagSockId.STRUCT_SIZE;
+
+    private final byte mSdiagFamily;
+    private final byte mSdiagProtocol;
+    private final byte mIdiagExt;
+    private final byte mPad;
+    private final StructInetDiagSockId mId;
+    private final int mState;
+    public static final int INET_DIAG_REQ_V2_ALL_STATES = (int) 0xffffffff;
+
+    public StructInetDiagReqV2(int protocol, @Nullable StructInetDiagSockId id, int family, int pad,
+            int extension, int state) {
+        mSdiagFamily = (byte) family;
+        mSdiagProtocol = (byte) protocol;
+        mId = id;
+        mPad = (byte) pad;
+        mIdiagExt = (byte) extension;
+        mState = state;
+    }
+
+    /**
+     * Write the int diag request v2 message to ByteBuffer.
+     */
+    public void pack(ByteBuffer byteBuffer) {
+        // The ByteOrder must have already been set by the caller.
+        byteBuffer.put((byte) mSdiagFamily);
+        byteBuffer.put((byte) mSdiagProtocol);
+        byteBuffer.put((byte) mIdiagExt);
+        byteBuffer.put((byte) mPad);
+        byteBuffer.putInt(mState);
+        if (mId != null) mId.pack(byteBuffer);
+    }
+
+    @Override
+    public String toString() {
+        final String familyStr = NetlinkConstants.stringForAddressFamily(mSdiagFamily);
+        final String protocolStr = NetlinkConstants.stringForAddressFamily(mSdiagProtocol);
+
+        return "StructInetDiagReqV2{ "
+                + "sdiag_family{" + familyStr + "}, "
+                + "sdiag_protocol{" + protocolStr + "}, "
+                + "idiag_ext{" + mIdiagExt + ")}, "
+                + "pad{" + mPad + "}, "
+                + "idiag_states{" + Integer.toHexString(mState) + "}, "
+                + ((mId != null) ? mId.toString() : "inet_diag_sockid=null")
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java
new file mode 100644
index 0000000..dd85934
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java
@@ -0,0 +1,174 @@
+/*
+ * 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.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+
+import static java.nio.ByteOrder.BIG_ENDIAN;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * struct inet_diag_req_v2
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/inet_diag.h
+ *
+ * struct inet_diag_sockid {
+ *        __be16    idiag_sport;
+ *        __be16    idiag_dport;
+ *        __be32    idiag_src[4];
+ *        __be32    idiag_dst[4];
+ *        __u32     idiag_if;
+ *        __u32     idiag_cookie[2];
+ * #define INET_DIAG_NOCOOKIE (~0U)
+ * };
+ *
+ * @hide
+ */
+public class StructInetDiagSockId {
+    private static final String TAG = StructInetDiagSockId.class.getSimpleName();
+    public static final int STRUCT_SIZE = 48;
+
+    private static final long INET_DIAG_NOCOOKIE = ~0L;
+    private static final byte[] IPV4_PADDING = new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+    public final InetSocketAddress locSocketAddress;
+    public final InetSocketAddress remSocketAddress;
+    public final int ifIndex;
+    public final long cookie;
+
+    public StructInetDiagSockId(InetSocketAddress loc, InetSocketAddress rem) {
+        this(loc, rem, 0 /* ifIndex */, INET_DIAG_NOCOOKIE);
+    }
+
+    public StructInetDiagSockId(InetSocketAddress loc, InetSocketAddress rem,
+            int ifIndex, long cookie) {
+        this.locSocketAddress = loc;
+        this.remSocketAddress = rem;
+        this.ifIndex = ifIndex;
+        this.cookie = cookie;
+    }
+
+    /**
+     * Parse inet diag socket id from buffer.
+     */
+    @Nullable
+    public static StructInetDiagSockId parse(final ByteBuffer byteBuffer, final short family) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) {
+            return null;
+        }
+
+        byteBuffer.order(BIG_ENDIAN);
+        final int srcPort = Short.toUnsignedInt(byteBuffer.getShort());
+        final int dstPort = Short.toUnsignedInt(byteBuffer.getShort());
+
+        final InetAddress srcAddr;
+        final InetAddress dstAddr;
+        if (family == AF_INET) {
+            final byte[] srcAddrByte = new byte[IPV4_ADDR_LEN];
+            final byte[] dstAddrByte = new byte[IPV4_ADDR_LEN];
+            byteBuffer.get(srcAddrByte);
+            // Address always uses IPV6_ADDR_LEN in the buffer. So if the address is IPv4, position
+            // needs to be advanced to the next field.
+            byteBuffer.position(byteBuffer.position() + (IPV6_ADDR_LEN - IPV4_ADDR_LEN));
+            byteBuffer.get(dstAddrByte);
+            byteBuffer.position(byteBuffer.position() + (IPV6_ADDR_LEN - IPV4_ADDR_LEN));
+            try {
+                srcAddr = Inet4Address.getByAddress(srcAddrByte);
+                dstAddr = Inet4Address.getByAddress(dstAddrByte);
+            } catch (UnknownHostException e) {
+                Log.wtf(TAG, "Failed to parse address: " + e);
+                return null;
+            }
+        } else if (family == AF_INET6) {
+            final byte[] srcAddrByte = new byte[IPV6_ADDR_LEN];
+            final byte[] dstAddrByte = new byte[IPV6_ADDR_LEN];
+            byteBuffer.get(srcAddrByte);
+            byteBuffer.get(dstAddrByte);
+            try {
+                // Using Inet6Address.getByAddress to be consistent with idiag_family field since
+                // InetAddress.getByAddress returns Inet4Address if the address is v4-mapped v6
+                // address.
+                srcAddr = Inet6Address.getByAddress(
+                        null /* host */, srcAddrByte, -1 /* scope_id */);
+                dstAddr = Inet6Address.getByAddress(
+                        null /* host */, dstAddrByte, -1 /* scope_id */);
+            } catch (UnknownHostException e) {
+                Log.wtf(TAG, "Failed to parse address: " + e);
+                return null;
+            }
+        } else {
+            Log.wtf(TAG, "Invalid address family: " + family);
+            return null;
+        }
+
+        final InetSocketAddress srcSocketAddr = new InetSocketAddress(srcAddr, srcPort);
+        final InetSocketAddress dstSocketAddr = new InetSocketAddress(dstAddr, dstPort);
+
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final int ifIndex = byteBuffer.getInt();
+        final long cookie = byteBuffer.getLong();
+        return new StructInetDiagSockId(srcSocketAddr, dstSocketAddr, ifIndex, cookie);
+    }
+
+    /**
+     * Write inet diag socket id message to ByteBuffer in big endian.
+     */
+    public void pack(ByteBuffer byteBuffer) {
+        byteBuffer.order(BIG_ENDIAN);
+        byteBuffer.putShort((short) locSocketAddress.getPort());
+        byteBuffer.putShort((short) remSocketAddress.getPort());
+        byteBuffer.put(locSocketAddress.getAddress().getAddress());
+        if (locSocketAddress.getAddress() instanceof Inet4Address) {
+            byteBuffer.put(IPV4_PADDING);
+        }
+        byteBuffer.put(remSocketAddress.getAddress().getAddress());
+        if (remSocketAddress.getAddress() instanceof Inet4Address) {
+            byteBuffer.put(IPV4_PADDING);
+        }
+        byteBuffer.order(ByteOrder.nativeOrder());
+        byteBuffer.putInt(ifIndex);
+        byteBuffer.putLong(cookie);
+    }
+
+    @Override
+    public String toString() {
+        return "StructInetDiagSockId{ "
+                + "idiag_sport{" + locSocketAddress.getPort() + "}, "
+                + "idiag_dport{" + remSocketAddress.getPort() + "}, "
+                + "idiag_src{" + locSocketAddress.getAddress().getHostAddress() + "}, "
+                + "idiag_dst{" + remSocketAddress.getAddress().getHostAddress() + "}, "
+                + "idiag_if{" + ifIndex + "}, "
+                + "idiag_cookie{"
+                + (cookie == INET_DIAG_NOCOOKIE ? "INET_DIAG_NOCOOKIE" : cookie) + "}"
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNdMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructNdMsg.java
new file mode 100644
index 0000000..53ce899
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNdMsg.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import android.system.OsConstants;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct ndmsg
+ *
+ * see: &lt;linux_src&gt;/include/uapi/linux/neighbour.h
+ *
+ * @hide
+ */
+public class StructNdMsg {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 12;
+
+    // Neighbor Cache Entry States
+    public static final short NUD_NONE        = 0x00;
+    public static final short NUD_INCOMPLETE  = 0x01;
+    public static final short NUD_REACHABLE   = 0x02;
+    public static final short NUD_STALE       = 0x04;
+    public static final short NUD_DELAY       = 0x08;
+    public static final short NUD_PROBE       = 0x10;
+    public static final short NUD_FAILED      = 0x20;
+    public static final short NUD_NOARP       = 0x40;
+    public static final short NUD_PERMANENT   = 0x80;
+
+    /**
+     * Convert neighbor cache entry state integer to string.
+     */
+    public static String stringForNudState(short nudState) {
+        switch (nudState) {
+            case NUD_NONE: return "NUD_NONE";
+            case NUD_INCOMPLETE: return "NUD_INCOMPLETE";
+            case NUD_REACHABLE: return "NUD_REACHABLE";
+            case NUD_STALE: return "NUD_STALE";
+            case NUD_DELAY: return "NUD_DELAY";
+            case NUD_PROBE: return "NUD_PROBE";
+            case NUD_FAILED: return "NUD_FAILED";
+            case NUD_NOARP: return "NUD_NOARP";
+            case NUD_PERMANENT: return "NUD_PERMANENT";
+            default:
+                return "unknown NUD state: " + String.valueOf(nudState);
+        }
+    }
+
+    /**
+     * Check whether a neighbor is connected or not.
+     */
+    public static boolean isNudStateConnected(short nudState) {
+        return ((nudState & (NUD_PERMANENT | NUD_NOARP | NUD_REACHABLE)) != 0);
+    }
+
+    /**
+     * Check whether a neighbor is in the valid NUD state or not.
+     */
+    public static boolean isNudStateValid(short nudState) {
+        return (isNudStateConnected(nudState)
+                || ((nudState & (NUD_PROBE | NUD_STALE | NUD_DELAY)) != 0));
+    }
+
+    // Neighbor Cache Entry Flags
+    public static byte NTF_USE       = (byte) 0x01;
+    public static byte NTF_SELF      = (byte) 0x02;
+    public static byte NTF_MASTER    = (byte) 0x04;
+    public static byte NTF_PROXY     = (byte) 0x08;
+    public static byte NTF_ROUTER    = (byte) 0x80;
+
+    private static String stringForNudFlags(byte flags) {
+        final StringBuilder sb = new StringBuilder();
+        if ((flags & NTF_USE) != 0) {
+            sb.append("NTF_USE");
+        }
+        if ((flags & NTF_SELF) != 0) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NTF_SELF");
+        }
+        if ((flags & NTF_MASTER) != 0) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NTF_MASTER");
+        }
+        if ((flags & NTF_PROXY) != 0) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NTF_PROXY");
+        }
+        if ((flags & NTF_ROUTER) != 0) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NTF_ROUTER");
+        }
+        return sb.toString();
+    }
+
+    private static boolean hasAvailableSpace(ByteBuffer byteBuffer) {
+        return byteBuffer != null && byteBuffer.remaining() >= STRUCT_SIZE;
+    }
+
+    /**
+     * Parse a neighbor discovery netlink message header from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the nd netlink message header.
+     * @return the parsed nd netlink message header, or {@code null} if the nd netlink message
+     *         header could not be parsed successfully (for example, if it was truncated).
+     */
+    public static StructNdMsg parse(ByteBuffer byteBuffer) {
+        if (!hasAvailableSpace(byteBuffer)) return null;
+
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the possible
+        // exception of usage within unittests.
+        final StructNdMsg struct = new StructNdMsg();
+        struct.ndm_family = byteBuffer.get();
+        final byte pad1 = byteBuffer.get();
+        final short pad2 = byteBuffer.getShort();
+        struct.ndm_ifindex = byteBuffer.getInt();
+        struct.ndm_state = byteBuffer.getShort();
+        struct.ndm_flags = byteBuffer.get();
+        struct.ndm_type = byteBuffer.get();
+        return struct;
+    }
+
+    public byte ndm_family;
+    public int ndm_ifindex;
+    public short ndm_state;
+    public byte ndm_flags;
+    public byte ndm_type;
+
+    public StructNdMsg() {
+        ndm_family = (byte) OsConstants.AF_UNSPEC;
+    }
+
+    /**
+     * Write the neighbor discovery message header to {@link ByteBuffer}.
+     */
+    public void pack(ByteBuffer byteBuffer) {
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the exception
+        // of usage within unittests.
+        byteBuffer.put(ndm_family);
+        byteBuffer.put((byte) 0);         // pad1
+        byteBuffer.putShort((short) 0);   // pad2
+        byteBuffer.putInt(ndm_ifindex);
+        byteBuffer.putShort(ndm_state);
+        byteBuffer.put(ndm_flags);
+        byteBuffer.put(ndm_type);
+    }
+
+    /**
+     * Check whether a neighbor is connected or not.
+     */
+    public boolean nudConnected() {
+        return isNudStateConnected(ndm_state);
+    }
+
+    /**
+     * Check whether a neighbor is in the valid NUD state or not.
+     */
+    public boolean nudValid() {
+        return isNudStateValid(ndm_state);
+    }
+
+    @Override
+    public String toString() {
+        final String stateStr = "" + ndm_state + " (" + stringForNudState(ndm_state) + ")";
+        final String flagsStr = "" + ndm_flags + " (" + stringForNudFlags(ndm_flags) + ")";
+        return "StructNdMsg{ "
+                + "family{" + NetlinkConstants.stringForAddressFamily((int) ndm_family) + "}, "
+                + "ifindex{" + ndm_ifindex + "}, "
+                + "state{" + stateStr + "}, "
+                + "flags{" + flagsStr + "}, "
+                + "type{" + ndm_type + "} "
+                + "}";
+    }
+}
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/StructNdOptPref64.java b/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPref64.java
new file mode 100644
index 0000000..8226346
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPref64.java
@@ -0,0 +1,171 @@
+/*
+ * 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.netlink;
+
+import android.annotation.SuppressLint;
+import android.net.IpPrefix;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * The PREF64 router advertisement option. RFC 8781.
+ *
+ * 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     |     Scaled Lifetime     | PLC |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * +                                                               +
+ * |              Highest 96 bits of the Prefix                    |
+ * +                                                               +
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ */
+public class StructNdOptPref64 extends NdOption {
+    public static final int STRUCT_SIZE = 16;
+    public static final int TYPE = 38;
+    public static final byte LENGTH = 2;
+
+    private static final String TAG = StructNdOptPref64.class.getSimpleName();
+
+    /**
+     * How many seconds the prefix is expected to remain valid.
+     * Valid values are from 0 to 65528 in multiples of 8.
+     */
+    public final int lifetime;
+    /** The NAT64 prefix. */
+    @NonNull public final IpPrefix prefix;
+
+    static int plcToPrefixLength(int plc) {
+        switch (plc) {
+            case 0: return 96;
+            case 1: return 64;
+            case 2: return 56;
+            case 3: return 48;
+            case 4: return 40;
+            case 5: return 32;
+            default:
+                throw new IllegalArgumentException("Invalid prefix length code " + plc);
+        }
+    }
+
+    static int prefixLengthToPlc(int prefixLength) {
+        switch (prefixLength) {
+            case 96: return 0;
+            case 64: return 1;
+            case 56: return 2;
+            case 48: return 3;
+            case 40: return 4;
+            case 32: return 5;
+            default:
+                throw new IllegalArgumentException("Invalid prefix length " + prefixLength);
+        }
+    }
+
+    /**
+     * Returns the 2-byte "scaled lifetime and prefix length code" field: 13-bit lifetime, 3-bit PLC
+     */
+    static short getScaledLifetimePlc(int lifetime, int prefixLengthCode) {
+        return (short) ((lifetime & 0xfff8) | (prefixLengthCode & 0x7));
+    }
+
+    public StructNdOptPref64(@NonNull IpPrefix prefix, int lifetime) {
+        super((byte) TYPE, LENGTH);
+
+        Objects.requireNonNull(prefix, "prefix must not be null");
+        if (!(prefix.getAddress() instanceof Inet6Address)) {
+            throw new IllegalArgumentException("Must be an IPv6 prefix: " + prefix);
+        }
+        prefixLengthToPlc(prefix.getPrefixLength());  // Throw if the prefix length is invalid.
+        this.prefix = prefix;
+
+        if (lifetime < 0 || lifetime > 0xfff8) {
+            throw new IllegalArgumentException("Invalid lifetime " + lifetime);
+        }
+        this.lifetime = lifetime & 0xfff8;
+    }
+
+    @SuppressLint("NewApi")
+    private StructNdOptPref64(@NonNull ByteBuffer buf) {
+        super(buf.get(), Byte.toUnsignedInt(buf.get()));
+        if (type != TYPE) throw new IllegalArgumentException("Invalid type " + type);
+        if (length != LENGTH) throw new IllegalArgumentException("Invalid length " + length);
+
+        int scaledLifetimePlc = Short.toUnsignedInt(buf.getShort());
+        lifetime = scaledLifetimePlc & 0xfff8;
+
+        byte[] addressBytes = new byte[16];
+        buf.get(addressBytes, 0, 12);
+        InetAddress addr;
+        try {
+            addr = InetAddress.getByAddress(addressBytes);
+        } catch (UnknownHostException e) {
+            throw new AssertionError("16-byte array not valid InetAddress?");
+        }
+        prefix = new IpPrefix(addr, plcToPrefixLength(scaledLifetimePlc & 7));
+    }
+
+    /**
+     * Parses an 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
+     *         (for example, if it was truncated, or if the prefix length code was wrong).
+     */
+    public static StructNdOptPref64 parse(@NonNull ByteBuffer buf) {
+        if (buf.remaining() < STRUCT_SIZE) return null;
+        try {
+            return new StructNdOptPref64(buf);
+        } catch (IllegalArgumentException 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, so
+            // callers must already handle it.
+            Log.d(TAG, "Invalid PREF64 option: " + e);
+            return null;
+        }
+    }
+
+    protected void writeToByteBuffer(ByteBuffer buf) {
+        super.writeToByteBuffer(buf);
+        buf.putShort(getScaledLifetimePlc(lifetime,  prefixLengthToPlc(prefix.getPrefixLength())));
+        buf.put(prefix.getRawAddress(), 0, 12);
+    }
+
+    /** Outputs the wire format of the option to a new big-endian ByteBuffer. */
+    public ByteBuffer toByteBuffer() {
+        ByteBuffer buf = ByteBuffer.allocate(STRUCT_SIZE);
+        writeToByteBuffer(buf);
+        buf.flip();
+        return buf;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return String.format("NdOptPref64(%s, %d)", prefix, lifetime);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNdOptRdnss.java b/staticlibs/device/com/android/net/module/util/netlink/StructNdOptRdnss.java
new file mode 100644
index 0000000..6dee0c4
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNdOptRdnss.java
@@ -0,0 +1,134 @@
+/*
+ * 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.net.module.util.netlink;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.RdnssOption;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * The Recursive DNS Server Option. RFC 8106.
+ *
+ * 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    |           Reserved            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                           Lifetime                            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * :            Addresses of IPv6 Recursive DNS Servers            :
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class StructNdOptRdnss extends NdOption {
+    private static final String TAG = StructNdOptRdnss.class.getSimpleName();
+    public static final int TYPE = 25;
+    // Length in 8-byte units, only if one IPv6 address included.
+    public static final byte MIN_OPTION_LEN = 3;
+
+    public final RdnssOption header;
+    @NonNull
+    public final Inet6Address[] servers;
+
+    public StructNdOptRdnss(@NonNull final Inet6Address[] servers, long lifetime) {
+        super((byte) TYPE, servers.length * 2 + 1);
+
+        Objects.requireNonNull(servers, "Recursive DNS Servers address array must not be null");
+        if (servers.length == 0) {
+            throw new IllegalArgumentException("DNS server address array must not be empty");
+        }
+
+        this.header = new RdnssOption((byte) TYPE, (byte) (servers.length * 2 + 1),
+                (short) 0 /* reserved */, lifetime);
+        this.servers = servers.clone();
+    }
+
+    /**
+     * Parses an RDNSS 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 StructNdOptRdnss parse(@NonNull ByteBuffer buf) {
+        if (buf == null || buf.remaining() < MIN_OPTION_LEN * 8) return null;
+        try {
+            final RdnssOption header = Struct.parse(RdnssOption.class, buf);
+            if (header.type != TYPE) {
+                throw new IllegalArgumentException("Invalid type " + header.type);
+            }
+            if (header.length < MIN_OPTION_LEN || (header.length % 2 == 0)) {
+                throw new IllegalArgumentException("Invalid length " + header.length);
+            }
+
+            final int numOfDnses = (header.length - 1) / 2;
+            final Inet6Address[] servers = new Inet6Address[numOfDnses];
+            for (int i = 0; i < numOfDnses; i++) {
+                byte[] rawAddress = new byte[IPV6_ADDR_LEN];
+                buf.get(rawAddress);
+                servers[i] = (Inet6Address) InetAddress.getByAddress(rawAddress);
+            }
+            return new StructNdOptRdnss(servers, header.lifetime);
+        } catch (IllegalArgumentException | BufferUnderflowException | UnknownHostException 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 RDNSS option: " + e);
+            return null;
+        }
+    }
+
+    protected void writeToByteBuffer(ByteBuffer buf) {
+        header.writeToByteBuffer(buf);
+        for (int i = 0; i < servers.length; i++) {
+            buf.put(servers[i].getAddress());
+        }
+    }
+
+    /** Outputs the wire format of the option to a new big-endian ByteBuffer. */
+    public ByteBuffer toByteBuffer() {
+        final ByteBuffer buf = ByteBuffer.allocate(Struct.getSize(RdnssOption.class)
+                + servers.length * IPV6_ADDR_LEN);
+        writeToByteBuffer(buf);
+        buf.flip();
+        return buf;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        final StringJoiner sj = new StringJoiner(",", "[", "]");
+        for (int i = 0; i < servers.length; i++) {
+            sj.add(servers[i].getHostAddress());
+        }
+        return String.format("NdOptRdnss(%s,servers:%s)", header.toString(), sj.toString());
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNdaCacheInfo.java b/staticlibs/device/com/android/net/module/util/netlink/StructNdaCacheInfo.java
new file mode 100644
index 0000000..1f9bb7e
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNdaCacheInfo.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.netlink.NetlinkUtils.ticksToMilliSeconds;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct nda_cacheinfo
+ *
+ * see: &lt;linux_src&gt;/include/uapi/linux/neighbour.h
+ *
+ * @hide
+ */
+public class StructNdaCacheInfo {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 16;
+
+    private static boolean hasAvailableSpace(ByteBuffer byteBuffer) {
+        return byteBuffer != null && byteBuffer.remaining() >= STRUCT_SIZE;
+    }
+
+    /**
+     * Parse a nd cacheinfo netlink attribute from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the nd cacheinfo attribute.
+     * @return the parsed nd cacheinfo attribute, or {@code null} if the nd cacheinfo attribute
+     *         could not be parsed successfully (for example, if it was truncated).
+     */
+    public static StructNdaCacheInfo parse(ByteBuffer byteBuffer) {
+        if (!hasAvailableSpace(byteBuffer)) return null;
+
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the possible
+        // exception of usage within unittests.
+        final StructNdaCacheInfo struct = new StructNdaCacheInfo();
+        struct.ndm_used = byteBuffer.getInt();
+        struct.ndm_confirmed = byteBuffer.getInt();
+        struct.ndm_updated = byteBuffer.getInt();
+        struct.ndm_refcnt = byteBuffer.getInt();
+        return struct;
+    }
+
+    /**
+     * Explanatory notes, for reference.
+     *
+     * Before being returned to user space, the neighbor entry times are
+     * converted to clock_t's like so:
+     *
+     *     ndm_used      = jiffies_to_clock_t(now - neigh->used);
+     *     ndm_confirmed = jiffies_to_clock_t(now - neigh->confirmed);
+     *     ndm_updated   = jiffies_to_clock_t(now - neigh->updated);
+     *
+     * meaning that these values are expressed as "clock ticks ago".  To
+     * convert these clock ticks to seconds divide by sysconf(_SC_CLK_TCK).
+     * When _SC_CLK_TCK is 100, for example, the ndm_* times are expressed
+     * in centiseconds.
+     *
+     * These values are unsigned, but fortunately being expressed as "some
+     * clock ticks ago", these values are typically very small (and
+     * 2^31 centiseconds = 248 days).
+     *
+     * By observation, it appears that:
+     *     ndm_used: the last time ARP/ND took place for this neighbor
+     *     ndm_confirmed: the last time ARP/ND succeeded for this neighbor OR
+     *                    higher layer confirmation (TCP or MSG_CONFIRM)
+     *                    was received
+     *     ndm_updated: the time when the current NUD state was entered
+     */
+    public int ndm_used;
+    public int ndm_confirmed;
+    public int ndm_updated;
+    public int ndm_refcnt;
+
+    public StructNdaCacheInfo() {}
+
+    /**
+     * The last time ARP/ND took place for this neighbor.
+     */
+    public long lastUsed() {
+        return ticksToMilliSeconds(ndm_used);
+    }
+
+    /**
+     * The last time ARP/ND succeeded for this neighbor or higher layer confirmation (TCP or
+     * MSG_CONFIRM) was received.
+     */
+    public long lastConfirmed() {
+        return ticksToMilliSeconds(ndm_confirmed);
+    }
+
+    /**
+     * The time when the current NUD state was entered.
+     */
+    public long lastUpdated() {
+        return ticksToMilliSeconds(ndm_updated);
+    }
+
+    @Override
+    public String toString() {
+        return "NdaCacheInfo{ "
+                + "ndm_used{" + lastUsed() + "}, "
+                + "ndm_confirmed{" + lastConfirmed() + "}, "
+                + "ndm_updated{" + lastUpdated() + "}, "
+                + "ndm_refcnt{" + ndm_refcnt + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNfGenMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructNfGenMsg.java
new file mode 100644
index 0000000..2de5490
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNfGenMsg.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Nullable;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Objects;
+
+/**
+ * struct nfgenmsg
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/netfilter/nfnetlink.h
+ *
+ * @hide
+ */
+public class StructNfGenMsg {
+    public static final int STRUCT_SIZE = 2 + Short.BYTES;
+
+    public static final int NFNETLINK_V0 = 0;
+
+    public final byte nfgen_family;
+    public final byte version;
+    public final short res_id;  // N.B.: this is big endian in the kernel
+
+    /**
+     * Parse a netfilter netlink header from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the netfilter netlink header.
+     * @return the parsed netfilter netlink header, or {@code null} if the netfilter netlink header
+     *         could not be parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructNfGenMsg parse(@NonNull ByteBuffer byteBuffer) {
+        Objects.requireNonNull(byteBuffer);
+
+        if (!hasAvailableSpace(byteBuffer)) return null;
+
+        final byte nfgen_family = byteBuffer.get();
+        final byte version = byteBuffer.get();
+
+        final ByteOrder originalOrder = byteBuffer.order();
+        byteBuffer.order(ByteOrder.BIG_ENDIAN);
+        final short res_id = byteBuffer.getShort();
+        byteBuffer.order(originalOrder);
+
+        return new StructNfGenMsg(nfgen_family, version, res_id);
+    }
+
+    public StructNfGenMsg(byte family, byte ver, short id) {
+        nfgen_family = family;
+        version = ver;
+        res_id = id;
+    }
+
+    public StructNfGenMsg(byte family) {
+        nfgen_family = family;
+        version = (byte) NFNETLINK_V0;
+        res_id = (short) 0;
+    }
+
+    /**
+     * Write a netfilter netlink header to a {@link ByteBuffer}.
+     */
+    public void pack(ByteBuffer byteBuffer) {
+        byteBuffer.put(nfgen_family);
+        byteBuffer.put(version);
+
+        final ByteOrder originalOrder = byteBuffer.order();
+        byteBuffer.order(ByteOrder.BIG_ENDIAN);
+        byteBuffer.putShort(res_id);
+        byteBuffer.order(originalOrder);
+    }
+
+    private static boolean hasAvailableSpace(@NonNull ByteBuffer byteBuffer) {
+        return byteBuffer.remaining() >= STRUCT_SIZE;
+    }
+
+    @Override
+    public String toString() {
+        final String familyStr = NetlinkConstants.stringForAddressFamily(nfgen_family);
+
+        return "NfGenMsg{ "
+                + "nfgen_family{" + familyStr + "}, "
+                + "version{" + Byte.toUnsignedInt(version) + "}, "
+                + "res_id{" + Short.toUnsignedInt(res_id) + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
new file mode 100644
index 0000000..43e8312
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import static java.nio.ByteOrder.nativeOrder;
+
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/**
+ * struct nlattr
+ *
+ * see: &lt;linux_src&gt;/include/uapi/linux/netlink.h
+ *
+ * @hide
+ */
+public class StructNlAttr {
+    // Already aligned.
+    public static final int NLA_HEADERLEN  = 4;
+    public static final int NLA_F_NESTED   = (1 << 15);
+
+    /**
+     * Set carries nested attributes bit.
+     */
+    public static short makeNestedType(short type) {
+        return (short) (type | NLA_F_NESTED);
+    }
+
+    /**
+     * Peek and parse the netlink attribute from {@link ByteBuffer}.
+     *
+     * Return a (length, type) object only, without consuming any bytes in
+     * |byteBuffer| and without copying or interpreting any value bytes.
+     * This is used for scanning over a packed set of struct nlattr's,
+     * looking for instances of a particular type.
+     */
+    public static StructNlAttr peek(ByteBuffer byteBuffer) {
+        if (byteBuffer == null || byteBuffer.remaining() < NLA_HEADERLEN) {
+            return null;
+        }
+        final int baseOffset = byteBuffer.position();
+
+        final StructNlAttr struct = new StructNlAttr();
+        final ByteOrder originalOrder = byteBuffer.order();
+        byteBuffer.order(ByteOrder.nativeOrder());
+        try {
+            struct.nla_len = byteBuffer.getShort();
+            struct.nla_type = byteBuffer.getShort();
+        } finally {
+            byteBuffer.order(originalOrder);
+        }
+
+        byteBuffer.position(baseOffset);
+        if (struct.nla_len < NLA_HEADERLEN) {
+            // Malformed.
+            return null;
+        }
+        return struct;
+    }
+
+    /**
+     * Parse a netlink attribute from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the netlink attriute.
+     * @return the parsed netlink attribute, or {@code null} if the netlink attribute
+     *         could not be parsed successfully (for example, if it was truncated).
+     */
+    public static StructNlAttr parse(ByteBuffer byteBuffer) {
+        final StructNlAttr struct = peek(byteBuffer);
+        if (struct == null || byteBuffer.remaining() < struct.getAlignedLength()) {
+            return null;
+        }
+
+        final int baseOffset = byteBuffer.position();
+        byteBuffer.position(baseOffset + NLA_HEADERLEN);
+
+        int valueLen = ((int) struct.nla_len) & 0xffff;
+        valueLen -= NLA_HEADERLEN;
+        if (valueLen > 0) {
+            struct.nla_value = new byte[valueLen];
+            byteBuffer.get(struct.nla_value, 0, valueLen);
+            byteBuffer.position(baseOffset + struct.getAlignedLength());
+        }
+        return struct;
+    }
+
+    /**
+     * Find next netlink attribute with a given type from {@link ByteBuffer}.
+     *
+     * @param attrType The given netlink attribute type is requested for.
+     * @param byteBuffer The buffer from which to find the netlink attribute.
+     * @return the found netlink attribute, or {@code null} if the netlink attribute could not be
+     *         found or parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructNlAttr findNextAttrOfType(short attrType,
+            @Nullable ByteBuffer byteBuffer) {
+        while (byteBuffer != null && byteBuffer.remaining() > 0) {
+            final StructNlAttr nlAttr = StructNlAttr.peek(byteBuffer);
+            if (nlAttr == null) {
+                break;
+            }
+            if (nlAttr.nla_type == attrType) {
+                return StructNlAttr.parse(byteBuffer);
+            }
+            if (byteBuffer.remaining() < nlAttr.getAlignedLength()) {
+                break;
+            }
+            byteBuffer.position(byteBuffer.position() + nlAttr.getAlignedLength());
+        }
+        return null;
+    }
+
+    public short nla_len = (short) NLA_HEADERLEN;
+    public short nla_type;
+    public byte[] nla_value;
+
+    public StructNlAttr() {}
+
+    public StructNlAttr(short type, byte value) {
+        nla_type = type;
+        setValue(new byte[1]);
+        nla_value[0] = value;
+    }
+
+    public StructNlAttr(short type, short value) {
+        this(type, value, ByteOrder.nativeOrder());
+    }
+
+    public StructNlAttr(short type, short value, ByteOrder order) {
+        nla_type = type;
+        setValue(new byte[Short.BYTES]);
+        final ByteBuffer buf = getValueAsByteBuffer();
+        // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
+        try {
+            buf.order(order);
+            buf.putShort(value);
+        } finally {
+            buf.order(nativeOrder());
+        }
+    }
+
+    public StructNlAttr(short type, int value) {
+        this(type, value, ByteOrder.nativeOrder());
+    }
+
+    public StructNlAttr(short type, int value, ByteOrder order) {
+        nla_type = type;
+        setValue(new byte[Integer.BYTES]);
+        final ByteBuffer buf = getValueAsByteBuffer();
+        // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
+        try {
+            buf.order(order);
+            buf.putInt(value);
+        } finally {
+            buf.order(nativeOrder());
+        }
+    }
+
+    public StructNlAttr(short type, long value) {
+        this(type, value, ByteOrder.nativeOrder());
+    }
+
+    public StructNlAttr(short type, long value, ByteOrder order) {
+        nla_type = type;
+        setValue(new byte[Long.BYTES]);
+        final ByteBuffer buf = getValueAsByteBuffer();
+        // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
+        try {
+            buf.order(order);
+            buf.putLong(value);
+        } finally {
+            buf.order(nativeOrder());
+        }
+    }
+
+    public StructNlAttr(short type, @NonNull final byte[] value) {
+        nla_type = type;
+        setValue(value);
+    }
+
+    public StructNlAttr(short type, @NonNull final InetAddress ip) {
+        nla_type = type;
+        setValue(ip.getAddress());
+    }
+
+    public StructNlAttr(short type, @NonNull final MacAddress mac) {
+        nla_type = type;
+        setValue(mac.toByteArray());
+    }
+
+    public StructNlAttr(short type, @NonNull final String string) {
+        nla_type = type;
+        byte[] value = null;
+        try {
+            final byte[] stringBytes = string.getBytes("UTF-8");
+            // Append '\0' at the end of interface name string bytes.
+            value = Arrays.copyOf(stringBytes, stringBytes.length + 1);
+        } catch (UnsupportedEncodingException ignored) {
+            // Do nothing.
+        } finally {
+            setValue(value);
+        }
+    }
+
+    public StructNlAttr(short type, StructNlAttr... nested) {
+        this();
+        nla_type = makeNestedType(type);
+
+        int payloadLength = 0;
+        for (StructNlAttr nla : nested) payloadLength += nla.getAlignedLength();
+        setValue(new byte[payloadLength]);
+
+        final ByteBuffer buf = getValueAsByteBuffer();
+        for (StructNlAttr nla : nested) {
+            nla.pack(buf);
+        }
+    }
+
+    /**
+     * Get aligned attribute length.
+     */
+    public int getAlignedLength() {
+        return NetlinkConstants.alignedLengthOf(nla_len);
+    }
+
+    /**
+     * Get attribute value as BE16.
+     */
+    public short getValueAsBe16(short defaultValue) {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Short.BYTES) {
+            return defaultValue;
+        }
+        final ByteOrder originalOrder = byteBuffer.order();
+        try {
+            byteBuffer.order(ByteOrder.BIG_ENDIAN);
+            return byteBuffer.getShort();
+        } finally {
+            byteBuffer.order(originalOrder);
+        }
+    }
+
+    /**
+     * Get attribute value as BE32.
+     */
+    public int getValueAsBe32(int defaultValue) {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Integer.BYTES) {
+            return defaultValue;
+        }
+        final ByteOrder originalOrder = byteBuffer.order();
+        try {
+            byteBuffer.order(ByteOrder.BIG_ENDIAN);
+            return byteBuffer.getInt();
+        } finally {
+            byteBuffer.order(originalOrder);
+        }
+    }
+
+    /**
+     * Get attribute value as ByteBuffer.
+     */
+    public ByteBuffer getValueAsByteBuffer() {
+        if (nla_value == null) return null;
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(nla_value);
+        // By convention, all buffers in this library are in native byte order because netlink is in
+        // native byte order. It's the order that is used by NetlinkSocket.recvMessage and the only
+        // order accepted by NetlinkMessage.parse.
+        byteBuffer.order(ByteOrder.nativeOrder());
+        return byteBuffer;
+    }
+
+    /**
+     * Get attribute value as byte.
+     */
+    public byte getValueAsByte(byte defaultValue) {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Byte.BYTES) {
+            return defaultValue;
+        }
+        return getValueAsByteBuffer().get();
+    }
+
+    /**
+     * Get attribute value as Integer, or null if malformed (e.g., length is not 4 bytes).
+     * The attribute value is assumed to be in native byte order.
+     */
+    public Integer getValueAsInteger() {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Integer.BYTES) {
+            return null;
+        }
+        return byteBuffer.getInt();
+    }
+
+    /**
+     * Get attribute value as Long, or null if malformed (e.g., length is not 8 bytes).
+     * The attribute value is assumed to be in native byte order.
+     */
+    public Long getValueAsLong() {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Long.BYTES) {
+            return null;
+        }
+        return byteBuffer.getLong();
+    }
+
+    /**
+     * Get attribute value as Int, default value if malformed.
+     */
+    public int getValueAsInt(int defaultValue) {
+        final Integer value = getValueAsInteger();
+        return (value != null) ? value : defaultValue;
+    }
+
+    /**
+     * Get attribute value as InetAddress.
+     *
+     * @return the InetAddress instance representation of attribute value or null if IP address
+     *         is of illegal length.
+     */
+    @Nullable
+    public InetAddress getValueAsInetAddress() {
+        if (nla_value == null) return null;
+
+        try {
+            return InetAddress.getByAddress(nla_value);
+        } catch (UnknownHostException ignored) {
+            return null;
+        }
+    }
+
+    /**
+     * Get attribute value as MacAddress.
+     *
+     * @return the MacAddress instance representation of attribute value or null if the given byte
+     *         array is not a valid representation(e.g, not all link layers have 6-byte link-layer
+     *         addresses)
+     */
+    @Nullable
+    public MacAddress getValueAsMacAddress() {
+        if (nla_value == null) return null;
+
+        try {
+            return MacAddress.fromBytes(nla_value);
+        } catch (IllegalArgumentException ignored) {
+            return null;
+        }
+    }
+
+    /**
+     * Get attribute value as a unicode string.
+     *
+     * @return a unicode string or null if UTF-8 charset is not supported.
+     */
+    @Nullable
+    public String getValueAsString() {
+        if (nla_value == null) return null;
+        // Check the attribute value length after removing string termination flag '\0'.
+        // This assumes that all netlink strings are null-terminated.
+        if (nla_value.length < (nla_len - NLA_HEADERLEN - 1)) return null;
+
+        try {
+            final byte[] array = Arrays.copyOf(nla_value, nla_len - NLA_HEADERLEN - 1);
+            return new String(array, "UTF-8");
+        } catch (UnsupportedEncodingException | NegativeArraySizeException ignored) {
+            return null;
+        }
+    }
+
+    /**
+     * Write the netlink attribute to {@link ByteBuffer}.
+     */
+    public void pack(ByteBuffer byteBuffer) {
+        final ByteOrder originalOrder = byteBuffer.order();
+        final int originalPosition = byteBuffer.position();
+
+        byteBuffer.order(ByteOrder.nativeOrder());
+        try {
+            byteBuffer.putShort(nla_len);
+            byteBuffer.putShort(nla_type);
+            if (nla_value != null) byteBuffer.put(nla_value);
+        } finally {
+            byteBuffer.order(originalOrder);
+        }
+        byteBuffer.position(originalPosition + getAlignedLength());
+    }
+
+    private void setValue(byte[] value) {
+        nla_value = value;
+        nla_len = (short) (NLA_HEADERLEN + ((nla_value != null) ? nla_value.length : 0));
+    }
+
+    @Override
+    public String toString() {
+        return "StructNlAttr{ "
+                + "nla_len{" + nla_len + "}, "
+                + "nla_type{" + nla_type + "}, "
+                + "nla_value{" + NetlinkConstants.hexify(nla_value) + "}, "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgErr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgErr.java
new file mode 100644
index 0000000..b6620f3
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgErr.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct nlmsgerr
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/netlink.h
+ *
+ * @hide
+ */
+public class StructNlMsgErr {
+    public static final int STRUCT_SIZE = Integer.BYTES + StructNlMsgHdr.STRUCT_SIZE;
+
+    private static boolean hasAvailableSpace(ByteBuffer byteBuffer) {
+        return byteBuffer != null && byteBuffer.remaining() >= STRUCT_SIZE;
+    }
+
+    /**
+     * Parse a netlink error message payload from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the netlink error message payload.
+     * @return the parsed netlink error message payload, or {@code null} if the netlink error
+     *         message payload could not be parsed successfully (for example, if it was truncated).
+     */
+    public static StructNlMsgErr parse(ByteBuffer byteBuffer) {
+        if (!hasAvailableSpace(byteBuffer)) return null;
+
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the exception
+        // of usage within unittests.
+        final StructNlMsgErr struct = new StructNlMsgErr();
+        struct.error = byteBuffer.getInt();
+        struct.msg = StructNlMsgHdr.parse(byteBuffer);
+        return struct;
+    }
+
+    public int error;
+    public StructNlMsgHdr msg;
+
+    /**
+     * Write the netlink error message payload to {@link ByteBuffer}.
+     */
+    public void pack(ByteBuffer byteBuffer) {
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the possible
+        // exception of usage within unittests.
+        byteBuffer.putInt(error);
+        if (msg != null) {
+            msg.pack(byteBuffer);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "StructNlMsgErr{ "
+                + "error{" + error + "}, "
+                + "msg{" + (msg == null ? "" : msg.toString()) + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
new file mode 100644
index 0000000..5272366
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct nlmsghdr
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/netlink.h
+ *
+ * @hide
+ */
+public class StructNlMsgHdr {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 16;
+
+    public static final short NLM_F_REQUEST = 0x0001;
+    public static final short NLM_F_MULTI   = 0x0002;
+    public static final short NLM_F_ACK     = 0x0004;
+    public static final short NLM_F_ECHO    = 0x0008;
+    // Flags for a GET request.
+    public static final short NLM_F_ROOT    = 0x0100;
+    public static final short NLM_F_MATCH   = 0x0200;
+    public static final short NLM_F_DUMP    = NLM_F_ROOT | NLM_F_MATCH;
+    // Flags for a NEW request.
+    public static final short NLM_F_REPLACE   = 0x100;
+    public static final short NLM_F_EXCL      = 0x200;
+    public static final short NLM_F_CREATE    = 0x400;
+    public static final short NLM_F_APPEND    = 0x800;
+
+    // TODO: Probably need to distinguish the flags which have the same value. For example,
+    // NLM_F_MATCH (0x200) and NLM_F_EXCL (0x200).
+    private static String stringForNlMsgFlags(short flags) {
+        final StringBuilder sb = new StringBuilder();
+        if ((flags & NLM_F_REQUEST) != 0) {
+            sb.append("NLM_F_REQUEST");
+        }
+        if ((flags & NLM_F_MULTI) != 0) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NLM_F_MULTI");
+        }
+        if ((flags & NLM_F_ACK) != 0) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NLM_F_ACK");
+        }
+        if ((flags & NLM_F_ECHO) != 0) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NLM_F_ECHO");
+        }
+        if ((flags & NLM_F_DUMP) == NLM_F_DUMP) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NLM_F_DUMP");
+        } else if ((flags & NLM_F_ROOT) != 0) { // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NLM_F_ROOT");
+        } else if ((flags & NLM_F_MATCH) != 0) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NLM_F_MATCH");
+        }
+        return sb.toString();
+    }
+
+    private static boolean hasAvailableSpace(ByteBuffer byteBuffer) {
+        return byteBuffer != null && byteBuffer.remaining() >= STRUCT_SIZE;
+    }
+
+    /**
+     * Parse netlink message header from buffer.
+     */
+    @Nullable
+    public static StructNlMsgHdr parse(@NonNull ByteBuffer byteBuffer) {
+        if (!hasAvailableSpace(byteBuffer)) return null;
+
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the exception
+        // of usage within unittests.
+        final StructNlMsgHdr struct = new StructNlMsgHdr();
+        struct.nlmsg_len = byteBuffer.getInt();
+        struct.nlmsg_type = byteBuffer.getShort();
+        struct.nlmsg_flags = byteBuffer.getShort();
+        struct.nlmsg_seq = byteBuffer.getInt();
+        struct.nlmsg_pid = byteBuffer.getInt();
+
+        if (struct.nlmsg_len < STRUCT_SIZE) {
+            // Malformed.
+            return null;
+        }
+        return struct;
+    }
+
+    public int nlmsg_len;
+    public short nlmsg_type;
+    public short nlmsg_flags;
+    public int nlmsg_seq;
+    public int nlmsg_pid;
+
+    public StructNlMsgHdr() {
+        nlmsg_len = 0;
+        nlmsg_type = 0;
+        nlmsg_flags = 0;
+        nlmsg_seq = 0;
+        nlmsg_pid = 0;
+    }
+
+    public StructNlMsgHdr(int payloadLen, short type, short flags, int seq) {
+        nlmsg_len = StructNlMsgHdr.STRUCT_SIZE + payloadLen;
+        nlmsg_type = type;
+        nlmsg_flags = flags;
+        nlmsg_seq = seq;
+        nlmsg_pid = 0;
+    }
+
+    /**
+     * Write netlink message header to ByteBuffer.
+     */
+    public void pack(ByteBuffer byteBuffer) {
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the possible
+        // exception of usage within unittests.
+        byteBuffer.putInt(nlmsg_len);
+        byteBuffer.putShort(nlmsg_type);
+        byteBuffer.putShort(nlmsg_flags);
+        byteBuffer.putInt(nlmsg_seq);
+        byteBuffer.putInt(nlmsg_pid);
+    }
+
+    @Override
+    public String toString() {
+        return toString(null /* unknown netlink family */);
+    }
+
+    /**
+     * Transform a netlink header into a string. The netlink family is required for transforming
+     * a netlink type integer into a string.
+     * @param nlFamily netlink family. Using Integer will not incur autoboxing penalties because
+     *                 family values are small, and all Integer objects between -128 and 127 are
+     *                 statically cached. See Integer.IntegerCache.
+     * @return A list of header elements.
+     */
+    @NonNull
+    public String toString(@Nullable Integer nlFamily) {
+        final String typeStr = "" + nlmsg_type
+                + "(" + (nlFamily == null
+                ? "" : NetlinkConstants.stringForNlMsgType(nlmsg_type, nlFamily))
+                + ")";
+        final String flagsStr = "" + nlmsg_flags
+                + "(" + stringForNlMsgFlags(nlmsg_flags) + ")";
+        return "StructNlMsgHdr{ "
+                + "nlmsg_len{" + nlmsg_len + "}, "
+                + "nlmsg_type{" + typeStr + "}, "
+                + "nlmsg_flags{" + flagsStr + "}, "
+                + "nlmsg_seq{" + nlmsg_seq + "}, "
+                + "nlmsg_pid{" + nlmsg_pid + "} "
+                + "}";
+    }
+}
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/netlink/StructRtMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
new file mode 100644
index 0000000..6d9318c
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
@@ -0,0 +1,97 @@
+/*
+ * 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.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+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 rtmsg
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class StructRtMsg extends Struct {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 12;
+
+    @Field(order = 0, type = Type.U8)
+    public final short family; // Address family of route.
+    @Field(order = 1, type = Type.U8)
+    public final short dstLen; // Length of destination.
+    @Field(order = 2, type = Type.U8)
+    public final short srcLen; // Length of source.
+    @Field(order = 3, type = Type.U8)
+    public final short tos;    // TOS filter.
+    @Field(order = 4, type = Type.U8)
+    public final short table;  // Routing table ID.
+    @Field(order = 5, type = Type.U8)
+    public final short protocol; // Routing protocol.
+    @Field(order = 6, type = Type.U8)
+    public final short scope;  // distance to the destination.
+    @Field(order = 7, type = Type.U8)
+    public final short type;   // route type
+    @Field(order = 8, type = Type.U32)
+    public final long flags;
+
+    @VisibleForTesting
+    public StructRtMsg(short family, short dstLen, short srcLen, short tos, short table,
+            short protocol, short scope, short type, long flags) {
+        this.family = family;
+        this.dstLen = dstLen;
+        this.srcLen = srcLen;
+        this.tos = tos;
+        this.table = table;
+        this.protocol = protocol;
+        this.scope = scope;
+        this.type = type;
+        this.flags = flags;
+    }
+
+    /**
+     * Parse a rtmsg struct from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the rtmsg struct.
+     * @return the parsed rtmsg struct, or {@code null} if the rtmsg struct could not be
+     *         parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructRtMsg parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) return null;
+
+        // The ByteOrder must already have been set to native order.
+        return Struct.parse(StructRtMsg.class, byteBuffer);
+    }
+
+    /**
+     * Write the rtmsg struct to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        this.writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructRtaCacheInfo.java b/staticlibs/device/com/android/net/module/util/netlink/StructRtaCacheInfo.java
new file mode 100644
index 0000000..fef1f9e
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructRtaCacheInfo.java
@@ -0,0 +1,92 @@
+/*
+ * 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.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+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 rta_cacheinfo
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class StructRtaCacheInfo extends Struct {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 32;
+
+    @Field(order = 0, type = Type.U32)
+    public final long clntref;
+    @Field(order = 1, type = Type.U32)
+    public final long lastuse;
+    @Field(order = 2, type = Type.S32)
+    public final int expires;
+    @Field(order = 3, type = Type.U32)
+    public final long error;
+    @Field(order = 4, type = Type.U32)
+    public final long used;
+    @Field(order = 5, type = Type.U32)
+    public final long id;
+    @Field(order = 6, type = Type.U32)
+    public final long ts;
+    @Field(order = 7, type = Type.U32)
+    public final long tsage;
+
+    StructRtaCacheInfo(long clntref, long lastuse, int expires, long error, long used, long id,
+            long ts, long tsage) {
+        this.clntref = clntref;
+        this.lastuse = lastuse;
+        this.expires = expires;
+        this.error = error;
+        this.used = used;
+        this.id = id;
+        this.ts = ts;
+        this.tsage = tsage;
+    }
+
+    /**
+     * Parse an rta_cacheinfo struct from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the rta_cacheinfo.
+     * @return the parsed rta_cacheinfo struct, or {@code null} if the rta_cacheinfo struct
+     *         could not be parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructRtaCacheInfo parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) return null;
+
+        // The ByteOrder must already have been set to native order.
+        return Struct.parse(StructRtaCacheInfo.class, byteBuffer);
+    }
+
+    /**
+     * Write a rta_cacheinfo struct to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        this.writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/OWNERS b/staticlibs/device/com/android/net/module/util/netlink/xfrm/OWNERS
new file mode 100644
index 0000000..fca70aa
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 685852
+file:platform/frameworks/base:main:/services/core/java/com/android/server/vcn/OWNERS
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmAddressT.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmAddressT.java
new file mode 100644
index 0000000..cef1f56
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmAddressT.java
@@ -0,0 +1,84 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+
+import android.system.OsConstants;
+
+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.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Struct xfrm_address_t
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * typedef union {
+ *      __be32 a4;
+ *      __be32 a6[4];
+ *      struct in6_addr in6;
+ * } xfrm_address_t;
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmAddressT extends Struct {
+    public static final int STRUCT_SIZE = 16;
+
+    @Field(order = 0, type = Type.ByteArray, arraysize = STRUCT_SIZE)
+    public final byte[] address;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmAddressT(@NonNull final byte[] address) {
+        this.address = address.clone();
+    }
+
+    // Constructor to build a new message
+    public StructXfrmAddressT(@NonNull final InetAddress inetAddress) {
+        this.address = new byte[STRUCT_SIZE];
+        final byte[] addressBytes = inetAddress.getAddress();
+        System.arraycopy(addressBytes, 0, address, 0, addressBytes.length);
+    }
+
+    /** Return the address in InetAddress */
+    public InetAddress getAddress(int family) {
+        final byte[] addressBytes;
+        if (family == OsConstants.AF_INET6) {
+            addressBytes = this.address;
+        } else if (family == OsConstants.AF_INET) {
+            addressBytes = new byte[IPV4_ADDR_LEN];
+            System.arraycopy(this.address, 0, addressBytes, 0, addressBytes.length);
+        } else {
+            throw new IllegalArgumentException("Invalid IP family " + family);
+        }
+
+        try {
+            return InetAddress.getByAddress(addressBytes);
+        } catch (UnknownHostException e) {
+            // This should never happen
+            throw new IllegalArgumentException(
+                    "Illegal length of IP address " + addressBytes.length, e);
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmId.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmId.java
new file mode 100644
index 0000000..bdcdcd8
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmId.java
@@ -0,0 +1,72 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+
+import java.net.InetAddress;
+
+/**
+ * Struct xfrm_id
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_id {
+ *      xfrm_address_t daddr;
+ *      __be32 spi;
+ *      __u8 proto;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmId extends Struct {
+    public static final int STRUCT_SIZE = 24;
+
+    @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+    public final byte[] nestedStructDAddr;
+
+    @Field(order = 1, type = Type.UBE32)
+    public final long spi;
+
+    @Field(order = 2, type = Type.U8, padding = 3)
+    public final short proto;
+
+    @Computed private final StructXfrmAddressT mDestXfrmAddressT;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmId(@NonNull final byte[] nestedStructDAddr, long spi, short proto) {
+        this.nestedStructDAddr = nestedStructDAddr.clone();
+        this.spi = spi;
+        this.proto = proto;
+
+        mDestXfrmAddressT = new StructXfrmAddressT(this.nestedStructDAddr);
+    }
+
+    // Constructor to build a new message
+    public StructXfrmId(@NonNull final InetAddress destAddress, long spi, short proto) {
+        this(new StructXfrmAddressT(destAddress).writeToBytes(), spi, proto);
+    }
+
+    /** Return the destination address */
+    public InetAddress getDestAddress(int family) {
+        return mDestXfrmAddressT.getAddress(family);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfg.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfg.java
new file mode 100644
index 0000000..12f68c8
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfg.java
@@ -0,0 +1,106 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_INF;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+
+import java.math.BigInteger;
+
+/**
+ * Struct xfrm_lifetime_cfg
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_lifetime_cfg {
+ *      __u64 soft_byte_limit;
+ *      __u64 hard_byte_limit;
+ *      __u64 soft_packet_limit;
+ *      __u64 hard_packet_limit;
+ *      __u64 soft_add_expires_seconds;
+ *      __u64 hard_add_expires_seconds;
+ *      __u64 soft_use_expires_seconds;
+ *      __u64 hard_use_expires_seconds;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmLifetimeCfg extends Struct {
+    public static final int STRUCT_SIZE = 64;
+
+    @Field(order = 0, type = Type.U64)
+    public final BigInteger softByteLimit;
+
+    @Field(order = 1, type = Type.U64)
+    public final BigInteger hardByteLimit;
+
+    @Field(order = 2, type = Type.U64)
+    public final BigInteger softPacketLimit;
+
+    @Field(order = 3, type = Type.U64)
+    public final BigInteger hardPacketLimit;
+
+    @Field(order = 4, type = Type.U64)
+    public final BigInteger softAddExpiresSeconds;
+
+    @Field(order = 5, type = Type.U64)
+    public final BigInteger hardAddExpiresSeconds;
+
+    @Field(order = 6, type = Type.U64)
+    public final BigInteger softUseExpiresSeconds;
+
+    @Field(order = 7, type = Type.U64)
+    public final BigInteger hardUseExpiresSeconds;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmLifetimeCfg(
+            @NonNull final BigInteger softByteLimit,
+            @NonNull final BigInteger hardByteLimit,
+            @NonNull final BigInteger softPacketLimit,
+            @NonNull final BigInteger hardPacketLimit,
+            @NonNull final BigInteger softAddExpiresSeconds,
+            @NonNull final BigInteger hardAddExpiresSeconds,
+            @NonNull final BigInteger softUseExpiresSeconds,
+            @NonNull final BigInteger hardUseExpiresSeconds) {
+        this.softByteLimit = softByteLimit;
+        this.hardByteLimit = hardByteLimit;
+        this.softPacketLimit = softPacketLimit;
+        this.hardPacketLimit = hardPacketLimit;
+        this.softAddExpiresSeconds = softAddExpiresSeconds;
+        this.hardAddExpiresSeconds = hardAddExpiresSeconds;
+        this.softUseExpiresSeconds = softUseExpiresSeconds;
+        this.hardUseExpiresSeconds = hardUseExpiresSeconds;
+    }
+
+    // Constructor to build a new message
+    public StructXfrmLifetimeCfg() {
+        this(
+                XFRM_INF,
+                XFRM_INF,
+                XFRM_INF,
+                XFRM_INF,
+                BigInteger.ZERO,
+                BigInteger.ZERO,
+                BigInteger.ZERO,
+                BigInteger.ZERO);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCur.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCur.java
new file mode 100644
index 0000000..6a539c7
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCur.java
@@ -0,0 +1,66 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+
+import java.math.BigInteger;
+
+/**
+ * Struct xfrm_lifetime_cur
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_lifetime_cur {
+ *      __u64 bytes;
+ *      __u64 packets;
+ *      __u64 add_time;
+ *      __u64 use_time;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmLifetimeCur extends Struct {
+    public static final int STRUCT_SIZE = 32;
+
+    @Field(order = 0, type = Type.U64)
+    public final BigInteger bytes;
+
+    @Field(order = 1, type = Type.U64)
+    public final BigInteger packets;
+
+    @Field(order = 2, type = Type.U64)
+    public final BigInteger addTime;
+
+    @Field(order = 3, type = Type.U64)
+    public final BigInteger useTime;
+
+    public StructXfrmLifetimeCur(
+            @NonNull final BigInteger bytes,
+            @NonNull final BigInteger packets,
+            @NonNull final BigInteger addTime,
+            @NonNull final BigInteger useTime) {
+        this.bytes = bytes;
+        this.packets = packets;
+        this.addTime = addTime;
+        this.useTime = useTime;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java
new file mode 100644
index 0000000..a7bdcd9
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java
@@ -0,0 +1,203 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+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;
+import java.nio.ByteOrder;
+
+/**
+ * Struct xfrm_replay_state_esn
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_replay_state_esn {
+ *      unsigned int bmp_len;
+ *      __u32 oseq;
+ *      __u32 seq;
+ *      __u32 oseq_hi;
+ *      __u32 seq_hi;
+ *      __u32 replay_window;
+ *      __u32 bmp[];
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmReplayStateEsn {
+    // include/uapi/linux/xfrm.h XFRMA_REPLAY_ESN_MAX
+    private static final int XFRMA_REPLAY_ESN_BMP_LEN_MAX = 128;
+
+    @NonNull private final StructXfrmReplayStateEsnWithoutBitmap mWithoutBitmap;
+    @NonNull private final byte[] mBitmap;
+
+    private StructXfrmReplayStateEsn(
+            @NonNull final StructXfrmReplayStateEsnWithoutBitmap withoutBitmap,
+            @NonNull final byte[] bitmap) {
+        mWithoutBitmap = withoutBitmap;
+        mBitmap = bitmap;
+        validate();
+    }
+
+    /** Constructor to build a new message */
+    public StructXfrmReplayStateEsn(
+            long bmpLen,
+            long oSeq,
+            long seq,
+            long oSeqHi,
+            long seqHi,
+            long replayWindow,
+            @NonNull final byte[] bitmap) {
+        mWithoutBitmap =
+                new StructXfrmReplayStateEsnWithoutBitmap(
+                        bmpLen, oSeq, seq, oSeqHi, seqHi, replayWindow);
+        mBitmap = bitmap.clone();
+        validate();
+    }
+
+    private void validate() {
+        if (mWithoutBitmap.mBmpLenInBytes != mBitmap.length) {
+            throw new IllegalArgumentException(
+                    "mWithoutBitmap.mBmpLenInBytes not aligned with bitmap."
+                            + " mWithoutBitmap.mBmpLenInBytes: "
+                            + mWithoutBitmap.mBmpLenInBytes
+                            + " bitmap.length "
+                            + mBitmap.length);
+        }
+    }
+
+    /** Parse IpSecStructXfrmReplayStateEsn from ByteBuffer. */
+    @Nullable
+    public static StructXfrmReplayStateEsn parse(@NonNull final ByteBuffer buf) {
+        final StructXfrmReplayStateEsnWithoutBitmap withoutBitmap =
+                Struct.parse(StructXfrmReplayStateEsnWithoutBitmap.class, buf);
+        if (withoutBitmap == null) {
+            return null;
+        }
+
+        final byte[] bitmap = new byte[withoutBitmap.mBmpLenInBytes];
+        buf.get(bitmap);
+
+        return new StructXfrmReplayStateEsn(withoutBitmap, bitmap);
+    }
+
+    /** Convert the parsed object to ByteBuffer. */
+    public void writeToByteBuffer(@NonNull final ByteBuffer buf) {
+        mWithoutBitmap.writeToByteBuffer(buf);
+        buf.put(mBitmap);
+    }
+
+    /** Return the struct size */
+    public int getStructSize() {
+        return StructXfrmReplayStateEsnWithoutBitmap.STRUCT_SIZE + mBitmap.length;
+    }
+
+    /** Return the bitmap */
+    public byte[] getBitmap() {
+        return mBitmap.clone();
+    }
+
+    /** Return the bmp_len */
+    public long getBmpLen() {
+        return mWithoutBitmap.bmpLen;
+    }
+
+    /** Return the replay_window */
+    public long getReplayWindow() {
+        return mWithoutBitmap.replayWindow;
+    }
+
+    /** Return the TX sequence number in unisgned long */
+    public long getTxSequenceNumber() {
+        return getSequenceNumber(mWithoutBitmap.oSeqHi, mWithoutBitmap.oSeq);
+    }
+
+    /** Return the RX sequence number in unisgned long */
+    public long getRxSequenceNumber() {
+        return getSequenceNumber(mWithoutBitmap.seqHi, mWithoutBitmap.seq);
+    }
+
+    @VisibleForTesting
+    static long getSequenceNumber(long hi, long low) {
+        final ByteBuffer buffer = ByteBuffer.allocate(8);
+        buffer.order(ByteOrder.BIG_ENDIAN);
+        buffer.putInt((int) hi).putInt((int) low);
+        buffer.rewind();
+
+        return buffer.getLong();
+    }
+
+    /** The xfrm_replay_state_esn struct without the bitmap */
+    // Because the size of the bitmap is decided at runtime, it cannot be included in a Struct
+    // subclass. Therefore, this nested class is defined to include all other fields supported by
+    // Struct for code reuse.
+    public static class StructXfrmReplayStateEsnWithoutBitmap extends Struct {
+        public static final int STRUCT_SIZE = 24;
+
+        @Field(order = 0, type = Type.U32)
+        public final long bmpLen; // replay bitmap length in 32-bit integers
+
+        @Field(order = 1, type = Type.U32)
+        public final long oSeq;
+
+        @Field(order = 2, type = Type.U32)
+        public final long seq;
+
+        @Field(order = 3, type = Type.U32)
+        public final long oSeqHi;
+
+        @Field(order = 4, type = Type.U32)
+        public final long seqHi;
+
+        @Field(order = 5, type = Type.U32)
+        public final long replayWindow; // replay bitmap length in bit
+
+        @Computed private final int mBmpLenInBytes; // replay bitmap length in bytes
+
+        public StructXfrmReplayStateEsnWithoutBitmap(
+                long bmpLen, long oSeq, long seq, long oSeqHi, long seqHi, long replayWindow) {
+            this.bmpLen = bmpLen;
+            this.oSeq = oSeq;
+            this.seq = seq;
+            this.oSeqHi = oSeqHi;
+            this.seqHi = seqHi;
+            this.replayWindow = replayWindow;
+
+            if (bmpLen > XFRMA_REPLAY_ESN_BMP_LEN_MAX) {
+                throw new IllegalArgumentException("Invalid bmpLen " + bmpLen);
+            }
+
+            if (bmpLen * 4 * 8 != replayWindow) {
+                throw new IllegalArgumentException(
+                        "bmpLen not aligned with replayWindow. bmpLen: "
+                                + bmpLen
+                                + " replayWindow "
+                                + replayWindow);
+            }
+
+            mBmpLenInBytes = (int) bmpLen * 4;
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmSelector.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmSelector.java
new file mode 100644
index 0000000..7bd2ca1
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmSelector.java
@@ -0,0 +1,132 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+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;
+
+/**
+ * Struct xfrm_selector
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_selector {
+ *      xfrm_address_t daddr;
+ *      xfrm_address_t saddr;
+ *      __be16 dport;
+ *      __be16 dport_mask;
+ *      __be16 sport;
+ *      __be16 sport_mask;
+ *      __u16 family;
+ *      __u8 prefixlen_d;
+ *      __u8 prefixlen_s;
+ *      __u8 proto;
+ *      int ifindex;
+ *      __kernel_uid32_t user;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmSelector extends Struct {
+    public static final int STRUCT_SIZE = 56;
+
+    @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+    public final byte[] nestedStructDAddr;
+
+    @Field(order = 1, type = Type.ByteArray, arraysize = 16)
+    public final byte[] nestedStructSAddr;
+
+    @Field(order = 2, type = Type.UBE16)
+    public final int dPort;
+
+    @Field(order = 3, type = Type.UBE16)
+    public final int dPortMask;
+
+    @Field(order = 4, type = Type.UBE16)
+    public final int sPort;
+
+    @Field(order = 5, type = Type.UBE16)
+    public final int sPortMask;
+
+    @Field(order = 6, type = Type.U16)
+    public final int selectorFamily;
+
+    @Field(order = 7, type = Type.U8, padding = 1)
+    public final short prefixlenD;
+
+    @Field(order = 8, type = Type.U8, padding = 1)
+    public final short prefixlenS;
+
+    @Field(order = 9, type = Type.U8, padding = 1)
+    public final short proto;
+
+    @Field(order = 10, type = Type.S32)
+    public final int ifIndex;
+
+    @Field(order = 11, type = Type.S32)
+    public final int user;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmSelector(
+            @NonNull final byte[] nestedStructDAddr,
+            @NonNull final byte[] nestedStructSAddr,
+            int dPort,
+            int dPortMask,
+            int sPort,
+            int sPortMask,
+            int selectorFamily,
+            short prefixlenD,
+            short prefixlenS,
+            short proto,
+            int ifIndex,
+            int user) {
+        this.nestedStructDAddr = nestedStructDAddr.clone();
+        this.nestedStructSAddr = nestedStructSAddr.clone();
+        this.dPort = dPort;
+        this.dPortMask = dPortMask;
+        this.sPort = sPort;
+        this.sPortMask = sPortMask;
+        this.selectorFamily = selectorFamily;
+        this.prefixlenD = prefixlenD;
+        this.prefixlenS = prefixlenS;
+        this.proto = proto;
+        this.ifIndex = ifIndex;
+        this.user = user;
+    }
+
+    // Constructor to build a new message
+    public StructXfrmSelector(int selectorFamily) {
+        this(
+                new byte[StructXfrmAddressT.STRUCT_SIZE],
+                new byte[StructXfrmAddressT.STRUCT_SIZE],
+                0 /* dPort */,
+                0 /* dPortMask */,
+                0 /* sPort */,
+                0 /* sPortMask */,
+                selectorFamily,
+                (short) 0 /* prefixlenD */,
+                (short) 0 /* prefixlenS */,
+                (short) 0 /* proto */,
+                0 /* ifIndex */,
+                0 /* user */);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmStats.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmStats.java
new file mode 100644
index 0000000..be13293
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmStats.java
@@ -0,0 +1,61 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import com.android.net.module.util.Struct;
+
+/**
+ * Struct xfrm_lifetime_cur
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_stats {
+ *      __u32 replay_window;
+ *      __u32 replay;
+ *      __u32 integrity_failed;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmStats extends Struct {
+    public static final int STRUCT_SIZE = 12;
+
+    /** Number of packets that fall out of the replay window */
+    @Field(order = 0, type = Type.U32)
+    public final long replayWindow;
+
+    /** Number of replayed packets */
+    @Field(order = 1, type = Type.U32)
+    public final long replay;
+
+    /** Number of packets that failed authentication */
+    @Field(order = 2, type = Type.U32)
+    public final long integrityFailed;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmStats(long replayWindow, long replay, long integrityFailed) {
+        this.replayWindow = replayWindow;
+        this.replay = replay;
+        this.integrityFailed = integrityFailed;
+    }
+
+    // Constructor to build a new message
+    public StructXfrmStats() {
+        this(0, 0, 0);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaId.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaId.java
new file mode 100644
index 0000000..5ebc69c
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaId.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+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.net.InetAddress;
+
+/**
+ * Struct xfrm_usersa_id
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_usersa_id {
+ *      xfrm_address_t      daddr;
+ *      __be32              spi;
+ *      __u16               family;
+ *      __u8                proto;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmUsersaId extends Struct {
+    public static final int STRUCT_SIZE = 24;
+
+    @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+    public final byte[] nestedStructDAddr; // xfrm_address_t
+
+    @Field(order = 1, type = Type.UBE32)
+    public final long spi;
+
+    @Field(order = 2, type = Type.U16)
+    public final int family;
+
+    @Field(order = 3, type = Type.U8, padding = 1)
+    public final short proto;
+
+    @Computed private final StructXfrmAddressT mDestXfrmAddressT;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmUsersaId(
+            @NonNull final byte[] nestedStructDAddr, long spi, int family, short proto) {
+        this.nestedStructDAddr = nestedStructDAddr.clone();
+        this.spi = spi;
+        this.family = family;
+        this.proto = proto;
+
+        mDestXfrmAddressT = new StructXfrmAddressT(this.nestedStructDAddr);
+    }
+
+    // Constructor to build a new message
+    public StructXfrmUsersaId(
+            @NonNull final InetAddress destAddress, long spi, int family, short proto) {
+        this(new StructXfrmAddressT(destAddress).writeToBytes(), spi, family, proto);
+    }
+
+    /** Return the destination address */
+    public InetAddress getDestAddress() {
+        return mDestXfrmAddressT.getAddress(family);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfo.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfo.java
new file mode 100644
index 0000000..740a801
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfo.java
@@ -0,0 +1,215 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+
+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.math.BigInteger;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Struct xfrm_usersa_info
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_usersa_info {
+ *      struct xfrm_selector sel;
+ *      struct xfrm_id id;
+ *      xfrm_address_t saddr;
+ *      struct xfrm_lifetime_cfg lft;
+ *      struct xfrm_lifetime_cur curlft;
+ *      struct xfrm_stats stats;
+ *      __u32 seq;
+ *      __u32 reqid;
+ *      __u16 family;
+ *      __u8 mode;
+ *      __u8 replay_window;
+ *      __u8 flags;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmUsersaInfo extends Struct {
+    private static final int NESTED_STRUCTS_SIZE =
+            StructXfrmSelector.STRUCT_SIZE
+                    + StructXfrmId.STRUCT_SIZE
+                    + StructXfrmAddressT.STRUCT_SIZE
+                    + StructXfrmLifetimeCfg.STRUCT_SIZE
+                    + StructXfrmLifetimeCur.STRUCT_SIZE
+                    + StructXfrmStats.STRUCT_SIZE;
+
+    public static final int STRUCT_SIZE = NESTED_STRUCTS_SIZE + 20;
+
+    @Computed private final StructXfrmSelector mXfrmSelector;
+    @Computed private final StructXfrmId mXfrmId;
+    @Computed private final StructXfrmAddressT mSourceXfrmAddressT;
+    @Computed private final StructXfrmLifetimeCfg mXfrmLifetime;
+    @Computed private final StructXfrmLifetimeCur mXfrmCurrentLifetime;
+    @Computed private final StructXfrmStats mXfrmStats;
+
+    @Field(order = 0, type = Type.ByteArray, arraysize = NESTED_STRUCTS_SIZE)
+    public final byte[] nestedStructs;
+
+    @Field(order = 1, type = Type.U32)
+    public final long seq;
+
+    @Field(order = 2, type = Type.U32)
+    public final long reqId;
+
+    @Field(order = 3, type = Type.U16)
+    public final int family;
+
+    @Field(order = 4, type = Type.U8)
+    public final short mode;
+
+    @Field(order = 5, type = Type.U8)
+    public final short replayWindowLegacy;
+
+    @Field(order = 6, type = Type.U8, padding = 7)
+    public final short flags;
+
+    // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+    public StructXfrmUsersaInfo(
+            @NonNull final byte[] nestedStructs,
+            long seq,
+            long reqId,
+            int family,
+            short mode,
+            short replayWindowLegacy,
+            short flags) {
+        this.nestedStructs = nestedStructs.clone();
+        this.seq = seq;
+        this.reqId = reqId;
+        this.family = family;
+        this.mode = mode;
+        this.replayWindowLegacy = replayWindowLegacy;
+        this.flags = flags;
+
+        final ByteBuffer nestedStructsBuff = ByteBuffer.wrap(nestedStructs);
+        nestedStructsBuff.order(ByteOrder.nativeOrder());
+
+        // The parsing order matters
+        mXfrmSelector = Struct.parse(StructXfrmSelector.class, nestedStructsBuff);
+        mXfrmId = Struct.parse(StructXfrmId.class, nestedStructsBuff);
+        mSourceXfrmAddressT = Struct.parse(StructXfrmAddressT.class, nestedStructsBuff);
+        mXfrmLifetime = Struct.parse(StructXfrmLifetimeCfg.class, nestedStructsBuff);
+        mXfrmCurrentLifetime = Struct.parse(StructXfrmLifetimeCur.class, nestedStructsBuff);
+        mXfrmStats = Struct.parse(StructXfrmStats.class, nestedStructsBuff);
+    }
+
+    // Constructor to build a new message for TESTING
+    StructXfrmUsersaInfo(
+            @NonNull final InetAddress dAddr,
+            @NonNull final InetAddress sAddr,
+            @NonNull final BigInteger addTime,
+            int selectorFamily,
+            long spi,
+            long seq,
+            long reqId,
+            short proto,
+            short mode,
+            short replayWindowLegacy,
+            short flags) {
+        this.seq = seq;
+        this.reqId = reqId;
+        this.family = dAddr instanceof Inet4Address ? OsConstants.AF_INET : OsConstants.AF_INET6;
+        this.mode = mode;
+        this.replayWindowLegacy = replayWindowLegacy;
+        this.flags = flags;
+
+        mXfrmSelector = new StructXfrmSelector(selectorFamily);
+        mXfrmId = new StructXfrmId(dAddr, spi, proto);
+        mSourceXfrmAddressT = new StructXfrmAddressT(sAddr);
+        mXfrmLifetime = new StructXfrmLifetimeCfg();
+        mXfrmCurrentLifetime =
+                new StructXfrmLifetimeCur(
+                        BigInteger.ZERO, BigInteger.ZERO, addTime, BigInteger.ZERO);
+        mXfrmStats = new StructXfrmStats();
+
+        final ByteBuffer nestedStructsBuff = ByteBuffer.allocate(NESTED_STRUCTS_SIZE);
+        nestedStructsBuff.order(ByteOrder.nativeOrder());
+
+        mXfrmSelector.writeToByteBuffer(nestedStructsBuff);
+        mXfrmId.writeToByteBuffer(nestedStructsBuff);
+        mSourceXfrmAddressT.writeToByteBuffer(nestedStructsBuff);
+        mXfrmLifetime.writeToByteBuffer(nestedStructsBuff);
+        mXfrmCurrentLifetime.writeToByteBuffer(nestedStructsBuff);
+        mXfrmStats.writeToByteBuffer(nestedStructsBuff);
+
+        this.nestedStructs = nestedStructsBuff.array();
+    }
+
+    // Constructor to build a new message
+    public StructXfrmUsersaInfo(
+            @NonNull final InetAddress dAddr,
+            @NonNull final InetAddress sAddr,
+            long spi,
+            long seq,
+            long reqId,
+            short proto,
+            short mode,
+            short replayWindowLegacy,
+            short flags) {
+        // Use AF_UNSPEC for all SAs selectors. In transport mode, kernel picks selector family
+        // based on usersa->family, while in tunnel mode, the XFRM_STATE_AF_UNSPEC flag allows
+        // dual-stack SAs.
+        this(
+                dAddr,
+                sAddr,
+                BigInteger.ZERO,
+                OsConstants.AF_UNSPEC,
+                spi,
+                seq,
+                reqId,
+                proto,
+                mode,
+                replayWindowLegacy,
+                flags);
+    }
+
+    /** Return the destination address */
+    public InetAddress getDestAddress() {
+        return mXfrmId.getDestAddress(family);
+    }
+
+    /** Return the source address */
+    public InetAddress getSrcAddress() {
+        return mSourceXfrmAddressT.getAddress(family);
+    }
+
+    /** Return the SPI */
+    public long getSpi() {
+        return mXfrmId.spi;
+    }
+
+    /** Return the current lifetime */
+    public StructXfrmLifetimeCur getCurrentLifetime() {
+        return mXfrmCurrentLifetime;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessage.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessage.java
new file mode 100644
index 0000000..680a7ca
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessage.java
@@ -0,0 +1,116 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MSG_GETSA;
+
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.netlink.StructNlMsgHdr;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+/**
+ * An XfrmNetlinkMessage subclass for XFRM_MSG_GETSA messages.
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <p>XFRM_MSG_GETSA syntax
+ *
+ * <ul>
+ *   <li>TLV: xfrm_usersa_id
+ *   <li>Optional Attributes: XFRMA_MARK, XFRMA_SRCADDR
+ * </ul>
+ *
+ * @hide
+ */
+public class XfrmNetlinkGetSaMessage extends XfrmNetlinkMessage {
+    @NonNull private final StructXfrmUsersaId mXfrmUsersaId;
+
+    private XfrmNetlinkGetSaMessage(
+            @NonNull final StructNlMsgHdr header, @NonNull final StructXfrmUsersaId xfrmUsersaId) {
+        super(header);
+        mXfrmUsersaId = xfrmUsersaId;
+    }
+
+    private XfrmNetlinkGetSaMessage(
+            @NonNull final StructNlMsgHdr header,
+            @NonNull final InetAddress destAddress,
+            long spi,
+            short proto) {
+        super(header);
+
+        final int family =
+                destAddress instanceof Inet4Address ? OsConstants.AF_INET : OsConstants.AF_INET6;
+        mXfrmUsersaId = new StructXfrmUsersaId(destAddress, spi, family, proto);
+    }
+
+    @Override
+    protected void packPayload(@NonNull final ByteBuffer byteBuffer) {
+        mXfrmUsersaId.writeToByteBuffer(byteBuffer);
+    }
+
+    /**
+     * Parse XFRM_MSG_GETSA message from ByteBuffer.
+     *
+     * <p>This method should be called from NetlinkMessage#parse(ByteBuffer, int) for generic
+     * message validation and processing
+     *
+     * @param nlmsghdr netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes. MUST be
+     *                   host order
+     */
+    @Nullable
+    static XfrmNetlinkGetSaMessage parseInternal(
+            @NonNull final StructNlMsgHdr nlmsghdr, @NonNull final ByteBuffer byteBuffer) {
+        final StructXfrmUsersaId xfrmUsersaId = Struct.parse(StructXfrmUsersaId.class, byteBuffer);
+        if (xfrmUsersaId == null) {
+            return null;
+        }
+
+        // Attributes not supported. Don't bother handling them.
+
+        return new XfrmNetlinkGetSaMessage(nlmsghdr, xfrmUsersaId);
+    }
+
+    /** A convenient method to create a XFRM_MSG_GETSA message. */
+    public static byte[] newXfrmNetlinkGetSaMessage(
+            @NonNull final InetAddress destAddress, long spi, short proto) {
+        final int payloadLen = StructXfrmUsersaId.STRUCT_SIZE;
+
+        final StructNlMsgHdr nlmsghdr =
+                new StructNlMsgHdr(payloadLen, XFRM_MSG_GETSA, NLM_F_REQUEST, 0);
+        final XfrmNetlinkGetSaMessage message =
+                new XfrmNetlinkGetSaMessage(nlmsghdr, destAddress, spi, proto);
+
+        final ByteBuffer byteBuffer = newNlMsgByteBuffer(payloadLen);
+        message.pack(byteBuffer);
+
+        return byteBuffer.array();
+    }
+
+    public StructXfrmUsersaId getStructXfrmUsersaId() {
+        return mXfrmUsersaId;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkMessage.java
new file mode 100644
index 0000000..72d02d4
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkMessage.java
@@ -0,0 +1,86 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.StructNlMsgHdr;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/** Base calss for XFRM netlink messages */
+// Developer notes: The Linux kernel includes a number of XFRM structs that are not standard netlink
+// attributes (e.g., xfrm_usersa_id). These structs are unlikely to change size, so this XFRM
+// netlink message implementation assumes their sizes will remain stable. If any non-attribute
+// struct size changes, it should be caught by CTS and then developers should add
+// kernel-version-based behvaiours.
+public abstract class XfrmNetlinkMessage extends NetlinkMessage {
+    // TODO: b/312498032 Remove it when OsConstants.IPPROTO_ESP is stable
+    public static final int IPPROTO_ESP = 50;
+    // TODO: b/312498032 Remove it when OsConstants.NETLINK_XFRM is stable
+    public static final int NETLINK_XFRM = 6;
+
+    /* see include/uapi/linux/xfrm.h */
+    public static final short XFRM_MSG_NEWSA = 16;
+    public static final short XFRM_MSG_GETSA = 18;
+
+    public static final int XFRM_MODE_TRANSPORT = 0;
+    public static final int XFRM_MODE_TUNNEL = 1;
+
+    public static final short XFRMA_REPLAY_VAL = 10;
+    public static final short XFRMA_REPLAY_ESN_VAL = 23;
+
+    public static final BigInteger XFRM_INF = new BigInteger("FFFFFFFFFFFFFFFF", 16);
+
+    public XfrmNetlinkMessage(@NonNull final StructNlMsgHdr header) {
+        super(header);
+    }
+
+    /**
+     * Parse XFRM message from ByteBuffer.
+     *
+     * <p>This method should be called from NetlinkMessage#parse(ByteBuffer, int) for generic
+     * message validation and processing
+     *
+     * @param nlmsghdr netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes. MUST be
+     *                   host order
+     */
+    @Nullable
+    public static XfrmNetlinkMessage parseXfrmInternal(
+            @NonNull final StructNlMsgHdr nlmsghdr, @NonNull final ByteBuffer byteBuffer) {
+        switch (nlmsghdr.nlmsg_type) {
+            case XFRM_MSG_NEWSA:
+                return XfrmNetlinkNewSaMessage.parseInternal(nlmsghdr, byteBuffer);
+            case XFRM_MSG_GETSA:
+                return XfrmNetlinkGetSaMessage.parseInternal(nlmsghdr, byteBuffer);
+            default:
+                return null;
+        }
+    }
+
+    protected abstract void packPayload(@NonNull final ByteBuffer byteBuffer);
+
+    /** Write a XFRM message to {@link ByteBuffer}. */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer);
+        packPayload(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessage.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessage.java
new file mode 100644
index 0000000..2f374ba
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessage.java
@@ -0,0 +1,157 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRMA_REPLAY_ESN_VAL;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.netlink.StructNlAttr;
+import com.android.net.module.util.netlink.StructNlMsgHdr;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A NetlinkMessage subclass for XFRM_MSG_NEWSA messages.
+ *
+ * <p>see also: &lt;linux_src&gt;/include/uapi/linux/xfrm.h
+ *
+ * <p>XFRM_MSG_NEWSA syntax
+ *
+ * <ul>
+ *   <li>TLV: xfrm_usersa_info
+ *   <li>Attributes: XFRMA_ALG_CRYPT, XFRMA_ALG_AUTH, XFRMA_OUTPUT_MARK, XFRMA_IF_ID,
+ *       XFRMA_REPLAY_ESN_VAL,XFRMA_REPLAY_VAL
+ * </ul>
+ *
+ * @hide
+ */
+public class XfrmNetlinkNewSaMessage extends XfrmNetlinkMessage {
+    private static final String TAG = XfrmNetlinkNewSaMessage.class.getSimpleName();
+    @NonNull private final StructXfrmUsersaInfo mXfrmUsersaInfo;
+
+    @NonNull private final StructXfrmReplayStateEsn mXfrmReplayStateEsn;
+
+    private XfrmNetlinkNewSaMessage(
+            @NonNull final StructNlMsgHdr header,
+            @NonNull final StructXfrmUsersaInfo xfrmUsersaInfo,
+            @NonNull final StructXfrmReplayStateEsn xfrmReplayStateEsn) {
+        super(header);
+        mXfrmUsersaInfo = xfrmUsersaInfo;
+        mXfrmReplayStateEsn = xfrmReplayStateEsn;
+    }
+
+    @Override
+    protected void packPayload(@NonNull final ByteBuffer byteBuffer) {
+        mXfrmUsersaInfo.writeToByteBuffer(byteBuffer);
+        if (mXfrmReplayStateEsn != null) {
+            mXfrmReplayStateEsn.writeToByteBuffer(byteBuffer);
+        }
+    }
+
+    /**
+     * Parse XFRM_MSG_NEWSA message from ByteBuffer.
+     *
+     * <p>This method should be called from NetlinkMessage#parse(ByteBuffer, int) for generic
+     * message validation and processing
+     *
+     * @param nlmsghdr netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes. MUST be
+     *                   host order
+     */
+    @Nullable
+    static XfrmNetlinkNewSaMessage parseInternal(
+            @NonNull final StructNlMsgHdr nlmsghdr, @NonNull final ByteBuffer byteBuffer) {
+        final StructXfrmUsersaInfo xfrmUsersaInfo =
+                Struct.parse(StructXfrmUsersaInfo.class, byteBuffer);
+        if (xfrmUsersaInfo == null) {
+            Log.d(TAG, "parse: fail to parse xfrmUsersaInfo");
+            return null;
+        }
+
+        StructXfrmReplayStateEsn xfrmReplayStateEsn = null;
+
+        final int payloadLen = nlmsghdr.nlmsg_len - StructNlMsgHdr.STRUCT_SIZE;
+        int parsedLength = StructXfrmUsersaInfo.STRUCT_SIZE;
+        while (parsedLength < payloadLen) {
+            final StructNlAttr attr = StructNlAttr.parse(byteBuffer);
+
+            if (attr == null) {
+                Log.d(TAG, "parse: fail to parse netlink attributes");
+                return null;
+            }
+
+            final ByteBuffer attrValueBuff = ByteBuffer.wrap(attr.nla_value);
+            attrValueBuff.order(ByteOrder.nativeOrder());
+
+            if (attr.nla_type == XFRMA_REPLAY_ESN_VAL) {
+                xfrmReplayStateEsn = StructXfrmReplayStateEsn.parse(attrValueBuff);
+            }
+
+            parsedLength += attr.nla_len;
+        }
+
+        // TODO: Add the support of XFRMA_REPLAY_VAL
+
+        if (xfrmReplayStateEsn == null) {
+            Log.d(TAG, "parse: xfrmReplayStateEsn not found");
+            return null;
+        }
+
+        final XfrmNetlinkNewSaMessage msg =
+                new XfrmNetlinkNewSaMessage(nlmsghdr, xfrmUsersaInfo, xfrmReplayStateEsn);
+
+        return msg;
+    }
+
+    /** Return the TX sequence number in unisgned long */
+    public long getTxSequenceNumber() {
+        return mXfrmReplayStateEsn.getTxSequenceNumber();
+    }
+
+    /** Return the RX sequence number in unisgned long */
+    public long getRxSequenceNumber() {
+        return mXfrmReplayStateEsn.getRxSequenceNumber();
+    }
+
+    /** Return the bitmap */
+    public byte[] getBitmap() {
+        return mXfrmReplayStateEsn.getBitmap();
+    }
+
+    /** Return the packet count in unsigned long */
+    public long getPacketCount() {
+        // It is safe because "packets" is a 64-bit value
+        return mXfrmUsersaInfo.getCurrentLifetime().packets.longValue();
+    }
+
+    /** Return the byte count in unsigned long */
+    public long getByteCount() {
+        // It is safe because "bytes" is a 64-bit value
+        return mXfrmUsersaInfo.getCurrentLifetime().bytes.longValue();
+    }
+
+    /** Return the xfrm_usersa_info */
+    public StructXfrmUsersaInfo getXfrmUsersaInfo() {
+        return mXfrmUsersaInfo;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/EthernetHeader.java b/staticlibs/device/com/android/net/module/util/structs/EthernetHeader.java
new file mode 100644
index 0000000..92ef8a7
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/EthernetHeader.java
@@ -0,0 +1,60 @@
+/*
+ * 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.net.module.util.structs;
+
+import android.net.MacAddress;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * L2 ethernet header as per IEEE 802.3. Does not include a 802.1Q tag.
+ *
+ * 0                   1
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |          Destination          |
+ * +-                             -+
+ * |            Ethernet           |
+ * +-                             -+
+ * |            Address            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |             Source            |
+ * +-                             -+
+ * |            Ethernet           |
+ * +-                             -+
+ * |            Address            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |            EtherType          |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class EthernetHeader extends Struct {
+    @Field(order = 0, type = Type.EUI48)
+    public final MacAddress dstMac;
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress srcMac;
+    @Field(order = 2, type = Type.U16)
+    public final int etherType;
+
+    public EthernetHeader(final MacAddress dstMac, final MacAddress srcMac,
+            final int etherType) {
+        this.dstMac = dstMac;
+        this.srcMac = srcMac;
+        this.etherType = etherType;
+    }
+}
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/IaAddressOption.java b/staticlibs/device/com/android/net/module/util/structs/IaAddressOption.java
new file mode 100644
index 0000000..575a1f3
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/IaAddressOption.java
@@ -0,0 +1,84 @@
+/*
+ * 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.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IA_ADDR;
+
+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.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * DHCPv6 IA Address option.
+ * https://tools.ietf.org/html/rfc8415. This does not contain any option.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |          OPTION_IAADDR        |          option-len           |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * |                         IPv6-address                          |
+ * |                                                               |
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                      preferred-lifetime                       |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                        valid-lifetime                         |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * .                                                               .
+ * .                        IAaddr-options                         .
+ * .                                                               .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class IaAddressOption extends Struct {
+    public static final int LENGTH = 24; // option length excluding IAaddr-options
+
+    @Field(order = 0, type = Type.S16)
+    public final short code;
+    @Field(order = 1, type = Type.S16)
+    public final short length;
+    @Field(order = 2, type = Type.Ipv6Address)
+    public final Inet6Address address;
+    @Field(order = 3, type = Type.U32)
+    public final long preferred;
+    @Field(order = 4, type = Type.U32)
+    public final long valid;
+
+    IaAddressOption(final short code, final short length, final Inet6Address address,
+            final long preferred, final long valid) {
+        this.code = code;
+        this.length = length;
+        this.address = address;
+        this.preferred = preferred;
+        this.valid = valid;
+    }
+
+    /**
+     * Build an IA Address option from the required specific parameters.
+     */
+    public static ByteBuffer build(final short length, final long id, final Inet6Address address,
+            final long preferred, final long valid) {
+        final IaAddressOption option = new IaAddressOption((short) DHCP6_OPTION_IA_ADDR,
+                length /* 24 + IAaddr-options length */, address, preferred, valid);
+        return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPdOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPdOption.java
new file mode 100644
index 0000000..dbf79dc
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPdOption.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IA_PD;
+
+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;
+import java.nio.ByteOrder;
+
+/**
+ * DHCPv6 IA_PD option.
+ * https://tools.ietf.org/html/rfc8415. This does not contain any option.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |         OPTION_IA_PD          |           option-len          |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                         IAID (4 octets)                       |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                              T1                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                              T2                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * .                                                               .
+ * .                          IA_PD-options                        .
+ * .                                                               .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ */
+public class IaPdOption extends Struct {
+    public static final int LENGTH = 12; // option length excluding IA_PD options
+
+    @Field(order = 0, type = Type.S16)
+    public final short code;
+    @Field(order = 1, type = Type.S16)
+    public final short length;
+    @Field(order = 2, type = Type.U32)
+    public final long id;
+    @Field(order = 3, type = Type.U32)
+    public final long t1;
+    @Field(order = 4, type = Type.U32)
+    public final long t2;
+
+    IaPdOption(final short code, final short length, final long id, final long t1,
+            final long t2) {
+        this.code = code;
+        this.length = length;
+        this.id = id;
+        this.t1 = t1;
+        this.t2 = t2;
+    }
+
+    /**
+     * Build an IA_PD option from the required specific parameters.
+     */
+    public static ByteBuffer build(final short length, final long id, final long t1,
+            final long t2) {
+        final IaPdOption option = new IaPdOption((short) DHCP6_OPTION_IA_PD,
+                length /* 12 + IA_PD options length */, id, t1, t2);
+        return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
new file mode 100644
index 0000000..b0f19e2
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
@@ -0,0 +1,158 @@
+/*
+ * 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.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IAPREFIX;
+
+import android.net.IpPrefix;
+import android.util.Log;
+
+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;
+
+/**
+ * DHCPv6 IA Prefix Option.
+ * https://tools.ietf.org/html/rfc8415. This does not contain any option.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |        OPTION_IAPREFIX        |           option-len          |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                      preferred-lifetime                       |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                        valid-lifetime                         |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | prefix-length |                                               |
+ * +-+-+-+-+-+-+-+-+          IPv6-prefix                          |
+ * |                           (16 octets)                         |
+ * |                                                               |
+ * |                                                               |
+ * |                                                               |
+ * |               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |               |                                               .
+ * +-+-+-+-+-+-+-+-+                                               .
+ * .                       IAprefix-options                        .
+ * .                                                               .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class IaPrefixOption extends Struct {
+    private static final String TAG = IaPrefixOption.class.getSimpleName();
+    public static final int LENGTH = 25; // option length excluding IAprefix-options
+
+    @Field(order = 0, type = Type.S16)
+    public final short code;
+    @Field(order = 1, type = Type.S16)
+    public final short length;
+    @Field(order = 2, type = Type.U32)
+    public final long preferred;
+    @Field(order = 3, type = Type.U32)
+    public final long valid;
+    @Field(order = 4, type = Type.S8)
+    public final byte prefixLen;
+    @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+    public final byte[] prefix;
+
+    @Computed
+    private final IpPrefix mIpPrefix;
+
+    // Constructor used by Struct.parse()
+    protected IaPrefixOption(final short code, final short length, final long preferred,
+            final long valid, final byte prefixLen, final byte[] prefix) {
+        this.code = code;
+        this.length = length;
+        this.preferred = preferred;
+        this.valid = valid;
+        this.prefixLen = prefixLen;
+        this.prefix = prefix.clone();
+
+        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);
+        }
+    }
+
+    public IaPrefixOption(final short length, final long preferred, final long valid,
+            final byte prefixLen, final byte[] prefix) {
+        this((byte) DHCP6_OPTION_IAPREFIX, length, preferred, valid, prefixLen, prefix);
+    }
+
+    /**
+     * Check whether or not IA Prefix option in IA_PD option is valid per RFC8415#section-21.22.
+     *
+     * Note: an expired prefix can still be valid.
+     */
+    public boolean isValid() {
+        if (preferred < 0) {
+            Log.w(TAG, "Invalid preferred lifetime: " + this);
+            return false;
+        }
+        if (valid < 0) {
+            Log.w(TAG, "Invalid valid lifetime: " + this);
+            return false;
+        }
+        if (preferred > valid) {
+            Log.w(TAG, "Invalid lifetime. Preferred lifetime > valid lifetime: " + this);
+            return false;
+        }
+        if (prefixLen > 64) {
+            Log.w(TAG, "Invalid prefix length: " + this);
+            return false;
+        }
+        return true;
+    }
+
+    public IpPrefix getIpPrefix() {
+        return mIpPrefix;
+    }
+
+    /**
+     * Check whether or not IA Prefix option has 0 preferred and valid lifetimes.
+     */
+    public boolean withZeroLifetimes() {
+        return preferred == 0 && valid == 0;
+    }
+
+    /**
+     * Build an IA_PD prefix option with given specific parameters.
+     */
+    public static ByteBuffer build(final short length, final long preferred, final long valid,
+            final byte prefixLen, final byte[] prefix) {
+        final IaPrefixOption option = new IaPrefixOption(
+                length /* 25 + IAPrefix options length */, preferred, valid, prefixLen, prefix);
+        return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+
+    @Override
+    public String toString() {
+        return "IA Prefix, length " + length + ": " + mIpPrefix + ", pref " + preferred + ", valid "
+                + valid;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/Icmpv4Header.java b/staticlibs/device/com/android/net/module/util/structs/Icmpv4Header.java
new file mode 100644
index 0000000..454bb07
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/Icmpv4Header.java
@@ -0,0 +1,43 @@
+/*
+ * 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.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;
+
+/**
+ * ICMPv4 header as per https://tools.ietf.org/html/rfc792.
+ *
+ * 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             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class Icmpv4Header extends Struct {
+    @Field(order = 0, type = Type.U8)
+    public short type;
+    @Field(order = 1, type = Type.U8)
+    public short code;
+    @Field(order = 2, type = Type.S16)
+    public short checksum;
+    public Icmpv4Header(final short type, final short code, final short checksum) {
+        this.type = type;
+        this.code = code;
+        this.checksum = checksum;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/Icmpv6Header.java b/staticlibs/device/com/android/net/module/util/structs/Icmpv6Header.java
new file mode 100644
index 0000000..c82ae02
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/Icmpv6Header.java
@@ -0,0 +1,45 @@
+/*
+ * 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.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;
+
+/**
+ * ICMPv6 header as per https://tools.ietf.org/html/rfc4443.
+ *
+ * 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             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class Icmpv6Header extends Struct {
+    @Field(order = 0, type = Type.U8)
+    public short type;
+    @Field(order = 1, type = Type.U8)
+    public short code;
+    @Field(order = 2, type = Type.S16)
+    public short checksum;
+
+    public Icmpv6Header(final short type, final short code, final short checksum) {
+        this.type = type;
+        this.code = code;
+        this.checksum = checksum;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/Ipv4Header.java b/staticlibs/device/com/android/net/module/util/structs/Ipv4Header.java
new file mode 100644
index 0000000..5249454
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/Ipv4Header.java
@@ -0,0 +1,94 @@
+/*
+ * 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.net.module.util.structs;
+
+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.net.Inet4Address;
+
+/**
+ * L3 IPv4 header as per https://tools.ietf.org/html/rfc791.
+ * This class doesn't contain options field.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |Version|  IHL  |Type of Service|          Total Length         |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |         Identification        |Flags|      Fragment Offset    |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |  Time to Live |    Protocol   |         Header Checksum       |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                       Source Address                          |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                    Destination Address                        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class Ipv4Header extends Struct {
+    // IP Version=IPv4, IHL is always 5(*4bytes) because options are not supported.
+    @VisibleForTesting
+    public static final byte IPHDR_VERSION_IHL = 0x45;
+
+    @Field(order = 0, type = Type.S8)
+    // version (4 bits), IHL (4 bits)
+    public final byte vi;
+    @Field(order = 1, type = Type.S8)
+    public final byte tos;
+    @Field(order = 2, type = Type.U16)
+    public final int totalLength;
+    @Field(order = 3, type = Type.S16)
+    public final short id;
+    @Field(order = 4, type = Type.S16)
+    // flags (3 bits), fragment offset (13 bits)
+    public final short flagsAndFragmentOffset;
+    @Field(order = 5, type = Type.U8)
+    public final short ttl;
+    @Field(order = 6, type = Type.S8)
+    public final byte protocol;
+    @Field(order = 7, type = Type.S16)
+    public final short checksum;
+    @Field(order = 8, type = Type.Ipv4Address)
+    public final Inet4Address srcIp;
+    @Field(order = 9, type = Type.Ipv4Address)
+    public final Inet4Address dstIp;
+
+    public Ipv4Header(final byte tos, final int totalLength, final short id,
+            final short flagsAndFragmentOffset, final short ttl, final byte protocol,
+            final short checksum, final Inet4Address srcIp, final Inet4Address dstIp) {
+        this(IPHDR_VERSION_IHL, tos, totalLength, id, flagsAndFragmentOffset, ttl,
+                protocol, checksum, srcIp, dstIp);
+    }
+
+    private Ipv4Header(final byte vi, final byte tos, final int totalLength, final short id,
+            final short flagsAndFragmentOffset, final short ttl, final byte protocol,
+            final short checksum, final Inet4Address srcIp, final Inet4Address dstIp) {
+        this.vi = vi;
+        this.tos = tos;
+        this.totalLength = totalLength;
+        this.id = id;
+        this.flagsAndFragmentOffset = flagsAndFragmentOffset;
+        this.ttl = ttl;
+        this.protocol = protocol;
+        this.checksum = checksum;
+        this.srcIp = srcIp;
+        this.dstIp = dstIp;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/Ipv6Header.java b/staticlibs/device/com/android/net/module/util/structs/Ipv6Header.java
new file mode 100644
index 0000000..a14e064
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/Ipv6Header.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.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;
+
+import java.net.Inet6Address;
+
+/**
+ * L3 IPv6 header as per https://tools.ietf.org/html/rfc8200.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |Version| Traffic Class |           Flow Label                  |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |         Payload Length        |  Next Header  |   Hop Limit   |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +                         Source Address                        +
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +                      Destination Address                      +
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class Ipv6Header extends Struct {
+    @Field(order = 0, type = Type.S32)
+    public int vtf;
+    @Field(order = 1, type = Type.U16)
+    public int payloadLength;
+    @Field(order = 2, type = Type.S8)
+    public byte nextHeader;
+    @Field(order = 3, type = Type.U8)
+    public short hopLimit;
+    @Field(order = 4, type = Type.Ipv6Address)
+    public Inet6Address srcIp;
+    @Field(order = 5, type = Type.Ipv6Address)
+    public Inet6Address dstIp;
+
+    public Ipv6Header(final int vtf, final int payloadLength, final byte nextHeader,
+            final short hopLimit, final Inet6Address srcIp, final Inet6Address dstIp) {
+        this.vtf = vtf;
+        this.payloadLength = payloadLength;
+        this.nextHeader = nextHeader;
+        this.hopLimit = hopLimit;
+        this.srcIp = srcIp;
+        this.dstIp = dstIp;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/Ipv6PktInfo.java b/staticlibs/device/com/android/net/module/util/structs/Ipv6PktInfo.java
new file mode 100644
index 0000000..0dccb72
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/Ipv6PktInfo.java
@@ -0,0 +1,42 @@
+/*
+ * 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.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;
+
+import java.net.Inet6Address;
+
+/**
+ * structure in6_pktinfo
+ *
+ * see also:
+ *
+ *     include/uapi/linux/ipv6.h
+ */
+public class Ipv6PktInfo extends Struct {
+    @Field(order = 0, type = Type.Ipv6Address)
+    public final Inet6Address addr; // IPv6 source or destination address
+    @Field(order = 1, type = Type.S32)
+    public final int ifindex;       // interface index
+
+    public Ipv6PktInfo(final Inet6Address addr, final int ifindex) {
+        this.addr = addr;
+        this.ifindex = ifindex;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/LlaOption.java b/staticlibs/device/com/android/net/module/util/structs/LlaOption.java
new file mode 100644
index 0000000..fbaccab
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/LlaOption.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import android.net.MacAddress;
+
+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;
+import java.nio.ByteOrder;
+
+/**
+ * ICMPv6 source/target link-layer address option, as per https://tools.ietf.org/html/rfc4861.
+ *
+ * 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     |    Link-Layer Address ...
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class LlaOption extends Struct {
+    @Field(order = 0, type = Type.S8)
+    public final byte type;
+    @Field(order = 1, type = Type.S8)
+    public final byte length; // Length in 8-byte units
+    @Field(order = 2, type = Type.EUI48)
+    // Link layer address length and format varies on different link layers, which is not
+    // guaranteed to be a 6-byte MAC address. However, Struct only supports 6-byte MAC
+    // addresses type(EUI-48) for now.
+    public final MacAddress linkLayerAddress;
+
+    LlaOption(final byte type, final byte length, final MacAddress linkLayerAddress) {
+        this.type = type;
+        this.length = length;
+        this.linkLayerAddress = linkLayerAddress;
+    }
+
+    /**
+     * Build a target link-layer address option from the required specified parameters.
+     */
+    public static ByteBuffer build(final byte type, final MacAddress linkLayerAddress) {
+        final LlaOption option = new LlaOption(type, (byte) 1 /* option len */, linkLayerAddress);
+        return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/MtuOption.java b/staticlibs/device/com/android/net/module/util/structs/MtuOption.java
new file mode 100644
index 0000000..34bc21c
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/MtuOption.java
@@ -0,0 +1,65 @@
+/*
+ * 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.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
+
+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;
+import java.nio.ByteOrder;
+
+/**
+ * ICMPv6 MTU option, as per https://tools.ietf.org/html/rfc4861.
+ *
+ * 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     |           Reserved            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                              MTU                              |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class MtuOption extends Struct {
+    @Field(order = 0, type = Type.S8)
+    public final byte type;
+    @Field(order = 1, type = Type.S8)
+    public final byte length; // Length in 8-byte units
+    @Field(order = 2, type = Type.S16)
+    public final short reserved;
+    @Field(order = 3, type = Type.U32)
+    public final long mtu;
+
+    MtuOption(final byte type, final byte length, final short reserved,
+            final long mtu) {
+        this.type = type;
+        this.length = length;
+        this.reserved = reserved;
+        this.mtu = mtu;
+    }
+
+    /**
+     * Build a MTU option from the required specified parameters.
+     */
+    public static ByteBuffer build(final long mtu) {
+        final MtuOption option = new MtuOption((byte) ICMPV6_ND_OPTION_MTU,
+                (byte) 1 /* option len */, (short) 0 /* reserved */, mtu);
+        return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/NaHeader.java b/staticlibs/device/com/android/net/module/util/structs/NaHeader.java
new file mode 100644
index 0000000..90c078e
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/NaHeader.java
@@ -0,0 +1,57 @@
+/*
+ * 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.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;
+
+import java.net.Inet6Address;
+
+/**
+ * ICMPv6 Neighbor Advertisement header, follow {@link Icmpv6Header}, as per
+ * https://tools.ietf.org/html/rfc4861. This does not contain any option.
+ *
+ *  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             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |R|S|O|                     Reserved                            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +                       Target Address                          +
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |   Options ...
+ * +-+-+-+-+-+-+-+-+-+-+-+-
+ */
+public class NaHeader extends Struct {
+    @Field(order = 0, type = Type.S32)
+    public int flags; // Router flag, Solicited flag, Override flag and 29 Reserved bits.
+    @Field(order = 1, type = Type.Ipv6Address)
+    public Inet6Address target;
+
+    public NaHeader(final int flags, final Inet6Address target) {
+        this.flags = flags;
+        this.target = target;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/NsHeader.java b/staticlibs/device/com/android/net/module/util/structs/NsHeader.java
new file mode 100644
index 0000000..2e8b77b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/NsHeader.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.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;
+
+import java.net.Inet6Address;
+
+/**
+ * ICMPv6 Neighbor Solicitation header, follow {@link Icmpv6Header}, as per
+ * https://tools.ietf.org/html/rfc4861. This does not contain any option.
+ *
+ *  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             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                           Reserved                            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +                       Target Address                          +
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |   Options ...
+ * +-+-+-+-+-+-+-+-+-+-+-+-
+ */
+public class NsHeader extends Struct {
+    @Field(order = 0, type = Type.S32)
+    public int reserved; // 32 Reserved bits.
+    @Field(order = 1, type = Type.Ipv6Address)
+    public Inet6Address target;
+
+    NsHeader(int reserved, final Inet6Address target) {
+        this.reserved = reserved;
+        this.target = target;
+    }
+
+    public NsHeader(final Inet6Address target) {
+        this(0, target);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java b/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
new file mode 100644
index 0000000..bbbe571
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
@@ -0,0 +1,124 @@
+/*
+ * 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.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+
+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;
+
+/**
+ * ICMPv6 prefix information option, as per https://tools.ietf.org/html/rfc4861.
+ *
+ * 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| Reserved1 |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                         Valid Lifetime                        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                       Preferred Lifetime                      |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                           Reserved2                           |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +                            Prefix                             +
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class PrefixInformationOption extends Struct {
+    @Field(order = 0, type = Type.S8)
+    public final byte type;
+    @Field(order = 1, type = Type.S8)
+    public final byte length; // Length in 8-byte units
+    @Field(order = 2, type = Type.S8)
+    public final byte prefixLen;
+    @Field(order = 3, type = Type.S8)
+    // On-link flag, Autonomous address configuration flag, 6-reserved bits
+    public final byte flags;
+    @Field(order = 4, type = Type.U32)
+    public final long validLifetime;
+    @Field(order = 5, type = Type.U32)
+    public final long preferredLifetime;
+    @Field(order = 6, type = Type.S32)
+    public final int reserved;
+    @Field(order = 7, type = Type.ByteArray, arraysize = 16)
+    public final byte[] prefix;
+
+    @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;
+        this.length = length;
+        this.prefixLen = prefixLen;
+        this.flags = flags;
+        this.validLifetime = validLifetime;
+        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;
+    }
+
+    /**
+     * Build a Prefix Information option from the required specified parameters.
+     */
+    public static ByteBuffer build(final IpPrefix prefix, final byte flags,
+            final long validLifetime, final long preferredLifetime) {
+        final PrefixInformationOption option = new PrefixInformationOption(
+                (byte) ICMPV6_ND_OPTION_PIO, (byte) 4 /* option len */,
+                (byte) prefix.getPrefixLength(), flags, validLifetime, preferredLifetime,
+                (int) 0, prefix.getRawAddress());
+        return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/RaHeader.java b/staticlibs/device/com/android/net/module/util/structs/RaHeader.java
new file mode 100644
index 0000000..31a5cb7
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/RaHeader.java
@@ -0,0 +1,62 @@
+/*
+ * 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.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;
+
+/**
+ * ICMPv6 Router Advertisement header, follow [Icmpv6Header], as per
+ * https://tools.ietf.org/html/rfc4861. This does not contain any option.
+ *
+ * 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             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Cur Hop Limit |M|O|  Reserved |       Router Lifetime         |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                         Reachable Time                        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                          Retrans Timer                        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |   Options ...
+ * +-+-+-+-+-+-+-+-+-+-+-+-
+ */
+public class RaHeader extends Struct {
+    @Field(order = 0, type = Type.S8)
+    public final byte hopLimit;
+    // "Managed address configuration", "Other configuration" bits, and 6 reserved bits
+    @Field(order = 1, type = Type.S8)
+    public final byte flags;
+    @Field(order = 2, type = Type.U16)
+    public final int lifetime;
+    @Field(order = 3, type = Type.U32)
+    public final long reachableTime;
+    @Field(order = 4, type = Type.U32)
+    public final long retransTimer;
+
+    public RaHeader(final byte hopLimit, final byte flags, final int lifetime,
+            final long reachableTime, final long retransTimer) {
+        this.hopLimit = hopLimit;
+        this.flags = flags;
+        this.lifetime = lifetime;
+        this.reachableTime = reachableTime;
+        this.retransTimer = retransTimer;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/RdnssOption.java b/staticlibs/device/com/android/net/module/util/structs/RdnssOption.java
new file mode 100644
index 0000000..4a5bd7e
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/RdnssOption.java
@@ -0,0 +1,92 @@
+/*
+ * 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.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_RDNSS;
+
+import android.net.InetAddresses;
+
+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.net.Inet6Address;
+import java.nio.ByteBuffer;
+
+/**
+ * IPv6 RA recursive DNS server option, as per https://tools.ietf.org/html/rfc8106.
+ * This should be followed by a series of DNSv6 server addresses.
+ *
+ * 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    |           Reserved            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                           Lifetime                            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * :            Addresses of IPv6 Recursive DNS Servers            :
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class RdnssOption extends Struct {
+    @Field(order = 0, type = Type.S8)
+    public final byte type;
+    @Field(order = 1, type = Type.S8)
+    public final byte length; // Length in 8-byte units
+    @Field(order = 2, type = Type.S16)
+    public final short reserved;
+    @Field(order = 3, type = Type.U32)
+    public final long lifetime;
+
+    public RdnssOption(final byte type, final byte length, final short reserved,
+            final long lifetime) {
+        this.type = type;
+        this.length = length;
+        this.reserved = reserved;
+        this.lifetime = lifetime;
+    }
+
+    /**
+     * Build a RDNSS option from the required specified Inet6Address parameters.
+     */
+    public static ByteBuffer build(final long lifetime, final Inet6Address... servers) {
+        final byte length = (byte) (1 + 2 * servers.length);
+        final RdnssOption option = new RdnssOption((byte) ICMPV6_ND_OPTION_RDNSS,
+                length, (short) 0, lifetime);
+        final ByteBuffer buffer = ByteBuffer.allocate(length * 8);
+        option.writeToByteBuffer(buffer);
+        for (Inet6Address server : servers) {
+            buffer.put(server.getAddress());
+        }
+        buffer.flip();
+        return buffer;
+    }
+
+    /**
+     * Build a RDNSS option from the required specified String parameters.
+     *
+     * @throws IllegalArgumentException if {@code servers} does not contain only numeric addresses.
+     */
+    public static ByteBuffer build(final long lifetime, final String... servers) {
+        final Inet6Address[] serverArray = new Inet6Address[servers.length];
+        for (int i = 0; i < servers.length; i++) {
+            serverArray[i] = (Inet6Address) InetAddresses.parseNumericAddress(servers[i]);
+        }
+        return build(lifetime, serverArray);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/RouteInformationOption.java b/staticlibs/device/com/android/net/module/util/structs/RouteInformationOption.java
new file mode 100644
index 0000000..49bafed
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/RouteInformationOption.java
@@ -0,0 +1,97 @@
+/*
+ * 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.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_RIO;
+
+import android.net.IpPrefix;
+
+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;
+import java.nio.ByteOrder;
+
+/**
+ * ICMPv6 route information option, as per https://tools.ietf.org/html/rfc4191.
+ *
+ * 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 |Resvd|Prf|Resvd|
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                        Route Lifetime                         |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                   Prefix (Variable Length)                    |
+ * .                                                               .
+ * .                                                               .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class RouteInformationOption extends Struct {
+    public enum Preference {
+        HIGH((byte) 0x1),
+        MEDIUM((byte) 0x0),
+        LOW((byte) 0x3),
+        RESERVED((byte) 0x2);
+
+        final byte mValue;
+        Preference(byte value) {
+            this.mValue = value;
+        }
+    }
+
+    @Field(order = 0, type = Type.S8)
+    public final byte type;
+    @Field(order = 1, type = Type.S8)
+    public final byte length; // Length in 8-byte octets
+    @Field(order = 2, type = Type.U8)
+    public final short prefixLen;
+    @Field(order = 3, type = Type.S8)
+    public final byte prf;
+    @Field(order = 4, type = Type.U32)
+    public final long routeLifetime;
+    @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+    public final byte[] prefix;
+
+    RouteInformationOption(final byte type, final byte length, final short prefixLen,
+            final byte prf, final long routeLifetime, @NonNull final byte[] prefix) {
+        this.type = type;
+        this.length = length;
+        this.prefixLen = prefixLen;
+        this.prf = prf;
+        this.routeLifetime = routeLifetime;
+        this.prefix = prefix;
+    }
+
+    /**
+     * Build a Route Information option from the required specified parameters.
+     */
+    public static ByteBuffer build(final IpPrefix prefix, final Preference prf,
+            final long routeLifetime) {
+        // The prefix field is always assumed to have 16 bytes, but the number of leading
+        // bits in this prefix depends on IpPrefix#prefixLength, then we can simply set the
+        // option length to 3.
+        final RouteInformationOption option = new RouteInformationOption(
+                (byte) ICMPV6_ND_OPTION_RIO, (byte) 3 /* option length */,
+                (short) prefix.getPrefixLength(), (byte) (prf.mValue << 3), routeLifetime,
+                prefix.getRawAddress());
+        return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/RsHeader.java b/staticlibs/device/com/android/net/module/util/structs/RsHeader.java
new file mode 100644
index 0000000..0b51ff2
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/RsHeader.java
@@ -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.
+ */
+
+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;
+
+/**
+ * ICMPv6 Router Solicitation header, follow [Icmpv6Header], as per
+ * https://tools.ietf.org/html/rfc4861. This does not contain any option.
+ *
+ * 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             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                            Reserved                           |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |   Options ...
+ * +-+-+-+-+-+-+-+-+-+-+-+-
+ */
+public class RsHeader extends Struct {
+    @Field(order = 0, type = Type.S32)
+    public final int reserved;
+
+    public RsHeader(final int reserved) {
+        this.reserved = reserved;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java
new file mode 100644
index 0000000..24e0a97
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java
@@ -0,0 +1,99 @@
+/*
+ * 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.net.module.util.structs;
+
+import static android.system.OsConstants.AF_INET6;
+
+import com.android.net.module.util.Struct;
+import java.net.Inet6Address;
+import java.util.Set;
+
+/*
+ * Implements the mf6cctl structure which is used to add a multicast forwarding
+ * cache, see /usr/include/linux/mroute6.h
+ */
+public class StructMf6cctl extends Struct {
+    // struct sockaddr_in6 mf6cc_origin, added the fields directly as Struct
+    // doesn't support nested Structs
+    @Field(order = 0, type = Type.U16)
+    public final int originFamily; // AF_INET6
+    @Field(order = 1, type = Type.U16)
+    public final int originPort; // Transport layer port # of origin
+    @Field(order = 2, type = Type.U32)
+    public final long originFlowinfo; // IPv6 flow information
+    @Field(order = 3, type = Type.ByteArray, arraysize = 16)
+    public final byte[] originAddress; //the IPv6 address of origin
+    @Field(order = 4, type = Type.U32)
+    public final long originScopeId; // scope id, not used
+
+    // struct sockaddr_in6 mf6cc_mcastgrp
+    @Field(order = 5, type = Type.U16)
+    public final int groupFamily; // AF_INET6
+    @Field(order = 6, type = Type.U16)
+    public final int groupPort; // Transport layer port # of multicast group
+    @Field(order = 7, type = Type.U32)
+    public final long groupFlowinfo; // IPv6 flow information
+    @Field(order = 8, type = Type.ByteArray, arraysize = 16)
+    public final byte[] groupAddress; //the IPv6 address of multicast group
+    @Field(order = 9, type = Type.U32)
+    public final long groupScopeId; // scope id, not used
+
+    @Field(order = 10, type = Type.U16, padding = 2)
+    public final int mf6ccParent; // incoming interface
+    @Field(order = 11, type = Type.ByteArray, arraysize = 32)
+    public final byte[] mf6ccIfset; // outgoing interfaces
+
+    public StructMf6cctl(final Inet6Address origin, final Inet6Address group,
+            final int mf6ccParent, final Set<Integer> oifset) {
+        this(AF_INET6, 0, (long) 0, origin.getAddress(), (long) 0, AF_INET6,
+                0, (long) 0, group.getAddress(), (long) 0, mf6ccParent,
+                getMf6ccIfsetBytes(oifset));
+    }
+
+    private StructMf6cctl(int originFamily, int originPort, long originFlowinfo,
+            byte[] originAddress, long originScopeId, int groupFamily, int groupPort,
+            long groupFlowinfo, byte[] groupAddress, long groupScopeId, int mf6ccParent,
+            byte[] mf6ccIfset) {
+        this.originFamily = originFamily;
+        this.originPort = originPort;
+        this.originFlowinfo = originFlowinfo;
+        this.originAddress = originAddress;
+        this.originScopeId = originScopeId;
+        this.groupFamily = groupFamily;
+        this.groupPort = groupPort;
+        this.groupFlowinfo = groupFlowinfo;
+        this.groupAddress = groupAddress;
+        this.groupScopeId = groupScopeId;
+        this.mf6ccParent = mf6ccParent;
+        this.mf6ccIfset = mf6ccIfset;
+    }
+
+    private static byte[] getMf6ccIfsetBytes(final Set<Integer> oifs)
+            throws IllegalArgumentException {
+        byte[] mf6ccIfset = new byte[32];
+        for (int oif : oifs) {
+            int idx = oif / 8;
+            if (idx >= 32) {
+                // invalid oif index, too big to fit in mf6ccIfset
+                throw new IllegalArgumentException("Invalid oif index" + oif);
+            }
+            int offset = oif % 8;
+            mf6ccIfset[idx] |= (byte) (1 << offset);
+        }
+        return mf6ccIfset;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java
new file mode 100644
index 0000000..626a170
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java
@@ -0,0 +1,46 @@
+/*
+ * 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.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+
+/*
+ * Implements the mif6ctl structure which is used to add a multicast routing
+ * interface, see /usr/include/linux/mroute6.h
+ */
+public class StructMif6ctl extends Struct {
+    @Field(order = 0, type = Type.U16)
+    public final int mif6cMifi; // Index of MIF
+    @Field(order = 1, type = Type.U8)
+    public final short mif6cFlags; // MIFF_ flags
+    @Field(order = 2, type = Type.U8)
+    public final short vifcThreshold; // ttl limit
+    @Field(order = 3, type = Type.U16)
+    public final int mif6cPifi; //the index of the physical IF
+    @Field(order = 4, type = Type.U32, padding = 2)
+    public final long vifcRateLimit; // Rate limiter values (NI)
+
+    public StructMif6ctl(final int mif6cMifi, final short mif6cFlags, final short vifcThreshold,
+            final int mif6cPifi, final long vifcRateLimit) {
+        this.mif6cMifi = mif6cMifi;
+        this.mif6cFlags = mif6cFlags;
+        this.vifcThreshold = vifcThreshold;
+        this.mif6cPifi = mif6cPifi;
+        this.vifcRateLimit = vifcRateLimit;
+    }
+}
+
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java b/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java
new file mode 100644
index 0000000..569e361
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java
@@ -0,0 +1,52 @@
+/*
+ * 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.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class StructMrt6Msg extends Struct {
+    public static final byte MRT6MSG_NOCACHE = 1;
+
+    @Field(order = 0, type = Type.S8)
+    public final byte mbz;
+    @Field(order = 1, type = Type.S8)
+    public final byte msgType; // message type
+    @Field(order = 2, type = Type.U16, padding = 4)
+    public final int mif; // mif received on
+    @Field(order = 3, type = Type.Ipv6Address)
+    public final Inet6Address src;
+    @Field(order = 4, type = Type.Ipv6Address)
+    public final Inet6Address dst;
+
+    public StructMrt6Msg(final byte mbz, final byte msgType, final int mif,
+                  final Inet6Address source, final Inet6Address destination) {
+        this.mbz = mbz; // kernel should set it to 0
+        this.msgType = msgType;
+        this.mif = mif;
+        this.src = source;
+        this.dst = destination;
+    }
+
+    public static StructMrt6Msg parse(ByteBuffer byteBuffer) {
+        byteBuffer.order(ByteOrder.nativeOrder());
+        return Struct.parse(StructMrt6Msg.class, byteBuffer);
+    }
+}
+
diff --git a/staticlibs/device/com/android/net/module/util/structs/TcpHeader.java b/staticlibs/device/com/android/net/module/util/structs/TcpHeader.java
new file mode 100644
index 0000000..0c97401
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/TcpHeader.java
@@ -0,0 +1,79 @@
+/*
+ * 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.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;
+
+/**
+ * L4 TCP header as per https://tools.ietf.org/html/rfc793.
+ * This class does not contain option and data fields.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |          Source Port          |       Destination Port        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                        Sequence Number                        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                    Acknowledgment Number                      |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |  Data |           |U|A|P|R|S|F|                               |
+ * | Offset| Reserved  |R|C|S|S|Y|I|            Window             |
+ * |       |           |G|K|H|T|N|N|                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |           Checksum            |         Urgent Pointer        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                    Options                    |    Padding    |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                             data                              |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class TcpHeader extends Struct {
+    @Field(order = 0, type = Type.U16)
+    public final int srcPort;
+    @Field(order = 1, type = Type.U16)
+    public final int dstPort;
+    @Field(order = 2, type = Type.U32)
+    public final long seq;
+    @Field(order = 3, type = Type.U32)
+    public final long ack;
+    @Field(order = 4, type = Type.S16)
+    // data Offset (4 bits), reserved (6 bits), control bits (6 bits)
+    // TODO: update with bitfields once class Struct supports it
+    public final short dataOffsetAndControlBits;
+    @Field(order = 5, type = Type.U16)
+    public final int window;
+    @Field(order = 6, type = Type.S16)
+    public final short checksum;
+    @Field(order = 7, type = Type.U16)
+    public final int urgentPointer;
+
+    public TcpHeader(final int srcPort, final int dstPort, final long seq, final long ack,
+            final short dataOffsetAndControlBits, final int window, final short checksum,
+            final int urgentPointer) {
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.seq = seq;
+        this.ack = ack;
+        this.dataOffsetAndControlBits = dataOffsetAndControlBits;
+        this.window = window;
+        this.checksum = checksum;
+        this.urgentPointer = urgentPointer;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/UdpHeader.java b/staticlibs/device/com/android/net/module/util/structs/UdpHeader.java
new file mode 100644
index 0000000..8b0316b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/UdpHeader.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.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;
+
+/**
+ * L4 UDP header as per https://tools.ietf.org/html/rfc768.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |          Source Port          |       Destination Port        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |           Length              |          Checksum             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                          data octets  ...
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ...
+ */
+public class UdpHeader extends Struct {
+    @Field(order = 0, type = Type.U16)
+    public final int srcPort;
+    @Field(order = 1, type = Type.U16)
+    public final int dstPort;
+    @Field(order = 2, type = Type.U16)
+    public final int length;
+    @Field(order = 3, type = Type.S16)
+    public final short checksum;
+
+    public UdpHeader(final int srcPort, final int dstPort, final int length,
+            final short checksum) {
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.length = length;
+        this.checksum = checksum;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/wear/NetPacketHelpers.java b/staticlibs/device/com/android/net/module/util/wear/NetPacketHelpers.java
new file mode 100644
index 0000000..341c44b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/wear/NetPacketHelpers.java
@@ -0,0 +1,41 @@
+/*
+ * 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.net.module.util.wear;
+
+import com.android.net.module.util.async.ReadableByteBuffer;
+
+/**
+ * Implements utilities for decoding parts of TCP/UDP/IP headers.
+ *
+ * @hide
+ */
+final class NetPacketHelpers {
+    static void encodeNetworkUnsignedInt16(int value, byte[] dst, final int dstPos) {
+        dst[dstPos] = (byte) ((value >> 8) & 0xFF);
+        dst[dstPos + 1] = (byte) (value & 0xFF);
+    }
+
+    static int decodeNetworkUnsignedInt16(byte[] data, final int pos) {
+        return ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
+    }
+
+    static int decodeNetworkUnsignedInt16(ReadableByteBuffer data, final int pos) {
+        return ((data.peek(pos) & 0xFF) << 8) | (data.peek(pos + 1) & 0xFF);
+    }
+
+    private NetPacketHelpers() {}
+}
diff --git a/staticlibs/device/com/android/net/module/util/wear/PacketFile.java b/staticlibs/device/com/android/net/module/util/wear/PacketFile.java
new file mode 100644
index 0000000..7f5ed78
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/wear/PacketFile.java
@@ -0,0 +1,85 @@
+/*
+ * 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.net.module.util.wear;
+
+/**
+ * Defines bidirectional file where all transmissions are made as complete packets.
+ *
+ * Automatically manages all readability and writeability events in EventManager:
+ *   - When read buffer has more space - asks EventManager to notify on more data
+ *   - When write buffer has more space - asks the user to provide more data
+ *   - When underlying file cannot accept more data - registers EventManager callback
+ *
+ * @hide
+ */
+public interface PacketFile {
+    /** @hide */
+    public enum ErrorCode {
+        UNEXPECTED_ERROR,
+        IO_ERROR,
+        INBOUND_PACKET_TOO_LARGE,
+        OUTBOUND_PACKET_TOO_LARGE,
+    }
+
+    /**
+     * Receives notifications when new data or output space is available.
+     *
+     * @hide
+     */
+    public interface Listener {
+        /**
+         * Handles the initial part of the stream, which on some systems provides lower-level
+         * configuration data.
+         *
+         * Returns the number of bytes consumed, or zero if the preamble has been fully read.
+         */
+        int onPreambleData(byte[] data, int pos, int len);
+
+        /** Handles one extracted packet. */
+        void onInboundPacket(byte[] data, int pos, int len);
+
+        /** Notifies on new data being added to the buffer. */
+        void onInboundBuffered(int newByteCount, int totalBufferedSize);
+
+        /** Notifies on data being flushed from output buffer. */
+        void onOutboundPacketSpace();
+
+        /** Notifies on unrecoverable error in the packet processing. */
+        void onPacketFileError(ErrorCode error, String message);
+    }
+
+    /** Requests this file to be closed. */
+    void close();
+
+    /** Permanently disables reading of this file, and clears all buffered data. */
+    void shutdownReading();
+
+    /** Starts or resumes async read operations on this file. */
+    void continueReading();
+
+    /** Returns the number of bytes currently buffered as input. */
+    int getInboundBufferSize();
+
+    /** Returns the number of bytes currently available for buffering for output. */
+    int getOutboundFreeSize();
+
+    /**
+     * Queues the given data for output.
+     * Throws runtime exception if there is not enough space.
+     */
+    boolean enqueueOutboundPacket(byte[] data, int pos, int len);
+}
diff --git a/staticlibs/device/com/android/net/module/util/wear/StreamingPacketFile.java b/staticlibs/device/com/android/net/module/util/wear/StreamingPacketFile.java
new file mode 100644
index 0000000..52dbee4
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/wear/StreamingPacketFile.java
@@ -0,0 +1,221 @@
+/*
+ * 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.net.module.util.wear;
+
+import com.android.net.module.util.async.BufferedFile;
+import com.android.net.module.util.async.EventManager;
+import com.android.net.module.util.async.FileHandle;
+import com.android.net.module.util.async.Assertions;
+import com.android.net.module.util.async.ReadableByteBuffer;
+
+import java.io.IOException;
+
+/**
+ * Implements PacketFile based on a streaming file descriptor.
+ *
+ * Packets are delineated using network-order 2-byte length indicators.
+ *
+ * @hide
+ */
+public final class StreamingPacketFile implements PacketFile, BufferedFile.Listener {
+    private static final int HEADER_SIZE = 2;
+
+    private final EventManager mEventManager;
+    private final Listener mListener;
+    private final BufferedFile mFile;
+    private final int mMaxPacketSize;
+    private final ReadableByteBuffer mInboundBuffer;
+    private boolean mIsInPreamble = true;
+
+    private final byte[] mTempPacketReadBuffer;
+    private final byte[] mTempHeaderWriteBuffer;
+
+    public StreamingPacketFile(
+            EventManager eventManager,
+            FileHandle fileHandle,
+            Listener listener,
+            int maxPacketSize,
+            int maxBufferedInboundPackets,
+            int maxBufferedOutboundPackets) throws IOException {
+        if (eventManager == null || fileHandle == null || listener == null) {
+            throw new NullPointerException();
+        }
+
+        mEventManager = eventManager;
+        mListener = listener;
+        mMaxPacketSize = maxPacketSize;
+
+        final int maxTotalLength = HEADER_SIZE + maxPacketSize;
+
+        mFile = BufferedFile.create(eventManager, fileHandle, this,
+            maxTotalLength * maxBufferedInboundPackets,
+            maxTotalLength * maxBufferedOutboundPackets);
+        mInboundBuffer = mFile.getInboundBuffer();
+
+        mTempPacketReadBuffer = new byte[maxTotalLength];
+        mTempHeaderWriteBuffer = new byte[HEADER_SIZE];
+    }
+
+    @Override
+    public void close() {
+        mFile.close();
+    }
+
+    public BufferedFile getUnderlyingFileForTest() {
+        return mFile;
+    }
+
+    @Override
+    public void shutdownReading() {
+        mFile.shutdownReading();
+    }
+
+    @Override
+    public void continueReading() {
+        mFile.continueReading();
+    }
+
+    @Override
+    public int getInboundBufferSize() {
+        return mInboundBuffer.size();
+    }
+
+    @Override
+    public void onBufferedFileClosed() {
+    }
+
+    @Override
+    public void onBufferedFileInboundData(int readByteCount) {
+        if (mFile.isReadingShutdown()) {
+            return;
+        }
+
+        if (readByteCount > 0) {
+            mListener.onInboundBuffered(readByteCount, mInboundBuffer.size());
+        }
+
+        if (extractOnePacket() && !mFile.isReadingShutdown()) {
+            // There could be more packets already buffered, continue parsing next
+            // packet even before another read event comes
+            mEventManager.execute(() -> {
+                onBufferedFileInboundData(0);
+            });
+        } else {
+            continueReading();
+        }
+    }
+
+    private boolean extractOnePacket() {
+        while (mIsInPreamble) {
+            final int directReadSize = Math.min(
+                mInboundBuffer.getDirectReadSize(), mTempPacketReadBuffer.length);
+            if (directReadSize == 0) {
+                return false;
+            }
+
+            // Copy for safety, so higher-level callback cannot modify the data.
+            System.arraycopy(mInboundBuffer.getDirectReadBuffer(),
+                mInboundBuffer.getDirectReadPos(), mTempPacketReadBuffer, 0, directReadSize);
+
+            final int preambleConsumedBytes = mListener.onPreambleData(
+                mTempPacketReadBuffer, 0, directReadSize);
+            if (mFile.isReadingShutdown()) {
+                return false;  // The callback has called shutdownReading().
+            }
+
+            if (preambleConsumedBytes == 0) {
+                mIsInPreamble = false;
+                break;
+            }
+
+            mInboundBuffer.accountForDirectRead(preambleConsumedBytes);
+        }
+
+        final int bufferedSize = mInboundBuffer.size();
+        if (bufferedSize < HEADER_SIZE) {
+            return false;
+        }
+
+        final int dataLength = NetPacketHelpers.decodeNetworkUnsignedInt16(mInboundBuffer, 0);
+        if (dataLength > mMaxPacketSize) {
+            mListener.onPacketFileError(
+                PacketFile.ErrorCode.INBOUND_PACKET_TOO_LARGE,
+                "Inbound packet length: " + dataLength);
+            return false;
+        }
+
+        final int totalLength = HEADER_SIZE + dataLength;
+        if (bufferedSize < totalLength) {
+            return false;
+        }
+
+        mInboundBuffer.readBytes(mTempPacketReadBuffer, 0, totalLength);
+
+        mListener.onInboundPacket(mTempPacketReadBuffer, HEADER_SIZE, dataLength);
+        return true;
+    }
+
+    @Override
+    public int getOutboundFreeSize() {
+        final int freeSize = mFile.getOutboundBufferFreeSize();
+        return (freeSize > HEADER_SIZE ? freeSize - HEADER_SIZE : 0);
+    }
+
+    @Override
+    public boolean enqueueOutboundPacket(byte[] buffer, int pos, int len) {
+        Assertions.throwsIfOutOfBounds(buffer, pos, len);
+
+        if (len == 0) {
+            return true;
+        }
+
+        if (len > mMaxPacketSize) {
+            mListener.onPacketFileError(
+                PacketFile.ErrorCode.OUTBOUND_PACKET_TOO_LARGE,
+                "Outbound packet length: " + len);
+            return false;
+        }
+
+        NetPacketHelpers.encodeNetworkUnsignedInt16(len, mTempHeaderWriteBuffer, 0);
+
+        mFile.enqueueOutboundData(
+            mTempHeaderWriteBuffer, 0, mTempHeaderWriteBuffer.length,
+            buffer, pos, len);
+        return true;
+    }
+
+    @Override
+    public void onBufferedFileOutboundSpace() {
+        mListener.onOutboundPacketSpace();
+    }
+
+    @Override
+    public void onBufferedFileIoError(String message) {
+        mListener.onPacketFileError(PacketFile.ErrorCode.IO_ERROR, message);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("maxPacket=");
+        sb.append(mMaxPacketSize);
+        sb.append(", file={");
+        sb.append(mFile);
+        sb.append("}");
+        return sb.toString();
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/BestClock.java b/staticlibs/framework/com/android/net/module/util/BestClock.java
new file mode 100644
index 0000000..35391ad
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/BestClock.java
@@ -0,0 +1,78 @@
+/*
+ * 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.net.module.util;
+
+import android.util.Log;
+
+import java.time.Clock;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Arrays;
+
+/**
+ * Single {@link Clock} that will return the best available time from a set of
+ * prioritized {@link Clock} instances.
+ * <p>
+ * For example, when {@link SystemClock#currentNetworkTimeClock()} isn't able to
+ * provide the time, this class could use {@link Clock#systemUTC()} instead.
+ *
+ * Note that this is re-implemented based on {@code android.os.BestClock} to be used inside
+ * the mainline module. And the class does NOT support serialization.
+ *
+ * @hide
+ */
+final public class BestClock extends Clock {
+    private static final String TAG = "BestClock";
+    private final ZoneId mZone;
+    private final Clock[] mClocks;
+
+    public BestClock(ZoneId zone, Clock... clocks) {
+        super();
+        this.mZone = zone;
+        this.mClocks = clocks;
+    }
+
+    @Override
+    public long millis() {
+        for (Clock clock : mClocks) {
+            try {
+                return clock.millis();
+            } catch (DateTimeException e) {
+                // Ignore and attempt the next clock
+                Log.w(TAG, e.toString());
+            }
+        }
+        throw new DateTimeException(
+                "No clocks in " + Arrays.toString(mClocks) + " were able to provide time");
+    }
+
+    @Override
+    public ZoneId getZone() {
+        return mZone;
+    }
+
+    @Override
+    public Clock withZone(ZoneId zone) {
+        return new BestClock(zone, mClocks);
+    }
+
+    @Override
+    public Instant instant() {
+        return Instant.ofEpochMilli(millis());
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/BinderUtils.java b/staticlibs/framework/com/android/net/module/util/BinderUtils.java
new file mode 100644
index 0000000..e4d14ea
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/BinderUtils.java
@@ -0,0 +1,96 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+
+import java.util.function.Supplier;
+
+/**
+ * Collection of utilities for {@link Binder} and related classes.
+ * @hide
+ */
+public class BinderUtils {
+    /**
+     * Convenience method for running the provided action enclosed in
+     * {@link Binder#clearCallingIdentity}/{@link Binder#restoreCallingIdentity}
+     *
+     * Any exception thrown by the given action will be caught and rethrown after the call to
+     * {@link Binder#restoreCallingIdentity}
+     *
+     * Note that this is copied from Binder#withCleanCallingIdentity with minor changes
+     * since it is not public.
+     *
+     * @hide
+     */
+    public static final <T extends Exception> void withCleanCallingIdentity(
+            @NonNull ThrowingRunnable<T> action) throws T {
+        final long callingIdentity = Binder.clearCallingIdentity();
+        try {
+            action.run();
+        } finally {
+            Binder.restoreCallingIdentity(callingIdentity);
+        }
+    }
+
+    /**
+     * Like a Runnable, but declared to throw an exception.
+     *
+     * @param <T> The exception class which is declared to be thrown.
+     */
+    @FunctionalInterface
+    public interface ThrowingRunnable<T extends Exception> {
+        /** @see java.lang.Runnable */
+        void run() throws T;
+    }
+
+    /**
+     * Convenience method for running the provided action enclosed in
+     * {@link Binder#clearCallingIdentity}/{@link Binder#restoreCallingIdentity} returning the
+     * result.
+     *
+     * <p>Any exception thrown by the given action will be caught and rethrown after
+     * the call to {@link Binder#restoreCallingIdentity}.
+     *
+     * Note that this is copied from Binder#withCleanCallingIdentity with minor changes
+     * since it is not public.
+     *
+     * @hide
+     */
+    public static final <T, E extends Exception> T withCleanCallingIdentity(
+            @NonNull ThrowingSupplier<T, E> action) throws E {
+        final long callingIdentity = Binder.clearCallingIdentity();
+        try {
+            return action.get();
+        } finally {
+            Binder.restoreCallingIdentity(callingIdentity);
+        }
+    }
+
+    /**
+     * An equivalent of {@link Supplier}
+     *
+     * @param <T> The class which is declared to be returned.
+     * @param <E> The exception class which is declared to be thrown.
+     */
+    @FunctionalInterface
+    public interface ThrowingSupplier<T, E extends Exception> {
+        /** @see java.util.function.Supplier */
+        T get() throws E;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/BitUtils.java b/staticlibs/framework/com/android/net/module/util/BitUtils.java
new file mode 100644
index 0000000..3062d8c
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/BitUtils.java
@@ -0,0 +1,140 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * @hide
+ */
+public class BitUtils {
+    /**
+     * Unpacks long value into an array of bits.
+     */
+    public static int[] unpackBits(long val) {
+        int size = Long.bitCount(val);
+        int[] result = new int[size];
+        int index = 0;
+        int bitPos = 0;
+        while (val != 0) {
+            if ((val & 1) == 1) result[index++] = bitPos;
+            val = val >>> 1;
+            bitPos++;
+        }
+        return result;
+    }
+
+    /**
+     * Packs a list of ints in the same way as packBits()
+     *
+     * Each passed int is the rank of a bit that should be set in the returned long.
+     * Example : passing (1,3) will return in 0b00001010 and passing (5,6,0) will return 0b01100001
+     *
+     * @param bits bits to pack
+     * @return a long with the specified bits set.
+     */
+    public static long packBitList(int... bits) {
+        return packBits(bits);
+    }
+
+    /**
+     * Packs array of bits into a long value.
+     *
+     * Each passed int is the rank of a bit that should be set in the returned long.
+     * Example : passing [1,3] will return in 0b00001010 and passing [5,6,0] will return 0b01100001
+     *
+     * @param bits bits to pack
+     * @return a long with the specified bits set.
+     */
+    public static long packBits(int[] bits) {
+        long packed = 0;
+        for (int b : bits) {
+            packed |= (1L << b);
+        }
+        return packed;
+    }
+
+    /**
+     * An interface for a function that can retrieve a name associated with an int.
+     *
+     * This is useful for bitfields like network capabilities or network score policies.
+     */
+    @FunctionalInterface
+    public interface NameOf {
+        /** Retrieve the name associated with the passed value */
+        String nameOf(int value);
+    }
+
+    /**
+     * Given a bitmask and a name fetcher, append names of all set bits to the builder
+     *
+     * This method takes all bit sets in the passed bitmask, will figure out the name associated
+     * with the weight of each bit with the passed name fetcher, and append each name to the
+     * passed StringBuilder, separated by the passed separator.
+     *
+     * For example, if the bitmask is 0110, and the name fetcher return "BIT_1" to "BIT_4" for
+     * numbers from 1 to 4, and the separator is "&", this method appends "BIT_2&BIT3" to the
+     * StringBuilder.
+     */
+    public static void appendStringRepresentationOfBitMaskToStringBuilder(@NonNull StringBuilder sb,
+            long bitMask, @NonNull NameOf nameFetcher, @NonNull String separator) {
+        int bitPos = 0;
+        boolean firstElementAdded = false;
+        while (bitMask != 0) {
+            if ((bitMask & 1) != 0) {
+                if (firstElementAdded) {
+                    sb.append(separator);
+                } else {
+                    firstElementAdded = true;
+                }
+                sb.append(nameFetcher.nameOf(bitPos));
+            }
+            bitMask >>>= 1;
+            ++bitPos;
+        }
+    }
+
+    /**
+     * Returns a short but human-readable string of updates between an old and a new bit fields.
+     *
+     * @param oldVal the old bit field to diff from
+     * @param newVal the new bit field to diff to
+     * @return a string fit for logging differences, or null if no differences.
+     *         this method cannot return the empty string.
+     */
+    @Nullable
+    public static String describeDifferences(final long oldVal, final long newVal,
+            @NonNull final NameOf nameFetcher) {
+        final long changed = oldVal ^ newVal;
+        if (0 == changed) return null;
+        // If the control reaches here, there are changes (additions, removals, or both) so
+        // the code below is guaranteed to add something to the string and can't return "".
+        final long removed = oldVal & changed;
+        final long added = newVal & changed;
+        final StringBuilder sb = new StringBuilder();
+        if (0 != removed) {
+            sb.append("-");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, removed, nameFetcher, "-");
+        }
+        if (0 != added) {
+            sb.append("+");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, added, nameFetcher, "+");
+        }
+        return sb.toString();
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/ByteUtils.java b/staticlibs/framework/com/android/net/module/util/ByteUtils.java
new file mode 100644
index 0000000..290ed46
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/ByteUtils.java
@@ -0,0 +1,67 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+
+/**
+ * Byte utility functions.
+ * @hide
+ */
+public class ByteUtils {
+    /**
+     * Returns the index of the first appearance of the value {@code target} in {@code array}.
+     *
+     * @param array an array of {@code byte} values, possibly empty
+     * @param target a primitive {@code byte} value
+     * @return the least index {@code i} for which {@code array[i] == target}, or {@code -1} if no
+     *     such index exists.
+     */
+    public static int indexOf(@NonNull byte[] array, byte target) {
+        return indexOf(array, target, 0, array.length);
+    }
+
+    private static int indexOf(byte[] array, byte target, int start, int end) {
+        for (int i = start; i < end; i++) {
+            if (array[i] == target) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Returns the values from each provided array combined into a single array. For example, {@code
+     * concat(new byte[] {a, b}, new byte[] {}, new byte[] {c}} returns the array {@code {a, b, c}}.
+     *
+     * @param arrays zero or more {@code byte} arrays
+     * @return a single array containing all the values from the source arrays, in order
+     */
+    public static byte[] concat(@NonNull byte[]... arrays) {
+        int length = 0;
+        for (byte[] array : arrays) {
+            length += array.length;
+        }
+        byte[] result = new byte[length];
+        int pos = 0;
+        for (byte[] array : arrays) {
+            System.arraycopy(array, 0, result, pos, array.length);
+            pos += array.length;
+        }
+        return result;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
new file mode 100644
index 0000000..f3d8c4a
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
@@ -0,0 +1,416 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Utilities for {@link Collection} and arrays.
+ * @hide
+ */
+public final class CollectionUtils {
+    private CollectionUtils() {}
+
+    /**
+     * @return True if the array is null or 0-length.
+     */
+    public static <T> boolean isEmpty(@Nullable T[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * @return True if the collection is null or 0-length.
+     */
+    public static <T> boolean isEmpty(@Nullable Collection<T> collection) {
+        return collection == null || collection.isEmpty();
+    }
+
+    /**
+     * Returns an int array from the given Integer list.
+     */
+    @NonNull
+    public static int[] toIntArray(@NonNull Collection<Integer> list) {
+        int[] array = new int[list.size()];
+        int i = 0;
+        for (Integer item : list) {
+            array[i] = item;
+            i++;
+        }
+        return array;
+    }
+
+    /**
+     * Returns a long array from the given long list.
+     */
+    @NonNull
+    public static long[] toLongArray(@NonNull Collection<Long> list) {
+        long[] array = new long[list.size()];
+        int i = 0;
+        for (Long item : list) {
+            array[i] = item;
+            i++;
+        }
+        return array;
+    }
+
+    /**
+     * @return True if all elements satisfy the predicate, false otherwise.
+     *   Note that means this always returns true for empty collections.
+     */
+    public static <T> boolean all(@NonNull Collection<T> elem, @NonNull Predicate<T> predicate) {
+        for (final T e : elem) {
+            if (!predicate.test(e)) return false;
+        }
+        return true;
+
+    }
+
+    /**
+     * @return True if any element satisfies the predicate, false otherwise.
+     *   Note that means this always returns false for empty collections.
+     */
+    public static <T> boolean any(@NonNull Collection<T> elem, @NonNull Predicate<T> predicate) {
+        return indexOf(elem, predicate) >= 0;
+    }
+
+    /**
+     * @return The index of the first element that matches the predicate, or -1 if none.
+     */
+    public static <T> int indexOf(@NonNull final Collection<T> elem,
+            @NonNull final Predicate<? super T> predicate) {
+        int idx = 0;
+        for (final T e : elem) {
+            if (predicate.test(e)) return idx;
+            idx++;
+        }
+        return -1;
+    }
+
+    /**
+     * @return True if there exists at least one element in the sparse array for which
+     * condition {@code predicate}
+     */
+    public static <T> boolean any(@NonNull SparseArray<T> array, @NonNull Predicate<T> predicate) {
+        for (int i = 0; i < array.size(); ++i) {
+            if (predicate.test(array.valueAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the array contains the specified value.
+     */
+    public static boolean contains(@Nullable short[] array, short value) {
+        if (array == null) return false;
+        for (int element : array) {
+            if (element == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the array contains the specified value.
+     */
+    public static boolean contains(@Nullable int[] array, int value) {
+        if (array == null) return false;
+        for (int element : array) {
+            if (element == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the array contains the specified value.
+     */
+    public static <T> boolean contains(@Nullable T[] array, @Nullable T value) {
+        return indexOf(array, value) != -1;
+    }
+
+    /**
+     * Return first index of value in given array, or -1 if not found.
+     */
+    public static <T> int indexOf(@Nullable T[] array, @Nullable T value) {
+        if (array == null) return -1;
+        for (int i = 0; i < array.length; i++) {
+            if (Objects.equals(array[i], value)) return i;
+        }
+        return -1;
+    }
+
+    /**
+     * Returns the index of the needle array in the haystack array, or -1 if it can't be found.
+     * This is a byte array equivalent of Collections.indexOfSubList().
+     */
+    public static int indexOfSubArray(@NonNull byte[] haystack, @NonNull byte[] needle) {
+        for (int i = 0; i < haystack.length - needle.length + 1; i++) {
+            boolean found = true;
+            for (int j = 0; j < needle.length; j++) {
+                if (haystack[i + j] != needle[j]) {
+                    found = false;
+                    break;
+                }
+            }
+            if (found) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Returns a new collection of elements that match the passed predicate.
+     * @param source the elements to filter.
+     * @param test the predicate to test for.
+     * @return a new collection containing only the source elements that satisfy the predicate.
+     */
+    @NonNull public static <T> ArrayList<T> filter(@NonNull final Collection<T> source,
+            @NonNull final Predicate<T> test) {
+        final ArrayList<T> matches = new ArrayList<>();
+        for (final T e : source) {
+            if (test.test(e)) {
+                matches.add(e);
+            }
+        }
+        return matches;
+    }
+
+    /**
+     * Return sum of the given long array.
+     */
+    public static long total(@Nullable long[] array) {
+        long total = 0;
+        if (array != null) {
+            for (long value : array) {
+                total += value;
+            }
+        }
+        return total;
+    }
+
+    /**
+     * Returns true if the first collection contains any of the elements of the second.
+     * @param haystack where to search
+     * @param needles what to search for
+     * @param <T> type of elements
+     * @return true if |haystack| contains any of the |needles|, false otherwise
+     */
+    public static <T> boolean containsAny(@NonNull final Collection<T> haystack,
+            @NonNull final Collection<? extends T> needles) {
+        for (T needle : needles) {
+            if (haystack.contains(needle)) return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the first collection contains all of the elements of the second.
+     * @param haystack where to search
+     * @param needles what to search for
+     * @param <T> type of elements
+     * @return true if |haystack| contains all of the |needles|, false otherwise
+     */
+    public static <T> boolean containsAll(@NonNull final Collection<T> haystack,
+            @NonNull final Collection<? extends T> needles) {
+        return haystack.containsAll(needles);
+    }
+
+    /**
+     * Returns the first item of a collection that matches the predicate.
+     * @param haystack The collection to search.
+     * @param condition The predicate to match.
+     * @param <T> The type of element in the collection.
+     * @return The first element matching the predicate, or null if none.
+     */
+    @Nullable
+    public static <T> T findFirst(@NonNull final Collection<T> haystack,
+            @NonNull final Predicate<? super T> condition) {
+        for (T needle : haystack) {
+            if (condition.test(needle)) return needle;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the last item of a List that matches the predicate.
+     * @param haystack The List to search.
+     * @param condition The predicate to match.
+     * @param <T> The type of element in the list.
+     * @return The last element matching the predicate, or null if none.
+     */
+    // There is no way to reverse iterate a Collection in Java (e.g. the collection may
+    // be a single-linked list), so implementing this on Collection is necessarily very
+    // wasteful (store and reverse a copy, test all elements, or recurse to the end of the
+    // list to test on the up path and possibly blow the call stack)
+    @Nullable
+    public static <T> T findLast(@NonNull final List<T> haystack,
+            @NonNull final Predicate<? super T> condition) {
+        for (int i = haystack.size() - 1; i >= 0; --i) {
+            final T needle = haystack.get(i);
+            if (condition.test(needle)) return needle;
+        }
+        return null;
+    }
+
+    /**
+     * Returns whether a collection contains an element matching a condition
+     * @param haystack The collection to search.
+     * @param condition The predicate to match.
+     * @param <T> The type of element in the collection.
+     * @return Whether the collection contains any element matching the condition.
+     */
+    public static <T> boolean contains(@NonNull final Collection<T> haystack,
+            @NonNull final Predicate<? super T> condition) {
+        return -1 != indexOf(haystack, condition);
+    }
+
+    /**
+     * Standard map function, but returns a new modifiable ArrayList
+     *
+     * This returns a new list that contains, for each element of the source collection, its
+     * image through the passed transform.
+     * Elements in the source can be null if the transform accepts null inputs.
+     * Elements in the output can be null if the transform ever returns null.
+     * This function never returns null. If the source collection is empty, it returns the
+     * empty list.
+     * Contract : this method calls the transform function exactly once for each element in the
+     * list, in iteration order.
+     *
+     * @param source the source collection
+     * @param transform the function to transform the elements
+     * @param <T> type of source elements
+     * @param <R> type of destination elements
+     * @return an unmodifiable list of transformed elements
+     */
+    @NonNull
+    public static <T, R> ArrayList<R> map(@NonNull final Collection<T> source,
+            @NonNull final Function<? super T, ? extends R> transform) {
+        final ArrayList<R> dest = new ArrayList<>(source.size());
+        for (final T e : source) {
+            dest.add(transform.apply(e));
+        }
+        return dest;
+    }
+
+    /**
+     * Standard zip function, but returns a new modifiable ArrayList
+     *
+     * This returns a list of pairs containing, at each position, a pair of the element from the
+     * first list at that index and the element from the second list at that index.
+     * Both lists must be the same size. They may contain null.
+     *
+     * The easiest way to visualize what's happening is to think of two lists being laid out next
+     * to each other and stitched together with a zipper.
+     *
+     * Contract : this method will read each element of each list exactly once, in some unspecified
+     * order. If it throws, it will not read any element.
+     *
+     * @param first the first list of elements
+     * @param second the second list of elements
+     * @param <T> the type of first elements
+     * @param <R> the type of second elements
+     * @return the zipped list
+     */
+    @NonNull
+    public static <T, R> ArrayList<Pair<T, R>> zip(@NonNull final List<T> first,
+            @NonNull final List<R> second) {
+        final int size = first.size();
+        if (size != second.size()) {
+            throw new IllegalArgumentException("zip : collections must be the same size");
+        }
+        final ArrayList<Pair<T, R>> dest = new ArrayList<>(size);
+        for (int i = 0; i < size; ++i) {
+            dest.add(new Pair<>(first.get(i), second.get(i)));
+        }
+        return dest;
+    }
+
+    /**
+     * Returns a new ArrayMap that associates each key with the value at the same index.
+     *
+     * Both lists must be the same size.
+     * Both keys and values may contain null.
+     * Keys may not contain the same value twice.
+     *
+     * Contract : this method will read each element of each list exactly once, but does not
+     * specify the order, except if it throws in which case the number of reads is undefined.
+     *
+     * @param keys The list of keys
+     * @param values The list of values
+     * @param <T> The type of keys
+     * @param <R> The type of values
+     * @return The associated map
+     */
+    @NonNull
+    public static <T, R> ArrayMap<T, R> assoc(
+            @NonNull final List<T> keys, @NonNull final List<R> values) {
+        final int size = keys.size();
+        if (size != values.size()) {
+            throw new IllegalArgumentException("assoc : collections must be the same size");
+        }
+        final ArrayMap<T, R> dest = new ArrayMap<>(size);
+        for (int i = 0; i < size; ++i) {
+            final T key = keys.get(i);
+            if (dest.containsKey(key)) {
+                throw new IllegalArgumentException(
+                        "assoc : keys may not contain the same value twice");
+            }
+            dest.put(key, values.get(i));
+        }
+        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/ConnectivitySettingsUtils.java b/staticlibs/framework/com/android/net/module/util/ConnectivitySettingsUtils.java
new file mode 100644
index 0000000..f4856b3
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/ConnectivitySettingsUtils.java
@@ -0,0 +1,131 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+/**
+ * Collection of connectivity settings utilities.
+ *
+ * @hide
+ */
+public class ConnectivitySettingsUtils {
+    public static final int PRIVATE_DNS_MODE_OFF = 1;
+    public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC = 2;
+    public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3;
+
+    public static final String PRIVATE_DNS_DEFAULT_MODE = "private_dns_default_mode";
+    public static final String PRIVATE_DNS_MODE = "private_dns_mode";
+    public static final String PRIVATE_DNS_MODE_OFF_STRING = "off";
+    public static final String PRIVATE_DNS_MODE_OPPORTUNISTIC_STRING = "opportunistic";
+    public static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME_STRING = "hostname";
+    public static final String PRIVATE_DNS_SPECIFIER = "private_dns_specifier";
+
+    /**
+     * Get private DNS mode as string.
+     *
+     * @param mode One of the private DNS values.
+     * @return A string of private DNS mode.
+     */
+    public static String getPrivateDnsModeAsString(int mode) {
+        switch (mode) {
+            case PRIVATE_DNS_MODE_OFF:
+                return PRIVATE_DNS_MODE_OFF_STRING;
+            case PRIVATE_DNS_MODE_OPPORTUNISTIC:
+                return PRIVATE_DNS_MODE_OPPORTUNISTIC_STRING;
+            case PRIVATE_DNS_MODE_PROVIDER_HOSTNAME:
+                return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME_STRING;
+            default:
+                throw new IllegalArgumentException("Invalid private dns mode: " + mode);
+        }
+    }
+
+    private static int getPrivateDnsModeAsInt(String mode) {
+        // If both PRIVATE_DNS_MODE and PRIVATE_DNS_DEFAULT_MODE are not set, choose
+        // PRIVATE_DNS_MODE_OPPORTUNISTIC as default mode.
+        if (TextUtils.isEmpty(mode))
+            return PRIVATE_DNS_MODE_OPPORTUNISTIC;
+        switch (mode) {
+            case "off":
+                return PRIVATE_DNS_MODE_OFF;
+            case "hostname":
+                return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+            case "opportunistic":
+                return PRIVATE_DNS_MODE_OPPORTUNISTIC;
+            default:
+                // b/260211513: adb shell settings put global private_dns_mode foo
+                // can result in arbitrary strings - treat any unknown value as empty string.
+                // throw new IllegalArgumentException("Invalid private dns mode: " + mode);
+                return PRIVATE_DNS_MODE_OPPORTUNISTIC;
+        }
+    }
+
+    /**
+     * Get private DNS mode from settings.
+     *
+     * @param context The Context to query the private DNS mode from settings.
+     * @return An integer of private DNS mode.
+     */
+    public static int getPrivateDnsMode(@NonNull Context context) {
+        final ContentResolver cr = context.getContentResolver();
+        String mode = Settings.Global.getString(cr, PRIVATE_DNS_MODE);
+        if (TextUtils.isEmpty(mode)) mode = Settings.Global.getString(cr, PRIVATE_DNS_DEFAULT_MODE);
+        return getPrivateDnsModeAsInt(mode);
+    }
+
+    /**
+     * Set private DNS mode to settings.
+     *
+     * @param context The {@link Context} to set the private DNS mode.
+     * @param mode The private dns mode. This should be one of the PRIVATE_DNS_MODE_* constants.
+     */
+    public static void setPrivateDnsMode(@NonNull Context context, int mode) {
+        if (!(mode == PRIVATE_DNS_MODE_OFF
+                || mode == PRIVATE_DNS_MODE_OPPORTUNISTIC
+                || mode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME)) {
+            throw new IllegalArgumentException("Invalid private dns mode: " + mode);
+        }
+        Settings.Global.putString(context.getContentResolver(), PRIVATE_DNS_MODE,
+                getPrivateDnsModeAsString(mode));
+    }
+
+    /**
+     * Get specific private dns provider name from {@link Settings}.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @return The specific private dns provider name, or null if no setting value.
+     */
+    @Nullable
+    public static String getPrivateDnsHostname(@NonNull Context context) {
+        return Settings.Global.getString(context.getContentResolver(), PRIVATE_DNS_SPECIFIER);
+    }
+
+    /**
+     * Set specific private dns provider name to {@link Settings}.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param specifier The specific private dns provider name.
+     */
+    public static void setPrivateDnsHostname(@NonNull Context context, @Nullable String specifier) {
+        Settings.Global.putString(context.getContentResolver(), PRIVATE_DNS_SPECIFIER, specifier);
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/ConnectivityUtils.java b/staticlibs/framework/com/android/net/module/util/ConnectivityUtils.java
new file mode 100644
index 0000000..c135e46
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/ConnectivityUtils.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+
+import android.annotation.Nullable;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+/**
+ * Various utilities used in connectivity code.
+ * @hide
+ */
+public final class ConnectivityUtils {
+    private ConnectivityUtils() {}
+
+
+    /**
+     * Return IP address and port in a string format.
+     */
+    public static String addressAndPortToString(InetAddress address, int port) {
+        return String.format(
+                (address instanceof Inet6Address) ? "[%s]:%d" : "%s:%d",
+                        address.getHostAddress(), port);
+    }
+
+    /**
+     * Return true if the provided address is non-null and an IPv6 Unique Local Address (RFC4193).
+     */
+    public static boolean isIPv6ULA(@Nullable InetAddress addr) {
+        return addr instanceof Inet6Address
+                && ((addr.getAddress()[0] & 0xfe) == 0xfc);
+    }
+
+    /**
+     * Returns the {@code int} nearest in value to {@code value}.
+     *
+     * @param value any {@code long} value
+     * @return the same value cast to {@code int} if it is in the range of the {@code int}
+     * type, {@link Integer#MAX_VALUE} if it is too large, or {@link Integer#MIN_VALUE} if
+     * it is too small
+     */
+    public static int saturatedCast(long value) {
+        if (value > Integer.MAX_VALUE) {
+            return Integer.MAX_VALUE;
+        }
+        if (value < Integer.MIN_VALUE) {
+            return Integer.MIN_VALUE;
+        }
+        return (int) value;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsPacket.java b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
new file mode 100644
index 0000000..63106a1
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
@@ -0,0 +1,602 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.net.module.util.DnsPacketUtils.DnsRecordParser.domainNameToLabels;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.DnsPacketUtils.DnsRecordParser;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Defines basic data for DNS protocol based on RFC 1035.
+ * Subclasses create the specific format used in DNS packet.
+ *
+ * @hide
+ */
+public abstract class DnsPacket {
+    /**
+     * Type of the canonical name for an alias. Refer to RFC 1035 section 3.2.2.
+     */
+    // TODO: Define the constant as a public constant in DnsResolver since it can never change.
+    private static final int TYPE_CNAME = 5;
+    public static final int TYPE_SVCB = 64;
+
+    /**
+     * Thrown when parsing packet failed.
+     */
+    public static class ParseException extends RuntimeException {
+        public String reason;
+        public ParseException(@NonNull String reason) {
+            super(reason);
+            this.reason = reason;
+        }
+
+        public ParseException(@NonNull String reason, @NonNull Throwable cause) {
+            super(reason, cause);
+            this.reason = reason;
+        }
+    }
+
+    /**
+     * DNS header for DNS protocol based on RFC 1035 section 4.1.1.
+     *
+     *                                     1  1  1  1  1  1
+     *       0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                      ID                       |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                    QDCOUNT                    |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                    ANCOUNT                    |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                    NSCOUNT                    |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                    ARCOUNT                    |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     */
+    public static class DnsHeader {
+        private static final String TAG = "DnsHeader";
+        private static final int SIZE_IN_BYTES = 12;
+        private final int mId;
+        private final int mFlags;
+        private final int[] mRecordCount;
+
+        /* If this bit in the 'flags' field is set to 0, the DNS message corresponding to this
+         * header is a query; otherwise, it is a response.
+         */
+        private static final int FLAGS_SECTION_QR_BIT = 15;
+
+        /**
+         * Create a new DnsHeader from a positioned ByteBuffer.
+         *
+         * The ByteBuffer must be in network byte order (which is the default).
+         * Reads the passed ByteBuffer from its current position and decodes a DNS header.
+         * When this constructor returns, the reading position of the ByteBuffer has been
+         * advanced to the end of the DNS header record.
+         * This is meant to chain with other methods reading a DNS response in sequence.
+         */
+        @VisibleForTesting
+        public DnsHeader(@NonNull ByteBuffer buf) throws BufferUnderflowException {
+            Objects.requireNonNull(buf);
+            mId = Short.toUnsignedInt(buf.getShort());
+            mFlags = Short.toUnsignedInt(buf.getShort());
+            mRecordCount = new int[NUM_SECTIONS];
+            for (int i = 0; i < NUM_SECTIONS; ++i) {
+                mRecordCount[i] = Short.toUnsignedInt(buf.getShort());
+            }
+        }
+
+        /**
+         * Determines if the DNS message corresponding to this header is a response, as defined in
+         * RFC 1035 Section 4.1.1.
+         */
+        public boolean isResponse() {
+            return (mFlags & (1 << FLAGS_SECTION_QR_BIT)) != 0;
+        }
+
+        /**
+         * Create a new DnsHeader from specified parameters.
+         *
+         * This constructor only builds the question and answer sections. Authority
+         * and additional sections are not supported. Useful when synthesizing dns
+         * responses from query or reply packets.
+         */
+        @VisibleForTesting
+        public DnsHeader(int id, int flags, int qdcount, int ancount) {
+            this.mId = id;
+            this.mFlags = flags;
+            mRecordCount = new int[NUM_SECTIONS];
+            mRecordCount[QDSECTION] = qdcount;
+            mRecordCount[ANSECTION] = ancount;
+        }
+
+        /**
+         * Get record count by type.
+         */
+        public int getRecordCount(int type) {
+            return mRecordCount[type];
+        }
+
+        /**
+         * Get flags of this instance.
+         */
+        public int getFlags() {
+            return mFlags;
+        }
+
+        /**
+         * Get id of this instance.
+         */
+        public int getId() {
+            return mId;
+        }
+
+        @Override
+        public String toString() {
+            return "DnsHeader{" + "id=" + mId + ", flags=" + mFlags
+                    + ", recordCounts=" + Arrays.toString(mRecordCount) + '}';
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o.getClass() != getClass()) return false;
+            final DnsHeader other = (DnsHeader) o;
+            return mId == other.mId
+                    && mFlags == other.mFlags
+                    && Arrays.equals(mRecordCount, other.mRecordCount);
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 * mId + 37 * mFlags + Arrays.hashCode(mRecordCount);
+        }
+
+        /**
+         * Get DnsHeader as byte array.
+         */
+        @NonNull
+        public byte[] getBytes() {
+            // TODO: if this is called often, optimize the ByteBuffer out and write to the
+            //  array directly.
+            final ByteBuffer buf = ByteBuffer.allocate(SIZE_IN_BYTES);
+            buf.putShort((short) mId);
+            buf.putShort((short) mFlags);
+            for (int i = 0; i < NUM_SECTIONS; ++i) {
+                buf.putShort((short) mRecordCount[i]);
+            }
+            return buf.array();
+        }
+    }
+
+    /**
+     * Superclass for DNS questions and DNS resource records.
+     *
+     * DNS questions (No TTL/RDLENGTH/RDATA) based on RFC 1035 section 4.1.2.
+     *                                     1  1  1  1  1  1
+     *       0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                                               |
+     *     /                     QNAME                     /
+     *     /                                               /
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                     QTYPE                     |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                     QCLASS                    |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *
+     * DNS resource records (With TTL/RDLENGTH/RDATA) based on RFC 1035 section 4.1.3.
+     *                                     1  1  1  1  1  1
+     *       0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                                               |
+     *     /                                               /
+     *     /                      NAME                     /
+     *     |                                               |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                      TYPE                     |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                     CLASS                     |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                      TTL                      |
+     *     |                                               |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *     |                   RDLENGTH                    |
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
+     *     /                     RDATA                     /
+     *     /                                               /
+     *     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+     *
+     * Note that this class is meant to be used by composition and not inheritance, and
+     * that classes implementing more specific DNS records should call #parse.
+     */
+    // TODO: Make DnsResourceRecord and DnsQuestion subclasses of DnsRecord.
+    public static class DnsRecord {
+        // Refer to RFC 1035 section 2.3.4 for MAXNAMESIZE.
+        // NAME_NORMAL and NAME_COMPRESSION are used for checking name compression,
+        // refer to rfc 1035 section 4.1.4.
+        public static final int MAXNAMESIZE = 255;
+        public static final int NAME_NORMAL = 0;
+        public static final int NAME_COMPRESSION = 0xC0;
+
+        private static final String TAG = "DnsRecord";
+
+        public final String dName;
+        public final int nsType;
+        public final int nsClass;
+        public final long ttl;
+        private final byte[] mRdata;
+        /**
+         * Type of this DNS record.
+         */
+        @RecordType
+        public final int rType;
+
+        /**
+         * Create a new DnsRecord from a positioned ByteBuffer.
+         *
+         * Reads the passed ByteBuffer from its current position and decodes a DNS record.
+         * When this constructor returns, the reading position of the ByteBuffer has been
+         * advanced to the end of the DNS resource record.
+         * This is meant to chain with other methods reading a DNS response in sequence.
+         *
+         * @param rType Type of the record.
+         * @param buf ByteBuffer input of record, must be in network byte order
+         *         (which is the default).
+         */
+        protected DnsRecord(@RecordType int rType, @NonNull ByteBuffer buf)
+                throws BufferUnderflowException, ParseException {
+            Objects.requireNonNull(buf);
+            this.rType = rType;
+            dName = DnsRecordParser.parseName(buf, 0 /* Parse depth */,
+                    true /* isNameCompressionSupported */);
+            if (dName.length() > MAXNAMESIZE) {
+                throw new ParseException(
+                        "Parse name fail, name size is too long: " + dName.length());
+            }
+            nsType = Short.toUnsignedInt(buf.getShort());
+            nsClass = Short.toUnsignedInt(buf.getShort());
+
+            if (rType != QDSECTION) {
+                ttl = Integer.toUnsignedLong(buf.getInt());
+                final int length = Short.toUnsignedInt(buf.getShort());
+                mRdata = new byte[length];
+                buf.get(mRdata);
+            } else {
+                ttl = 0;
+                mRdata = null;
+            }
+        }
+
+        /**
+         * Create a new DnsRecord or subclass of DnsRecord instance from a positioned ByteBuffer.
+         *
+         * Peek the nsType, sending the buffer to corresponding DnsRecord subclass constructors
+         * to allow constructing the corresponding object.
+         */
+        @VisibleForTesting(visibility = PRIVATE)
+        public static DnsRecord parse(@RecordType int rType, @NonNull ByteBuffer buf)
+                throws BufferUnderflowException, ParseException {
+            Objects.requireNonNull(buf);
+            final int oldPos = buf.position();
+            // Parsed name not used, just for jumping to nsType position.
+            DnsRecordParser.parseName(buf, 0 /* Parse depth */,
+                    true /* isNameCompressionSupported */);
+            // Peek the nsType.
+            final int nsType = Short.toUnsignedInt(buf.getShort());
+            buf.position(oldPos);
+            // Return a DnsRecord instance by default for backward compatibility, this is useful
+            // when a partner supports new type of DnsRecord but does not inherit DnsRecord.
+            switch (nsType) {
+                case TYPE_SVCB:
+                    return new DnsSvcbRecord(rType, buf);
+                default:
+                    return new DnsRecord(rType, buf);
+            }
+        }
+
+        /**
+         * Make an A or AAAA record based on the specified parameters.
+         *
+         * @param rType Type of the record, can be {@link #ANSECTION}, {@link #ARSECTION}
+         *              or {@link #NSSECTION}.
+         * @param dName Domain name of the record.
+         * @param nsClass Class of the record. See RFC 1035 section 3.2.4.
+         * @param ttl time interval (in seconds) that the resource record may be
+         *            cached before it should be discarded. Zero values are
+         *            interpreted to mean that the RR can only be used for the
+         *            transaction in progress, and should not be cached.
+         * @param address Instance of {@link InetAddress}
+         * @return A record if the {@code address} is an IPv4 address, or AAAA record if the
+         *         {@code address} is an IPv6 address.
+         */
+        public static DnsRecord makeAOrAAAARecord(int rType, @NonNull String dName,
+                int nsClass, long ttl, @NonNull InetAddress address) throws IOException {
+            final int nsType = (address.getAddress().length == 4) ? TYPE_A : TYPE_AAAA;
+            return new DnsRecord(rType, dName, nsType, nsClass, ttl, address, null /* rDataStr */);
+        }
+
+        /**
+         * Make an CNAME record based on the specified parameters.
+         *
+         * @param rType Type of the record, can be {@link #ANSECTION}, {@link #ARSECTION}
+         *              or {@link #NSSECTION}.
+         * @param dName Domain name of the record.
+         * @param nsClass Class of the record. See RFC 1035 section 3.2.4.
+         * @param ttl time interval (in seconds) that the resource record may be
+         *            cached before it should be discarded. Zero values are
+         *            interpreted to mean that the RR can only be used for the
+         *            transaction in progress, and should not be cached.
+         * @param domainName Canonical name of the {@code dName}.
+         * @return A record if the {@code address} is an IPv4 address, or AAAA record if the
+         *         {@code address} is an IPv6 address.
+         */
+        public static DnsRecord makeCNameRecord(int rType, @NonNull String dName, int nsClass,
+                long ttl, @NonNull String domainName) throws IOException {
+            return new DnsRecord(rType, dName, TYPE_CNAME, nsClass, ttl, null /* address */,
+                    domainName);
+        }
+
+        /**
+         * Make a DNS question based on the specified parameters.
+         */
+        public static DnsRecord makeQuestion(@NonNull String dName, int nsType, int nsClass) {
+            return new DnsRecord(dName, nsType, nsClass);
+        }
+
+        private static String requireHostName(@NonNull String name) {
+            if (!DnsRecordParser.isHostName(name)) {
+                throw new IllegalArgumentException("Expected domain name but got " + name);
+            }
+            return name;
+        }
+
+        /**
+         * Create a new query DnsRecord from specified parameters, useful when synthesizing
+         * dns response.
+         */
+        private DnsRecord(@NonNull String dName, int nsType, int nsClass) {
+            this.rType = QDSECTION;
+            this.dName = requireHostName(dName);
+            this.nsType = nsType;
+            this.nsClass = nsClass;
+            mRdata = null;
+            this.ttl = 0;
+        }
+
+        /**
+         * Create a new CNAME/A/AAAA DnsRecord from specified parameters.
+         *
+         * @param address The address only used when synthesizing A or AAAA record.
+         * @param rDataStr The alias of the domain, only used when synthesizing CNAME record.
+         */
+        private DnsRecord(@RecordType int rType, @NonNull String dName, int nsType, int nsClass,
+                long ttl, @Nullable InetAddress address, @Nullable String rDataStr)
+                throws IOException {
+            this.rType = rType;
+            this.dName = requireHostName(dName);
+            this.nsType = nsType;
+            this.nsClass = nsClass;
+            if (rType < 0 || rType >= NUM_SECTIONS || rType == QDSECTION) {
+                throw new IllegalArgumentException("Unexpected record type: " + rType);
+            }
+            mRdata = nsType == TYPE_CNAME ? domainNameToLabels(rDataStr) : address.getAddress();
+            this.ttl = ttl;
+        }
+
+        /**
+         * Get a copy of rdata.
+         */
+        @Nullable
+        public byte[] getRR() {
+            return (mRdata == null) ? null : mRdata.clone();
+        }
+
+        /**
+         * Get DnsRecord as byte array.
+         */
+        @NonNull
+        public byte[] getBytes() throws IOException {
+            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            final DataOutputStream dos = new DataOutputStream(baos);
+            dos.write(domainNameToLabels(dName));
+            dos.writeShort(nsType);
+            dos.writeShort(nsClass);
+            if (rType != QDSECTION) {
+                dos.writeInt((int) ttl);
+                if (mRdata == null) {
+                    dos.writeShort(0);
+                } else {
+                    dos.writeShort(mRdata.length);
+                    dos.write(mRdata);
+                }
+            }
+            return baos.toByteArray();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o.getClass() != getClass()) return false;
+            final DnsRecord other = (DnsRecord) o;
+            return rType == other.rType
+                    && nsType == other.nsType
+                    && nsClass == other.nsClass
+                    && ttl == other.ttl
+                    && TextUtils.equals(dName, other.dName)
+                    && Arrays.equals(mRdata, other.mRdata);
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 * Objects.hash(dName)
+                    + 37 * ((int) (ttl & 0xFFFFFFFF))
+                    + 41 * ((int) (ttl >> 32))
+                    + 43 * nsType
+                    + 47 * nsClass
+                    + 53 * rType
+                    + Arrays.hashCode(mRdata);
+        }
+
+        @Override
+        public String toString() {
+            return "DnsRecord{"
+                    + "rType=" + rType
+                    + ", dName='" + dName + '\''
+                    + ", nsType=" + nsType
+                    + ", nsClass=" + nsClass
+                    + ", ttl=" + ttl
+                    + ", mRdata=" + Arrays.toString(mRdata)
+                    + '}';
+        }
+    }
+
+    /**
+     * Header section types, refer to RFC 1035 section 4.1.1.
+     */
+    public static final int QDSECTION = 0;
+    public static final int ANSECTION = 1;
+    public static final int NSSECTION = 2;
+    public static final int ARSECTION = 3;
+    @VisibleForTesting(visibility = PRIVATE)
+    static final int NUM_SECTIONS = ARSECTION + 1;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            QDSECTION,
+            ANSECTION,
+            NSSECTION,
+            ARSECTION,
+    })
+    public @interface RecordType {}
+
+
+    private static final String TAG = DnsPacket.class.getSimpleName();
+
+    protected final DnsHeader mHeader;
+    protected final List<DnsRecord>[] mRecords;
+
+    protected DnsPacket(@NonNull byte[] data) throws ParseException {
+        if (null == data) {
+            throw new ParseException("Parse header failed, null input data");
+        }
+
+        final ByteBuffer buffer;
+        try {
+            buffer = ByteBuffer.wrap(data);
+            mHeader = new DnsHeader(buffer);
+        } catch (BufferUnderflowException e) {
+            throw new ParseException("Parse Header fail, bad input data", e);
+        }
+
+        mRecords = new ArrayList[NUM_SECTIONS];
+
+        for (int i = 0; i < NUM_SECTIONS; ++i) {
+            final int count = mHeader.getRecordCount(i);
+            mRecords[i] = new ArrayList(count);
+            for (int j = 0; j < count; ++j) {
+                try {
+                    mRecords[i].add(DnsRecord.parse(i, buffer));
+                } catch (BufferUnderflowException e) {
+                    throw new ParseException("Parse record fail", e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Create a new {@link #DnsPacket} from specified parameters.
+     *
+     * Note that authority records section and additional records section is not supported.
+     */
+    protected DnsPacket(@NonNull DnsHeader header, @NonNull List<DnsRecord> qd,
+            @NonNull List<DnsRecord> an) {
+        mHeader = Objects.requireNonNull(header);
+        mRecords = new List[NUM_SECTIONS];
+        mRecords[QDSECTION] = Collections.unmodifiableList(new ArrayList<>(qd));
+        mRecords[ANSECTION] = Collections.unmodifiableList(new ArrayList<>(an));
+        mRecords[NSSECTION] = new ArrayList<>();
+        mRecords[ARSECTION] = new ArrayList<>();
+        for (int i = 0; i < NUM_SECTIONS; i++) {
+            if (mHeader.mRecordCount[i] != mRecords[i].size()) {
+                throw new IllegalArgumentException("Record count mismatch: expected "
+                        + mHeader.mRecordCount[i] + " but was " + mRecords[i]);
+            }
+        }
+    }
+
+    /**
+     * Get DnsPacket as byte array.
+     */
+    public @NonNull byte[] getBytes() throws IOException {
+        final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+        buf.write(mHeader.getBytes());
+
+        for (int i = 0; i < NUM_SECTIONS; ++i) {
+            for (final DnsRecord record : mRecords[i]) {
+                buf.write(record.getBytes());
+            }
+        }
+        return buf.toByteArray();
+    }
+
+    @Override
+    public String toString() {
+        return "DnsPacket{" + "header=" + mHeader + ", records='" + Arrays.toString(mRecords) + '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o.getClass() != getClass()) return false;
+        final DnsPacket other = (DnsPacket) o;
+        return Objects.equals(mHeader, other.mHeader)
+                && Arrays.deepEquals(mRecords, other.mRecords);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = Objects.hash(mHeader);
+        result = 31 * result + Arrays.hashCode(mRecords);
+        return result;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java b/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
new file mode 100644
index 0000000..105d783
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static com.android.net.module.util.DnsPacket.DnsRecord.NAME_COMPRESSION;
+import static com.android.net.module.util.DnsPacket.DnsRecord.NAME_NORMAL;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.InetAddresses;
+import android.net.ParseException;
+import android.text.TextUtils;
+import android.util.Patterns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+
+/**
+ * Utilities for decoding the contents of a DnsPacket.
+ *
+ * @hide
+ */
+public final class DnsPacketUtils {
+    /**
+     * Reads the passed ByteBuffer from its current position and decodes a DNS record.
+     */
+    public static class DnsRecordParser {
+        private static final int MAXLABELSIZE = 63;
+        private static final int MAXNAMESIZE = 255;
+        private static final int MAXLABELCOUNT = 128;
+
+        private static final DecimalFormat sByteFormat = new DecimalFormat();
+        private static final FieldPosition sPos = new FieldPosition(0);
+
+        /**
+         * Convert label from {@code byte[]} to {@code String}
+         *
+         * <p>Follows the same conversion rules of the native code (ns_name.c in libc).
+         */
+        @VisibleForTesting
+        static String labelToString(@NonNull byte[] label) {
+            final StringBuffer sb = new StringBuffer();
+
+            for (int i = 0; i < label.length; ++i) {
+                int b = Byte.toUnsignedInt(label[i]);
+                // Control characters and non-ASCII characters.
+                if (b <= 0x20 || b >= 0x7f) {
+                    // Append the byte as an escaped decimal number, e.g., "\19" for 0x13.
+                    sb.append('\\');
+                    sByteFormat.format(b, sb, sPos);
+                } else if (b == '"' || b == '.' || b == ';' || b == '\\' || b == '(' || b == ')'
+                        || b == '@' || b == '$') {
+                    // Append the byte as an escaped character, e.g., "\:" for 0x3a.
+                    sb.append('\\');
+                    sb.append((char) b);
+                } else {
+                    // Append the byte as a character, e.g., "a" for 0x61.
+                    sb.append((char) b);
+                }
+            }
+            return sb.toString();
+        }
+
+        /**
+         * Converts domain name to labels according to RFC 1035.
+         *
+         * @param name Domain name as String that needs to be converted to labels.
+         * @return An encoded byte array that is constructed out of labels,
+         *         and ends with zero-length label.
+         * @throws ParseException if failed to parse the given domain name or
+         *         IOException if failed to output labels.
+         */
+        public static @NonNull byte[] domainNameToLabels(@NonNull String name) throws
+                IOException, ParseException {
+            if (name.length() > MAXNAMESIZE) {
+                throw new ParseException("Domain name exceeds max length: " + name.length());
+            }
+            if (!isHostName(name)) {
+                throw new ParseException("Failed to parse domain name: " + name);
+            }
+            final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+            final String[] labels = name.split("\\.");
+            for (final String label : labels) {
+                if (label.length() > MAXLABELSIZE) {
+                    throw new ParseException("label is too long: " + label);
+                }
+                buf.write(label.length());
+                // Encode as UTF-8 as suggested in RFC 6055 section 3.
+                buf.write(label.getBytes(StandardCharsets.UTF_8));
+            }
+            buf.write(0x00); // end with zero-length label
+            return buf.toByteArray();
+        }
+
+        /**
+         * Check whether the input is a valid hostname based on rfc 1035 section 3.3.
+         *
+         * @param hostName the target host name.
+         * @return true if the input is a valid hostname.
+         */
+        public static boolean isHostName(@Nullable String hostName) {
+            // TODO: Use {@code Patterns.HOST_NAME} if available.
+            // Patterns.DOMAIN_NAME accepts host names or IP addresses, so reject
+            // IP addresses.
+            return hostName != null
+                    && Patterns.DOMAIN_NAME.matcher(hostName).matches()
+                    && !InetAddresses.isNumericAddress(hostName);
+        }
+
+        /**
+         * Parses the domain / target name of a DNS record.
+         */
+        public static String parseName(final ByteBuffer buf, int depth,
+                boolean isNameCompressionSupported) throws
+                BufferUnderflowException, DnsPacket.ParseException {
+            return parseName(buf, depth, MAXLABELCOUNT, isNameCompressionSupported);
+        }
+
+        /**
+         * Parses the domain / target name of a DNS record.
+         *
+         * As described in RFC 1035 Section 4.1.3, the NAME field of a DNS Resource Record always
+         * supports Name Compression, whereas domain names contained in the RDATA payload of a DNS
+         * record may or may not support Name Compression, depending on the record TYPE. Moreover,
+         * even if Name Compression is supported, its usage is left to the implementation.
+         */
+        public static String parseName(final ByteBuffer buf, int depth, int maxLabelCount,
+                boolean isNameCompressionSupported) throws
+                BufferUnderflowException, DnsPacket.ParseException {
+            if (depth > maxLabelCount) {
+                throw new DnsPacket.ParseException("Failed to parse name, too many labels");
+            }
+            final int len = Byte.toUnsignedInt(buf.get());
+            final int mask = len & NAME_COMPRESSION;
+            if (0 == len) {
+                return "";
+            } else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION
+                    || (!isNameCompressionSupported && mask == NAME_COMPRESSION)) {
+                throw new DnsPacket.ParseException("Parse name fail, bad label type: " + mask);
+            } else if (mask == NAME_COMPRESSION) {
+                // Name compression based on RFC 1035 - 4.1.4 Message compression
+                final int offset = ((len & ~NAME_COMPRESSION) << 8) + Byte.toUnsignedInt(buf.get());
+                final int oldPos = buf.position();
+                if (offset >= oldPos - 2) {
+                    throw new DnsPacket.ParseException(
+                            "Parse compression name fail, invalid compression");
+                }
+                buf.position(offset);
+                final String pointed = parseName(buf, depth + 1, maxLabelCount,
+                        isNameCompressionSupported);
+                buf.position(oldPos);
+                return pointed;
+            } else {
+                final byte[] label = new byte[len];
+                buf.get(label);
+                final String head = labelToString(label);
+                if (head.length() > MAXLABELSIZE) {
+                    throw new DnsPacket.ParseException("Parse name fail, invalid label length");
+                }
+                final String tail = parseName(buf, depth + 1, maxLabelCount,
+                        isNameCompressionSupported);
+                return TextUtils.isEmpty(tail) ? head : head + "." + tail;
+            }
+        }
+
+        private DnsRecordParser() {}
+    }
+
+    private DnsPacketUtils() {}
+}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSdTxtRecord.java b/staticlibs/framework/com/android/net/module/util/DnsSdTxtRecord.java
new file mode 100644
index 0000000..760891b
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsSdTxtRecord.java
@@ -0,0 +1,325 @@
+/* -*- Mode: Java; tab-width: 4 -*-
+ *
+ * Copyright (c) 2004 Apple Computer, 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.
+
+ To do:
+ - implement remove()
+ - fix set() to replace existing values
+ */
+
+package com.android.net.module.util;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import java.util.Arrays;
+
+/**
+ * This class handles TXT record data for DNS based service discovery as specified at
+ * http://tools.ietf.org/html/draft-cheshire-dnsext-dns-sd-11
+ *
+ * DNS-SD specifies that a TXT record corresponding to an SRV record consist of
+ * a packed array of bytes, each preceded by a length byte. Each string
+ * is an attribute-value pair.
+ *
+ * The DnsSdTxtRecord object stores the entire TXT data as a single byte array, traversing it
+ * as need be to implement its various methods.
+ * @hide
+ *
+ */
+public class DnsSdTxtRecord implements Parcelable {
+    private static final byte mSeparator = '=';
+
+    private byte[] mData;
+
+    /** Constructs a new, empty TXT record. */
+    public DnsSdTxtRecord()  {
+        mData = new byte[0];
+    }
+
+    /** Constructs a new TXT record from a byte array in the standard format. */
+    public DnsSdTxtRecord(byte[] data) {
+        mData = (byte[]) data.clone();
+    }
+
+    /** Copy constructor */
+    public DnsSdTxtRecord(DnsSdTxtRecord src) {
+        if (src != null && src.mData != null) {
+            mData = (byte[]) src.mData.clone();
+        }
+    }
+
+    /**
+     * Set a key/value pair. Setting an existing key will replace its value.
+     * @param key Must be ascii with no '='
+     * @param value matching value to key
+     */
+    public void set(String key, String value) {
+        byte[] keyBytes;
+        byte[] valBytes;
+        int valLen;
+
+        if (value != null) {
+            valBytes = value.getBytes();
+            valLen = valBytes.length;
+        } else {
+            valBytes = null;
+            valLen = 0;
+        }
+
+        try {
+            keyBytes = key.getBytes("US-ASCII");
+        }
+        catch (java.io.UnsupportedEncodingException e) {
+            throw new IllegalArgumentException("key should be US-ASCII");
+        }
+
+        for (int i = 0; i < keyBytes.length; i++) {
+            if (keyBytes[i] == '=') {
+                throw new IllegalArgumentException("= is not a valid character in key");
+            }
+        }
+
+        if (keyBytes.length + valLen >= 255) {
+            throw new IllegalArgumentException("Key and Value length cannot exceed 255 bytes");
+        }
+
+        int currentLoc = remove(key);
+        if (currentLoc == -1)
+            currentLoc = keyCount();
+
+        insert(keyBytes, valBytes, currentLoc);
+    }
+
+    /**
+     * Get a value for a key
+     *
+     * @param key
+     * @return The value associated with the key
+     */
+    public String get(String key) {
+        byte[] val = this.getValue(key);
+        return val != null ? new String(val) : null;
+    }
+
+    /** Remove a key/value pair. If found, returns the index or -1 if not found */
+    public int remove(String key) {
+        int avStart = 0;
+
+        for (int i=0; avStart < mData.length; i++) {
+            int avLen = mData[avStart];
+            if (key.length() <= avLen &&
+                    (key.length() == avLen || mData[avStart + key.length() + 1] == mSeparator)) {
+                String s = new String(mData, avStart + 1, key.length());
+                if (0 == key.compareToIgnoreCase(s)) {
+                    byte[] oldBytes = mData;
+                    mData = new byte[oldBytes.length - avLen - 1];
+                    System.arraycopy(oldBytes, 0, mData, 0, avStart);
+                    System.arraycopy(oldBytes, avStart + avLen + 1, mData, avStart,
+                            oldBytes.length - avStart - avLen - 1);
+                    return i;
+                }
+            }
+            avStart += (0xFF & (avLen + 1));
+        }
+        return -1;
+    }
+
+    /** Return the count of keys */
+    public int keyCount() {
+        int count = 0, nextKey;
+        for (nextKey = 0; nextKey < mData.length; count++) {
+            nextKey += (0xFF & (mData[nextKey] + 1));
+        }
+        return count;
+    }
+
+    /** Return true if key is present, false if not. */
+    public boolean contains(String key) {
+        String s = null;
+        for (int i = 0; null != (s = this.getKey(i)); i++) {
+            if (0 == key.compareToIgnoreCase(s)) return true;
+        }
+        return false;
+    }
+
+    /* Gets the size in bytes */
+    public int size() {
+        return mData.length;
+    }
+
+    /* Gets the raw data in bytes */
+    public byte[] getRawData() {
+        return (byte[]) mData.clone();
+    }
+
+    private void insert(byte[] keyBytes, byte[] value, int index) {
+        byte[] oldBytes = mData;
+        int valLen = (value != null) ? value.length : 0;
+        int insertion = 0;
+        int newLen, avLen;
+
+        for (int i = 0; i < index && insertion < mData.length; i++) {
+            insertion += (0xFF & (mData[insertion] + 1));
+        }
+
+        avLen = keyBytes.length + valLen + (value != null ? 1 : 0);
+        newLen = avLen + oldBytes.length + 1;
+
+        mData = new byte[newLen];
+        System.arraycopy(oldBytes, 0, mData, 0, insertion);
+        int secondHalfLen = oldBytes.length - insertion;
+        System.arraycopy(oldBytes, insertion, mData, newLen - secondHalfLen, secondHalfLen);
+        mData[insertion] = (byte) avLen;
+        System.arraycopy(keyBytes, 0, mData, insertion + 1, keyBytes.length);
+        if (value != null) {
+            mData[insertion + 1 + keyBytes.length] = mSeparator;
+            System.arraycopy(value, 0, mData, insertion + keyBytes.length + 2, valLen);
+        }
+    }
+
+    /** Return a key in the TXT record by zero-based index. Returns null if index exceeds the total number of keys. */
+    private String getKey(int index) {
+        int avStart = 0;
+
+        for (int i=0; i < index && avStart < mData.length; i++) {
+            avStart += mData[avStart] + 1;
+        }
+
+        if (avStart < mData.length) {
+            int avLen = mData[avStart];
+            int aLen = 0;
+
+            for (aLen=0; aLen < avLen; aLen++) {
+                if (mData[avStart + aLen + 1] == mSeparator) break;
+            }
+            return new String(mData, avStart + 1, aLen);
+        }
+        return null;
+    }
+
+    /**
+     * Look up a key in the TXT record by zero-based index and return its value.
+     * Returns null if index exceeds the total number of keys.
+     * Returns null if the key is present with no value.
+     */
+    private byte[] getValue(int index) {
+        int avStart = 0;
+        byte[] value = null;
+
+        for (int i=0; i < index && avStart < mData.length; i++) {
+            avStart += mData[avStart] + 1;
+        }
+
+        if (avStart < mData.length) {
+            int avLen = mData[avStart];
+            int aLen = 0;
+
+            for (aLen=0; aLen < avLen; aLen++) {
+                if (mData[avStart + aLen + 1] == mSeparator) {
+                    value = new byte[avLen - aLen - 1];
+                    System.arraycopy(mData, avStart + aLen + 2, value, 0, avLen - aLen - 1);
+                    break;
+                }
+            }
+        }
+        return value;
+    }
+
+    private String getValueAsString(int index) {
+        byte[] value = this.getValue(index);
+        return value != null ? new String(value) : null;
+    }
+
+    private byte[] getValue(String forKey) {
+        String s = null;
+        int i;
+
+        for (i = 0; null != (s = this.getKey(i)); i++) {
+            if (0 == forKey.compareToIgnoreCase(s)) {
+                return this.getValue(i);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Return a string representation.
+     * Example : {key1=value1},{key2=value2}..
+     *
+     * For a key say like "key3" with null value
+     * {key1=value1},{key2=value2}{key3}
+     */
+    public String toString() {
+        String a, result = null;
+
+        for (int i = 0; null != (a = this.getKey(i)); i++) {
+            String av =  "{" + a;
+            String val = this.getValueAsString(i);
+            if (val != null)
+                av += "=" + val + "}";
+            else
+                av += "}";
+            if (result == null)
+                result = av;
+            else
+                result = result + ", " + av;
+        }
+        return result != null ? result : "";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof DnsSdTxtRecord)) {
+            return false;
+        }
+
+        DnsSdTxtRecord record = (DnsSdTxtRecord)o;
+        return  Arrays.equals(record.mData, mData);
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(mData);
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(mData);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<DnsSdTxtRecord> CREATOR =
+        new Creator<DnsSdTxtRecord>() {
+            public DnsSdTxtRecord createFromParcel(Parcel in) {
+                DnsSdTxtRecord info = new DnsSdTxtRecord();
+                in.readByteArray(info.mData);
+                return info;
+            }
+
+            public DnsSdTxtRecord[] newArray(int size) {
+                return new DnsSdTxtRecord[size];
+            }
+        };
+}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
new file mode 100644
index 0000000..d298599
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
@@ -0,0 +1,170 @@
+/*
+ * 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.net.module.util;
+
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A class for a DNS SVCB response packet.
+ *
+ * @hide
+ */
+public class DnsSvcbPacket extends DnsPacket {
+    public static final int TYPE_SVCB = 64;
+
+    private static final String TAG = DnsSvcbPacket.class.getSimpleName();
+
+    /**
+     * Creates a DnsSvcbPacket object from the given wire-format DNS packet.
+     */
+    private DnsSvcbPacket(@NonNull byte[] data) throws DnsPacket.ParseException {
+        // If data is null, ParseException will be thrown.
+        super(data);
+
+        final int questions = mHeader.getRecordCount(QDSECTION);
+        if (questions != 1) {
+            throw new DnsPacket.ParseException("Unexpected question count " + questions);
+        }
+        final int nsType = mRecords[QDSECTION].get(0).nsType;
+        if (nsType != TYPE_SVCB) {
+            throw new DnsPacket.ParseException("Unexpected query type " + nsType);
+        }
+    }
+
+    /**
+     * Returns true if the DnsSvcbPacket is a DNS response.
+     */
+    public boolean isResponse() {
+        return mHeader.isResponse();
+    }
+
+    /**
+     * Returns whether the given protocol alpn is supported.
+     */
+    public boolean isSupported(@NonNull String alpn) {
+        return findSvcbRecord(alpn) != null;
+    }
+
+    /**
+     * Returns the TargetName associated with the given protocol alpn.
+     * If the alpn is not supported, a null is returned.
+     */
+    @Nullable
+    public String getTargetName(@NonNull String alpn) {
+        final DnsSvcbRecord record = findSvcbRecord(alpn);
+        return (record != null) ? record.getTargetName() : null;
+    }
+
+    /**
+     * Returns the TargetName that associated with the given protocol alpn.
+     * If the alpn is not supported, -1 is returned.
+     */
+    public int getPort(@NonNull String alpn) {
+        final DnsSvcbRecord record = findSvcbRecord(alpn);
+        return (record != null) ? record.getPort() : -1;
+    }
+
+    /**
+     * Returns the IP addresses that support the given protocol alpn.
+     * If the alpn is not supported, an empty list is returned.
+     */
+    @NonNull
+    public List<InetAddress> getAddresses(@NonNull String alpn) {
+        final DnsSvcbRecord record = findSvcbRecord(alpn);
+        if (record == null) return Collections.EMPTY_LIST;
+
+        // As per draft-ietf-dnsop-svcb-https-10#section-7.4 and draft-ietf-add-ddr-10#section-4,
+        // if A/AAAA records are available in the Additional section, use the IP addresses in
+        // those records instead of the IP addresses in ipv4hint/ipv6hint.
+        final List<InetAddress> out = getAddressesFromAdditionalSection();
+        if (out.size() > 0) return out;
+
+        return record.getAddresses();
+    }
+
+    /**
+     * Returns the value of SVCB key dohpath that associated with the given protocol alpn.
+     * If the alpn is not supported, a null is returned.
+     */
+    @Nullable
+    public String getDohPath(@NonNull String alpn) {
+        final DnsSvcbRecord record = findSvcbRecord(alpn);
+        return (record != null) ? record.getDohPath() : null;
+    }
+
+    /**
+     * Returns the DnsSvcbRecord associated with the given protocol alpn.
+     * If the alpn is not supported, a null is returned.
+     */
+    @Nullable
+    private DnsSvcbRecord findSvcbRecord(@NonNull String alpn) {
+        for (final DnsRecord record : mRecords[ANSECTION]) {
+            if (record instanceof DnsSvcbRecord) {
+                final DnsSvcbRecord svcbRecord = (DnsSvcbRecord) record;
+                if (svcbRecord.getAlpns().contains(alpn)) {
+                    return svcbRecord;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the IP addresses in additional section.
+     */
+    @NonNull
+    private List<InetAddress> getAddressesFromAdditionalSection() {
+        final List<InetAddress> out = new ArrayList<InetAddress>();
+        if (mHeader.getRecordCount(ARSECTION) == 0) {
+            return out;
+        }
+        for (final DnsRecord record : mRecords[ARSECTION]) {
+            if (record.nsType != TYPE_A && record.nsType != TYPE_AAAA) {
+                Log.d(TAG, "Found type other than A/AAAA in Additional section: " + record.nsType);
+                continue;
+            }
+            try {
+                out.add(InetAddress.getByAddress(record.getRR()));
+            } catch (UnknownHostException e) {
+                Log.w(TAG, "Failed to parse address");
+            }
+        }
+        return out;
+    }
+
+    /**
+     * Creates a DnsSvcbPacket object from the given wire-format DNS answer.
+     */
+    public static DnsSvcbPacket fromResponse(@NonNull byte[] data) throws DnsPacket.ParseException {
+        DnsSvcbPacket out = new DnsSvcbPacket(data);
+        if (!out.isResponse()) {
+            throw new DnsPacket.ParseException("Not an answer packet");
+        }
+        return out;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
new file mode 100644
index 0000000..935cdf6
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
@@ -0,0 +1,539 @@
+/*
+ * 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.net.module.util;
+
+import static android.net.DnsResolver.CLASS_IN;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.net.module.util.DnsPacket.ParseException;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * A class for an SVCB record.
+ * https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml
+ * @hide
+ */
+@VisibleForTesting(visibility = PACKAGE)
+public final class DnsSvcbRecord extends DnsPacket.DnsRecord {
+    /**
+     * The following SvcParamKeys KEY_* are defined in
+     * https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml.
+     */
+
+    // The SvcParamKey "mandatory". The associated implementation of SvcParam is SvcParamMandatory.
+    private static final int KEY_MANDATORY = 0;
+
+    // The SvcParamKey "alpn". The associated implementation of SvcParam is SvcParamAlpn.
+    private static final int KEY_ALPN = 1;
+
+    // The SvcParamKey "no-default-alpn". The associated implementation of SvcParam is
+    // SvcParamNoDefaultAlpn.
+    private static final int KEY_NO_DEFAULT_ALPN = 2;
+
+    // The SvcParamKey "port". The associated implementation of SvcParam is SvcParamPort.
+    private static final int KEY_PORT = 3;
+
+    // The SvcParamKey "ipv4hint". The associated implementation of SvcParam is SvcParamIpv4Hint.
+    private static final int KEY_IPV4HINT = 4;
+
+    // The SvcParamKey "ech". The associated implementation of SvcParam is SvcParamEch.
+    private static final int KEY_ECH = 5;
+
+    // The SvcParamKey "ipv6hint". The associated implementation of SvcParam is SvcParamIpv6Hint.
+    private static final int KEY_IPV6HINT = 6;
+
+    // The SvcParamKey "dohpath". The associated implementation of SvcParam is SvcParamDohPath.
+    private static final int KEY_DOHPATH = 7;
+
+    // The minimal size of a SvcParam.
+    // https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-12.html#name-rdata-wire-format
+    private static final int MINSVCPARAMSIZE = 4;
+
+    private static final String TAG = DnsSvcbRecord.class.getSimpleName();
+
+    private final int mSvcPriority;
+
+    @NonNull
+    private final String mTargetName;
+
+    @NonNull
+    private final SparseArray<SvcParam> mAllSvcParams = new SparseArray<>();
+
+    @VisibleForTesting(visibility = PACKAGE)
+    public DnsSvcbRecord(@DnsPacket.RecordType int rType, @NonNull ByteBuffer buff)
+            throws IllegalStateException, ParseException {
+        super(rType, buff);
+        if (nsType != DnsPacket.TYPE_SVCB) {
+            throw new IllegalStateException("incorrect nsType: " + nsType);
+        }
+        if (nsClass != CLASS_IN) {
+            throw new ParseException("incorrect nsClass: " + nsClass);
+        }
+
+        // DNS Record in Question Section doesn't have Rdata.
+        if (rType == DnsPacket.QDSECTION) {
+            mSvcPriority = 0;
+            mTargetName = "";
+            return;
+        }
+
+        final byte[] rdata = getRR();
+        if (rdata == null) {
+            throw new ParseException("SVCB rdata is empty");
+        }
+
+        final ByteBuffer buf = ByteBuffer.wrap(rdata).asReadOnlyBuffer();
+        mSvcPriority = Short.toUnsignedInt(buf.getShort());
+        mTargetName = DnsPacketUtils.DnsRecordParser.parseName(buf, 0 /* Parse depth */,
+                false /* isNameCompressionSupported */);
+
+        if (mTargetName.length() > DnsPacket.DnsRecord.MAXNAMESIZE) {
+            throw new ParseException(
+                    "Failed to parse SVCB target name, name size is too long: "
+                            + mTargetName.length());
+        }
+        while (buf.remaining() >= MINSVCPARAMSIZE) {
+            final SvcParam svcParam = parseSvcParam(buf);
+            final int key = svcParam.getKey();
+            if (mAllSvcParams.get(key) != null) {
+                throw new ParseException("Invalid DnsSvcbRecord, key " + key + " is repeated");
+            }
+            mAllSvcParams.put(key, svcParam);
+        }
+        if (buf.hasRemaining()) {
+            throw new ParseException("Invalid DnsSvcbRecord. Got "
+                    + buf.remaining() + " remaining bytes after parsing");
+        }
+    }
+
+    /**
+     * Returns the TargetName.
+     */
+    @VisibleForTesting(visibility = PACKAGE)
+    @NonNull
+    public String getTargetName() {
+        return mTargetName;
+    }
+
+    /**
+     * Returns an unmodifiable list of alpns from SvcParam alpn.
+     */
+    @VisibleForTesting(visibility = PACKAGE)
+    @NonNull
+    public List<String> getAlpns() {
+        final SvcParamAlpn sp = (SvcParamAlpn) mAllSvcParams.get(KEY_ALPN);
+        final List<String> list = (sp != null) ? sp.getValue() : Collections.EMPTY_LIST;
+        return Collections.unmodifiableList(list);
+    }
+
+    /**
+     * Returns the port number from SvcParam port.
+     */
+    @VisibleForTesting(visibility = PACKAGE)
+    public int getPort() {
+        final SvcParamPort sp = (SvcParamPort) mAllSvcParams.get(KEY_PORT);
+        return (sp != null) ? sp.getValue() : -1;
+    }
+
+    /**
+     * Returns a list of the IP addresses from both of SvcParam ipv4hint and ipv6hint.
+     */
+    @VisibleForTesting(visibility = PACKAGE)
+    @NonNull
+    public List<InetAddress> getAddresses() {
+        final List<InetAddress> out = new ArrayList<>();
+        final SvcParamIpHint sp4 = (SvcParamIpHint) mAllSvcParams.get(KEY_IPV4HINT);
+        if (sp4 != null) {
+            out.addAll(sp4.getValue());
+        }
+        final SvcParamIpHint sp6 = (SvcParamIpHint) mAllSvcParams.get(KEY_IPV6HINT);
+        if (sp6 != null) {
+            out.addAll(sp6.getValue());
+        }
+        return out;
+    }
+
+    /**
+     * Returns the doh path from SvcParam dohPath.
+     */
+    @VisibleForTesting(visibility = PACKAGE)
+    @NonNull
+    public String getDohPath() {
+        final SvcParamDohPath sp = (SvcParamDohPath) mAllSvcParams.get(KEY_DOHPATH);
+        return (sp != null) ? sp.getValue() : "";
+    }
+
+    @Override
+    public String toString() {
+        if (rType == DnsPacket.QDSECTION) {
+            return dName + " IN SVCB";
+        }
+
+        final StringJoiner sj = new StringJoiner(" ");
+        for (int i = 0; i < mAllSvcParams.size(); i++) {
+            sj.add(mAllSvcParams.valueAt(i).toString());
+        }
+        return dName + " " + ttl + " IN SVCB " + mSvcPriority + " " + mTargetName + " "
+                + sj.toString();
+    }
+
+    private static SvcParam parseSvcParam(@NonNull ByteBuffer buf) throws ParseException {
+        try {
+            final int key = Short.toUnsignedInt(buf.getShort());
+            switch (key) {
+                case KEY_MANDATORY: return new SvcParamMandatory(buf);
+                case KEY_ALPN: return new SvcParamAlpn(buf);
+                case KEY_NO_DEFAULT_ALPN: return new SvcParamNoDefaultAlpn(buf);
+                case KEY_PORT: return new SvcParamPort(buf);
+                case KEY_IPV4HINT: return new SvcParamIpv4Hint(buf);
+                case KEY_ECH: return new SvcParamEch(buf);
+                case KEY_IPV6HINT: return new SvcParamIpv6Hint(buf);
+                case KEY_DOHPATH: return new SvcParamDohPath(buf);
+                default: return new SvcParamGeneric(key, buf);
+            }
+        } catch (BufferUnderflowException e) {
+            throw new ParseException("Malformed packet", e);
+        }
+    }
+
+    /**
+     * The base class for all SvcParam.
+     */
+    private abstract static class SvcParam<T> {
+        private final int mKey;
+
+        SvcParam(int key) {
+            mKey = key;
+        }
+
+        int getKey() {
+            return mKey;
+        }
+
+        abstract T getValue();
+    }
+
+    private static class SvcParamMandatory extends SvcParam<short[]> {
+        private final short[] mValue;
+
+        private SvcParamMandatory(@NonNull ByteBuffer buf) throws BufferUnderflowException,
+                ParseException {
+            super(KEY_MANDATORY);
+            // The caller already read 2 bytes for SvcParamKey.
+            final int len = Short.toUnsignedInt(buf.getShort());
+            final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
+            mValue = SvcParamValueUtil.toShortArray(svcParamValue);
+            if (mValue.length == 0) {
+                throw new ParseException("mandatory value must be non-empty");
+            }
+        }
+
+        @Override
+        short[] getValue() {
+            /* Not yet implemented */
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            final StringJoiner valueJoiner = new StringJoiner(",");
+            for (short key : mValue) {
+                valueJoiner.add(toKeyName(key));
+            }
+            return toKeyName(getKey()) + "=" + valueJoiner.toString();
+        }
+    }
+
+    private static class SvcParamAlpn extends SvcParam<List<String>> {
+        private final List<String> mValue;
+
+        SvcParamAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+            super(KEY_ALPN);
+            // The caller already read 2 bytes for SvcParamKey.
+            final int len = Short.toUnsignedInt(buf.getShort());
+            final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
+            mValue = SvcParamValueUtil.toStringList(svcParamValue);
+            if (mValue.isEmpty()) {
+                throw new ParseException("alpn value must be non-empty");
+            }
+        }
+
+        @Override
+        List<String> getValue() {
+            return Collections.unmodifiableList(mValue);
+        }
+
+        @Override
+        public String toString() {
+            return toKeyName(getKey()) + "=" + TextUtils.join(",", mValue);
+        }
+    }
+
+    private static class SvcParamNoDefaultAlpn extends SvcParam<Void> {
+        SvcParamNoDefaultAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException,
+                ParseException {
+            super(KEY_NO_DEFAULT_ALPN);
+            // The caller already read 2 bytes for SvcParamKey.
+            final int len = buf.getShort();
+            if (len != 0) {
+                throw new ParseException("no-default-alpn value must be empty");
+            }
+        }
+
+        @Override
+        Void getValue() {
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            return toKeyName(getKey());
+        }
+    }
+
+    private static class SvcParamPort extends SvcParam<Integer> {
+        private final int mValue;
+
+        SvcParamPort(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+            super(KEY_PORT);
+            // The caller already read 2 bytes for SvcParamKey.
+            final int len = buf.getShort();
+            if (len != Short.BYTES) {
+                throw new ParseException("key port len is not 2 but " + len);
+            }
+            mValue = Short.toUnsignedInt(buf.getShort());
+        }
+
+        @Override
+        Integer getValue() {
+            return mValue;
+        }
+
+        @Override
+        public String toString() {
+            return toKeyName(getKey()) + "=" + mValue;
+        }
+    }
+
+    private static class SvcParamIpHint extends SvcParam<List<InetAddress>> {
+        private final List<InetAddress> mValue;
+
+        private SvcParamIpHint(int key, @NonNull ByteBuffer buf, int addrLen) throws
+                BufferUnderflowException, ParseException {
+            super(key);
+            // The caller already read 2 bytes for SvcParamKey.
+            final int len = Short.toUnsignedInt(buf.getShort());
+            final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
+            mValue = SvcParamValueUtil.toInetAddressList(svcParamValue, addrLen);
+            if (mValue.isEmpty()) {
+                throw new ParseException(toKeyName(getKey()) + " value must be non-empty");
+            }
+        }
+
+        @Override
+        List<InetAddress> getValue() {
+            return Collections.unmodifiableList(mValue);
+        }
+
+        @Override
+        public String toString() {
+            final StringJoiner valueJoiner = new StringJoiner(",");
+            for (InetAddress ip : mValue) {
+                valueJoiner.add(ip.getHostAddress());
+            }
+            return toKeyName(getKey()) + "=" + valueJoiner.toString();
+        }
+    }
+
+    private static class SvcParamIpv4Hint extends SvcParamIpHint {
+        SvcParamIpv4Hint(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+            super(KEY_IPV4HINT, buf, NetworkStackConstants.IPV4_ADDR_LEN);
+        }
+    }
+
+    private static class SvcParamIpv6Hint extends SvcParamIpHint {
+        SvcParamIpv6Hint(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+            super(KEY_IPV6HINT, buf, NetworkStackConstants.IPV6_ADDR_LEN);
+        }
+    }
+
+    private static class SvcParamEch extends SvcParamGeneric {
+        SvcParamEch(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+            super(KEY_ECH, buf);
+        }
+    }
+
+    private static class SvcParamDohPath extends SvcParam<String> {
+        private final String mValue;
+
+        SvcParamDohPath(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+            super(KEY_DOHPATH);
+            // The caller already read 2 bytes for SvcParamKey.
+            final int len = Short.toUnsignedInt(buf.getShort());
+            final byte[] value = new byte[len];
+            buf.get(value);
+            mValue = new String(value, StandardCharsets.UTF_8);
+        }
+
+        @Override
+        String getValue() {
+            return mValue;
+        }
+
+        @Override
+        public String toString() {
+            return toKeyName(getKey()) + "=" + mValue;
+        }
+    }
+
+    // For other unrecognized and unimplemented SvcParams, they are stored as SvcParamGeneric.
+    private static class SvcParamGeneric extends SvcParam<byte[]> {
+        private final byte[] mValue;
+
+        SvcParamGeneric(int key, @NonNull ByteBuffer buf) throws BufferUnderflowException,
+                ParseException {
+            super(key);
+            // The caller already read 2 bytes for SvcParamKey.
+            final int len = Short.toUnsignedInt(buf.getShort());
+            mValue = new byte[len];
+            buf.get(mValue);
+        }
+
+        @Override
+        byte[] getValue() {
+            /* Not yet implemented */
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder out = new StringBuilder();
+            out.append(toKeyName(getKey()));
+            if (mValue != null && mValue.length > 0) {
+                out.append("=");
+                out.append(HexDump.toHexString(mValue));
+            }
+            return out.toString();
+        }
+    }
+
+    private static String toKeyName(int key) {
+        switch (key) {
+            case KEY_MANDATORY: return "mandatory";
+            case KEY_ALPN: return "alpn";
+            case KEY_NO_DEFAULT_ALPN: return "no-default-alpn";
+            case KEY_PORT: return "port";
+            case KEY_IPV4HINT: return "ipv4hint";
+            case KEY_ECH: return "ech";
+            case KEY_IPV6HINT: return "ipv6hint";
+            case KEY_DOHPATH: return "dohpath";
+            default: return "key" + key;
+        }
+    }
+
+    /**
+     * Returns a read-only ByteBuffer (with position = 0, limit = `length`, and capacity = `length`)
+     * sliced from `buf`'s current position, and moves the position of `buf` by `length`.
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    public static ByteBuffer sliceAndAdvance(@NonNull ByteBuffer buf, int length)
+            throws BufferUnderflowException {
+        if (buf.remaining() < length) {
+            throw new BufferUnderflowException();
+        }
+        final int pos = buf.position();
+
+        // `out` equals to `buf.slice(pos, length)` that is supported in API level 34.
+        final ByteBuffer out = ((ByteBuffer) buf.slice().limit(length)).slice();
+
+        buf.position(pos + length);
+        return out.asReadOnlyBuffer();
+    }
+
+    // A utility to convert the byte array of SvcParamValue to other types.
+    private static class SvcParamValueUtil {
+        // Refer to draft-ietf-dnsop-svcb-https-10#section-7.1 for the wire format of alpn.
+        @NonNull
+        private static List<String> toStringList(@NonNull ByteBuffer buf)
+                throws BufferUnderflowException, ParseException {
+            final List<String> out = new ArrayList<>();
+            while (buf.hasRemaining()) {
+                final int alpnLen = Byte.toUnsignedInt(buf.get());
+                if (alpnLen == 0) {
+                    throw new ParseException("alpn should not be an empty string");
+                }
+                final byte[] alpn = new byte[alpnLen];
+                buf.get(alpn);
+                out.add(new String(alpn, StandardCharsets.UTF_8));
+            }
+            return out;
+        }
+
+        // Refer to draft-ietf-dnsop-svcb-https-10#section-7.5 for the wire format of SvcParamKey
+        // "mandatory".
+        @NonNull
+        private static short[] toShortArray(@NonNull ByteBuffer buf)
+                throws BufferUnderflowException, ParseException {
+            if (buf.remaining() % Short.BYTES != 0) {
+                throw new ParseException("Can't parse whole byte array");
+            }
+            final ShortBuffer sb = buf.asShortBuffer();
+            final short[] out = new short[sb.remaining()];
+            sb.get(out);
+            return out;
+        }
+
+        // Refer to draft-ietf-dnsop-svcb-https-10#section-7.4 for the wire format of ipv4hint and
+        // ipv6hint.
+        @NonNull
+        private static List<InetAddress> toInetAddressList(@NonNull ByteBuffer buf, int addrLen)
+                throws BufferUnderflowException, ParseException {
+            if (buf.remaining() % addrLen != 0) {
+                throw new ParseException("Can't parse whole byte array");
+            }
+
+            final List<InetAddress> out = new ArrayList<>();
+            final byte[] addr = new byte[addrLen];
+            while (buf.remaining() >= addrLen) {
+                buf.get(addr);
+                try {
+                    out.add(InetAddress.getByAddress(addr));
+                } catch (UnknownHostException e) {
+                    throw new ParseException("Can't parse byte array as an IP address");
+                }
+            }
+            return out;
+        }
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/HexDump.java b/staticlibs/framework/com/android/net/module/util/HexDump.java
new file mode 100644
index 0000000..a22c258
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/HexDump.java
@@ -0,0 +1,238 @@
+/*
+ * 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.net.module.util;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Hex utility functions.
+ *
+ * @hide
+ */
+public class HexDump {
+    private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+            'A', 'B', 'C', 'D', 'E', 'F' };
+    private static final char[] HEX_LOWER_CASE_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7',
+            '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+    /**
+     * Dump the hex string corresponding to the specified byte array.
+     *
+     * @param array byte array to be dumped.
+     */
+    public static String dumpHexString(@Nullable byte[] array) {
+        if (array == null) return "(null)";
+        return dumpHexString(array, 0, array.length);
+    }
+
+    /**
+     * Dump the hex string corresponding to the specified byte array.
+     *
+     * @param array byte array to be dumped.
+     * @param offset the offset in array where dump should start.
+     * @param length the length of bytes to be dumped.
+     */
+    public static String dumpHexString(@Nullable byte[] array, int offset, int length) {
+        if (array == null) return "(null)";
+        StringBuilder result = new StringBuilder();
+
+        byte[] line = new byte[16];
+        int lineIndex = 0;
+
+        result.append("\n0x");
+        result.append(toHexString(offset));
+
+        for (int i = offset; i < offset + length; i++) {
+            if (lineIndex == 16) {
+                result.append(" ");
+
+                for (int j = 0; j < 16; j++) {
+                    if (line[j] > ' ' && line[j] < '~') {
+                        result.append(new String(line, j, 1));
+                    } else {
+                        result.append(".");
+                    }
+                }
+
+                result.append("\n0x");
+                result.append(toHexString(i));
+                lineIndex = 0;
+            }
+
+            byte b = array[i];
+            result.append(" ");
+            result.append(HEX_DIGITS[(b >>> 4) & 0x0F]);
+            result.append(HEX_DIGITS[b & 0x0F]);
+
+            line[lineIndex++] = b;
+        }
+
+        if (lineIndex != 16) {
+            int count = (16 - lineIndex) * 3;
+            count++;
+            for (int i = 0; i < count; i++) {
+                result.append(" ");
+            }
+
+            for (int i = 0; i < lineIndex; i++) {
+                if (line[i] > ' ' && line[i] < '~') {
+                    result.append(new String(line, i, 1));
+                } else {
+                    result.append(".");
+                }
+            }
+        }
+
+        return result.toString();
+    }
+
+    /**
+     * Convert a byte to an uppercase hex string.
+     *
+     * @param b the byte to be converted.
+     */
+    public static String toHexString(byte b) {
+        return toHexString(toByteArray(b));
+    }
+
+    /**
+     * Convert a byte array to an uppercase hex string.
+     *
+     * @param array the byte array to be converted.
+     */
+    public static String toHexString(byte[] array) {
+        return toHexString(array, 0, array.length, true);
+    }
+
+    /**
+     * Convert a byte array to a hex string.
+     *
+     * @param array the byte array to be converted.
+     * @param upperCase whether the converted hex string should be uppercase or not.
+     */
+    public static String toHexString(byte[] array, boolean upperCase) {
+        return toHexString(array, 0, array.length, upperCase);
+    }
+
+    /**
+     * Convert a byte array to hex string.
+     *
+     * @param array the byte array to be converted.
+     * @param offset the offset in array where conversion should start.
+     * @param length the length of bytes to be converted.
+     */
+    public static String toHexString(byte[] array, int offset, int length) {
+        return toHexString(array, offset, length, true);
+    }
+
+    /**
+     * Convert a byte array to hex string.
+     *
+     * @param array the byte array to be converted.
+     * @param offset the offset in array where conversion should start.
+     * @param length the length of bytes to be converted.
+     * @param upperCase whether the converted hex string should be uppercase or not.
+     */
+    public static String toHexString(byte[] array, int offset, int length, boolean upperCase) {
+        char[] digits = upperCase ? HEX_DIGITS : HEX_LOWER_CASE_DIGITS;
+        char[] buf = new char[length * 2];
+
+        int bufIndex = 0;
+        for (int i = offset; i < offset + length; i++) {
+            byte b = array[i];
+            buf[bufIndex++] = digits[(b >>> 4) & 0x0F];
+            buf[bufIndex++] = digits[b & 0x0F];
+        }
+
+        return new String(buf);
+    }
+
+    /**
+     * Convert an integer to hex string.
+     *
+     * @param i the integer to be converted.
+     */
+    public static String toHexString(int i) {
+        return toHexString(toByteArray(i));
+    }
+
+    /**
+     * Convert a byte to byte array.
+     *
+     * @param b the byte to be converted.
+     */
+    public static byte[] toByteArray(byte b) {
+        byte[] array = new byte[1];
+        array[0] = b;
+        return array;
+    }
+
+    /**
+     * Convert an integer to byte array.
+     *
+     * @param i the integer to be converted.
+     */
+    public static byte[] toByteArray(int i) {
+        byte[] array = new byte[4];
+
+        array[3] = (byte) (i & 0xFF);
+        array[2] = (byte) ((i >> 8) & 0xFF);
+        array[1] = (byte) ((i >> 16) & 0xFF);
+        array[0] = (byte) ((i >> 24) & 0xFF);
+
+        return array;
+    }
+
+    private static int toByte(char c) {
+        if (c >= '0' && c <= '9') return (c - '0');
+        if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
+        if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
+
+        throw new RuntimeException("Invalid hex char '" + c + "'");
+    }
+
+    /**
+     * Convert a hex string to a byte array.
+     *
+     * @param hexString the string to be converted.
+     */
+    public static byte[] hexStringToByteArray(String hexString) {
+        int length = hexString.length();
+        byte[] buffer = new byte[length / 2];
+
+        for (int i = 0; i < length; i += 2) {
+            buffer[i / 2] =
+                    (byte) ((toByte(hexString.charAt(i)) << 4) | toByte(hexString.charAt(i + 1)));
+        }
+
+        return buffer;
+    }
+
+    /**
+     * Convert a byte to hex string and append it to StringBuilder.
+     *
+     * @param sb StringBuilder instance.
+     * @param b the byte to be converted.
+     * @param upperCase whether the converted hex string should be uppercase or not.
+     */
+    public static StringBuilder appendByteAsHex(StringBuilder sb, byte b, boolean upperCase) {
+        char[] digits = upperCase ? HEX_DIGITS : HEX_LOWER_CASE_DIGITS;
+        sb.append(digits[(b >> 4) & 0xf]);
+        sb.append(digits[b & 0xf]);
+        return sb;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/Inet4AddressUtils.java b/staticlibs/framework/com/android/net/module/util/Inet4AddressUtils.java
new file mode 100644
index 0000000..87f43d5
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/Inet4AddressUtils.java
@@ -0,0 +1,192 @@
+/*
+ * 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 java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Collection of utilities to work with IPv4 addresses.
+ * @hide
+ */
+public class Inet4AddressUtils {
+
+    /**
+     * Convert a IPv4 address from an integer to an InetAddress (0x04030201 -> 1.2.3.4)
+     *
+     * <p>This method uses the higher-order int bytes as the lower-order IPv4 address bytes,
+     * which is an unusual convention. Consider {@link #intToInet4AddressHTH(int)} instead.
+     * @param hostAddress an int coding for an IPv4 address, where higher-order int byte is
+     *                    lower-order IPv4 address byte
+     */
+    public static Inet4Address intToInet4AddressHTL(int hostAddress) {
+        return intToInet4AddressHTH(Integer.reverseBytes(hostAddress));
+    }
+
+    /**
+     * Convert a IPv4 address from an integer to an InetAddress (0x01020304 -> 1.2.3.4)
+     * @param hostAddress an int coding for an IPv4 address
+     */
+    public static Inet4Address intToInet4AddressHTH(int hostAddress) {
+        byte[] addressBytes = { (byte) (0xff & (hostAddress >> 24)),
+                (byte) (0xff & (hostAddress >> 16)),
+                (byte) (0xff & (hostAddress >> 8)),
+                (byte) (0xff & hostAddress) };
+
+        try {
+            return (Inet4Address) InetAddress.getByAddress(addressBytes);
+        } catch (UnknownHostException e) {
+            throw new AssertionError();
+        }
+    }
+
+    /**
+     * Convert an IPv4 address from an InetAddress to an integer (1.2.3.4 -> 0x01020304)
+     *
+     * <p>This conversion can help order IP addresses: considering the ordering
+     * 192.0.2.1 < 192.0.2.2 < ..., resulting ints will follow that ordering if read as unsigned
+     * integers with {@link Integer#toUnsignedLong}.
+     * @param inetAddr is an InetAddress corresponding to the IPv4 address
+     * @return the IP address as integer
+     */
+    public static int inet4AddressToIntHTH(Inet4Address inetAddr)
+            throws IllegalArgumentException {
+        byte [] addr = inetAddr.getAddress();
+        return ((addr[0] & 0xff) << 24) | ((addr[1] & 0xff) << 16)
+                | ((addr[2] & 0xff) << 8) | (addr[3] & 0xff);
+    }
+
+    /**
+     * Convert a IPv4 address from an InetAddress to an integer (1.2.3.4 -> 0x04030201)
+     *
+     * <p>This method stores the higher-order IPv4 address bytes in the lower-order int bytes,
+     * which is an unusual convention. Consider {@link #inet4AddressToIntHTH(Inet4Address)} instead.
+     * @param inetAddr is an InetAddress corresponding to the IPv4 address
+     * @return the IP address as integer
+     */
+    public static int inet4AddressToIntHTL(Inet4Address inetAddr) {
+        return Integer.reverseBytes(inet4AddressToIntHTH(inetAddr));
+    }
+
+    /**
+     * Convert a network prefix length to an IPv4 netmask integer (prefixLength 17 -> 0xffff8000)
+     * @return the IPv4 netmask as an integer
+     */
+    public static int prefixLengthToV4NetmaskIntHTH(int prefixLength)
+            throws IllegalArgumentException {
+        if (prefixLength < 0 || prefixLength > 32) {
+            throw new IllegalArgumentException("Invalid prefix length (0 <= prefix <= 32)");
+        }
+        // (int)a << b is equivalent to a << (b & 0x1f): can't shift by 32 (-1 << 32 == -1)
+        return prefixLength == 0 ? 0 : 0xffffffff << (32 - prefixLength);
+    }
+
+    /**
+     * Convert a network prefix length to an IPv4 netmask integer (prefixLength 17 -> 0x0080ffff).
+     *
+     * <p>This method stores the higher-order IPv4 address bytes in the lower-order int bytes,
+     * which is an unusual convention. Consider {@link #prefixLengthToV4NetmaskIntHTH(int)} instead.
+     * @return the IPv4 netmask as an integer
+     */
+    public static int prefixLengthToV4NetmaskIntHTL(int prefixLength)
+            throws IllegalArgumentException {
+        return Integer.reverseBytes(prefixLengthToV4NetmaskIntHTH(prefixLength));
+    }
+
+    /**
+     * Convert an IPv4 netmask to a prefix length, checking that the netmask is contiguous.
+     * @param netmask as a {@code Inet4Address}.
+     * @return the network prefix length
+     * @throws IllegalArgumentException the specified netmask was not contiguous.
+     * @hide
+     */
+    public static int netmaskToPrefixLength(Inet4Address netmask) {
+        // inetAddressToInt returns an int in *network* byte order.
+        int i = inet4AddressToIntHTH(netmask);
+        int prefixLength = Integer.bitCount(i);
+        int trailingZeros = Integer.numberOfTrailingZeros(i);
+        if (trailingZeros != 32 - prefixLength) {
+            throw new IllegalArgumentException("Non-contiguous netmask: " + Integer.toHexString(i));
+        }
+        return prefixLength;
+    }
+
+    /**
+     * Returns the implicit netmask of an IPv4 address, as was the custom before 1993.
+     */
+    public static int getImplicitNetmask(Inet4Address address) {
+        int firstByte = address.getAddress()[0] & 0xff;  // Convert to an unsigned value.
+        if (firstByte < 128) {
+            return 8;
+        } else if (firstByte < 192) {
+            return 16;
+        } else if (firstByte < 224) {
+            return 24;
+        } else {
+            return 32;  // Will likely not end well for other reasons.
+        }
+    }
+
+    /**
+     * Get the broadcast address for a given prefix.
+     *
+     * <p>For example 192.168.0.1/24 -> 192.168.0.255
+     */
+    public static Inet4Address getBroadcastAddress(Inet4Address addr, int prefixLength)
+            throws IllegalArgumentException {
+        final int intBroadcastAddr = inet4AddressToIntHTH(addr)
+                | ~prefixLengthToV4NetmaskIntHTH(prefixLength);
+        return intToInet4AddressHTH(intBroadcastAddr);
+    }
+
+    /**
+     * Get a prefix mask as Inet4Address for a given prefix length.
+     *
+     * <p>For example 20 -> 255.255.240.0
+     */
+    public static Inet4Address getPrefixMaskAsInet4Address(int prefixLength)
+            throws IllegalArgumentException {
+        return intToInet4AddressHTH(prefixLengthToV4NetmaskIntHTH(prefixLength));
+    }
+
+    /**
+     * Trim leading zeros from IPv4 address strings
+     * Non-v4 addresses and host names remain unchanged.
+     * For example, 192.168.000.010 -> 192.168.0.10
+     * @param addr a string representing an ip address
+     * @return a string properly trimmed
+     */
+    public static String trimAddressZeros(String addr) {
+        if (addr == null) return null;
+        String[] octets = addr.split("\\.");
+        if (octets.length != 4) return addr;
+        StringBuilder builder = new StringBuilder(16);
+        String result = null;
+        for (int i = 0; i < 4; i++) {
+            try {
+                if (octets[i].length() > 3) return addr;
+                builder.append(Integer.parseInt(octets[i]));
+            } catch (NumberFormatException e) {
+                return addr;
+            }
+            if (i < 3) builder.append('.');
+        }
+        result = builder.toString();
+        return result;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
new file mode 100644
index 0000000..4b27a97
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.util.Log;
+
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Collection of utilities to interact with {@link InetAddress}
+ * @hide
+ */
+public class InetAddressUtils {
+
+    private static final String TAG = InetAddressUtils.class.getSimpleName();
+    private static final int INET4_ADDR_LENGTH = 4;
+    private static final int INET6_ADDR_LENGTH = 16;
+
+    /**
+     * Writes an InetAddress to a parcel. The address may be null. This is likely faster than
+     * calling writeSerializable.
+     * @hide
+     */
+    public static void parcelInetAddress(Parcel parcel, InetAddress address, int flags) {
+        byte[] addressArray = (address != null) ? address.getAddress() : null;
+        parcel.writeByteArray(addressArray);
+        if (address instanceof Inet6Address) {
+            final Inet6Address v6Address = (Inet6Address) address;
+            final boolean hasScopeId = v6Address.getScopeId() != 0;
+            parcel.writeBoolean(hasScopeId);
+            if (hasScopeId) parcel.writeInt(v6Address.getScopeId());
+        }
+
+    }
+
+    /**
+     * Reads an InetAddress from a parcel. Returns null if the address that was written was null
+     * or if the data is invalid.
+     * @hide
+     */
+    public static InetAddress unparcelInetAddress(Parcel in) {
+        byte[] addressArray = in.createByteArray();
+        if (addressArray == null) {
+            return null;
+        }
+
+        try {
+            if (addressArray.length == INET6_ADDR_LENGTH) {
+                final boolean hasScopeId = in.readBoolean();
+                final int scopeId = hasScopeId ? in.readInt() : 0;
+                return Inet6Address.getByAddress(null /* host */, addressArray, scopeId);
+            }
+
+            return InetAddress.getByAddress(addressArray);
+        } catch (UnknownHostException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Create a Inet6Address with scope id if it is a link local address. Otherwise, returns the
+     * original address.
+     */
+    public static Inet6Address withScopeId(@NonNull final Inet6Address addr, int scopeid) {
+        if (!addr.isLinkLocalAddress()) {
+            return addr;
+        }
+        try {
+            return Inet6Address.getByAddress(null /* host */, addr.getAddress(),
+                    scopeid);
+        } catch (UnknownHostException impossible) {
+            Log.wtf(TAG, "Cannot construct scoped Inet6Address with Inet6Address.getAddress("
+                    + addr.getHostAddress() + "): ", impossible);
+            return null;
+        }
+    }
+
+    /**
+     * Create a v4-mapped v6 address from v4 address
+     *
+     * @param v4Addr Inet4Address which is converted to v4-mapped v6 address
+     * @return v4-mapped v6 address
+     */
+    public static Inet6Address v4MappedV6Address(@NonNull final Inet4Address v4Addr) {
+        final byte[] v6AddrBytes = new byte[INET6_ADDR_LENGTH];
+        v6AddrBytes[10] = (byte) 0xFF;
+        v6AddrBytes[11] = (byte) 0xFF;
+        System.arraycopy(v4Addr.getAddress(), 0 /* srcPos */, v6AddrBytes, 12 /* dstPos */,
+                INET4_ADDR_LENGTH);
+        try {
+            // Using Inet6Address.getByAddress since InetAddress.getByAddress converts v4-mapped v6
+            // address to v4 address internally and returns Inet4Address
+            return Inet6Address.getByAddress(null /* host */, v6AddrBytes, -1 /* scope_id */);
+        } catch (UnknownHostException impossible) {
+            // getByAddress throws UnknownHostException when the argument is the invalid length
+            // but INET6_ADDR_LENGTH(16) is the valid length.
+            Log.wtf(TAG, "Failed to generate v4-mapped v6 address from " + v4Addr, impossible);
+            return null;
+        }
+    }
+
+    private InetAddressUtils() {}
+}
diff --git a/staticlibs/framework/com/android/net/module/util/InterfaceParams.java b/staticlibs/framework/com/android/net/module/util/InterfaceParams.java
new file mode 100644
index 0000000..30762eb
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/InterfaceParams.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.net.MacAddress;
+import android.text.TextUtils;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+
+
+/**
+ * Encapsulate the interface parameters common to IpClient/IpServer components.
+ *
+ * Basically all java.net.NetworkInterface methods throw Exceptions. IpClient
+ * and IpServer (sub)components need most or all of this information at some
+ * point during their lifecycles, so pass only this simplified object around
+ * which can be created once when IpClient/IpServer are told to start.
+ *
+ * @hide
+ */
+public class InterfaceParams {
+    public final String name;
+    public final int index;
+    public final boolean hasMacAddress;
+    public final MacAddress macAddr;
+    public final int defaultMtu;
+
+    // TODO: move the below to NetworkStackConstants when this class is moved to the NetworkStack.
+    private static final int ETHER_MTU = 1500;
+    private static final int IPV6_MIN_MTU = 1280;
+
+
+    /**
+     * Return InterfaceParams corresponding with an interface name
+     * @param name the interface name
+     */
+    public static InterfaceParams getByName(String name) {
+        final NetworkInterface netif = getNetworkInterfaceByName(name);
+        if (netif == null) return null;
+
+        // Not all interfaces have MAC addresses, e.g. rmnet_data0.
+        final MacAddress macAddr = getMacAddress(netif);
+
+        try {
+            return new InterfaceParams(name, netif.getIndex(), macAddr, netif.getMTU());
+        } catch (IllegalArgumentException | SocketException e) {
+            return null;
+        }
+    }
+
+    public InterfaceParams(String name, int index, MacAddress macAddr) {
+        this(name, index, macAddr, ETHER_MTU);
+    }
+
+    public InterfaceParams(String name, int index, MacAddress macAddr, int defaultMtu) {
+        if (TextUtils.isEmpty(name)) {
+            throw new IllegalArgumentException("impossible interface name");
+        }
+
+        if (index <= 0) throw new IllegalArgumentException("invalid interface index");
+
+        this.name = name;
+        this.index = index;
+        this.hasMacAddress = (macAddr != null);
+        this.macAddr = hasMacAddress ? macAddr : MacAddress.fromBytes(new byte[] {
+                0x02, 0x00, 0x00, 0x00, 0x00, 0x00 });
+        this.defaultMtu = (defaultMtu > IPV6_MIN_MTU) ? defaultMtu : IPV6_MIN_MTU;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s/%d/%s/%d", name, index, macAddr, defaultMtu);
+    }
+
+    private static NetworkInterface getNetworkInterfaceByName(String name) {
+        try {
+            return NetworkInterface.getByName(name);
+        } catch (NullPointerException | SocketException e) {
+            return null;
+        }
+    }
+
+    private static MacAddress getMacAddress(NetworkInterface netif) {
+        try {
+            return MacAddress.fromBytes(netif.getHardwareAddress());
+        } catch (IllegalArgumentException | NullPointerException | SocketException e) {
+            return null;
+        }
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/IpRange.java b/staticlibs/framework/com/android/net/module/util/IpRange.java
new file mode 100644
index 0000000..0a3f95b
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/IpRange.java
@@ -0,0 +1,223 @@
+/*
+ * 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 static com.android.internal.annotations.VisibleForTesting.Visibility;
+
+import android.annotation.NonNull;
+import android.net.IpPrefix;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Queue;
+
+/**
+ * This class represents an IP range, i.e., a contiguous block of IP addresses defined by a starting
+ * and ending IP address. These addresses may not be power-of-two aligned.
+ *
+ * <p>Conversion to prefixes are deterministic, and will always return the same set of {@link
+ * IpPrefix}(es). Ordering of IpPrefix instances is not guaranteed.
+ *
+ * @hide
+ */
+public final class IpRange {
+    private static final int SIGNUM_POSITIVE = 1;
+
+    private final byte[] mStartAddr;
+    private final byte[] mEndAddr;
+
+    public IpRange(@NonNull InetAddress startAddr, @NonNull InetAddress endAddr) {
+        Objects.requireNonNull(startAddr, "startAddr must not be null");
+        Objects.requireNonNull(endAddr, "endAddr must not be null");
+
+        if (!startAddr.getClass().equals(endAddr.getClass())) {
+            throw new IllegalArgumentException("Invalid range: Address family mismatch");
+        }
+        if (addrToBigInteger(startAddr.getAddress()).compareTo(
+                addrToBigInteger(endAddr.getAddress())) >= 0) {
+            throw new IllegalArgumentException(
+                    "Invalid range; start address must be before end address");
+        }
+
+        mStartAddr = startAddr.getAddress();
+        mEndAddr = endAddr.getAddress();
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public IpRange(@NonNull IpPrefix prefix) {
+        Objects.requireNonNull(prefix, "prefix must not be null");
+
+        // Use masked address from IpPrefix to zero out lower order bits.
+        mStartAddr = prefix.getRawAddress();
+
+        // Set all non-prefix bits to max.
+        mEndAddr = prefix.getRawAddress();
+        for (int bitIndex = prefix.getPrefixLength(); bitIndex < 8 * mEndAddr.length; ++bitIndex) {
+            mEndAddr[bitIndex / 8] |= (byte) (0x80 >> (bitIndex % 8));
+        }
+    }
+
+    private static InetAddress getAsInetAddress(byte[] address) {
+        try {
+            return InetAddress.getByAddress(address);
+        } catch (UnknownHostException e) {
+            // Cannot happen. InetAddress.getByAddress can only throw an exception if the byte
+            // array is the wrong length, but are always generated from InetAddress(es).
+            throw new IllegalArgumentException("Address is invalid");
+        }
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public InetAddress getStartAddr() {
+        return getAsInetAddress(mStartAddr);
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public InetAddress getEndAddr() {
+        return getAsInetAddress(mEndAddr);
+    }
+
+    /**
+     * Converts this IP range to a list of IpPrefix instances.
+     *
+     * <p>This method outputs the IpPrefix instances for use in the routing architecture.
+     *
+     * <p>For example, the range 192.0.2.4 - 192.0.3.1 converts to the following prefixes:
+     *
+     * <ul>
+     *   <li>192.0.2.128/25
+     *   <li>192.0.2.64/26
+     *   <li>192.0.2.32/27
+     *   <li>192.0.2.16/28
+     *   <li>192.0.2.8/29
+     *   <li>192.0.2.4/30
+     *   <li>192.0.3.0/31
+     * </ul>
+     */
+    public List<IpPrefix> asIpPrefixes() {
+        final boolean isIpv6 = (mStartAddr.length == 16);
+        final List<IpPrefix> result = new ArrayList<>();
+        final Queue<IpPrefix> workingSet = new LinkedList<>();
+
+        // Start with the any-address. Inet4/6Address.ANY requires compiling against
+        // core-platform-api.
+        workingSet.add(new IpPrefix(isIpv6 ? getAsInetAddress(new byte[16]) /* IPv6_ANY */
+                : getAsInetAddress(new byte[4]) /* IPv4_ANY */, 0));
+
+        // While items are still in the queue, test and narrow to subsets.
+        while (!workingSet.isEmpty()) {
+            final IpPrefix workingPrefix = workingSet.poll();
+            final IpRange workingRange = new IpRange(workingPrefix);
+
+            // If the other range is contained within, it's part of the output. Do not test subsets,
+            // or we will end up with duplicates.
+            if (containsRange(workingRange)) {
+                result.add(workingPrefix);
+                continue;
+            }
+
+            // If there is any overlap, split the working range into it's two subsets, and
+            // reevaluate those.
+            if (overlapsRange(workingRange)) {
+                workingSet.addAll(getSubsetPrefixes(workingPrefix));
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns the two prefixes that comprise the given prefix.
+     *
+     * <p>For example, for the prefix 192.0.2.0/24, this will return the two prefixes that combined
+     * make up the current prefix:
+     *
+     * <ul>
+     *   <li>192.0.2.0/25
+     *   <li>192.0.2.128/25
+     * </ul>
+     */
+    private static List<IpPrefix> getSubsetPrefixes(IpPrefix prefix) {
+        final List<IpPrefix> result = new ArrayList<>();
+
+        final int currentPrefixLen = prefix.getPrefixLength();
+        result.add(new IpPrefix(prefix.getAddress(), currentPrefixLen + 1));
+
+        final byte[] other = prefix.getRawAddress();
+        other[currentPrefixLen / 8] =
+                (byte) (other[currentPrefixLen / 8] ^ (0x80 >> (currentPrefixLen % 8)));
+        result.add(new IpPrefix(getAsInetAddress(other), currentPrefixLen + 1));
+
+        return result;
+    }
+
+    /**
+     * Checks if the other IP range is contained within this one
+     *
+     * <p>Checks based on byte values. For other to be contained within this IP range, other's
+     * starting address must be greater or equal to the current IpRange's starting address, and the
+     * other's ending address must be less than or equal to the current IP range's ending address.
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public boolean containsRange(IpRange other) {
+        return addrToBigInteger(mStartAddr).compareTo(addrToBigInteger(other.mStartAddr)) <= 0
+                && addrToBigInteger(mEndAddr).compareTo(addrToBigInteger(other.mEndAddr)) >= 0;
+    }
+
+    /**
+     * Checks if the other IP range overlaps with this one
+     *
+     * <p>Checks based on byte values. For there to be overlap, this IpRange's starting address must
+     * be less than the other's ending address, and vice versa.
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public boolean overlapsRange(IpRange other) {
+        return addrToBigInteger(mStartAddr).compareTo(addrToBigInteger(other.mEndAddr)) <= 0
+                && addrToBigInteger(other.mStartAddr).compareTo(addrToBigInteger(mEndAddr)) <= 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(Arrays.hashCode(mStartAddr), Arrays.hashCode(mEndAddr));
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof IpRange)) {
+            return false;
+        }
+
+        final IpRange other = (IpRange) obj;
+        return Arrays.equals(mStartAddr, other.mStartAddr)
+                && Arrays.equals(mEndAddr, other.mEndAddr);
+    }
+
+    /** Gets the InetAddress in BigInteger form */
+    private static BigInteger addrToBigInteger(byte[] addr) {
+        // Since addr.getAddress() returns network byte order (big-endian), it is compatible with
+        // the BigInteger constructor (which assumes big-endian).
+        return new BigInteger(SIGNUM_POSITIVE, addr);
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/IpUtils.java b/staticlibs/framework/com/android/net/module/util/IpUtils.java
new file mode 100644
index 0000000..18d96f3
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/IpUtils.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.internal.annotations.VisibleForTesting;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+
+/**
+ * @hide
+ */
+public class IpUtils {
+    /**
+     * Converts a signed short value to an unsigned int value.  Needed
+     * because Java does not have unsigned types.
+     */
+    private static int intAbs(short v) {
+        return v & 0xFFFF;
+    }
+
+    /**
+     * Performs an IP checksum (used in IP header and across UDP
+     * payload) or ICMP checksum on the specified portion of a ByteBuffer.  The seed
+     * allows the checksum to commence with a specified value.
+     */
+    @VisibleForTesting
+    public static int checksum(ByteBuffer buf, int seed, int start, int end) {
+        int sum = seed + 0xFFFF;  // to make things work with empty / zero-filled buffer
+        final int bufPosition = buf.position();
+
+        // set position of original ByteBuffer, so that the ShortBuffer
+        // will be correctly initialized
+        buf.position(start);
+        ShortBuffer shortBuf = buf.asShortBuffer();
+
+        // re-set ByteBuffer position
+        buf.position(bufPosition);
+
+        final int numShorts = (end - start) / 2;
+        for (int i = 0; i < numShorts; i++) {
+            sum += intAbs(shortBuf.get(i));
+        }
+        start += numShorts * 2;
+
+        // see if a singleton byte remains
+        if (end != start) {
+            short b = buf.get(start);
+
+            // make it unsigned
+            if (b < 0) {
+                b += 256;
+            }
+
+            sum += b * 256;  // assumes bytebuffer is network order (ie. big endian)
+        }
+
+        sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF);  // max sum is 0x1FFFE
+        sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF);  // max sum is 0xFFFF
+        return sum ^ 0xFFFF;  // u16 bitwise negation
+    }
+
+    private static int pseudoChecksumIPv4(
+            ByteBuffer buf, int headerOffset, int protocol, int transportLen) {
+        int partial = protocol + transportLen;
+        partial += intAbs(buf.getShort(headerOffset + 12));
+        partial += intAbs(buf.getShort(headerOffset + 14));
+        partial += intAbs(buf.getShort(headerOffset + 16));
+        partial += intAbs(buf.getShort(headerOffset + 18));
+        return partial;
+    }
+
+    private static int pseudoChecksumIPv6(
+            ByteBuffer buf, int headerOffset, int protocol, int transportLen) {
+        int partial = protocol + transportLen;
+        for (int offset = 8; offset < 40; offset += 2) {
+            partial += intAbs(buf.getShort(headerOffset + offset));
+        }
+        return partial;
+    }
+
+    private static byte ipversion(ByteBuffer buf, int headerOffset) {
+        return (byte) ((buf.get(headerOffset) & (byte) 0xf0) >> 4);
+   }
+
+    public static short ipChecksum(ByteBuffer buf, int headerOffset) {
+        byte ihl = (byte) (buf.get(headerOffset) & 0x0f);
+        return (short) checksum(buf, 0, headerOffset, headerOffset + ihl * 4);
+    }
+
+    private static short transportChecksum(ByteBuffer buf, int protocol,
+            int ipOffset, int transportOffset, int transportLen) {
+        if (transportLen < 0) {
+            throw new IllegalArgumentException("Transport length < 0: " + transportLen);
+        }
+        int sum;
+        byte ver = ipversion(buf, ipOffset);
+        if (ver == 4) {
+            sum = pseudoChecksumIPv4(buf, ipOffset, protocol, transportLen);
+        } else if (ver == 6) {
+            sum = pseudoChecksumIPv6(buf, ipOffset, protocol, transportLen);
+        } else {
+            throw new UnsupportedOperationException("Checksum must be IPv4 or IPv6");
+        }
+
+        sum = checksum(buf, sum, transportOffset, transportOffset + transportLen);
+        if (protocol == IPPROTO_UDP && sum == 0) {
+            sum = (short) 0xffff;
+        }
+        return (short) sum;
+    }
+
+    /**
+     * Calculate the UDP checksum for an UDP packet.
+     */
+    public static short udpChecksum(ByteBuffer buf, int ipOffset, int transportOffset) {
+        int transportLen = intAbs(buf.getShort(transportOffset + 4));
+        return transportChecksum(buf, IPPROTO_UDP, ipOffset, transportOffset, transportLen);
+    }
+
+    /**
+     * Calculate the TCP checksum for a TCP packet.
+     */
+    public static short tcpChecksum(ByteBuffer buf, int ipOffset, int transportOffset,
+            int transportLen) {
+        return transportChecksum(buf, IPPROTO_TCP, ipOffset, transportOffset, transportLen);
+    }
+
+    /**
+     * Calculate the ICMP checksum for an ICMPv4 packet.
+     */
+    public static short icmpChecksum(ByteBuffer buf, int transportOffset, int transportLen) {
+        // ICMP checksum doesn't include pseudo-header. See RFC 792.
+        return (short) checksum(buf, 0, transportOffset, transportOffset + transportLen);
+    }
+
+    /**
+     * Calculate the ICMPv6 checksum for an ICMPv6 packet.
+     */
+    public static short icmpv6Checksum(ByteBuffer buf, int ipOffset, int transportOffset,
+            int transportLen) {
+        return transportChecksum(buf, IPPROTO_ICMPV6, ipOffset, transportOffset, transportLen);
+    }
+
+    public static String addressAndPortToString(InetAddress address, int port) {
+        return String.format(
+                (address instanceof Inet6Address) ? "[%s]:%d" : "%s:%d",
+                address.getHostAddress(), port);
+    }
+
+    public static boolean isValidUdpOrTcpPort(int port) {
+        return port > 0 && port < 65536;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java b/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java
new file mode 100644
index 0000000..e271f64
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+import android.text.TextUtils;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * Collection of link properties utilities.
+ * @hide
+ */
+public final class LinkPropertiesUtils {
+
+    /**
+     * @param <T> The type of data to compare.
+     */
+    public static class CompareResult<T> {
+        public final List<T> removed = new ArrayList<>();
+        public final List<T> added = new ArrayList<>();
+
+        public CompareResult() {}
+
+        public CompareResult(@Nullable Collection<T> oldItems, @Nullable Collection<T> newItems) {
+            if (oldItems != null) {
+                removed.addAll(oldItems);
+            }
+            if (newItems != null) {
+                for (T newItem : newItems) {
+                    if (!removed.remove(newItem)) {
+                        added.add(newItem);
+                    }
+                }
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "removed=[" + TextUtils.join(",", removed)
+                    + "] added=[" + TextUtils.join(",", added)
+                    + "]";
+        }
+    }
+
+    /**
+     * Generic class to compare two lists of items of type {@code T} whose properties can change.
+     * The items to be compared must provide a way to calculate a corresponding key of type
+     * {@code K} such that if (and only if) an old and a new item have the same key, then the new
+     * item is an update of the old item. Both the old list and the new list may not contain more
+     * than one item with the same key, and may not contain any null items.
+     *
+     * @param <K> A class that represents the key of the items to be compared.
+     * @param <T> The class that represents the object to be compared.
+     */
+    public static class CompareOrUpdateResult<K, T> {
+        public final List<T> added = new ArrayList<>();
+        public final List<T> removed = new ArrayList<>();
+        public final List<T> updated = new ArrayList<>();
+
+        /**
+         * Compares two lists of items.
+         * @param oldItems the old list of items.
+         * @param newItems the new list of items.
+         * @param keyCalculator a {@link Function} that calculates an item's key.
+         */
+        public CompareOrUpdateResult(Collection<T> oldItems, Collection<T> newItems,
+                Function<T, K> keyCalculator) {
+            HashMap<K, T> updateTracker = new HashMap<>();
+
+            if (oldItems != null) {
+                for (T oldItem : oldItems) {
+                    updateTracker.put(keyCalculator.apply(oldItem), oldItem);
+                }
+            }
+
+            if (newItems != null) {
+                for (T newItem : newItems) {
+                    T oldItem = updateTracker.remove(keyCalculator.apply(newItem));
+                    if (oldItem != null) {
+                        if (!oldItem.equals(newItem)) {
+                            // Update of existing item.
+                            updated.add(newItem);
+                        }
+                    } else {
+                        // New item.
+                        added.add(newItem);
+                    }
+                }
+            }
+
+            removed.addAll(updateTracker.values());
+        }
+
+        @Override
+        public String toString() {
+            return "removed=[" + TextUtils.join(",", removed)
+                    + "] added=[" + TextUtils.join(",", added)
+                    + "] updated=[" + TextUtils.join(",", updated)
+                    + "]";
+        }
+    }
+
+    /**
+     * Compares the addresses in {@code left} LinkProperties with {@code right}
+     * LinkProperties, examining only addresses on the base link.
+     *
+     * @param left A LinkProperties with the old list of addresses.
+     * @param right A LinkProperties with the new list of addresses.
+     * @return the differences between the addresses.
+     */
+    public static @NonNull CompareResult<LinkAddress> compareAddresses(
+            @Nullable LinkProperties left, @Nullable LinkProperties right) {
+        /*
+         * Duplicate the LinkAddresses into removed, we will be removing
+         * address which are common between mLinkAddresses and target
+         * leaving the addresses that are different. And address which
+         * are in target but not in mLinkAddresses are placed in the
+         * addedAddresses.
+         */
+        return new CompareResult<>(left != null ? left.getLinkAddresses() : null,
+                right != null ? right.getLinkAddresses() : null);
+    }
+
+    /**
+     * Compares {@code left} {@code LinkProperties} allLinkAddresses against the {@code right}.
+     *
+     * @param left A LinkProperties or null
+     * @param right A LinkProperties or null
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @see LinkProperties#getAllLinkAddresses()
+     */
+    public static boolean isIdenticalAllLinkAddresses(@Nullable LinkProperties left,
+            @Nullable LinkProperties right) {
+        if (left == right) return true;
+        if (left == null || right == null) return false;
+        final List<LinkAddress> leftAddresses = left.getAllLinkAddresses();
+        final List<LinkAddress> rightAddresses = right.getAllLinkAddresses();
+        if (leftAddresses.size() != rightAddresses.size()) return false;
+        return leftAddresses.containsAll(rightAddresses);
+    }
+
+   /**
+     * Compares {@code left} {@code LinkProperties} interface addresses against the {@code right}.
+     *
+     * @param left A LinkProperties.
+     * @param right LinkProperties to be compared with {@code left}.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     */
+    public static boolean isIdenticalAddresses(@NonNull LinkProperties left,
+            @NonNull LinkProperties right) {
+        final Collection<InetAddress> leftAddresses = left.getAddresses();
+        final Collection<InetAddress> rightAddresses = right.getAddresses();
+        return (leftAddresses.size() == rightAddresses.size())
+                    ? leftAddresses.containsAll(rightAddresses) : false;
+    }
+
+    /**
+     * Compares {@code left} {@code LinkProperties} DNS addresses against the {@code right}.
+     *
+     * @param left A LinkProperties.
+     * @param right A LinkProperties to be compared with {@code left}.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     */
+    public static boolean isIdenticalDnses(@NonNull LinkProperties left,
+            @NonNull LinkProperties right) {
+        final Collection<InetAddress> leftDnses = left.getDnsServers();
+        final Collection<InetAddress> rightDnses = right.getDnsServers();
+
+        final String leftDomains = left.getDomains();
+        final String rightDomains = right.getDomains();
+        if (leftDomains == null) {
+            if (rightDomains != null) return false;
+        } else {
+            if (!leftDomains.equals(rightDomains)) return false;
+        }
+        return (leftDnses.size() == rightDnses.size())
+                ? leftDnses.containsAll(rightDnses) : false;
+    }
+
+    /**
+     * Compares {@code left} {@code LinkProperties} HttpProxy against the {@code right}.
+     *
+     * @param left A LinkProperties.
+     * @param right A LinkProperties to be compared with {@code left}.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     */
+    public static boolean isIdenticalHttpProxy(@NonNull LinkProperties left,
+            @NonNull LinkProperties right) {
+        return Objects.equals(left.getHttpProxy(), right.getHttpProxy());
+    }
+
+    /**
+     * Compares {@code left} {@code LinkProperties} interface name against the {@code right}.
+     *
+     * @param left A LinkProperties.
+     * @param right A LinkProperties to be compared with {@code left}.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     */
+    public static boolean isIdenticalInterfaceName(@NonNull LinkProperties left,
+            @NonNull LinkProperties right) {
+        return TextUtils.equals(left.getInterfaceName(), right.getInterfaceName());
+    }
+
+    /**
+     * Compares {@code left} {@code LinkProperties} Routes against the {@code right}.
+     *
+     * @param left A LinkProperties.
+     * @param right A LinkProperties to be compared with {@code left}.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     */
+    public static boolean isIdenticalRoutes(@NonNull LinkProperties left,
+            @NonNull LinkProperties right) {
+        final Collection<RouteInfo> leftRoutes = left.getRoutes();
+        final Collection<RouteInfo> rightRoutes = right.getRoutes();
+        return (leftRoutes.size() == rightRoutes.size())
+                ? leftRoutes.containsAll(rightRoutes) : false;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
new file mode 100644
index 0000000..f6bee69
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
@@ -0,0 +1,310 @@
+/*
+ * 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.Manifest;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.net.NetworkStack;
+import android.os.Binder;
+import android.os.Build;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
+/**
+ * The methods used for location permission and location mode checking.
+ *
+ * @hide
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+public class LocationPermissionChecker {
+
+    private static final String TAG = "LocationPermissionChecker";
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"LOCATION_PERMISSION_CHECK_STATUS_"}, value = {
+        SUCCEEDED,
+        ERROR_LOCATION_MODE_OFF,
+        ERROR_LOCATION_PERMISSION_MISSING,
+    })
+    public @interface LocationPermissionCheckStatus{}
+
+    // The location permission check succeeded.
+    public static final int SUCCEEDED = 0;
+    // The location mode turns off for the caller.
+    public static final int ERROR_LOCATION_MODE_OFF = 1;
+    // The location permission isn't granted for the caller.
+    public static final int ERROR_LOCATION_PERMISSION_MISSING = 2;
+
+    private final Context mContext;
+    private final AppOpsManager mAppOpsManager;
+
+    public LocationPermissionChecker(Context context) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            throw new UnsupportedOperationException("This utility is not supported before R");
+        }
+
+        mContext = context;
+        mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+    }
+
+    /**
+     * Check location permission granted by the caller.
+     *
+     * This API check if the location mode enabled for the caller and the caller has
+     * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
+     *
+     * @param pkgName package name of the application requesting access
+     * @param featureId The feature in the package
+     * @param uid The uid of the package
+     * @param message A message describing why the permission was checked. Only needed if this is
+     *                not inside of a two-way binder call from the data receiver
+     *
+     * @return {@code true} returns if the caller has location permission and the location mode is
+     *         enabled.
+     */
+    public boolean checkLocationPermission(String pkgName, @Nullable String featureId,
+            int uid, @Nullable String message) {
+        return checkLocationPermissionInternal(pkgName, featureId, uid, message) == SUCCEEDED;
+    }
+
+    /**
+     * Check location permission granted by the caller.
+     *
+     * This API check if the location mode enabled for the caller and the caller has
+     * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
+     * Compared with {@link #checkLocationPermission(String, String, int, String)}, this API returns
+     * the detail information about the checking result, including the reason why it's failed and
+     * logs the error for the caller.
+     *
+     * @param pkgName package name of the application requesting access
+     * @param featureId The feature in the package
+     * @param uid The uid of the package
+     * @param message A message describing why the permission was checked. Only needed if this is
+     *                not inside of a two-way binder call from the data receiver
+     *
+     * @return {@link LocationPermissionCheckStatus} the result of the location permission check.
+     */
+    public @LocationPermissionCheckStatus int checkLocationPermissionWithDetailInfo(
+            String pkgName, @Nullable String featureId, int uid, @Nullable String message) {
+        final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
+        switch (result) {
+            case ERROR_LOCATION_MODE_OFF:
+                Log.e(TAG, "Location mode is disabled for the device");
+                break;
+            case ERROR_LOCATION_PERMISSION_MISSING:
+                Log.e(TAG, "UID " + uid + " has no location permission");
+                break;
+        }
+        return result;
+    }
+
+    /**
+     * Enforce the caller has location permission.
+     *
+     * This API determines if the location mode enabled for the caller and the caller has
+     * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
+     * SecurityException is thrown if the caller has no permission or the location mode is disabled.
+     *
+     * @param pkgName package name of the application requesting access
+     * @param featureId The feature in the package
+     * @param uid The uid of the package
+     * @param message A message describing why the permission was checked. Only needed if this is
+     *                not inside of a two-way binder call from the data receiver
+     */
+    public void enforceLocationPermission(String pkgName, @Nullable String featureId, int uid,
+            @Nullable String message) throws SecurityException {
+        final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
+
+        switch (result) {
+            case ERROR_LOCATION_MODE_OFF:
+                throw new SecurityException("Location mode is disabled for the device");
+            case ERROR_LOCATION_PERMISSION_MISSING:
+                throw new SecurityException("UID " + uid + " has no location permission");
+        }
+    }
+
+    private int checkLocationPermissionInternal(String pkgName, @Nullable String featureId,
+            int uid, @Nullable String message) {
+        checkPackage(uid, pkgName);
+
+        // Apps with NETWORK_SETTINGS, NETWORK_SETUP_WIZARD, NETWORK_STACK & MAINLINE_NETWORK_STACK
+        // are granted a bypass.
+        if (checkNetworkSettingsPermission(uid) || checkNetworkSetupWizardPermission(uid)
+                || checkNetworkStackPermission(uid) || checkMainlineNetworkStackPermission(uid)) {
+            return SUCCEEDED;
+        }
+
+        // Location mode must be enabled
+        if (!isLocationModeEnabled()) {
+            return ERROR_LOCATION_MODE_OFF;
+        }
+
+        // LocationAccess by App: caller must have Coarse/Fine Location permission to have access to
+        // location information.
+        if (!checkCallersLocationPermission(pkgName, featureId, uid,
+                true /* coarseForTargetSdkLessThanQ */, message)) {
+            return ERROR_LOCATION_PERMISSION_MISSING;
+        }
+        return SUCCEEDED;
+    }
+
+    /**
+     * Checks that calling process has android.Manifest.permission.ACCESS_FINE_LOCATION or
+     * android.Manifest.permission.ACCESS_COARSE_LOCATION (depending on config/targetSDK level)
+     * and a corresponding app op is allowed for this package and uid.
+     *
+     * @param pkgName PackageName of the application requesting access
+     * @param featureId The feature in the package
+     * @param uid The uid of the package
+     * @param coarseForTargetSdkLessThanQ If true and the targetSDK < Q then will check for COARSE
+     *                                    else (false or targetSDK >= Q) then will check for FINE
+     * @param message A message describing why the permission was checked. Only needed if this is
+     *                not inside of a two-way binder call from the data receiver
+     */
+    public boolean checkCallersLocationPermission(@Nullable String pkgName,
+            @Nullable String featureId, int uid, boolean coarseForTargetSdkLessThanQ,
+            @Nullable String message) {
+
+        boolean isTargetSdkLessThanQ = isTargetSdkLessThan(pkgName, Build.VERSION_CODES.Q, uid);
+
+        String permissionType = Manifest.permission.ACCESS_FINE_LOCATION;
+        if (coarseForTargetSdkLessThanQ && isTargetSdkLessThanQ) {
+            // Having FINE permission implies having COARSE permission (but not the reverse)
+            permissionType = Manifest.permission.ACCESS_COARSE_LOCATION;
+        }
+        if (getUidPermission(permissionType, uid) == PackageManager.PERMISSION_DENIED) {
+            return false;
+        }
+
+        // Always checking FINE - even if will not enforce. This will record the request for FINE
+        // so that a location request by the app is surfaced to the user.
+        boolean isFineLocationAllowed = noteAppOpAllowed(
+                AppOpsManager.OPSTR_FINE_LOCATION, pkgName, featureId, uid, message);
+        if (isFineLocationAllowed) {
+            return true;
+        }
+        if (coarseForTargetSdkLessThanQ && isTargetSdkLessThanQ) {
+            return noteAppOpAllowed(AppOpsManager.OPSTR_COARSE_LOCATION, pkgName, featureId, uid,
+                    message);
+        }
+        return false;
+    }
+
+    /**
+     * Retrieves a handle to LocationManager (if not already done) and check if location is enabled.
+     */
+    public boolean isLocationModeEnabled() {
+        final LocationManager LocationManager = mContext.getSystemService(LocationManager.class);
+        try {
+            return LocationManager.isLocationEnabledForUser(UserHandle.of(
+                    getCurrentUser()));
+        } catch (Exception e) {
+            Log.e(TAG, "Failure to get location mode via API, falling back to settings", e);
+            return false;
+        }
+    }
+
+    private boolean isTargetSdkLessThan(String packageName, int versionCode, int callingUid) {
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            if (mContext.getPackageManager().getApplicationInfoAsUser(
+                    packageName, 0,
+                    UserHandle.getUserHandleForUid(callingUid)).targetSdkVersion
+                    < versionCode) {
+                return true;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            // In case of exception, assume unknown app (more strict checking)
+            // Note: This case will never happen since checkPackage is
+            // called to verify validity before checking App's version.
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+        return false;
+    }
+
+    private boolean noteAppOpAllowed(String op, String pkgName, @Nullable String featureId,
+            int uid, @Nullable String message) {
+        return mAppOpsManager.noteOp(op, uid, pkgName, featureId, message)
+                == AppOpsManager.MODE_ALLOWED;
+    }
+
+    private void checkPackage(int uid, String pkgName)
+            throws SecurityException {
+        if (pkgName == null) {
+            throw new SecurityException("Checking UID " + uid + " but Package Name is Null");
+        }
+        mAppOpsManager.checkPackage(uid, pkgName);
+    }
+
+    @VisibleForTesting
+    protected int getCurrentUser() {
+        return ActivityManager.getCurrentUser();
+    }
+
+    private int getUidPermission(String permissionType, int uid) {
+        // We don't care about pid, pass in -1
+        return mContext.checkPermission(permissionType, -1, uid);
+    }
+
+    /**
+     * Returns true if the |uid| holds NETWORK_SETTINGS permission.
+     */
+    public boolean checkNetworkSettingsPermission(int uid) {
+        return getUidPermission(android.Manifest.permission.NETWORK_SETTINGS, uid)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * Returns true if the |uid| holds NETWORK_SETUP_WIZARD permission.
+     */
+    public boolean checkNetworkSetupWizardPermission(int uid) {
+        return getUidPermission(android.Manifest.permission.NETWORK_SETUP_WIZARD, uid)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * Returns true if the |uid| holds NETWORK_STACK permission.
+     */
+    public boolean checkNetworkStackPermission(int uid) {
+        return getUidPermission(android.Manifest.permission.NETWORK_STACK, uid)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * Returns true if the |uid| holds MAINLINE_NETWORK_STACK permission.
+     */
+    public boolean checkMainlineNetworkStackPermission(int uid) {
+        return getUidPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, uid)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+}
diff --git a/staticlibs/framework/com/android/net/module/util/MacAddressUtils.java b/staticlibs/framework/com/android/net/module/util/MacAddressUtils.java
new file mode 100644
index 0000000..ab0040c
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/MacAddressUtils.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.MacAddress;
+
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Random;
+
+/**
+ * Collection of MAC address utilities.
+ * @hide
+ */
+public final class MacAddressUtils {
+
+    private static final long VALID_LONG_MASK = (1L << 48) - 1;
+    private static final long LOCALLY_ASSIGNED_MASK = longAddrFromByteAddr(
+            MacAddress.fromString("2:0:0:0:0:0").toByteArray());
+    private static final long MULTICAST_MASK = longAddrFromByteAddr(
+            MacAddress.fromString("1:0:0:0:0:0").toByteArray());
+    private static final long OUI_MASK = longAddrFromByteAddr(
+            MacAddress.fromString("ff:ff:ff:0:0:0").toByteArray());
+    private static final long NIC_MASK = longAddrFromByteAddr(
+            MacAddress.fromString("0:0:0:ff:ff:ff").toByteArray());
+    // Matches WifiInfo.DEFAULT_MAC_ADDRESS
+    private static final MacAddress DEFAULT_MAC_ADDRESS =
+            MacAddress.fromString("02:00:00:00:00:00");
+    private static final int ETHER_ADDR_LEN = 6;
+
+    /**
+     * @return true if this MacAddress is a multicast address.
+     */
+    public static boolean isMulticastAddress(@NonNull MacAddress address) {
+        return (longAddrFromByteAddr(address.toByteArray()) & MULTICAST_MASK) != 0;
+    }
+
+    /**
+     * Returns a generated MAC address whose 46 bits, excluding the locally assigned bit and the
+     * unicast bit, are randomly selected.
+     *
+     * The locally assigned bit is always set to 1. The multicast bit is always set to 0.
+     *
+     * @return a random locally assigned, unicast MacAddress.
+     */
+    public static @NonNull MacAddress createRandomUnicastAddress() {
+        return createRandomUnicastAddress(null, new SecureRandom());
+    }
+
+    /**
+     * Returns a randomly generated MAC address using the given Random object and the same
+     * OUI values as the given MacAddress.
+     *
+     * The locally assigned bit is always set to 1. The multicast bit is always set to 0.
+     *
+     * @param base a base MacAddress whose OUI is used for generating the random address.
+     *             If base == null then the OUI will also be randomized.
+     * @param r a standard Java Random object used for generating the random address.
+     * @return a random locally assigned MacAddress.
+     */
+    public static @NonNull MacAddress createRandomUnicastAddress(@Nullable MacAddress base,
+            @NonNull Random r) {
+        long addr;
+
+        if (base == null) {
+            addr = r.nextLong() & VALID_LONG_MASK;
+        } else {
+            addr = (longAddrFromByteAddr(base.toByteArray()) & OUI_MASK)
+                    | (NIC_MASK & r.nextLong());
+        }
+        addr |= LOCALLY_ASSIGNED_MASK;
+        addr &= ~MULTICAST_MASK;
+        MacAddress mac = MacAddress.fromBytes(byteAddrFromLongAddr(addr));
+        if (mac.equals(DEFAULT_MAC_ADDRESS)) {
+            return createRandomUnicastAddress(base, r);
+        }
+        return mac;
+    }
+
+    /**
+     * Convert a byte address to long address.
+     */
+    public static long longAddrFromByteAddr(byte[] addr) {
+        Objects.requireNonNull(addr);
+        if (!isMacAddress(addr)) {
+            throw new IllegalArgumentException(
+                    Arrays.toString(addr) + " was not a valid MAC address");
+        }
+        long longAddr = 0;
+        for (byte b : addr) {
+            final int uint8Byte = b & 0xff;
+            longAddr = (longAddr << 8) + uint8Byte;
+        }
+        return longAddr;
+    }
+
+    /**
+     * Convert a long address to byte address.
+     */
+    public static byte[] byteAddrFromLongAddr(long addr) {
+        byte[] bytes = new byte[ETHER_ADDR_LEN];
+        int index = ETHER_ADDR_LEN;
+        while (index-- > 0) {
+            bytes[index] = (byte) addr;
+            addr = addr >> 8;
+        }
+        return bytes;
+    }
+
+    /**
+     * Returns true if the given byte array is a valid MAC address.
+     * A valid byte array representation for a MacAddress is a non-null array of length 6.
+     *
+     * @param addr a byte array.
+     * @return true if the given byte array is not null and has the length of a MAC address.
+     *
+     */
+    public static boolean isMacAddress(byte[] addr) {
+        return addr != null && addr.length == ETHER_ADDR_LEN;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetUtils.java b/staticlibs/framework/com/android/net/module/util/NetUtils.java
new file mode 100644
index 0000000..ae1b9bb
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/NetUtils.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.RouteInfo;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Collection;
+
+/**
+ * Collection of network common utilities.
+ * @hide
+ */
+public final class NetUtils {
+
+    /**
+     * Check if IP address type is consistent between two InetAddress.
+     * @return true if both are the same type. False otherwise.
+     */
+    public static boolean addressTypeMatches(@NonNull InetAddress left,
+            @NonNull InetAddress right) {
+        return (((left instanceof Inet4Address) && (right instanceof Inet4Address))
+                || ((left instanceof Inet6Address) && (right instanceof Inet6Address)));
+    }
+
+    /**
+     * Find the route from a Collection of routes that best matches a given address.
+     * May return null if no routes are applicable, or the best route is not a route of
+     * {@link RouteInfo.RTN_UNICAST} type.
+     *
+     * @param routes a Collection of RouteInfos to chose from
+     * @param dest the InetAddress your trying to get to
+     * @return the RouteInfo from the Collection that best fits the given address
+     */
+    @Nullable
+    public static RouteInfo selectBestRoute(@Nullable Collection<RouteInfo> routes,
+            @Nullable InetAddress dest) {
+        if ((routes == null) || (dest == null)) return null;
+
+        RouteInfo bestRoute = null;
+
+        // pick a longest prefix match under same address type
+        for (RouteInfo route : routes) {
+            if (addressTypeMatches(route.getDestination().getAddress(), dest)) {
+                if ((bestRoute != null)
+                        && (bestRoute.getDestination().getPrefixLength()
+                        >= route.getDestination().getPrefixLength())) {
+                    continue;
+                }
+                if (route.matches(dest)) bestRoute = route;
+            }
+        }
+
+        if (bestRoute != null && bestRoute.getType() == RouteInfo.RTN_UNICAST) {
+            return bestRoute;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Get InetAddress masked with prefixLength.  Will never return null.
+     * @param address the IP address to mask with
+     * @param prefixLength the prefixLength used to mask the IP
+     */
+    public static InetAddress getNetworkPart(InetAddress address, int prefixLength) {
+        byte[] array = address.getAddress();
+        maskRawAddress(array, prefixLength);
+
+        InetAddress netPart = null;
+        try {
+            netPart = InetAddress.getByAddress(array);
+        } catch (UnknownHostException e) {
+            throw new RuntimeException("getNetworkPart error - " + e.toString());
+        }
+        return netPart;
+    }
+
+    /**
+     *  Masks a raw IP address byte array with the specified prefix length.
+     */
+    public static void maskRawAddress(byte[] array, int prefixLength) {
+        if (prefixLength < 0 || prefixLength > array.length * 8) {
+            throw new RuntimeException("IP address with " + array.length
+                    + " bytes has invalid prefix length " + prefixLength);
+        }
+
+        int offset = prefixLength / 8;
+        int remainder = prefixLength % 8;
+        byte mask = (byte) (0xFF << (8 - remainder));
+
+        if (offset < array.length) array[offset] = (byte) (array[offset] & mask);
+
+        offset++;
+
+        for (; offset < array.length; offset++) {
+            array[offset] = 0;
+        }
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
new file mode 100644
index 0000000..7066131
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
@@ -0,0 +1,193 @@
+/*
+ * 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 static android.net.NetworkCapabilities.NET_CAPABILITY_BIP;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_IA;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MCX;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MMTEL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_RCS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VEHICLE_INTERNAL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VSIM;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP;
+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.packBits;
+import static com.android.net.module.util.BitUtils.unpackBits;
+
+import android.annotation.NonNull;
+import android.net.NetworkCapabilities;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Utilities to examine {@link android.net.NetworkCapabilities}.
+ * @hide
+ */
+public final class NetworkCapabilitiesUtils {
+    // Transports considered to classify networks in UI, in order of which transport should be
+    // surfaced when there are multiple transports. Transports not in this list do not have
+    // an ordering preference (in practice they will have a deterministic order based on the
+    // transport int itself).
+    private static final int[] DISPLAY_TRANSPORT_PRIORITIES = new int[] {
+        // Users think of their VPNs as VPNs, not as any of the underlying nets
+        TRANSPORT_VPN,
+        // If the network has cell, prefer showing that because it's usually metered.
+        TRANSPORT_CELLULAR,
+        // If the network has WiFi aware, prefer showing that as it's a more specific use case.
+        // Ethernet can masquerade as other transports, where the device uses ethernet to connect to
+        // a box providing cell or wifi. Today this is represented by only the masqueraded type for
+        // backward compatibility, but these networks should morally have Ethernet & the masqueraded
+        // type. Because of this, prefer other transports instead of Ethernet.
+        TRANSPORT_WIFI_AWARE,
+        TRANSPORT_BLUETOOTH,
+        TRANSPORT_WIFI,
+        TRANSPORT_ETHERNET,
+        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.
+    };
+
+    /**
+     * Capabilities that suggest that a network is restricted.
+     * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted},
+      * and {@code FORCE_RESTRICTED_CAPABILITIES}.
+     */
+    @VisibleForTesting
+    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 =
+            (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 =
+            (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.
+     *
+     * While networks can have multiple transports, users generally think of them as "wifi",
+     * "mobile data", "vpn" and expect them to be classified as such in UI such as settings.
+     * @param transports Non-empty array of transports on a network
+     * @return A single transport
+     * @throws IllegalArgumentException The array is empty
+     */
+    public static int getDisplayTransport(@NonNull int[] transports) {
+        for (int transport : DISPLAY_TRANSPORT_PRIORITIES) {
+            if (CollectionUtils.contains(transports, transport)) {
+                return transport;
+            }
+        }
+
+        if (transports.length < 1) {
+            // All NetworkCapabilities representing a network have at least one transport, so an
+            // empty transport array would be created by the caller instead of extracted from
+            // NetworkCapabilities.
+            throw new IllegalArgumentException("No transport in the provided array");
+        }
+        return transports[0];
+    }
+
+
+    /**
+     * Infers that all the capabilities it provides are typically provided by restricted networks
+     * or not.
+     *
+     * @param nc the {@link NetworkCapabilities} to infer the restricted capabilities.
+     *
+     * @return {@code true} if the network should be restricted.
+     */
+    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.
+        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.
+        if ((capabilities & UNRESTRICTED_CAPABILITIES) != 0) {
+            return false;
+        }
+
+        // Must have at least some restricted capabilities.
+        if ((capabilities & RESTRICTED_CAPABILITIES) != 0) {
+            return true;
+        }
+        return false;
+    }
+
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkIdentityUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkIdentityUtils.java
new file mode 100644
index 0000000..b641753
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/NetworkIdentityUtils.java
@@ -0,0 +1,52 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Utilities to examine {@link android.net.NetworkIdentity}.
+ * @hide
+ */
+public class NetworkIdentityUtils {
+    /**
+     * Scrub given IMSI on production builds.
+     */
+    @NonNull
+    public static String scrubSubscriberId(@Nullable String subscriberId) {
+        if (subscriberId != null) {
+            // TODO: parse this as MCC+MNC instead of hard-coding
+            return subscriberId.substring(0, Math.min(6, subscriberId.length())) + "...";
+        } else {
+            return "null";
+        }
+    }
+
+    /**
+     * Scrub given IMSI on production builds.
+     */
+    @Nullable
+    public static String[] scrubSubscriberIds(@Nullable String[] subscriberIds) {
+        if (subscriberIds == null) return null;
+        final String[] res = new String[subscriberIds.length];
+        for (int i = 0; i < res.length; i++) {
+            res[i] = scrubSubscriberId(subscriberIds[i]);
+        }
+        return res;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
new file mode 100644
index 0000000..f1ff2e4
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -0,0 +1,361 @@
+/*
+ * 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.net.InetAddresses;
+import android.net.IpPrefix;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Network constants used by the network stack.
+ * @hide
+ */
+public final class NetworkStackConstants {
+
+    /**
+     * Ethernet constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc894
+     *     - https://tools.ietf.org/html/rfc2464
+     *     - https://tools.ietf.org/html/rfc7042
+     *     - http://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml
+     *     - http://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml
+     */
+    public static final int ETHER_DST_ADDR_OFFSET = 0;
+    public static final int ETHER_SRC_ADDR_OFFSET = 6;
+    public static final int ETHER_ADDR_LEN = 6;
+    public static final int ETHER_TYPE_OFFSET = 12;
+    public static final int ETHER_TYPE_LENGTH = 2;
+    public static final int ETHER_TYPE_ARP  = 0x0806;
+    public static final int ETHER_TYPE_IPV4 = 0x0800;
+    public static final int ETHER_TYPE_IPV6 = 0x86dd;
+    public static final int ETHER_HEADER_LEN = 14;
+    public static final int ETHER_MTU = 1500;
+    public static final byte[] ETHER_BROADCAST = new byte[] {
+            (byte) 0xff, (byte) 0xff, (byte) 0xff,
+            (byte) 0xff, (byte) 0xff, (byte) 0xff,
+    };
+
+    /**
+     * ARP constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc826
+     *     - http://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml
+     */
+    public static final int ARP_PAYLOAD_LEN = 28;  // For Ethernet+IPv4.
+    public static final int ARP_ETHER_IPV4_LEN = ARP_PAYLOAD_LEN + ETHER_HEADER_LEN;
+    public static final int ARP_REQUEST = 1;
+    public static final int ARP_REPLY   = 2;
+    public static final int ARP_HWTYPE_RESERVED_LO = 0;
+    public static final int ARP_HWTYPE_ETHER       = 1;
+    public static final int ARP_HWTYPE_RESERVED_HI = 0xffff;
+
+    /**
+     * IPv4 Address Conflict Detection constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc5227
+     */
+    public static final int IPV4_CONFLICT_PROBE_NUM = 3;
+    public static final int IPV4_CONFLICT_ANNOUNCE_NUM = 2;
+
+    /**
+     * IPv4 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc791
+     */
+    public static final int IPV4_ADDR_BITS = 32;
+    public static final int IPV4_MIN_MTU = 68;
+    public static final int IPV4_MAX_MTU = 65_535;
+    public static final int IPV4_HEADER_MIN_LEN = 20;
+    public static final int IPV4_IHL_MASK = 0xf;
+    public static final int IPV4_LENGTH_OFFSET = 2;
+    public static final int IPV4_FLAGS_OFFSET = 6;
+    public static final int IPV4_FRAGMENT_MASK = 0x1fff;
+    public static final int IPV4_PROTOCOL_OFFSET = 9;
+    public static final int IPV4_CHECKSUM_OFFSET = 10;
+    public static final int IPV4_SRC_ADDR_OFFSET = 12;
+    public static final int IPV4_DST_ADDR_OFFSET = 16;
+    public static final int IPV4_ADDR_LEN = 4;
+    public static final int IPV4_FLAG_MF = 0x2000;
+    public static final int IPV4_FLAG_DF = 0x4000;
+    // getSockOpt() for v4 MTU
+    public static final int IP_MTU = 14;
+    public static final Inet4Address IPV4_ADDR_ALL = makeInet4Address(
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff);
+    public static final Inet4Address IPV4_ADDR_ANY = makeInet4Address(
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0);
+    public static final Inet6Address IPV6_ADDR_ANY = makeInet6Address(new byte[]{
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0 });
+
+    /**
+     * CLAT constants
+     */
+    public static final IpPrefix CLAT_PREFIX = new IpPrefix("192.0.0.0/29");
+
+    /**
+     * IPv6 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc2460
+     */
+    public static final int IPV6_ADDR_LEN = 16;
+    public static final int IPV6_HEADER_LEN = 40;
+    public static final int IPV6_LEN_OFFSET = 4;
+    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
+    public static final int IPV6_MTU = 24;
+    public static final Inet6Address IPV6_ADDR_ALL_NODES_MULTICAST =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff02::1");
+    public static final Inet6Address IPV6_ADDR_ALL_ROUTERS_MULTICAST =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
+    public static final Inet6Address IPV6_ADDR_ALL_HOSTS_MULTICAST =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff02::3");
+
+    public static final int IPPROTO_FRAGMENT = 44;
+
+    /**
+     * ICMP constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc792
+     */
+    public static final int ICMP_CHECKSUM_OFFSET = 2;
+    public static final int ICMP_HEADER_LEN = 8;
+    /**
+     * ICMPv6 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc4191
+     *     - https://tools.ietf.org/html/rfc4443
+     *     - https://tools.ietf.org/html/rfc4861
+     */
+    public static final int ICMPV6_HEADER_MIN_LEN = 4;
+    public static final int ICMPV6_CHECKSUM_OFFSET = 2;
+    public static final int ICMPV6_ECHO_REPLY_TYPE = 129;
+    public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
+    public static final int ICMPV6_ROUTER_SOLICITATION    = 133;
+    public static final int ICMPV6_ROUTER_ADVERTISEMENT   = 134;
+    public static final int ICMPV6_NEIGHBOR_SOLICITATION  = 135;
+    public static final int ICMPV6_NEIGHBOR_ADVERTISEMENT = 136;
+    public static final int ICMPV6_ND_OPTION_MIN_LENGTH = 8;
+    public static final int ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR = 8;
+    public static final int ICMPV6_ND_OPTION_SLLA  = 1;
+    public static final int ICMPV6_ND_OPTION_TLLA  = 2;
+    public static final int ICMPV6_ND_OPTION_PIO   = 3;
+    public static final int ICMPV6_ND_OPTION_MTU   = 5;
+    public static final int ICMPV6_ND_OPTION_RIO   = 24;
+    public static final int ICMPV6_ND_OPTION_RDNSS = 25;
+    public static final int ICMPV6_ND_OPTION_PREF64 = 38;
+
+    public static final int ICMPV6_RS_HEADER_LEN = 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;
+    public static final int NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE  = 1 << 29;
+
+    public static final byte ROUTER_ADVERTISEMENT_FLAG_MANAGED_ADDRESS = (byte) (1 << 7);
+    public static final byte ROUTER_ADVERTISEMENT_FLAG_OTHER = (byte) (1 << 6);
+
+    public static final byte PIO_FLAG_ON_LINK = (byte) (1 << 7);
+    public static final byte PIO_FLAG_AUTONOMOUS = (byte) (1 << 6);
+    public static final byte PIO_FLAG_DHCPV6_PD_PREFERRED = (byte) (1 << 4);
+
+    /**
+     * TCP constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc793
+     */
+    public static final int TCP_HEADER_MIN_LEN = 20;
+    public static final int TCP_CHECKSUM_OFFSET = 16;
+    public static final byte TCPHDR_FIN = (byte) (1 << 0);
+    public static final byte TCPHDR_SYN = (byte) (1 << 1);
+    public static final byte TCPHDR_RST = (byte) (1 << 2);
+    public static final byte TCPHDR_PSH = (byte) (1 << 3);
+    public static final byte TCPHDR_ACK = (byte) (1 << 4);
+    public static final byte TCPHDR_URG = (byte) (1 << 5);
+
+    /**
+     * UDP constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc768
+     */
+    public static final int UDP_HEADER_LEN = 8;
+    public static final int UDP_SRCPORT_OFFSET = 0;
+    public static final int UDP_DSTPORT_OFFSET = 2;
+    public static final int UDP_LENGTH_OFFSET = 4;
+    public static final int UDP_CHECKSUM_OFFSET = 6;
+
+    /**
+     * DHCP constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc2131
+     */
+    public static final int INFINITE_LEASE = 0xffffffff;
+    public static final int DHCP4_CLIENT_PORT = 68;
+    // The maximum length of a DHCP packet that can be constructed.
+    public static final int DHCP_MAX_LENGTH = 1500;
+    public static final int DHCP_MAX_OPTION_LEN = 255;
+
+    /**
+     * DHCPv6 constants.
+     *
+     * See also:
+     *     - https://datatracker.ietf.org/doc/html/rfc8415
+     *     - https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml
+     */
+    public static final int DHCP6_CLIENT_PORT = 546;
+    public static final int DHCP6_SERVER_PORT = 547;
+    public static final Inet6Address ALL_DHCP_RELAY_AGENTS_AND_SERVERS =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff02::1:2");
+    public static final int DHCP6_OPTION_IA_ADDR = 5;
+    public static final int DHCP6_OPTION_IA_PD = 25;
+    public static final int DHCP6_OPTION_IAPREFIX = 26;
+
+    /**
+     * DNS constants.
+     *
+     * See also:
+     *     - https://datatracker.ietf.org/doc/html/rfc7858#section-3.1
+     */
+    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:
+     *     - https://ieeexplore.ieee.org/document/7786995
+     */
+    public static final int VENDOR_SPECIFIC_IE_ID = 0xdd;
+
+
+    /**
+     * TrafficStats constants.
+     */
+    // These tags are used by the network stack to do traffic for its own purposes. Traffic
+    // tagged with these will be counted toward the network stack and must stay inside the
+    // range defined by
+    // {@link android.net.TrafficStats#TAG_NETWORK_STACK_RANGE_START} and
+    // {@link android.net.TrafficStats#TAG_NETWORK_STACK_RANGE_END}.
+    public static final int TAG_SYSTEM_DHCP = 0xFFFFFE01;
+    public static final int TAG_SYSTEM_NEIGHBOR = 0xFFFFFE02;
+    public static final int TAG_SYSTEM_DHCP_SERVER = 0xFFFFFE03;
+
+    // These tags are used by the network stack to do traffic on behalf of apps. Traffic
+    // tagged with these will be counted toward the app on behalf of which the network
+    // stack is doing this traffic. These values must stay inside the range defined by
+    // {@link android.net.TrafficStats#TAG_NETWORK_STACK_IMPERSONATION_RANGE_START} and
+    // {@link android.net.TrafficStats#TAG_NETWORK_STACK_IMPERSONATION_RANGE_END}.
+    public static final int TAG_SYSTEM_PROBE = 0xFFFFFF81;
+    public static final int TAG_SYSTEM_DNS = 0xFFFFFF82;
+
+    /**
+     * A test URL used to override configuration settings and overlays for the network validation
+     * HTTPS URL, when set in {@link android.provider.DeviceConfig} configuration.
+     *
+     * <p>This URL will be ignored if the host is not "localhost" (it can only be used to test with
+     * a local test server), and must not be set in production scenarios (as enforced by CTS tests).
+     *
+     * <p>{@link #TEST_URL_EXPIRATION_TIME} must also be set to use this setting.
+     */
+    public static final String TEST_CAPTIVE_PORTAL_HTTPS_URL = "test_captive_portal_https_url";
+    /**
+     * A test URL used to override configuration settings and overlays for the network validation
+     * HTTP URL, when set in {@link android.provider.DeviceConfig} configuration.
+     *
+     * <p>This URL will be ignored if the host is not "localhost" (it can only be used to test with
+     * a local test server), and must not be set in production scenarios (as enforced by CTS tests).
+     *
+     * <p>{@link #TEST_URL_EXPIRATION_TIME} must also be set to use this setting.
+     */
+    public static final String TEST_CAPTIVE_PORTAL_HTTP_URL = "test_captive_portal_http_url";
+    /**
+     * Expiration time of the test URL, in ms, relative to {@link System#currentTimeMillis()}.
+     *
+     * <p>After this expiration time, test URLs will be ignored. They will also be ignored if
+     * the expiration time is more than 10 minutes in the future, to avoid misconfiguration
+     * following test runs.
+     */
+    public static final String TEST_URL_EXPIRATION_TIME = "test_url_expiration_time";
+
+    // TODO: Move to Inet4AddressUtils
+    // See aosp/1455936: NetworkStackConstants can't depend on it as it causes jarjar-related issues
+    // for users of both the net-utils-device-common and net-utils-framework-common libraries.
+    // Jarjar rule management needs to be simplified for that: b/170445871
+
+    /**
+     * Make an Inet4Address from 4 bytes in network byte order.
+     */
+    private static Inet4Address makeInet4Address(byte b1, byte b2, byte b3, byte b4) {
+        try {
+            return (Inet4Address) InetAddress.getByAddress(new byte[] { b1, b2, b3, b4 });
+        } catch (UnknownHostException e) {
+            throw new IllegalArgumentException("addr must be 4 bytes: this should never happen");
+        }
+    }
+
+    /**
+     * Make an Inet6Address from 16 bytes in network byte order.
+     */
+    private static Inet6Address makeInet6Address(byte[] bytes) {
+        try {
+            return (Inet6Address) InetAddress.getByAddress(bytes);
+        } catch (UnknownHostException e) {
+            throw new IllegalArgumentException("addr must be 16 bytes: this should never happen");
+        }
+    }
+    private NetworkStackConstants() {
+        throw new UnsupportedOperationException("This class is not to be instantiated");
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
new file mode 100644
index 0000000..41a9428
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
@@ -0,0 +1,175 @@
+/*
+ * 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.net.module.util;
+
+import android.app.usage.NetworkStats;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Various utilities used for NetworkStats related code.
+ *
+ * @hide
+ */
+public class NetworkStatsUtils {
+    // These constants must be synced with the definition in android.net.NetworkStats.
+    // TODO: update to formal APIs once all downstreams have these APIs.
+    private static final int SET_ALL = -1;
+    private static final int METERED_ALL = -1;
+    private static final int ROAMING_ALL = -1;
+    private static final int DEFAULT_NETWORK_ALL = -1;
+
+    /**
+     * Safely multiple a value by a rational.
+     * <p>
+     * Internally it uses integer-based math whenever possible, but switches
+     * over to double-based math if values would overflow.
+     * @hide
+     */
+    public static long multiplySafeByRational(long value, long num, long den) {
+        if (den == 0) {
+            throw new ArithmeticException("Invalid Denominator");
+        }
+        long x = value;
+        long y = num;
+
+        // Logic shamelessly borrowed from Math.multiplyExact()
+        long r = x * y;
+        long ax = Math.abs(x);
+        long ay = Math.abs(y);
+        if (((ax | ay) >>> 31 != 0)) {
+            // Some bits greater than 2^31 that might cause overflow
+            // Check the result using the divide operator
+            // and check for the special case of Long.MIN_VALUE * -1
+            if (((y != 0) && (r / y != x))
+                    || (x == Long.MIN_VALUE && y == -1)) {
+                // Use double math to avoid overflowing
+                return (long) (((double) num / den) * value);
+            }
+        }
+        return r / den;
+    }
+
+    /**
+     * Value of the match rule of the subscriberId to match networks with specific subscriberId.
+     *
+     * @hide
+     */
+    public static final int SUBSCRIBER_ID_MATCH_RULE_EXACT = 0;
+    /**
+     * Value of the match rule of the subscriberId to match networks with any subscriberId which
+     * includes null and non-null.
+     *
+     * @hide
+     */
+    public static final int SUBSCRIBER_ID_MATCH_RULE_ALL = 1;
+
+    /**
+     * Name representing {@link #bandwidthSetGlobalAlert(long)} limit when delivered to
+     * {@link AlertObserver#onQuotaLimitReached(String, String)}.
+     */
+    public static final String LIMIT_GLOBAL_ALERT = "globalAlert";
+
+    /**
+     * Return the constrained value by given the lower and upper bounds.
+     */
+    public static int constrain(int amount, int low, int high) {
+        if (low > high) throw new IllegalArgumentException("low(" + low + ") > high(" + high + ")");
+        return amount < low ? low : (amount > high ? high : amount);
+    }
+
+    /**
+     * Return the constrained value by given the lower and upper bounds.
+     */
+    public static long constrain(long amount, long low, long high) {
+        if (low > high) throw new IllegalArgumentException("low(" + low + ") > high(" + high + ")");
+        return amount < low ? low : (amount > high ? high : amount);
+    }
+
+    /**
+     * Convert structure from android.app.usage.NetworkStats to android.net.NetworkStats.
+     */
+    public static android.net.NetworkStats fromPublicNetworkStats(
+            NetworkStats publiceNetworkStats) {
+        android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);
+        while (publiceNetworkStats.hasNextBucket()) {
+            NetworkStats.Bucket bucket = new NetworkStats.Bucket();
+            publiceNetworkStats.getNextBucket(bucket);
+            final android.net.NetworkStats.Entry entry = fromBucket(bucket);
+            stats = stats.addEntry(entry);
+        }
+        return stats;
+    }
+
+    @VisibleForTesting
+    public static android.net.NetworkStats.Entry fromBucket(NetworkStats.Bucket bucket) {
+        return new android.net.NetworkStats.Entry(
+                null /* IFACE_ALL */, bucket.getUid(), convertBucketState(bucket.getState()),
+                convertBucketTag(bucket.getTag()), convertBucketMetered(bucket.getMetered()),
+                convertBucketRoaming(bucket.getRoaming()),
+                convertBucketDefaultNetworkStatus(bucket.getDefaultNetworkStatus()),
+                bucket.getRxBytes(), bucket.getRxPackets(),
+                bucket.getTxBytes(), bucket.getTxPackets(), 0 /* operations */);
+    }
+
+    private static int convertBucketState(int networkStatsSet) {
+        switch (networkStatsSet) {
+            case NetworkStats.Bucket.STATE_ALL: return SET_ALL;
+            case NetworkStats.Bucket.STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT;
+            case NetworkStats.Bucket.STATE_FOREGROUND:
+                return android.net.NetworkStats.SET_FOREGROUND;
+        }
+        return 0;
+    }
+
+    private static int convertBucketTag(int tag) {
+        switch (tag) {
+            case NetworkStats.Bucket.TAG_NONE: return android.net.NetworkStats.TAG_NONE;
+        }
+        return tag;
+    }
+
+    private static int convertBucketMetered(int metered) {
+        switch (metered) {
+            case NetworkStats.Bucket.METERED_ALL: return METERED_ALL;
+            case NetworkStats.Bucket.METERED_NO: return android.net.NetworkStats.METERED_NO;
+            case NetworkStats.Bucket.METERED_YES: return android.net.NetworkStats.METERED_YES;
+        }
+        return 0;
+    }
+
+    private static int convertBucketRoaming(int roaming) {
+        switch (roaming) {
+            case NetworkStats.Bucket.ROAMING_ALL: return ROAMING_ALL;
+            case NetworkStats.Bucket.ROAMING_NO: return android.net.NetworkStats.ROAMING_NO;
+            case NetworkStats.Bucket.ROAMING_YES: return android.net.NetworkStats.ROAMING_YES;
+        }
+        return 0;
+    }
+
+    private static int convertBucketDefaultNetworkStatus(int defaultNetworkStatus) {
+        switch (defaultNetworkStatus) {
+            case NetworkStats.Bucket.DEFAULT_NETWORK_ALL:
+                return DEFAULT_NETWORK_ALL;
+            case NetworkStats.Bucket.DEFAULT_NETWORK_NO:
+                return android.net.NetworkStats.DEFAULT_NETWORK_NO;
+            case NetworkStats.Bucket.DEFAULT_NETWORK_YES:
+                return android.net.NetworkStats.DEFAULT_NETWORK_YES;
+        }
+        return 0;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/PerUidCounter.java b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
new file mode 100644
index 0000000..463b0c4
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
@@ -0,0 +1,94 @@
+/*
+ * 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.net.module.util;
+
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Keeps track of the counters under different uid, fire exception if the counter
+ * exceeded the specified maximum value.
+ *
+ * @hide
+ */
+public class PerUidCounter {
+    private final int mMaxCountPerUid;
+
+    // Map from UID to count that UID has filed.
+    @VisibleForTesting
+    @GuardedBy("this")
+    final SparseIntArray mUidToCount = new SparseIntArray();
+
+    /**
+     * Constructor
+     *
+     * @param maxCountPerUid the maximum count per uid allowed
+     */
+    public PerUidCounter(final int maxCountPerUid) {
+        if (maxCountPerUid <= 0) {
+            throw new IllegalArgumentException("Maximum counter value must be positive");
+        }
+        mMaxCountPerUid = maxCountPerUid;
+    }
+
+    /**
+     * Increments the count of the given uid.  Throws an exception if the number
+     * of the counter for the uid exceeds the value of maxCounterPerUid which is the value
+     * passed into the constructor. see: {@link #PerUidCounter(int)}.
+     *
+     * @throws IllegalStateException if the number of counter for the uid exceed
+     *         the allowed number.
+     *
+     * @param uid the uid that the counter was made under
+     */
+    public synchronized void incrementCountOrThrow(final int uid) {
+        final long newCount = ((long) mUidToCount.get(uid, 0)) + 1;
+        if (newCount > mMaxCountPerUid) {
+            throw new IllegalStateException("Uid " + uid + " exceeded its allowed limit");
+        }
+        // Since the count cannot be greater than Integer.MAX_VALUE here since mMaxCountPerUid
+        // is an integer, it is safe to cast to int.
+        mUidToCount.put(uid, (int) newCount);
+    }
+
+    /**
+     * Decrements the count of the given uid. Throws an exception if the number
+     * of the counter goes below zero.
+     *
+     * @throws IllegalStateException if the number of counter for the uid goes below
+     *         zero.
+     *
+     * @param uid the uid that the count was made under
+     */
+    public synchronized void decrementCountOrThrow(final int uid) {
+        final int newCount = mUidToCount.get(uid, 0) - 1;
+        if (newCount < 0) {
+            throw new IllegalStateException("BUG: too small count " + newCount + " for UID " + uid);
+        } else if (newCount == 0) {
+            mUidToCount.delete(uid);
+        } else {
+            mUidToCount.put(uid, newCount);
+        }
+    }
+
+    @VisibleForTesting
+    public synchronized int get(int uid) {
+        return mUidToCount.get(uid, 0);
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
new file mode 100644
index 0000000..0d7d96f
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 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.net.module.util;
+
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
+import static android.Manifest.permission.NETWORK_STACK;
+import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+
+import android.annotation.CheckResult;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.UserHandle;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Collection of permission utilities.
+ * @hide
+ */
+public final class PermissionUtils {
+    /**
+     * Return true if the context has one of given permission.
+     */
+    @CheckResult
+    public static boolean hasAnyPermissionOf(@NonNull Context context,
+                                             @NonNull String... permissions) {
+        for (String permission : permissions) {
+            if (context.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return true if the context has one of given permission that is allowed
+     * for a particular process and user ID running in the system.
+     */
+    @CheckResult
+    public static boolean hasAnyPermissionOf(@NonNull Context context,
+                                             int pid, int uid, @NonNull String... permissions) {
+        for (String permission : permissions) {
+            if (context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Enforce permission check on the context that should have one of given permission.
+     */
+    public static void enforceAnyPermissionOf(@NonNull Context context,
+            @NonNull String... permissions) {
+        if (!hasAnyPermissionOf(context, permissions)) {
+            throw new SecurityException("Requires one of the following permissions: "
+                    + String.join(", ", permissions) + ".");
+        }
+    }
+
+    /**
+     * If the NetworkStack, MAINLINE_NETWORK_STACK are not allowed for a particular process, throw a
+     * {@link SecurityException}.
+     *
+     * @param context {@link android.content.Context} for the process.
+     */
+    public static void enforceNetworkStackPermission(final @NonNull Context context) {
+        enforceNetworkStackPermissionOr(context);
+    }
+
+    /**
+     * If the NetworkStack, MAINLINE_NETWORK_STACK or other specified permissions are not allowed
+     * for a particular process, throw a {@link SecurityException}.
+     *
+     * @param context {@link android.content.Context} for the process.
+     * @param otherPermissions The set of permissions that could be the candidate permissions , or
+     *                         empty string if none of other permissions needed.
+     */
+    public static void enforceNetworkStackPermissionOr(final @NonNull Context context,
+            final @NonNull String... otherPermissions) {
+        ArrayList<String> permissions = new ArrayList<String>(Arrays.asList(otherPermissions));
+        permissions.add(NETWORK_STACK);
+        permissions.add(PERMISSION_MAINLINE_NETWORK_STACK);
+        enforceAnyPermissionOf(context, permissions.toArray(new String[0]));
+    }
+
+    /**
+     * If the CONNECTIVITY_USE_RESTRICTED_NETWORKS is not allowed for a particular process, throw a
+     * {@link SecurityException}.
+     *
+     * @param context {@link android.content.Context} for the process.
+     * @param message A message to include in the exception if it is thrown.
+     */
+    public static void enforceRestrictedNetworkPermission(
+            final @NonNull Context context, final @Nullable String message) {
+        context.enforceCallingOrSelfPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, message);
+    }
+
+    /**
+     * If the ACCESS_NETWORK_STATE is not allowed for a particular process, throw a
+     * {@link SecurityException}.
+     *
+     * @param context {@link android.content.Context} for the process.
+     * @param message A message to include in the exception if it is thrown.
+     */
+    public static void enforceAccessNetworkStatePermission(
+            final @NonNull Context context, final @Nullable String message) {
+        context.enforceCallingOrSelfPermission(ACCESS_NETWORK_STATE, message);
+    }
+
+    /**
+     * Return true if the context has DUMP permission.
+     */
+    @CheckResult
+    public static boolean hasDumpPermission(Context context, String tag, PrintWriter pw) {
+        if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PERMISSION_GRANTED) {
+            pw.println("Permission Denial: can't dump " + tag + " from from pid="
+                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+                    + " due to missing android.permission.DUMP permission");
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Enforce that a given feature is available and if not, throw an
+     * {@link UnsupportedOperationException}.
+     *
+     * @param context {@link android.content.Context} for the process.
+     * @param feature the feature name to enforce.
+     * @param errorMessage an optional error message to include.
+     */
+    public static void enforceSystemFeature(final @NonNull Context context,
+            final @NonNull String feature, final @Nullable String errorMessage) {
+        final boolean hasSystemFeature =
+                context.getPackageManager().hasSystemFeature(feature);
+        if (!hasSystemFeature) {
+            if (null == errorMessage) {
+                throw new UnsupportedOperationException();
+            }
+            throw new UnsupportedOperationException(errorMessage);
+        }
+    }
+
+    /**
+     * Get the list of granted permissions for a package info.
+     *
+     * PackageInfo contains the list of requested permissions, and their state (whether they
+     * were granted or not, in particular) as a parallel array. Most users care only about
+     * granted permissions. This method returns the list of them.
+     *
+     * @param packageInfo the package info for the relevant uid.
+     * @return the list of granted permissions.
+     */
+    public static List<String> getGrantedPermissions(final @NonNull PackageInfo packageInfo) {
+        if (null == packageInfo.requestedPermissions) return Collections.emptyList();
+        final ArrayList<String> result = new ArrayList<>(packageInfo.requestedPermissions.length);
+        for (int i = 0; i < packageInfo.requestedPermissions.length; ++i) {
+            if (0 != (REQUESTED_PERMISSION_GRANTED & packageInfo.requestedPermissionsFlags[i])) {
+                result.add(packageInfo.requestedPermissions[i]);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Enforces that the given package name belongs to the given uid.
+     *
+     * @param context {@link android.content.Context} for the process.
+     * @param uid User ID to check the package ownership for.
+     * @param packageName Package name to verify.
+     * @throws SecurityException If the package does not belong to the specified uid.
+     */
+    public static void enforcePackageNameMatchesUid(
+            @NonNull Context context, int uid, @Nullable String packageName) {
+        final UserHandle user = UserHandle.getUserHandleForUid(uid);
+        if (getAppUid(context, packageName, user) != uid) {
+            throw new SecurityException(packageName + " does not belong to uid " + uid);
+        }
+    }
+
+    private static int getAppUid(Context context, final String app, final UserHandle user) {
+        final PackageManager pm =
+                context.createContextAsUser(user, 0 /* flags */).getPackageManager();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return pm.getPackageUid(app, 0 /* flags */);
+        } catch (PackageManager.NameNotFoundException e) {
+            return -1;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/ProxyUtils.java b/staticlibs/framework/com/android/net/module/util/ProxyUtils.java
new file mode 100644
index 0000000..fdd7dca
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/ProxyUtils.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 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.text.TextUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Collection of network common utilities.
+ *
+ * @hide
+ */
+public final class ProxyUtils {
+
+    public static final int PROXY_VALID             = 0;
+    public static final int PROXY_HOSTNAME_EMPTY    = 1;
+    public static final int PROXY_HOSTNAME_INVALID  = 2;
+    public static final int PROXY_PORT_EMPTY        = 3;
+    public static final int PROXY_PORT_INVALID      = 4;
+    public static final int PROXY_EXCLLIST_INVALID  = 5;
+
+    // Hostname / IP REGEX validation
+    // Matches blank input, ips, and domain names
+    private static final String NAME_IP_REGEX =
+            "[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*(\\.[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*)*";
+    private static final Pattern HOSTNAME_PATTERN;
+    private static final String HOSTNAME_REGEXP = "^$|^" + NAME_IP_REGEX + "$";
+    private static final Pattern EXCLLIST_PATTERN;
+    private static final String EXCL_REGEX =
+            "[a-zA-Z0-9*]+(\\-[a-zA-Z0-9*]+)*(\\.[a-zA-Z0-9*]+(\\-[a-zA-Z0-9*]+)*)*";
+    private static final String EXCLLIST_REGEXP = "^$|^" + EXCL_REGEX + "(," + EXCL_REGEX + ")*$";
+    static {
+        HOSTNAME_PATTERN = Pattern.compile(HOSTNAME_REGEXP);
+        EXCLLIST_PATTERN = Pattern.compile(EXCLLIST_REGEXP);
+    }
+
+    /** Converts exclusion list from String to List. */
+    public static List<String> exclusionStringAsList(String exclusionList) {
+        if (exclusionList == null) {
+            return Collections.emptyList();
+        }
+        return Arrays.asList(exclusionList.toLowerCase(Locale.ROOT).split(","));
+    }
+
+    /** Converts exclusion list from List to string */
+    public static String exclusionListAsString(String[] exclusionList) {
+        if (exclusionList == null) {
+            return "";
+        }
+        return TextUtils.join(",", exclusionList);
+    }
+
+    /**
+     * Validate syntax of hostname, port and exclusion list entries
+     */
+    public static int validate(String hostname, String port, String exclList) {
+        Matcher match = HOSTNAME_PATTERN.matcher(hostname);
+        Matcher listMatch = EXCLLIST_PATTERN.matcher(exclList);
+
+        if (!match.matches()) return PROXY_HOSTNAME_INVALID;
+
+        if (!listMatch.matches()) return PROXY_EXCLLIST_INVALID;
+
+        if (hostname.length() > 0 && port.length() == 0) return PROXY_PORT_EMPTY;
+
+        if (port.length() > 0) {
+            if (hostname.length() == 0) return PROXY_HOSTNAME_EMPTY;
+            int portVal = -1;
+            try {
+                portVal = Integer.parseInt(port);
+            } catch (NumberFormatException ex) {
+                return PROXY_PORT_INVALID;
+            }
+            if (portVal <= 0 || portVal > 0xFFFF) return PROXY_PORT_INVALID;
+        }
+        return PROXY_VALID;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/RouteUtils.java b/staticlibs/framework/com/android/net/module/util/RouteUtils.java
new file mode 100644
index 0000000..c241680
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/RouteUtils.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+/** @hide */
+// RouteUtils is now empty, because some new methods will be added to it soon and it is less
+// expensive to keep it empty than to remove it now and add it again later.
+public class RouteUtils {
+}
diff --git a/staticlibs/framework/com/android/net/module/util/SdkUtil.java b/staticlibs/framework/com/android/net/module/util/SdkUtil.java
new file mode 100644
index 0000000..5006ba9
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/SdkUtil.java
@@ -0,0 +1,49 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.Nullable;
+
+/**
+ * Utilities to deal with multiple SDKs in a single mainline module.
+ * @hide
+ */
+public class SdkUtil {
+    /**
+     * Holder class taking advantage of erasure to avoid reflection running into class not found
+     * exceptions.
+     *
+     * This is useful to store a reference to a class that might not be present at runtime when
+     * fields are examined through reflection. An example is the MessageUtils class, which tries
+     * to get all fields in a class and therefore will try to load any class for which there
+     * is a member. Another example would be arguments or return values of methods in tests,
+     * when the testing framework uses reflection to list methods and their arguments.
+     *
+     * In these cases, LateSdk<T> can be used to hide type T from reflection, since it's erased
+     * and it becomes a vanilla LateSdk in Java bytecode. The T still can't be instantiated at
+     * runtime of course, but runtime tests will avoid that.
+     *
+     * @param <T> The held type
+     * @hide
+     */
+    public static class LateSdk<T> {
+        @Nullable public final T value;
+        public LateSdk(@Nullable final T value) {
+            this.value = value;
+        }
+    }
+}
diff --git a/staticlibs/jarjar-rules-shared.txt b/staticlibs/jarjar-rules-shared.txt
new file mode 100644
index 0000000..e26999d
--- /dev/null
+++ b/staticlibs/jarjar-rules-shared.txt
@@ -0,0 +1,3 @@
+rule android.annotation.** com.android.net.module.annotation.@1
+rule com.android.internal.annotations.** com.android.net.module.annotation.@1
+rule com.android.modules.utils.build.** com.android.net.module.util.build.$1
diff --git a/staticlibs/native/README.md b/staticlibs/native/README.md
new file mode 100644
index 0000000..1f505c4
--- /dev/null
+++ b/staticlibs/native/README.md
@@ -0,0 +1,30 @@
+# JNI
+As a general rule, jarjar every static library dependency used in a mainline module into the
+modules's namespace (especially if it is also used by other modules)
+
+Fully-qualified name of java class needs to be hard-coded into the JNI .so, because JNI_OnLoad
+does not take any parameters. This means that there needs to be a different .so target for each
+post-jarjared package, so for each module.
+
+This is the guideline to provide JNI library shared with modules:
+
+* provide a common java library in frameworks/libs/net with the Java class (e.g. BpfMap.java).
+
+* provide a common native library in frameworks/libs/net with the JNI and provide the native
+  register function with class_name parameter. See register_com_android_net_module_util_BpfMap
+  function in frameworks/libs/net/common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+  as an example.
+
+When you want to use JNI library from frameworks/lib/net:
+
+* Each module includes the java library (e.g. net-utils-device-common-bpf) and applies its jarjar
+  rules after build.
+
+* Each module creates a native library in their directory, which statically links against the
+  common native library (e.g. libnet_utils_device_common_bpf), and calls the native registered
+  function by hardcoding the post-jarjar class_name. Linkage *MUST* be static because common
+  functions in the file (e.g., `register_com_android_net_module_util_BpfMap`) will appear in the
+  library (`.so`) file, and different versions of the library loaded in the same process by
+  different modules will in general have different versions. It's important that each of these
+  libraries loads the common function from its own library. Static linkage should guarantee this
+  because static linkage resolves symbols at build time, not runtime.
\ No newline at end of file
diff --git a/staticlibs/native/bpf_headers/Android.bp b/staticlibs/native/bpf_headers/Android.bp
new file mode 100644
index 0000000..d55584a
--- /dev/null
+++ b/staticlibs/native/bpf_headers/Android.bp
@@ -0,0 +1,66 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_headers {
+    name: "bpf_headers",
+    vendor_available: true,
+    recovery_available: true,
+    host_supported: true,
+    native_bridge_supported: true,
+    header_libs: ["bpf_syscall_wrappers"],
+    export_header_lib_headers: ["bpf_syscall_wrappers"],
+    export_include_dirs: ["include"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    sdk_version: "30",
+    min_sdk_version: "30",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.art.debug",
+        "com.android.os.statsd",
+        "com.android.resolv",
+        "com.android.tethering",
+    ],
+}
+
+cc_test {
+    // TODO: Rename to bpf_map_test and modify .gcls as well.
+    name: "libbpf_android_test",
+    srcs: [
+        "BpfMapTest.cpp",
+        "BpfRingbufTest.cpp",
+    ],
+    defaults: ["bpf_defaults"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-error=unused-variable",
+    ],
+    header_libs: ["bpf_headers"],
+    static_libs: ["libgmock"],
+    shared_libs: [
+        "libbase",
+        "liblog",
+        "libutils",
+    ],
+    require_root: true,
+    test_suites: ["general-tests"],
+}
diff --git a/staticlibs/native/bpf_headers/BpfMapTest.cpp b/staticlibs/native/bpf_headers/BpfMapTest.cpp
new file mode 100644
index 0000000..862114d
--- /dev/null
+++ b/staticlibs/native/bpf_headers/BpfMapTest.cpp
@@ -0,0 +1,254 @@
+/*
+ * 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.
+ */
+
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <vector>
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/inet_diag.h>
+#include <linux/sock_diag.h>
+#include <net/if.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <gtest/gtest.h>
+
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+#include "bpf/BpfMap.h"
+#include "bpf/BpfUtils.h"
+
+using ::testing::Test;
+
+namespace android {
+namespace bpf {
+
+using base::Result;
+using base::unique_fd;
+
+constexpr uint32_t TEST_MAP_SIZE = 10;
+constexpr uint32_t TEST_KEY1 = 1;
+constexpr uint32_t TEST_VALUE1 = 10;
+constexpr const char PINNED_MAP_PATH[] = "/sys/fs/bpf/testMap";
+
+class BpfMapTest : public testing::Test {
+  protected:
+    BpfMapTest() {}
+
+    void SetUp() {
+        EXPECT_EQ(0, setrlimitForTest());
+        if (!access(PINNED_MAP_PATH, R_OK)) {
+            EXPECT_EQ(0, remove(PINNED_MAP_PATH));
+        }
+    }
+
+    void TearDown() {
+        if (!access(PINNED_MAP_PATH, R_OK)) {
+            EXPECT_EQ(0, remove(PINNED_MAP_PATH));
+        }
+    }
+
+    void checkMapInvalid(BpfMap<uint32_t, uint32_t>& map) {
+        EXPECT_FALSE(map.isValid());
+        EXPECT_EQ(-1, map.getMap().get());
+    }
+
+    void checkMapValid(BpfMap<uint32_t, uint32_t>& map) {
+        EXPECT_LE(0, map.getMap().get());
+        EXPECT_TRUE(map.isValid());
+    }
+
+    void writeToMapAndCheck(BpfMap<uint32_t, uint32_t>& map, uint32_t key, uint32_t value) {
+        ASSERT_RESULT_OK(map.writeValue(key, value, BPF_ANY));
+        uint32_t value_read;
+        ASSERT_EQ(0, findMapEntry(map.getMap(), &key, &value_read));
+        checkValueAndStatus(value, value_read);
+    }
+
+    void checkValueAndStatus(uint32_t refValue, Result<uint32_t> value) {
+        ASSERT_RESULT_OK(value);
+        ASSERT_EQ(refValue, value.value());
+    }
+
+    void populateMap(uint32_t total, BpfMap<uint32_t, uint32_t>& map) {
+        for (uint32_t key = 0; key < total; key++) {
+            uint32_t value = key * 10;
+            EXPECT_RESULT_OK(map.writeValue(key, value, BPF_ANY));
+        }
+    }
+
+    void expectMapEmpty(BpfMap<uint32_t, uint32_t>& map) {
+        Result<bool> isEmpty = map.isEmpty();
+        ASSERT_RESULT_OK(isEmpty);
+        ASSERT_TRUE(isEmpty.value());
+    }
+};
+
+TEST_F(BpfMapTest, constructor) {
+    BpfMap<uint32_t, uint32_t> testMap1;
+    checkMapInvalid(testMap1);
+
+    BpfMap<uint32_t, uint32_t> testMap2;
+    ASSERT_RESULT_OK(testMap2.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
+    checkMapValid(testMap2);
+}
+
+TEST_F(BpfMapTest, basicHelpers) {
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
+    uint32_t key = TEST_KEY1;
+    uint32_t value_write = TEST_VALUE1;
+    writeToMapAndCheck(testMap, key, value_write);
+    Result<uint32_t> value_read = testMap.readValue(key);
+    checkValueAndStatus(value_write, value_read);
+    Result<uint32_t> key_read = testMap.getFirstKey();
+    checkValueAndStatus(key, key_read);
+    ASSERT_RESULT_OK(testMap.deleteValue(key));
+    ASSERT_GT(0, findMapEntry(testMap.getMap(), &key, &value_read));
+    ASSERT_EQ(ENOENT, errno);
+}
+
+TEST_F(BpfMapTest, reset) {
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
+    uint32_t key = TEST_KEY1;
+    uint32_t value_write = TEST_VALUE1;
+    writeToMapAndCheck(testMap, key, value_write);
+
+    testMap.reset(-1);
+    checkMapInvalid(testMap);
+    ASSERT_GT(0, findMapEntry(testMap.getMap(), &key, &value_write));
+    ASSERT_EQ(EBADF, errno);
+}
+
+TEST_F(BpfMapTest, moveConstructor) {
+    BpfMap<uint32_t, uint32_t> testMap1;
+    ASSERT_RESULT_OK(testMap1.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
+    BpfMap<uint32_t, uint32_t> testMap2;
+    testMap2 = std::move(testMap1);
+    uint32_t key = TEST_KEY1;
+    checkMapInvalid(testMap1);
+    uint32_t value = TEST_VALUE1;
+    writeToMapAndCheck(testMap2, key, value);
+}
+
+TEST_F(BpfMapTest, SetUpMap) {
+    EXPECT_NE(0, access(PINNED_MAP_PATH, R_OK));
+    BpfMap<uint32_t, uint32_t> testMap1;
+    ASSERT_RESULT_OK(testMap1.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
+    ASSERT_EQ(0, bpfFdPin(testMap1.getMap(), PINNED_MAP_PATH));
+    EXPECT_EQ(0, access(PINNED_MAP_PATH, R_OK));
+    checkMapValid(testMap1);
+    BpfMap<uint32_t, uint32_t> testMap2;
+    EXPECT_RESULT_OK(testMap2.init(PINNED_MAP_PATH));
+    checkMapValid(testMap2);
+    uint32_t key = TEST_KEY1;
+    uint32_t value = TEST_VALUE1;
+    writeToMapAndCheck(testMap1, key, value);
+    Result<uint32_t> value_read = testMap2.readValue(key);
+    checkValueAndStatus(value, value_read);
+}
+
+TEST_F(BpfMapTest, iterate) {
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
+    populateMap(TEST_MAP_SIZE, testMap);
+    int totalCount = 0;
+    int totalSum = 0;
+    const auto iterateWithDeletion = [&totalCount, &totalSum](const uint32_t& key,
+                                                              BpfMap<uint32_t, uint32_t>& map) {
+        EXPECT_GE((uint32_t)TEST_MAP_SIZE, key);
+        totalCount++;
+        totalSum += key;
+        return map.deleteValue(key);
+    };
+    EXPECT_RESULT_OK(testMap.iterate(iterateWithDeletion));
+    EXPECT_EQ((int)TEST_MAP_SIZE, totalCount);
+    EXPECT_EQ(((1 + TEST_MAP_SIZE - 1) * (TEST_MAP_SIZE - 1)) / 2, (uint32_t)totalSum);
+    expectMapEmpty(testMap);
+}
+
+TEST_F(BpfMapTest, iterateWithValue) {
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
+    populateMap(TEST_MAP_SIZE, testMap);
+    int totalCount = 0;
+    int totalSum = 0;
+    const auto iterateWithDeletion = [&totalCount, &totalSum](const uint32_t& key,
+                                                              const uint32_t& value,
+                                                              BpfMap<uint32_t, uint32_t>& map) {
+        EXPECT_GE((uint32_t)TEST_MAP_SIZE, key);
+        EXPECT_EQ(value, key * 10);
+        totalCount++;
+        totalSum += value;
+        return map.deleteValue(key);
+    };
+    EXPECT_RESULT_OK(testMap.iterateWithValue(iterateWithDeletion));
+    EXPECT_EQ((int)TEST_MAP_SIZE, totalCount);
+    EXPECT_EQ(((1 + TEST_MAP_SIZE - 1) * (TEST_MAP_SIZE - 1)) * 5, (uint32_t)totalSum);
+    expectMapEmpty(testMap);
+}
+
+TEST_F(BpfMapTest, mapIsEmpty) {
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
+    expectMapEmpty(testMap);
+    uint32_t key = TEST_KEY1;
+    uint32_t value_write = TEST_VALUE1;
+    writeToMapAndCheck(testMap, key, value_write);
+    Result<bool> isEmpty = testMap.isEmpty();
+    ASSERT_RESULT_OK(isEmpty);
+    ASSERT_FALSE(isEmpty.value());
+    ASSERT_RESULT_OK(testMap.deleteValue(key));
+    ASSERT_GT(0, findMapEntry(testMap.getMap(), &key, &value_write));
+    ASSERT_EQ(ENOENT, errno);
+    expectMapEmpty(testMap);
+    int entriesSeen = 0;
+    EXPECT_RESULT_OK(testMap.iterate(
+            [&entriesSeen](const unsigned int&,
+                           const BpfMap<unsigned int, unsigned int>&) -> Result<void> {
+                entriesSeen++;
+                return {};
+            }));
+    EXPECT_EQ(0, entriesSeen);
+    EXPECT_RESULT_OK(testMap.iterateWithValue(
+            [&entriesSeen](const unsigned int&, const unsigned int&,
+                           const BpfMap<unsigned int, unsigned int>&) -> Result<void> {
+                entriesSeen++;
+                return {};
+            }));
+    EXPECT_EQ(0, entriesSeen);
+}
+
+TEST_F(BpfMapTest, mapClear) {
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE));
+    populateMap(TEST_MAP_SIZE, testMap);
+    Result<bool> isEmpty = testMap.isEmpty();
+    ASSERT_RESULT_OK(isEmpty);
+    ASSERT_FALSE(*isEmpty);
+    ASSERT_RESULT_OK(testMap.clear());
+    expectMapEmpty(testMap);
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
new file mode 100644
index 0000000..e81fb92
--- /dev/null
+++ b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ */
+
+#include <android-base/file.h>
+#include <android-base/macros.h>
+#include <android-base/result-gmock.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "BpfSyscallWrappers.h"
+#include "bpf/BpfRingbuf.h"
+#include "bpf/BpfUtils.h"
+#include "bpf/KernelUtils.h"
+
+#define TEST_RINGBUF_MAGIC_NUM 12345
+
+namespace android {
+namespace bpf {
+using ::android::base::testing::HasError;
+using ::android::base::testing::HasValue;
+using ::android::base::testing::WithCode;
+using ::testing::AllOf;
+using ::testing::Gt;
+using ::testing::HasSubstr;
+using ::testing::Lt;
+
+class BpfRingbufTest : public ::testing::Test {
+ protected:
+  BpfRingbufTest()
+      : mProgPath("/sys/fs/bpf/prog_bpfRingbufProg_skfilter_ringbuf_test"),
+        mRingbufPath("/sys/fs/bpf/map_bpfRingbufProg_test_ringbuf") {}
+
+  void SetUp() {
+    if (!android::bpf::isAtLeastKernelVersion(5, 8, 0)) {
+      GTEST_SKIP() << "BPF ring buffers not supported below 5.8";
+    }
+
+    errno = 0;
+    mProgram.reset(retrieveProgram(mProgPath.c_str()));
+    EXPECT_EQ(errno, 0);
+    ASSERT_GE(mProgram.get(), 0)
+        << mProgPath << " was either not found or inaccessible.";
+  }
+
+  void RunProgram() {
+    char fake_skb[128] = {};
+    EXPECT_EQ(runProgram(mProgram, fake_skb, sizeof(fake_skb)), 0);
+  }
+
+  void RunTestN(int n) {
+    int run_count = 0;
+    uint64_t output = 0;
+    auto callback = [&](const uint64_t& value) {
+      output = value;
+      run_count++;
+    };
+
+    auto result = BpfRingbuf<uint64_t>::Create(mRingbufPath.c_str());
+    ASSERT_RESULT_OK(result);
+    EXPECT_TRUE(result.value()->isEmpty());
+
+    struct timespec t1, t2;
+    EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t1));
+    EXPECT_FALSE(result.value()->wait(1000 /*ms*/));  // false because wait should timeout
+    EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t2));
+    long long time1 = t1.tv_sec * 1000000000LL + t1.tv_nsec;
+    long long time2 = t2.tv_sec * 1000000000LL + t2.tv_nsec;
+    EXPECT_GE(time2 - time1, 1000000000 /*ns*/);  // 1000 ms as ns
+
+    for (int i = 0; i < n; i++) {
+      RunProgram();
+    }
+
+    EXPECT_FALSE(result.value()->isEmpty());
+
+    EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t1));
+    EXPECT_TRUE(result.value()->wait());
+    EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t2));
+    time1 = t1.tv_sec * 1000000000LL + t1.tv_nsec;
+    time2 = t2.tv_sec * 1000000000LL + t2.tv_nsec;
+    EXPECT_LE(time2 - time1, 1000000 /*ns*/);  // in x86 CF testing < 5000 ns
+
+    EXPECT_THAT(result.value()->ConsumeAll(callback), HasValue(n));
+    EXPECT_TRUE(result.value()->isEmpty());
+    EXPECT_EQ(output, TEST_RINGBUF_MAGIC_NUM);
+    EXPECT_EQ(run_count, n);
+  }
+
+  std::string mProgPath;
+  std::string mRingbufPath;
+  android::base::unique_fd mProgram;
+};
+
+TEST_F(BpfRingbufTest, ConsumeSingle) { RunTestN(1); }
+TEST_F(BpfRingbufTest, ConsumeMultiple) { RunTestN(3); }
+
+TEST_F(BpfRingbufTest, FillAndWrap) {
+  int run_count = 0;
+  auto callback = [&](const uint64_t&) { run_count++; };
+
+  auto result = BpfRingbuf<uint64_t>::Create(mRingbufPath.c_str());
+  ASSERT_RESULT_OK(result);
+
+  // 4kb buffer with 16 byte payloads (8 byte data, 8 byte header) should fill
+  // after 255 iterations. Exceed that so that some events are dropped.
+  constexpr int iterations = 300;
+  for (int i = 0; i < iterations; i++) {
+    RunProgram();
+  }
+
+  // Some events were dropped, but consume all that succeeded.
+  EXPECT_THAT(result.value()->ConsumeAll(callback),
+              HasValue(AllOf(Gt(250), Lt(260))));
+  EXPECT_THAT(run_count, AllOf(Gt(250), Lt(260)));
+
+  // After consuming everything, we should be able to use the ring buffer again.
+  run_count = 0;
+  RunProgram();
+  EXPECT_THAT(result.value()->ConsumeAll(callback), HasValue(1));
+  EXPECT_EQ(run_count, 1);
+}
+
+TEST_F(BpfRingbufTest, WrongTypeSize) {
+  // The program under test writes 8-byte uint64_t values so a ringbuffer for
+  // 1-byte uint8_t values will fail to read from it. Note that the map_def does
+  // not specify the value size, so we fail on read, not creation.
+  auto result = BpfRingbuf<uint8_t>::Create(mRingbufPath.c_str());
+  ASSERT_RESULT_OK(result);
+
+  RunProgram();
+
+  EXPECT_THAT(result.value()->ConsumeAll([](const uint8_t&) {}),
+              HasError(WithCode(EMSGSIZE)));
+}
+
+TEST_F(BpfRingbufTest, InvalidPath) {
+  EXPECT_THAT(BpfRingbuf<int>::Create("/sys/fs/bpf/bad_path"),
+              HasError(WithCode(ENOENT)));
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/staticlibs/native/bpf_headers/TEST_MAPPING b/staticlibs/native/bpf_headers/TEST_MAPPING
new file mode 100644
index 0000000..9ec8a40
--- /dev/null
+++ b/staticlibs/native/bpf_headers/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "libbpf_android_test"
+    }
+  ]
+}
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
new file mode 100644
index 0000000..81be37d
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
@@ -0,0 +1,184 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+// Accept the full packet
+#define BPF_ACCEPT BPF_STMT(BPF_RET | BPF_K, 0xFFFFFFFF)
+
+// Reject the packet
+#define BPF_REJECT BPF_STMT(BPF_RET | BPF_K, 0)
+
+// Note arguments to BPF_JUMP(opcode, operand, true_offset, false_offset)
+
+// If not equal, jump over count instructions
+#define BPF_JUMP_IF_NOT_EQUAL(v, count) \
+	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 0, (count))
+
+// *TWO* instructions: compare and if not equal jump over the accept statement
+#define BPF2_ACCEPT_IF_EQUAL(v) \
+	BPF_JUMP_IF_NOT_EQUAL((v), 1), \
+	BPF_ACCEPT
+
+// *TWO* instructions: compare and if equal jump over the reject statement
+#define BPF2_REJECT_IF_NOT_EQUAL(v) \
+	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 1, 0), \
+	BPF_REJECT
+
+// *TWO* instructions: compare and if greater or equal jump over the reject statement
+#define BPF2_REJECT_IF_LESS_THAN(v) \
+	BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (v), 1, 0), \
+	BPF_REJECT
+
+// *TWO* instructions: compare and if *NOT* greater jump over the reject statement
+#define BPF2_REJECT_IF_GREATER_THAN(v) \
+	BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, (v), 0, 1), \
+	BPF_REJECT
+
+// *THREE* instructions: compare and if *NOT* in range [lo, hi], jump over the reject statement
+#define BPF3_REJECT_IF_NOT_IN_RANGE(lo, hi) \
+	BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (lo), 0, 1), \
+	BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, (hi), 0, 1), \
+	BPF_REJECT
+
+// *TWO* instructions: compare and if none of the bits are set jump over the reject statement
+#define BPF2_REJECT_IF_ANY_MASKED_BITS_SET(v) \
+	BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, (v), 0, 1), \
+	BPF_REJECT
+
+// loads skb->protocol
+#define BPF_LOAD_SKB_PROTOCOL \
+	BPF_STMT(BPF_LD | BPF_H | BPF_ABS, (__u32)SKF_AD_OFF + SKF_AD_PROTOCOL)
+
+// 8-bit load relative to start of link layer (mac/ethernet) header.
+#define BPF_LOAD_MAC_RELATIVE_U8(ofs) \
+	BPF_STMT(BPF_LD | BPF_B | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
+
+// Big/Network Endian 16-bit load relative to start of link layer (mac/ethernet) header.
+#define BPF_LOAD_MAC_RELATIVE_BE16(ofs) \
+	BPF_STMT(BPF_LD | BPF_H | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
+
+// Big/Network Endian 32-bit load relative to start of link layer (mac/ethernet) header.
+#define BPF_LOAD_MAC_RELATIVE_BE32(ofs) \
+	BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
+
+// 8-bit load relative to start of network (IPv4/IPv6) header.
+#define BPF_LOAD_NET_RELATIVE_U8(ofs) \
+	BPF_STMT(BPF_LD | BPF_B | BPF_ABS, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 16-bit load relative to start of network (IPv4/IPv6) header.
+#define BPF_LOAD_NET_RELATIVE_BE16(ofs) \
+	BPF_STMT(BPF_LD | BPF_H | BPF_ABS, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 32-bit load relative to start of network (IPv4/IPv6) header.
+#define BPF_LOAD_NET_RELATIVE_BE32(ofs) \
+	BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (__u32)SKF_NET_OFF + (ofs))
+
+#define field_sizeof(struct_type,field) sizeof(((struct_type *)0)->field)
+
+// 8-bit load from IPv4 header field.
+#define BPF_LOAD_IPV4_U8(field) \
+	BPF_LOAD_NET_RELATIVE_U8(({ \
+	  _Static_assert(field_sizeof(struct iphdr, field) == 1, "field of wrong size"); \
+	  offsetof(iphdr, field); \
+	}))
+
+// Big/Network Endian 16-bit load from IPv4 header field.
+#define BPF_LOAD_IPV4_BE16(field) \
+	BPF_LOAD_NET_RELATIVE_BE16(({ \
+	  _Static_assert(field_sizeof(struct iphdr, field) == 2, "field of wrong size"); \
+	  offsetof(iphdr, field); \
+	}))
+
+// Big/Network Endian 32-bit load from IPv4 header field.
+#define BPF_LOAD_IPV4_BE32(field) \
+	BPF_LOAD_NET_RELATIVE_BE32(({ \
+	  _Static_assert(field_sizeof(struct iphdr, field) == 4, "field of wrong size"); \
+	  offsetof(iphdr, field); \
+	}))
+
+// 8-bit load from IPv6 header field.
+#define BPF_LOAD_IPV6_U8(field) \
+	BPF_LOAD_NET_RELATIVE_U8(({ \
+	  _Static_assert(field_sizeof(struct ipv6hdr, field) == 1, "field of wrong size"); \
+	  offsetof(ipv6hdr, field); \
+	}))
+
+// Big/Network Endian 16-bit load from IPv6 header field.
+#define BPF_LOAD_IPV6_BE16(field) \
+	BPF_LOAD_NET_RELATIVE_BE16(({ \
+	  _Static_assert(field_sizeof(struct ipv6hdr, field) == 2, "field of wrong size"); \
+	  offsetof(ipv6hdr, field); \
+	}))
+
+// Big/Network Endian 32-bit load from IPv6 header field.
+#define BPF_LOAD_IPV6_BE32(field) \
+	BPF_LOAD_NET_RELATIVE_BE32(({ \
+	  _Static_assert(field_sizeof(struct ipv6hdr, field) == 4, "field of wrong size"); \
+	  offsetof(ipv6hdr, field); \
+	}))
+
+// Load the length of the IPv4 header into X index register.
+// ie. X := 4 * IPv4.IHL, where IPv4.IHL is the bottom nibble
+// of the first byte of the IPv4 (aka network layer) header.
+#define BPF_LOADX_NET_RELATIVE_IPV4_HLEN \
+    BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, (__u32)SKF_NET_OFF)
+
+// Blindly assumes no IPv6 extension headers, just does X := 40
+// You may later adjust this as you parse through IPv6 ext hdrs.
+#define BPF_LOADX_CONSTANT_IPV6_HLEN \
+    BPF_STMT(BPF_LDX | BPF_W | BPF_IMM, sizeof(struct ipv6hdr))
+
+// NOTE: all the following require X to be setup correctly (v4: 20+, v6: 40+)
+
+// 8-bit load from L4 (TCP/UDP/...) header
+#define BPF_LOAD_NETX_RELATIVE_L4_U8(ofs) \
+    BPF_STMT(BPF_LD | BPF_B | BPF_IND, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 16-bit load from L4 (TCP/UDP/...) header
+#define BPF_LOAD_NETX_RELATIVE_L4_BE16(ofs) \
+    BPF_STMT(BPF_LD | BPF_H | BPF_IND, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 32-bit load from L4 (TCP/UDP/...) header
+#define BPF_LOAD_NETX_RELATIVE_L4_BE32(ofs) \
+    BPF_STMT(BPF_LD | BPF_W | BPF_IND, (__u32)SKF_NET_OFF + (ofs))
+
+// Both ICMPv4 and ICMPv6 start with u8 type, u8 code
+#define BPF_LOAD_NETX_RELATIVE_ICMP_TYPE BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+#define BPF_LOAD_NETX_RELATIVE_ICMP_CODE BPF_LOAD_NETX_RELATIVE_L4_U8(1)
+
+// IPv6 extension headers (HOPOPTS, DSTOPS, FRAG) begin with a u8 nexthdr
+#define BPF_LOAD_NETX_RELATIVE_V6EXTHDR_NEXTHDR BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+
+// IPv6 fragment header is always exactly 8 bytes long
+#define BPF_LOAD_CONSTANT_V6FRAGHDR_LEN \
+    BPF_STMT(BPF_LD | BPF_IMM, 8)
+
+// HOPOPTS/DSTOPS follow up with 'u8 len', counting 8 byte units, (0->8, 1->16)
+// *THREE* instructions
+#define BPF3_LOAD_NETX_RELATIVE_V6EXTHDR_LEN \
+    BPF_LOAD_NETX_RELATIVE_L4_U8(1), \
+    BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 1), \
+    BPF_STMT(BPF_ALU | BPF_LSH | BPF_K, 3)
+
+// *TWO* instructions: A += X; X := A
+#define BPF2_ADD_A_TO_X \
+    BPF_STMT(BPF_ALU | BPF_ADD | BPF_X, 0), \
+    BPF_STMT(BPF_MISC | BPF_TAX, 0)
+
+// UDP/UDPLITE/TCP/SCTP/DCCP all start with be16 srcport, dstport
+#define BPF_LOAD_NETX_RELATIVE_SRC_PORT BPF_LOAD_NETX_RELATIVE_L4_BE16(0)
+#define BPF_LOAD_NETX_RELATIVE_DST_PORT BPF_LOAD_NETX_RELATIVE_L4_BE16(2)
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
new file mode 100644
index 0000000..1037beb
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
@@ -0,0 +1,381 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <linux/bpf.h>
+
+#include <android/log.h>
+#include <android-base/result.h>
+#include <android-base/stringprintf.h>
+#include <android-base/unique_fd.h>
+
+#include "BpfSyscallWrappers.h"
+#include "bpf/BpfUtils.h"
+
+#include <functional>
+
+namespace android {
+namespace bpf {
+
+using base::Result;
+using base::unique_fd;
+using std::function;
+
+// This is a class wrapper for eBPF maps. The eBPF map is a special in-kernel
+// data structure that stores data in <Key, Value> pairs. It can be read/write
+// from userspace by passing syscalls with the map file descriptor. This class
+// is used to generalize the procedure of interacting with eBPF maps and hide
+// the implementation detail from other process. Besides the basic syscalls
+// wrapper, it also provides some useful helper functions as well as an iterator
+// nested class to iterate the map more easily.
+//
+// NOTE: A kernel eBPF map may be accessed by both kernel and userspace
+// processes at the same time. Or if the map is pinned as a virtual file, it can
+// be obtained by multiple eBPF map class object and accessed concurrently.
+// Though the map class object and the underlying kernel map are thread safe, it
+// is not safe to iterate over a map while another thread or process is deleting
+// from it. In this case the iteration can return duplicate entries.
+template <class Key, class Value>
+class BpfMapRO {
+  public:
+    BpfMapRO<Key, Value>() {};
+
+    // explicitly force no copy constructor, since it would need to dup the fd
+    // (later on, for testing, we still make available a copy assignment operator)
+    BpfMapRO<Key, Value>(const BpfMapRO<Key, Value>&) = delete;
+
+  protected:
+    void abortOnMismatch(bool writable) const {
+        if (!mMapFd.ok()) abort();
+        if (isAtLeastKernelVersion(4, 14, 0)) {
+            int flags = bpfGetFdMapFlags(mMapFd);
+            if (flags < 0) abort();
+            if (flags & BPF_F_WRONLY) abort();
+            if (writable && (flags & BPF_F_RDONLY)) abort();
+            if (bpfGetFdKeySize(mMapFd) != sizeof(Key)) abort();
+            if (bpfGetFdValueSize(mMapFd) != sizeof(Value)) abort();
+        }
+    }
+
+  public:
+    explicit BpfMapRO<Key, Value>(const char* pathname) {
+        mMapFd.reset(mapRetrieveRO(pathname));
+        abortOnMismatch(/* writable */ false);
+    }
+
+    Result<Key> getFirstKey() const {
+        Key firstKey;
+        if (getFirstMapKey(mMapFd, &firstKey)) {
+            return ErrnoErrorf("BpfMap::getFirstKey() failed");
+        }
+        return firstKey;
+    }
+
+    Result<Key> getNextKey(const Key& key) const {
+        Key nextKey;
+        if (getNextMapKey(mMapFd, &key, &nextKey)) {
+            return ErrnoErrorf("BpfMap::getNextKey() failed");
+        }
+        return nextKey;
+    }
+
+    Result<Value> readValue(const Key key) const {
+        Value value;
+        if (findMapEntry(mMapFd, &key, &value)) {
+            return ErrnoErrorf("BpfMap::readValue() failed");
+        }
+        return value;
+    }
+
+  protected:
+    [[clang::reinitializes]] Result<void> init(const char* path, int fd, bool writable) {
+        mMapFd.reset(fd);
+        if (!mMapFd.ok()) {
+            return ErrnoErrorf("Pinned map not accessible or does not exist: ({})", path);
+        }
+        // Normally we should return an error here instead of calling abort,
+        // but this cannot happen at runtime without a massive code bug (K/V type mismatch)
+        // and as such it's better to just blow the system up and let the developer fix it.
+        // Crashes are much more likely to be noticed than logs and missing functionality.
+        abortOnMismatch(writable);
+        return {};
+    }
+
+  public:
+    // Function that tries to get map from a pinned path.
+    [[clang::reinitializes]] Result<void> init(const char* path) {
+        return init(path, mapRetrieveRO(path), /* writable */ false);
+    }
+
+    // Iterate through the map and handle each key retrieved based on the filter
+    // without modification of map content.
+    Result<void> iterate(
+            const function<Result<void>(const Key& key,
+                                        const BpfMapRO<Key, Value>& map)>& filter) const;
+
+    // Iterate through the map and get each <key, value> pair, handle each <key,
+    // value> pair based on the filter without modification of map content.
+    Result<void> iterateWithValue(
+            const function<Result<void>(const Key& key, const Value& value,
+                                        const BpfMapRO<Key, Value>& map)>& filter) const;
+
+#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+    const unique_fd& getMap() const { return mMapFd; };
+
+    // Copy assignment operator - due to need for fd duping, should not be used in non-test code.
+    BpfMapRO<Key, Value>& operator=(const BpfMapRO<Key, Value>& other) {
+        if (this != &other) mMapFd.reset(fcntl(other.mMapFd.get(), F_DUPFD_CLOEXEC, 0));
+        return *this;
+    }
+#else
+    BpfMapRO<Key, Value>& operator=(const BpfMapRO<Key, Value>&) = delete;
+#endif
+
+    // Move assignment operator
+    BpfMapRO<Key, Value>& operator=(BpfMapRO<Key, Value>&& other) noexcept {
+        if (this != &other) {
+            mMapFd = std::move(other.mMapFd);
+            other.reset();
+        }
+        return *this;
+    }
+
+#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+    // Note that unique_fd.reset() carefully saves and restores the errno,
+    // and BpfMap.reset() won't touch the errno if passed in fd is negative either,
+    // hence you can do something like BpfMap.reset(systemcall()) and then
+    // check BpfMap.isValid() and look at errno and see why systemcall() failed.
+    [[clang::reinitializes]] void reset(int fd) {
+        mMapFd.reset(fd);
+        if (mMapFd.ok()) abortOnMismatch(/* writable */ false);  // false isn't ideal
+    }
+
+    // unique_fd has an implicit int conversion defined, which combined with the above
+    // reset(int) would result in double ownership of the fd, hence we either need a custom
+    // implementation of reset(unique_fd), or to delete it and thus cause compile failures
+    // to catch this and prevent it.
+    void reset(unique_fd fd) = delete;
+#endif
+
+    [[clang::reinitializes]] void reset() {
+        mMapFd.reset();
+    }
+
+    bool isValid() const { return mMapFd.ok(); }
+
+    Result<bool> isEmpty() const {
+        auto key = getFirstKey();
+        if (key.ok()) return false;
+        if (key.error().code() == ENOENT) return true;
+        return key.error();
+    }
+
+  protected:
+    unique_fd mMapFd;
+};
+
+template <class Key, class Value>
+Result<void> BpfMapRO<Key, Value>::iterate(
+        const function<Result<void>(const Key& key,
+                                    const BpfMapRO<Key, Value>& map)>& filter) const {
+    Result<Key> curKey = getFirstKey();
+    while (curKey.ok()) {
+        const Result<Key>& nextKey = getNextKey(curKey.value());
+        Result<void> status = filter(curKey.value(), *this);
+        if (!status.ok()) return status;
+        curKey = nextKey;
+    }
+    if (curKey.error().code() == ENOENT) return {};
+    return curKey.error();
+}
+
+template <class Key, class Value>
+Result<void> BpfMapRO<Key, Value>::iterateWithValue(
+        const function<Result<void>(const Key& key, const Value& value,
+                                    const BpfMapRO<Key, Value>& map)>& filter) const {
+    Result<Key> curKey = getFirstKey();
+    while (curKey.ok()) {
+        const Result<Key>& nextKey = getNextKey(curKey.value());
+        Result<Value> curValue = readValue(curKey.value());
+        if (!curValue.ok()) return curValue.error();
+        Result<void> status = filter(curKey.value(), curValue.value(), *this);
+        if (!status.ok()) return status;
+        curKey = nextKey;
+    }
+    if (curKey.error().code() == ENOENT) return {};
+    return curKey.error();
+}
+
+template <class Key, class Value>
+class BpfMap : public BpfMapRO<Key, Value> {
+  protected:
+    using BpfMapRO<Key, Value>::mMapFd;
+    using BpfMapRO<Key, Value>::abortOnMismatch;
+
+  public:
+    using BpfMapRO<Key, Value>::getFirstKey;
+    using BpfMapRO<Key, Value>::getNextKey;
+    using BpfMapRO<Key, Value>::readValue;
+
+    BpfMap<Key, Value>() {};
+
+    explicit BpfMap<Key, Value>(const char* pathname) {
+        mMapFd.reset(mapRetrieveRW(pathname));
+        abortOnMismatch(/* writable */ true);
+    }
+
+    // Function that tries to get map from a pinned path.
+    [[clang::reinitializes]] Result<void> init(const char* path) {
+        return BpfMapRO<Key,Value>::init(path, mapRetrieveRW(path), /* writable */ true);
+    }
+
+    Result<void> writeValue(const Key& key, const Value& value, uint64_t flags) {
+        if (writeToMapEntry(mMapFd, &key, &value, flags)) {
+            return ErrnoErrorf("BpfMap::writeValue() failed");
+        }
+        return {};
+    }
+
+    Result<void> deleteValue(const Key& key) {
+        if (deleteMapEntry(mMapFd, &key)) {
+            return ErrnoErrorf("BpfMap::deleteValue() failed");
+        }
+        return {};
+    }
+
+    Result<void> clear() {
+        while (true) {
+            auto key = getFirstKey();
+            if (!key.ok()) {
+                if (key.error().code() == ENOENT) return {};  // empty: success
+                return key.error();                           // Anything else is an error
+            }
+            auto res = deleteValue(key.value());
+            if (!res.ok()) {
+                // Someone else could have deleted the key, so ignore ENOENT
+                if (res.error().code() == ENOENT) continue;
+                ALOGE("Failed to delete data %s", strerror(res.error().code()));
+                return res.error();
+            }
+        }
+    }
+
+#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+    [[clang::reinitializes]] Result<void> resetMap(bpf_map_type map_type,
+                                                   uint32_t max_entries,
+                                                   uint32_t map_flags = 0) {
+        if (map_flags & BPF_F_WRONLY) abort();
+        if (map_flags & BPF_F_RDONLY) abort();
+        mMapFd.reset(createMap(map_type, sizeof(Key), sizeof(Value), max_entries,
+                               map_flags));
+        if (!mMapFd.ok()) return ErrnoErrorf("BpfMap::resetMap() failed");
+        abortOnMismatch(/* writable */ true);
+        return {};
+    }
+#endif
+
+    // Iterate through the map and handle each key retrieved based on the filter
+    // without modification of map content.
+    Result<void> iterate(
+            const function<Result<void>(const Key& key,
+                                        const BpfMap<Key, Value>& map)>& filter) const;
+
+    // Iterate through the map and get each <key, value> pair, handle each <key,
+    // value> pair based on the filter without modification of map content.
+    Result<void> iterateWithValue(
+            const function<Result<void>(const Key& key, const Value& value,
+                                        const BpfMap<Key, Value>& map)>& filter) const;
+
+    // Iterate through the map and handle each key retrieved based on the filter
+    Result<void> iterate(
+            const function<Result<void>(const Key& key,
+                                        BpfMap<Key, Value>& map)>& filter);
+
+    // Iterate through the map and get each <key, value> pair, handle each <key,
+    // value> pair based on the filter.
+    Result<void> iterateWithValue(
+            const function<Result<void>(const Key& key, const Value& value,
+                                        BpfMap<Key, Value>& map)>& filter);
+
+};
+
+template <class Key, class Value>
+Result<void> BpfMap<Key, Value>::iterate(
+        const function<Result<void>(const Key& key,
+                                    const BpfMap<Key, Value>& map)>& filter) const {
+    Result<Key> curKey = getFirstKey();
+    while (curKey.ok()) {
+        const Result<Key>& nextKey = getNextKey(curKey.value());
+        Result<void> status = filter(curKey.value(), *this);
+        if (!status.ok()) return status;
+        curKey = nextKey;
+    }
+    if (curKey.error().code() == ENOENT) return {};
+    return curKey.error();
+}
+
+template <class Key, class Value>
+Result<void> BpfMap<Key, Value>::iterateWithValue(
+        const function<Result<void>(const Key& key, const Value& value,
+                                    const BpfMap<Key, Value>& map)>& filter) const {
+    Result<Key> curKey = getFirstKey();
+    while (curKey.ok()) {
+        const Result<Key>& nextKey = getNextKey(curKey.value());
+        Result<Value> curValue = readValue(curKey.value());
+        if (!curValue.ok()) return curValue.error();
+        Result<void> status = filter(curKey.value(), curValue.value(), *this);
+        if (!status.ok()) return status;
+        curKey = nextKey;
+    }
+    if (curKey.error().code() == ENOENT) return {};
+    return curKey.error();
+}
+
+template <class Key, class Value>
+Result<void> BpfMap<Key, Value>::iterate(
+        const function<Result<void>(const Key& key,
+                                    BpfMap<Key, Value>& map)>& filter) {
+    Result<Key> curKey = getFirstKey();
+    while (curKey.ok()) {
+        const Result<Key>& nextKey = getNextKey(curKey.value());
+        Result<void> status = filter(curKey.value(), *this);
+        if (!status.ok()) return status;
+        curKey = nextKey;
+    }
+    if (curKey.error().code() == ENOENT) return {};
+    return curKey.error();
+}
+
+template <class Key, class Value>
+Result<void> BpfMap<Key, Value>::iterateWithValue(
+        const function<Result<void>(const Key& key, const Value& value,
+                                    BpfMap<Key, Value>& map)>& filter) {
+    Result<Key> curKey = getFirstKey();
+    while (curKey.ok()) {
+        const Result<Key>& nextKey = getNextKey(curKey.value());
+        Result<Value> curValue = readValue(curKey.value());
+        if (!curValue.ok()) return curValue.error();
+        Result<void> status = filter(curKey.value(), curValue.value(), *this);
+        if (!status.ok()) return status;
+        curKey = nextKey;
+    }
+    if (curKey.error().code() == ENOENT) return {};
+    return curKey.error();
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
new file mode 100644
index 0000000..cd51004
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
@@ -0,0 +1,279 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <android-base/result.h>
+#include <android-base/unique_fd.h>
+#include <linux/bpf.h>
+#include <poll.h>
+#include <sys/mman.h>
+#include <utils/Log.h>
+
+#include "bpf/BpfUtils.h"
+
+#include <atomic>
+
+namespace android {
+namespace bpf {
+
+// BpfRingbufBase contains the non-templated functionality of BPF ring buffers.
+class BpfRingbufBase {
+ public:
+  ~BpfRingbufBase() {
+    if (mConsumerPos) munmap(mConsumerPos, mConsumerSize);
+    if (mProducerPos) munmap(mProducerPos, mProducerSize);
+    mConsumerPos = nullptr;
+    mProducerPos = nullptr;
+  }
+
+  bool isEmpty(void);
+
+  // returns !isEmpty() for convenience
+  bool wait(int timeout_ms = -1);
+
+ protected:
+  // Non-initializing constructor, used by Create.
+  BpfRingbufBase(size_t value_size) : mValueSize(value_size) {}
+
+  // Full construction that aborts on error (use Create/Init to handle errors).
+  BpfRingbufBase(const char* path, size_t value_size) : mValueSize(value_size) {
+    if (auto status = Init(path); !status.ok()) {
+      ALOGE("BpfRingbuf init failed: %s", status.error().message().c_str());
+      abort();
+    }
+  }
+
+  // Delete copy constructor (class owns raw pointers).
+  BpfRingbufBase(const BpfRingbufBase&) = delete;
+
+  // Initialize the base ringbuffer components. Must be called exactly once.
+  base::Result<void> Init(const char* path);
+
+  // Consumes all messages from the ring buffer, passing them to the callback.
+  base::Result<int> ConsumeAll(
+      const std::function<void(const void*)>& callback);
+
+  // Replicates c-style void* "byte-wise" pointer addition.
+  template <typename Ptr>
+  static Ptr pointerAddBytes(void* base, ssize_t offset_bytes) {
+    return reinterpret_cast<Ptr>(reinterpret_cast<char*>(base) + offset_bytes);
+  }
+
+  // Rounds len by clearing bitmask, adding header, and aligning to 8 bytes.
+  static uint32_t roundLength(uint32_t len) {
+    len &= ~(BPF_RINGBUF_BUSY_BIT | BPF_RINGBUF_DISCARD_BIT);
+    len += BPF_RINGBUF_HDR_SZ;
+    return (len + 7) & ~7;
+  }
+
+  const size_t mValueSize;
+
+  size_t mConsumerSize;
+  size_t mProducerSize;
+  unsigned long mPosMask;
+  android::base::unique_fd mRingFd;
+
+  void* mDataPos = nullptr;
+  // The kernel uses an "unsigned long" type for both consumer and producer position.
+  // Unsigned long is a 4 byte value on a 32-bit kernel, and an 8 byte value on a 64-bit kernel.
+  // To support 32-bit kernels, producer pos is capped at 4 bytes (despite it being 8 bytes on
+  // 64-bit kernels) and all comparisons of consumer and producer pos only compare the low-order 4
+  // bytes (an inequality comparison is performed to support overflow).
+  // This solution is bitness agnostic. The consumer only increments the 8 byte consumer pos, which,
+  // in a little-endian architecture, is safe since the entire page is mapped into memory and a
+  // 32-bit kernel will just ignore the high-order bits.
+  std::atomic_uint64_t* mConsumerPos = nullptr;
+  std::atomic_uint32_t* mProducerPos = nullptr;
+
+  // In order to guarantee atomic access in a 32 bit userspace environment, atomic_uint64_t is used
+  // in addition to std::atomic<T>::is_always_lock_free that guarantees that read / write operations
+  // are indeed atomic.
+  // Since std::atomic does not support wrapping preallocated memory, an additional static assert on
+  // the size of the atomic and the underlying type is added to ensure a reinterpret_cast from type
+  // to its atomic version is safe (is_always_lock_free being true should provide additional
+  // confidence).
+  static_assert(std::atomic_uint64_t::is_always_lock_free);
+  static_assert(std::atomic_uint32_t::is_always_lock_free);
+  static_assert(sizeof(std::atomic_uint64_t) == sizeof(uint64_t));
+  static_assert(sizeof(std::atomic_uint32_t) == sizeof(uint32_t));
+};
+
+// This is a class wrapper for eBPF ring buffers. An eBPF ring buffer is a
+// special type of eBPF map used for sending messages from eBPF to userspace.
+// The implementation relies on fast shared memory and atomics for the producer
+// and consumer management. Ring buffers are a faster alternative to eBPF perf
+// buffers.
+//
+// This class is thread compatible, but not thread safe.
+//
+// Note: A kernel eBPF ring buffer may be accessed by both kernel and userspace
+// processes at the same time. However, the userspace consumers of a given ring
+// buffer all share a single read pointer. There is no guarantee which readers
+// will read which messages.
+template <typename Value>
+class BpfRingbuf : public BpfRingbufBase {
+ public:
+  using MessageCallback = std::function<void(const Value&)>;
+
+  // Creates a ringbuffer wrapper from a pinned path. This initialization will
+  // abort on error. To handle errors, initialize with Create instead.
+  BpfRingbuf(const char* path) : BpfRingbufBase(path, sizeof(Value)) {}
+
+  // Creates a ringbuffer wrapper from a pinned path. There are no guarantees
+  // that the ringbuf outputs messaged of type `Value`, only that they are the
+  // same size. Size is only checked in ConsumeAll.
+  static base::Result<std::unique_ptr<BpfRingbuf<Value>>> Create(
+      const char* path);
+
+  // 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:
+  // Empty ctor for use by Create.
+  BpfRingbuf() : BpfRingbufBase(sizeof(Value)) {}
+};
+
+
+inline base::Result<void> BpfRingbufBase::Init(const char* path) {
+  mRingFd.reset(mapRetrieveExclusiveRW(path));
+  if (!mRingFd.ok()) {
+    return android::base::ErrnoError()
+           << "failed to retrieve ringbuffer at " << path;
+  }
+
+  int map_type = android::bpf::bpfGetFdMapType(mRingFd);
+  if (map_type != BPF_MAP_TYPE_RINGBUF) {
+    errno = EINVAL;
+    return android::base::ErrnoError()
+           << "bpf map has wrong type: want BPF_MAP_TYPE_RINGBUF ("
+           << BPF_MAP_TYPE_RINGBUF << ") got " << map_type;
+  }
+
+  int max_entries = android::bpf::bpfGetFdMaxEntries(mRingFd);
+  if (max_entries < 0) {
+    return android::base::ErrnoError()
+           << "failed to read max_entries from ringbuf";
+  }
+  if (max_entries == 0) {
+    errno = EINVAL;
+    return android::base::ErrnoError() << "max_entries must be non-zero";
+  }
+
+  mPosMask = max_entries - 1;
+  mConsumerSize = getpagesize();
+  mProducerSize = getpagesize() + 2 * max_entries;
+
+  {
+    void* ptr = mmap(NULL, mConsumerSize, PROT_READ | PROT_WRITE, MAP_SHARED,
+                     mRingFd, 0);
+    if (ptr == MAP_FAILED) {
+      return android::base::ErrnoError()
+             << "failed to mmap ringbuf consumer pages";
+    }
+    mConsumerPos = reinterpret_cast<decltype(mConsumerPos)>(ptr);
+  }
+
+  {
+    void* ptr = mmap(NULL, mProducerSize, PROT_READ, MAP_SHARED, mRingFd,
+                     mConsumerSize);
+    if (ptr == MAP_FAILED) {
+      return android::base::ErrnoError()
+             << "failed to mmap ringbuf producer page";
+    }
+    mProducerPos = reinterpret_cast<decltype(mProducerPos)>(ptr);
+  }
+
+  mDataPos = pointerAddBytes<void*>(mProducerPos, getpagesize());
+  return {};
+}
+
+inline bool BpfRingbufBase::isEmpty(void) {
+  uint32_t prod_pos = mProducerPos->load(std::memory_order_relaxed);
+  uint64_t cons_pos = mConsumerPos->load(std::memory_order_relaxed);
+  return (cons_pos & 0xFFFFFFFF) == prod_pos;
+}
+
+inline bool BpfRingbufBase::wait(int timeout_ms) {
+  // possible optimization: if (!isEmpty()) return true;
+  struct pollfd pfd = {  // 1-element array
+    .fd = mRingFd.get(),
+    .events = POLLIN,
+  };
+  (void)poll(&pfd, 1, timeout_ms);  // 'best effort' poll
+  return !isEmpty();
+}
+
+inline base::Result<int> BpfRingbufBase::ConsumeAll(
+    const std::function<void(const void*)>& callback) {
+  int64_t count = 0;
+  uint32_t prod_pos = mProducerPos->load(std::memory_order_acquire);
+  // Only userspace writes to mConsumerPos, so no need to use std::memory_order_acquire
+  uint64_t cons_pos = mConsumerPos->load(std::memory_order_relaxed);
+  while ((cons_pos & 0xFFFFFFFF) != prod_pos) {
+    // Find the start of the entry for this read (wrapping is done here).
+    void* start_ptr = pointerAddBytes<void*>(mDataPos, cons_pos & mPosMask);
+
+    // The entry has an 8 byte header containing the sample length.
+    // struct bpf_ringbuf_hdr {
+    //   u32 len;
+    //   u32 pg_off;
+    // };
+    uint32_t length = *reinterpret_cast<volatile uint32_t*>(start_ptr);
+
+    // If the sample isn't committed, we're caught up with the producer.
+    if (length & BPF_RINGBUF_BUSY_BIT) return count;
+
+    cons_pos += roundLength(length);
+
+    if ((length & BPF_RINGBUF_DISCARD_BIT) == 0) {
+      if (length != mValueSize) {
+        mConsumerPos->store(cons_pos, std::memory_order_release);
+        errno = EMSGSIZE;
+        return android::base::ErrnoError()
+               << "BPF ring buffer message has unexpected size (want "
+               << mValueSize << " bytes, got " << length << " bytes)";
+      }
+      callback(pointerAddBytes<const void*>(start_ptr, BPF_RINGBUF_HDR_SZ));
+      count++;
+    }
+
+    mConsumerPos->store(cons_pos, std::memory_order_release);
+  }
+
+  return count;
+}
+
+template <typename Value>
+inline base::Result<std::unique_ptr<BpfRingbuf<Value>>>
+BpfRingbuf<Value>::Create(const char* path) {
+  auto rb = std::unique_ptr<BpfRingbuf>(new BpfRingbuf);
+  if (auto status = rb->Init(path); !status.ok()) return status.error();
+  return rb;
+}
+
+template <typename Value>
+inline base::Result<int> BpfRingbuf<Value>::ConsumeAll(
+    const MessageCallback& callback) {
+  return BpfRingbufBase::ConsumeAll([&](const void* value) {
+    callback(*reinterpret_cast<const Value*>(value));
+  });
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
new file mode 100644
index 0000000..9dd5822
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 <errno.h>
+#include <linux/if_ether.h>
+#include <linux/pfkeyv2.h>
+#include <net/if.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <sys/utsname.h>
+
+#include <log/log.h>
+
+#include "KernelUtils.h"
+
+namespace android {
+namespace bpf {
+
+// See kernel's net/core/sock_diag.c __sock_gen_cookie()
+// the implementation of which guarantees 0 will never be returned,
+// primarily because 0 is used to mean not yet initialized,
+// and socket cookies are only assigned on first fetch.
+constexpr const uint64_t NONEXISTENT_COOKIE = 0;
+
+static inline uint64_t getSocketCookie(int sockFd) {
+    uint64_t sock_cookie;
+    socklen_t cookie_len = sizeof(sock_cookie);
+    if (getsockopt(sockFd, SOL_SOCKET, SO_COOKIE, &sock_cookie, &cookie_len)) {
+        // Failure is almost certainly either EBADF or ENOTSOCK
+        const int err = errno;
+        ALOGE("Failed to get socket cookie: %s\n", strerror(err));
+        errno = err;
+        return NONEXISTENT_COOKIE;
+    }
+    if (cookie_len != sizeof(sock_cookie)) {
+        // This probably cannot actually happen, but...
+        ALOGE("Failed to get socket cookie: len %d != 8\n", cookie_len);
+        errno = 523; // EBADCOOKIE: kernel internal, seems reasonable enough...
+        return NONEXISTENT_COOKIE;
+    }
+    return sock_cookie;
+}
+
+static inline int synchronizeKernelRCU() {
+    // This is a temporary hack for network stats map swap on devices running
+    // 4.9 kernels. The kernel code of socket release on pf_key socket will
+    // explicitly call synchronize_rcu() which is exactly what we need.
+    //
+    // Linux 4.14/4.19/5.4/5.10/5.15/6.1 (and 6.3-rc5) still have this same behaviour.
+    // see net/key/af_key.c: pfkey_release() -> synchronize_rcu()
+    // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.3-rc5#n185
+    const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
+
+    if (pfSocket < 0) {
+        const int err = errno;
+        ALOGE("create PF_KEY socket failed: %s", strerror(err));
+        return -err;
+    }
+
+    // When closing socket, synchronize_rcu() gets called in sock_release().
+    if (close(pfSocket)) {
+        const int err = errno;
+        ALOGE("failed to close the PF_KEY socket: %s", strerror(err));
+        return -err;
+    }
+    return 0;
+}
+
+static inline int setrlimitForTest() {
+    // Set the memory rlimit for the test process if the default MEMLOCK rlimit is not enough.
+    struct rlimit limit = {
+            .rlim_cur = 1073741824,  // 1 GiB
+            .rlim_max = 1073741824,  // 1 GiB
+    };
+    const int res = setrlimit(RLIMIT_MEMLOCK, &limit);
+    if (res) ALOGE("Failed to set the default MEMLOCK rlimit: %s", strerror(errno));
+    return res;
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h b/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h
new file mode 100644
index 0000000..417a5c4
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/personality.h>
+#include <sys/utsname.h>
+
+namespace android {
+namespace bpf {
+
+#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
+
+static inline unsigned uncachedKernelVersion() {
+    struct utsname buf;
+    if (uname(&buf)) return 0;
+
+    unsigned kver_major = 0;
+    unsigned kver_minor = 0;
+    unsigned kver_sub = 0;
+    (void)sscanf(buf.release, "%u.%u.%u", &kver_major, &kver_minor, &kver_sub);
+    return KVER(kver_major, kver_minor, kver_sub);
+}
+
+static inline unsigned kernelVersion() {
+    static unsigned kver = uncachedKernelVersion();
+    return kver;
+}
+
+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() {
+    return sizeof(void*) == 4;
+}
+
+static constexpr bool isUserspace64bit() {
+    return sizeof(void*) == 8;
+}
+
+#if defined(__LP64__)
+static_assert(isUserspace64bit(), "huh? LP64 must have 64-bit userspace");
+#elif defined(__ILP32__)
+static_assert(isUserspace32bit(), "huh? ILP32 must have 32-bit userspace");
+#else
+#error "huh? must be either LP64 (64-bit userspace) or ILP32 (32-bit userspace)"
+#endif
+
+static_assert(isUserspace32bit() || isUserspace64bit(), "must be either 32 or 64 bit");
+
+// Figure out the bitness of the kernel.
+static inline bool isKernel64Bit() {
+    // a 64-bit userspace requires a 64-bit kernel
+    if (isUserspace64bit()) return true;
+
+    static bool init = false;
+    static bool cache = false;
+    if (init) return cache;
+
+    // Retrieve current personality - on Linux this system call *cannot* fail.
+    int p = personality(0xffffffff);
+    // But if it does just assume kernel and userspace (which is 32-bit) match...
+    if (p == -1) return false;
+
+    // This will effectively mask out the bottom 8 bits, and switch to 'native'
+    // personality, and then return the previous personality of this thread
+    // (likely PER_LINUX or PER_LINUX32) with any extra options unmodified.
+    int q = personality((p & ~PER_MASK) | PER_LINUX);
+    // Per man page this theoretically could error out with EINVAL,
+    // but kernel code analysis suggests setting PER_LINUX cannot fail.
+    // Either way, assume kernel and userspace (which is 32-bit) match...
+    if (q != p) return false;
+
+    struct utsname u;
+    (void)uname(&u);  // only possible failure is EFAULT, but u is on stack.
+
+    // Switch back to previous personality.
+    // Theoretically could fail with EINVAL on arm64 with no 32-bit support,
+    // but then we wouldn't have fetched 'p' from the kernel in the first place.
+    // Either way there's nothing meaningful we can do in case of error.
+    // Since PER_LINUX32 vs PER_LINUX only affects uname.machine it doesn't
+    // really hurt us either.  We're really just switching back to be 'clean'.
+    (void)personality(p);
+
+    // Possible values of utsname.machine observed on x86_64 desktop (arm via qemu):
+    //   x86_64 i686 aarch64 armv7l
+    // additionally observed on arm device:
+    //   armv8l
+    // presumably also might just be possible:
+    //   i386 i486 i586
+    // and there might be other weird arm32 cases.
+    // We note that the 64 is present in both 64-bit archs,
+    // and in general is likely to be present in only 64-bit archs.
+    cache = !!strstr(u.machine, "64");
+    init = true;
+    return cache;
+}
+
+static inline __unused bool isKernel32Bit() {
+    return !isKernel64Bit();
+}
+
+static constexpr bool isArm() {
+#if defined(__arm__)
+    static_assert(isUserspace32bit(), "huh? arm must be 32 bit");
+    return true;
+#elif defined(__aarch64__)
+    static_assert(isUserspace64bit(), "aarch64 must be LP64 - no support for ILP32");
+    return true;
+#else
+    return false;
+#endif
+}
+
+static constexpr bool isX86() {
+#if defined(__i386__)
+    static_assert(isUserspace32bit(), "huh? i386 must be 32 bit");
+    return true;
+#elif defined(__x86_64__)
+    static_assert(isUserspace64bit(), "x86_64 must be LP64 - no support for ILP32 (x32)");
+    return true;
+#else
+    return false;
+#endif
+}
+
+static constexpr bool isRiscV() {
+#if defined(__riscv)
+    static_assert(isUserspace64bit(), "riscv must be 64 bit");
+    return true;
+#else
+    return false;
+#endif
+}
+
+static_assert(isArm() || isX86() || isRiscV(), "Unknown architecture");
+
+static __unused const char * describeArch() {
+    // ordered so as to make it easier to compile time optimize,
+    // only thing not known at compile time is isKernel64Bit()
+    if (isUserspace64bit()) {
+        if (isArm()) return "64-on-aarch64";
+        if (isX86()) return "64-on-x86-64";
+        if (isRiscV()) return "64-on-riscv64";
+    } else if (isKernel64Bit()) {
+        if (isArm()) return "32-on-aarch64";
+        if (isX86()) return "32-on-x86-64";
+    } else {
+        if (isArm()) return "32-on-arm32";
+        if (isX86()) return "32-on-x86-32";
+    }
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h b/staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
new file mode 100644
index 0000000..bc4168e
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ * Android BPF library - public API
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 <log/log.h>
+
+#include <android-base/properties.h>
+
+namespace android {
+namespace bpf {
+
+// Wait for bpfloader to load BPF programs.
+static inline void waitForProgsLoaded() {
+    // 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 (android::base::WaitForProperty("bpf.progs_loaded", "1", std::chrono::seconds(delay)))
+            return;
+        ALOGW("Waited %ds for bpf.progs_loaded, still waiting...", delay);
+    }
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
new file mode 100644
index 0000000..4ddec8b
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -0,0 +1,466 @@
+/* Common BPF helpers to be used by all BPF programs loaded by Android */
+
+#include <linux/bpf.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "bpf_map_def.h"
+
+/******************************************************************************
+ * WARNING: CHANGES TO THIS FILE OUTSIDE OF AOSP/MASTER ARE LIKELY TO BREAK   *
+ * DEVICE COMPATIBILITY WITH MAINLINE MODULES SHIPPING EBPF CODE.             *
+ *                                                                            *
+ * THIS WILL LIKELY RESULT IN BRICKED DEVICES AT SOME ARBITRARY FUTURE TIME   *
+ *                                                                            *
+ * THAT GOES ESPECIALLY FOR THE 'SECTION' 'LICENSE' AND 'CRITICAL' MACROS     *
+ *                                                                            *
+ * We strongly suggest that if you need changes to bpfloader functionality    *
+ * you get your changes reviewed and accepted into aosp/master.               *
+ *                                                                            *
+ ******************************************************************************/
+
+// The actual versions of the bpfloader that shipped in various Android releases
+
+// Android P/Q/R: BpfLoader was initially part of netd,
+// this was later split out into a standalone binary, but was unversioned.
+
+// Android S / 12 (api level 31) - added 'tethering' mainline eBPF support
+#define BPFLOADER_S_VERSION 2u
+
+// Android T / 13 (api level 33) - support for shared/selinux_context/pindir
+#define BPFLOADER_T_VERSION 19u
+
+// BpfLoader v0.25+ support obj@ver.o files
+#define BPFLOADER_OBJ_AT_VER_VERSION 25u
+
+// Bpfloader v0.33+ supports {map,prog}.ignore_on_{eng,user,userdebug}
+#define BPFLOADER_IGNORED_ON_VERSION 33u
+
+// Android U / 14 (api level 34) - various new program types added
+#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
+ * process the resulting .o file.
+ *
+ * While this will work outside of mainline too, there just is no point to
+ * using it when the .o and the bpfloader ship in sync with each other.
+ * In which case it's just best to use the default.
+ */
+#ifndef BPFLOADER_MIN_VER
+#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION
+#endif
+
+#ifndef BPFLOADER_MAX_VER
+#define BPFLOADER_MAX_VER DEFAULT_BPFLOADER_MAX_VER
+#endif
+
+/* place things in different elf sections */
+#define SECTION(NAME) __attribute__((section(NAME), used))
+
+/* Must be present in every program, example usage:
+ *   LICENSE("GPL"); or LICENSE("Apache 2.0");
+ *
+ * We also take this opportunity to embed a bunch of other useful values in
+ * the resulting .o (This is to enable some limited forward compatibility
+ * with mainline module shipped ebpf programs)
+ *
+ * The bpfloader_{min/max}_ver defines the [min, max) range of bpfloader
+ * versions that should load this .o file (bpfloaders outside of this range
+ * will simply ignore/skip this *entire* .o)
+ * The [inclusive,exclusive) matches what we do for kernel ver dependencies.
+ *
+ * The size_of_bpf_{map,prog}_def allow the bpfloader to load programs where
+ * these structures have been extended with additional fields (they will of
+ * course simply be ignored then).
+ *
+ * If missing, bpfloader_{min/max}_ver default to 0/0x10000 ie. [v0.0, v1.0),
+ * while size_of_bpf_{map/prog}_def default to 32/20 which are the v0.0 sizes.
+ */
+#define LICENSE(NAME)                                                                           \
+    unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER;           \
+    unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER;           \
+    size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def);    \
+    size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
+    char _license[] SECTION("license") = (NAME)
+
+/* This macro disables loading BTF map debug information on Android <=U *and* all user builds.
+ *
+ * Note: Bpfloader v0.39+ honours 'btf_user_min_bpfloader_ver' on user builds,
+ * and 'btf_min_bpfloader_ver' on non-user builds.
+ * Older BTF capable versions unconditionally honour 'btf_min_bpfloader_ver'
+ */
+#define DISABLE_BTF_ON_USER_BUILDS() \
+    unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = 39u; \
+    unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu
+
+/* flag the resulting bpf .o file as critical to system functionality,
+ * loading all kernel version appropriate programs in it must succeed
+ * for bpfloader success
+ */
+#define CRITICAL(REASON) char _critical[] SECTION("critical") = (REASON)
+
+/*
+ * Helper functions called from eBPF programs written in C. These are
+ * implemented in the kernel sources.
+ */
+
+struct kver_uint { unsigned int kver; };
+#define KVER_(v) ((struct kver_uint){ .kver = (v) })
+#define KVER(a, b, c) KVER_(((a) << 24) + ((b) << 16) + (c))
+#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_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)
+
+/*
+ * BPFFS (ie. /sys/fs/bpf) labelling is as follows:
+ *   subdirectory   selinux context      mainline  usecase / usable by
+ *   /              fs_bpf               no [*]    core operating system (ie. platform)
+ *   /loader        fs_bpf_loader        no, U+    (as yet unused)
+ *   /net_private   fs_bpf_net_private   yes, T+   network_stack
+ *   /net_shared    fs_bpf_net_shared    yes, T+   network_stack & system_server
+ *   /netd_readonly fs_bpf_netd_readonly yes, T+   network_stack & system_server & r/o to netd
+ *   /netd_shared   fs_bpf_netd_shared   yes, T+   network_stack & system_server & netd [**]
+ *   /tethering     fs_bpf_tethering     yes, S+   network_stack
+ *   /vendor        fs_bpf_vendor        no, T+    vendor
+ *
+ * [*] initial support for bpf was added back in P,
+ *     but things worked differently back then with no bpfloader,
+ *     and instead netd doing stuff by hand,
+ *     bpfloader with pinning into /sys/fs/bpf was (I believe) added in Q
+ *     (and was definitely there in R).
+ *
+ * [**] additionally bpf programs are accessible to netutils_wrapper
+ *      for use by iptables xt_bpf extensions.
+ *
+ * See cs/p:aosp-master%20-file:prebuilts/%20file:genfs_contexts%20"genfscon%20bpf"
+ */
+
+/* generic functions */
+
+/*
+ * Type-unsafe bpf map functions - avoid if possible.
+ *
+ * Using these it is possible to pass in keys/values of the wrong type/size,
+ * or, for 'bpf_map_lookup_elem_unsafe' receive into a pointer to the wrong type.
+ * You will not get a compile time failure, and for certain types of errors you
+ * might not even get a failure from the kernel's ebpf verifier during program load,
+ * instead stuff might just not work right at runtime.
+ *
+ * Instead please use:
+ *   DEFINE_BPF_MAP(foo_map, TYPE, KeyType, ValueType, num_entries)
+ * where TYPE can be something like HASH or ARRAY, and num_entries is an integer.
+ *
+ * This defines the map (hence this should not be used in a header file included
+ * from multiple locations) and provides type safe accessors:
+ *   ValueType * bpf_foo_map_lookup_elem(const KeyType *)
+ *   int bpf_foo_map_update_elem(const KeyType *, const ValueType *, flags)
+ *   int bpf_foo_map_delete_elem(const KeyType *)
+ *
+ * This will make sure that if you change the type of a map you'll get compile
+ * errors at any spots you forget to update with the new type.
+ *
+ * Note: these all take pointers to const map because from the C/eBPF point of view
+ * the map struct is really just a readonly map definition of the in kernel object.
+ * Runtime modification of the map defining struct is meaningless, since
+ * the contents is only ever used during bpf program loading & map creation
+ * by the bpf loader, and not by the eBPF program itself.
+ */
+static void* (*bpf_map_lookup_elem_unsafe)(const struct bpf_map_def* map,
+                                           const void* key) = (void*)BPF_FUNC_map_lookup_elem;
+static int (*bpf_map_update_elem_unsafe)(const struct bpf_map_def* map, const void* key,
+                                         const void* value, unsigned long long flags) = (void*)
+        BPF_FUNC_map_update_elem;
+static int (*bpf_map_delete_elem_unsafe)(const struct bpf_map_def* map,
+                                         const void* key) = (void*)BPF_FUNC_map_delete_elem;
+static int (*bpf_ringbuf_output_unsafe)(const struct bpf_map_def* ringbuf,
+                                        const void* data, __u64 size, __u64 flags) = (void*)
+        BPF_FUNC_ringbuf_output;
+static void* (*bpf_ringbuf_reserve_unsafe)(const struct bpf_map_def* ringbuf,
+                                           __u64 size, __u64 flags) = (void*)
+        BPF_FUNC_ringbuf_reserve;
+static void (*bpf_ringbuf_submit_unsafe)(const void* data, __u64 flags) = (void*)
+        BPF_FUNC_ringbuf_submit;
+
+#define BPF_ANNOTATE_KV_PAIR(name, type_key, type_val)  \
+        struct ____btf_map_##name {                     \
+                type_key key;                           \
+                type_val value;                         \
+        };                                              \
+        struct ____btf_map_##name                       \
+        __attribute__ ((section(".maps." #name), used)) \
+                ____btf_map_##name = { }
+
+#define BPF_ASSERT_LOADER_VERSION(min_loader, ignore_eng, ignore_user, ignore_userdebug) \
+    _Static_assert(                                                                      \
+        (min_loader) >= BPFLOADER_IGNORED_ON_VERSION ||                                  \
+            !((ignore_eng).ignore_on_eng ||                                              \
+              (ignore_user).ignore_on_user ||                                            \
+              (ignore_userdebug).ignore_on_userdebug),                                   \
+        "bpfloader min version must be >= 0.33 in order to use ignored_on");
+
+#define DEFINE_BPF_MAP_BASE(the_map, TYPE, keysize, valuesize, num_entries, \
+                            usr, grp, md, selinux, pindir, share, minkver,  \
+                            maxkver, minloader, maxloader, ignore_eng,      \
+                            ignore_user, ignore_userdebug)                  \
+    const struct bpf_map_def SECTION("maps") the_map = {                    \
+        .type = BPF_MAP_TYPE_##TYPE,                                        \
+        .key_size = (keysize),                                              \
+        .value_size = (valuesize),                                          \
+        .max_entries = (num_entries),                                       \
+        .map_flags = 0,                                                     \
+        .uid = (usr),                                                       \
+        .gid = (grp),                                                       \
+        .mode = (md),                                                       \
+        .bpfloader_min_ver = (minloader),                                   \
+        .bpfloader_max_ver = (maxloader),                                   \
+        .min_kver = (minkver).kver,                                         \
+        .max_kver = (maxkver).kver,                                         \
+        .selinux_context = (selinux),                                       \
+        .pin_subdir = (pindir),                                             \
+        .shared = (share).shared,                                           \
+        .ignore_on_eng = (ignore_eng).ignore_on_eng,                        \
+        .ignore_on_user = (ignore_user).ignore_on_user,                     \
+        .ignore_on_userdebug = (ignore_userdebug).ignore_on_userdebug,      \
+    };                                                                      \
+    BPF_ASSERT_LOADER_VERSION(minloader, ignore_eng, ignore_user, ignore_userdebug);
+
+// Type safe macro to declare a ring buffer and related output functions.
+// Compatibility:
+// * BPF ring buffers are only available kernels 5.8 and above. Any program
+//   accessing the ring buffer should set a program level min_kver >= 5.8.
+// * The definition below sets a map min_kver of 5.8 which requires targeting
+//   a BPFLOADER_MIN_VER >= BPFLOADER_S_VERSION.
+#define DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md,   \
+                               selinux, pindir, share, min_loader, max_loader, \
+                               ignore_eng, ignore_user, ignore_userdebug)      \
+    DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md,      \
+                        selinux, pindir, share, KVER_5_8, KVER_INF,            \
+                        min_loader, max_loader, ignore_eng, ignore_user,       \
+                        ignore_userdebug);                                     \
+                                                                               \
+    _Static_assert((size_bytes) >= 4096, "min 4 kiB ringbuffer size");         \
+    _Static_assert((size_bytes) <= 0x10000000, "max 256 MiB ringbuffer size"); \
+    _Static_assert(((size_bytes) & ((size_bytes) - 1)) == 0,                   \
+                   "ring buffer size must be a power of two");                 \
+                                                                               \
+    static inline __always_inline __unused int bpf_##the_map##_output(         \
+            const ValueType* v) {                                              \
+        return bpf_ringbuf_output_unsafe(&the_map, v, sizeof(*v), 0);          \
+    }                                                                          \
+                                                                               \
+    static inline __always_inline __unused                                     \
+            ValueType* bpf_##the_map##_reserve() {                             \
+        return bpf_ringbuf_reserve_unsafe(&the_map, sizeof(ValueType), 0);     \
+    }                                                                          \
+                                                                               \
+    static inline __always_inline __unused void bpf_##the_map##_submit(        \
+            const ValueType* v) {                                              \
+        bpf_ringbuf_submit_unsafe(v, 0);                                       \
+    }
+
+/* There exist buggy kernels with pre-T OS, that due to
+ * kernel patch "[ALPS05162612] bpf: fix ubsan error"
+ * do not support userspace writes into non-zero index of bpf map arrays.
+ *
+ * We use this assert to prevent us from being able to define such a map.
+ */
+
+#ifdef THIS_BPF_PROGRAM_IS_FOR_TEST_PURPOSES_ONLY
+#define BPF_MAP_ASSERT_OK(type, entries, mode)
+#elif BPFLOADER_MIN_VER >= BPFLOADER_T_VERSION
+#define BPF_MAP_ASSERT_OK(type, entries, mode)
+#else
+#define BPF_MAP_ASSERT_OK(type, entries, mode) \
+  _Static_assert(((type) != BPF_MAP_TYPE_ARRAY) || ((entries) <= 1) || !((mode) & 0222), \
+  "Writable arrays with more than 1 element not supported on pre-T devices.")
+#endif
+
+/* type safe macro to declare a map and related accessor functions */
+#define DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md,         \
+                           selinux, pindir, share, min_loader, max_loader, ignore_eng,           \
+                           ignore_user, ignore_userdebug)                                        \
+  DEFINE_BPF_MAP_BASE(the_map, TYPE, sizeof(KeyType), sizeof(ValueType),                         \
+                      num_entries, usr, grp, md, selinux, pindir, share,                         \
+                      KVER_NONE, KVER_INF, min_loader, max_loader,                               \
+                      ignore_eng, ignore_user, ignore_userdebug);                                \
+    BPF_MAP_ASSERT_OK(BPF_MAP_TYPE_##TYPE, (num_entries), (md));                                 \
+    _Static_assert(sizeof(KeyType) < 1024, "aosp/2370288 requires < 1024 byte keys");            \
+    _Static_assert(sizeof(ValueType) < 65536, "aosp/2370288 requires < 65536 byte values");      \
+    BPF_ANNOTATE_KV_PAIR(the_map, KeyType, ValueType);                                           \
+                                                                                                 \
+    static inline __always_inline __unused ValueType* bpf_##the_map##_lookup_elem(               \
+            const KeyType* k) {                                                                  \
+        return bpf_map_lookup_elem_unsafe(&the_map, k);                                          \
+    };                                                                                           \
+                                                                                                 \
+    static inline __always_inline __unused int bpf_##the_map##_update_elem(                      \
+            const KeyType* k, const ValueType* v, unsigned long long flags) {                    \
+        return bpf_map_update_elem_unsafe(&the_map, k, v, flags);                                \
+    };                                                                                           \
+                                                                                                 \
+    static inline __always_inline __unused int bpf_##the_map##_delete_elem(const KeyType* k) {   \
+        return bpf_map_delete_elem_unsafe(&the_map, k);                                          \
+    };
+
+#ifndef DEFAULT_BPF_MAP_SELINUX_CONTEXT
+#define DEFAULT_BPF_MAP_SELINUX_CONTEXT ""
+#endif
+
+#ifndef DEFAULT_BPF_MAP_PIN_SUBDIR
+#define DEFAULT_BPF_MAP_PIN_SUBDIR ""
+#endif
+
+#ifndef DEFAULT_BPF_MAP_UID
+#define DEFAULT_BPF_MAP_UID AID_ROOT
+#elif BPFLOADER_MIN_VER < 28u
+#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
+#endif
+
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md)     \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md,         \
+                       DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, PRIVATE, \
+                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG,                    \
+                       LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
+                       DEFAULT_BPF_MAP_UID, AID_ROOT, 0600)
+
+#define DEFINE_BPF_MAP_RO(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
+                       DEFAULT_BPF_MAP_UID, gid, 0440)
+
+#define DEFINE_BPF_MAP_GWO(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
+                       DEFAULT_BPF_MAP_UID, gid, 0620)
+
+#define DEFINE_BPF_MAP_GRO(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
+                       DEFAULT_BPF_MAP_UID, gid, 0640)
+
+#define DEFINE_BPF_MAP_GRW(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
+                       DEFAULT_BPF_MAP_UID, gid, 0660)
+
+// LLVM eBPF builtins: they directly generate BPF_LD_ABS/BPF_LD_IND (skb may be ignored?)
+unsigned long long load_byte(void* skb, unsigned long long off) asm("llvm.bpf.load.byte");
+unsigned long long load_half(void* skb, unsigned long long off) asm("llvm.bpf.load.half");
+unsigned long long load_word(void* skb, unsigned long long off) asm("llvm.bpf.load.word");
+
+static int (*bpf_probe_read)(void* dst, int size, void* unsafe_ptr) = (void*) BPF_FUNC_probe_read;
+static int (*bpf_probe_read_str)(void* dst, int size, void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_str;
+static int (*bpf_probe_read_user)(void* dst, int size, const void* unsafe_ptr) = (void*)BPF_FUNC_probe_read_user;
+static int (*bpf_probe_read_user_str)(void* dst, int size, const void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_user_str;
+static unsigned long long (*bpf_ktime_get_ns)(void) = (void*) BPF_FUNC_ktime_get_ns;
+static unsigned long long (*bpf_ktime_get_boot_ns)(void) = (void*)BPF_FUNC_ktime_get_boot_ns;
+static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
+static unsigned long long (*bpf_get_current_pid_tgid)(void) = (void*) BPF_FUNC_get_current_pid_tgid;
+static unsigned long long (*bpf_get_current_uid_gid)(void) = (void*) BPF_FUNC_get_current_uid_gid;
+static unsigned long long (*bpf_get_smp_processor_id)(void) = (void*) BPF_FUNC_get_smp_processor_id;
+static long (*bpf_get_stackid)(void* ctx, void* map, uint64_t flags) = (void*) BPF_FUNC_get_stackid;
+static long (*bpf_get_current_comm)(void* buf, uint32_t buf_size) = (void*) BPF_FUNC_get_current_comm;
+
+#define DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv,  \
+                            min_loader, max_loader, opt, selinux, pindir, ignore_eng,    \
+                            ignore_user, ignore_userdebug)                               \
+    const struct bpf_prog_def SECTION("progs") the_prog##_def = {                        \
+        .uid = (prog_uid),                                                               \
+        .gid = (prog_gid),                                                               \
+        .min_kver = (min_kv).kver,                                                       \
+        .max_kver = (max_kv).kver,                                                       \
+        .optional = (opt).optional,                                                      \
+        .bpfloader_min_ver = (min_loader),                                               \
+        .bpfloader_max_ver = (max_loader),                                               \
+        .selinux_context = (selinux),                                                    \
+        .pin_subdir = (pindir),                                                          \
+        .ignore_on_eng = (ignore_eng).ignore_on_eng,                                     \
+        .ignore_on_user = (ignore_user).ignore_on_user,                                  \
+        .ignore_on_userdebug = (ignore_userdebug).ignore_on_userdebug,                   \
+    };                                                                                   \
+    SECTION(SECTION_NAME)                                                                \
+    int the_prog
+
+#ifndef DEFAULT_BPF_PROG_SELINUX_CONTEXT
+#define DEFAULT_BPF_PROG_SELINUX_CONTEXT ""
+#endif
+
+#ifndef DEFAULT_BPF_PROG_PIN_SUBDIR
+#define DEFAULT_BPF_PROG_PIN_SUBDIR ""
+#endif
+
+#define DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
+                                       opt)                                                        \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv,                \
+                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt,                                 \
+                        DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR,             \
+                        LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+// Programs (here used in the sense of functions/sections) marked optional are allowed to fail
+// to load (for example due to missing kernel patches).
+// The bpfloader will just ignore these failures and continue processing the next section.
+//
+// A non-optional program (function/section) failing to load causes a failure and aborts
+// processing of the entire .o, if the .o is additionally marked critical, this will result
+// in the entire bpfloader process terminating with a failure and not setting the bpf.progs_loaded
+// system property.  This in turn results in waitForProgsLoaded() never finishing.
+//
+// ie. a non-optional program in a critical .o is mandatory for kernels matching the min/max kver.
+
+// programs requiring a kernel version >= min_kv && < max_kv
+#define DEFINE_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv) \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
+                                   MANDATORY)
+#define DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, \
+                                            max_kv)                                             \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
+                                   OPTIONAL)
+
+// programs requiring a kernel version >= min_kv
+#define DEFINE_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv)                 \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
+                                   MANDATORY)
+#define DEFINE_OPTIONAL_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv)        \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
+                                   OPTIONAL)
+
+// programs with no kernel version requirements
+#define DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+                                   MANDATORY)
+#define DEFINE_OPTIONAL_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+                                   OPTIONAL)
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
new file mode 100644
index 0000000..00ef91a
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -0,0 +1,267 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+/* This file is separate because it's included both by eBPF programs (via include
+ * in bpf_helpers.h) and directly by the boot time bpfloader (Loader.cpp).
+ */
+
+#include <linux/bpf.h>
+
+// Pull in AID_* constants from //system/core/libcutils/include/private/android_filesystem_config.h
+#include <cutils/android_filesystem_config.h>
+
+/******************************************************************************
+ *                                                                            *
+ *                          ! ! ! W A R N I N G ! ! !                         *
+ *                                                                            *
+ * CHANGES TO THESE STRUCTURE DEFINITIONS OUTSIDE OF AOSP/MASTER *WILL* BREAK *
+ * MAINLINE MODULE COMPATIBILITY                                              *
+ *                                                                            *
+ * AND THUS MAY RESULT IN YOUR DEVICE BRICKING AT SOME ARBITRARY POINT IN     *
+ * THE FUTURE                                                                 *
+ *                                                                            *
+ * (and even in aosp/master you may only append new fields at the very end,   *
+ *  you may *never* delete fields, change their types, ordering, insert in    *
+ *  the middle, etc.  If a mainline module using the old definition has       *
+ *  already shipped (which happens roughly monthly), then it's set in stone)  *
+ *                                                                            *
+ ******************************************************************************/
+
+// These are the values used if these fields are missing
+#define DEFAULT_BPFLOADER_MIN_VER 0u        // v0.0 (this is inclusive ie. >= v0.0)
+#define DEFAULT_BPFLOADER_MAX_VER 0x10000u  // v1.0 (this is exclusive ie. < v1.0)
+#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
+
+/*
+ * 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++
+ * compiler for the native Android architecture for the bpfloader.
+ *
+ * For things to work, their layout must be the same between the two.
+ * The BPF architecture is platform independent ('64-bit LSB bpf').
+ * So this effectively means these structures must be the same layout
+ * on 5 architectures, all of them little endian:
+ *   64-bit BPF, x86_64, arm  and  32-bit x86 and arm
+ *
+ * As such for any types we use inside of these structs we must make sure that
+ * the size and alignment are the same, so the same amount of padding is used.
+ *
+ * Currently we only use: bool, enum bpf_map_type and unsigned int.
+ * Additionally we use char for padding.
+ *
+ * !!! WARNING: HERE BE DRAGONS !!!
+ *
+ * Be particularly careful with 64-bit integers.
+ * You will need to manually override their alignment to 8 bytes.
+ *
+ * To quote some parts of https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69560
+ *
+ * Some types have weaker alignment requirements when they are structure members.
+ *
+ * unsigned long long on x86 is such a type.
+ *
+ * C distinguishes C11 _Alignof (the minimum alignment the type is guaranteed
+ * to have in all contexts, so 4, see min_align_of_type) from GNU C __alignof
+ * (the normal alignment of the type, so 8).
+ *
+ * alignof / _Alignof == minimum alignment required by target ABI
+ * __alignof / __alignof__ == preferred alignment
+ *
+ * When in a struct, apparently the minimum alignment is used.
+ */
+
+_Static_assert(sizeof(bool) == 1, "sizeof bool != 1");
+_Static_assert(__alignof__(bool) == 1, "__alignof__ bool != 1");
+_Static_assert(_Alignof(bool) == 1, "_Alignof bool != 1");
+
+_Static_assert(sizeof(char) == 1, "sizeof char != 1");
+_Static_assert(__alignof__(char) == 1, "__alignof__ char != 1");
+_Static_assert(_Alignof(char) == 1, "_Alignof char != 1");
+
+// This basically verifies that an enum is 'just' a 32-bit int
+_Static_assert(sizeof(enum bpf_map_type) == 4, "sizeof enum bpf_map_type != 4");
+_Static_assert(__alignof__(enum bpf_map_type) == 4, "__alignof__ enum bpf_map_type != 4");
+_Static_assert(_Alignof(enum bpf_map_type) == 4, "_Alignof enum bpf_map_type != 4");
+
+// Linux kernel requires sizeof(int) == 4, sizeof(void*) == sizeof(long), sizeof(long long) == 8
+_Static_assert(sizeof(unsigned int) == 4, "sizeof unsigned int != 4");
+_Static_assert(__alignof__(unsigned int) == 4, "__alignof__ unsigned int != 4");
+_Static_assert(_Alignof(unsigned int) == 4, "_Alignof unsigned int != 4");
+
+// We don't currently use any 64-bit types in these structs, so this is purely to document issue.
+// Here sizeof & __alignof__ are consistent, but _Alignof is not: compile for 'aosp_cf_x86_phone'
+_Static_assert(sizeof(unsigned long long) == 8, "sizeof unsigned long long != 8");
+_Static_assert(__alignof__(unsigned long long) == 8, "__alignof__ unsigned long long != 8");
+// BPF wants 8, but 32-bit x86 wants 4
+//_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+
+
+// for maps:
+struct shared_bool { bool shared; };
+#define PRIVATE ((struct shared_bool){ .shared = false })
+#define SHARED ((struct shared_bool){ .shared = true })
+
+// for programs:
+struct optional_bool { bool optional; };
+#define MANDATORY ((struct optional_bool){ .optional = false })
+#define OPTIONAL ((struct optional_bool){ .optional = true })
+
+// for both maps and programs:
+struct ignore_on_eng_bool { bool ignore_on_eng; };
+#define LOAD_ON_ENG ((struct ignore_on_eng_bool){ .ignore_on_eng = false })
+#define IGNORE_ON_ENG ((struct ignore_on_eng_bool){ .ignore_on_eng = true })
+
+struct ignore_on_user_bool { bool ignore_on_user; };
+#define LOAD_ON_USER ((struct ignore_on_user_bool){ .ignore_on_user = false })
+#define IGNORE_ON_USER ((struct ignore_on_user_bool){ .ignore_on_user = true })
+
+struct ignore_on_userdebug_bool { bool ignore_on_userdebug; };
+#define LOAD_ON_USERDEBUG ((struct ignore_on_userdebug_bool){ .ignore_on_userdebug = false })
+#define IGNORE_ON_USERDEBUG ((struct ignore_on_userdebug_bool){ .ignore_on_userdebug = true })
+
+
+// Length of strings (incl. selinux_context and pin_subdir)
+// in the bpf_map_def and bpf_prog_def structs.
+//
+// WARNING: YOU CANNOT *EVER* CHANGE THESE
+// as this would affect the structure size in backwards incompatible ways
+// and break mainline module loading on older Android T devices
+#define BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE 32
+#define BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE 32
+
+/*
+ * Map structure to be used by Android eBPF C programs. The Android eBPF loader
+ * uses this structure from eBPF object to create maps at boot time.
+ *
+ * The eBPF C program should define structure in the maps section using
+ * SECTION("maps") otherwise it will be ignored by the eBPF loader.
+ *
+ * For example:
+ *   const struct bpf_map_def SECTION("maps") mymap { .type=... , .key_size=... }
+ *
+ * See 'bpf_helpers.h' for helpful macros for eBPF program use.
+ */
+struct bpf_map_def {
+    enum bpf_map_type type;
+    unsigned int key_size;
+    unsigned int value_size;
+    unsigned int max_entries;
+    unsigned int map_flags;
+
+    // The following are not supported by the Android bpfloader:
+    //   unsigned int inner_map_idx;
+    //   unsigned int numa_node;
+
+    unsigned int zero;  // uid_t, for compat with old (buggy) bpfloader must be AID_ROOT == 0
+    unsigned int gid;   // gid_t
+    unsigned int mode;  // mode_t
+
+    // The following fields were added in version 0.1
+    unsigned int bpfloader_min_ver;  // if missing, defaults to 0, ie. v0.0
+    unsigned int bpfloader_max_ver;  // if missing, defaults to 0x10000, ie. v1.0
+
+    // The following fields were added in version 0.2 (S)
+    // kernelVersion() must be >= min_kver and < max_kver
+    unsigned int min_kver;
+    unsigned int max_kver;
+
+    // The following fields were added in version 0.18 (T)
+    //
+    // These are fixed length strings, padded with null bytes
+    //
+    // Warning: supported values depend on .o location
+    // (additionally a newer Android OS and/or bpfloader may support more values)
+    //
+    // overrides default selinux context (which is based on pin subdir)
+    char selinux_context[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE];
+    //
+    // overrides default prefix (which is based on .o location)
+    char pin_subdir[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE];
+
+    bool shared;  // use empty string as 'file' component of pin path - allows cross .o map sharing
+
+    // The following 3 ignore_on_* fields were added in version 0.32 (U). These are ignored in
+    // older bpfloader versions, and zero in programs compiled before 0.32.
+    bool ignore_on_eng:1;
+    bool ignore_on_user:1;
+    bool ignore_on_userdebug:1;
+    // The following 5 ignore_on_* fields were added in version 0.38 (U). These are ignored in
+    // older bpfloader versions, and zero in programs compiled before 0.38.
+    // These are tests on the kernel architecture, ie. they ignore userspace bit-ness.
+    bool ignore_on_arm32:1;
+    bool ignore_on_aarch64:1;
+    bool ignore_on_x86_32:1;
+    bool ignore_on_x86_64:1;
+    bool ignore_on_riscv64:1;
+
+    char pad0[2];  // manually pad up to 4 byte alignment, may be used for extensions in the future
+
+    unsigned int uid;   // uid_t
+};
+
+_Static_assert(sizeof(((struct bpf_map_def *)0)->selinux_context) == 32, "must be 32 bytes");
+_Static_assert(sizeof(((struct bpf_map_def *)0)->pin_subdir) == 32, "must be 32 bytes");
+
+// This needs to be updated whenever the above structure definition is expanded.
+_Static_assert(sizeof(struct bpf_map_def) == 120, "sizeof struct bpf_map_def != 120");
+_Static_assert(__alignof__(struct bpf_map_def) == 4, "__alignof__ struct bpf_map_def != 4");
+_Static_assert(_Alignof(struct bpf_map_def) == 4, "_Alignof struct bpf_map_def != 4");
+
+struct bpf_prog_def {
+    unsigned int uid;
+    unsigned int gid;
+
+    // kernelVersion() must be >= min_kver and < max_kver
+    unsigned int min_kver;
+    unsigned int max_kver;
+
+    bool optional;  // program section (ie. function) may fail to load, continue onto next func.
+
+    // The following 3 ignore_on_* fields were added in version 0.33 (U). These are ignored in
+    // older bpfloader versions, and zero in programs compiled before 0.33.
+    bool ignore_on_eng:1;
+    bool ignore_on_user:1;
+    bool ignore_on_userdebug:1;
+    // The following 5 ignore_on_* fields were added in version 0.38 (U). These are ignored in
+    // older bpfloader versions, and zero in programs compiled before 0.38.
+    // These are tests on the kernel architecture, ie. they ignore userspace bit-ness.
+    bool ignore_on_arm32:1;
+    bool ignore_on_aarch64:1;
+    bool ignore_on_x86_32:1;
+    bool ignore_on_x86_64:1;
+    bool ignore_on_riscv64:1;
+
+    char pad0[2];  // manually pad up to 4 byte alignment, may be used for extensions in the future
+
+    // The following fields were added in version 0.1
+    unsigned int bpfloader_min_ver;  // if missing, defaults to 0, ie. v0.0
+    unsigned int bpfloader_max_ver;  // if missing, defaults to 0x10000, ie. v1.0
+
+    // The following fields were added in version 0.18, see description up above in bpf_map_def
+    char selinux_context[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE];
+    char pin_subdir[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE];
+};
+
+_Static_assert(sizeof(((struct bpf_prog_def *)0)->selinux_context) == 32, "must be 32 bytes");
+_Static_assert(sizeof(((struct bpf_prog_def *)0)->pin_subdir) == 32, "must be 32 bytes");
+
+// This needs to be updated whenever the above structure definition is expanded.
+_Static_assert(sizeof(struct bpf_prog_def) == 92, "sizeof struct bpf_prog_def != 92");
+_Static_assert(__alignof__(struct bpf_prog_def) == 4, "__alignof__ struct bpf_prog_def != 4");
+_Static_assert(_Alignof(struct bpf_prog_def) == 4, "_Alignof struct bpf_prog_def != 4");
diff --git a/staticlibs/native/bpf_syscall_wrappers/Android.bp b/staticlibs/native/bpf_syscall_wrappers/Android.bp
new file mode 100644
index 0000000..1e0cb22
--- /dev/null
+++ b/staticlibs/native/bpf_syscall_wrappers/Android.bp
@@ -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.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_headers {
+    name: "bpf_syscall_wrappers",
+    vendor_available: true,
+    recovery_available: true,
+    host_supported: true,
+    native_bridge_supported: true,
+    export_include_dirs: ["include"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    sdk_version: "30",
+    min_sdk_version: "30",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.art.debug",
+        "com.android.mediaprovider",
+        "com.android.os.statsd",
+        "com.android.resolv",
+        "com.android.tethering",
+    ],
+}
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
new file mode 100644
index 0000000..73cef89
--- /dev/null
+++ b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
@@ -0,0 +1,301 @@
+/*
+ * 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.
+ */
+
+#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
+  #define BPF_FD_TO_U32(x) static_cast<__u32>(x)
+#else
+  #include <android-base/unique_fd.h>
+  #define BPF_FD_TYPE base::unique_fd&
+  #define BPF_FD_TO_U32(x) static_cast<__u32>((x).get())
+#endif
+
+namespace android {
+namespace bpf {
+
+inline uint64_t ptr_to_u64(const void * const x) {
+    return (uint64_t)(uintptr_t)x;
+}
+
+/* Note: bpf_attr is a union which might have a much larger size then the anonymous struct portion
+ * of it that we are using.  The kernel's bpf() system call will perform a strict check to ensure
+ * all unused portions are zero.  It will fail with E2BIG if we don't fully zero bpf_attr.
+ */
+
+inline int bpf(enum bpf_cmd cmd, const bpf_attr& attr) {
+    return syscall(__NR_bpf, cmd, &attr, sizeof(attr));
+}
+
+// this version is meant for use with cmd's which mutate the argument
+inline int bpf(enum bpf_cmd cmd, bpf_attr *attr) {
+    return syscall(__NR_bpf, cmd, attr, sizeof(*attr));
+}
+
+inline int createMap(bpf_map_type map_type, uint32_t key_size, uint32_t value_size,
+                     uint32_t max_entries, uint32_t map_flags) {
+    return bpf(BPF_MAP_CREATE, {
+                                       .map_type = map_type,
+                                       .key_size = key_size,
+                                       .value_size = value_size,
+                                       .max_entries = max_entries,
+                                       .map_flags = map_flags,
+                               });
+}
+
+// Note:
+//   'map_type' must be one of BPF_MAP_TYPE_{ARRAY,HASH}_OF_MAPS
+//   'value_size' must be sizeof(u32), ie. 4
+//   'inner_map_fd' is basically a template specifying {map_type, key_size, value_size, max_entries, map_flags}
+//   of the inner map type (and possibly only key_size/value_size actually matter?).
+inline int createOuterMap(bpf_map_type map_type, uint32_t key_size, uint32_t value_size,
+                          uint32_t max_entries, uint32_t map_flags, const BPF_FD_TYPE inner_map_fd) {
+    return bpf(BPF_MAP_CREATE, {
+                                       .map_type = map_type,
+                                       .key_size = key_size,
+                                       .value_size = value_size,
+                                       .max_entries = max_entries,
+                                       .map_flags = map_flags,
+                                       .inner_map_fd = BPF_FD_TO_U32(inner_map_fd),
+                               });
+}
+
+inline int writeToMapEntry(const BPF_FD_TYPE map_fd, const void* key, const void* value,
+                           uint64_t flags) {
+    return bpf(BPF_MAP_UPDATE_ELEM, {
+                                            .map_fd = BPF_FD_TO_U32(map_fd),
+                                            .key = ptr_to_u64(key),
+                                            .value = ptr_to_u64(value),
+                                            .flags = flags,
+                                    });
+}
+
+inline int findMapEntry(const BPF_FD_TYPE map_fd, const void* key, void* value) {
+    return bpf(BPF_MAP_LOOKUP_ELEM, {
+                                            .map_fd = BPF_FD_TO_U32(map_fd),
+                                            .key = ptr_to_u64(key),
+                                            .value = ptr_to_u64(value),
+                                    });
+}
+
+inline int deleteMapEntry(const BPF_FD_TYPE map_fd, const void* key) {
+    return bpf(BPF_MAP_DELETE_ELEM, {
+                                            .map_fd = BPF_FD_TO_U32(map_fd),
+                                            .key = ptr_to_u64(key),
+                                    });
+}
+
+inline int getNextMapKey(const BPF_FD_TYPE map_fd, const void* key, void* next_key) {
+    return bpf(BPF_MAP_GET_NEXT_KEY, {
+                                             .map_fd = BPF_FD_TO_U32(map_fd),
+                                             .key = ptr_to_u64(key),
+                                             .next_key = ptr_to_u64(next_key),
+                                     });
+}
+
+inline int getFirstMapKey(const BPF_FD_TYPE map_fd, void* firstKey) {
+    return getNextMapKey(map_fd, NULL, firstKey);
+}
+
+inline int bpfFdPin(const BPF_FD_TYPE map_fd, const char* pathname) {
+    return bpf(BPF_OBJ_PIN, {
+                                    .pathname = ptr_to_u64(pathname),
+                                    .bpf_fd = BPF_FD_TO_U32(map_fd),
+                            });
+}
+
+inline int bpfFdGet(const char* pathname, uint32_t flag) {
+    return bpf(BPF_OBJ_GET, {
+                                    .pathname = ptr_to_u64(pathname),
+                                    .file_flags = 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 bpfLock(mapRetrieveLocklessRW(pathname), F_RDLCK);
+}
+
+inline int mapRetrieveRO(const char* pathname) {
+    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 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, {
+                                        .target_fd = BPF_FD_TO_U32(cg_fd),
+                                        .attach_bpf_fd = BPF_FD_TO_U32(prog_fd),
+                                        .attach_type = type,
+                                        .attach_flags = flags,
+                                });
+}
+
+inline int detachProgram(bpf_attach_type type, const BPF_FD_TYPE cg_fd) {
+    return bpf(BPF_PROG_DETACH, {
+                                        .target_fd = BPF_FD_TO_U32(cg_fd),
+                                        .attach_type = type,
+                                });
+}
+
+inline int queryProgram(const BPF_FD_TYPE cg_fd,
+                        enum bpf_attach_type attach_type,
+                        __u32 query_flags = 0,
+                        __u32 attach_flags = 0) {
+    int prog_id = -1;  // equivalent to an array of one integer.
+    bpf_attr arg = {
+            .query = {
+                    .target_fd = BPF_FD_TO_U32(cg_fd),
+                    .attach_type = attach_type,
+                    .query_flags = query_flags,
+                    .attach_flags = attach_flags,
+                    .prog_ids = ptr_to_u64(&prog_id),  // pointer to output array
+                    .prog_cnt = 1,  // in: space - nr of ints in the array, out: used
+            }
+    };
+    int v = bpf(BPF_PROG_QUERY, &arg);
+    if (v) return v;  // error case
+    if (!arg.query.prog_cnt) return 0;  // no program, kernel never returns zero id
+    return prog_id;  // return actual id
+}
+
+inline int detachSingleProgram(bpf_attach_type type, const BPF_FD_TYPE prog_fd,
+                               const BPF_FD_TYPE cg_fd) {
+    return bpf(BPF_PROG_DETACH, {
+                                        .target_fd = BPF_FD_TO_U32(cg_fd),
+                                        .attach_bpf_fd = BPF_FD_TO_U32(prog_fd),
+                                        .attach_type = type,
+                                });
+}
+
+// Available in 4.12 and later kernels.
+inline int runProgram(const BPF_FD_TYPE prog_fd, const void* data,
+                      const uint32_t data_size) {
+    return bpf(BPF_PROG_RUN, {
+                                     .test = {
+                                             .prog_fd = BPF_FD_TO_U32(prog_fd),
+                                             .data_size_in = data_size,
+                                             .data_in = ptr_to_u64(data),
+                                     },
+                             });
+}
+
+// BPF_OBJ_GET_INFO_BY_FD requires 4.14+ kernel
+//
+// Note: some fields are only defined in newer kernels (ie. the map_info struct grows
+// over time), so we need to check that the field we're interested in is actually
+// supported/returned by the running kernel.  We do this by checking it is fully
+// within the bounds of the struct size as reported by the kernel.
+#define DEFINE_BPF_GET_FD(TYPE, NAME, FIELD) \
+inline int bpfGetFd ## NAME(const BPF_FD_TYPE fd) { \
+    struct bpf_ ## TYPE ## _info info = {}; \
+    union bpf_attr attr = { .info = { \
+        .bpf_fd = BPF_FD_TO_U32(fd), \
+        .info_len = sizeof(info), \
+        .info = ptr_to_u64(&info), \
+    }}; \
+    int rv = bpf(BPF_OBJ_GET_INFO_BY_FD, attr); \
+    if (rv) return rv; \
+    if (attr.info.info_len < offsetof(bpf_ ## TYPE ## _info, FIELD) + sizeof(info.FIELD)) { \
+        errno = EOPNOTSUPP; \
+        return -1; \
+    }; \
+    return info.FIELD; \
+}
+
+// All 7 of these fields are already present in Linux v4.14 (even ACK 4.14-P)
+// while BPF_OBJ_GET_INFO_BY_FD is not implemented at all in v4.9 (even ACK 4.9-Q)
+DEFINE_BPF_GET_FD(map, MapType, type)            // int bpfGetFdMapType(const BPF_FD_TYPE map_fd)
+DEFINE_BPF_GET_FD(map, MapId, id)                // int bpfGetFdMapId(const BPF_FD_TYPE map_fd)
+DEFINE_BPF_GET_FD(map, KeySize, key_size)        // int bpfGetFdKeySize(const BPF_FD_TYPE map_fd)
+DEFINE_BPF_GET_FD(map, ValueSize, value_size)    // int bpfGetFdValueSize(const BPF_FD_TYPE map_fd)
+DEFINE_BPF_GET_FD(map, MaxEntries, max_entries)  // int bpfGetFdMaxEntries(const BPF_FD_TYPE map_fd)
+DEFINE_BPF_GET_FD(map, MapFlags, map_flags)      // int bpfGetFdMapFlags(const BPF_FD_TYPE map_fd)
+DEFINE_BPF_GET_FD(prog, ProgId, id)              // int bpfGetFdProgId(const BPF_FD_TYPE prog_fd)
+
+#undef DEFINE_BPF_GET_FD
+
+}  // namespace bpf
+}  // namespace android
+
+#undef BPF_FD_TO_U32
+#undef BPF_FD_TYPE
+#undef BPF_FD_JUST_USE_INT
diff --git a/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
new file mode 100644
index 0000000..969ebd4
--- /dev/null
+++ b/staticlibs/native/bpfmapjni/Android.bp
@@ -0,0 +1,53 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libnet_utils_device_common_bpfjni",
+    srcs: [
+        "com_android_net_module_util_BpfMap.cpp",
+        "com_android_net_module_util_TcUtils.cpp",
+    ],
+    header_libs: [
+        "bpf_headers",
+        "jni_headers",
+    ],
+    shared_libs: [
+        "liblog",
+        "libnativehelper_compat_libc++",
+    ],
+    whole_static_libs: [
+        "libtcutils",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+    ],
+    sdk_version: "current",
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+        // TODO: remove after NetworkStatsService moves to the module.
+        "//frameworks/base/packages/ConnectivityT/service",
+    ],
+}
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
new file mode 100644
index 0000000..1923ceb
--- /dev/null
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+
+#include <errno.h>
+#include <linux/pfkeyv2.h>
+#include <sys/socket.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+
+#include "nativehelper/scoped_primitive_array.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
+
+#include "bpf/KernelUtils.h"
+
+namespace android {
+
+static jint com_android_net_module_util_BpfMap_nativeBpfFdGet(JNIEnv *env, jclass clazz,
+        jstring path, jint mode, jint keySize, jint valueSize) {
+    ScopedUtfChars pathname(env, path);
+
+    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);
+        return -1;
+    }
+
+    if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
+        // These likely fail with -1 and set errno to EINVAL on <4.14
+        if (bpf::bpfGetFdKeySize(fd) != keySize) {
+            close(fd);
+            jniThrowErrnoException(env, "nativeBpfFdGet KeySize", EBADFD);
+            return -1;
+        }
+        if (bpf::bpfGetFdValueSize(fd) != valueSize) {
+            close(fd);
+            jniThrowErrnoException(env, "nativeBpfFdGet ValueSize", EBADFD);
+            return -1;
+        }
+    }
+
+    return fd;
+}
+
+static void com_android_net_module_util_BpfMap_nativeWriteToMapEntry(JNIEnv *env, jobject self,
+        jint fd, jbyteArray key, jbyteArray value, jint flags) {
+    ScopedByteArrayRO keyRO(env, key);
+    ScopedByteArrayRO valueRO(env, value);
+
+    int ret = bpf::writeToMapEntry(static_cast<int>(fd), keyRO.get(), valueRO.get(),
+            static_cast<int>(flags));
+
+    if (ret) jniThrowErrnoException(env, "nativeWriteToMapEntry", errno);
+}
+
+static jboolean throwIfNotEnoent(JNIEnv *env, const char* functionName, int ret, int err) {
+    if (ret == 0) return true;
+
+    if (err != ENOENT) jniThrowErrnoException(env, functionName, err);
+    return false;
+}
+
+static jboolean com_android_net_module_util_BpfMap_nativeDeleteMapEntry(JNIEnv *env, jobject self,
+        jint fd, jbyteArray key) {
+    ScopedByteArrayRO keyRO(env, key);
+
+    // On success, zero is returned.  If the element is not found, -1 is returned and errno is set
+    // to ENOENT.
+    int ret = bpf::deleteMapEntry(static_cast<int>(fd), keyRO.get());
+
+    return throwIfNotEnoent(env, "nativeDeleteMapEntry", ret, errno);
+}
+
+static jboolean com_android_net_module_util_BpfMap_nativeGetNextMapKey(JNIEnv *env, jobject self,
+        jint fd, jbyteArray key, jbyteArray nextKey) {
+    // If key is found, the operation returns zero and sets the next key pointer to the key of the
+    // next element.  If key is not found, the operation returns zero and sets the next key pointer
+    // to the key of the first element.  If key is the last element, -1 is returned and errno is
+    // set to ENOENT.  Other possible errno values are ENOMEM, EFAULT, EPERM, and EINVAL.
+    ScopedByteArrayRW nextKeyRW(env, nextKey);
+    int ret;
+    if (key == nullptr) {
+        // Called by getFirstKey. Find the first key in the map.
+        ret = bpf::getNextMapKey(static_cast<int>(fd), nullptr, nextKeyRW.get());
+    } else {
+        ScopedByteArrayRO keyRO(env, key);
+        ret = bpf::getNextMapKey(static_cast<int>(fd), keyRO.get(), nextKeyRW.get());
+    }
+
+    return throwIfNotEnoent(env, "nativeGetNextMapKey", ret, errno);
+}
+
+static jboolean com_android_net_module_util_BpfMap_nativeFindMapEntry(JNIEnv *env, jobject self,
+        jint fd, jbyteArray key, jbyteArray value) {
+    ScopedByteArrayRO keyRO(env, key);
+    ScopedByteArrayRW valueRW(env, value);
+
+    // If an element is found, the operation returns zero and stores the element's value into
+    // "value".  If no element is found, the operation returns -1 and sets errno to ENOENT.
+    int ret = bpf::findMapEntry(static_cast<int>(fd), keyRO.get(), valueRW.get());
+
+    return throwIfNotEnoent(env, "nativeFindMapEntry", ret, errno);
+}
+
+static void com_android_net_module_util_BpfMap_nativeSynchronizeKernelRCU(JNIEnv *env,
+                                                                          jclass clazz) {
+    const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
+
+    if (pfSocket < 0) {
+        jniThrowErrnoException(env, "nativeSynchronizeKernelRCU:socket", errno);
+        return;
+    }
+
+    if (close(pfSocket)) {
+        jniThrowErrnoException(env, "nativeSynchronizeKernelRCU:close", errno);
+        return;
+    }
+    return;
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    { "nativeBpfFdGet", "(Ljava/lang/String;III)I",
+        (void*) com_android_net_module_util_BpfMap_nativeBpfFdGet },
+    { "nativeWriteToMapEntry", "(I[B[BI)V",
+        (void*) com_android_net_module_util_BpfMap_nativeWriteToMapEntry },
+    { "nativeDeleteMapEntry", "(I[B)Z",
+        (void*) com_android_net_module_util_BpfMap_nativeDeleteMapEntry },
+    { "nativeGetNextMapKey", "(I[B[B)Z",
+        (void*) com_android_net_module_util_BpfMap_nativeGetNextMapKey },
+    { "nativeFindMapEntry", "(I[B[B)Z",
+        (void*) com_android_net_module_util_BpfMap_nativeFindMapEntry },
+    { "nativeSynchronizeKernelRCU", "()V",
+        (void*) com_android_net_module_util_BpfMap_nativeSynchronizeKernelRCU },
+
+};
+
+int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name) {
+    return jniRegisterNativeMethods(env,
+            class_name,
+            gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
new file mode 100644
index 0000000..2a587b6
--- /dev/null
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#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) {
+  jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg,
+                       strerror(error));
+}
+
+static jboolean com_android_net_module_util_TcUtils_isEthernet(JNIEnv *env,
+                                                               jclass clazz,
+                                                               jstring iface) {
+  ScopedUtfChars interface(env, iface);
+  bool result = false;
+  int error = isEthernet(interface.c_str(), result);
+  if (error) {
+    throwIOException(
+        env, "com_android_net_module_util_TcUtils_isEthernet error: ", -error);
+  }
+  // result is not touched when error is returned; leave false.
+  return result;
+}
+
+// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
+// /sys/fs/bpf/... direct-action
+static void com_android_net_module_util_TcUtils_tcFilterAddDevBpf(
+    JNIEnv *env, jclass clazz, jint ifIndex, jboolean ingress, jshort prio,
+    jshort proto, jstring bpfProgPath) {
+  ScopedUtfChars pathname(env, bpfProgPath);
+  int error = tcAddBpfFilter(ifIndex, ingress, prio, proto, pathname.c_str());
+  if (error) {
+    throwIOException(
+        env,
+        "com_android_net_module_util_TcUtils_tcFilterAddDevBpf error: ", -error);
+  }
+}
+
+// tc filter add dev .. ingress prio .. protocol .. matchall \
+//     action police rate .. burst .. conform-exceed pipe/continue \
+//     action bpf object-pinned .. \
+//     drop
+static void com_android_net_module_util_TcUtils_tcFilterAddDevIngressPolice(
+    JNIEnv *env, jclass clazz, jint ifIndex, jshort prio, jshort proto,
+    jint rateInBytesPerSec, jstring bpfProgPath) {
+  ScopedUtfChars pathname(env, bpfProgPath);
+  int error = tcAddIngressPoliceFilter(ifIndex, prio, proto, rateInBytesPerSec,
+                                       pathname.c_str());
+  if (error) {
+    throwIOException(env,
+                     "com_android_net_module_util_TcUtils_"
+                     "tcFilterAddDevIngressPolice error: ",
+                     -error);
+  }
+}
+
+// tc filter del dev .. in/egress prio .. protocol ..
+static void com_android_net_module_util_TcUtils_tcFilterDelDev(
+    JNIEnv *env, jclass clazz, jint ifIndex, jboolean ingress, jshort prio,
+    jshort proto) {
+  int error = tcDeleteFilter(ifIndex, ingress, prio, proto);
+  if (error) {
+    throwIOException(
+        env,
+        "com_android_net_module_util_TcUtils_tcFilterDelDev error: ", -error);
+  }
+}
+
+// tc qdisc add dev .. clsact
+static void com_android_net_module_util_TcUtils_tcQdiscAddDevClsact(JNIEnv *env,
+                                                                    jclass clazz,
+                                                                    jint ifIndex) {
+  int error = tcAddQdiscClsact(ifIndex);
+  if (error) {
+    throwIOException(
+        env,
+        "com_android_net_module_util_TcUtils_tcQdiscAddDevClsact error: ", -error);
+  }
+}
+
+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.
+ */
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    {"isEthernet", "(Ljava/lang/String;)Z",
+     (void *)com_android_net_module_util_TcUtils_isEthernet},
+    {"tcFilterAddDevBpf", "(IZSSLjava/lang/String;)V",
+     (void *)com_android_net_module_util_TcUtils_tcFilterAddDevBpf},
+    {"tcFilterAddDevIngressPolice", "(ISSILjava/lang/String;)V",
+     (void *)com_android_net_module_util_TcUtils_tcFilterAddDevIngressPolice},
+    {"tcFilterDelDev", "(IZSS)V",
+     (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,
+                                                 char const *class_name) {
+  return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/native/bpfutiljni/Android.bp b/staticlibs/native/bpfutiljni/Android.bp
new file mode 100644
index 0000000..1ef01a6
--- /dev/null
+++ b/staticlibs/native/bpfutiljni/Android.bp
@@ -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.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libnet_utils_device_common_bpfutils",
+    srcs: ["com_android_net_module_util_BpfUtils.cpp"],
+    header_libs: [
+        "bpf_syscall_wrappers",
+        "jni_headers",
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+        "libnativehelper_compat_libc++",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+    ],
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    visibility: [
+        "//packages/modules/Connectivity/service",
+    ],
+}
diff --git a/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp b/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
new file mode 100644
index 0000000..bcc3ded
--- /dev/null
+++ b/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+#include <android-base/unique_fd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <nativehelper/scoped_utf_chars.h>
+#include <string.h>
+#include <sys/socket.h>
+
+#include "BpfSyscallWrappers.h"
+
+namespace android {
+
+using base::unique_fd;
+
+static jint com_android_net_module_util_BpfUtil_getProgramIdFromCgroup(JNIEnv *env,
+        jclass clazz, jint type, jstring cgroupPath) {
+
+    ScopedUtfChars dirPath(env, cgroupPath);
+    unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
+    if (cg_fd == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException",
+                             "Failed to open the cgroup directory %s: %s",
+                             dirPath.c_str(), strerror(errno));
+        return -1;
+    }
+
+    int id = bpf::queryProgram(cg_fd, (bpf_attach_type) type);
+    if (id < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException",
+                             "Failed to query bpf program %d at %s: %s",
+                             type, dirPath.c_str(), strerror(errno));
+        return -1;
+    }
+    return id;  // may return 0 meaning none
+}
+
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    { "native_getProgramIdFromCgroup", "(ILjava/lang/String;)I",
+        (void*) com_android_net_module_util_BpfUtil_getProgramIdFromCgroup },
+};
+
+int register_com_android_net_module_util_BpfUtils(JNIEnv* env, char const* class_name) {
+    return jniRegisterNativeMethods(env,
+            class_name,
+            gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/native/ip_checksum/Android.bp b/staticlibs/native/ip_checksum/Android.bp
new file mode 100644
index 0000000..e2e118e
--- /dev/null
+++ b/staticlibs/native/ip_checksum/Android.bp
@@ -0,0 +1,47 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libip_checksum",
+
+    srcs: [
+        "checksum.c",
+    ],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+
+    export_include_dirs: ["."],
+
+    // Needed because libnetutils depends on libip_checksum, and libnetutils has
+    // vendor_available = true. Making this library vendor_available does not create any maintenance
+    // burden or version skew issues because this library is only static, not dynamic, and thus is
+    // not installed on the device.
+    //
+    // TODO: delete libnetutils from the VNDK in T, and remove this.
+    vendor_available: true,
+
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+}
diff --git a/staticlibs/native/ip_checksum/checksum.c b/staticlibs/native/ip_checksum/checksum.c
new file mode 100644
index 0000000..5641fad
--- /dev/null
+++ b/staticlibs/native/ip_checksum/checksum.c
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * checksum.c - ipv4/ipv6 checksum calculation
+ */
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
+
+#include "checksum.h"
+
+/* function: ip_checksum_add
+ * adds data to a checksum. only known to work on little-endian hosts
+ * current - the current checksum (or 0 to start a new checksum)
+ *   data        - the data to add to the checksum
+ *   len         - length of data
+ */
+uint32_t ip_checksum_add(uint32_t current, const void* data, int len) {
+    const uint16_t* data_16 = data;
+
+    while (len >= 2) {
+        current += *data_16;
+        data_16++;
+        len -= 2;
+    }
+    if (len) current += *(uint8_t*)data_16;  // assumes little endian!
+
+    return current;
+}
+
+/* function: ip_checksum_fold
+ * folds a 32-bit partial checksum into 16 bits
+ *   temp_sum - sum from ip_checksum_add
+ *   returns: the folded checksum in network byte order
+ */
+uint16_t ip_checksum_fold(uint32_t temp_sum) {
+    temp_sum = (temp_sum >> 16) + (temp_sum & 0xFFFF);
+    temp_sum = (temp_sum >> 16) + (temp_sum & 0xFFFF);
+    return temp_sum;
+}
+
+/* function: ip_checksum_finish
+ * folds and closes the checksum
+ *   temp_sum - sum from ip_checksum_add
+ *   returns: a header checksum value in network byte order
+ */
+uint16_t ip_checksum_finish(uint32_t temp_sum) {
+    return ~ip_checksum_fold(temp_sum);
+}
+
+/* function: ip_checksum
+ * combined ip_checksum_add and ip_checksum_finish
+ *   data - data to checksum
+ *   len  - length of data
+ */
+uint16_t ip_checksum(const void* data, int len) {
+    return ip_checksum_finish(ip_checksum_add(0xFFFF, data, len));
+}
+
+/* function: ipv6_pseudo_header_checksum
+ * calculate the pseudo header checksum for use in tcp/udp/icmp headers
+ *   ip6      - the ipv6 header
+ *   len      - the transport length (transport header + payload)
+ *   protocol - the transport layer protocol, can be different from ip6->ip6_nxt for fragments
+ */
+uint32_t ipv6_pseudo_header_checksum(const struct ip6_hdr* ip6, uint32_t len, uint8_t protocol) {
+    uint32_t checksum_len = htonl(len);
+    uint32_t checksum_next = htonl(protocol);
+    uint32_t current = 0;
+
+    current = ip_checksum_add(current, &(ip6->ip6_src), sizeof(struct in6_addr));
+    current = ip_checksum_add(current, &(ip6->ip6_dst), sizeof(struct in6_addr));
+    current = ip_checksum_add(current, &checksum_len, sizeof(checksum_len));
+    current = ip_checksum_add(current, &checksum_next, sizeof(checksum_next));
+
+    return current;
+}
+
+/* function: ipv4_pseudo_header_checksum
+ * calculate the pseudo header checksum for use in tcp/udp headers
+ *   ip      - the ipv4 header
+ *   len     - the transport length (transport header + payload)
+ */
+uint32_t ipv4_pseudo_header_checksum(const struct iphdr* ip, uint16_t len) {
+    uint16_t temp_protocol = htons(ip->protocol);
+    uint16_t temp_length = htons(len);
+    uint32_t current = 0;
+
+    current = ip_checksum_add(current, &(ip->saddr), sizeof(uint32_t));
+    current = ip_checksum_add(current, &(ip->daddr), sizeof(uint32_t));
+    current = ip_checksum_add(current, &temp_protocol, sizeof(uint16_t));
+    current = ip_checksum_add(current, &temp_length, sizeof(uint16_t));
+
+    return current;
+}
+
+/* function: ip_checksum_adjust
+ * calculates a new checksum given a previous checksum and the old and new pseudo-header checksums
+ *   checksum    - the header checksum in the original packet in network byte order
+ *   old_hdr_sum - the pseudo-header checksum of the original packet
+ *   new_hdr_sum - the pseudo-header checksum of the translated packet
+ *   returns: the new header checksum in network byte order
+ */
+uint16_t ip_checksum_adjust(uint16_t checksum, uint32_t old_hdr_sum, uint32_t new_hdr_sum) {
+    // Algorithm suggested in RFC 1624.
+    // http://tools.ietf.org/html/rfc1624#section-3
+    checksum = ~checksum;
+    uint16_t folded_sum = ip_checksum_fold(new_hdr_sum + checksum);
+    uint16_t folded_old = ip_checksum_fold(old_hdr_sum);
+    if (folded_sum > folded_old) {
+        return ~(folded_sum - folded_old);
+    } else {
+        return ~(folded_sum - folded_old - 1);  // end-around borrow
+    }
+}
diff --git a/staticlibs/native/ip_checksum/checksum.h b/staticlibs/native/ip_checksum/checksum.h
new file mode 100644
index 0000000..87393c9
--- /dev/null
+++ b/staticlibs/native/ip_checksum/checksum.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <stdint.h>
+
+uint32_t ip_checksum_add(uint32_t current, const void* data, int len);
+uint16_t ip_checksum_finish(uint32_t temp_sum);
+uint16_t ip_checksum(const void* data, int len);
+
+uint32_t ipv6_pseudo_header_checksum(const struct ip6_hdr* ip6, uint32_t len, uint8_t protocol);
+uint32_t ipv4_pseudo_header_checksum(const struct iphdr* ip, uint16_t len);
+
+uint16_t ip_checksum_adjust(uint16_t checksum, uint32_t old_hdr_sum, uint32_t new_hdr_sum);
diff --git a/staticlibs/native/netjniutils/Android.bp b/staticlibs/native/netjniutils/Android.bp
new file mode 100644
index 0000000..4cab459
--- /dev/null
+++ b/staticlibs/native/netjniutils/Android.bp
@@ -0,0 +1,42 @@
+// 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 {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libnetjniutils",
+    srcs: ["netjniutils.cpp"],
+    static_libs: [
+        "libmodules-utils-build",
+    ],
+    header_libs: ["jni_headers"],
+    shared_libs: ["liblog"],
+    export_header_lib_headers: ["jni_headers"],
+    export_include_dirs: ["include"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+    ],
+    sdk_version: "30",
+    min_sdk_version: "30",
+    apex_available: [
+        "//apex_available:anyapex",
+        "//apex_available:platform",
+    ],
+    visibility: ["//visibility:public"],
+}
diff --git a/staticlibs/native/netjniutils/include/netjniutils/netjniutils.h b/staticlibs/native/netjniutils/include/netjniutils/netjniutils.h
new file mode 100644
index 0000000..1b2b035
--- /dev/null
+++ b/staticlibs/native/netjniutils/include/netjniutils/netjniutils.h
@@ -0,0 +1,25 @@
+// 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.
+
+#pragma once
+
+#include <jni.h>
+
+namespace android {
+namespace netjniutils {
+
+int GetNativeFileDescriptor(JNIEnv* env, jobject javaFd);
+
+}  // namespace netjniutils
+}  // namepsace android
diff --git a/staticlibs/native/netjniutils/netjniutils.cpp b/staticlibs/native/netjniutils/netjniutils.cpp
new file mode 100644
index 0000000..8b7f903
--- /dev/null
+++ b/staticlibs/native/netjniutils/netjniutils.cpp
@@ -0,0 +1,85 @@
+// 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.
+
+#define LOG_TAG "netjniutils"
+
+#include "netjniutils/netjniutils.h"
+#include <android-modules-utils/sdk_level.h>
+
+#include <dlfcn.h>
+#include <stdbool.h>
+#include <string.h>
+#include <sys/system_properties.h>
+
+#include <android/api-level.h>
+#include <android/log.h>
+
+namespace android {
+namespace netjniutils {
+
+namespace {
+
+
+int GetNativeFileDescriptorWithoutNdk(JNIEnv* env, jobject javaFd) {
+  // Prior to Android S, we need to find the descriptor field in the FileDescriptor class. The
+  // symbol name has been stable in libcore, but is a private implementation detail.
+  // Older libnativehelper_compat_c++ versions had a jniGetFdFromFileDescriptor method, but this
+  // was removed in S to replace it with the NDK API in libnativehelper.
+  // The code is copied here instead. This code can be removed once R is not supported anymore.
+  static const jfieldID descriptorFieldID = [env]() -> jfieldID {
+    jclass cls = env->FindClass("java/io/FileDescriptor");
+    jfieldID fieldID = env->GetFieldID(cls, "descriptor", "I");
+    env->DeleteLocalRef(cls);
+    if (fieldID == nullptr) {
+      __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "Failed to get descriptor field.");
+    }
+    return fieldID;
+  }();
+
+  return javaFd != nullptr ? env->GetIntField(javaFd, descriptorFieldID) : -1;
+}
+
+int GetNativeFileDescriptorWithNdk(JNIEnv* env, jobject javaFd) {
+  // Since Android S, there is an NDK API to get a file descriptor present in libnativehelper.so.
+  // libnativehelper is loaded into all processes by the zygote since the zygote uses it
+  // to load the Android Runtime and is also a public library (because of the NDK API).
+  typedef int (*ndkGetFd_t)(JNIEnv*, jobject);
+  static const ndkGetFd_t ndkGetFd = []() -> ndkGetFd_t {
+    void* handle = dlopen("libnativehelper.so", RTLD_NOLOAD | RTLD_NODELETE);
+    auto ndkGetFd = reinterpret_cast<ndkGetFd_t>(dlsym(handle, "AFileDescriptor_getFd"));
+    if (ndkGetFd == nullptr) {
+      __android_log_print(ANDROID_LOG_FATAL, LOG_TAG,
+                          "Failed to dlsym(AFileDescriptor_getFd): %s", dlerror());
+      dlclose(handle);
+    }
+    return ndkGetFd;
+  }();
+
+  return javaFd != nullptr ? ndkGetFd(env, javaFd) : -1;
+}
+
+}  //  namespace
+
+int GetNativeFileDescriptor(JNIEnv* env, jobject javaFd) {
+  static const bool preferNdkFileDescriptorApi = []() -> bool
+   { return android::modules::sdklevel::IsAtLeastS(); }();
+  if (preferNdkFileDescriptorApi) {
+    return GetNativeFileDescriptorWithNdk(env, javaFd);
+  } else {
+    return GetNativeFileDescriptorWithoutNdk(env, javaFd);
+  }
+}
+
+}  // namespace netjniutils
+}  // namespace android
diff --git a/staticlibs/native/nettestutils/Android.bp b/staticlibs/native/nettestutils/Android.bp
new file mode 100644
index 0000000..ef87f04
--- /dev/null
+++ b/staticlibs/native/nettestutils/Android.bp
@@ -0,0 +1,39 @@
+// 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 {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libnettestutils",
+    export_include_dirs: ["include"],
+    srcs: ["DumpService.cpp"],
+
+    // Don't depend on libbinder, because some users of this library may not want to link to it.
+    // CtsNativeNetPlatformTestCases is one such user. See r.android.com/2599405 .
+    header_libs: [
+        "libbinder_headers",
+    ],
+
+    shared_libs: [
+        "libutils",
+        "libbinder_ndk",
+    ],
+    cflags: [
+        "-Werror",
+        "-Wall",
+    ],
+}
diff --git a/staticlibs/native/nettestutils/DumpService.cpp b/staticlibs/native/nettestutils/DumpService.cpp
new file mode 100644
index 0000000..40c3b9a
--- /dev/null
+++ b/staticlibs/native/nettestutils/DumpService.cpp
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+
+#include "nettestutils/DumpService.h"
+
+#include <android-base/file.h>
+#include <android/binder_status.h>
+
+#include <sstream>
+#include <thread>
+
+// Version for code using libbinder (e.g., AIDL interfaces with the C++ backend).
+android::status_t dumpService(const android::sp<android::IBinder>& binder,
+                              const std::vector<std::string>& args,
+                              std::vector<std::string>& outputLines) {
+  if (!outputLines.empty()) return -EUCLEAN;
+
+  android::base::unique_fd localFd, remoteFd;
+  if (!Pipe(&localFd, &remoteFd)) return -errno;
+
+  android::Vector<android::String16> str16Args;
+  for (const auto& arg : args) {
+    str16Args.push(android::String16(arg.c_str()));
+  }
+  android::status_t ret;
+  // dump() blocks until another thread has consumed all its output.
+  std::thread dumpThread =
+      std::thread([&ret, binder, remoteFd{std::move(remoteFd)}, str16Args]() {
+        ret = binder->dump(remoteFd, str16Args);
+      });
+
+  std::string dumpContent;
+  if (!android::base::ReadFdToString(localFd.get(), &dumpContent)) {
+    return -errno;
+  }
+  dumpThread.join();
+  if (ret != android::OK) return ret;
+
+  std::stringstream dumpStream(std::move(dumpContent));
+  std::string line;
+  while (std::getline(dumpStream, line)) {
+    outputLines.push_back(line);
+  }
+
+  return android::OK;
+}
+
+// Version for code using libbinder_ndk (e.g., AIDL interfaces with the NDK backend)..
+android::status_t dumpService(const ndk::SpAIBinder& binder,
+                              const char** args,
+                              uint32_t num_args,
+                              std::vector<std::string>& outputLines) {
+  if (!outputLines.empty()) return -EUCLEAN;
+
+  android::base::unique_fd localFd, remoteFd;
+  if (!Pipe(&localFd, &remoteFd)) return -errno;
+
+  android::status_t ret;
+  // dump() blocks until another thread has consumed all its output.
+  std::thread dumpThread =
+      std::thread([&ret, binder, remoteFd{std::move(remoteFd)}, args, num_args]() {
+        ret = AIBinder_dump(binder.get(), remoteFd, args, num_args);
+  });
+
+  std::string dumpContent;
+  if (!android::base::ReadFdToString(localFd.get(), &dumpContent)) {
+    return -errno;
+  }
+  dumpThread.join();
+  if (ret != android::OK) return ret;
+
+  std::stringstream dumpStream(dumpContent);
+  std::string line;
+  while (std::getline(dumpStream, line)) {
+    outputLines.push_back(std::move(line));
+  }
+
+  return android::OK;
+}
diff --git a/staticlibs/native/nettestutils/include/nettestutils/DumpService.h b/staticlibs/native/nettestutils/include/nettestutils/DumpService.h
new file mode 100644
index 0000000..323d752
--- /dev/null
+++ b/staticlibs/native/nettestutils/include/nettestutils/DumpService.h
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+#include <binder/Binder.h>
+#include <android/binder_auto_utils.h>
+
+#include <vector>
+
+android::status_t dumpService(const android::sp<android::IBinder>& binder,
+                              const std::vector<std::string>& args,
+                              std::vector<std::string>& outputLines);
+
+android::status_t dumpService(const ndk::SpAIBinder& binder,
+                              const char** args,
+                              uint32_t num_args,
+                              std::vector<std::string>& outputLines);
diff --git a/staticlibs/native/tcutils/Android.bp b/staticlibs/native/tcutils/Android.bp
new file mode 100644
index 0000000..e4742ce
--- /dev/null
+++ b/staticlibs/native/tcutils/Android.bp
@@ -0,0 +1,71 @@
+// 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 {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libtcutils",
+    srcs: ["tcutils.cpp"],
+    export_include_dirs: ["include"],
+    header_libs: [
+        "bpf_headers",
+        "libbase_headers",
+    ],
+    shared_libs: [
+        "liblog",
+    ],
+    stl: "libc++_static",
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+    ],
+    sdk_version: "current",
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+        "//system/netd:__subpackages__",
+    ],
+}
+
+cc_test {
+    name: "libtcutils_test",
+    srcs: [
+        "tests/tcutils_test.cpp",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-error=unused-variable",
+    ],
+    header_libs: ["bpf_headers"],
+    static_libs: [
+        "libgmock",
+        "libtcutils",
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    min_sdk_version: "30",
+    require_root: true,
+    test_suites: ["general-tests"],
+}
diff --git a/staticlibs/native/tcutils/include/tcutils/tcutils.h b/staticlibs/native/tcutils/include/tcutils/tcutils.h
new file mode 100644
index 0000000..a8ec2e8
--- /dev/null
+++ b/staticlibs/native/tcutils/include/tcutils/tcutils.h
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <linux/rtnetlink.h>
+
+namespace android {
+
+int isEthernet(const char *iface, bool &isEthernet);
+
+int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags);
+
+static inline int tcAddQdiscClsact(int ifIndex) {
+  return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE);
+}
+
+static inline int tcReplaceQdiscClsact(int ifIndex) {
+  return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_REPLACE);
+}
+
+static inline int tcDeleteQdiscClsact(int ifIndex) {
+  return doTcQdiscClsact(ifIndex, RTM_DELQDISC, 0);
+}
+
+int tcAddBpfFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto,
+                   const char *bpfProgPath);
+int tcAddIngressPoliceFilter(int ifIndex, uint16_t prio, uint16_t proto,
+                             unsigned rateInBytesPerSec,
+                             const char *bpfProgPath);
+int tcDeleteFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto);
+
+} // namespace android
diff --git a/staticlibs/native/tcutils/logging.h b/staticlibs/native/tcutils/logging.h
new file mode 100644
index 0000000..7ed8f66
--- /dev/null
+++ b/staticlibs/native/tcutils/logging.h
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <android/log.h>
+#include <stdarg.h>
+
+#ifndef LOG_TAG
+#define LOG_TAG "TcUtils_Undef"
+#endif
+
+namespace android {
+
+static inline void ALOGE(const char *fmt...) {
+  va_list args;
+  va_start(args, fmt);
+  __android_log_vprint(ANDROID_LOG_ERROR, LOG_TAG, fmt, args);
+  va_end(args);
+}
+
+static inline void ALOGW(const char *fmt...) {
+  va_list args;
+  va_start(args, fmt);
+  __android_log_vprint(ANDROID_LOG_WARN, LOG_TAG, fmt, args);
+  va_end(args);
+}
+
+static inline void ALOGI(const char *fmt...) {
+  va_list args;
+  va_start(args, fmt);
+  __android_log_vprint(ANDROID_LOG_INFO, LOG_TAG, fmt, args);
+  va_end(args);
+}
+
+static inline void ALOGD(const char *fmt...) {
+  va_list args;
+  va_start(args, fmt);
+  __android_log_vprint(ANDROID_LOG_DEBUG, LOG_TAG, fmt, args);
+  va_end(args);
+}
+
+}
diff --git a/staticlibs/native/tcutils/tcutils.cpp b/staticlibs/native/tcutils/tcutils.cpp
new file mode 100644
index 0000000..21e781c
--- /dev/null
+++ b/staticlibs/native/tcutils/tcutils.cpp
@@ -0,0 +1,732 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "TcUtils"
+
+#include "tcutils/tcutils.h"
+
+#include "logging.h"
+#include "bpf/KernelUtils.h"
+
+#include <BpfSyscallWrappers.h>
+#include <android-base/scopeguard.h>
+#include <android-base/unique_fd.h>
+#include <arpa/inet.h>
+#include <cerrno>
+#include <cstring>
+#include <libgen.h>
+#include <linux/if_arp.h>
+#include <linux/if_ether.h>
+#include <linux/netlink.h>
+#include <linux/pkt_cls.h>
+#include <linux/pkt_sched.h>
+#include <linux/rtnetlink.h>
+#include <linux/tc_act/tc_bpf.h>
+#include <net/if.h>
+#include <stdio.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include <utility>
+
+// The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c.
+#define CLS_BPF_NAME_LEN 256
+
+// Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c.
+#define CLS_BPF_KIND_NAME "bpf"
+
+namespace android {
+namespace {
+
+using base::make_scope_guard;
+using base::unique_fd;
+
+/**
+ * IngressPoliceFilterBuilder builds a nlmsg request equivalent to the following
+ * tc command:
+ *
+ * tc filter add dev .. ingress prio .. protocol .. matchall \
+ *     action police rate .. burst .. conform-exceed pipe/continue \
+ *     action bpf object-pinned .. \
+ *     drop
+ */
+class IngressPoliceFilterBuilder final {
+  // default mtu is 2047, so the cell logarithm factor (cell_log) is 3.
+  // 0x7FF >> 0x3FF x 2^1 >> 0x1FF x 2^2 >> 0xFF x 2^3
+  static constexpr int RTAB_CELL_LOGARITHM = 3;
+  static constexpr size_t RTAB_SIZE = 256;
+  static constexpr unsigned TIME_UNITS_PER_SEC = 1000000;
+
+  struct Request {
+    nlmsghdr n;
+    tcmsg t;
+    struct {
+      nlattr attr;
+      char str[NLMSG_ALIGN(sizeof("matchall"))];
+    } kind;
+    struct {
+      nlattr attr;
+      struct {
+        nlattr attr;
+        struct {
+          nlattr attr;
+          struct {
+            nlattr attr;
+            char str[NLMSG_ALIGN(sizeof("police"))];
+          } kind;
+          struct {
+            nlattr attr;
+            struct {
+              nlattr attr;
+              struct tc_police obj;
+            } police;
+            struct {
+              nlattr attr;
+              uint32_t u32[RTAB_SIZE];
+            } rtab;
+            struct {
+              nlattr attr;
+              int32_t s32;
+            } notexceedact;
+          } opt;
+        } act1;
+        struct {
+          nlattr attr;
+          struct {
+            nlattr attr;
+            char str[NLMSG_ALIGN(sizeof("bpf"))];
+          } kind;
+          struct {
+            nlattr attr;
+            struct {
+              nlattr attr;
+              uint32_t u32;
+            } fd;
+            struct {
+              nlattr attr;
+              char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)];
+            } name;
+            struct {
+              nlattr attr;
+              struct tc_act_bpf obj;
+            } parms;
+          } opt;
+        } act2;
+      } acts;
+    } opt;
+  };
+
+  // class members
+  const unsigned mBurstInBytes;
+  const char *mBpfProgPath;
+  unique_fd mBpfFd;
+  Request mRequest;
+
+  static double getTickInUsec() {
+    FILE *fp = fopen("/proc/net/psched", "re");
+    if (!fp) {
+      ALOGE("fopen(\"/proc/net/psched\"): %s", strerror(errno));
+      return 0.0;
+    }
+    auto scopeGuard = make_scope_guard([fp] { fclose(fp); });
+
+    uint32_t t2us;
+    uint32_t us2t;
+    uint32_t clockRes;
+    const bool isError =
+        fscanf(fp, "%08x%08x%08x", &t2us, &us2t, &clockRes) != 3;
+
+    if (isError) {
+      ALOGE("fscanf(/proc/net/psched, \"%%08x%%08x%%08x\"): %s",
+               strerror(errno));
+      return 0.0;
+    }
+
+    const double clockFactor =
+        static_cast<double>(clockRes) / TIME_UNITS_PER_SEC;
+    return static_cast<double>(t2us) / static_cast<double>(us2t) * clockFactor;
+  }
+
+  static inline const double kTickInUsec = getTickInUsec();
+
+public:
+  // clang-format off
+  IngressPoliceFilterBuilder(int ifIndex, uint16_t prio, uint16_t proto, unsigned rateInBytesPerSec,
+                      unsigned burstInBytes, const char* bpfProgPath)
+      : mBurstInBytes(burstInBytes),
+        mBpfProgPath(bpfProgPath),
+        mRequest{
+            .n = {
+                .nlmsg_len = sizeof(mRequest),
+                .nlmsg_type = RTM_NEWTFILTER,
+                .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL | NLM_F_CREATE,
+            },
+            .t = {
+                .tcm_family = AF_UNSPEC,
+                .tcm_ifindex = ifIndex,
+                .tcm_handle = TC_H_UNSPEC,
+                .tcm_parent = TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS),
+                .tcm_info = (static_cast<uint32_t>(prio) << 16)
+                            | static_cast<uint32_t>(htons(proto)),
+            },
+            .kind = {
+                .attr = {
+                    .nla_len = sizeof(mRequest.kind),
+                    .nla_type = TCA_KIND,
+                },
+                .str = "matchall",
+            },
+            .opt = {
+                .attr = {
+                    .nla_len = sizeof(mRequest.opt),
+                    .nla_type = TCA_OPTIONS,
+                },
+                .acts = {
+                    .attr = {
+                        .nla_len = sizeof(mRequest.opt.acts),
+                        .nla_type = TCA_MATCHALL_ACT,
+                    },
+                    .act1 = {
+                        .attr = {
+                            .nla_len = sizeof(mRequest.opt.acts.act1),
+                            .nla_type = 1, // action priority
+                        },
+                        .kind = {
+                            .attr = {
+                                .nla_len = sizeof(mRequest.opt.acts.act1.kind),
+                                .nla_type = TCA_ACT_KIND,
+                            },
+                            .str = "police",
+                        },
+                        .opt = {
+                            .attr = {
+                                .nla_len = sizeof(mRequest.opt.acts.act1.opt),
+                                .nla_type = TCA_ACT_OPTIONS | NLA_F_NESTED,
+                            },
+                            .police = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act1.opt.police),
+                                    .nla_type = TCA_POLICE_TBF,
+                                },
+                                .obj = {
+                                    .action = TC_ACT_PIPE,
+                                    .burst = 0,
+                                    .rate = {
+                                        .cell_log = RTAB_CELL_LOGARITHM,
+                                        .linklayer = TC_LINKLAYER_ETHERNET,
+                                        .cell_align = -1,
+                                        .rate = rateInBytesPerSec,
+                                    },
+                                },
+                            },
+                            .rtab = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act1.opt.rtab),
+                                    .nla_type = TCA_POLICE_RATE,
+                                },
+                                .u32 = {},
+                            },
+                            .notexceedact = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act1.opt.notexceedact),
+                                    .nla_type = TCA_POLICE_RESULT,
+                                },
+                                .s32 = TC_ACT_UNSPEC,
+                            },
+                        },
+                    },
+                    .act2 = {
+                        .attr = {
+                            .nla_len = sizeof(mRequest.opt.acts.act2),
+                            .nla_type = 2, // action priority
+                        },
+                        .kind = {
+                            .attr = {
+                                .nla_len = sizeof(mRequest.opt.acts.act2.kind),
+                                .nla_type = TCA_ACT_KIND,
+                            },
+                            .str = "bpf",
+                        },
+                        .opt = {
+                            .attr = {
+                                .nla_len = sizeof(mRequest.opt.acts.act2.opt),
+                                .nla_type = TCA_ACT_OPTIONS | NLA_F_NESTED,
+                            },
+                            .fd = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act2.opt.fd),
+                                    .nla_type = TCA_ACT_BPF_FD,
+                                },
+                                .u32 = 0, // set during build()
+                            },
+                            .name = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act2.opt.name),
+                                    .nla_type = TCA_ACT_BPF_NAME,
+                                },
+                                .str = "placeholder",
+                            },
+                            .parms = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act2.opt.parms),
+                                    .nla_type = TCA_ACT_BPF_PARMS,
+                                },
+                                .obj = {
+                                    // default action to be executed when bpf prog
+                                    // returns TC_ACT_UNSPEC.
+                                    .action = TC_ACT_SHOT,
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        } {
+      // constructor body
+  }
+  // clang-format on
+
+  constexpr unsigned getRequestSize() const { return sizeof(Request); }
+
+private:
+  unsigned calculateXmitTime(unsigned size) {
+    const uint32_t rate = mRequest.opt.acts.act1.opt.police.obj.rate.rate;
+    return (static_cast<double>(size) / static_cast<double>(rate)) *
+           TIME_UNITS_PER_SEC * kTickInUsec;
+  }
+
+  void initBurstRate() {
+    mRequest.opt.acts.act1.opt.police.obj.burst =
+        calculateXmitTime(mBurstInBytes);
+  }
+
+  // Calculates a table with 256 transmission times for different packet sizes
+  // (all the way up to MTU). RTAB_CELL_LOGARITHM is used as a scaling factor.
+  // In this case, MTU size is always 2048, so RTAB_CELL_LOGARITHM is always
+  // 3. Therefore, this function generates the transmission times for packets
+  // of size 1..256 x 2^3.
+  void initRateTable() {
+    for (unsigned i = 0; i < RTAB_SIZE; ++i) {
+      unsigned adjustedSize = (i + 1) << RTAB_CELL_LOGARITHM;
+      mRequest.opt.acts.act1.opt.rtab.u32[i] = calculateXmitTime(adjustedSize);
+    }
+  }
+
+  int initBpfFd() {
+    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.get());
+    snprintf(mRequest.opt.acts.act2.opt.name.str,
+             sizeof(mRequest.opt.acts.act2.opt.name.str), "%s:[*fsobj]",
+             basename(mBpfProgPath));
+
+    return 0;
+  }
+
+public:
+  int build() {
+    if (kTickInUsec == 0.0) {
+      return -EINVAL;
+    }
+
+    initBurstRate();
+    initRateTable();
+    return initBpfFd();
+  }
+
+  const Request *getRequest() const {
+    // Make sure to call build() before calling this function. Otherwise, the
+    // request will be invalid.
+    return &mRequest;
+  }
+};
+
+const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
+const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
+
+int sendAndProcessNetlinkResponse(const void *req, int len) {
+  // TODO: use unique_fd instead of ScopeGuard
+  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;
+  }
+
+  static constexpr int on = 1;
+  if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) {
+    int error = errno;
+    ALOGE("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, 1): %d", error);
+    return -error;
+  }
+
+  if (setsockopt(fd, SOL_NETLINK, NETLINK_EXT_ACK, &on, sizeof(on))) {
+    int error = errno;
+    ALOGW("setsockopt(fd, SOL_NETLINK, NETLINK_EXT_ACK, 1): %d", error);
+    // will fail on 4.9 kernels so don't: return -error;
+  }
+
+  // this is needed to get valid strace netlink parsing, it allocates the pid
+  if (bind(fd, (const struct sockaddr *)&KERNEL_NLADDR,
+           sizeof(KERNEL_NLADDR))) {
+    int error = errno;
+    ALOGE("bind(fd, {AF_NETLINK, 0, 0}: %d)", error);
+    return -error;
+  }
+
+  // we do not want to receive messages from anyone besides the kernel
+  if (connect(fd, (const struct sockaddr *)&KERNEL_NLADDR,
+              sizeof(KERNEL_NLADDR))) {
+    int error = errno;
+    ALOGE("connect(fd, {AF_NETLINK, 0, 0}): %d", error);
+    return -error;
+  }
+
+  int rv = send(fd, req, len, 0);
+
+  if (rv == -1) {
+    int error = errno;
+    ALOGE("send(fd, req, len, 0) failed: %d", error);
+    return -error;
+  }
+
+  if (rv != len) {
+    ALOGE("send(fd, req, len = %d, 0) returned invalid message size %d", len,
+             rv);
+    return -EMSGSIZE;
+  }
+
+  struct {
+    nlmsghdr h;
+    nlmsgerr e;
+    char buf[256];
+  } resp = {};
+
+  rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC);
+
+  if (rv == -1) {
+    int error = errno;
+    ALOGE("recv() failed: %d", error);
+    return -error;
+  }
+
+  if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
+    ALOGE("recv() returned short packet: %d", rv);
+    return -EBADMSG;
+  }
+
+  if (resp.h.nlmsg_len != (unsigned)rv) {
+    ALOGE("recv() returned invalid header length: %d != %d",
+             resp.h.nlmsg_len, rv);
+    return -EBADMSG;
+  }
+
+  if (resp.h.nlmsg_type != NLMSG_ERROR) {
+    ALOGE("recv() did not return NLMSG_ERROR message: %d",
+             resp.h.nlmsg_type);
+    return -ENOMSG;
+  }
+
+  if (resp.e.error) {
+    ALOGE("NLMSG_ERROR message return error: %d", resp.e.error);
+  }
+  return resp.e.error; // returns 0 on success
+}
+
+int hardwareAddressType(const char *interface) {
+  unique_fd fd(socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0));
+  if (!fd.ok())
+    return -errno;
+
+  struct ifreq ifr = {};
+  // We use strncpy() instead of strlcpy() since kernel has to be able
+  // to handle non-zero terminated junk passed in by userspace anyway,
+  // and this way too long interface names (more than IFNAMSIZ-1 = 15
+  // characters plus terminating NULL) will not get truncated to 15
+  // characters and zero-terminated and thus potentially erroneously
+  // match a truncated interface if one were to exist.
+  strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name));
+
+  if (ioctl(fd, SIOCGIFHWADDR, &ifr, sizeof(ifr))) {
+    return -errno;
+  }
+  return ifr.ifr_hwaddr.sa_family;
+}
+
+} // namespace
+
+int isEthernet(const char *iface, bool &isEthernet) {
+  int rv = hardwareAddressType(iface);
+  if (rv < 0) {
+    ALOGE("Get hardware address type of interface %s failed: %s", iface,
+             strerror(-rv));
+    return rv;
+  }
+
+  // Backwards compatibility with pre-GKI kernels that use various custom
+  // ARPHRD_* for their cellular interface
+  switch (rv) {
+  // ARPHRD_PUREIP on at least some Mediatek Android kernels
+  // example: wembley with 4.19 kernel
+  case 520:
+  // in Linux 4.14+ rmnet support was upstreamed and ARHRD_RAWIP became 519,
+  // but it is 530 on at least some Qualcomm Android 4.9 kernels with rmnet
+  // example: Pixel 3 family
+  case 530:
+    // >5.4 kernels are GKI2.0 and thus upstream compatible, however 5.10
+    // shipped with Android S, so (for safety) let's limit ourselves to
+    // >5.10, ie. 5.11+ as a guarantee we're on Android T+ and thus no
+    // longer need this non-upstream compatibility logic
+    static bool is_pre_5_11_kernel = !bpf::isAtLeastKernelVersion(5, 11, 0);
+    if (is_pre_5_11_kernel)
+      return false;
+  }
+
+  switch (rv) {
+  case ARPHRD_ETHER:
+    isEthernet = true;
+    return 0;
+  case ARPHRD_NONE:
+  case ARPHRD_PPP:
+  case ARPHRD_RAWIP:
+    isEthernet = false;
+    return 0;
+  default:
+    ALOGE("Unknown hardware address type %d on interface %s", rv, iface);
+    return -EAFNOSUPPORT;
+  }
+}
+
+// ADD:     nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_EXCL|NLM_F_CREATE
+// REPLACE: nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_CREATE|NLM_F_REPLACE
+// DEL:     nlMsgType=RTM_DELQDISC nlMsgFlags=0
+int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags) {
+  // This is the name of the qdisc we are attaching.
+  // Some hoop jumping to make this compile time constant with known size,
+  // so that the structure declaration is well defined at compile time.
+#define CLSACT "clsact"
+  // sizeof() includes the terminating NULL
+  static constexpr size_t ASCIIZ_LEN_CLSACT = sizeof(CLSACT);
+
+  const struct {
+    nlmsghdr n;
+    tcmsg t;
+    struct {
+      nlattr attr;
+      char str[NLMSG_ALIGN(ASCIIZ_LEN_CLSACT)];
+    } kind;
+  } req = {
+      .n =
+          {
+              .nlmsg_len = sizeof(req),
+              .nlmsg_type = nlMsgType,
+              .nlmsg_flags =
+                  static_cast<__u16>(NETLINK_REQUEST_FLAGS | nlMsgFlags),
+          },
+      .t =
+          {
+              .tcm_family = AF_UNSPEC,
+              .tcm_ifindex = ifIndex,
+              .tcm_handle = TC_H_MAKE(TC_H_CLSACT, 0),
+              .tcm_parent = TC_H_CLSACT,
+          },
+      .kind =
+          {
+              .attr =
+                  {
+                      .nla_len = NLA_HDRLEN + ASCIIZ_LEN_CLSACT,
+                      .nla_type = TCA_KIND,
+                  },
+              .str = CLSACT,
+          },
+  };
+#undef CLSACT
+
+  return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
+// /sys/fs/bpf/... direct-action
+int tcAddBpfFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto,
+                   const char *bpfProgPath) {
+  unique_fd bpfFd(bpf::retrieveProgram(bpfProgPath));
+  if (!bpfFd.ok()) {
+    ALOGE("retrieveProgram failed: %d", errno);
+    return -errno;
+  }
+
+  struct {
+    nlmsghdr n;
+    tcmsg t;
+    struct {
+      nlattr attr;
+      // The maximum classifier name length is defined in
+      // tcf_proto_ops in include/net/sch_generic.h.
+      char str[NLMSG_ALIGN(sizeof(CLS_BPF_KIND_NAME))];
+    } kind;
+    struct {
+      nlattr attr;
+      struct {
+        nlattr attr;
+        __u32 u32;
+      } fd;
+      struct {
+        nlattr attr;
+        char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)];
+      } name;
+      struct {
+        nlattr attr;
+        __u32 u32;
+      } flags;
+    } options;
+  } req = {
+      .n =
+          {
+              .nlmsg_len = sizeof(req),
+              .nlmsg_type = RTM_NEWTFILTER,
+              .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE,
+          },
+      .t =
+          {
+              .tcm_family = AF_UNSPEC,
+              .tcm_ifindex = ifIndex,
+              .tcm_handle = TC_H_UNSPEC,
+              .tcm_parent = TC_H_MAKE(TC_H_CLSACT, ingress ? TC_H_MIN_INGRESS
+                                                           : TC_H_MIN_EGRESS),
+              .tcm_info =
+                  static_cast<__u32>((static_cast<uint16_t>(prio) << 16) |
+                                     htons(static_cast<uint16_t>(proto))),
+          },
+      .kind =
+          {
+              .attr =
+                  {
+                      .nla_len = sizeof(req.kind),
+                      .nla_type = TCA_KIND,
+                  },
+              .str = CLS_BPF_KIND_NAME,
+          },
+      .options =
+          {
+              .attr =
+                  {
+                      .nla_len = sizeof(req.options),
+                      .nla_type = NLA_F_NESTED | TCA_OPTIONS,
+                  },
+              .fd =
+                  {
+                      .attr =
+                          {
+                              .nla_len = sizeof(req.options.fd),
+                              .nla_type = TCA_BPF_FD,
+                          },
+                      .u32 = static_cast<__u32>(bpfFd),
+                  },
+              .name =
+                  {
+                      .attr =
+                          {
+                              .nla_len = sizeof(req.options.name),
+                              .nla_type = TCA_BPF_NAME,
+                          },
+                      // Visible via 'tc filter show', but
+                      // is overwritten by strncpy below
+                      .str = "placeholder",
+                  },
+              .flags =
+                  {
+                      .attr =
+                          {
+                              .nla_len = sizeof(req.options.flags),
+                              .nla_type = TCA_BPF_FLAGS,
+                          },
+                      .u32 = TCA_BPF_FLAG_ACT_DIRECT,
+                  },
+          },
+  };
+
+  snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]",
+           basename(bpfProgPath));
+
+  int error = sendAndProcessNetlinkResponse(&req, sizeof(req));
+  return error;
+}
+
+// tc filter add dev .. ingress prio .. protocol .. matchall \
+//     action police rate .. burst .. conform-exceed pipe/continue \
+//     action bpf object-pinned .. \
+//     drop
+//
+// TODO: tc-police does not do ECN marking, so in the future, we should consider
+// adding a second tc-police filter at a lower priority that rate limits traffic
+// at something like 0.8 times the global rate limit and ecn marks exceeding
+// packets inside a bpf program (but does not drop them).
+int tcAddIngressPoliceFilter(int ifIndex, uint16_t prio, uint16_t proto,
+                             unsigned rateInBytesPerSec,
+                             const char *bpfProgPath) {
+  // TODO: this value needs to be validated.
+  // TCP IW10 (initial congestion window) means servers will send 10 mtus worth
+  // of data on initial connect.
+  // If nic is LRO capable it could aggregate up to 64KiB, so again probably a
+  // bad idea to set burst below that, because ingress packets could get
+  // aggregated to 64KiB at the nic.
+  // I don't know, but I wonder whether we shouldn't just do 128KiB and not do
+  // any math.
+  static constexpr unsigned BURST_SIZE_IN_BYTES = 128 * 1024; // 128KiB
+  IngressPoliceFilterBuilder filter(ifIndex, prio, proto, rateInBytesPerSec,
+                                    BURST_SIZE_IN_BYTES, bpfProgPath);
+  const int error = filter.build();
+  if (error) {
+    return error;
+  }
+  return sendAndProcessNetlinkResponse(filter.getRequest(),
+                                       filter.getRequestSize());
+}
+
+// tc filter del dev .. in/egress prio .. protocol ..
+int tcDeleteFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto) {
+  const struct {
+    nlmsghdr n;
+    tcmsg t;
+  } req = {
+      .n =
+          {
+              .nlmsg_len = sizeof(req),
+              .nlmsg_type = RTM_DELTFILTER,
+              .nlmsg_flags = NETLINK_REQUEST_FLAGS,
+          },
+      .t =
+          {
+              .tcm_family = AF_UNSPEC,
+              .tcm_ifindex = ifIndex,
+              .tcm_handle = TC_H_UNSPEC,
+              .tcm_parent = TC_H_MAKE(TC_H_CLSACT, ingress ? TC_H_MIN_INGRESS
+                                                           : TC_H_MIN_EGRESS),
+              .tcm_info =
+                  static_cast<__u32>((static_cast<uint16_t>(prio) << 16) |
+                                     htons(static_cast<uint16_t>(proto))),
+          },
+  };
+
+  return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+} // namespace android
diff --git a/staticlibs/native/tcutils/tests/tcutils_test.cpp b/staticlibs/native/tcutils/tests/tcutils_test.cpp
new file mode 100644
index 0000000..7732247
--- /dev/null
+++ b/staticlibs/native/tcutils/tests/tcutils_test.cpp
@@ -0,0 +1,154 @@
+/*
+ * Copyright 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.
+ *
+ * TcUtilsTest.cpp - unit tests for TcUtils.cpp
+ */
+
+#include <gtest/gtest.h>
+
+#include "bpf/KernelUtils.h"
+#include <tcutils/tcutils.h>
+
+#include <BpfSyscallWrappers.h>
+#include <errno.h>
+#include <linux/if_ether.h>
+
+namespace android {
+
+TEST(LibTcUtilsTest, IsEthernetOfNonExistingIf) {
+  bool result = false;
+  int error = isEthernet("not_existing_if", result);
+  ASSERT_FALSE(result);
+  ASSERT_EQ(-ENODEV, error);
+}
+
+TEST(LibTcUtilsTest, IsEthernetOfLoopback) {
+  bool result = false;
+  int error = isEthernet("lo", result);
+  ASSERT_FALSE(result);
+  ASSERT_EQ(-EAFNOSUPPORT, error);
+}
+
+// If wireless 'wlan0' interface exists it should be Ethernet.
+// See also HardwareAddressTypeOfWireless.
+TEST(LibTcUtilsTest, IsEthernetOfWireless) {
+  bool result = false;
+  int error = isEthernet("wlan0", result);
+  if (!result && error == -ENODEV)
+    return;
+
+  ASSERT_EQ(0, error);
+  ASSERT_TRUE(result);
+}
+
+// If cellular 'rmnet_data0' interface exists it should
+// *probably* not be Ethernet and instead be RawIp.
+// See also HardwareAddressTypeOfCellular.
+TEST(LibTcUtilsTest, IsEthernetOfCellular) {
+  bool result = false;
+  int error = isEthernet("rmnet_data0", result);
+  if (!result && error == -ENODEV)
+    return;
+
+  ASSERT_EQ(0, error);
+  ASSERT_FALSE(result);
+}
+
+// See Linux kernel source in include/net/flow.h
+static constexpr int LOOPBACK_IFINDEX = 1;
+
+TEST(LibTcUtilsTest, AttachReplaceDetachClsactLo) {
+  // This attaches and detaches a configuration-less and thus no-op clsact
+  // qdisc to loopback interface (and it takes fractions of a second)
+  EXPECT_EQ(0, tcAddQdiscClsact(LOOPBACK_IFINDEX));
+  EXPECT_EQ(0, tcReplaceQdiscClsact(LOOPBACK_IFINDEX));
+  EXPECT_EQ(0, tcDeleteQdiscClsact(LOOPBACK_IFINDEX));
+  EXPECT_EQ(-EINVAL, tcDeleteQdiscClsact(LOOPBACK_IFINDEX));
+}
+
+TEST(LibTcUtilsTest, AddAndDeleteBpfFilter) {
+  // TODO: this should likely be in the tethering module, where using netd.h would be ok
+  static constexpr char bpfProgPath[] =
+      "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream6_ether";
+  const int errNOENT = bpf::isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
+
+  // static test values
+  static constexpr bool ingress = true;
+  static constexpr uint16_t prio = 17;
+  static constexpr uint16_t proto = ETH_P_ALL;
+
+  // try to delete missing filter from missing qdisc
+  EXPECT_EQ(-EINVAL, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto));
+  // try to attach bpf filter to missing qdisc
+  EXPECT_EQ(-EINVAL, tcAddBpfFilter(LOOPBACK_IFINDEX, ingress, prio, proto,
+                                    bpfProgPath));
+  // add the clsact qdisc
+  EXPECT_EQ(0, tcAddQdiscClsact(LOOPBACK_IFINDEX));
+  // try to delete missing filter when there is a qdisc attached
+  EXPECT_EQ(-errNOENT, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto));
+  // add and delete a bpf filter
+  EXPECT_EQ(
+      0, tcAddBpfFilter(LOOPBACK_IFINDEX, ingress, prio, proto, bpfProgPath));
+  EXPECT_EQ(0, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto));
+  // try to remove the same filter a second time
+  EXPECT_EQ(-errNOENT, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto));
+  // remove the clsact qdisc
+  EXPECT_EQ(0, tcDeleteQdiscClsact(LOOPBACK_IFINDEX));
+  // once again, try to delete missing filter from missing qdisc
+  EXPECT_EQ(-EINVAL, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto));
+}
+
+TEST(LibTcUtilsTest, AddAndDeleteIngressPoliceFilter) {
+  // TODO: this should likely be in the tethering module, where using netd.h would be ok
+  static constexpr char bpfProgPath[] =
+      "/sys/fs/bpf/netd_shared/prog_netd_schedact_ingress_account";
+  int fd = bpf::retrieveProgram(bpfProgPath);
+  ASSERT_LE(3, fd);
+  close(fd);
+
+  const int errNOENT = bpf::isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
+
+  // static test values
+  static constexpr unsigned rateInBytesPerSec =
+      1024 * 1024; // 8mbit/s => 1mbyte/s => 1024*1024 bytes/s.
+  static constexpr uint16_t prio = 17;
+  static constexpr uint16_t proto = ETH_P_ALL;
+
+  // try to delete missing filter from missing qdisc
+  EXPECT_EQ(-EINVAL,
+            tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto));
+  // try to attach bpf filter to missing qdisc
+  EXPECT_EQ(-EINVAL, tcAddIngressPoliceFilter(LOOPBACK_IFINDEX, prio, proto,
+                                              rateInBytesPerSec, bpfProgPath));
+  // add the clsact qdisc
+  EXPECT_EQ(0, tcAddQdiscClsact(LOOPBACK_IFINDEX));
+  // try to delete missing filter when there is a qdisc attached
+  EXPECT_EQ(-errNOENT,
+            tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto));
+  // add and delete a bpf filter
+  EXPECT_EQ(0, tcAddIngressPoliceFilter(LOOPBACK_IFINDEX, prio, proto,
+                                        rateInBytesPerSec, bpfProgPath));
+  EXPECT_EQ(0, tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto));
+  // try to remove the same filter a second time
+  EXPECT_EQ(-errNOENT,
+            tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto));
+  // remove the clsact qdisc
+  EXPECT_EQ(0, tcDeleteQdiscClsact(LOOPBACK_IFINDEX));
+  // once again, try to delete missing filter from missing qdisc
+  EXPECT_EQ(-EINVAL,
+            tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto));
+}
+
+} // namespace android
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
new file mode 100644
index 0000000..44abba2
--- /dev/null
+++ b/staticlibs/netd/Android.bp
@@ -0,0 +1,262 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "netd_aidl_interface-lateststable-java",
+    sdk_version: "system_current",
+    min_sdk_version: "30",
+    static_libs: [
+        "netd_aidl_interface-V15-java",
+    ],
+    apex_available: [
+        "//apex_available:platform", // used from services.net
+        "com.android.tethering",
+        "com.android.wifi",
+    ],
+}
+
+cc_library_static {
+    name: "netd_event_listener_interface-lateststable-ndk",
+    whole_static_libs: [
+        "netd_event_listener_interface-V1-ndk",
+    ],
+    apex_available: [
+        "com.android.resolv",
+    ],
+    min_sdk_version: "30",
+}
+
+cc_library_static {
+    name: "netd_aidl_interface-lateststable-ndk",
+    whole_static_libs: [
+        "netd_aidl_interface-V15-ndk",
+    ],
+    apex_available: [
+        "com.android.resolv",
+        "com.android.tethering",
+    ],
+    min_sdk_version: "30",
+}
+
+cc_defaults {
+    name: "netd_aidl_interface_lateststable_cpp_static",
+    static_libs: ["netd_aidl_interface-V15-cpp"],
+}
+
+cc_defaults {
+    name: "netd_aidl_interface_lateststable_cpp_shared",
+    shared_libs: ["netd_aidl_interface-V15-cpp"],
+}
+
+aidl_interface {
+    name: "netd_aidl_interface",
+    local_include_dir: "binder",
+    srcs: [
+        "binder/android/net/INetd.aidl",
+        // AIDL interface that callers can implement to receive networking events from netd.
+        "binder/android/net/INetdUnsolicitedEventListener.aidl",
+        "binder/android/net/InterfaceConfigurationParcel.aidl",
+        "binder/android/net/IpSecMigrateInfoParcel.aidl",
+        "binder/android/net/MarkMaskParcel.aidl",
+        "binder/android/net/NativeNetworkConfig.aidl",
+        "binder/android/net/NativeNetworkType.aidl",
+        "binder/android/net/NativeVpnType.aidl",
+        "binder/android/net/RouteInfoParcel.aidl",
+        "binder/android/net/TetherConfigParcel.aidl",
+        "binder/android/net/TetherOffloadRuleParcel.aidl",
+        "binder/android/net/TetherStatsParcel.aidl",
+        "binder/android/net/UidRangeParcel.aidl",
+        // Add new AIDL classes in android.net.netd.aidl to consist with other network modules.
+        "binder/android/net/netd/aidl/**/*.aidl",
+    ],
+    backend: {
+        cpp: {
+            gen_log: true,
+        },
+        java: {
+            // TODO: Remove apex_available and restrict visibility to only mainline modules that are
+            // either outside the system server or use jarjar to rename the generated AIDL classes.
+            apex_available: [
+                "//apex_available:platform", // used from services.net
+                "com.android.tethering",
+                "com.android.wifi",
+            ],
+            // this is part of updatable modules(NetworkStack) which targets 30(R)
+            min_sdk_version: "30",
+        },
+        ndk: {
+            apex_available: [
+                "//apex_available:platform",
+                "com.android.tethering",
+            ],
+            // This is necessary for the DnsResovler tests to run in Android R.
+            // Soong would recognize this value and produce the R compatible aidl library.
+            min_sdk_version: "30",
+        },
+    },
+    versions_with_info: [
+        {
+            version: "1",
+            imports: [],
+        },
+        {
+            version: "2",
+            imports: [],
+        },
+        {
+            version: "3",
+            imports: [],
+        },
+        {
+            version: "4",
+            imports: [],
+        },
+        {
+            version: "5",
+            imports: [],
+        },
+        {
+            version: "6",
+            imports: [],
+        },
+        {
+            version: "7",
+            imports: [],
+        },
+        {
+            version: "8",
+            imports: [],
+        },
+        {
+            version: "9",
+            imports: [],
+        },
+        {
+            version: "10",
+            imports: [],
+        },
+        {
+            version: "11",
+            imports: [],
+        },
+        {
+            version: "12",
+            imports: [],
+        },
+        {
+            version: "13",
+            imports: [],
+        },
+        {
+            version: "14",
+            imports: [],
+        },
+        {
+            version: "15",
+            imports: [],
+        },
+
+    ],
+    frozen: true,
+
+}
+
+java_library {
+    name: "netd_event_listener_interface-lateststable-java",
+    sdk_version: "system_current",
+    min_sdk_version: "30",
+    static_libs: [
+        "netd_event_listener_interface-V1-java",
+    ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.wifi",
+        "com.android.tethering",
+    ],
+}
+
+aidl_interface {
+    name: "netd_event_listener_interface",
+    local_include_dir: "binder",
+    srcs: [
+        "binder/android/net/metrics/INetdEventListener.aidl",
+    ],
+
+    backend: {
+        ndk: {
+            apex_available: [
+                "//apex_available:platform",
+                "com.android.resolv",
+            ],
+            min_sdk_version: "30",
+        },
+        java: {
+            apex_available: [
+                "//apex_available:platform",
+                "com.android.wifi",
+                "com.android.tethering",
+            ],
+            min_sdk_version: "30",
+        },
+    },
+    versions_with_info: [
+        {
+            version: "1",
+            imports: [],
+        },
+        {
+            version: "2",
+            imports: [],
+        },
+
+    ],
+    frozen: true,
+
+}
+
+aidl_interface {
+    name: "mdns_aidl_interface",
+    local_include_dir: "binder",
+    srcs: [
+        "binder/android/net/mdns/aidl/**/*.aidl",
+    ],
+    backend: {
+        java: {
+            sdk_version: "module_current",
+            apex_available: [
+                "//apex_available:platform",
+                "com.android.tethering",
+            ],
+            min_sdk_version: "30",
+        },
+    },
+    versions_with_info: [
+        {
+            version: "1",
+            imports: [],
+        },
+        {
+            version: "2",
+            imports: [],
+        },
+
+    ],
+    frozen: true,
+
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/1/.hash b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/.hash
new file mode 100644
index 0000000..d952c59
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/.hash
@@ -0,0 +1 @@
+ae4cfe565d66acc7d816aabd0dfab991e64031ab
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/DiscoveryInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/DiscoveryInfo.aidl
new file mode 100644
index 0000000..d31a327
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/DiscoveryInfo.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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable DiscoveryInfo {
+  int id;
+  int result;
+  @utf8InCpp String serviceName;
+  @utf8InCpp String registrationType;
+  @utf8InCpp String domainName;
+  int interfaceIdx;
+  int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/GetAddressInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/GetAddressInfo.aidl
new file mode 100644
index 0000000..2049274
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/GetAddressInfo.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable GetAddressInfo {
+  int id;
+  int result;
+  @utf8InCpp String hostname;
+  @utf8InCpp String address;
+  int interfaceIdx;
+  int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/IMDns.aidl
new file mode 100644
index 0000000..ecbe966
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/IMDns.aidl
@@ -0,0 +1,46 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+interface IMDns {
+  void startDaemon();
+  void stopDaemon();
+  void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+  void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+  void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+  void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+  void stopOperation(int id);
+  void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+  void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/IMDnsEventListener.aidl
new file mode 100644
index 0000000..4625cac
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -0,0 +1,50 @@
+/**
+ * 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.mdns.aidl;
+/* @hide */
+interface IMDnsEventListener {
+  oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+  oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+  oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+  oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
+  const int SERVICE_DISCOVERY_FAILED = 602;
+  const int SERVICE_FOUND = 603;
+  const int SERVICE_LOST = 604;
+  const int SERVICE_REGISTRATION_FAILED = 605;
+  const int SERVICE_REGISTERED = 606;
+  const int SERVICE_RESOLUTION_FAILED = 607;
+  const int SERVICE_RESOLVED = 608;
+  const int SERVICE_GET_ADDR_FAILED = 611;
+  const int SERVICE_GET_ADDR_SUCCESS = 612;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/RegistrationInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/RegistrationInfo.aidl
new file mode 100644
index 0000000..185111b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/RegistrationInfo.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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable RegistrationInfo {
+  int id;
+  int result;
+  @utf8InCpp String serviceName;
+  @utf8InCpp String registrationType;
+  int port;
+  byte[] txtRecord;
+  int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/ResolutionInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/ResolutionInfo.aidl
new file mode 100644
index 0000000..4aa7d79
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/1/android/net/mdns/aidl/ResolutionInfo.aidl
@@ -0,0 +1,48 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable ResolutionInfo {
+  int id;
+  int result;
+  @utf8InCpp String serviceName;
+  @utf8InCpp String registrationType;
+  @utf8InCpp String domain;
+  @utf8InCpp String serviceFullName;
+  @utf8InCpp String hostname;
+  int port;
+  byte[] txtRecord;
+  int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash
new file mode 100644
index 0000000..785d42d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash
@@ -0,0 +1 @@
+0e5d9ad0664b8b3ec9d323534c42333cf6f6ed3d
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl
new file mode 100644
index 0000000..d31a327
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable DiscoveryInfo {
+  int id;
+  int result;
+  @utf8InCpp String serviceName;
+  @utf8InCpp String registrationType;
+  @utf8InCpp String domainName;
+  int interfaceIdx;
+  int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl
new file mode 100644
index 0000000..2049274
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable GetAddressInfo {
+  int id;
+  int result;
+  @utf8InCpp String hostname;
+  @utf8InCpp String address;
+  int interfaceIdx;
+  int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl
new file mode 100644
index 0000000..d84742b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl
@@ -0,0 +1,73 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+interface IMDns {
+  /**
+   * @deprecated unimplemented on V+.
+   */
+  void startDaemon();
+  /**
+   * @deprecated unimplemented on V+.
+   */
+  void stopDaemon();
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void stopOperation(int id);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl
new file mode 100644
index 0000000..187a3d2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -0,0 +1,62 @@
+/**
+ * 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.mdns.aidl;
+/* @hide */
+interface IMDnsEventListener {
+  /**
+   * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+   */
+  oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+  /**
+   * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+   */
+  oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+  /**
+   * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+   */
+  oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+  /**
+   * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+   */
+  oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
+  const int SERVICE_DISCOVERY_FAILED = 602;
+  const int SERVICE_FOUND = 603;
+  const int SERVICE_LOST = 604;
+  const int SERVICE_REGISTRATION_FAILED = 605;
+  const int SERVICE_REGISTERED = 606;
+  const int SERVICE_RESOLUTION_FAILED = 607;
+  const int SERVICE_RESOLVED = 608;
+  const int SERVICE_GET_ADDR_FAILED = 611;
+  const int SERVICE_GET_ADDR_SUCCESS = 612;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl
new file mode 100644
index 0000000..185111b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable RegistrationInfo {
+  int id;
+  int result;
+  @utf8InCpp String serviceName;
+  @utf8InCpp String registrationType;
+  int port;
+  byte[] txtRecord;
+  int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl
new file mode 100644
index 0000000..4aa7d79
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl
@@ -0,0 +1,48 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable ResolutionInfo {
+  int id;
+  int result;
+  @utf8InCpp String serviceName;
+  @utf8InCpp String registrationType;
+  @utf8InCpp String domain;
+  @utf8InCpp String serviceFullName;
+  @utf8InCpp String hostname;
+  int port;
+  byte[] txtRecord;
+  int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/DiscoveryInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/DiscoveryInfo.aidl
new file mode 100644
index 0000000..d31a327
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/DiscoveryInfo.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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable DiscoveryInfo {
+  int id;
+  int result;
+  @utf8InCpp String serviceName;
+  @utf8InCpp String registrationType;
+  @utf8InCpp String domainName;
+  int interfaceIdx;
+  int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/GetAddressInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/GetAddressInfo.aidl
new file mode 100644
index 0000000..2049274
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/GetAddressInfo.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable GetAddressInfo {
+  int id;
+  int result;
+  @utf8InCpp String hostname;
+  @utf8InCpp String address;
+  int interfaceIdx;
+  int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
new file mode 100644
index 0000000..d84742b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
@@ -0,0 +1,73 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+interface IMDns {
+  /**
+   * @deprecated unimplemented on V+.
+   */
+  void startDaemon();
+  /**
+   * @deprecated unimplemented on V+.
+   */
+  void stopDaemon();
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void stopOperation(int id);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+  /**
+   * @deprecated unimplemented on U+.
+   */
+  void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
new file mode 100644
index 0000000..187a3d2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -0,0 +1,62 @@
+/**
+ * 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.mdns.aidl;
+/* @hide */
+interface IMDnsEventListener {
+  /**
+   * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+   */
+  oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+  /**
+   * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+   */
+  oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+  /**
+   * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+   */
+  oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+  /**
+   * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+   */
+  oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
+  const int SERVICE_DISCOVERY_FAILED = 602;
+  const int SERVICE_FOUND = 603;
+  const int SERVICE_LOST = 604;
+  const int SERVICE_REGISTRATION_FAILED = 605;
+  const int SERVICE_REGISTERED = 606;
+  const int SERVICE_RESOLUTION_FAILED = 607;
+  const int SERVICE_RESOLVED = 608;
+  const int SERVICE_GET_ADDR_FAILED = 611;
+  const int SERVICE_GET_ADDR_SUCCESS = 612;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/RegistrationInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/RegistrationInfo.aidl
new file mode 100644
index 0000000..185111b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/RegistrationInfo.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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable RegistrationInfo {
+  int id;
+  int result;
+  @utf8InCpp String serviceName;
+  @utf8InCpp String registrationType;
+  int port;
+  byte[] txtRecord;
+  int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/ResolutionInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/ResolutionInfo.aidl
new file mode 100644
index 0000000..4aa7d79
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/ResolutionInfo.aidl
@@ -0,0 +1,48 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable ResolutionInfo {
+  int id;
+  int result;
+  @utf8InCpp String serviceName;
+  @utf8InCpp String registrationType;
+  @utf8InCpp String domain;
+  @utf8InCpp String serviceFullName;
+  @utf8InCpp String hostname;
+  int port;
+  byte[] txtRecord;
+  int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/1/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/1/.hash
new file mode 100644
index 0000000..d33e903
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/1/.hash
@@ -0,0 +1 @@
+69c2ac134efbb31e9591d7e5c3640fb839e23bdb
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/INetd.aidl
new file mode 100644
index 0000000..664c643
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/INetd.aidl
@@ -0,0 +1,132 @@
+package android.net;
+interface INetd {
+  boolean isAlive();
+  boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isWhitelist, in int[] uids);
+  boolean bandwidthEnableDataSaver(boolean enable);
+  void networkCreatePhysical(int netId, int permission);
+  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);
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  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 LOCAL_NET_ID = 99;
+  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;
+  const int FIREWALL_WHITELIST = 0;
+  const int FIREWALL_BLACKLIST = 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 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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..18631ff
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,14 @@
+package android.net;
+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/1/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..93407dc
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,8 @@
+package android.net;
+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/1/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..d1782bb
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,8 @@
+package android.net;
+parcelable TetherStatsParcel {
+  @utf8InCpp String iface;
+  long rxBytes;
+  long rxPackets;
+  long txBytes;
+  long txPackets;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..d3bc7ed
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/1/android/net/UidRangeParcel.aidl
@@ -0,0 +1,5 @@
+package android.net;
+parcelable UidRangeParcel {
+  int start;
+  int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/10/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/10/.hash
new file mode 100644
index 0000000..6ec3613
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/.hash
@@ -0,0 +1 @@
+3943383e838f39851675e3640fcdf27b42f8c9fc
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/INetd.aidl
new file mode 100644
index 0000000..e671fdb
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/INetd.aidl
@@ -0,0 +1,222 @@
+/**
+ * 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();
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  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 = -559038041;
+  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;
+  /**
+   * @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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/10/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/10/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/10/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/10/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..06c8979
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/NativeNetworkType.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/10/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/10/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/10/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/10/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/10/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/10/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/10/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/10/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/11/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/11/.hash
new file mode 100644
index 0000000..d22cb04
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/.hash
@@ -0,0 +1 @@
+d384a1d9f2f6dc0301a43d2b9d6d3a49e3898ae7
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/INetd.aidl
new file mode 100644
index 0000000..e671fdb
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/INetd.aidl
@@ -0,0 +1,222 @@
+/**
+ * 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();
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  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 = -559038041;
+  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;
+  /**
+   * @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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/11/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/11/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/11/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/12/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/12/.hash
new file mode 100644
index 0000000..7a4d53c
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/.hash
@@ -0,0 +1 @@
+5a3a5f46fe31c118889d7a92be4faafb3df651ff
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/INetd.aidl
new file mode 100644
index 0000000..763b92a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/INetd.aidl
@@ -0,0 +1,225 @@
+/**
+ * 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();
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  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 = -559038041;
+  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;
+  /**
+   * @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/12/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/12/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/12/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/12/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/13/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/13/.hash
new file mode 100644
index 0000000..d3c72cf
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/.hash
@@ -0,0 +1 @@
+38614f80a23b92603d4851177e57c460aec1b606
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/INetd.aidl
new file mode 100644
index 0000000..3507784
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/INetd.aidl
@@ -0,0 +1,226 @@
+/**
+ * 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();
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  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);
+  /**
+   * @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/13/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/13/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/13/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/13/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/14/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/14/.hash
new file mode 100644
index 0000000..0bf7bde
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/.hash
@@ -0,0 +1 @@
+50bce96bc8d5811ed952950df30ec503f8a561ed
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl
new file mode 100644
index 0000000..8ccefb2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl
@@ -0,0 +1,259 @@
+/**
+ * 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);
+  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/14/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/14/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/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/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/2/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/2/.hash
new file mode 100644
index 0000000..5fc5b2d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/2/.hash
@@ -0,0 +1 @@
+e395d63302c47e7d2dac0d503045779029ff598b
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/INetd.aidl
new file mode 100644
index 0000000..0e2d5f4
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/INetd.aidl
@@ -0,0 +1,153 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a frozen snapshot of an AIDL interface (or parcelable). Do not
+// try to edit this file. It looks like you are doing that because you have
+// modified an AIDL interface in a backward-incompatible way, e.g., deleting a
+// function from an interface or a field from a parcelable and it broke the
+// build. That breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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;
+interface INetd {
+  boolean isAlive();
+  boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isWhitelist, in int[] uids);
+  boolean bandwidthEnableDataSaver(boolean enable);
+  void networkCreatePhysical(int netId, int permission);
+  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);
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  void trafficSwapActiveStatsMap();
+  IBinder getOemNetd();
+  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 LOCAL_NET_ID = 99;
+  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;
+  const int FIREWALL_WHITELIST = 0;
+  const int FIREWALL_BLACKLIST = 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 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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..621f1cf
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,31 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a frozen snapshot of an AIDL interface (or parcelable). Do not
+// try to edit this file. It looks like you are doing that because you have
+// modified an AIDL interface in a backward-incompatible way, e.g., deleting a
+// function from an interface or a field from a parcelable and it broke the
+// build. That breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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;
+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/2/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..18de61f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,25 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a frozen snapshot of an AIDL interface (or parcelable). Do not
+// try to edit this file. It looks like you are doing that because you have
+// modified an AIDL interface in a backward-incompatible way, e.g., deleting a
+// function from an interface or a field from a parcelable and it broke the
+// build. That breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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 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/2/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..c0ba676
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,25 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a frozen snapshot of an AIDL interface (or parcelable). Do not
+// try to edit this file. It looks like you are doing that because you have
+// modified an AIDL interface in a backward-incompatible way, e.g., deleting a
+// function from an interface or a field from a parcelable and it broke the
+// build. That breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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 TetherStatsParcel {
+  @utf8InCpp String iface;
+  long rxBytes;
+  long rxPackets;
+  long txBytes;
+  long txPackets;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..c2c35db
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/2/android/net/UidRangeParcel.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a frozen snapshot of an AIDL interface (or parcelable). Do not
+// try to edit this file. It looks like you are doing that because you have
+// modified an AIDL interface in a backward-incompatible way, e.g., deleting a
+// function from an interface or a field from a parcelable and it broke the
+// build. That breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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 UidRangeParcel {
+  int start;
+  int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/3/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/3/.hash
new file mode 100644
index 0000000..59cf708
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/3/.hash
@@ -0,0 +1 @@
+e17c1f9b2068b539b22e3a4a447edea3c80aee4b
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/INetd.aidl
new file mode 100644
index 0000000..135b738
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/INetd.aidl
@@ -0,0 +1,161 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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();
+  boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isWhitelist, in int[] uids);
+  boolean bandwidthEnableDataSaver(boolean enable);
+  void networkCreatePhysical(int netId, int permission);
+  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);
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+  void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+  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 LOCAL_NET_ID = 99;
+  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;
+  const int FIREWALL_WHITELIST = 0;
+  const int FIREWALL_BLACKLIST = 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 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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..4459363
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,32 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/3/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..01e0f95
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/3/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..62be838
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/3/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5e0ee62
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/3/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..b136454
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/3/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..3abf0f8
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,27 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..71ffb9b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..84ff457
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/3/android/net/UidRangeParcel.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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 UidRangeParcel {
+  int start;
+  int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/4/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/4/.hash
new file mode 100644
index 0000000..0c3f810
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/4/.hash
@@ -0,0 +1 @@
+63adaa5098e4d8621e90c5a84f7cb93505c79311
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/INetd.aidl
new file mode 100644
index 0000000..47e2931
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/INetd.aidl
@@ -0,0 +1,164 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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();
+  boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isWhitelist, in int[] uids);
+  boolean bandwidthEnableDataSaver(boolean enable);
+  void networkCreatePhysical(int netId, int permission);
+  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);
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+  void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+  android.net.TetherStatsParcel[] tetherOffloadGetStats();
+  void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+  android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+  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 LOCAL_NET_ID = 99;
+  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;
+  const int FIREWALL_WHITELIST = 0;
+  const int FIREWALL_BLACKLIST = 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 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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..4459363
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,32 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/4/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..01e0f95
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/4/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..62be838
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/4/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5e0ee62
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/4/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..b136454
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/4/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..c9d8458
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,28 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/4/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..0b0960e
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,27 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/4/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..84ff457
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/4/android/net/UidRangeParcel.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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 UidRangeParcel {
+  int start;
+  int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/5/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/5/.hash
new file mode 100644
index 0000000..a6ced45
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/5/.hash
@@ -0,0 +1 @@
+d97c56dd789cee9eeb5cdcec43a99df0a01873a5
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/INetd.aidl
new file mode 100644
index 0000000..b30748a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/INetd.aidl
@@ -0,0 +1,167 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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();
+  boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
+  boolean bandwidthEnableDataSaver(boolean enable);
+  void networkCreatePhysical(int netId, int permission);
+  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);
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+  void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+  android.net.TetherStatsParcel[] tetherOffloadGetStats();
+  void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+  android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+  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 LOCAL_NET_ID = 99;
+  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;
+  const @JavaPassthrough(annotation="@Deprecated") int FIREWALL_WHITELIST = 0;
+  const int FIREWALL_ALLOWLIST = 0;
+  const @JavaPassthrough(annotation="@Deprecated") 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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..4459363
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,32 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/5/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..01e0f95
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/5/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..62be838
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/5/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5e0ee62
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/5/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..b136454
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/5/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..c9d8458
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,28 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/5/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..0b0960e
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,27 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/5/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..debc6be
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/5/android/net/UidRangeParcel.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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/6/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/6/.hash
new file mode 100644
index 0000000..f5acf5d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/.hash
@@ -0,0 +1 @@
+b08451d9673b09cba84f1fd8740e1fdac64ff7be
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/INetd.aidl
new file mode 100644
index 0000000..a7952f2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/INetd.aidl
@@ -0,0 +1,198 @@
+/**
+ * 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();
+  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);
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+  void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+  android.net.TetherStatsParcel[] tetherOffloadGetStats();
+  void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+  android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+  void networkCreate(in android.net.NativeNetworkConfig config);
+  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 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;
+  /**
+   * @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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/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/6/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/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/6/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/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/6/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..76562b2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// 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;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..06c8979
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/NativeNetworkType.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/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/6/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/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/6/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/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/6/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/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/6/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/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/6/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/6/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/6/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/7/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/7/.hash
new file mode 100644
index 0000000..cad59df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/.hash
@@ -0,0 +1 @@
+850353de5d19a0dd718f8fd20791f0532e6a34c7
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/INetd.aidl
new file mode 100644
index 0000000..ec03d86
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/INetd.aidl
@@ -0,0 +1,200 @@
+/**
+ * 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();
+  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);
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+  void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+  android.net.TetherStatsParcel[] tetherOffloadGetStats();
+  void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+  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);
+  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 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;
+  /**
+   * @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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/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/7/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/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/7/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/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/7/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..76562b2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// 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;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..06c8979
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/NativeNetworkType.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/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/7/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/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/7/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/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/7/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/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/7/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/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/7/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/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/7/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/7/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/7/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/8/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/8/.hash
new file mode 100644
index 0000000..0933816
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/.hash
@@ -0,0 +1 @@
+e8cf8586fc5da9063818d8775e9a21c4b0addb5b
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetd.aidl
new file mode 100644
index 0000000..ec03d86
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetd.aidl
@@ -0,0 +1,200 @@
+/**
+ * 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();
+  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);
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+  void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+  android.net.TetherStatsParcel[] tetherOffloadGetStats();
+  void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+  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);
+  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 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;
+  /**
+   * @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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/8/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/8/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/8/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/8/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..06c8979
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeNetworkType.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/8/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/8/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/8/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/8/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/8/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/8/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/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/9/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/9/.hash
new file mode 100644
index 0000000..94dd240
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/.hash
@@ -0,0 +1 @@
+2bffe06ea8c13f35f90b86d6dfd1a2b4c4d4daf5
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/INetd.aidl
new file mode 100644
index 0000000..c780a59
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/INetd.aidl
@@ -0,0 +1,221 @@
+/**
+ * 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();
+  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);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  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);
+  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);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  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);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  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);
+  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 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;
+  /**
+   * @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";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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/9/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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/9/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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/9/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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/9/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..06c8979
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/NativeNetworkType.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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/9/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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/9/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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/9/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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/9/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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/9/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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/9/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/9/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/9/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
new file mode 100644
index 0000000..80b3b62
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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/current/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/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_event_listener_interface/1/.hash b/staticlibs/netd/aidl_api/netd_event_listener_interface/1/.hash
new file mode 100644
index 0000000..f39f730
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_event_listener_interface/1/.hash
@@ -0,0 +1 @@
+8e27594d285ca7c567d87e8cf74766c27647e02b
diff --git a/staticlibs/netd/aidl_api/netd_event_listener_interface/1/android/net/metrics/INetdEventListener.aidl b/staticlibs/netd/aidl_api/netd_event_listener_interface/1/android/net/metrics/INetdEventListener.aidl
new file mode 100644
index 0000000..9898a67
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_event_listener_interface/1/android/net/metrics/INetdEventListener.aidl
@@ -0,0 +1,34 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a frozen snapshot of an AIDL interface (or parcelable). Do not
+// try to edit this file. It looks like you are doing that because you have
+// modified an AIDL interface in a backward-incompatible way, e.g., deleting a
+// function from an interface or a field from a parcelable and it broke the
+// build. That breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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.metrics;
+interface INetdEventListener {
+  oneway void onDnsEvent(int netId, int eventType, int returnCode, int latencyMs, @utf8InCpp String hostname, in @utf8InCpp String[] ipAddresses, int ipAddressesCount, int uid);
+  oneway void onPrivateDnsValidationEvent(int netId, String ipAddress, String hostname, boolean validated);
+  oneway void onConnectEvent(int netId, int error, int latencyMs, String ipAddr, int port, int uid);
+  oneway void onWakeupEvent(String prefix, int uid, int ethertype, int ipNextHeader, in byte[] dstHw, String srcIp, String dstIp, int srcPort, int dstPort, long timestampNs);
+  oneway void onTcpSocketStatsEvent(in int[] networkIds, in int[] sentPackets, in int[] lostPackets, in int[] rttUs, in int[] sentAckDiffMs);
+  oneway void onNat64PrefixEvent(int netId, boolean added, @utf8InCpp String prefixString, int prefixLength);
+  const int EVENT_GETADDRINFO = 1;
+  const int EVENT_GETHOSTBYNAME = 2;
+  const int EVENT_GETHOSTBYADDR = 3;
+  const int EVENT_RES_NSEND = 4;
+  const int REPORTING_LEVEL_NONE = 0;
+  const int REPORTING_LEVEL_METRICS = 1;
+  const int REPORTING_LEVEL_FULL = 2;
+  const int DNS_REPORTED_IP_ADDRESSES_LIMIT = 10;
+}
diff --git a/staticlibs/netd/aidl_api/netd_event_listener_interface/2/.hash b/staticlibs/netd/aidl_api/netd_event_listener_interface/2/.hash
new file mode 100644
index 0000000..67c55b7
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_event_listener_interface/2/.hash
@@ -0,0 +1 @@
+1b765b02815e970a124de92e793e42e0ceff5384
diff --git a/staticlibs/netd/aidl_api/netd_event_listener_interface/2/android/net/metrics/INetdEventListener.aidl b/staticlibs/netd/aidl_api/netd_event_listener_interface/2/android/net/metrics/INetdEventListener.aidl
new file mode 100644
index 0000000..1b0fe13
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_event_listener_interface/2/android/net/metrics/INetdEventListener.aidl
@@ -0,0 +1,51 @@
+/**
+ * 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.metrics;
+/* @hide */
+interface INetdEventListener {
+  oneway void onDnsEvent(int netId, int eventType, int returnCode, int latencyMs, @utf8InCpp String hostname, in @utf8InCpp String[] ipAddresses, int ipAddressesCount, int uid);
+  oneway void onPrivateDnsValidationEvent(int netId, String ipAddress, String hostname, boolean validated);
+  oneway void onConnectEvent(int netId, int error, int latencyMs, String ipAddr, int port, int uid);
+  oneway void onWakeupEvent(String prefix, int uid, int ethertype, int ipNextHeader, in byte[] dstHw, String srcIp, String dstIp, int srcPort, int dstPort, long timestampNs);
+  oneway void onTcpSocketStatsEvent(in int[] networkIds, in int[] sentPackets, in int[] lostPackets, in int[] rttUs, in int[] sentAckDiffMs);
+  oneway void onNat64PrefixEvent(int netId, boolean added, @utf8InCpp String prefixString, int prefixLength);
+  const int EVENT_GETADDRINFO = 1;
+  const int EVENT_GETHOSTBYNAME = 2;
+  const int EVENT_GETHOSTBYADDR = 3;
+  const int EVENT_RES_NSEND = 4;
+  const int REPORTING_LEVEL_NONE = 0;
+  const int REPORTING_LEVEL_METRICS = 1;
+  const int REPORTING_LEVEL_FULL = 2;
+  const int DNS_REPORTED_IP_ADDRESSES_LIMIT = 10;
+}
diff --git a/staticlibs/netd/aidl_api/netd_event_listener_interface/current/android/net/metrics/INetdEventListener.aidl b/staticlibs/netd/aidl_api/netd_event_listener_interface/current/android/net/metrics/INetdEventListener.aidl
new file mode 100644
index 0000000..d71c3f2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_event_listener_interface/current/android/net/metrics/INetdEventListener.aidl
@@ -0,0 +1,35 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files 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.metrics;
+/* @hide */
+interface INetdEventListener {
+  oneway void onDnsEvent(int netId, int eventType, int returnCode, int latencyMs, @utf8InCpp String hostname, in @utf8InCpp String[] ipAddresses, int ipAddressesCount, int uid);
+  oneway void onPrivateDnsValidationEvent(int netId, String ipAddress, String hostname, boolean validated);
+  oneway void onConnectEvent(int netId, int error, int latencyMs, String ipAddr, int port, int uid);
+  oneway void onWakeupEvent(String prefix, int uid, int ethertype, int ipNextHeader, in byte[] dstHw, String srcIp, String dstIp, int srcPort, int dstPort, long timestampNs);
+  oneway void onTcpSocketStatsEvent(in int[] networkIds, in int[] sentPackets, in int[] lostPackets, in int[] rttUs, in int[] sentAckDiffMs);
+  oneway void onNat64PrefixEvent(int netId, boolean added, @utf8InCpp String prefixString, int prefixLength);
+  const int EVENT_GETADDRINFO = 1;
+  const int EVENT_GETHOSTBYNAME = 2;
+  const int EVENT_GETHOSTBYADDR = 3;
+  const int EVENT_RES_NSEND = 4;
+  const int REPORTING_LEVEL_NONE = 0;
+  const int REPORTING_LEVEL_METRICS = 1;
+  const int REPORTING_LEVEL_FULL = 2;
+  const int DNS_REPORTED_IP_ADDRESSES_LIMIT = 10;
+}
diff --git a/staticlibs/netd/binder/android/net/INetd.aidl b/staticlibs/netd/binder/android/net/INetd.aidl
new file mode 100644
index 0000000..e4c63b9
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/INetd.aidl
@@ -0,0 +1,1472 @@
+/**
+ * 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 android.net;
+
+import android.net.INetdUnsolicitedEventListener;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpSecMigrateInfoParcel;
+import android.net.MarkMaskParcel;
+import android.net.NativeNetworkConfig;
+import android.net.RouteInfoParcel;
+import android.net.TetherConfigParcel;
+import android.net.TetherOffloadRuleParcel;
+import android.net.TetherStatsParcel;
+import android.net.UidRangeParcel;
+import android.net.netd.aidl.NativeUidRangeConfig;
+
+/** {@hide} */
+interface INetd {
+    /**
+     * Returns true if the service is responding.
+     */
+    boolean isAlive();
+
+    /**
+     * Replaces the contents of the specified UID-based firewall chain.
+     *
+     * The chain may be an allowlist chain or a denylist chain. A denylist chain contains DROP
+     * rules for the specified UIDs and a RETURN rule at the end. An allowlist chain contains RETURN
+     * rules for the system UID range (0 to {@code UID_APP} - 1), RETURN rules for for the specified
+     * UIDs, and a DROP rule at the end. The chain will be created if it does not exist.
+     *
+     * @param chainName The name of the chain to replace.
+     * @param isAllowlist Whether this is an allowlist or denylist chain.
+     * @param uids The list of UIDs to allow/deny.
+     * @return true if the chain was successfully replaced, false otherwise.
+     * @deprecated unimplemented on T+.
+     */
+    boolean firewallReplaceUidChain(in @utf8InCpp String chainName,
+                                    boolean isAllowlist,
+                                    in int[] uids);
+
+    /**
+     * Enables or disables data saver mode on costly network interfaces.
+     *
+     * - When disabled, all packets to/from apps in the penalty box chain are rejected on costly
+     *   interfaces. Traffic to/from other apps or on other network interfaces is allowed.
+     * - When enabled, only apps that are in the happy box chain and not in the penalty box chain
+     *   are allowed network connectivity on costly interfaces. All other packets on these
+     *   interfaces are rejected. The happy box chain always contains all system UIDs; to disallow
+     *   traffic from system UIDs, place them in the penalty box chain.
+     *
+     * By default, data saver mode is disabled. This command has no effect but might still return an
+     * error) if {@code enable} is the same as the current value.
+     *
+     * @param enable whether to enable or disable data saver mode.
+     * @return true if the if the operation was successful, false otherwise.
+     */
+    boolean bandwidthEnableDataSaver(boolean enable);
+
+    /**
+     * Creates a physical network (i.e., one containing physical interfaces.
+     * @deprecated use networkCreate() instead.
+     *
+     * @param netId the networkId to create.
+     * @param permission the permission necessary to use the network. Must be one of
+     *         PERMISSION_NONE/PERMISSION_NETWORK/PERMISSION_SYSTEM.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkCreatePhysical(int netId, int permission);
+
+    /**
+     * Creates a VPN network.
+     * @deprecated use networkCreate() instead.
+     *
+     * @param netId the network to create.
+     * @param secure whether unprivileged apps are allowed to bypass the VPN.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkCreateVpn(int netId, boolean secure);
+
+    /**
+     * Destroys a network. Any interfaces added to the network are removed, and the network ceases
+     * to be the default network.
+     *
+     * @param netId the network to destroy.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkDestroy(int netId);
+
+    /**
+     * Adds an interface to a network. The interface must not be assigned to any network, including
+     * the specified network.
+     *
+     * @param netId the network to add the interface to.
+     * @param interface the name of the interface to add.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkAddInterface(int netId, in @utf8InCpp String iface);
+
+    /**
+     * Adds an interface to a network. The interface must be assigned to the specified network.
+     *
+     * @param netId the network to remove the interface from.
+     * @param interface the name of the interface to remove.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkRemoveInterface(int netId, in @utf8InCpp String iface);
+
+    /**
+     * Adds the specified UID ranges to the specified network. The network can be physical or
+     * virtual. Traffic from the UID ranges will be routed to the network by default.
+     *
+     * @param netId the network ID of the network to add the ranges to.
+     * @param uidRanges a set of non-overlapping ranges of UIDs to add. These exact ranges
+     *        must not overlap with existing ranges assigned to this network.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkAddUidRanges(int netId, in UidRangeParcel[] uidRanges);
+
+    /**
+     * Remove the specified UID ranges from the specified network. The network can be physical or
+     * virtual. Traffic from the UID ranges will no longer be routed to the network by default.
+     *
+     * @param netId the network ID of the network to remove the ranges from.
+     * @param uidRanges a set of non-overlapping ranges of UIDs to remove. These exact ranges
+     *        must already be assigned to this network.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkRemoveUidRanges(int netId, in UidRangeParcel[] uidRanges);
+
+    /**
+     * Adds or removes one rule for each supplied UID range to prohibit all network activity outside
+     * of secure VPN.
+     *
+     * When a UID is covered by one of these rules, traffic sent through any socket that is not
+     * protected or explicitly overriden by the system will be rejected. The kernel will respond
+     * with an ICMP prohibit message.
+     *
+     * Initially, there are no such rules. Any rules that are added will only last until the next
+     * restart of netd or the device.
+     *
+     * @param add {@code true} if the specified UID ranges should be denied access to any network
+     *        which is not secure VPN by adding rules, {@code false} to remove existing rules.
+     * @param uidRanges a set of non-overlapping, contiguous ranges of UIDs to which to apply or
+     *        remove this restriction.
+     *        <p> Added rules should not overlap with existing rules. Likewise, removed rules should
+     *        each correspond to an existing rule.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkRejectNonSecureVpn(boolean add, in UidRangeParcel[] uidRanges);
+
+    /**
+     * Administratively closes sockets belonging to the specified UIDs.
+     */
+    void socketDestroy(in UidRangeParcel[] uidRanges, in int[] exemptUids);
+
+    /**
+     * Instruct the tethering DNS server to reevaluated serving interfaces.
+     * This is needed to for the DNS server to observe changes in the set
+     * of potential listening IP addresses. (Listening on wildcard addresses
+     * can turn the device into an open resolver; b/7530468)
+     *
+     * TODO: Return something richer than just a boolean.
+     */
+    boolean tetherApplyDnsInterfaces();
+
+    /**
+     * Return tethering statistics.
+     *
+     * @return an array of TetherStatsParcel, where each entry contains the upstream interface
+     *         name and its tethering statistics since netd startup.
+     *         There will only ever be one entry for a given interface.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    TetherStatsParcel[] tetherGetStats();
+
+    /**
+     * Add/Remove and IP address from an interface.
+     *
+     * @param ifName the interface name
+     * @param addrString the IP address to add/remove as a string literal
+     * @param prefixLength the prefix length associated with this IP address
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void interfaceAddAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString,
+            int prefixLength);
+    void interfaceDelAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString,
+            int prefixLength);
+
+    /**
+     * Set and get /proc/sys/net interface configuration parameters.
+     *
+     * @param ipversion One of IPV4/IPV6 integers, indicating the desired IP version directory.
+     * @param which One of CONF/NEIGH integers, indicating the desired parameter category directory.
+     * @param ifname The interface name portion of the path; may also be "all" or "default".
+     * @param parameter The parameter name portion of the path.
+     * @param value The value string to be written into the assembled path.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+
+    const int IPV4  = 4;
+    const int IPV6  = 6;
+    const int CONF  = 1;
+    const int NEIGH = 2;
+    @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);
+
+   /**
+    * Sets owner of socket ParcelFileDescriptor to the new UID, checking to ensure that the caller's
+    * uid is that of the old owner's, and that this is a UDP-encap socket
+    *
+    * @param ParcelFileDescriptor socket Socket file descriptor
+    * @param int newUid UID of the new socket fd owner
+    */
+    void ipSecSetEncapSocketOwner(in ParcelFileDescriptor socket, int newUid);
+
+   /**
+    * Reserve an SPI from the kernel
+    *
+    * @param transformId a unique identifier for allocated resources
+    * @param sourceAddress InetAddress as string for the sending endpoint
+    * @param destinationAddress InetAddress as string for the receiving endpoint
+    * @param spi a requested 32-bit unique ID or 0 to request random allocation
+    * @return the SPI that was allocated or 0 if failed
+    */
+    int ipSecAllocateSpi(
+            int transformId,
+            in @utf8InCpp String sourceAddress,
+            in @utf8InCpp String destinationAddress,
+            int spi);
+
+   /**
+    * Update an IPsec SA (xfrm_state) describing how ip(v6) traffic will be encrypted
+    * or decrypted.
+    *
+    * @param transformId a unique identifier for allocated resources
+    * @param mode either Transport or Tunnel mode
+    * @param sourceAddress InetAddress as string for the sending endpoint
+    * @param destinationAddress InetAddress as string for the receiving endpoint
+    * @param underlyingNetId the netId of the network to which the SA is applied. Only accepted for
+    *        tunnel mode SAs.
+    * @param spi a 32-bit unique ID allocated to the user
+    * @param markValue a 32-bit unique ID chosen by the user
+    * @param markMask a 32-bit mask chosen by the user
+    * @param authAlgo a string identifying the authentication algorithm to be used
+    * @param authKey a byte array containing the authentication key
+    * @param authTruncBits the truncation length of the MAC produced by the authentication algorithm
+    * @param cryptAlgo a string identifying the encryption algorithm to be used
+    * @param cryptKey a byte arrray containing the encryption key
+    * @param cryptTruncBits unused parameter
+    * @param aeadAlgo a string identifying the authenticated encryption algorithm to be used
+    * @param aeadKey a byte arrray containing the key to be used in authenticated encryption
+    * @param aeadIcvBits the truncation length of the ICV produced by the authentication algorithm
+    *        (similar to authTruncBits in function)
+    * @param encapType encapsulation type used (if any) for the udp encap socket
+    * @param encapLocalPort the port number on the host to be used in encap packets
+    * @param encapRemotePort the port number of the remote to be used for encap packets
+    * @param interfaceId the identifier for the IPsec tunnel interface.
+    *        Only accepted for tunnel mode SAs.
+    */
+    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);
+
+   /**
+    * Delete a previously created security association identified by the provided parameters
+    *
+    * @param transformId a unique identifier for allocated resources
+    * @param sourceAddress InetAddress as string for the sending endpoint
+    * @param destinationAddress InetAddress as string for the receiving endpoint
+    * @param spi a requested 32-bit unique ID allocated to the user
+    * @param markValue a 32-bit unique ID chosen by the user
+    * @param markMask a 32-bit mask chosen by the user
+    * @param interfaceId the identifier for the IPsec tunnel interface.
+    */
+    void ipSecDeleteSecurityAssociation(
+            int transformId,
+            in @utf8InCpp String sourceAddress,
+            in @utf8InCpp String destinationAddress,
+            int spi,
+            int markValue,
+            int markMask,
+            int interfaceId);
+
+   /**
+    * Apply a previously created SA to a specified socket, starting IPsec on that socket
+    *
+    * @param socket a user-provided socket that will have IPsec applied
+    * @param transformId a unique identifier for allocated resources
+    * @param direction DIRECTION_IN or DIRECTION_OUT
+    * @param sourceAddress InetAddress as string for the sending endpoint
+    * @param destinationAddress InetAddress as string for the receiving endpoint
+    * @param spi a 32-bit unique ID allocated to the user (socket owner)
+    */
+    void ipSecApplyTransportModeTransform(
+            in ParcelFileDescriptor socket,
+            int transformId,
+            int direction,
+            in @utf8InCpp String sourceAddress,
+            in @utf8InCpp String destinationAddress,
+            int spi);
+
+   /**
+    * Remove an IPsec SA from a given socket. This will allow unencrypted traffic to flow
+    * on that socket if a transform had been previously applied.
+    *
+    * @param socket a user-provided socket from which to remove any IPsec configuration
+    */
+    void ipSecRemoveTransportModeTransform(
+            in ParcelFileDescriptor socket);
+
+   /**
+    * Adds an IPsec global policy.
+    *
+    * @param transformId a unique identifier for allocated resources
+    * @param selAddrFamily the address family identifier for the selector
+    * @param direction DIRECTION_IN or DIRECTION_OUT
+    * @param tmplSrcAddress InetAddress as string for the sending endpoint
+    * @param tmplDstAddress InetAddress as string for the receiving endpoint
+    * @param spi a 32-bit unique ID allocated to the user
+    * @param markValue a 32-bit unique ID chosen by the user
+    * @param markMask a 32-bit mask chosen by the user
+    * @param interfaceId the identifier for the IPsec tunnel interface.
+    */
+    void ipSecAddSecurityPolicy(
+            int transformId,
+            int selAddrFamily,
+            int direction,
+            in @utf8InCpp String tmplSrcAddress,
+            in @utf8InCpp String tmplDstAddress,
+            int spi,
+            int markValue,
+            int markMask,
+            int interfaceId);
+
+   /**
+    * Updates an IPsec global policy.
+    *
+    * @param transformId a unique identifier for allocated resources
+    * @param selAddrFamily the address family identifier for the selector
+    * @param direction DIRECTION_IN or DIRECTION_OUT
+    * @param tmplSrcAddress InetAddress as string for the sending endpoint
+    * @param tmplDstAddress InetAddress as string for the receiving endpoint
+    * @param spi a 32-bit unique ID allocated to the user
+    * @param markValue a 32-bit unique ID chosen by the user
+    * @param markMask a 32-bit mask chosen by the user
+    * @param interfaceId the identifier for the IPsec tunnel interface.
+    */
+    void ipSecUpdateSecurityPolicy(
+            int transformId,
+            int selAddrFamily,
+            int direction,
+            in @utf8InCpp String tmplSrcAddress,
+            in @utf8InCpp String tmplDstAddress,
+            int spi,
+            int markValue,
+            int markMask,
+            int interfaceId);
+
+   /**
+    * Deletes an IPsec global policy.
+    *
+    * Deletion of global policies does not do any matching based on the templates, thus
+    * template source/destination addresses are not needed (as opposed to add/update).
+    *
+    * @param transformId a unique identifier for allocated resources
+    * @param selAddrFamily the address family identifier for the selector
+    * @param direction DIRECTION_IN or DIRECTION_OUT
+    * @param markValue a 32-bit unique ID chosen by the user
+    * @param markMask a 32-bit mask chosen by the user
+    * @param interfaceId the identifier for the IPsec tunnel interface.
+    */
+    void ipSecDeleteSecurityPolicy(
+            int transformId,
+            int selAddrFamily,
+            int direction,
+            int markValue,
+            int markMask,
+            int interfaceId);
+
+    // This could not be declared as @uft8InCpp; thus, when used in native code it must be
+    // converted from a UTF-16 string to an ASCII string.
+    const String IPSEC_INTERFACE_PREFIX = "ipsec";
+
+   /**
+    * Add a IPsec Tunnel Interface.
+    *
+    * @param devName a unique identifier that represents the name of the device
+    * @param localAddress InetAddress as string for the local endpoint
+    * @param remoteAddress InetAddress as string for the remote endpoint
+    * @param iKey, to match Policies and SAs for input packets.
+    * @param oKey, to match Policies and SAs for output packets.
+    * @param interfaceId the identifier for the IPsec tunnel interface.
+    */
+    void ipSecAddTunnelInterface(
+            in @utf8InCpp String deviceName,
+            in @utf8InCpp String localAddress,
+            in @utf8InCpp String remoteAddress,
+            int iKey,
+            int oKey,
+            int interfaceId);
+
+   /**
+    * Update a IPsec Tunnel Interface.
+    *
+    * @param devName a unique identifier that represents the name of the device
+    * @param localAddress InetAddress as string for the local endpoint
+    * @param remoteAddress InetAddress as string for the remote endpoint
+    * @param iKey, to match Policies and SAs for input packets.
+    * @param oKey, to match Policies and SAs for output packets.
+    * @param interfaceId the identifier for the IPsec tunnel interface.
+    */
+    void ipSecUpdateTunnelInterface(
+            in @utf8InCpp String deviceName,
+            in @utf8InCpp String localAddress,
+            in @utf8InCpp String remoteAddress,
+            int iKey,
+            int oKey,
+            int interfaceId);
+
+   /**
+    * Removes a IPsec Tunnel Interface.
+    *
+    * @param devName a unique identifier that represents the name of the device
+    */
+    void ipSecRemoveTunnelInterface(in @utf8InCpp String deviceName);
+
+   /**
+    * Request notification of wakeup packets arriving on an interface. Notifications will be
+    * delivered to INetdEventListener.onWakeupEvent().
+    *
+    * @param ifName the interface
+    * @param prefix arbitrary string used to identify wakeup sources in onWakeupEvent
+    */
+    void wakeupAddInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+
+   /**
+    * Stop notification of wakeup packets arriving on an interface.
+    *
+    * @param ifName the interface
+    * @param prefix arbitrary string used to identify wakeup sources in onWakeupEvent
+    */
+    void wakeupDelInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+
+    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;
+   /**
+    * Set IPv6 address generation mode. IPv6 should be disabled before changing mode.
+    *
+    * @param mode SLAAC address generation mechanism to use
+    */
+    void setIPv6AddrGenMode(in @utf8InCpp String ifName, int mode);
+
+   /**
+    * Add idletimer for specific interface
+    *
+    * @param ifName Name of target interface
+    * @param timeout The time in seconds that will trigger idletimer
+    * @param classLabel The unique identifier for this idletimer
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void idletimerAddInterface(
+            in @utf8InCpp String ifName,
+            int timeout,
+            in @utf8InCpp String classLabel);
+
+   /**
+    * Remove idletimer for specific interface
+    *
+    * @param ifName Name of target interface
+    * @param timeout The time in seconds that will trigger idletimer
+    * @param classLabel The unique identifier for this idletimer
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void idletimerRemoveInterface(
+            in @utf8InCpp String ifName,
+            int timeout,
+            in @utf8InCpp String classLabel);
+
+    const int PENALTY_POLICY_ACCEPT = 1;
+    const int PENALTY_POLICY_LOG = 2;
+    const int PENALTY_POLICY_REJECT = 3;
+
+   /**
+    * Offers to detect sockets sending data not wrapped inside a layer of SSL/TLS encryption.
+    *
+    * @param uid Uid of the app
+    * @param policyPenalty The penalty policy of the app
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void strictUidCleartextPenalty(int uid, int policyPenalty);
+
+   /**
+    * Start clatd
+    *
+    * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd
+    *             control plane moved to the mainline module starting in T. See ClatCoordinator.
+    * @param ifName interface name to start clatd
+    * @param nat64Prefix the NAT64 prefix, e.g., "2001:db8:64::/96".
+    * @return a string, the IPv6 address that will be used for 464xlat.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+
+   /**
+    * Stop clatd
+    *
+    * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd
+    *             control plane moved to the mainline module starting in T. See ClatCoordinator.
+    * @param ifName interface name to stop clatd
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void clatdStop(in @utf8InCpp String ifName);
+
+    /**
+     * Packet mark that identifies non-offloaded ingress clat packets.
+     */
+    const int CLAT_MARK = 0xdeadc1a7;
+
+   /**
+    * Get status of IP forwarding
+    *
+    * @return true if IP forwarding is enabled, false otherwise.
+    */
+    boolean ipfwdEnabled();
+
+   /**
+    * Get requester list of IP forwarding
+    *
+    * @return An array of strings containing requester list of IP forwarding
+    */
+    @utf8InCpp String[] ipfwdGetRequesterList();
+
+   /**
+    * Enable IP forwarding for specific requester
+    *
+    * @param requester requester name to enable IP forwarding. It is a unique name which will be
+    *                  stored in Netd to make sure if any requester needs IP forwarding.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void ipfwdEnableForwarding(in @utf8InCpp String requester);
+
+   /**
+    * Disable IP forwarding for specific requester
+    *
+    * @param requester requester name to disable IP forwarding. This name should match the
+    *                  names which are set by ipfwdEnableForwarding.
+    *                  IP forwarding would be disabled if it is the last requester.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void ipfwdDisableForwarding(in @utf8InCpp String requester);
+
+   /**
+    * Add forwarding ip rule
+    *
+    * @param fromIface interface name to add forwarding ip rule
+    * @param toIface interface name to add forwarding ip rule
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void ipfwdAddInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+
+   /**
+    * Remove forwarding ip rule
+    *
+    * @param fromIface interface name to remove forwarding ip rule
+    * @param toIface interface name to remove forwarding ip rule
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void ipfwdRemoveInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+
+   /**
+    * Set quota for interface
+    *
+    * @param ifName Name of target interface
+    * @param bytes Quota value in bytes
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void bandwidthSetInterfaceQuota(in @utf8InCpp String ifName, long bytes);
+
+   /**
+    * Remove quota for interface
+    *
+    * @param ifName Name of target interface
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void bandwidthRemoveInterfaceQuota(in @utf8InCpp String ifName);
+
+   /**
+    * Set alert for interface
+    *
+    * @param ifName Name of target interface
+    * @param bytes Alert value in bytes
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
+
+   /**
+    * Remove alert for interface
+    *
+    * @param ifName Name of target interface
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
+
+   /**
+    * Set global alert
+    *
+    * @param bytes Alert value in bytes
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void bandwidthSetGlobalAlert(long bytes);
+
+   /**
+    * 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.
+    * @deprecated unimplemented on T+.
+    */
+    void bandwidthAddNaughtyApp(int uid);
+
+   /**
+    * 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.
+    * @deprecated unimplemented on T+.
+    */
+    void bandwidthRemoveNaughtyApp(int uid);
+
+   /**
+    * 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.
+    * @deprecated unimplemented on T+.
+    */
+    void bandwidthAddNiceApp(int uid);
+
+   /**
+    * 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.
+    * @deprecated unimplemented on T+.
+    */
+    void bandwidthRemoveNiceApp(int uid);
+
+   /**
+    * Start tethering
+    *
+    * @param dhcpRanges dhcp ranges to set.
+    *                   dhcpRanges might contain many addresss {addr1, addr2, aadr3, addr4...}
+    *                   Netd splits them into ranges: addr1-addr2, addr3-addr4, etc.
+    *                   An odd number of addrs will fail.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void tetherStart(in @utf8InCpp String[] dhcpRanges);
+
+   /**
+    * Stop tethering
+    *
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void tetherStop();
+
+   /**
+    * Get status of tethering
+    *
+    * @return true if tethering is enabled, false otherwise.
+    */
+    boolean tetherIsEnabled();
+
+   /**
+    * Setup interface for tethering
+    *
+    * @param ifName interface name to add
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void tetherInterfaceAdd(in @utf8InCpp String ifName);
+
+   /**
+    * Reset interface for tethering
+    *
+    * @param ifName interface name to remove
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void tetherInterfaceRemove(in @utf8InCpp String ifName);
+
+   /**
+    * Get the interface list which is stored in netd
+    * The list contains the interfaces managed by tetherInterfaceAdd/tetherInterfaceRemove
+    *
+    * @return An array of strings containing interface list result
+    */
+    @utf8InCpp String[] tetherInterfaceList();
+
+   /**
+    * Set DNS forwarder server
+    *
+    * @param netId the upstream network to forward DNS queries to
+    * @param dnsAddrs DNS server address to set
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void tetherDnsSet(int netId, in @utf8InCpp String[] dnsAddrs);
+
+   /**
+    * Return the DNS list set by tetherDnsSet
+    *
+    * @return An array of strings containing the list of DNS servers
+    */
+    @utf8InCpp String[] tetherDnsList();
+
+    const int LOCAL_NET_ID = 99;
+
+    /**
+     * Constant net ID for the "dummy" network.
+     *
+     * The dummy network is used to blackhole or reject traffic. Any attempt to use it will
+     * either drop the packets or fail with ENETUNREACH.
+     */
+    const int DUMMY_NET_ID = 51;
+
+    /**
+     * Constant net ID for the "unreachable" network.
+     *
+     * The unreachable network is used to reject traffic. Any attempt to use it will fail
+     * with ENETUNREACH.
+     */
+    const int UNREACHABLE_NET_ID = 52;
+
+    // Route does not specify a next hop
+    const String NEXTHOP_NONE = "";
+    // Route next hop is unreachable
+    const String NEXTHOP_UNREACHABLE = "unreachable";
+    // Route next hop is throw
+    const String NEXTHOP_THROW = "throw";
+
+   /**
+    * Add a route for specific network
+    *
+    * @param netId the network to add the route to
+    * @param ifName the name of interface of the route.
+    *               This interface should be assigned to the netID.
+    * @param destination the destination of the route
+    * @param nextHop The route's next hop address,
+    *                or it could be either NEXTHOP_NONE, NEXTHOP_UNREACHABLE, NEXTHOP_THROW.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void networkAddRoute(
+            int netId,
+            in @utf8InCpp String ifName,
+            in @utf8InCpp String destination,
+            in @utf8InCpp String nextHop);
+
+   /**
+    * Remove a route for specific network
+    *
+    * @param netId the network to remove the route from
+    * @param ifName the name of interface of the route.
+    *               This interface should be assigned to the netID.
+    * @param destination the destination of the route
+    * @param nextHop The route's next hop address,
+    *                or it could be either NEXTHOP_NONE, NEXTHOP_UNREACHABLE, NEXTHOP_THROW.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void networkRemoveRoute(
+            int netId,
+            in @utf8InCpp String ifName,
+            in @utf8InCpp String destination,
+            in @utf8InCpp String nextHop);
+
+   /**
+    * Add a route to legacy routing table for specific network
+    *
+    * @param netId the network to add the route to
+    * @param ifName the name of interface of the route.
+    *               This interface should be assigned to the netID.
+    * @param destination the destination of the route
+    * @param nextHop The route's next hop address,
+    *                or it could be either NEXTHOP_NONE, NEXTHOP_UNREACHABLE, NEXTHOP_THROW.
+    * @param uid uid of the user
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void networkAddLegacyRoute(
+            int netId,
+            in @utf8InCpp String ifName,
+            in @utf8InCpp String destination,
+            in @utf8InCpp String nextHop,
+            int uid);
+
+   /**
+    * Remove a route from legacy routing table for specific network
+    *
+    * @param netId the network to remove the route from
+    * @param ifName the name of interface of the route.
+    *               This interface should be assigned to the netID.
+    * @param destination the destination of the route
+    * @param nextHop The route's next hop address,
+    *                or it could be either NEXTHOP_NONE, NEXTHOP_UNREACHABLE, NEXTHOP_THROW.
+    * @param uid uid of the user
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void networkRemoveLegacyRoute(
+            int netId,
+            in @utf8InCpp String ifName,
+            in @utf8InCpp String destination,
+            in @utf8InCpp String nextHop,
+            int uid);
+
+   /**
+    * Get default network
+    *
+    * @return netId of default network
+    */
+    int networkGetDefault();
+
+   /**
+    * Set network as default network
+    *
+    * @param netId the network to set as the default
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void networkSetDefault(int netId);
+
+   /**
+    * Clear default network
+    *
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void networkClearDefault();
+
+   /**
+    * PERMISSION_NONE is used for regular networks and apps. TODO: use PERMISSION_INTERNET
+    * for this instead, and use PERMISSION_NONE to indicate no network permissions at all.
+    */
+    const int PERMISSION_NONE = 0;
+
+   /**
+    * PERMISSION_NETWORK represents the CHANGE_NETWORK_STATE permission.
+    */
+    const int PERMISSION_NETWORK = 1;
+
+   /**
+    * PERMISSION_SYSTEM represents the ability to use restricted networks. This is mostly
+    * equivalent to the CONNECTIVITY_USE_RESTRICTED_NETWORKS permission.
+    */
+    const int PERMISSION_SYSTEM = 2;
+
+   /**
+    * NO_PERMISSIONS indicates that this app is installed and doesn't have either
+    * PERMISSION_INTERNET or PERMISSION_UPDATE_DEVICE_STATS.
+    * TODO: use PERMISSION_NONE to represent this case
+    */
+    const int NO_PERMISSIONS = 0;
+
+   /**
+    * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets
+    */
+    const int PERMISSION_INTERNET = 4;
+
+   /**
+    * PERMISSION_UPDATE_DEVICE_STATS is used for system UIDs and privileged apps
+    * that have the UPDATE_DEVICE_STATS permission
+    */
+    const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+
+   /**
+    * PERMISSION_UNINSTALLED is used when an app is uninstalled from the device. All internet
+    * related permissions need to be cleaned
+    */
+    const int PERMISSION_UNINSTALLED = -1;
+
+
+   /**
+    * Sets the permission required to access a specific network.
+    *
+    * @param netId the network to set
+    * @param permission network permission to use
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void networkSetPermissionForNetwork(int netId, int permission);
+
+   /**
+    * Assigns network access permissions to the specified users.
+    *
+    * @param permission network permission to use
+    * @param uids uid of users to set permission
+    */
+    void networkSetPermissionForUser(int permission, in int[] uids);
+
+   /**
+    * Clears network access permissions for the specified users.
+    *
+    * @param uids uid of users to clear permission
+    */
+    void networkClearPermissionForUser(in int[] uids);
+
+   /**
+    * Assigns android.permission.INTERNET and/or android.permission.UPDATE_DEVICE_STATS to the uids
+    * specified. Or remove all permissions from the uids.
+    *
+    * @param permission The permission to grant, it could be either PERMISSION_INTERNET and/or
+    *                   PERMISSION_UPDATE_DEVICE_STATS. If the permission is NO_PERMISSIONS, then
+    *                   revoke all permissions for the uids.
+    * @param uids uid of users to grant permission
+    * @deprecated unimplemented on T+.
+    */
+    void trafficSetNetPermForUids(int permission, in int[] uids);
+
+   /**
+    * Gives the specified user permission to protect sockets from VPNs.
+    * Typically used by VPN apps themselves, to ensure that the sockets
+    * they use to communicate with the VPN server aren't routed through
+    * the VPN network.
+    *
+    * @param uid uid of user to set
+    */
+    void networkSetProtectAllow(int uid);
+
+   /**
+    * Removes the permission to protect sockets from VPN.
+    *
+    * @param uid uid of user to set
+    */
+    void networkSetProtectDeny(int uid);
+
+   /**
+    * Get the status of network protect for user
+    *
+    * @param uids uid of user
+    * @return true if the user can protect sockets from VPN, false otherwise.
+    */
+    boolean networkCanProtect(int uid);
+
+    /** Only allows packets from specific UID/Interface.
+        @deprecated use FIREWALL_ALLOWLIST. */
+    const int FIREWALL_WHITELIST = 0;
+
+    /** Only allows packets from specific UID/Interface. */
+    const int FIREWALL_ALLOWLIST = 0;
+
+    /** Blocks packets from specific UID/Interface.
+        @deprecated use FIREWALL_DENYLIST. */
+    const int FIREWALL_BLACKLIST = 1;
+
+    /** Blocks packets from specific UID/Interface. */
+    const int FIREWALL_DENYLIST = 1;
+
+   /**
+    * Set type of firewall
+    * Type allowlist only allows packets from specific UID/Interface
+    * Type denylist blocks packets from specific UID/Interface
+    *
+    * @param firewalltype type of firewall, either FIREWALL_ALLOWLIST or FIREWALL_DENYLIST
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void firewallSetFirewallType(int firewalltype);
+
+    // Specify allow Rule which allows packets
+    const int FIREWALL_RULE_ALLOW = 1;
+    // Specify deny Rule which drops packets
+    const int FIREWALL_RULE_DENY = 2;
+
+    // No specific chain is chosen, use general firewall chain(fw_input, fw_output)
+    const int FIREWALL_CHAIN_NONE = 0;
+    // Specify DOZABLE chain(fw_dozable) which is used in dozable mode
+    const int FIREWALL_CHAIN_DOZABLE = 1;
+    // Specify STANDBY chain(fw_standby) which is used in standby mode
+    const int FIREWALL_CHAIN_STANDBY = 2;
+    // Specify POWERSAVE chain(fw_powersave) which is used in power save mode
+    const int FIREWALL_CHAIN_POWERSAVE = 3;
+    // Specify RESTRICTED chain(fw_restricted) which is used in restricted
+    // networking mode
+    const int FIREWALL_CHAIN_RESTRICTED = 4;
+
+   /**
+    * Set firewall rule for interface
+    *
+    * @param ifName the interface to allow/deny
+    * @param firewallRule either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+
+   /**
+    * Set firewall rule for uid
+    *
+    * @param childChain target chain
+    * @param uid uid to allow/deny
+    * @param firewallRule either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    * @deprecated unimplemented on T+.
+    */
+    void firewallSetUidRule(int childChain, int uid, int firewallRule);
+
+   /**
+    * Enable/Disable target firewall child chain
+    *
+    * @param childChain target chain to enable
+    * @param enable whether to enable or disable child chain.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    * @deprecated unimplemented on T+.
+    */
+    void firewallEnableChildChain(int childChain, boolean enable);
+
+   /**
+    * Get interface list
+    *
+    * @return An array of strings containing all the interfaces on the system.
+    * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+    *         unix errno.
+    */
+    @utf8InCpp String[] interfaceGetList();
+
+    // Must be kept in sync with constant in InterfaceConfiguration.java
+    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";
+
+   /**
+    * Get interface configuration
+    *
+    * @param ifName interface name
+    * @return An InterfaceConfigurationParcel for the specified interface.
+    * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+    *         unix errno.
+    */
+    InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
+
+   /**
+    * Set interface configuration
+    *
+    * @param cfg Interface configuration to set
+    * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+    *         unix errno.
+    */
+    void interfaceSetCfg(in InterfaceConfigurationParcel cfg);
+
+   /**
+    * Set interface IPv6 privacy extensions
+    *
+    * @param ifName interface name
+    * @param enable whether to enable or disable this setting.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void interfaceSetIPv6PrivacyExtensions(in @utf8InCpp String ifName, boolean enable);
+
+   /**
+    * Clear all IP addresses on the given interface
+    *
+    * @param ifName interface name
+    * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+    *         POSIX errno.
+    */
+    void interfaceClearAddrs(in @utf8InCpp String ifName);
+
+   /**
+    * Enable or disable IPv6 on the given interface
+    *
+    * @param ifName interface name
+    * @param enable whether to enable or disable this setting.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void interfaceSetEnableIPv6(in @utf8InCpp String ifName, boolean enable);
+
+   /**
+    * Set interface MTU
+    *
+    * @param ifName interface name
+    * @param mtu MTU value
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void interfaceSetMtu(in @utf8InCpp String ifName, int mtu);
+
+   /**
+    * Add forwarding rule/stats on given interface.
+    *
+    * @param intIface downstream interface
+    * @param extIface upstream interface
+    */
+    void tetherAddForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+
+   /**
+    * Remove forwarding rule/stats on given interface.
+    *
+    * @param intIface downstream interface
+    * @param extIface upstream interface
+    */
+    void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+
+   /**
+    * Set the values of tcp_{rmem,wmem}.
+    *
+    * @param rmemValues the target values of tcp_rmem, each value is separated by spaces
+    * @param wmemValues the target values of tcp_wmem, each value is separated by spaces
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
+
+   /**
+    * Register unsolicited event listener
+    * Netd supports multiple unsolicited event listeners.
+    *
+    * @param listener unsolicited event listener to register
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void registerUnsolicitedEventListener(INetdUnsolicitedEventListener listener);
+
+    /**
+     * Add ingress interface filtering rules to a list of UIDs
+     *
+     * For a given uid, once a filtering rule is added, the kernel will only allow packets from the
+     * allowed interface and loopback to be sent to the list of UIDs.
+     *
+     * Calling this method on one or more UIDs with an existing filtering rule but a different
+     * interface name will result in the filtering rule being updated to allow the new interface
+     * instead. Otherwise calling this method will not affect existing rules set on other UIDs.
+     *
+     * @param ifName the name of the interface on which the filtering rules will allow packets to
+              be received.
+     * @param uids an array of UIDs which the filtering rules will be set
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     * @deprecated unimplemented on T+.
+     */
+    void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+
+    /**
+     * Remove ingress interface filtering rules from a list of UIDs
+     *
+     * Clear the ingress interface filtering rules from the list of UIDs which were previously set
+     * by firewallAddUidInterfaceRules(). Ignore any uid which does not have filtering rule.
+     *
+     * @param uids an array of UIDs from which the filtering rules will be removed
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     * @deprecated unimplemented on T+.
+     */
+    void firewallRemoveUidInterfaceRules(in int[] uids);
+
+   /**
+    * Request netd to change the current active network stats map.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    * @deprecated unimplemented on T+.
+    */
+    void trafficSwapActiveStatsMap();
+
+   /**
+    * Retrieves OEM netd listener interface
+    *
+    * @return a IBinder object, it could be casted to oem specific interface.
+    */
+    IBinder getOemNetd();
+
+   /**
+    * Start tethering with given configuration
+    *
+    * @param config config to start tethering.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void tetherStartWithConfiguration(in TetherConfigParcel config);
+
+
+    /**
+     * Get the fwmark and its net id mask for the given network id.
+     *
+     * @param netId the network to get the fwmark and mask for.
+     * @return A MarkMaskParcel of the given network id.
+     */
+    MarkMaskParcel getFwmarkForNetwork(int netId);
+
+    /**
+    * Add a route for specific network
+    *
+    * @param netId the network to add the route to
+    * @param routeInfo parcelable with route information
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void networkAddRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+
+    /**
+    * Update a route for specific network
+    *
+    * @param routeInfo parcelable with route information
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void networkUpdateRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+
+    /**
+    * Remove a route for specific network
+    *
+    * @param routeInfo parcelable with route information
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void networkRemoveRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+
+    /**
+     * Adds a tethering offload rule, or updates it if it already exists.
+     *
+     * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be updated
+     * if the input interface and destination prefix match. Otherwise, a new rule will be created.
+     *
+     * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline
+     *             module accesses the BPF map directly starting in S. See BpfCoordinator.
+     * @param rule The rule to add or update.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    void tetherOffloadRuleAdd(in TetherOffloadRuleParcel rule);
+
+    /**
+     * Deletes a tethering offload rule.
+     *
+     * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be deleted
+     * if the destination IP address and the source interface match. It is not an error if there is
+     * no matching rule to delete.
+     *
+     * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline
+     *             module accesses the BPF map directly starting in S. See BpfCoordinator.
+     * @param rule The rule to delete.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    void tetherOffloadRuleRemove(in TetherOffloadRuleParcel rule);
+
+    /**
+     * Return BPF tethering offload statistics.
+     *
+     * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline
+     *             module accesses the BPF map directly starting in S. See BpfCoordinator.
+     * @return an array of TetherStatsParcel's, where each entry contains the upstream interface
+     *         index and its tethering statistics since tethering was first started.
+     *         There will only ever be one entry for a given interface index.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    TetherStatsParcel[] tetherOffloadGetStats();
+
+   /**
+    * Set a per-interface quota for tethering offload.
+    *
+    * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline
+    *             module accesses the BPF map directly starting in S. See BpfCoordinator.
+    * @param ifIndex Index of upstream interface
+    * @param quotaBytes The quota defined as the number of bytes, starting from zero and counting
+    *       from *now*. A value of QUOTA_UNLIMITED (-1) indicates there is no limit.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+
+    /**
+     * Return BPF tethering offload statistics and clear the stats for a given upstream.
+     *
+     * Must only be called once all offload rules have already been deleted for the given upstream
+     * interface. The existing stats will be fetched and returned. The stats and the limit for the
+     * given upstream interface will be deleted as well.
+     *
+     * The stats and limit for a given upstream interface must be initialized (using
+     * tetherOffloadSetInterfaceQuota) before any offload will occur on that interface.
+     *
+     * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline
+     *             module accesses the BPF map directly starting in S. See BpfCoordinator.
+     * @param ifIndex Index of upstream interface.
+     * @return TetherStatsParcel, which contains the given upstream interface index and its
+     *         tethering statistics since tethering was first started on that upstream interface.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+     TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+
+    /**
+     * Creates a network.
+     *
+     * @param config the configuration of network.
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkCreate(in NativeNetworkConfig config);
+
+    /**
+     * Adds the specified UID ranges to the specified network. The network can be physical or
+     * virtual. Traffic from the UID ranges will be routed to the network by default. The possible
+     * value of subsidiary priority for physical and unreachable networks is 0-999. 0 is the highest
+     * priority. 0 is also the default value. Virtual network supports only the default value.
+     *
+     * @param NativeUidRangeConfig a parcel contains netId, UID ranges, subsidiary priority, etc.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkAddUidRangesParcel(in NativeUidRangeConfig uidRangesConfig);
+
+    /**
+     * Removes the specified UID ranges from the specified network. The network can be physical or
+     * virtual. Traffic from the UID ranges will no longer be routed to the network by default. The
+     * possible value of subsidiary priority for physical and unreachable networks is 0-999. 0 is
+     * the highest priority. 0 is also the default value. Virtual network supports only the default
+     * value.
+     *
+     * @param NativeUidRangeConfig a parcel contains netId, UID ranges, subsidiary priority, etc.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void networkRemoveUidRangesParcel(in NativeUidRangeConfig uidRangesConfig);
+
+    /**
+     * Migrate an existing IPsec tunnel mode SA to different addresses.
+     *
+     * If the underlying network also changes, caller must update it by
+     * calling ipSecAddSecurityAssociation.
+     *
+     * @param migrateInfo parcelable with migration info.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+     void ipSecMigrate(in android.net.IpSecMigrateInfoParcel migrateInfo);
+
+     /**
+      * IPSEC_DIRECTION_IN is used for IPsec SAs or policies that direct traffic towards the host.
+      */
+     const int IPSEC_DIRECTION_IN = 0;
+
+     /**
+      * IPSEC_DIRECTION_OUT is used for IPsec SAs or policies that direct traffic away from the host.
+      */
+     const int IPSEC_DIRECTION_OUT = 1;
+
+    /**
+    * Set the list of allowed UIDs for all networks with restrictions.
+    *
+    * This list is the entire list of restrictions for all networks known by
+    * netd. Calling this function always defines the entire list of restrictions,
+    * and networks not in the passed list are always reset to having no
+    * restrictions.
+    *
+    * @param NativeUidRangeConfig[] An array of allowlists, one per network. For each allowlist:
+    *                               - netId: the netId on which to set the allowlist
+    *                               - uidRanges: the UIDs allowed to use this network
+    *                               - 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/binder/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/binder/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..652a79c
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,145 @@
+/**
+ * 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.
+ */
+
+package android.net;
+
+/**
+ * Unsolicited netd events which are reported by the kernel via netlink.
+ * This one-way interface groups asynchronous notifications sent
+ * by netd to any process that registered itself via INetd.registerUnsolEventListener.
+ *
+ * {@hide}
+ */
+oneway interface INetdUnsolicitedEventListener {
+
+    /**
+     * Notifies that an interface has been idle/active for a certain period of time.
+     * It is the event for idletimer.
+     *
+     * @param isActive true for active status, false for idle
+     * @param timerLabel unique identifier of the idletimer.
+     *              Since NMS only set the identifier as int, only report event with int label.
+     * @param timestampNs kernel timestamp of this event, 0 for no timestamp
+     * @param uid uid of this event, -1 for no uid.
+     *            It represents the uid that was responsible for waking the radio.
+     */
+    void onInterfaceClassActivityChanged(
+            boolean isActive,
+            int timerLabel,
+            long timestampNs,
+            int uid);
+
+    /**
+     * Notifies that a specific interface reached its quota limit.
+     *
+     * @param alertName alert name of the quota limit
+     * @param ifName interface which reached the limit
+     */
+    void onQuotaLimitReached(@utf8InCpp String alertName, @utf8InCpp String ifName);
+
+    /**
+     * Provides information on IPv6 DNS servers on a specific interface.
+     *
+     * @param ifName interface name
+     * @param lifetimeS lifetime for the DNS servers in seconds
+     * @param servers the address of servers.
+     *                  e.g. IpV6: "2001:4860:4860::6464"
+     *
+     */
+    void onInterfaceDnsServerInfo(
+            @utf8InCpp String ifName, long lifetimeS, in @utf8InCpp String[] servers);
+
+    /**
+     * Notifies that an address has updated on a specific interface.
+     *
+     * @param addr address that is being updated
+     * @param ifName the name of the interface on which the address is configured
+     * @param flags address flags, see ifa_flags in if_addr.h
+     * @param scope current scope of the address
+     */
+    void onInterfaceAddressUpdated(
+            @utf8InCpp String addr,
+            @utf8InCpp String ifName,
+            int flags,
+            int scope);
+
+    /**
+     * Notifies that an address has been removed on a specific interface.
+     *
+     * @param addr address of this change
+     * @param ifName the name of the interface that changed addresses
+     * @param flags address flags, see ifa_flags in if_addr.h
+     * @param scope address address scope
+     */
+    void onInterfaceAddressRemoved(
+            @utf8InCpp String addr,
+            @utf8InCpp String ifName,
+            int flags,
+            int scope);
+
+    /**
+     * Notifies that an interface has been added.
+     *
+     * @param ifName the name of the added interface
+     */
+    void onInterfaceAdded(@utf8InCpp String ifName);
+
+    /**
+     * Notifies that an interface has been removed.
+     *
+     * @param ifName the name of the removed interface
+     */
+    void onInterfaceRemoved(@utf8InCpp String ifName);
+
+    /**
+     * Notifies that the status of the specific interface has changed.
+     *
+     * @param ifName the name of the interface that changed status
+     * @param up true for interface up, false for down
+     */
+    void onInterfaceChanged(@utf8InCpp String ifName, boolean up);
+
+    /**
+     * Notifies that the link state of the specific interface has changed.
+     *
+     * @param ifName the name of the interface whose link state has changed
+     * @param up true for interface link state up, false for link state down
+     */
+    void onInterfaceLinkStateChanged(@utf8InCpp String ifName, boolean up);
+
+    /**
+     * Notifies that an IP route has changed.
+     *
+     * @param updated true for update, false for remove
+     * @param route destination prefix of this route, e.g., "2001:db8::/64"
+     * @param gateway address of gateway, empty string for no gateway
+     * @param ifName interface name of this route, empty string for no interface
+     */
+    void onRouteChanged(
+            boolean updated,
+            @utf8InCpp String route,
+            @utf8InCpp String gateway,
+            @utf8InCpp String ifName);
+
+    /**
+     * Notifies that kernel has detected a socket sending data not wrapped
+     * inside a layer of SSL/TLS encryption.
+     *
+     * @param uid uid of this event
+     * @param hex packet content in hex format
+     */
+    void onStrictCleartextDetected(int uid, @utf8InCpp String hex);
+}
diff --git a/staticlibs/netd/binder/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/binder/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..c20792c
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+/**
+ * Configuration details for a network interface.
+ *
+ * {@hide}
+ */
+parcelable InterfaceConfigurationParcel {
+    @utf8InCpp String ifName;
+    @utf8InCpp String hwAddr;
+    @utf8InCpp String ipv4Addr;
+    int prefixLength;
+    /**
+    * Interface flags, String versions of IFF_* defined in netd/if.h
+    */
+    @utf8InCpp String[] flags;
+}
diff --git a/staticlibs/netd/binder/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/binder/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..e192d66
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/IpSecMigrateInfoParcel.aidl
@@ -0,0 +1,50 @@
+/**
+ * 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;
+
+@JavaOnlyImmutable
+parcelable IpSecMigrateInfoParcel {
+  /** The unique identifier for allocated resources. */
+  int requestId;
+  /**
+   * The address family identifier for the new selector. Can be AF_INET
+   * or AF_INET6.
+   */
+  int selAddrFamily;
+  /** IPSEC_DIRECTION_IN or IPSEC_DIRECTION_OUT. */
+  int direction;
+  /**
+   * The IP address for the current sending endpoint.
+   *
+   * The local address for an outbound SA and the remote address for an
+   * inbound SA.
+   */
+  @utf8InCpp String oldSourceAddress;
+  /**
+   * The IP address for the current receiving endpoint.
+   *
+   * The remote address for an outbound SA and the local address for an
+   * inbound SA.
+   */
+  @utf8InCpp String oldDestinationAddress;
+  /** The IP address for the new sending endpoint. */
+  @utf8InCpp String newSourceAddress;
+  /** The IP address for the new receiving endpoint. */
+  @utf8InCpp String newDestinationAddress;
+  /** The identifier for the XFRM interface. */
+  int interfaceId;
+}
diff --git a/staticlibs/netd/binder/android/net/MarkMaskParcel.aidl b/staticlibs/netd/binder/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..932b7bf
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * Structure that stores a firewall mark and its mask.
+ *
+ * {@hide}
+ */
+parcelable MarkMaskParcel {
+    // The fwmark.
+    int mark;
+    // Net id mask of fwmark.
+    int mask;
+}
diff --git a/staticlibs/netd/binder/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/binder/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..96eccc7
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,57 @@
+/*
+ * 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 android.net;
+
+import android.net.NativeNetworkType;
+import android.net.NativeVpnType;
+
+/**
+ * The configuration to create a network.
+ *
+ * {@hide}
+ */
+@JavaDerive(toString=true, equals=true)
+@JavaOnlyImmutable
+parcelable NativeNetworkConfig {
+    /** The networkId to create. */
+    int netId;
+
+    /**
+     * The type of network : virtual, physical or physical local network.
+     */
+    NativeNetworkType networkType = NativeNetworkType.PHYSICAL;
+
+    /**
+     * The permission necessary to use the network. Must be PERMISSION_NONE, PERMISSION_NETWORK
+     * or PERMISSION_SYSTEM. Ignored for virtual network types.
+     */
+    int permission;
+
+    /**
+     *  For virtual networks. Whether unprivileged apps are allowed to bypass the VPN. Ignored for
+     *  all other network types.
+     */
+    boolean secure;
+
+    /** For virtual networks. The type of VPN to create.  Ignored for all other network types. */
+    NativeVpnType vpnType = NativeVpnType.PLATFORM;
+
+    /**
+     * For virtual networks. Whether local traffic is excluded from the VPN.
+     */
+    boolean excludeLocalRoutes = false;
+}
diff --git a/staticlibs/netd/binder/android/net/NativeNetworkType.aidl b/staticlibs/netd/binder/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..7005535
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/NativeNetworkType.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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 android.net;
+
+@Backing(type="int")
+enum NativeNetworkType {
+  /**
+   * Physical network type.
+   */
+  PHYSICAL = 0,
+
+  /**
+   * Virtual private network type.
+   */
+  VIRTUAL = 1,
+
+  /**
+   * Physical local network, such as a tethering downstream.
+   */
+  PHYSICAL_LOCAL = 2,
+}
diff --git a/staticlibs/netd/binder/android/net/NativeVpnType.aidl b/staticlibs/netd/binder/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..cd1b447
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/NativeVpnType.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.
+ */
+
+package android.net;
+
+@Backing(type="int")
+enum NativeVpnType {
+  /**
+   * A VPN created by an app using the VpnService API.
+   */
+  SERVICE = 1,
+
+  /**
+   * A VPN created using a VpnManager API such as startProvisionedVpnProfile.
+   */
+  PLATFORM = 2,
+
+  /**
+   * An IPsec VPN created by the built-in LegacyVpnRunner.
+   */
+  LEGACY = 3,
+
+  /**
+   * An VPN created by OEM code through other means than VpnService or VpnManager.
+   */
+  OEM = 4,
+}
\ No newline at end of file
diff --git a/staticlibs/netd/binder/android/net/RouteInfoParcel.aidl b/staticlibs/netd/binder/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..fcc86e3
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,28 @@
+/**
+ * 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 android.net;
+
+parcelable RouteInfoParcel {
+  // The destination of the route.
+  @utf8InCpp String destination;
+  // The name of interface of the route. This interface should be assigned to the netID.
+  @utf8InCpp String ifName;
+  // The route's next hop address, or one of the NEXTHOP_* constants defined in INetd.aidl.
+  @utf8InCpp String nextHop;
+  // The MTU of the route.
+  int mtu;
+}
diff --git a/staticlibs/netd/binder/android/net/TetherConfigParcel.aidl b/staticlibs/netd/binder/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..9f371ce
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * The configuration to start tethering.
+ *
+ * {@hide}
+ */
+parcelable TetherConfigParcel {
+    // Whether to enable or disable legacy DNS proxy server.
+    boolean usingLegacyDnsProxy;
+    // DHCP ranges to set.
+    // dhcpRanges might contain many addresss {addr1, addr2, addr3, addr4...}
+    // Netd splits them into ranges: addr1-addr2, addr3-addr4, etc.
+    // An odd number of addrs will fail.
+    @utf8InCpp String[] dhcpRanges;
+}
diff --git a/staticlibs/netd/binder/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/binder/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..c549e61
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,45 @@
+/*
+ * 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 android.net;
+
+/**
+ * Represents a forwarding rule for tethering offload.
+ *
+ * {@hide}
+ */
+parcelable TetherOffloadRuleParcel {
+    /** The interface index of the input interface. */
+    int inputInterfaceIndex;
+
+    /** The interface index of the output interface. */
+    int outputInterfaceIndex;
+
+    /** The base IP address of the destination prefix as a byte array. */
+    byte[] destination;
+
+    /** The destination prefix length. */
+    int prefixLength;
+
+    /** The source link-layer address. Currently, must be a 6-byte MAC address.*/
+    byte[] srcL2Address;
+
+    /** The destination link-layer address. Currently, must be a 6-byte MAC address. */
+    byte[] dstL2Address;
+
+    /** The outbound path mtu. */
+    int pmtu = 1500;
+}
diff --git a/staticlibs/netd/binder/android/net/TetherStatsParcel.aidl b/staticlibs/netd/binder/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..6bf60a8
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+/**
+ * The statistics of tethering interface
+ *
+ * {@hide}
+ */
+parcelable TetherStatsParcel {
+    /**
+     * Parcel representing tethering interface statistics.
+     *
+     * This parcel is used by tetherGetStats, tetherOffloadGetStats and
+     * tetherOffloadGetAndClearStats in INetd.aidl. tetherGetStats uses this parcel to return the
+     * tethering statistics since netd startup and presents the interface via its interface name.
+     * Both tetherOffloadGetStats and tetherOffloadGetAndClearStats use this parcel to return
+     * the tethering statistics since tethering was first started. They present the interface via
+     * its interface index. Note that the interface must be presented by either interface name
+     * |iface| or interface index |ifIndex| in this parcel. The unused interface name is set to
+     * an empty string "" by default and the unused interface index is set to 0 by default.
+     */
+
+    /** The interface name. */
+    @utf8InCpp String iface;
+
+    /** Total number of received bytes. */
+    long rxBytes;
+
+    /** Total number of received packets. */
+    long rxPackets;
+
+    /** Total number of transmitted bytes. */
+    long txBytes;
+
+    /** Total number of transmitted packets. */
+    long txPackets;
+
+    /** The interface index. */
+    int ifIndex = 0;
+}
diff --git a/staticlibs/netd/binder/android/net/UidRangeParcel.aidl b/staticlibs/netd/binder/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..8f1fef6
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/UidRangeParcel.aidl
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+/**
+ * An inclusive range of UIDs.
+ *
+ * {@hide}
+ */
+@JavaOnlyImmutable @JavaDerive(toString=true, equals=true)
+parcelable UidRangeParcel {
+    int start;
+    int stop;
+}
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/DiscoveryInfo.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/DiscoveryInfo.aidl
new file mode 100644
index 0000000..f827382
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/DiscoveryInfo.aidl
@@ -0,0 +1,79 @@
+/*
+ * 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.mdns.aidl;
+
+/**
+ * Discovery service information.
+ * This information combine all arguments that used by both request and callback.
+ * Arguments are used by request:
+ * - id
+ * - registrationType
+ * - interfaceIdx
+ *
+ * Arguments are used by callback:
+ * - id
+ * - serviceName
+ * - registrationType
+ * - domainName
+ * - interfaceIdx
+ * - netId
+ * - result
+ *
+ * {@hide}
+ */
+@JavaOnlyImmutable
+@JavaDerive(equals=true, toString=true)
+parcelable DiscoveryInfo {
+    /**
+     * The operation ID.
+     * Must be unique among all operations (registration/discovery/resolution/getting address) and
+     * can't be reused.
+     * To stop a operation, it needs to use corresponding operation id.
+     */
+    int id;
+
+    /**
+     * The discovery result.
+     */
+    int result;
+
+    /**
+     * The discovered service name.
+     */
+    @utf8InCpp String serviceName;
+
+    /**
+     * The service type being discovered for followed by the protocol, separated by a dot
+     * (e.g. "_ftp._tcp"). The transport protocol must be "_tcp" or "_udp".
+     */
+    @utf8InCpp String registrationType;
+
+    /**
+     * The domain of the discovered service instance.
+     */
+    @utf8InCpp String domainName;
+
+    /**
+     * The interface index on which to discover services. 0 indicates "all interfaces".
+     */
+    int interfaceIdx;
+
+    /**
+     * The net id on which the service is advertised.
+     */
+    int netId;
+}
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/GetAddressInfo.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/GetAddressInfo.aidl
new file mode 100644
index 0000000..d53174a
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/GetAddressInfo.aidl
@@ -0,0 +1,69 @@
+/*
+ * 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.mdns.aidl;
+
+/**
+ * Get service address information.
+ * This information combine all arguments that used by both request and callback.
+ * Arguments are used by request:
+ * - id
+ * - hostname
+ * - interfaceIdx
+ *
+ * Arguments are used by callback:
+ * - id
+ * - hostname
+ * - interfaceIdx
+ * - netId
+ * - address
+ * - result
+ *
+ * {@hide}
+ */
+@JavaOnlyImmutable
+@JavaDerive(equals=true, toString=true)
+parcelable GetAddressInfo {
+    /**
+     * The operation ID.
+     */
+    int id;
+
+    /**
+     * The getting address result.
+     */
+    int result;
+
+    /**
+     * The fully qualified domain name of the host to be queried for.
+     */
+    @utf8InCpp String hostname;
+
+    /**
+     * The service address info, it's IPv4 or IPv6 addres.
+     */
+    @utf8InCpp String address;
+
+    /**
+     * The interface index on which to issue the query. 0 indicates "all interfaces".
+     */
+    int interfaceIdx;
+
+    /**
+     * The net id to which the answers pertain.
+     */
+    int netId;
+}
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
new file mode 100644
index 0000000..3bf1da8
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
@@ -0,0 +1,145 @@
+/*
+ * 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.mdns.aidl;
+
+import android.net.mdns.aidl.DiscoveryInfo;
+import android.net.mdns.aidl.GetAddressInfo;
+import android.net.mdns.aidl.IMDnsEventListener;
+import android.net.mdns.aidl.RegistrationInfo;
+import android.net.mdns.aidl.ResolutionInfo;
+
+/** {@hide} */
+interface IMDns {
+    /**
+     * Start the MDNSResponder daemon.
+     *
+     * @throws ServiceSpecificException with unix errno EALREADY if daemon is already running.
+     * @throws UnsupportedOperationException on Android V and after.
+     * @deprecated unimplemented on V+.
+     */
+    void startDaemon();
+
+    /**
+     * Stop the MDNSResponder daemon.
+     *
+     * @throws ServiceSpecificException with unix errno EBUSY if daemon is still in use.
+     * @throws UnsupportedOperationException on Android V and after.
+     * @deprecated unimplemented on V+.
+     */
+    void stopDaemon();
+
+    /**
+     * Start registering a service.
+     * This operation will send a service registration request to MDNSResponder. Register a listener
+     * via IMDns#registerEventListener to get the registration result SERVICE_REGISTERED/
+     * SERVICE_REGISTRATION_FAILED from callback IMDnsEventListener#onServiceRegistrationStatus.
+     *
+     * @param info The service information to register.
+     *
+     * @throws ServiceSpecificException with one of the following error values:
+     *         - Unix errno EBUSY if request id is already in use.
+     *         - kDNSServiceErr_* list in dns_sd.h if registration fail.
+     * @throws UnsupportedOperationException on Android U and after.
+     * @deprecated unimplemented on U+.
+     */
+    void registerService(in RegistrationInfo info);
+
+    /**
+     * Start discovering services.
+     * This operation will send a request to MDNSResponder to discover services. Register a listener
+     * via IMDns#registerEventListener to get the discovery result SERVICE_FOUND/SERVICE_LOST/
+     * SERVICE_DISCOVERY_FAILED from callback IMDnsEventListener#onServiceDiscoveryStatus.
+     *
+     * @param info The service to discover.
+     *
+     * @throws ServiceSpecificException with one of the following error values:
+     *         - Unix errno EBUSY if request id is already in use.
+     *         - kDNSServiceErr_* list in dns_sd.h if discovery fail.
+     * @throws UnsupportedOperationException on Android U and after.
+     * @deprecated unimplemented on U+.
+     */
+    void discover(in DiscoveryInfo info);
+
+    /**
+     * Start resolving the target service.
+     * This operation will send a request to MDNSResponder to resolve the target service. Register a
+     * listener via IMDns#registerEventListener to get the resolution result SERVICE_RESOLVED/
+     * SERVICE_RESOLUTION_FAILED from callback IMDnsEventListener#onServiceResolutionStatus.
+     *
+     * @param info The service to resolve.
+     *
+     * @throws ServiceSpecificException with one of the following error values:
+     *         - Unix errno EBUSY if request id is already in use.
+     *         - kDNSServiceErr_* list in dns_sd.h if resolution fail.
+     * @throws UnsupportedOperationException on Android U and after.
+     * @deprecated unimplemented on U+.
+     */
+    void resolve(in ResolutionInfo info);
+
+    /**
+     * Start getting the target service address.
+     * This operation will send a request to MDNSResponder to get the target service address.
+     * Register a listener via IMDns#registerEventListener to get the query result
+     * SERVICE_GET_ADDR_SUCCESS/SERVICE_GET_ADDR_FAILED from callback
+     * IMDnsEventListener#onGettingServiceAddressStatus.
+     *
+     * @param info the getting service address information.
+     *
+     * @throws ServiceSpecificException with one of the following error values:
+     *         - Unix errno EBUSY if request id is already in use.
+     *         - kDNSServiceErr_* list in dns_sd.h if getting address fail.
+     * @throws UnsupportedOperationException on Android U and after.
+     * @deprecated unimplemented on U+.
+     */
+    void getServiceAddress(in GetAddressInfo info);
+
+    /**
+     * Stop a operation which's requested before.
+     *
+     * @param id the operation id to be stopped.
+     *
+     * @throws ServiceSpecificException with unix errno ESRCH if request id is not in use.
+     * @throws UnsupportedOperationException on Android U and after.
+     * @deprecated unimplemented on U+.
+     */
+    void stopOperation(int id);
+
+    /**
+     * Register an event listener.
+     *
+     * @param listener The listener to be registered.
+     *
+     * @throws ServiceSpecificException with one of the following error values:
+     *         - Unix errno EINVAL if listener is null.
+     *         - Unix errno EEXIST if register duplicated listener.
+     * @throws UnsupportedOperationException on Android U and after.
+     * @deprecated unimplemented on U+.
+     */
+    void registerEventListener(in IMDnsEventListener listener);
+
+    /**
+     * Unregister an event listener.
+     *
+     * @param listener The listener to be unregistered.
+     *
+     * @throws ServiceSpecificException with unix errno EINVAL if listener is null.
+     * @throws UnsupportedOperationException on Android U and after.
+     * @deprecated unimplemented on U+.
+     */
+    void unregisterEventListener(in IMDnsEventListener listener);
+}
+
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
new file mode 100644
index 0000000..f7f028b
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -0,0 +1,74 @@
+/**
+ * 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.mdns.aidl;
+
+import android.net.mdns.aidl.DiscoveryInfo;
+import android.net.mdns.aidl.GetAddressInfo;
+import android.net.mdns.aidl.RegistrationInfo;
+import android.net.mdns.aidl.ResolutionInfo;
+
+/**
+ * MDNS events which are reported by the MDNSResponder.
+ * This one-way interface defines the asynchronous notifications sent by mdns service to any process
+ * that registered itself via IMDns.registerEventListener.
+ *
+ * {@hide}
+ */
+oneway interface IMDnsEventListener {
+    /**
+     * Types for MDNS operation result.
+     * These are in sync with packages/modules/Connectivity/staticlibs/netd/libnetdutils/include/\
+     * netdutils/ResponseCode.h
+     */
+    const int SERVICE_DISCOVERY_FAILED     = 602;
+    const int SERVICE_FOUND                = 603;
+    const int SERVICE_LOST                 = 604;
+    const int SERVICE_REGISTRATION_FAILED  = 605;
+    const int SERVICE_REGISTERED           = 606;
+    const int SERVICE_RESOLUTION_FAILED    = 607;
+    const int SERVICE_RESOLVED             = 608;
+    const int SERVICE_GET_ADDR_FAILED      = 611;
+    const int SERVICE_GET_ADDR_SUCCESS     = 612;
+
+    /**
+     * Notify service registration status.
+     *
+     * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+     */
+    void onServiceRegistrationStatus(in RegistrationInfo status);
+
+    /**
+     * Notify service discovery status.
+     *
+     * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+     */
+    void onServiceDiscoveryStatus(in DiscoveryInfo status);
+
+    /**
+     * Notify service resolution status.
+     *
+     * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+     */
+    void onServiceResolutionStatus(in ResolutionInfo status);
+
+    /**
+     * Notify getting service address status.
+     *
+     * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+     */
+    void onGettingServiceAddressStatus(in GetAddressInfo status);
+}
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/RegistrationInfo.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/RegistrationInfo.aidl
new file mode 100644
index 0000000..5483559
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/RegistrationInfo.aidl
@@ -0,0 +1,78 @@
+/*
+ * 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.mdns.aidl;
+
+/**
+ * Registration service information.
+ * This information combine all arguments that used by both request and callback.
+ * Arguments are used by request:
+ * - id
+ * - serviceName
+ * - registrationType
+ * - port
+ * - txtRecord
+ * - interfaceIdx
+ *
+ * Arguments are used by callback:
+ * - id
+ * - serviceName
+ * - registrationType
+ * - result
+ *
+ * {@hide}
+ */
+@JavaOnlyImmutable
+@JavaDerive(equals=true, toString=true)
+parcelable RegistrationInfo {
+    /**
+     * The operation ID.
+     */
+    int id;
+
+    /**
+     * The registration result.
+     */
+    int result;
+
+    /**
+     * The service name to be registered.
+     */
+    @utf8InCpp String serviceName;
+
+    /**
+     * The service type followed by the protocol, separated by a dot (e.g. "_ftp._tcp"). The service
+     * type must be an underscore, followed by 1-15 characters, which may be letters, digits, or
+     * hyphens. The transport protocol must be "_tcp" or "_udp". New service types should be
+     * registered at <http://www.dns-sd.org/ServiceTypes.html>.
+     */
+    @utf8InCpp String registrationType;
+
+    /**
+     * The port on which the service accepts connections.
+     */
+    int port;
+
+    /**
+     * The txt record.
+     */
+    byte[] txtRecord;
+
+    /**
+     * The interface index on which to register the service. 0 indicates "all interfaces".
+     */
+    int interfaceIdx;
+}
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/ResolutionInfo.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/ResolutionInfo.aidl
new file mode 100644
index 0000000..26e0cee
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/ResolutionInfo.aidl
@@ -0,0 +1,92 @@
+/*
+ * 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.mdns.aidl;
+
+/**
+ * Resolution service information.
+ * This information combine all arguments that used by both request and callback.
+ * Arguments are used by request:
+ * - id
+ * - serviceName
+ * - registrationType
+ * - domain
+ * - interfaceIdx
+ *
+ * Arguments are used by callback:
+ * - id
+ * - port
+ * - serviceFullName
+ * - hostname
+ * - txtRecord
+ * - interfaceIdx
+ * - result
+ *
+ * {@hide}
+ */
+@JavaOnlyImmutable
+@JavaDerive(equals=true, toString=true)
+parcelable ResolutionInfo {
+    /**
+     * The operation ID.
+     */
+    int id;
+
+    /**
+     * The resolution result.
+     */
+    int result;
+
+    /**
+     * The service name to be resolved.
+     */
+    @utf8InCpp String serviceName;
+
+    /**
+     * The service type to be resolved.
+     */
+    @utf8InCpp String registrationType;
+
+    /**
+     * The service domain to be resolved.
+     */
+    @utf8InCpp String domain;
+
+    /**
+     * The resolved full service domain name, in the form <servicename>.<protocol>.<domain>.
+     */
+    @utf8InCpp String serviceFullName;
+
+    /**
+     * The target hostname of the machine providing the service.
+     */
+    @utf8InCpp String hostname;
+
+    /**
+     * The port on which connections are accepted for this service.
+     */
+    int port;
+
+    /**
+     * The service's txt record.
+     */
+    byte[] txtRecord;
+
+    /**
+     * The interface index on which to resolve the service. 0 indicates "all interfaces".
+     */
+    int interfaceIdx;
+}
diff --git a/staticlibs/netd/binder/android/net/metrics/INetdEventListener.aidl b/staticlibs/netd/binder/android/net/metrics/INetdEventListener.aidl
new file mode 100644
index 0000000..ef1b2cb
--- /dev/null
+++ b/staticlibs/netd/binder/android/net/metrics/INetdEventListener.aidl
@@ -0,0 +1,128 @@
+/**
+ * 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 android.net.metrics;
+
+/**
+ * Logs netd events.
+ *
+ * {@hide}
+ */
+oneway interface INetdEventListener {
+    const int EVENT_GETADDRINFO = 1;
+    const int EVENT_GETHOSTBYNAME = 2;
+    const int EVENT_GETHOSTBYADDR = 3;
+    const int EVENT_RES_NSEND = 4;
+
+    const int REPORTING_LEVEL_NONE = 0;
+    const int REPORTING_LEVEL_METRICS = 1;
+    const int REPORTING_LEVEL_FULL = 2;
+
+    // Maximum number of IP addresses logged for DNS lookups before we truncate the full list.
+    const int DNS_REPORTED_IP_ADDRESSES_LIMIT = 10;
+
+    /**
+     * Logs a DNS lookup function call (getaddrinfo and gethostbyname).
+     *
+     * @param netId the ID of the network the lookup was performed on.
+     * @param eventType one of the EVENT_* constants in this interface.
+     * @param returnCode the return value of the function call.
+     * @param latencyMs the latency of the function call.
+     * @param hostname the name that was looked up.
+     * @param ipAddresses (possibly a subset of) the IP addresses returned.
+     *        At most {@link #DNS_REPORTED_IP_ADDRESSES_LIMIT} addresses are logged.
+     * @param ipAddressesCount the number of IP addresses returned. May be different from the length
+     *        of ipAddresses if there were too many addresses to log.
+     * @param uid the UID of the application that performed the query.
+     */
+    void onDnsEvent(int netId, int eventType, int returnCode, int latencyMs,
+            @utf8InCpp String hostname, in @utf8InCpp String[] ipAddresses,
+            int ipAddressesCount, int uid);
+
+    /**
+     * Represents a private DNS validation success or failure.
+     *
+     * @param netId the ID of the network the validation was performed on.
+     * @param ipAddress the IP address for which validation was performed.
+     * @param hostname the hostname for which validation was performed.
+     * @param validated whether or not validation was successful.
+     */
+    void onPrivateDnsValidationEvent(int netId, String ipAddress, String hostname,
+            boolean validated);
+
+    /**
+     * Logs a single connect library call.
+     *
+     * @param netId the ID of the network the connect was performed on.
+     * @param error 0 if the connect call succeeded, otherwise errno if it failed.
+     * @param latencyMs the latency of the connect call.
+     * @param ipAddr destination IP address.
+     * @param port destination port number.
+     * @param uid the UID of the application that performed the connection.
+     */
+    void onConnectEvent(int netId, int error, int latencyMs, String ipAddr, int port, int uid);
+
+    /**
+     * Logs a single RX packet which caused the main CPU to exit sleep state.
+     * @param prefix arbitrary string provided via wakeupAddInterface()
+     * @param uid UID of the destination process or -1 if no UID is available.
+     * @param ethertype of the RX packet encoded in an int in native order, or -1 if not available.
+     * @param ipNextHeader ip protocol of the RX packet as IPPROTO_* number,
+              or -1 if the packet was not IPv4 or IPv6.
+     * @param dstHw destination hardware address, or 0 if not available.
+     * @param srcIp source IP address, or null if not available.
+     * @param dstIp destination IP address, or null if not available.
+     * @param srcPort src port of RX packet in native order, or -1 if the packet was not UDP or TCP.
+     * @param dstPort dst port of RX packet in native order, or -1 if the packet was not UDP or TCP.
+     * @param timestampNs receive timestamp for the offending packet. In units of nanoseconds and
+     *        synchronized to CLOCK_MONOTONIC.
+     */
+    void onWakeupEvent(String prefix, int uid, int ethertype, int ipNextHeader, in byte[] dstHw,
+            String srcIp, String dstIp, int srcPort, int dstPort, long timestampNs);
+
+    /**
+     * An event sent after every Netlink sock_diag poll performed by Netd. This reported batch
+     * groups TCP socket stats aggregated by network id. Per-network data are stored in a
+     * structure-of-arrays style where networkIds, sentPackets, lostPackets, rttUs, and
+     * sentAckDiffMs have the same length. Stats for the i-th network is spread across all these
+     * arrays at index i.
+     * @param networkIds an array of network ids for which there was tcp socket stats to collect in
+     *        the last sock_diag poll.
+     * @param sentPackets an array of packet sent across all TCP sockets still alive and new
+              TCP sockets since the last sock_diag poll, summed per network id.
+     * @param lostPackets, an array of packet lost across all TCP sockets still alive and new
+              TCP sockets since the last sock_diag poll, summed per network id.
+     * @param rttUs an array of smoothed round trip times in microseconds, averaged across all TCP
+              sockets since the last sock_diag poll for a given network id.
+     * @param sentAckDiffMs an array of milliseconds duration between the last packet sent and the
+              last ack received for a socket, averaged across all TCP sockets for a network id.
+     */
+    void onTcpSocketStatsEvent(in int[] networkIds, in int[] sentPackets,
+            in int[] lostPackets, in int[] rttUs, in int[] sentAckDiffMs);
+
+    /**
+     * Represents adding or removing a NAT64 prefix.
+     *
+     * @param netId the ID of the network the prefix was discovered on.
+     * @param added true if the NAT64 prefix was added, or false if the NAT64 prefix was removed.
+     *        There is only one prefix at a time for each netId. If a prefix is added, it replaces
+     *        the previous-added prefix.
+     * @param prefixString the detected NAT64 prefix as a string literal.
+     * @param prefixLength the prefix length associated with this NAT64 prefix.
+     */
+    void onNat64PrefixEvent(int netId, boolean added, @utf8InCpp String prefixString,
+            int prefixLength);
+}
diff --git a/staticlibs/netd/binder/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/binder/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..99497a8
--- /dev/null
+++ b/staticlibs/netd/binder/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.
+ */
+
+package android.net.netd.aidl;
+
+import android.net.UidRangeParcel;
+
+/**
+ * The configuration to add or remove UID ranges.
+ *
+ * {@hide}
+ */
+@JavaDerive(toString=true, equals=true)
+@JavaOnlyImmutable
+parcelable NativeUidRangeConfig {
+    /** The network ID of the network to add/remove the ranges to/from. */
+    int netId;
+
+    /** A set of non-overlapping ranges of UIDs. */
+    UidRangeParcel[] uidRanges;
+
+    /**
+     * The priority of this UID range config. 0 is the highest priority; 999 is the lowest priority.
+     * The function of this parameter is to adjust the priority when the same UID is set to
+     * different networks for different features.
+     */
+    int subPriority;
+}
\ No newline at end of file
diff --git a/staticlibs/netd/libnetdutils/Android.bp b/staticlibs/netd/libnetdutils/Android.bp
new file mode 100644
index 0000000..2ae5911
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Android.bp
@@ -0,0 +1,77 @@
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+    name: "libnetdutils",
+    srcs: [
+        "DumpWriter.cpp",
+        "Fd.cpp",
+        "InternetAddresses.cpp",
+        "Log.cpp",
+        "Netfilter.cpp",
+        "Netlink.cpp",
+        "NetlinkListener.cpp",
+        "Slice.cpp",
+        "Socket.cpp",
+        "SocketOption.cpp",
+        "Status.cpp",
+        "Syscalls.cpp",
+        "UniqueFd.cpp",
+        "UniqueFile.cpp",
+        "Utils.cpp",
+    ],
+    defaults: ["netd_defaults"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    export_shared_lib_headers: [
+        "libbase",
+    ],
+    export_include_dirs: ["include"],
+    sanitize: {
+        cfi: true,
+    },
+
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.resolv",
+        "com.android.tethering",
+    ],
+    min_sdk_version: "30",
+}
+
+cc_test {
+    name: "netdutils_test",
+    srcs: [
+        "BackoffSequenceTest.cpp",
+        "FdTest.cpp",
+        "InternetAddressesTest.cpp",
+        "LogTest.cpp",
+        "MemBlockTest.cpp",
+        "SliceTest.cpp",
+        "StatusTest.cpp",
+        "SyscallsTest.cpp",
+        "ThreadUtilTest.cpp",
+    ],
+    defaults: ["netd_defaults"],
+    test_suites: ["device-tests"],
+    static_libs: [
+        "libgmock",
+        "libnetdutils",
+    ],
+    shared_libs: [
+        "libbase",
+    ],
+}
+
+cc_library_headers {
+    name: "libnetd_utils_headers",
+    export_include_dirs: ["include"],
+}
diff --git a/staticlibs/netd/libnetdutils/BackoffSequenceTest.cpp b/staticlibs/netd/libnetdutils/BackoffSequenceTest.cpp
new file mode 100644
index 0000000..b6653fe
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/BackoffSequenceTest.cpp
@@ -0,0 +1,186 @@
+/*
+ * 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.
+ */
+
+#include <gtest/gtest.h>
+
+#include "netdutils/BackoffSequence.h"
+
+namespace android {
+namespace netdutils {
+
+TEST(BackoffSequence, defaults) {
+    BackoffSequence<uint32_t> backoff;
+
+    EXPECT_TRUE(backoff.hasNextTimeout());
+    EXPECT_EQ(0x00000001U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000002U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000004U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000008U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000010U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000020U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000040U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000080U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000100U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000200U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000400U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000800U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00001000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00002000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00004000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00008000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00010000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00020000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00040000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00080000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00100000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00200000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00400000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00800000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x01000000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x02000000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x04000000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x08000000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x10000000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x20000000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x40000000U, backoff.getNextTimeout());
+    EXPECT_EQ(0x80000000U, backoff.getNextTimeout());
+    // Maxes out, and stays there, ad infinitum.
+    for (int i = 0; i < 10; i++) {
+        EXPECT_TRUE(backoff.hasNextTimeout());
+        EXPECT_EQ(0xffffffffU, backoff.getNextTimeout());
+    }
+}
+
+TEST(BackoffSequence, backoffToOncePerHour) {
+    auto backoff = BackoffSequence<uint32_t>::Builder()
+            .withInitialRetransmissionTime(1)
+            .withMaximumRetransmissionTime(3600)
+            .build();
+
+    EXPECT_TRUE(backoff.hasNextTimeout());
+    EXPECT_EQ(0x00000001U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000002U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000004U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000008U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000010U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000020U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000040U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000080U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000100U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000200U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000400U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000800U, backoff.getNextTimeout());
+    // Maxes out, and stays there, ad infinitum.
+    for (int i = 0; i < 10; i++) {
+        EXPECT_TRUE(backoff.hasNextTimeout());
+        EXPECT_EQ(3600U, backoff.getNextTimeout());
+    }
+}
+
+TEST(BackoffSequence, simpleMaxRetransCount) {
+    auto backoff = BackoffSequence<uint32_t>::Builder()
+            .withInitialRetransmissionTime(3)
+            .withMaximumRetransmissionCount(7)
+            .build();
+
+    EXPECT_TRUE(backoff.hasNextTimeout());
+    EXPECT_EQ(0x00000003U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000006U, backoff.getNextTimeout());
+    EXPECT_EQ(0x0000000cU, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000018U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000030U, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000060U, backoff.getNextTimeout());
+    EXPECT_EQ(0x000000c0U, backoff.getNextTimeout());
+
+    for (int i = 0; i < 10; i++) {
+        EXPECT_FALSE(backoff.hasNextTimeout());
+        EXPECT_EQ(backoff.getEndOfSequenceIndicator(), backoff.getNextTimeout());
+    }
+}
+
+TEST(BackoffSequence, simpleMaxDuration) {
+    auto backoff = BackoffSequence<int>::Builder()
+            .withInitialRetransmissionTime(3)
+            .withMaximumRetransmissionDuration(7)
+            .withEndOfSequenceIndicator(-1)
+            .build();
+
+    EXPECT_TRUE(backoff.hasNextTimeout());
+    EXPECT_EQ(0x00000003, backoff.getNextTimeout());
+    EXPECT_EQ(0x00000004, backoff.getNextTimeout());
+
+    for (int i = 0; i < 10; i++) {
+        EXPECT_FALSE(backoff.hasNextTimeout());
+        EXPECT_EQ(backoff.getEndOfSequenceIndicator(), backoff.getNextTimeout());
+        EXPECT_EQ(-1, backoff.getNextTimeout());
+    }
+}
+
+TEST(PathologicalBackoffSequence, ZeroInitialRetransTime) {
+    auto backoff = BackoffSequence<std::chrono::seconds>::Builder()
+            .withInitialRetransmissionTime(std::chrono::seconds(0))
+            .build();
+
+    for (int i = 0; i < 10; i++) {
+        // TODO: Decide whether this needs fixing, and how.
+        EXPECT_TRUE(backoff.hasNextTimeout());
+        EXPECT_EQ(backoff.getEndOfSequenceIndicator(), backoff.getNextTimeout());
+    }
+}
+
+TEST(PathologicalBackoffSequence, MaxRetransDurationGreaterThanInitialRetransTime) {
+    auto backoff = BackoffSequence<std::chrono::milliseconds>::Builder()
+            .withInitialRetransmissionTime(std::chrono::milliseconds(5))
+            .withMaximumRetransmissionDuration(std::chrono::milliseconds(3))
+            .build();
+
+    EXPECT_EQ(std::chrono::milliseconds(3), backoff.getNextTimeout());
+    for (int i = 0; i < 10; i++) {
+        EXPECT_FALSE(backoff.hasNextTimeout());
+        EXPECT_EQ(backoff.getEndOfSequenceIndicator(), backoff.getNextTimeout());
+    }
+}
+
+TEST(PathologicalBackoffSequence, MaxRetransDurationEqualsInitialRetransTime) {
+    auto backoff = BackoffSequence<std::chrono::hours>::Builder()
+            .withInitialRetransmissionTime(std::chrono::hours(5))
+            .withMaximumRetransmissionDuration(std::chrono::hours(5))
+            .build();
+
+    EXPECT_EQ(std::chrono::hours(5), backoff.getNextTimeout());
+    for (int i = 0; i < 10; i++) {
+        EXPECT_FALSE(backoff.hasNextTimeout());
+        EXPECT_EQ(backoff.getEndOfSequenceIndicator(), backoff.getNextTimeout());
+    }
+}
+
+TEST(PathologicalBackoffSequence, MaxRetransTimeAndDurationGreaterThanInitialRetransTime) {
+    auto backoff = BackoffSequence<std::chrono::nanoseconds>::Builder()
+            .withInitialRetransmissionTime(std::chrono::nanoseconds(7))
+            .withMaximumRetransmissionTime(std::chrono::nanoseconds(3))
+            .withMaximumRetransmissionDuration(std::chrono::nanoseconds(5))
+            .build();
+
+    EXPECT_EQ(std::chrono::nanoseconds(3), backoff.getNextTimeout());
+    EXPECT_EQ(std::chrono::nanoseconds(2), backoff.getNextTimeout());
+    for (int i = 0; i < 10; i++) {
+        EXPECT_FALSE(backoff.hasNextTimeout());
+        EXPECT_EQ(backoff.getEndOfSequenceIndicator(), backoff.getNextTimeout());
+    }
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/DumpWriter.cpp b/staticlibs/netd/libnetdutils/DumpWriter.cpp
new file mode 100644
index 0000000..092ddba
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/DumpWriter.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+#include "netdutils/DumpWriter.h"
+
+#include <unistd.h>
+#include <limits>
+
+#include <android-base/stringprintf.h>
+#include <utils/String8.h>
+
+using android::base::StringAppendV;
+
+namespace android {
+namespace netdutils {
+
+namespace {
+
+const char kIndentString[] = "  ";
+const size_t kIndentStringLen = strlen(kIndentString);
+
+}  // namespace
+
+DumpWriter::DumpWriter(int fd) : mIndentLevel(0), mFd(fd) {}
+
+void DumpWriter::incIndent() {
+    if (mIndentLevel < std::numeric_limits<decltype(mIndentLevel)>::max()) {
+        mIndentLevel++;
+    }
+}
+
+void DumpWriter::decIndent() {
+    if (mIndentLevel > std::numeric_limits<decltype(mIndentLevel)>::min()) {
+        mIndentLevel--;
+    }
+}
+
+void DumpWriter::println(const std::string& line) {
+    if (!line.empty()) {
+        for (int i = 0; i < mIndentLevel; i++) {
+            ::write(mFd, kIndentString, kIndentStringLen);
+        }
+        ::write(mFd, line.c_str(), line.size());
+    }
+    ::write(mFd, "\n", 1);
+}
+
+// NOLINTNEXTLINE(cert-dcl50-cpp): Grandfathered C-style variadic function.
+void DumpWriter::println(const char* fmt, ...) {
+    std::string line;
+    va_list ap;
+    va_start(ap, fmt);
+    StringAppendV(&line, fmt, ap);
+    va_end(ap);
+    println(line);
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/Fd.cpp b/staticlibs/netd/libnetdutils/Fd.cpp
new file mode 100644
index 0000000..2651f90
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Fd.cpp
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "netdutils/Fd.h"
+
+namespace android {
+namespace netdutils {
+
+std::ostream& operator<<(std::ostream& os, const Fd& fd) {
+    return os << "Fd[" << fd.get() << "]";
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/FdTest.cpp b/staticlibs/netd/libnetdutils/FdTest.cpp
new file mode 100644
index 0000000..7080f83
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/FdTest.cpp
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cstdint>
+
+#include <gtest/gtest.h>
+
+#include "netdutils/MockSyscalls.h"
+#include "netdutils/Status.h"
+#include "netdutils/Syscalls.h"
+
+using testing::Mock;
+using testing::Return;
+using testing::StrictMock;
+
+namespace android {
+namespace netdutils {
+namespace {
+
+// Force implicit conversion from UniqueFd -> Fd
+inline Fd toFd(const UniqueFd& fd) {
+    return fd;
+}
+
+}  // namespace
+
+TEST(Fd, smoke) {
+    // Expect the following lines to compile
+    Fd fd1(1);
+    Fd fd2(fd1);
+    Fd fd3 = fd2;
+    const Fd fd4(8);
+    const Fd fd5(fd4);
+    const Fd fd6 = fd5;
+    EXPECT_TRUE(isWellFormed(fd3));
+    EXPECT_TRUE(isWellFormed(fd6));
+
+    // Corner case
+    Fd zero(0);
+    EXPECT_TRUE(isWellFormed(zero));
+
+    // Invalid file descriptors
+    Fd bad(-1);
+    Fd weird(-9);
+    EXPECT_FALSE(isWellFormed(bad));
+    EXPECT_FALSE(isWellFormed(weird));
+
+    // Default constructor
+    EXPECT_EQ(Fd(-1), Fd());
+    std::stringstream ss;
+    ss << fd3 << " " << fd6 << " " << bad << " " << weird;
+    EXPECT_EQ("Fd[1] Fd[8] Fd[-1] Fd[-9]", ss.str());
+}
+
+class UniqueFdTest : public testing::Test {
+  protected:
+    StrictMock<ScopedMockSyscalls> mSyscalls;
+};
+
+TEST_F(UniqueFdTest, operatorOstream) {
+    UniqueFd u(97);
+    EXPECT_CALL(mSyscalls, close(toFd(u))).WillOnce(Return(status::ok));
+    std::stringstream ss;
+    ss << u;
+    EXPECT_EQ("UniqueFd[Fd[97]]", ss.str());
+    u.reset();
+}
+
+TEST_F(UniqueFdTest, destructor) {
+    {
+        UniqueFd u(98);
+        EXPECT_CALL(mSyscalls, close(toFd(u))).WillOnce(Return(status::ok));
+    }
+    // Expectation above should be upon leaving nested scope
+    Mock::VerifyAndClearExpectations(&mSyscalls);
+}
+
+TEST_F(UniqueFdTest, reset) {
+    UniqueFd u(99);
+    EXPECT_CALL(mSyscalls, close(toFd(u))).WillOnce(Return(status::ok));
+    u.reset();
+
+    // Expectation above should be upon reset
+    Mock::VerifyAndClearExpectations(&mSyscalls);
+}
+
+TEST_F(UniqueFdTest, moveConstructor) {
+    constexpr Fd kFd(101);
+    UniqueFd u1(kFd);
+    {
+        UniqueFd u2(std::move(u1));
+        // NOLINTNEXTLINE bugprone-use-after-move
+        EXPECT_FALSE(isWellFormed(u1));
+        EXPECT_TRUE(isWellFormed(u2));
+        EXPECT_CALL(mSyscalls, close(kFd)).WillOnce(Return(status::ok));
+    }
+    // Expectation above should be upon leaving nested scope
+    Mock::VerifyAndClearExpectations(&mSyscalls);
+}
+
+TEST_F(UniqueFdTest, moveAssignment) {
+    constexpr Fd kFd(102);
+    UniqueFd u1(kFd);
+    {
+        UniqueFd u2 = std::move(u1);
+        // NOLINTNEXTLINE bugprone-use-after-move
+        EXPECT_FALSE(isWellFormed(u1));
+        EXPECT_TRUE(isWellFormed(u2));
+        UniqueFd u3;
+        u3 = std::move(u2);
+        // NOLINTNEXTLINE bugprone-use-after-move
+        EXPECT_FALSE(isWellFormed(u2));
+        EXPECT_TRUE(isWellFormed(u3));
+        EXPECT_CALL(mSyscalls, close(kFd)).WillOnce(Return(status::ok));
+    }
+    // Expectation above should be upon leaving nested scope
+    Mock::VerifyAndClearExpectations(&mSyscalls);
+}
+
+TEST_F(UniqueFdTest, constConstructor) {
+    constexpr Fd kFd(103);
+    const UniqueFd u(kFd);
+    EXPECT_CALL(mSyscalls, close(toFd(u))).WillOnce(Return(status::ok));
+}
+
+TEST_F(UniqueFdTest, closeFailure) {
+    constexpr Fd kFd(103);
+    UniqueFd u(kFd);
+    EXPECT_CALL(mSyscalls, close(toFd(u))).WillOnce(Return(statusFromErrno(EINTR, "test")));
+    EXPECT_DEBUG_DEATH(u.reset(), "");
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/InternetAddresses.cpp b/staticlibs/netd/libnetdutils/InternetAddresses.cpp
new file mode 100644
index 0000000..6d98608
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/InternetAddresses.cpp
@@ -0,0 +1,165 @@
+/*
+ * 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.
+ */
+
+#include "netdutils/InternetAddresses.h"
+
+#include <stdlib.h>
+#include <string>
+
+#include <android-base/stringprintf.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+namespace android {
+
+using base::StringPrintf;
+
+namespace netdutils {
+
+std::string IPAddress::toString() const noexcept {
+    char repr[INET6_ADDRSTRLEN] = "\0";
+
+    switch (mData.family) {
+        case AF_UNSPEC:
+            return "<unspecified>";
+        case AF_INET: {
+            const in_addr v4 = mData.ip.v4;
+            inet_ntop(AF_INET, &v4, repr, sizeof(repr));
+            break;
+        }
+        case AF_INET6: {
+            const in6_addr v6 = mData.ip.v6;
+            inet_ntop(AF_INET6, &v6, repr, sizeof(repr));
+            break;
+        }
+        default:
+            return "<unknown_family>";
+    }
+
+    if (mData.family == AF_INET6 && mData.scope_id > 0) {
+        return StringPrintf("%s%%%u", repr, mData.scope_id);
+    }
+
+    return repr;
+}
+
+bool IPAddress::forString(const std::string& repr, IPAddress* ip) {
+    const addrinfo hints = {
+            .ai_flags = AI_NUMERICHOST | AI_NUMERICSERV,
+    };
+    addrinfo* res;
+    const int ret = getaddrinfo(repr.c_str(), nullptr, &hints, &res);
+    ScopedAddrinfo res_cleanup(res);
+    if (ret != 0) {
+        return false;
+    }
+
+    bool rval = true;
+    switch (res[0].ai_family) {
+        case AF_INET: {
+            sockaddr_in* sin = (sockaddr_in*) res[0].ai_addr;
+            if (ip) *ip = IPAddress(sin->sin_addr);
+            break;
+        }
+        case AF_INET6: {
+            sockaddr_in6* sin6 = (sockaddr_in6*) res[0].ai_addr;
+            if (ip) *ip = IPAddress(sin6->sin6_addr, sin6->sin6_scope_id);
+            break;
+        }
+        default:
+            rval = false;
+            break;
+    }
+
+    return rval;
+}
+
+IPPrefix::IPPrefix(const IPAddress& ip, int length) : IPPrefix(ip) {
+    // Silently treat CIDR lengths like "-1" as meaning the full bit length
+    // appropriate to the address family.
+    if (length < 0) return;
+    if (length >= mData.cidrlen) return;
+
+    switch (mData.family) {
+        case AF_UNSPEC:
+            break;
+        case AF_INET: {
+            const in_addr_t mask = (length > 0) ? (~0U) << (IPV4_ADDR_BITS - length) : 0U;
+            mData.ip.v4.s_addr &= htonl(mask);
+            mData.cidrlen = static_cast<uint8_t>(length);
+            break;
+        }
+        case AF_INET6: {
+            // The byte in which this CIDR length falls.
+            const int which = length / 8;
+            const int mask = (length % 8 == 0) ? 0 : 0xff << (8 - length % 8);
+            mData.ip.v6.s6_addr[which] &= mask;
+            for (int i = which + 1; i < IPV6_ADDR_LEN; i++) {
+                mData.ip.v6.s6_addr[i] = 0U;
+            }
+            mData.cidrlen = static_cast<uint8_t>(length);
+            break;
+        }
+        default:
+            // TODO: Complain bitterly about possible data corruption?
+            return;
+    }
+}
+
+bool IPPrefix::isUninitialized() const noexcept {
+    static const internal_::compact_ipdata empty{};
+    return mData == empty;
+}
+
+bool IPPrefix::forString(const std::string& repr, IPPrefix* prefix) {
+    size_t index = repr.find('/');
+    if (index == std::string::npos) return false;
+
+    // Parse the IP address.
+    IPAddress ip;
+    if (!IPAddress::forString(repr.substr(0, index), &ip)) return false;
+
+    // Parse the prefix length. Can't use base::ParseUint because it accepts non-base 10 input.
+    const char* prefixString = repr.c_str() + index + 1;
+    if (!isdigit(*prefixString)) return false;
+    char* endptr;
+    unsigned long prefixlen = strtoul(prefixString, &endptr, 10);
+    if (*endptr != '\0') return false;
+
+    uint8_t maxlen = (ip.family() == AF_INET) ? 32 : 128;
+    if (prefixlen > maxlen) return false;
+
+    *prefix = IPPrefix(ip, prefixlen);
+    return true;
+}
+
+std::string IPPrefix::toString() const noexcept {
+    return StringPrintf("%s/%d", ip().toString().c_str(), mData.cidrlen);
+}
+
+std::string IPSockAddr::toString() const noexcept {
+    switch (mData.family) {
+        case AF_INET6:
+            return StringPrintf("[%s]:%u", ip().toString().c_str(), mData.port);
+        default:
+            return StringPrintf("%s:%u", ip().toString().c_str(), mData.port);
+    }
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/InternetAddressesTest.cpp b/staticlibs/netd/libnetdutils/InternetAddressesTest.cpp
new file mode 100644
index 0000000..9e37d11
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/InternetAddressesTest.cpp
@@ -0,0 +1,728 @@
+/*
+ * 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.
+ */
+
+#include <cstdint>
+#include <limits>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <android-base/macros.h>
+#include <fmt/format.h>
+#include <gtest/gtest.h>
+
+#include "netdutils/InternetAddresses.h"
+
+namespace android {
+namespace netdutils {
+namespace {
+
+enum Relation { EQ, LT };
+
+std::ostream& operator<<(std::ostream& os, Relation relation) {
+    switch (relation) {
+        case EQ: os << "eq"; break;
+        case LT: os << "lt"; break;
+        default: os << "?!"; break;
+    }
+    return os;
+}
+
+template <typename T>
+struct OperatorExpectation {
+    const Relation relation;
+    const T obj1;
+    const T obj2;
+
+    std::string toString() const {
+        std::stringstream output;
+        output << obj1 << " " << relation << " " << obj2;
+        return output.str();
+    }
+};
+
+template <typename T>
+void testGamutOfOperators(const OperatorExpectation<T>& expectation) {
+    switch (expectation.relation) {
+        case EQ:
+            EXPECT_TRUE(expectation.obj1 == expectation.obj2);
+            EXPECT_TRUE(expectation.obj1 <= expectation.obj2);
+            EXPECT_TRUE(expectation.obj1 >= expectation.obj2);
+            EXPECT_FALSE(expectation.obj1 != expectation.obj2);
+            EXPECT_FALSE(expectation.obj1 < expectation.obj2);
+            EXPECT_FALSE(expectation.obj1 > expectation.obj2);
+            break;
+
+        case LT:
+            EXPECT_TRUE(expectation.obj1 < expectation.obj2);
+            EXPECT_TRUE(expectation.obj1 <= expectation.obj2);
+            EXPECT_TRUE(expectation.obj1 != expectation.obj2);
+            EXPECT_FALSE(expectation.obj1 > expectation.obj2);
+            EXPECT_FALSE(expectation.obj1 >= expectation.obj2);
+            EXPECT_FALSE(expectation.obj1 == expectation.obj2);
+            break;
+
+        default:
+            FAIL() << "Unknown relation given in test expectation";
+    }
+}
+
+const in_addr IPV4_ANY{htonl(INADDR_ANY)};
+const in_addr IPV4_LOOPBACK{htonl(INADDR_LOOPBACK)};
+const in_addr IPV4_ONES{~0U};
+const in6_addr IPV6_ANY = IN6ADDR_ANY_INIT;
+const in6_addr IPV6_LOOPBACK = IN6ADDR_LOOPBACK_INIT;
+const in6_addr FE80{{{0xfe,0x80,0,0,0,0,0,0,0,0,0,0,0,0,0,0}}};
+const in6_addr FE80_1{{{0xfe,0x80,0,0,0,0,0,0,0,0,0,0,0,0,0,1}}};
+const in6_addr FE80_2{{{0xfe,0x80,0,0,0,0,0,0,0,0,0,0,0,0,0,2}}};
+const uint8_t ff = std::numeric_limits<uint8_t>::max();
+const in6_addr IPV6_ONES{{{ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff}}};
+
+TEST(IPAddressTest, GamutOfOperators) {
+    const std::vector<OperatorExpectation<IPAddress>> kExpectations{
+            {EQ, IPAddress(), IPAddress()},
+            {EQ, IPAddress(IPV4_ONES), IPAddress(IPV4_ONES)},
+            {EQ, IPAddress(IPV6_ONES), IPAddress(IPV6_ONES)},
+            {EQ, IPAddress(FE80_1), IPAddress(FE80_1)},
+            {EQ, IPAddress(FE80_2), IPAddress(FE80_2)},
+            {LT, IPAddress(), IPAddress(IPV4_ANY)},
+            {LT, IPAddress(), IPAddress(IPV4_ONES)},
+            {LT, IPAddress(), IPAddress(IPV6_ANY)},
+            {LT, IPAddress(), IPAddress(IPV6_ONES)},
+            {LT, IPAddress(IPV4_ANY), IPAddress(IPV4_ONES)},
+            {LT, IPAddress(IPV4_ANY), IPAddress(IPV6_ANY)},
+            {LT, IPAddress(IPV4_ONES), IPAddress(IPV6_ANY)},
+            {LT, IPAddress(IPV4_ONES), IPAddress(IPV6_ONES)},
+            {LT, IPAddress(IPV6_ANY), IPAddress(IPV6_LOOPBACK)},
+            {LT, IPAddress(IPV6_ANY), IPAddress(IPV6_ONES)},
+            {LT, IPAddress(IPV6_LOOPBACK), IPAddress(IPV6_ONES)},
+            {LT, IPAddress(FE80_1), IPAddress(FE80_2)},
+            {LT, IPAddress(FE80_1), IPAddress(IPV6_ONES)},
+            {LT, IPAddress(FE80_2), IPAddress(IPV6_ONES)},
+            // Sort by scoped_id within the same address.
+            {LT, IPAddress(FE80_1), IPAddress(FE80_1, 1)},
+            {LT, IPAddress(FE80_1, 1), IPAddress(FE80_1, 2)},
+            // Sort by address first, scope_id second.
+            {LT, IPAddress(FE80_1, 2), IPAddress(FE80_2, 1)},
+    };
+
+    size_t tests_run = 0;
+    for (const auto& expectation : kExpectations) {
+        SCOPED_TRACE(expectation.toString());
+        EXPECT_NO_FATAL_FAILURE(testGamutOfOperators(expectation));
+        tests_run++;
+    }
+    EXPECT_EQ(kExpectations.size(), tests_run);
+}
+
+TEST(IPAddressTest, ScopeIds) {
+    // Scope IDs ignored for IPv4 addresses.
+    const IPAddress ones(IPV4_ONES);
+    EXPECT_EQ(0U, ones.scope_id());
+    const IPAddress ones22(ones, 22);
+    EXPECT_EQ(0U, ones22.scope_id());
+    EXPECT_EQ(ones, ones22);
+    const IPAddress ones23(ones, 23);
+    EXPECT_EQ(0U, ones23.scope_id());
+    EXPECT_EQ(ones22, ones23);
+
+    EXPECT_EQ("fe80::1%22", IPAddress(FE80_1, 22).toString());
+    EXPECT_EQ("fe80::2%23", IPAddress(FE80_2, 23).toString());
+
+    // Verify that given an IPAddress with a scope_id an address without a
+    // scope_id can be constructed (just in case it's useful).
+    const IPAddress fe80_intf22(FE80_1, 22);
+    EXPECT_EQ(22U, fe80_intf22.scope_id());
+    EXPECT_EQ(fe80_intf22, IPAddress(fe80_intf22));
+    EXPECT_EQ(IPAddress(FE80_1), IPAddress(fe80_intf22, 0));
+}
+
+TEST(IPAddressTest, forString) {
+    IPAddress ip;
+
+    EXPECT_FALSE(IPAddress::forString("not_an_ip", &ip));
+    EXPECT_FALSE(IPAddress::forString("not_an_ip", nullptr));
+    EXPECT_EQ(IPAddress(), IPAddress::forString("not_an_ip"));
+
+    EXPECT_EQ(IPAddress(IPV4_ANY), IPAddress::forString("0.0.0.0"));
+    EXPECT_EQ(IPAddress(IPV4_ONES), IPAddress::forString("255.255.255.255"));
+    EXPECT_EQ(IPAddress(IPV4_LOOPBACK), IPAddress::forString("127.0.0.1"));
+
+    EXPECT_EQ(IPAddress(IPV6_ANY), IPAddress::forString("::"));
+    EXPECT_EQ(IPAddress(IPV6_ANY), IPAddress::forString("::0"));
+    EXPECT_EQ(IPAddress(IPV6_ANY), IPAddress::forString("0::"));
+    EXPECT_EQ(IPAddress(IPV6_LOOPBACK), IPAddress::forString("::1"));
+    EXPECT_EQ(IPAddress(IPV6_LOOPBACK), IPAddress::forString("0::1"));
+    EXPECT_EQ(IPAddress(FE80_1), IPAddress::forString("fe80::1"));
+    EXPECT_EQ(IPAddress(FE80_1, 22), IPAddress::forString("fe80::1%22"));
+    // This relies upon having a loopback interface named "lo" with ifindex 1.
+    EXPECT_EQ(IPAddress(FE80_1, 1), IPAddress::forString("fe80::1%lo"));
+}
+
+TEST(IPPrefixTest, forString) {
+    IPPrefix prefix;
+
+    EXPECT_FALSE(IPPrefix::forString("", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("invalid", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("192.0.2.0", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001::db8::", &prefix));
+
+    EXPECT_FALSE(IPPrefix::forString("2001:db8::/", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001:db8:://32", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001:db8::/32z", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001:db8::/32/", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001:db8::/0x20", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001:db8:: /32", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001:db8::/ 32", &prefix));
+    EXPECT_FALSE(IPPrefix::forString(" 2001:db8::/32", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001:db8::/32 ", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001:db8::/+32", &prefix));
+
+    EXPECT_FALSE(IPPrefix::forString("192.0.2.0/33", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001:db8::/129", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("192.0.2.0/-1", &prefix));
+    EXPECT_FALSE(IPPrefix::forString("2001:db8::/-1", &prefix));
+
+    EXPECT_TRUE(IPPrefix::forString("2001:db8::/32", &prefix));
+    EXPECT_EQ("2001:db8::/32", prefix.toString());
+    EXPECT_EQ(IPPrefix(IPAddress::forString("2001:db8::"), 32), prefix);
+
+    EXPECT_EQ(IPPrefix(), IPPrefix::forString("invalid"));
+
+    EXPECT_EQ("0.0.0.0/0", IPPrefix::forString("0.0.0.0/0").toString());
+    EXPECT_EQ("::/0", IPPrefix::forString("::/0").toString());
+    EXPECT_EQ("192.0.2.128/25", IPPrefix::forString("192.0.2.131/25").toString());
+    EXPECT_EQ("2001:db8:1:2:3:4:5:4/126",
+              IPPrefix::forString("2001:db8:1:2:3:4:5:6/126").toString());
+}
+
+TEST(IPPrefixTest, IPv4Truncation) {
+    const auto prefixStr = [](int length) -> std::string {
+        return IPPrefix(IPAddress(IPV4_ONES), length).toString();
+    };
+
+    EXPECT_EQ("0.0.0.0/0", prefixStr(0));
+
+    EXPECT_EQ("128.0.0.0/1", prefixStr(1));
+    EXPECT_EQ("192.0.0.0/2", prefixStr(2));
+    EXPECT_EQ("224.0.0.0/3", prefixStr(3));
+    EXPECT_EQ("240.0.0.0/4", prefixStr(4));
+    EXPECT_EQ("248.0.0.0/5", prefixStr(5));
+    EXPECT_EQ("252.0.0.0/6", prefixStr(6));
+    EXPECT_EQ("254.0.0.0/7", prefixStr(7));
+    EXPECT_EQ("255.0.0.0/8", prefixStr(8));
+
+    EXPECT_EQ("255.128.0.0/9", prefixStr(9));
+    EXPECT_EQ("255.192.0.0/10", prefixStr(10));
+    EXPECT_EQ("255.224.0.0/11", prefixStr(11));
+    EXPECT_EQ("255.240.0.0/12", prefixStr(12));
+    EXPECT_EQ("255.248.0.0/13", prefixStr(13));
+    EXPECT_EQ("255.252.0.0/14", prefixStr(14));
+    EXPECT_EQ("255.254.0.0/15", prefixStr(15));
+    EXPECT_EQ("255.255.0.0/16", prefixStr(16));
+
+    EXPECT_EQ("255.255.128.0/17", prefixStr(17));
+    EXPECT_EQ("255.255.192.0/18", prefixStr(18));
+    EXPECT_EQ("255.255.224.0/19", prefixStr(19));
+    EXPECT_EQ("255.255.240.0/20", prefixStr(20));
+    EXPECT_EQ("255.255.248.0/21", prefixStr(21));
+    EXPECT_EQ("255.255.252.0/22", prefixStr(22));
+    EXPECT_EQ("255.255.254.0/23", prefixStr(23));
+    EXPECT_EQ("255.255.255.0/24", prefixStr(24));
+
+    EXPECT_EQ("255.255.255.128/25", prefixStr(25));
+    EXPECT_EQ("255.255.255.192/26", prefixStr(26));
+    EXPECT_EQ("255.255.255.224/27", prefixStr(27));
+    EXPECT_EQ("255.255.255.240/28", prefixStr(28));
+    EXPECT_EQ("255.255.255.248/29", prefixStr(29));
+    EXPECT_EQ("255.255.255.252/30", prefixStr(30));
+    EXPECT_EQ("255.255.255.254/31", prefixStr(31));
+    EXPECT_EQ("255.255.255.255/32", prefixStr(32));
+}
+
+TEST(IPPrefixTest, IPv6Truncation) {
+    const auto prefixStr = [](int length) -> std::string {
+        return IPPrefix(IPAddress(IPV6_ONES), length).toString();
+    };
+
+    EXPECT_EQ("::/0", prefixStr(0));
+
+    EXPECT_EQ("8000::/1", prefixStr(1));
+    EXPECT_EQ("c000::/2", prefixStr(2));
+    EXPECT_EQ("e000::/3", prefixStr(3));
+    EXPECT_EQ("f000::/4", prefixStr(4));
+    EXPECT_EQ("f800::/5", prefixStr(5));
+    EXPECT_EQ("fc00::/6", prefixStr(6));
+    EXPECT_EQ("fe00::/7", prefixStr(7));
+    EXPECT_EQ("ff00::/8", prefixStr(8));
+
+    EXPECT_EQ("ff80::/9", prefixStr(9));
+    EXPECT_EQ("ffc0::/10", prefixStr(10));
+    EXPECT_EQ("ffe0::/11", prefixStr(11));
+    EXPECT_EQ("fff0::/12", prefixStr(12));
+    EXPECT_EQ("fff8::/13", prefixStr(13));
+    EXPECT_EQ("fffc::/14", prefixStr(14));
+    EXPECT_EQ("fffe::/15", prefixStr(15));
+    EXPECT_EQ("ffff::/16", prefixStr(16));
+
+    EXPECT_EQ("ffff:8000::/17", prefixStr(17));
+    EXPECT_EQ("ffff:c000::/18", prefixStr(18));
+    EXPECT_EQ("ffff:e000::/19", prefixStr(19));
+    EXPECT_EQ("ffff:f000::/20", prefixStr(20));
+    EXPECT_EQ("ffff:f800::/21", prefixStr(21));
+    EXPECT_EQ("ffff:fc00::/22", prefixStr(22));
+    EXPECT_EQ("ffff:fe00::/23", prefixStr(23));
+    EXPECT_EQ("ffff:ff00::/24", prefixStr(24));
+
+    EXPECT_EQ("ffff:ff80::/25", prefixStr(25));
+    EXPECT_EQ("ffff:ffc0::/26", prefixStr(26));
+    EXPECT_EQ("ffff:ffe0::/27", prefixStr(27));
+    EXPECT_EQ("ffff:fff0::/28", prefixStr(28));
+    EXPECT_EQ("ffff:fff8::/29", prefixStr(29));
+    EXPECT_EQ("ffff:fffc::/30", prefixStr(30));
+    EXPECT_EQ("ffff:fffe::/31", prefixStr(31));
+    EXPECT_EQ("ffff:ffff::/32", prefixStr(32));
+
+    EXPECT_EQ("ffff:ffff:8000::/33", prefixStr(33));
+    EXPECT_EQ("ffff:ffff:c000::/34", prefixStr(34));
+    EXPECT_EQ("ffff:ffff:e000::/35", prefixStr(35));
+    EXPECT_EQ("ffff:ffff:f000::/36", prefixStr(36));
+    EXPECT_EQ("ffff:ffff:f800::/37", prefixStr(37));
+    EXPECT_EQ("ffff:ffff:fc00::/38", prefixStr(38));
+    EXPECT_EQ("ffff:ffff:fe00::/39", prefixStr(39));
+    EXPECT_EQ("ffff:ffff:ff00::/40", prefixStr(40));
+
+    EXPECT_EQ("ffff:ffff:ff80::/41", prefixStr(41));
+    EXPECT_EQ("ffff:ffff:ffc0::/42", prefixStr(42));
+    EXPECT_EQ("ffff:ffff:ffe0::/43", prefixStr(43));
+    EXPECT_EQ("ffff:ffff:fff0::/44", prefixStr(44));
+    EXPECT_EQ("ffff:ffff:fff8::/45", prefixStr(45));
+    EXPECT_EQ("ffff:ffff:fffc::/46", prefixStr(46));
+    EXPECT_EQ("ffff:ffff:fffe::/47", prefixStr(47));
+    EXPECT_EQ("ffff:ffff:ffff::/48", prefixStr(48));
+
+    EXPECT_EQ("ffff:ffff:ffff:8000::/49", prefixStr(49));
+    EXPECT_EQ("ffff:ffff:ffff:c000::/50", prefixStr(50));
+    EXPECT_EQ("ffff:ffff:ffff:e000::/51", prefixStr(51));
+    EXPECT_EQ("ffff:ffff:ffff:f000::/52", prefixStr(52));
+    EXPECT_EQ("ffff:ffff:ffff:f800::/53", prefixStr(53));
+    EXPECT_EQ("ffff:ffff:ffff:fc00::/54", prefixStr(54));
+    EXPECT_EQ("ffff:ffff:ffff:fe00::/55", prefixStr(55));
+    EXPECT_EQ("ffff:ffff:ffff:ff00::/56", prefixStr(56));
+
+    EXPECT_EQ("ffff:ffff:ffff:ff80::/57", prefixStr(57));
+    EXPECT_EQ("ffff:ffff:ffff:ffc0::/58", prefixStr(58));
+    EXPECT_EQ("ffff:ffff:ffff:ffe0::/59", prefixStr(59));
+    EXPECT_EQ("ffff:ffff:ffff:fff0::/60", prefixStr(60));
+    EXPECT_EQ("ffff:ffff:ffff:fff8::/61", prefixStr(61));
+    EXPECT_EQ("ffff:ffff:ffff:fffc::/62", prefixStr(62));
+    EXPECT_EQ("ffff:ffff:ffff:fffe::/63", prefixStr(63));
+    EXPECT_EQ("ffff:ffff:ffff:ffff::/64", prefixStr(64));
+
+    EXPECT_EQ("ffff:ffff:ffff:ffff:8000::/65", prefixStr(65));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:c000::/66", prefixStr(66));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:e000::/67", prefixStr(67));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:f000::/68", prefixStr(68));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:f800::/69", prefixStr(69));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:fc00::/70", prefixStr(70));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:fe00::/71", prefixStr(71));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ff00::/72", prefixStr(72));
+
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ff80::/73", prefixStr(73));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffc0::/74", prefixStr(74));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffe0::/75", prefixStr(75));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:fff0::/76", prefixStr(76));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:fff8::/77", prefixStr(77));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:fffc::/78", prefixStr(78));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:fffe::/79", prefixStr(79));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff::/80", prefixStr(80));
+
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:8000::/81", prefixStr(81));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:c000::/82", prefixStr(82));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:e000::/83", prefixStr(83));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:f000::/84", prefixStr(84));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:f800::/85", prefixStr(85));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:fc00::/86", prefixStr(86));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:fe00::/87", prefixStr(87));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ff00::/88", prefixStr(88));
+
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ff80::/89", prefixStr(89));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffc0::/90", prefixStr(90));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffe0::/91", prefixStr(91));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:fff0::/92", prefixStr(92));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:fff8::/93", prefixStr(93));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:fffc::/94", prefixStr(94));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:fffe::/95", prefixStr(95));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff::/96", prefixStr(96));
+
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:8000:0/97", prefixStr(97));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:c000:0/98", prefixStr(98));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:e000:0/99", prefixStr(99));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:f000:0/100", prefixStr(100));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:f800:0/101", prefixStr(101));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:fc00:0/102", prefixStr(102));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:fe00:0/103", prefixStr(103));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ff00:0/104", prefixStr(104));
+
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ff80:0/105", prefixStr(105));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffc0:0/106", prefixStr(106));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffe0:0/107", prefixStr(107));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:fff0:0/108", prefixStr(108));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:fff8:0/109", prefixStr(109));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:fffc:0/110", prefixStr(110));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:fffe:0/111", prefixStr(111));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/112", prefixStr(112));
+
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:8000/113", prefixStr(113));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:c000/114", prefixStr(114));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:e000/115", prefixStr(115));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:f000/116", prefixStr(116));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:f800/117", prefixStr(117));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fc00/118", prefixStr(118));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fe00/119", prefixStr(119));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00/120", prefixStr(120));
+
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff80/121", prefixStr(121));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffc0/122", prefixStr(122));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffe0/123", prefixStr(123));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0/124", prefixStr(124));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff8/125", prefixStr(125));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffc/126", prefixStr(126));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe/127", prefixStr(127));
+    EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128", prefixStr(128));
+}
+
+TEST(IPPrefixTest, TruncationOther) {
+    const struct {
+        const char* ip;
+        const int cidrLen;
+        const char* ipTruncated;
+    } testExpectations[] = {
+            {"192.0.2.0", 24, "192.0.2.0"},
+            {"192.0.2.0", 23, "192.0.2.0"},
+            {"192.0.2.0", 22, "192.0.0.0"},
+            {"192.0.2.0", 1, "128.0.0.0"},
+            {"2001:db8:cafe:d00d::", 56, "2001:db8:cafe:d000::"},
+            {"2001:db8:cafe:d00d::", 48, "2001:db8:cafe::"},
+            {"2001:db8:cafe:d00d::", 47, "2001:db8:cafe::"},
+            {"2001:db8:cafe:d00d::", 46, "2001:db8:cafc::"},
+    };
+
+    for (const auto& expectation : testExpectations) {
+        IPAddress ip;
+        EXPECT_TRUE(IPAddress::forString(expectation.ip, &ip))
+                << "Failed to parse IP address " << expectation.ip;
+
+        IPAddress ipTruncated;
+        EXPECT_TRUE(IPAddress::forString(expectation.ipTruncated, &ipTruncated))
+                << "Failed to parse IP address " << expectation.ipTruncated;
+
+        IPPrefix prefix(ip, expectation.cidrLen);
+
+        EXPECT_EQ(expectation.cidrLen, prefix.length())
+                << "Unexpected cidrLen " << expectation.cidrLen;
+        EXPECT_EQ(ipTruncated, prefix.ip())
+                << "Unexpected IP truncation: " << prefix.ip() << ", expected: " << ipTruncated;
+    }
+}
+
+TEST(IPPrefixTest, containsPrefix) {
+    const struct {
+        const char* prefix;
+        const char* otherPrefix;
+        const bool expected;
+        std::string asParameters() const {
+            return fmt::format("prefix={}, other={}, expect={}", prefix, otherPrefix, expected);
+        }
+    } testExpectations[] = {
+            {"192.0.0.0/8", "192.0.0.0/8", true},
+            {"192.1.0.0/16", "192.1.0.0/16", true},
+            {"192.1.2.0/24", "192.1.2.0/24", true},
+            {"192.1.2.3/32", "192.1.2.3/32", true},
+            {"0.0.0.0/0", "192.0.0.0/8", true},
+            {"0.0.0.0/0", "192.1.0.0/16", true},
+            {"0.0.0.0/0", "192.1.2.0/24", true},
+            {"0.0.0.0/0", "192.1.2.3/32", true},
+            {"192.0.0.0/8", "192.1.0.0/16", true},
+            {"192.0.0.0/8", "192.1.2.0/24", true},
+            {"192.0.0.0/8", "192.1.2.5/32", true},
+            {"192.1.0.0/16", "192.1.2.0/24", true},
+            {"192.1.0.0/16", "192.1.3.6/32", true},
+            {"192.5.6.0/24", "192.5.6.7/32", true},
+            {"192.1.2.3/32", "192.1.2.0/24", false},
+            {"192.1.2.3/32", "192.1.0.0/16", false},
+            {"192.1.2.3/32", "192.0.0.0/8", false},
+            {"192.1.2.3/32", "0.0.0.0/0", false},
+            {"192.1.2.0/24", "192.1.0.0/16", false},
+            {"192.1.2.0/24", "192.0.0.0/8", false},
+            {"192.1.2.0/24", "0.0.0.0/0", false},
+            {"192.9.0.0/16", "192.0.0.0/8", false},
+            {"192.9.0.0/16", "0.0.0.0/0", false},
+            {"192.0.0.0/8", "0.0.0.0/0", false},
+            {"192.0.0.0/8", "191.0.0.0/8", false},
+            {"191.0.0.0/8", "192.0.0.0/8", false},
+            {"192.8.0.0/16", "192.7.0.0/16", false},
+            {"192.7.0.0/16", "192.8.0.0/16", false},
+            {"192.8.6.0/24", "192.7.5.0/24", false},
+            {"192.7.5.0/24", "192.8.6.0/24", false},
+            {"192.8.6.100/32", "192.8.6.200/32", false},
+            {"192.8.6.200/32", "192.8.6.100/32", false},
+            {"192.0.0.0/8", "192.0.0.0/12", true},
+            {"192.0.0.0/12", "192.0.0.0/8", false},
+            {"2001::/16", "2001::/16", true},
+            {"2001:db8::/32", "2001:db8::/32", true},
+            {"2001:db8:cafe::/48", "2001:db8:cafe::/48", true},
+            {"2001:db8:cafe:d00d::/64", "2001:db8:cafe:d00d::/64", true},
+            {"2001:db8:cafe:d00d:fec0::/80", "2001:db8:cafe:d00d:fec0::/80", true},
+            {"2001:db8:cafe:d00d:fec0:de::/96", "2001:db8:cafe:d00d:fec0:de::/96", true},
+            {"2001:db8:cafe:d00d:fec0:de:ac::/112", "2001:db8:cafe:d00d:fec0:de:ac::/112", true},
+            {"2001:db8::cafe:0:1/128", "2001:db8::cafe:0:1/128", true},
+            {"2001::/16", "2001:db8::/32", true},
+            {"2001::/16", "2001:db8:cafe::/48", true},
+            {"2001::/16", "2001:db8:cafe:d00d::/64", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0::/80", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0:de::/96", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0:de:ac::/112", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0:de:ac:dd/128", true},
+            {"::/0", "2001::/16", true},
+            {"::/0", "2001:db8::/32", true},
+            {"::/0", "2001:db8:cafe::/48", true},
+            {"::/0", "2001:db8:cafe:d00d::/64", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0::/80", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0:de::/96", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0:de:ac::/112", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0:de:ac:dd/128", true},
+            {"2001:db8::dd/128", "2001::/16", false},
+            {"2001:db8::dd/128", "2001:db8::/32", false},
+            {"2001:db8::dd/128", "2001:db8:cafe::/48", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d::/64", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d:fec0::/80", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d:fec0:de::/96", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d:fec0:de:ac::/112", false},
+            {"2001:db7::/32", "2001:db8::/32", false},
+            {"2001:db8::/32", "2001:db7::/32", false},
+            {"2001:db8:caff::/48", "2001:db8:cafe::/48", false},
+            {"2001:db8:cafe::/48", "2001:db8:caff::/48", false},
+            {"2001:db8:cafe:a00d::/64", "2001:db8:cafe:d00d::/64", false},
+            {"2001:db8:cafe:d00d::/64", "2001:db8:cafe:a00d::/64", false},
+            {"2001:db8:cafe:d00d:fec1::/80", "2001:db8:cafe:d00d:fec0::/80", false},
+            {"2001:db8:cafe:d00d:fec0::/80", "2001:db8:cafe:d00d:fec1::/80", false},
+            {"2001:db8:cafe:d00d:fec0:dd::/96", "2001:db8:cafe:d00d:fec0:ae::/96", false},
+            {"2001:db8:cafe:d00d:fec0:ae::/96", "2001:db8:cafe:d00d:fec0:dd::/96", false},
+            {"2001:db8:cafe:d00d:fec0:de:aa::/112", "2001:db8:cafe:d00d:fec0:de:ac::/112", false},
+            {"2001:db8:cafe:d00d:fec0:de:ac::/112", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"2001:db8::cafe:0:123/128", "2001:db8::cafe:0:456/128", false},
+            {"2001:db8::cafe:0:456/128", "2001:db8::cafe:0:123/128", false},
+            {"2001:db8::/32", "2001:db8::/64", true},
+            {"2001:db8::/64", "2001:db8::/32", false},
+            {"::/0", "0.0.0.0/0", false},
+            {"::/0", "1.0.0.0/8", false},
+            {"::/0", "1.2.0.0/16", false},
+            {"::/0", "1.2.3.0/24", false},
+            {"::/0", "1.2.3.4/32", false},
+            {"2001::/16", "1.2.3.4/32", false},
+            {"2001::db8::/32", "1.2.3.4/32", false},
+            {"2001:db8:cafe::/48", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d::/64", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d:fec0::/80", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d:fec0:ae::/96", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d:fec0:de:aa::/112", "1.2.3.4/32", false},
+            {"0.0.0.0/0", "::/0", false},
+            {"0.0.0.0/0", "2001::/16", false},
+            {"0.0.0.0/0", "2001::db8::/32", false},
+            {"0.0.0.0/0", "2001:db8:cafe::/48", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d::/64", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d:fec0::/80", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d:fec0:ae::/96", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.2.3.4/32", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.2.3.0/24", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.2.0.0/16", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.0.0.0/8", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+    };
+
+    for (const auto& expectation : testExpectations) {
+        SCOPED_TRACE(expectation.asParameters());
+        IPPrefix a = IPPrefix::forString(expectation.prefix);
+        IPPrefix b = IPPrefix::forString(expectation.otherPrefix);
+        EXPECT_EQ(expectation.expected, a.contains(b));
+    }
+}
+
+TEST(IPPrefixTest, containsAddress) {
+    const struct {
+        const char* prefix;
+        const char* address;
+        const bool expected;
+        std::string asParameters() const {
+            return fmt::format("prefix={}, address={}, expect={}", prefix, address, expected);
+        }
+    } testExpectations[] = {
+        {"0.0.0.0/0", "255.255.255.255", true},
+        {"0.0.0.0/0", "1.2.3.4", true},
+        {"0.0.0.0/0", "1.2.3.0", true},
+        {"0.0.0.0/0", "1.2.0.0", true},
+        {"0.0.0.0/0", "1.0.0.0", true},
+        {"0.0.0.0/0", "0.0.0.0", true},
+        {"0.0.0.0/0", "2001:4868:4860::8888", false},
+        {"0.0.0.0/0", "::/0", false},
+        {"192.0.2.0/23", "192.0.2.0", true},
+        {"192.0.2.0/23", "192.0.2.43", true},
+        {"192.0.2.0/23", "192.0.3.21", true},
+        {"192.0.2.0/23", "192.0.0.21", false},
+        {"192.0.2.0/23", "8.8.8.8", false},
+        {"192.0.2.0/23", "2001:4868:4860::8888", false},
+        {"192.0.2.0/23", "::/0", false},
+        {"1.2.3.4/32", "1.2.3.4", true},
+        {"1.2.3.4/32", "1.2.3.5", false},
+        {"10.0.0.0/8", "10.2.0.0", true},
+        {"10.0.0.0/8", "10.2.3.5", true},
+        {"10.0.0.0/8", "10.0.0.0", true},
+        {"10.0.0.0/8", "10.255.255.254", true},
+        {"10.0.0.0/8", "11.0.0.0", false},
+        {"::/0", "2001:db8:f000::ace:d00c", true},
+        {"::/0", "2002:db8:f00::ace:d00d", true},
+        {"::/0", "2001:db7:f00::ace:d00e", true},
+        {"::/0", "2001:db8:f01::bad:d00d", true},
+        {"::/0", "::", true},
+        {"::/0", "0.0.0.0", false},
+        {"::/0", "1.2.3.4", false},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::ace:d00c", true},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::ace:d00d", true},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::ace:d00e", false},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::bad:d00d", false},
+        {"2001:db8:f00::ace:d00d/127", "2001:4868:4860::8888", false},
+        {"2001:db8:f00::ace:d00d/127", "8.8.8.8", false},
+        {"2001:db8:f00::ace:d00d/127", "0.0.0.0", false},
+        {"2001:db8:f00::ace:d00d/128", "2001:db8:f00::ace:d00d", true},
+        {"2001:db8:f00::ace:d00d/128", "2001:db8:f00::ace:d00c", false},
+        {"2001::/16", "2001::", true},
+        {"2001::/16", "2001:db8:f00::ace:d00d", true},
+        {"2001::/16", "2001:db8:f00::bad:d00d", true},
+        {"2001::/16", "2001::abc", true},
+        {"2001::/16", "2001:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true},
+        {"2001::/16", "2000::", false},
+    };
+
+    for (const auto& expectation : testExpectations) {
+        SCOPED_TRACE(expectation.asParameters());
+        IPPrefix a = IPPrefix::forString(expectation.prefix);
+        IPAddress b = IPAddress::forString(expectation.address);
+        EXPECT_EQ(expectation.expected, a.contains(b));
+    }
+}
+
+TEST(IPPrefixTest, GamutOfOperators) {
+    const std::vector<OperatorExpectation<IPPrefix>> kExpectations{
+            {EQ, IPPrefix(), IPPrefix()},
+            {EQ, IPPrefix(IPAddress(IPV4_ANY), 0), IPPrefix(IPAddress(IPV4_ANY), 0)},
+            {EQ, IPPrefix(IPAddress(IPV4_ANY), IPV4_ADDR_BITS), IPPrefix(IPAddress(IPV4_ANY))},
+            {EQ, IPPrefix(IPAddress(IPV6_ANY), 0), IPPrefix(IPAddress(IPV6_ANY), 0)},
+            {EQ, IPPrefix(IPAddress(IPV6_ANY), IPV6_ADDR_BITS), IPPrefix(IPAddress(IPV6_ANY))},
+            // Needlessly fully-specified IPv6 link-local address.
+            {EQ, IPPrefix(IPAddress(FE80_1)), IPPrefix(IPAddress(FE80_1, 0), IPV6_ADDR_BITS)},
+            // Different IPv6 link-local addresses within the same /64, no scoped_id: same /64.
+            {EQ, IPPrefix(IPAddress(FE80_1), 64), IPPrefix(IPAddress(FE80_2), 64)},
+            // Different IPv6 link-local address within the same /64, same scoped_id: same /64.
+            {EQ, IPPrefix(IPAddress(FE80_1, 17), 64), IPPrefix(IPAddress(FE80_2, 17), 64)},
+            // Unspecified < IPv4.
+            {LT, IPPrefix(), IPPrefix(IPAddress(IPV4_ANY), 0)},
+            // Same IPv4 base address sorts by prefix length.
+            {LT, IPPrefix(IPAddress(IPV4_ANY), 0), IPPrefix(IPAddress(IPV4_ANY), 1)},
+            {LT, IPPrefix(IPAddress(IPV4_ANY), 1), IPPrefix(IPAddress(IPV4_ANY), IPV4_ADDR_BITS)},
+            // Truncation means each base IPv4 address is different.
+            {LT, IPPrefix(IPAddress(IPV4_ONES), 0), IPPrefix(IPAddress(IPV4_ONES), 1)},
+            {LT, IPPrefix(IPAddress(IPV4_ONES), 1), IPPrefix(IPAddress(IPV4_ONES), IPV4_ADDR_BITS)},
+            // Sort by base IPv4 addresses first.
+            {LT, IPPrefix(IPAddress(IPV4_ANY), 0), IPPrefix(IPAddress::forString("0.0.0.1"))},
+            {LT, IPPrefix(IPAddress(IPV4_ANY), 1), IPPrefix(IPAddress::forString("0.0.0.1"))},
+            {LT, IPPrefix(IPAddress(IPV4_ANY), 24), IPPrefix(IPAddress::forString("0.0.0.1"))},
+            // IPv4 < IPv6.
+            {LT, IPPrefix(IPAddress(IPV4_ANY), 0), IPPrefix(IPAddress(IPV6_ANY), 0)},
+            {LT, IPPrefix(IPAddress(IPV4_ONES)), IPPrefix(IPAddress(IPV6_ANY))},
+            // Unspecified < IPv6.
+            {LT, IPPrefix(), IPPrefix(IPAddress(IPV6_ANY), 0)},
+            // Same IPv6 base address sorts by prefix length.
+            {LT, IPPrefix(IPAddress(IPV6_ANY), 0), IPPrefix(IPAddress(IPV6_ANY), 1)},
+            {LT, IPPrefix(IPAddress(IPV6_ANY), 1), IPPrefix(IPAddress(IPV6_ANY), IPV6_ADDR_BITS)},
+            // Truncation means each base IPv6 address is different.
+            {LT, IPPrefix(IPAddress(IPV6_ONES), 0), IPPrefix(IPAddress(IPV6_ONES), 1)},
+            {LT, IPPrefix(IPAddress(IPV6_ONES), 1), IPPrefix(IPAddress(IPV6_ONES), IPV6_ADDR_BITS)},
+            // Different IPv6 link-local address in same /64, different scoped_id: different /64.
+            {LT, IPPrefix(IPAddress(FE80_1, 17), 64), IPPrefix(IPAddress(FE80_2, 22), 64)},
+            {LT, IPPrefix(IPAddress(FE80_1, 17), 64), IPPrefix(IPAddress(FE80_1, 18), 64)},
+            {LT, IPPrefix(IPAddress(FE80_1, 18), 64), IPPrefix(IPAddress(FE80_1, 19), 64)},
+    };
+
+    size_t tests_run = 0;
+    for (const auto& expectation : kExpectations) {
+        SCOPED_TRACE(expectation.toString());
+        EXPECT_NO_FATAL_FAILURE(testGamutOfOperators(expectation));
+        tests_run++;
+    }
+    EXPECT_EQ(kExpectations.size(), tests_run);
+}
+
+TEST(IPSockAddrTest, GamutOfOperators) {
+    const std::vector<OperatorExpectation<IPSockAddr>> kExpectations{
+            {EQ, IPSockAddr(), IPSockAddr()},
+            {EQ, IPSockAddr(IPAddress(IPV4_ANY)), IPSockAddr(IPAddress(IPV4_ANY), 0)},
+            {EQ, IPSockAddr(IPAddress(IPV6_ANY)), IPSockAddr(IPAddress(IPV6_ANY), 0)},
+            {EQ, IPSockAddr(IPAddress(FE80_1), 80), IPSockAddr(IPAddress(FE80_1), 80)},
+            {EQ, IPSockAddr(IPAddress(FE80_1, 17)), IPSockAddr(IPAddress(FE80_1, 17), 0)},
+            {LT, IPSockAddr(IPAddress(IPV4_ANY), 0), IPSockAddr(IPAddress(IPV4_ANY), 1)},
+            {LT, IPSockAddr(IPAddress(IPV4_ANY), 53), IPSockAddr(IPAddress(IPV4_ANY), 123)},
+            {LT, IPSockAddr(IPAddress(IPV4_ONES), 123), IPSockAddr(IPAddress(IPV6_ANY), 53)},
+            {LT, IPSockAddr(IPAddress(IPV6_ANY), 0), IPSockAddr(IPAddress(IPV6_ANY), 1)},
+            {LT, IPSockAddr(IPAddress(IPV6_ANY), 53), IPSockAddr(IPAddress(IPV6_ANY), 123)},
+            {LT, IPSockAddr(IPAddress(FE80_1), 80), IPSockAddr(IPAddress(FE80_1, 17), 80)},
+            {LT, IPSockAddr(IPAddress(FE80_1, 17), 80), IPSockAddr(IPAddress(FE80_1, 22), 80)},
+    };
+
+    size_t tests_run = 0;
+    for (const auto& expectation : kExpectations) {
+        SCOPED_TRACE(expectation.toString());
+        EXPECT_NO_FATAL_FAILURE(testGamutOfOperators(expectation));
+        tests_run++;
+    }
+    EXPECT_EQ(kExpectations.size(), tests_run);
+}
+
+TEST(IPSockAddrTest, toString) {
+    EXPECT_EQ("<unspecified>:0", IPSockAddr().toString());
+    EXPECT_EQ("0.0.0.0:0", IPSockAddr(IPAddress(IPV4_ANY)).toString());
+    EXPECT_EQ("255.255.255.255:67", IPSockAddr(IPAddress(IPV4_ONES), 67).toString());
+    EXPECT_EQ("[::]:0", IPSockAddr(IPAddress(IPV6_ANY)).toString());
+    EXPECT_EQ("[::1]:53", IPSockAddr(IPAddress(IPV6_LOOPBACK), 53).toString());
+    EXPECT_EQ("[fe80::1]:0", IPSockAddr(IPAddress(FE80_1)).toString());
+    EXPECT_EQ("[fe80::2%17]:123", IPSockAddr(IPAddress(FE80_2, 17), 123).toString());
+}
+
+TEST(CompatIPDataTest, ConversionsClearUnneededValues) {
+    const uint32_t idx = 17;
+    const IPSockAddr linkLocalNtpSockaddr(IPAddress(FE80_2, idx), 123);
+    EXPECT_EQ(IPAddress(FE80_2, idx), linkLocalNtpSockaddr.ip());
+    // IPSockAddr(IPSockaddr.ip()) see the port cleared.
+    EXPECT_EQ(0, IPSockAddr(linkLocalNtpSockaddr.ip()).port());
+    const IPPrefix linkLocalPrefix(linkLocalNtpSockaddr.ip(), 64);
+    EXPECT_EQ(IPAddress(FE80, idx), linkLocalPrefix.ip());
+    // IPPrefix(IPPrefix.ip()) see the CIDR length cleared.
+    EXPECT_EQ(IPV6_ADDR_BITS, IPPrefix(linkLocalPrefix.ip()).length());
+}
+
+}  // namespace
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/Log.cpp b/staticlibs/netd/libnetdutils/Log.cpp
new file mode 100644
index 0000000..d2ce98f
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Log.cpp
@@ -0,0 +1,216 @@
+/*
+ * 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.
+ */
+
+#include "netdutils/Log.h"
+#include "netdutils/Slice.h"
+
+#include <chrono>
+#include <ctime>
+#include <iomanip>
+#include <mutex>
+#include <sstream>
+
+#include <android-base/strings.h>
+#include <log/log.h>
+
+using ::android::base::Join;
+using ::android::base::StringPrintf;
+
+namespace android {
+namespace netdutils {
+
+namespace {
+
+std::string makeTimestampedEntry(const std::string& entry) {
+    using ::std::chrono::duration_cast;
+    using ::std::chrono::milliseconds;
+    using ::std::chrono::system_clock;
+
+    std::stringstream tsEntry;
+    const auto now = system_clock::now();
+    const auto time_sec = system_clock::to_time_t(now);
+    tsEntry << std::put_time(std::localtime(&time_sec), "%m-%d %H:%M:%S.") << std::setw(3)
+            << std::setfill('0')
+            << duration_cast<milliseconds>(now - system_clock::from_time_t(time_sec)).count() << " "
+            << entry;
+
+    return tsEntry.str();
+}
+
+}  // namespace
+
+std::string LogEntry::toString() const {
+    std::vector<std::string> text;
+
+    if (!mMsg.empty()) text.push_back(mMsg);
+    if (!mFunc.empty()) {
+        text.push_back(StringPrintf("%s(%s)", mFunc.c_str(), Join(mArgs, ", ").c_str()));
+    }
+    if (!mReturns.empty()) {
+        text.push_back("->");
+        text.push_back(StringPrintf("(%s)", Join(mReturns, ", ").c_str()));
+    }
+    if (!mUid.empty()) text.push_back(mUid);
+    if (!mDuration.empty()) text.push_back(StringPrintf("(%s)", mDuration.c_str()));
+
+    return Join(text, " ");
+}
+
+LogEntry& LogEntry::message(const std::string& message) {
+    mMsg = message;
+    return *this;
+}
+
+LogEntry& LogEntry::function(const std::string& function_name) {
+    mFunc = function_name;
+    return *this;
+}
+
+LogEntry& LogEntry::prettyFunction(const std::string& pretty_function) {
+    // __PRETTY_FUNCTION__ generally seems to be of the form:
+    //
+    //     qualifed::returnType qualified::function(args...)
+    //
+    // where the qualified forms include "(anonymous namespace)" in the
+    // "::"-delimited list and keywords like "virtual" (where applicable).
+    //
+    // Here we try to convert strings like:
+    //
+    //     virtual binder::Status android::net::NetdNativeService::isAlive(bool *)
+    //     netdutils::LogEntry android::netd::(anonymous namespace)::AAA::BBB::function()
+    //
+    // into just "NetdNativeService::isAlive" or "BBB::function". Note that
+    // without imposing convention, how to easily identify any namespace/class
+    // name boundary is not obvious.
+    const size_t endFuncName = pretty_function.rfind('(');
+    const size_t precedingSpace = pretty_function.rfind(' ', endFuncName);
+    size_t substrStart = (precedingSpace != std::string::npos) ? precedingSpace + 1 : 0;
+
+    const size_t beginFuncName = pretty_function.rfind("::", endFuncName);
+    if (beginFuncName != std::string::npos && substrStart < beginFuncName) {
+        const size_t previousNameBoundary = pretty_function.rfind("::", beginFuncName - 1);
+        if (previousNameBoundary < beginFuncName && substrStart < previousNameBoundary) {
+            substrStart = previousNameBoundary + 2;
+        } else {
+            substrStart = beginFuncName + 2;
+        }
+    }
+
+    mFunc = pretty_function.substr(substrStart, endFuncName - substrStart);
+    return *this;
+}
+
+LogEntry& LogEntry::arg(const std::string& val) {
+    mArgs.push_back(val.empty() ? "\"\"" : val);
+    return *this;
+}
+
+template <>
+LogEntry& LogEntry::arg<>(bool val) {
+    mArgs.push_back(val ? "true" : "false");
+    return *this;
+}
+
+LogEntry& LogEntry::arg(const std::vector<int32_t>& val) {
+    mArgs.push_back(StringPrintf("[%s]", Join(val, ", ").c_str()));
+    return *this;
+}
+
+LogEntry& LogEntry::arg(const std::vector<uint8_t>& val) {
+    mArgs.push_back('{' + toHex(makeSlice(val)) + '}');
+    return *this;
+}
+
+LogEntry& LogEntry::arg(const std::vector<std::string>& val) {
+    mArgs.push_back(StringPrintf("[%s]", Join(val, ", ").c_str()));
+    return *this;
+}
+
+LogEntry& LogEntry::returns(const std::string& rval) {
+    mReturns.push_back(rval);
+    return *this;
+}
+
+LogEntry& LogEntry::returns(bool rval) {
+    mReturns.push_back(rval ? "true" : "false");
+    return *this;
+}
+
+LogEntry& LogEntry::returns(const Status& status) {
+    mReturns.push_back(status.msg());
+    return *this;
+}
+
+LogEntry& LogEntry::withUid(uid_t uid) {
+    mUid = StringPrintf("(uid=%d)", uid);
+    return *this;
+}
+
+LogEntry& LogEntry::withAutomaticDuration() {
+    using ms = std::chrono::duration<float, std::ratio<1, 1000>>;
+
+    const std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
+    std::stringstream duration;
+    duration << std::setprecision(1) << std::chrono::duration_cast<ms>(end - mStart).count()
+             << "ms";
+    mDuration = duration.str();
+    return *this;
+}
+
+LogEntry& LogEntry::withDuration(const std::string& duration) {
+    mDuration = duration;
+    return *this;
+}
+
+Log::~Log() {
+    // TODO: dump the last N entries to the android log for possible posterity.
+    info(LogEntry().function(__FUNCTION__));
+}
+
+void Log::forEachEntry(const std::function<void(const std::string&)>& perEntryFn) const {
+    // We make a (potentially expensive) copy of the log buffer (including
+    // all strings), in case the |perEntryFn| takes its sweet time.
+    std::deque<std::string> entries;
+    {
+        std::shared_lock<std::shared_mutex> guard(mLock);
+        entries.assign(mEntries.cbegin(), mEntries.cend());
+    }
+
+    for (const std::string& entry : entries) perEntryFn(entry);
+}
+
+void Log::record(Log::Level lvl, const std::string& entry) {
+    switch (lvl) {
+        case Level::LOG:
+            break;
+        case Level::INFO:
+            ALOG(LOG_INFO, mTag.c_str(), "%s", entry.c_str());
+            break;
+        case Level::WARN:
+            ALOG(LOG_WARN, mTag.c_str(), "%s", entry.c_str());
+            break;
+        case Level::ERROR:
+            ALOG(LOG_ERROR, mTag.c_str(), "%s", entry.c_str());
+            break;
+    }
+
+    std::lock_guard guard(mLock);
+    mEntries.push_back(makeTimestampedEntry(entry));
+    while (mEntries.size() > mMaxEntries) mEntries.pop_front();
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/LogTest.cpp b/staticlibs/netd/libnetdutils/LogTest.cpp
new file mode 100644
index 0000000..1270560
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/LogTest.cpp
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+#include <gtest/gtest.h>
+
+#include "netdutils/Log.h"
+
+android::netdutils::LogEntry globalFunctionName() {
+    return android::netdutils::LogEntry().function(__FUNCTION__);
+}
+
+android::netdutils::LogEntry globalPrettyFunctionName() {
+    return android::netdutils::LogEntry().prettyFunction(__PRETTY_FUNCTION__);
+}
+
+namespace android {
+namespace netdutils {
+
+namespace {
+
+LogEntry functionName() {
+    return LogEntry().function(__FUNCTION__);
+}
+
+LogEntry prettyFunctionName() {
+    return LogEntry().prettyFunction(__PRETTY_FUNCTION__);
+}
+
+}  // namespace
+
+class AAA {
+  public:
+    AAA() = default;
+
+    LogEntry functionName() {
+        return LogEntry().function(__FUNCTION__);
+    }
+
+    LogEntry prettyFunctionName() {
+        return LogEntry().prettyFunction(__PRETTY_FUNCTION__);
+    }
+
+    class BBB {
+      public:
+        BBB() = default;
+
+        LogEntry functionName() {
+            return LogEntry().function(__FUNCTION__);
+        }
+
+        LogEntry prettyFunctionName() {
+            return LogEntry().prettyFunction(__PRETTY_FUNCTION__);
+        }
+    };
+};
+
+TEST(LogEntryTest, Empty) {
+    LogEntry empty;
+    EXPECT_EQ("", empty.toString());
+}
+
+TEST(LogEntryTest, GlobalFunction) {
+    EXPECT_EQ("globalFunctionName()", ::globalFunctionName().toString());
+}
+
+TEST(LogEntryTest, GlobalPrettyFunction) {
+    EXPECT_EQ("globalPrettyFunctionName()", ::globalPrettyFunctionName().toString());
+}
+
+TEST(LogEntryTest, UnnamedNamespaceFunction) {
+    const LogEntry entry = functionName();
+    EXPECT_EQ("functionName()", entry.toString());
+}
+
+TEST(LogEntryTest, UnnamedNamespacePrettyFunction) {
+    const LogEntry entry = prettyFunctionName();
+    EXPECT_EQ("prettyFunctionName()", entry.toString());
+}
+
+TEST(LogEntryTest, ClassFunction) {
+    const LogEntry entry = AAA().functionName();
+    EXPECT_EQ("functionName()", entry.toString());
+}
+
+TEST(LogEntryTest, ClassPrettyFunction) {
+    const LogEntry entry = AAA().prettyFunctionName();
+    EXPECT_EQ("AAA::prettyFunctionName()", entry.toString());
+}
+
+TEST(LogEntryTest, InnerClassFunction) {
+    const LogEntry entry = AAA::BBB().functionName();
+    EXPECT_EQ("functionName()", entry.toString());
+}
+
+TEST(LogEntryTest, InnerClassPrettyFunction) {
+    const LogEntry entry = AAA::BBB().prettyFunctionName();
+    EXPECT_EQ("BBB::prettyFunctionName()", entry.toString());
+}
+
+TEST(LogEntryTest, PrintChainedArguments) {
+    const LogEntry entry = LogEntry()
+            .function("testFunc")
+            .arg("hello")
+            .arg(42)
+            .arg(true);
+    EXPECT_EQ("testFunc(hello, 42, true)", entry.toString());
+}
+
+TEST(LogEntryTest, PrintIntegralTypes) {
+    const LogEntry entry = LogEntry()
+            .function("testFunc")
+            .arg('A')
+            .arg(100U)
+            .arg(-1000LL);
+    EXPECT_EQ("testFunc(65, 100, -1000)", entry.toString());
+}
+
+TEST(LogEntryTest, PrintHex) {
+    const std::vector<uint8_t> buf{0xDE, 0xAD, 0xBE, 0xEF};
+    const LogEntry entry = LogEntry().function("testFunc").arg(buf);
+    EXPECT_EQ("testFunc({deadbeef})", entry.toString());
+}
+
+TEST(LogEntryTest, PrintArgumentPack) {
+    const LogEntry entry = LogEntry().function("testFunc").args("hello", 42, false);
+    EXPECT_EQ("testFunc(hello, 42, false)", entry.toString());
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/MemBlockTest.cpp b/staticlibs/netd/libnetdutils/MemBlockTest.cpp
new file mode 100644
index 0000000..6455a7e
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/MemBlockTest.cpp
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+
+#include <algorithm>
+#include <cstdint>
+#include <utility>
+
+#include <gtest/gtest.h>
+
+#include "netdutils/MemBlock.h"
+#include "netdutils/Slice.h"
+
+namespace android {
+namespace netdutils {
+
+namespace {
+
+constexpr unsigned DNS_PACKET_SIZE = 512;
+constexpr int ARBITRARY_VALUE = 0x55;
+
+MemBlock makeArbitraryMemBlock(size_t len) {
+    MemBlock result(len);
+    // Do some fictional work before returning.
+    for (Slice slice = result.get(); !slice.empty(); slice = drop(slice, 1)) {
+        slice.base()[0] = ARBITRARY_VALUE;
+    }
+    return result;
+}
+
+void checkAllZeros(Slice slice) {
+    for (; !slice.empty(); slice = drop(slice, 1)) {
+        EXPECT_EQ(0U, slice.base()[0]);
+    }
+}
+
+void checkArbitraryMemBlock(const MemBlock& block, size_t expectedSize) {
+    Slice slice = block.get();
+    EXPECT_EQ(expectedSize, slice.size());
+    EXPECT_NE(nullptr, slice.base());
+    for (; !slice.empty(); slice = drop(slice, 1)) {
+        EXPECT_EQ(ARBITRARY_VALUE, slice.base()[0]);
+    }
+}
+
+void checkHelloMello(Slice dest, Slice src) {
+    EXPECT_EQ('h', dest.base()[0]);
+    EXPECT_EQ('e', dest.base()[1]);
+    EXPECT_EQ('l', dest.base()[2]);
+    EXPECT_EQ('l', dest.base()[3]);
+    EXPECT_EQ('o', dest.base()[4]);
+
+    src.base()[0] = 'm';
+    EXPECT_EQ('h', dest.base()[0]);
+}
+
+}  // namespace
+
+TEST(MemBlockTest, Empty) {
+    MemBlock empty;
+    EXPECT_TRUE(empty.get().empty());
+    EXPECT_EQ(nullptr, empty.get().base());
+}
+
+TEST(MemBlockTest, ExplicitZero) {
+    MemBlock zero(0);
+    EXPECT_TRUE(zero.get().empty());
+    EXPECT_EQ(nullptr, zero.get().base());
+}
+
+TEST(MemBlockTest, BasicAllocation) {
+    MemBlock dnsPacket(DNS_PACKET_SIZE);
+    Slice slice = dnsPacket.get();
+    EXPECT_EQ(DNS_PACKET_SIZE, slice.size());
+    // Verify the space is '\0'-initialized.
+    ASSERT_NO_FATAL_FAILURE(checkAllZeros(slice));
+    EXPECT_NE(nullptr, slice.base());
+}
+
+TEST(MemBlockTest, MoveConstruction) {
+    MemBlock block(makeArbitraryMemBlock(DNS_PACKET_SIZE));
+    ASSERT_NO_FATAL_FAILURE(checkArbitraryMemBlock(block, DNS_PACKET_SIZE));
+}
+
+TEST(MemBlockTest, MoveAssignmentOrConstruction) {
+    MemBlock block = makeArbitraryMemBlock(DNS_PACKET_SIZE);
+    ASSERT_NO_FATAL_FAILURE(checkArbitraryMemBlock(block, DNS_PACKET_SIZE));
+}
+
+TEST(MemBlockTest, StdMoveAssignment) {
+    constexpr unsigned SIZE = 10;
+
+    MemBlock block;
+    EXPECT_TRUE(block.get().empty());
+    EXPECT_EQ(nullptr, block.get().base());
+
+    {
+        MemBlock block2 = makeArbitraryMemBlock(SIZE);
+        EXPECT_EQ(SIZE, block2.get().size());
+        // More fictional work.
+        for (unsigned i = 0; i < SIZE; i++) {
+            block2.get().base()[i] = i;
+        }
+        block = std::move(block2);
+    }
+
+    EXPECT_EQ(SIZE, block.get().size());
+    for (unsigned i = 0; i < SIZE; i++) {
+        EXPECT_EQ(i, block.get().base()[i]);
+    }
+}
+
+TEST(MemBlockTest, ConstructionFromSlice) {
+    uint8_t data[] = {'h', 'e', 'l', 'l', 'o'};
+    Slice dataSlice(Slice(data, sizeof(data) / sizeof(data[0])));
+
+    MemBlock dataCopy(dataSlice);
+    ASSERT_NO_FATAL_FAILURE(checkHelloMello(dataCopy.get(), dataSlice));
+}
+
+TEST(MemBlockTest, ImplicitCastToSlice) {
+    uint8_t data[] = {'h', 'e', 'l', 'l', 'o'};
+    Slice dataSlice(Slice(data, sizeof(data) / sizeof(data[0])));
+
+    MemBlock dataCopy(dataSlice.size());
+    // NOTE: no explicit MemBlock::get().
+    // Verify the space is '\0'-initialized.
+    ASSERT_NO_FATAL_FAILURE(checkAllZeros(dataCopy));
+    copy(dataCopy, dataSlice);
+    ASSERT_NO_FATAL_FAILURE(checkHelloMello(dataCopy, dataSlice));
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/Netfilter.cpp b/staticlibs/netd/libnetdutils/Netfilter.cpp
new file mode 100644
index 0000000..bb43de0
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Netfilter.cpp
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <arpa/inet.h>
+#include <linux/netfilter.h>
+#include <linux/netfilter/nfnetlink.h>
+#include <linux/netlink.h>
+#include <ios>
+
+#include "netdutils/Netfilter.h"
+
+std::ostream& operator<<(std::ostream& os, const nfgenmsg& msg) {
+    return os << std::hex << "nfgenmsg["
+              << "family: 0x" << static_cast<int>(msg.nfgen_family) << ", version: 0x"
+              << static_cast<int>(msg.version) << ", res_id: 0x" << ntohs(msg.res_id) << "]"
+              << std::dec;
+}
diff --git a/staticlibs/netd/libnetdutils/Netlink.cpp b/staticlibs/netd/libnetdutils/Netlink.cpp
new file mode 100644
index 0000000..824c0f2
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Netlink.cpp
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <ios>
+#include <linux/netlink.h>
+
+#include "netdutils/Math.h"
+#include "netdutils/Netlink.h"
+
+namespace android {
+namespace netdutils {
+
+void forEachNetlinkMessage(const Slice buf,
+                           const std::function<void(const nlmsghdr&, const Slice)>& onMsg) {
+    Slice tail = buf;
+    while (tail.size() >= sizeof(nlmsghdr)) {
+        nlmsghdr hdr = {};
+        extract(tail, hdr);
+        const auto len = std::max<size_t>(hdr.nlmsg_len, sizeof(hdr));
+        onMsg(hdr, drop(take(tail, len), sizeof(hdr)));
+        tail = drop(tail, align(len, 2));
+    }
+}
+
+void forEachNetlinkAttribute(const Slice buf,
+                             const std::function<void(const nlattr&, const Slice)>& onAttr) {
+    Slice tail = buf;
+    while (tail.size() >= sizeof(nlattr)) {
+        nlattr hdr = {};
+        extract(tail, hdr);
+        const auto len = std::max<size_t>(hdr.nla_len, sizeof(hdr));
+        onAttr(hdr, drop(take(tail, len), sizeof(hdr)));
+        tail = drop(tail, align(len, 2));
+    }
+}
+
+}  // namespace netdutils
+}  // namespace android
+
+bool operator==(const sockaddr_nl& lhs, const sockaddr_nl& rhs) {
+    return (lhs.nl_family == rhs.nl_family) && (lhs.nl_pid == rhs.nl_pid) &&
+           (lhs.nl_groups == rhs.nl_groups);
+}
+
+bool operator!=(const sockaddr_nl& lhs, const sockaddr_nl& rhs) {
+    return !(lhs == rhs);
+}
+
+std::ostream& operator<<(std::ostream& os, const nlmsghdr& hdr) {
+    return os << std::hex << "nlmsghdr["
+              << "len: 0x" << hdr.nlmsg_len << ", type: 0x" << hdr.nlmsg_type << ", flags: 0x"
+              << hdr.nlmsg_flags << ", seq: 0x" << hdr.nlmsg_seq << ", pid: 0x" << hdr.nlmsg_pid
+              << "]" << std::dec;
+}
+
+std::ostream& operator<<(std::ostream& os, const nlattr& attr) {
+    return os << std::hex << "nlattr["
+              << "len: 0x" << attr.nla_len << ", type: 0x" << attr.nla_type << "]" << std::dec;
+}
+
+std::ostream& operator<<(std::ostream& os, const sockaddr_nl& addr) {
+    return os << std::hex << "sockaddr_nl["
+              << "family: " << addr.nl_family << ", pid: " << addr.nl_pid
+              << ", groups: " << addr.nl_groups << "]" << std::dec;
+}
diff --git a/staticlibs/netd/libnetdutils/NetlinkListener.cpp b/staticlibs/netd/libnetdutils/NetlinkListener.cpp
new file mode 100644
index 0000000..decaa9c
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/NetlinkListener.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 "NetlinkListener"
+
+#include <sstream>
+#include <vector>
+
+#include <linux/netfilter/nfnetlink.h>
+
+#include <log/log.h>
+#include <netdutils/Misc.h>
+#include <netdutils/NetlinkListener.h>
+#include <netdutils/Syscalls.h>
+
+namespace android {
+namespace netdutils {
+
+using netdutils::Fd;
+using netdutils::Slice;
+using netdutils::Status;
+using netdutils::UniqueFd;
+using netdutils::findWithDefault;
+using netdutils::forEachNetlinkMessage;
+using netdutils::makeSlice;
+using netdutils::sSyscalls;
+using netdutils::status::ok;
+using netdutils::statusFromErrno;
+
+namespace {
+
+constexpr int kNetlinkMsgErrorType = (NFNL_SUBSYS_NONE << 8) | NLMSG_ERROR;
+
+constexpr sockaddr_nl kKernelAddr = {
+    .nl_family = AF_NETLINK, .nl_pad = 0, .nl_pid = 0, .nl_groups = 0,
+};
+
+const NetlinkListener::DispatchFn kDefaultDispatchFn = [](const nlmsghdr& nlmsg, const Slice) {
+    std::stringstream ss;
+    ss << nlmsg;
+    ALOGE("unhandled netlink message: %s", ss.str().c_str());
+};
+
+}  // namespace
+
+NetlinkListener::NetlinkListener(UniqueFd event, UniqueFd sock, const std::string& name)
+    : mEvent(std::move(event)), mSock(std::move(sock)), mThreadName(name) {
+    const auto rxErrorHandler = [](const nlmsghdr& nlmsg, const Slice msg) {
+        std::stringstream ss;
+        ss << nlmsg << " " << msg << " " << netdutils::toHex(msg, 32);
+        ALOGE("unhandled netlink message: %s", ss.str().c_str());
+    };
+    expectOk(NetlinkListener::subscribe(kNetlinkMsgErrorType, rxErrorHandler));
+
+    mErrorHandler = [& name = mThreadName](const int fd, const int err) {
+        ALOGE("Error on NetlinkListener(%s) fd=%d: %s", name.c_str(), fd, strerror(err));
+    };
+
+    // Start the thread
+    mWorker = std::thread([this]() { run().ignoreError(); });
+}
+
+NetlinkListener::~NetlinkListener() {
+    const auto& sys = sSyscalls.get();
+    const uint64_t data = 1;
+    // eventfd should never enter an error state unexpectedly
+    expectOk(sys.write(mEvent, makeSlice(data)).status());
+    mWorker.join();
+}
+
+Status NetlinkListener::send(const Slice msg) {
+    const auto& sys = sSyscalls.get();
+    ASSIGN_OR_RETURN(auto sent, sys.sendto(mSock, msg, 0, kKernelAddr));
+    if (sent != msg.size()) {
+        return statusFromErrno(EMSGSIZE, "unexpect message size");
+    }
+    return ok;
+}
+
+Status NetlinkListener::subscribe(uint16_t type, const DispatchFn& fn) {
+    std::lock_guard guard(mMutex);
+    mDispatchMap[type] = fn;
+    return ok;
+}
+
+Status NetlinkListener::unsubscribe(uint16_t type) {
+    std::lock_guard guard(mMutex);
+    mDispatchMap.erase(type);
+    return ok;
+}
+
+void NetlinkListener::registerSkErrorHandler(const SkErrorHandler& handler) {
+    mErrorHandler = handler;
+}
+
+Status NetlinkListener::run() {
+    std::vector<char> rxbuf(4096);
+
+    const auto rxHandler = [this](const nlmsghdr& nlmsg, const Slice& buf) {
+        std::lock_guard guard(mMutex);
+        const auto& fn = findWithDefault(mDispatchMap, nlmsg.nlmsg_type, kDefaultDispatchFn);
+        fn(nlmsg, buf);
+    };
+
+    if (mThreadName.length() > 0) {
+        int ret = pthread_setname_np(pthread_self(), mThreadName.c_str());
+        if (ret) {
+            ALOGE("thread name set failed, name: %s, ret: %s", mThreadName.c_str(), strerror(ret));
+        }
+    }
+    const auto& sys = sSyscalls.get();
+    const std::array<Fd, 2> fds{{{mEvent}, {mSock}}};
+    const int events = POLLIN;
+    const double timeout = 3600;
+    while (true) {
+        ASSIGN_OR_RETURN(auto revents, sys.ppoll(fds, events, timeout));
+        // After mEvent becomes readable, we should stop servicing mSock and return
+        if (revents[0] & POLLIN) {
+            break;
+        }
+        if (revents[1] & (POLLIN|POLLERR)) {
+            auto rx = sys.recvfrom(mSock, makeSlice(rxbuf), 0);
+            int err = rx.status().code();
+            if (err) {
+                // Ignore errors. The only error we expect to see here is ENOBUFS, and there's
+                // nothing we can do about that. The recvfrom above will already have cleared the
+                // error indication and ensured we won't get EPOLLERR again.
+                // TODO: Consider using NETLINK_NO_ENOBUFS.
+                mErrorHandler(((Fd) mSock).get(), err);
+                continue;
+            }
+            forEachNetlinkMessage(rx.value(), rxHandler);
+        }
+    }
+    return ok;
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/Slice.cpp b/staticlibs/netd/libnetdutils/Slice.cpp
new file mode 100644
index 0000000..7a07d47
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Slice.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <sstream>
+
+#include "netdutils/Slice.h"
+
+namespace android {
+namespace netdutils {
+namespace {
+
+// Convert one byte to a two character hexadecimal string
+const std::string toHex(uint8_t byte) {
+    const std::array<char, 16> kLookup = {
+        {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}};
+    return {kLookup[byte >> 4], kLookup[byte & 0xf]};
+}
+
+}  // namespace
+
+std::string toString(const Slice s) {
+    return std::string(reinterpret_cast<char*>(s.base()), s.size());
+}
+
+std::string toHex(const Slice s, int wrap) {
+    Slice tail = s;
+    int count = 0;
+    std::stringstream ss;
+    while (!tail.empty()) {
+        uint8_t byte = 0;
+        extract(tail, byte);
+        ss << toHex(byte);
+        if ((++count % wrap) == 0) {
+            ss << "\n";
+        }
+        tail = drop(tail, 1);
+    }
+    return ss.str();
+}
+
+std::ostream& operator<<(std::ostream& os, const Slice& slice) {
+    return os << std::hex << "Slice[base: " << reinterpret_cast<void*>(slice.base())
+              << ", limit: " << reinterpret_cast<void*>(slice.limit()) << ", size: 0x"
+              << slice.size() << "]" << std::dec;
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/SliceTest.cpp b/staticlibs/netd/libnetdutils/SliceTest.cpp
new file mode 100644
index 0000000..a496933
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/SliceTest.cpp
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <array>
+#include <cstdint>
+
+#include <gtest/gtest.h>
+
+#include "netdutils/Slice.h"
+#include "netdutils/Status.h"
+#include "netdutils/StatusOr.h"
+
+namespace android {
+namespace netdutils {
+
+class SliceTest : public testing::Test {
+  protected:
+    std::array<char, 256> mRaw = {};
+};
+
+TEST_F(SliceTest, smoke) {
+    Slice s1 = makeSlice(mRaw);
+    Slice s2 = makeSlice(mRaw);
+    auto p = split(s1, 14);
+    s2 = p.first; // avoid warn-unused error
+    std::stringstream ss;
+    ss << Slice();
+    EXPECT_EQ("Slice[base: 0x0, limit: 0x0, size: 0x0]", ss.str());
+    constexpr size_t kBytes = 14;
+    EXPECT_EQ(s1.base(), take(s1, kBytes).base());
+    EXPECT_EQ(kBytes, take(s1, kBytes).size());
+    EXPECT_EQ(s1.base() + kBytes, drop(s1, kBytes).base());
+    EXPECT_EQ(s1.size() - kBytes, drop(s1, kBytes).size());
+    double a = 0;
+    double b = 0;
+    int c = 0;
+    EXPECT_EQ(sizeof(a), extract(s1, a));
+    EXPECT_EQ(sizeof(a) + sizeof(b), extract(s1, a, b));
+    EXPECT_EQ(sizeof(a) + sizeof(b) + sizeof(c), extract(s1, a, b, c));
+}
+
+TEST_F(SliceTest, constructor) {
+    // Expect the following lines to compile
+    Slice s1 = makeSlice(mRaw);
+    Slice s2(s1);
+    Slice s3 = s2;
+    const Slice s4(s3);
+    const Slice s5 = s4;
+    s3 = s5;
+    Slice s6(mRaw.data(), mRaw.size());
+    Slice s7(mRaw.data(), mRaw.data() + mRaw.size());
+    struct {
+      int a;
+      double b;
+      float c;
+    } anon;
+    makeSlice(anon);
+    EXPECT_EQ(reinterpret_cast<uint8_t*>(mRaw.data()), s1.base());
+    EXPECT_EQ(reinterpret_cast<uint8_t*>(mRaw.data()) + mRaw.size(), s1.limit());
+    EXPECT_EQ(mRaw.size(), s1.size());
+    EXPECT_FALSE(mRaw.empty());
+    EXPECT_TRUE(Slice().empty());
+    EXPECT_TRUE(Slice(nullptr, static_cast<size_t>(0)).empty());
+    EXPECT_TRUE(Slice(nullptr, nullptr).empty());
+}
+
+TEST_F(SliceTest, extract) {
+    struct A {
+        int a, b;
+        bool operator==(const A& other) const { return a == other.a && b == other.b; }
+    };
+    struct B {
+        char str[12];
+        bool b;
+        int i;
+        bool operator==(const B& other) const {
+            return b == other.b && i == other.i && 0 == strncmp(str, other.str, 12);
+        }
+    };
+
+    A origA1 = {1, 2};
+    A origA2 = {3, 4};
+    B origB = {"hello world", true, 1234};
+
+    // Populate buffer for extracting.
+    Slice buffer = makeSlice(mRaw);
+    copy(buffer, makeSlice(origA1));
+    copy(drop(buffer, sizeof(origA1)), makeSlice(origB));
+    copy(drop(buffer, sizeof(origA1) + sizeof(origB)), makeSlice(origA2));
+
+    {
+        // Non-variadic extract
+        A a1{};
+        size_t len = extract(buffer, a1);
+        EXPECT_EQ(sizeof(A), len);
+        EXPECT_EQ(origA1, a1);
+    }
+
+    {
+        // Variadic extract, 2 destinations
+        A a1{};
+        B b{};
+        size_t len = extract(buffer, a1, b);
+        EXPECT_EQ(sizeof(A) + sizeof(B), len);
+        EXPECT_EQ(origA1, a1);
+        EXPECT_EQ(origB, b);
+    }
+
+    {
+        // Variadic extract, 3 destinations
+        A a1{}, a2{};
+        B b{};
+        size_t len = extract(buffer, a1, b, a2);
+        EXPECT_EQ(2 * sizeof(A) + sizeof(B), len);
+        EXPECT_EQ(origA1, a1);
+        EXPECT_EQ(origB, b);
+        EXPECT_EQ(origA2, a2);
+    }
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/Socket.cpp b/staticlibs/netd/libnetdutils/Socket.cpp
new file mode 100644
index 0000000..e962b6e
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Socket.cpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <arpa/inet.h>
+
+#include "netdutils/Slice.h"
+#include "netdutils/Socket.h"
+
+namespace android {
+namespace netdutils {
+
+StatusOr<std::string> toString(const in6_addr& addr) {
+    std::array<char, INET6_ADDRSTRLEN> out = {};
+    auto* rv = inet_ntop(AF_INET6, &addr, out.data(), out.size());
+    if (rv == nullptr) {
+        return statusFromErrno(errno, "inet_ntop() failed");
+    }
+    return std::string(out.data());
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/SocketOption.cpp b/staticlibs/netd/libnetdutils/SocketOption.cpp
new file mode 100644
index 0000000..023df6e
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/SocketOption.cpp
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+#include "netdutils/SocketOption.h"
+
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <sys/socket.h>
+#include <utility>
+
+#include "netdutils/Syscalls.h"
+
+namespace android {
+namespace netdutils {
+
+Status enableSockopt(Fd sock, int level, int optname) {
+    auto& sys = sSyscalls.get();
+    const int on = 1;
+    return sys.setsockopt(sock, level, optname, &on, sizeof(on));
+}
+
+Status enableTcpKeepAlives(Fd sock, unsigned idleTime, unsigned numProbes, unsigned probeInterval) {
+    RETURN_IF_NOT_OK(enableSockopt(sock, SOL_SOCKET, SO_KEEPALIVE));
+
+    auto& sys = sSyscalls.get();
+    if (idleTime != 0) {
+        RETURN_IF_NOT_OK(sys.setsockopt(sock, SOL_TCP, TCP_KEEPIDLE, &idleTime, sizeof(idleTime)));
+    }
+    if (numProbes != 0) {
+        RETURN_IF_NOT_OK(sys.setsockopt(sock, SOL_TCP, TCP_KEEPCNT, &numProbes, sizeof(numProbes)));
+    }
+    if (probeInterval != 0) {
+        RETURN_IF_NOT_OK(sys.setsockopt(sock, SOL_TCP, TCP_KEEPINTVL, &probeInterval,
+                sizeof(probeInterval)));
+    }
+
+    return status::ok;
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/Status.cpp b/staticlibs/netd/libnetdutils/Status.cpp
new file mode 100644
index 0000000..acd8f11
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Status.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "netdutils/Status.h"
+
+#include <sstream>
+
+#include "android-base/stringprintf.h"
+
+namespace android {
+namespace netdutils {
+
+Status statusFromErrno(int err, const std::string& msg) {
+    return Status(err, base::StringPrintf("[%s] : %s", strerror(err), msg.c_str()));
+}
+
+bool equalToErrno(const Status& status, int err) {
+    return status.code() == err;
+}
+
+std::string toString(const Status& status) {
+    std::stringstream ss;
+    ss << status;
+    return ss.str();
+}
+
+std::ostream& operator<<(std::ostream& os, const Status& s) {
+    return os << "Status[code: " << s.code() << ", msg: \"" << s.msg() << "\"]";
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/StatusTest.cpp b/staticlibs/netd/libnetdutils/StatusTest.cpp
new file mode 100644
index 0000000..4cfc3bb
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/StatusTest.cpp
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "netdutils/Status.h"
+#include "netdutils/StatusOr.h"
+
+#include <sstream>
+
+#include <gtest/gtest.h>
+
+namespace android {
+namespace netdutils {
+namespace {
+
+TEST(StatusTest, valueSemantics) {
+    // Default constructor
+    EXPECT_EQ(status::ok, Status());
+
+    // Copy constructor
+    Status status1(1);
+    Status status2(status1);  // NOLINT(performance-unnecessary-copy-initialization)
+    EXPECT_EQ(1, status2.code());
+
+    // Copy assignment
+    Status status3;
+    status3 = status2;
+    EXPECT_EQ(1, status3.code());
+
+    // Same with const objects
+    const Status status4(4);
+    const Status status5(status4);  // NOLINT(performance-unnecessary-copy-initialization)
+    Status status6;
+    status6 = status5;
+    EXPECT_EQ(4, status6.code());
+}
+
+TEST(StatusTest, errorMessages) {
+    Status s(42, "for tea too");
+    EXPECT_EQ(42, s.code());
+    EXPECT_FALSE(s.ok());
+    EXPECT_EQ(s.msg(), "for tea too");
+}
+
+TEST(StatusOrTest, moveSemantics) {
+    // Status objects should be cheaply movable.
+    EXPECT_TRUE(std::is_nothrow_move_constructible<Status>::value);
+    EXPECT_TRUE(std::is_nothrow_move_assignable<Status>::value);
+
+    // Should move from a temporary Status (twice)
+    Status s(Status(Status(42, "move me")));
+    EXPECT_EQ(42, s.code());
+    EXPECT_EQ(s.msg(), "move me");
+
+    Status s2(666, "EDAEMON");
+    EXPECT_NE(s, s2);
+    s = s2;  // Invokes the move-assignment operator.
+    EXPECT_EQ(666, s.code());
+    EXPECT_EQ(s.msg(), "EDAEMON");
+    EXPECT_EQ(s, s2);
+
+    // A moved-from Status can be re-used.
+    s2 = s;
+
+    // Now both objects are valid.
+    EXPECT_EQ(666, s.code());
+    EXPECT_EQ(s.msg(), "EDAEMON");
+    EXPECT_EQ(s, s2);
+}
+
+TEST(StatusTest, ignoredStatus) {
+    statusFromErrno(ENOTTY, "Not a typewriter, what did you expect?").ignoreError();
+}
+
+TEST(StatusOrTest, ostream) {
+    {
+      StatusOr<int> so(11);
+      std::stringstream ss;
+      ss << so;
+      // TODO: Fix StatusOr to optionally output "value:".
+      EXPECT_EQ("StatusOr[status: Status[code: 0, msg: \"\"]]", ss.str());
+    }
+    {
+      StatusOr<int> err(status::undefined);
+      std::stringstream ss;
+      ss << err;
+      EXPECT_EQ("StatusOr[status: Status[code: 2147483647, msg: \"undefined\"]]", ss.str());
+    }
+}
+
+}  // namespace
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/Syscalls.cpp b/staticlibs/netd/libnetdutils/Syscalls.cpp
new file mode 100644
index 0000000..7e1a242
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Syscalls.cpp
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "netdutils/Syscalls.h"
+
+#include <atomic>
+#include <type_traits>
+#include <utility>
+
+namespace android {
+namespace netdutils {
+namespace {
+
+// Retry syscall fn as long as it returns -1 with errno == EINTR
+template <typename FnT, typename... Params>
+typename std::invoke_result<FnT, Params...>::type syscallRetry(FnT fn, Params&&... params) {
+    auto rv = fn(std::forward<Params>(params)...);
+    while ((rv == -1) && (errno == EINTR)) {
+        rv = fn(std::forward<Params>(params)...);
+    }
+    return rv;
+}
+
+}  // namespace
+
+// Production implementation of Syscalls that forwards to libc syscalls.
+class RealSyscalls final : public Syscalls {
+  public:
+    ~RealSyscalls() override = default;
+
+    StatusOr<UniqueFd> open(const std::string& pathname, int flags, mode_t mode) const override {
+        UniqueFd fd(::open(pathname.c_str(), flags, mode));
+        if (!isWellFormed(fd)) {
+            return statusFromErrno(errno, "open(\"" + pathname + "\"...) failed");
+        }
+        return fd;
+    }
+
+    StatusOr<UniqueFd> socket(int domain, int type, int protocol) const override {
+        UniqueFd sock(::socket(domain, type, protocol));
+        if (!isWellFormed(sock)) {
+            return statusFromErrno(errno, "socket() failed");
+        }
+        return sock;
+    }
+
+    Status getsockname(Fd sock, sockaddr* addr, socklen_t* addrlen) const override {
+        auto rv = ::getsockname(sock.get(), addr, addrlen);
+        if (rv == -1) {
+            return statusFromErrno(errno, "getsockname() failed");
+        }
+        return status::ok;
+    }
+
+    Status getsockopt(Fd sock, int level, int optname, void* optval,
+                      socklen_t* optlen) const override {
+        auto rv = ::getsockopt(sock.get(), level, optname, optval, optlen);
+        if (rv == -1) {
+            return statusFromErrno(errno, "getsockopt() failed");
+        }
+        return status::ok;
+    }
+
+    Status setsockopt(Fd sock, int level, int optname, const void* optval,
+                      socklen_t optlen) const override {
+        auto rv = ::setsockopt(sock.get(), level, optname, optval, optlen);
+        if (rv == -1) {
+            return statusFromErrno(errno, "setsockopt() failed");
+        }
+        return status::ok;
+    }
+
+    Status bind(Fd sock, const sockaddr* addr, socklen_t addrlen) const override {
+        auto rv = ::bind(sock.get(), addr, addrlen);
+        if (rv == -1) {
+            return statusFromErrno(errno, "bind() failed");
+        }
+        return status::ok;
+    }
+
+    Status connect(Fd sock, const sockaddr* addr, socklen_t addrlen) const override {
+        auto rv = syscallRetry(::connect, sock.get(), addr, addrlen);
+        if (rv == -1) {
+            return statusFromErrno(errno, "connect() failed");
+        }
+        return status::ok;
+    }
+
+    StatusOr<ifreq> ioctl(Fd sock, unsigned long request, ifreq* ifr) const override {
+        auto rv = ::ioctl(sock.get(), request, ifr);
+        if (rv == -1) {
+            return statusFromErrno(errno, "ioctl() failed");
+        }
+        return *ifr;
+    }
+
+    StatusOr<UniqueFd> eventfd(unsigned int initval, int flags) const override {
+        UniqueFd fd(::eventfd(initval, flags));
+        if (!isWellFormed(fd)) {
+            return statusFromErrno(errno, "eventfd() failed");
+        }
+        return fd;
+    }
+
+    StatusOr<int> ppoll(pollfd* fds, nfds_t nfds, double timeout) const override {
+        timespec ts = {};
+        ts.tv_sec = timeout;
+        ts.tv_nsec = (timeout - ts.tv_sec) * 1e9;
+        auto rv = syscallRetry(::ppoll, fds, nfds, &ts, nullptr);
+        if (rv == -1) {
+            return statusFromErrno(errno, "ppoll() failed");
+        }
+        return rv;
+    }
+
+    StatusOr<size_t> writev(Fd fd, const std::vector<iovec>& iov) const override {
+        auto rv = syscallRetry(::writev, fd.get(), iov.data(), iov.size());
+        if (rv == -1) {
+            return statusFromErrno(errno, "writev() failed");
+        }
+        return rv;
+    }
+
+    StatusOr<size_t> write(Fd fd, const Slice buf) const override {
+        auto rv = syscallRetry(::write, fd.get(), buf.base(), buf.size());
+        if (rv == -1) {
+            return statusFromErrno(errno, "write() failed");
+        }
+        return static_cast<size_t>(rv);
+    }
+
+    StatusOr<Slice> read(Fd fd, const Slice buf) const override {
+        auto rv = syscallRetry(::read, fd.get(), buf.base(), buf.size());
+        if (rv == -1) {
+            return statusFromErrno(errno, "read() failed");
+        }
+        return Slice(buf.base(), rv);
+    }
+
+    StatusOr<size_t> sendto(Fd sock, const Slice buf, int flags, const sockaddr* dst,
+                            socklen_t dstlen) const override {
+        auto rv = syscallRetry(::sendto, sock.get(), buf.base(), buf.size(), flags, dst, dstlen);
+        if (rv == -1) {
+            return statusFromErrno(errno, "sendto() failed");
+        }
+        return static_cast<size_t>(rv);
+    }
+
+    StatusOr<Slice> recvfrom(Fd sock, const Slice dst, int flags, sockaddr* src,
+                             socklen_t* srclen) const override {
+        auto rv = syscallRetry(::recvfrom, sock.get(), dst.base(), dst.size(), flags, src, srclen);
+        if (rv == -1) {
+            return statusFromErrno(errno, "recvfrom() failed");
+        }
+        if (rv == 0) {
+            return status::eof;
+        }
+        return take(dst, rv);
+    }
+
+    Status shutdown(Fd fd, int how) const override {
+        auto rv = ::shutdown(fd.get(), how);
+        if (rv == -1) {
+            return statusFromErrno(errno, "shutdown() failed");
+        }
+        return status::ok;
+    }
+
+    Status close(Fd fd) const override {
+        auto rv = ::close(fd.get());
+        if (rv == -1) {
+            return statusFromErrno(errno, "close() failed");
+        }
+        return status::ok;
+    }
+
+    StatusOr<UniqueFile> fopen(const std::string& path, const std::string& mode) const override {
+        UniqueFile file(::fopen(path.c_str(), mode.c_str()));
+        if (file == nullptr) {
+            return statusFromErrno(errno, "fopen(\"" + path + "\", \"" + mode + "\") failed");
+        }
+        return file;
+    }
+
+    StatusOr<pid_t> fork() const override {
+        pid_t rv = ::fork();
+        if (rv == -1) {
+            return statusFromErrno(errno, "fork() failed");
+        }
+        return rv;
+    }
+
+    StatusOr<int> vfprintf(FILE* file, const char* format, va_list ap) const override {
+        auto rv = ::vfprintf(file, format, ap);
+        if (rv == -1) {
+            return statusFromErrno(errno, "vfprintf() failed");
+        }
+        return rv;
+    }
+
+    StatusOr<int> vfscanf(FILE* file, const char* format, va_list ap) const override {
+        auto rv = ::vfscanf(file, format, ap);
+        if (rv == -1) {
+            return statusFromErrno(errno, "vfscanf() failed");
+        }
+        return rv;
+    }
+
+    Status fclose(FILE* file) const override {
+        auto rv = ::fclose(file);
+        if (rv == -1) {
+            return statusFromErrno(errno, "fclose() failed");
+        }
+        return status::ok;
+    }
+};
+
+SyscallsHolder::~SyscallsHolder() {
+    delete &get();
+}
+
+Syscalls& SyscallsHolder::get() {
+    while (true) {
+        // memory_order_relaxed gives the compiler and hardware more
+        // freedom. If we get a stale value (this should only happen
+        // early in the execution of a program) the exchange code below
+        // will loop until we get the most current value.
+        auto* syscalls = mSyscalls.load(std::memory_order_relaxed);
+        // Common case returns existing syscalls
+        if (syscalls) {
+            return *syscalls;
+        }
+
+        // This code will execute on first get()
+        std::unique_ptr<Syscalls> tmp(new RealSyscalls());
+        Syscalls* expected = nullptr;
+        bool success = mSyscalls.compare_exchange_strong(expected, tmp.get());
+        if (success) {
+            // Ownership was transferred to mSyscalls already, must release()
+            return *tmp.release();
+        }
+    }
+}
+
+Syscalls& SyscallsHolder::swap(Syscalls& syscalls) {
+    return *mSyscalls.exchange(&syscalls);
+}
+
+SyscallsHolder sSyscalls;
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/SyscallsTest.cpp b/staticlibs/netd/libnetdutils/SyscallsTest.cpp
new file mode 100644
index 0000000..78ffab5
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/SyscallsTest.cpp
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <array>
+#include <cstdint>
+#include <memory>
+
+#include <gtest/gtest.h>
+
+#include "netdutils/Handle.h"
+#include "netdutils/Math.h"
+#include "netdutils/MockSyscalls.h"
+#include "netdutils/Netfilter.h"
+#include "netdutils/Netlink.h"
+#include "netdutils/Slice.h"
+#include "netdutils/Status.h"
+#include "netdutils/StatusOr.h"
+#include "netdutils/Syscalls.h"
+
+using testing::_;
+using testing::ByMove;
+using testing::Invoke;
+using testing::Return;
+using testing::StrictMock;
+
+namespace android {
+namespace netdutils {
+
+class SyscallsTest : public testing::Test {
+  protected:
+    StrictMock<ScopedMockSyscalls> mSyscalls;
+};
+
+TEST(syscalls, scopedMock) {
+    auto& old = sSyscalls.get();
+    {
+        StrictMock<ScopedMockSyscalls> s;
+        EXPECT_EQ(&s, &sSyscalls.get());
+    }
+    EXPECT_EQ(&old, &sSyscalls.get());
+}
+
+TEST_F(SyscallsTest, open) {
+    const char kPath[] = "/test/path/please/ignore";
+    constexpr Fd kFd(40);
+    constexpr int kFlags = 883;
+    constexpr mode_t kMode = 37373;
+    const auto& sys = sSyscalls.get();
+    EXPECT_CALL(mSyscalls, open(kPath, kFlags, kMode)).WillOnce(Return(ByMove(UniqueFd(kFd))));
+    EXPECT_CALL(mSyscalls, close(kFd)).WillOnce(Return(status::ok));
+    auto result = sys.open(kPath, kFlags, kMode);
+    EXPECT_EQ(status::ok, result.status());
+    EXPECT_EQ(kFd, result.value());
+}
+
+TEST_F(SyscallsTest, getsockname) {
+    constexpr Fd kFd(40);
+    sockaddr_nl expected = {};
+    auto& sys = sSyscalls.get();
+
+    // Success
+    EXPECT_CALL(mSyscalls, getsockname(kFd, _, _))
+        .WillOnce(Invoke([expected](Fd, sockaddr* addr, socklen_t* addrlen) {
+            memcpy(addr, &expected, sizeof(expected));
+            EXPECT_EQ(*addrlen, static_cast<socklen_t>(sizeof(expected)));
+            return status::ok;
+        }));
+    const auto result = sys.getsockname<sockaddr_nl>(kFd);
+    EXPECT_TRUE(isOk(result));
+    EXPECT_EQ(expected, result.value());
+
+    // Failure
+    const Status kError = statusFromErrno(EINVAL, "test");
+    EXPECT_CALL(mSyscalls, getsockname(kFd, _, _)).WillOnce(Return(kError));
+    EXPECT_EQ(kError, sys.getsockname<sockaddr_nl>(kFd).status());
+}
+
+TEST_F(SyscallsTest, setsockopt) {
+    constexpr Fd kFd(40);
+    constexpr int kLevel = 50;
+    constexpr int kOptname = 70;
+    sockaddr_nl expected = {};
+    auto& sys = sSyscalls.get();
+
+    // Success
+    EXPECT_CALL(mSyscalls, setsockopt(kFd, kLevel, kOptname, &expected, sizeof(expected)))
+        .WillOnce(Return(status::ok));
+    EXPECT_EQ(status::ok, sys.setsockopt(kFd, kLevel, kOptname, expected));
+
+    // Failure
+    const Status kError = statusFromErrno(EINVAL, "test");
+    EXPECT_CALL(mSyscalls, setsockopt(kFd, kLevel, kOptname, &expected, sizeof(expected)))
+        .WillOnce(Return(kError));
+    EXPECT_EQ(kError, sys.setsockopt(kFd, kLevel, kOptname, expected));
+}
+
+TEST_F(SyscallsTest, getsockopt) {
+    constexpr Fd kFd(40);
+    constexpr int kLevel = 50;
+    constexpr int kOptname = 70;
+    sockaddr_nl expected = {};
+    socklen_t optLen = 0;
+    auto& sys = sSyscalls.get();
+
+    // Success
+    EXPECT_CALL(mSyscalls, getsockopt(kFd, kLevel, kOptname, &expected, &optLen))
+        .WillOnce(Return(status::ok));
+    EXPECT_EQ(status::ok, sys.getsockopt(kFd, kLevel, kOptname, &expected, &optLen));
+
+    // Failure
+    const Status kError = statusFromErrno(EINVAL, "test");
+    EXPECT_CALL(mSyscalls, getsockopt(kFd, kLevel, kOptname, &expected, &optLen))
+        .WillOnce(Return(kError));
+    EXPECT_EQ(kError, sys.getsockopt(kFd, kLevel, kOptname, &expected, &optLen));
+}
+
+TEST_F(SyscallsTest, bind) {
+    constexpr Fd kFd(40);
+    sockaddr_nl expected = {};
+    auto& sys = sSyscalls.get();
+
+    // Success
+    EXPECT_CALL(mSyscalls, bind(kFd, asSockaddrPtr(&expected), sizeof(expected)))
+        .WillOnce(Return(status::ok));
+    EXPECT_EQ(status::ok, sys.bind(kFd, expected));
+
+    // Failure
+    const Status kError = statusFromErrno(EINVAL, "test");
+    EXPECT_CALL(mSyscalls, bind(kFd, asSockaddrPtr(&expected), sizeof(expected)))
+        .WillOnce(Return(kError));
+    EXPECT_EQ(kError, sys.bind(kFd, expected));
+}
+
+TEST_F(SyscallsTest, connect) {
+    constexpr Fd kFd(40);
+    sockaddr_nl expected = {};
+    auto& sys = sSyscalls.get();
+
+    // Success
+    EXPECT_CALL(mSyscalls, connect(kFd, asSockaddrPtr(&expected), sizeof(expected)))
+        .WillOnce(Return(status::ok));
+    EXPECT_EQ(status::ok, sys.connect(kFd, expected));
+
+    // Failure
+    const Status kError = statusFromErrno(EINVAL, "test");
+    EXPECT_CALL(mSyscalls, connect(kFd, asSockaddrPtr(&expected), sizeof(expected)))
+        .WillOnce(Return(kError));
+    EXPECT_EQ(kError, sys.connect(kFd, expected));
+}
+
+TEST_F(SyscallsTest, sendto) {
+    constexpr Fd kFd(40);
+    constexpr int kFlags = 0;
+    std::array<char, 10> payload;
+    const auto slice = makeSlice(payload);
+    sockaddr_nl expected = {};
+    auto& sys = sSyscalls.get();
+
+    // Success
+    EXPECT_CALL(mSyscalls, sendto(kFd, slice, kFlags, asSockaddrPtr(&expected), sizeof(expected)))
+        .WillOnce(Return(slice.size()));
+    EXPECT_EQ(status::ok, sys.sendto(kFd, slice, kFlags, expected));
+}
+
+TEST_F(SyscallsTest, recvfrom) {
+    constexpr Fd kFd(40);
+    constexpr int kFlags = 0;
+    std::array<char, 10> payload;
+    const auto dst = makeSlice(payload);
+    const auto used = take(dst, 8);
+    sockaddr_nl expected = {};
+    auto& sys = sSyscalls.get();
+
+    // Success
+    EXPECT_CALL(mSyscalls, recvfrom(kFd, dst, kFlags, _, _))
+            .WillOnce(Invoke(
+                    [expected, used](Fd, const Slice, int, sockaddr* src, socklen_t* srclen) {
+                        *srclen = sizeof(expected);
+                        memcpy(src, &expected, *srclen);
+                        return used;
+                    }));
+    auto result = sys.recvfrom<sockaddr_nl>(kFd, dst, kFlags);
+    EXPECT_EQ(status::ok, result.status());
+    EXPECT_EQ(used, result.value().first);
+    EXPECT_EQ(expected, result.value().second);
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/ThreadUtilTest.cpp b/staticlibs/netd/libnetdutils/ThreadUtilTest.cpp
new file mode 100644
index 0000000..8fad8b8
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/ThreadUtilTest.cpp
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <string>
+
+#include <android-base/expected.h>
+#include <gtest/gtest.h>
+#include <netdutils/ThreadUtil.h>
+
+namespace android::netdutils {
+
+namespace {
+
+android::base::expected<std::string, int> getThreadName() {
+    char name[16] = {};
+    if (const int ret = pthread_getname_np(pthread_self(), name, sizeof(name)); ret != 0) {
+        return android::base::unexpected(ret);
+    }
+    return std::string(name);
+}
+
+class NoopRun {
+  public:
+    explicit NoopRun(const std::string& name = "") : mName(name) { instanceNum++; }
+
+    // Destructor happens in the thread.
+    ~NoopRun() {
+        if (checkName) {
+            auto expected = getThreadName();
+            EXPECT_TRUE(expected.has_value());
+            EXPECT_EQ(mExpectedName, expected.value());
+        }
+        instanceNum--;
+    }
+
+    void run() {}
+
+    std::string threadName() { return mName; }
+
+    // Set the expected thread name which will be used to check if it matches the actual thread
+    // name which is returned from the system call. The check will happen in the destructor.
+    void setExpectedName(const std::string& expectedName) {
+        checkName = true;
+        mExpectedName = expectedName;
+    }
+
+    static bool waitForAllReleased(int timeoutMs) {
+        constexpr int intervalMs = 20;
+        int limit = timeoutMs / intervalMs;
+        for (int i = 1; i < limit; i++) {
+            if (instanceNum == 0) {
+                return true;
+            }
+            usleep(intervalMs * 1000);
+        }
+        return false;
+    }
+
+    // To track how many instances are alive.
+    static std::atomic<int> instanceNum;
+
+  private:
+    std::string mName;
+    std::string mExpectedName;
+    bool checkName = false;
+};
+
+std::atomic<int> NoopRun::instanceNum;
+
+}  // namespace
+
+TEST(ThreadUtilTest, objectReleased) {
+    NoopRun::instanceNum = 0;
+    NoopRun* obj = new NoopRun();
+    EXPECT_EQ(1, NoopRun::instanceNum);
+    threadLaunch(obj);
+
+    // Wait for the object released along with the thread exited.
+    EXPECT_TRUE(NoopRun::waitForAllReleased(1000));
+    EXPECT_EQ(0, NoopRun::instanceNum);
+}
+
+TEST(ThreadUtilTest, SetThreadName) {
+    NoopRun::instanceNum = 0;
+
+    // Test thread name empty.
+    NoopRun* obj1 = new NoopRun();
+    obj1->setExpectedName("");
+
+    // Test normal case.
+    NoopRun* obj2 = new NoopRun("TestName");
+    obj2->setExpectedName("TestName");
+
+    // Test thread name too long.
+    std::string name("TestNameTooooLong");
+    NoopRun* obj3 = new NoopRun(name);
+    obj3->setExpectedName(name.substr(0, 15));
+
+    // Thread names are examined in their destructors.
+    EXPECT_EQ(3, NoopRun::instanceNum);
+    threadLaunch(obj1);
+    threadLaunch(obj2);
+    threadLaunch(obj3);
+
+    EXPECT_TRUE(NoopRun::waitForAllReleased(1000));
+    EXPECT_EQ(0, NoopRun::instanceNum);
+}
+
+}  // namespace android::netdutils
diff --git a/staticlibs/netd/libnetdutils/UniqueFd.cpp b/staticlibs/netd/libnetdutils/UniqueFd.cpp
new file mode 100644
index 0000000..1cb30ed
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/UniqueFd.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <algorithm>
+
+#include "netdutils/UniqueFd.h"
+#include "netdutils/Syscalls.h"
+
+namespace android {
+namespace netdutils {
+
+void UniqueFd::reset(Fd fd) {
+    auto& sys = sSyscalls.get();
+    std::swap(fd, mFd);
+    if (isWellFormed(fd)) {
+        expectOk(sys.close(fd));
+    }
+}
+
+std::ostream& operator<<(std::ostream& os, const UniqueFd& fd) {
+    return os << "UniqueFd[" << static_cast<Fd>(fd) << "]";
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/UniqueFile.cpp b/staticlibs/netd/libnetdutils/UniqueFile.cpp
new file mode 100644
index 0000000..21e8779
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/UniqueFile.cpp
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <algorithm>
+
+#include "netdutils/Syscalls.h"
+#include "netdutils/UniqueFile.h"
+
+namespace android {
+namespace netdutils {
+
+void UniqueFileDtor::operator()(FILE* file) const {
+    const auto& sys = sSyscalls.get();
+    sys.fclose(file).ignoreError();
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/Utils.cpp b/staticlibs/netd/libnetdutils/Utils.cpp
new file mode 100644
index 0000000..9b0b3e0
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Utils.cpp
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+#include <map>
+#include <vector>
+
+#include <net/if.h>
+
+#include "dirent.h"
+#include "netdutils/Status.h"
+#include "netdutils/Utils.h"
+
+namespace android {
+namespace netdutils {
+
+StatusOr<std::vector<std::string>> getIfaceNames() {
+    std::vector<std::string> ifaceNames;
+    DIR* d;
+    struct dirent* de;
+
+    if (!(d = opendir("/sys/class/net"))) {
+        return statusFromErrno(errno, "Cannot open iface directory");
+    }
+    while ((de = readdir(d))) {
+        if ((de->d_type != DT_DIR) && (de->d_type != DT_LNK)) continue;
+        if (de->d_name[0] == '.') continue;
+        ifaceNames.push_back(std::string(de->d_name));
+    }
+    closedir(d);
+    return ifaceNames;
+}
+
+StatusOr<std::map<std::string, uint32_t>> getIfaceList() {
+    std::map<std::string, uint32_t> ifacePairs;
+
+    ASSIGN_OR_RETURN(auto ifaceNames, getIfaceNames());
+
+    for (const auto& name : ifaceNames) {
+        uint32_t ifaceIndex = if_nametoindex(name.c_str());
+        if (ifaceIndex) {
+            ifacePairs.insert(std::pair<std::string, uint32_t>(name, ifaceIndex));
+        }
+    }
+    return ifacePairs;
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/BackoffSequence.h b/staticlibs/netd/libnetdutils/include/netdutils/BackoffSequence.h
new file mode 100644
index 0000000..a52e72d
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/BackoffSequence.h
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+
+#ifndef NETDUTILS_BACKOFFSEQUENCE_H
+#define NETDUTILS_BACKOFFSEQUENCE_H
+
+#include <stdint.h>
+#include <algorithm>
+#include <chrono>
+#include <limits>
+
+namespace android {
+namespace netdutils {
+
+// Encapsulate some RFC 3315 section 14 -style backoff mechanics.
+//
+//     https://tools.ietf.org/html/rfc3315#section-14
+template<typename time_type = std::chrono::seconds, typename counter_type = uint32_t>
+class BackoffSequence {
+  public:
+    struct Parameters {
+        time_type initialRetransTime{TIME_UNITY};
+        counter_type maxRetransCount{0U};
+        time_type maxRetransTime{TIME_ZERO};
+        time_type maxRetransDuration{TIME_ZERO};
+        time_type endOfSequenceIndicator{TIME_ZERO};
+    };
+
+    BackoffSequence() : BackoffSequence(Parameters{}) {}
+    BackoffSequence(const BackoffSequence &) = default;
+    BackoffSequence(BackoffSequence &&) = default;
+    BackoffSequence& operator=(const BackoffSequence &) = default;
+    BackoffSequence& operator=(BackoffSequence &&) = default;
+
+    bool hasNextTimeout() const noexcept {
+        return !maxRetransCountExceed() && !maxRetransDurationExceeded();
+    }
+
+    // Returns 0 when the sequence is exhausted.
+    time_type getNextTimeout() {
+        if (!hasNextTimeout()) return getEndOfSequenceIndicator();
+
+        mRetransTime = getNextTimeoutAfter(mRetransTime);
+
+        mRetransCount++;
+        mTotalRetransDuration += mRetransTime;
+        return mRetransTime;
+    }
+
+    time_type getEndOfSequenceIndicator() const noexcept {
+        return mParams.endOfSequenceIndicator;
+    }
+
+    class Builder {
+      public:
+        Builder() {}
+
+        constexpr Builder& withInitialRetransmissionTime(time_type irt) {
+            mParams.initialRetransTime = irt;
+            return *this;
+        }
+        constexpr Builder& withMaximumRetransmissionCount(counter_type mrc) {
+            mParams.maxRetransCount = mrc;
+            return *this;
+        }
+        constexpr Builder& withMaximumRetransmissionTime(time_type mrt) {
+            mParams.maxRetransTime = mrt;
+            return *this;
+        }
+        constexpr Builder& withMaximumRetransmissionDuration(time_type mrd) {
+            mParams.maxRetransDuration = mrd;
+            return *this;
+        }
+        constexpr Builder& withEndOfSequenceIndicator(time_type eos) {
+            mParams.endOfSequenceIndicator = eos;
+            return *this;
+        }
+
+        constexpr BackoffSequence build() const {
+            return BackoffSequence(mParams);
+        }
+
+      private:
+        Parameters mParams;
+    };
+
+  private:
+    static constexpr int PER_ITERATION_SCALING_FACTOR = 2;
+    static constexpr time_type TIME_ZERO = time_type();
+    static constexpr time_type TIME_UNITY = time_type(1);
+
+    constexpr BackoffSequence(const struct Parameters &params)
+            : mParams(params),
+              mRetransCount(0),
+              mRetransTime(TIME_ZERO),
+              mTotalRetransDuration(TIME_ZERO) {}
+
+    constexpr bool maxRetransCountExceed() const {
+        return (mParams.maxRetransCount > 0) && (mRetransCount >= mParams.maxRetransCount);
+    }
+
+    constexpr bool maxRetransDurationExceeded() const {
+        return (mParams.maxRetransDuration > TIME_ZERO) &&
+               (mTotalRetransDuration >= mParams.maxRetransDuration);
+    }
+
+    time_type getNextTimeoutAfter(time_type lastTimeout) const {
+        // TODO: Support proper random jitter. Also, consider supporting some
+        // per-iteration scaling factor other than doubling.
+        time_type nextTimeout = (lastTimeout > TIME_ZERO)
+                ? PER_ITERATION_SCALING_FACTOR * lastTimeout
+                : mParams.initialRetransTime;
+
+        // Check if overflow occurred.
+        if (nextTimeout < lastTimeout) {
+            nextTimeout = std::numeric_limits<time_type>::max();
+        }
+
+        // Cap to maximum allowed, if necessary.
+        if (mParams.maxRetransTime > TIME_ZERO) {
+            nextTimeout = std::min(nextTimeout, mParams.maxRetransTime);
+        }
+
+        // Don't overflow the maximum total duration.
+        if (mParams.maxRetransDuration > TIME_ZERO) {
+            nextTimeout = std::min(nextTimeout, mParams.maxRetransDuration - lastTimeout);
+        }
+        return nextTimeout;
+    }
+
+    const Parameters mParams;
+    counter_type mRetransCount;
+    time_type mRetransTime;
+    time_type mTotalRetransDuration;
+};
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETDUTILS_BACKOFFSEQUENCE_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/DumpWriter.h b/staticlibs/netd/libnetdutils/include/netdutils/DumpWriter.h
new file mode 100644
index 0000000..a50b5e6
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/DumpWriter.h
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+#ifndef NETDUTILS_DUMPWRITER_H_
+#define NETDUTILS_DUMPWRITER_H_
+
+#include <string>
+
+namespace android {
+namespace netdutils {
+
+class DumpWriter {
+  public:
+    DumpWriter(int fd);
+
+    void incIndent();
+    void decIndent();
+
+    void println(const std::string& line);
+    template <size_t n>
+    void println(const char line[n]) {
+        println(std::string(line));
+    }
+    // Hint to the compiler that it should apply printf validation of
+    // arguments (beginning at position 3) of the format (specified in
+    // position 2). Note that position 1 is the implicit "this" argument.
+    void println(const char* fmt, ...) __attribute__((__format__(__printf__, 2, 3)));
+    void blankline() { println(""); }
+
+  private:
+    uint8_t mIndentLevel;
+    int mFd;
+};
+
+class ScopedIndent {
+  public:
+    ScopedIndent() = delete;
+    ScopedIndent(const ScopedIndent&) = delete;
+    ScopedIndent(ScopedIndent&&) = delete;
+    explicit ScopedIndent(DumpWriter& dw) : mDw(dw) { mDw.incIndent(); }
+    ~ScopedIndent() { mDw.decIndent(); }
+    ScopedIndent& operator=(const ScopedIndent&) = delete;
+    ScopedIndent& operator=(ScopedIndent&&) = delete;
+
+    // TODO: consider additional {inc,dec}Indent methods and a counter that
+    // can be used to unwind all pending increments on exit.
+
+  private:
+    DumpWriter& mDw;
+};
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif  // NETDUTILS_DUMPWRITER_H_
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Fd.h b/staticlibs/netd/libnetdutils/include/netdutils/Fd.h
new file mode 100644
index 0000000..7db4087
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Fd.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_FD_H
+#define NETUTILS_FD_H
+
+#include <ostream>
+
+#include "netdutils/Status.h"
+
+namespace android {
+namespace netdutils {
+
+// Strongly typed wrapper for file descriptors with value semantics.
+// This class should typically hold unowned file descriptors.
+class Fd {
+  public:
+    constexpr Fd() = default;
+
+    constexpr Fd(int fd) : mFd(fd) {}
+
+    int get() const { return mFd; }
+
+    bool operator==(const Fd& other) const { return get() == other.get(); }
+    bool operator!=(const Fd& other) const { return get() != other.get(); }
+
+  private:
+    int mFd = -1;
+};
+
+// Return true if fd appears valid (non-negative)
+inline bool isWellFormed(const Fd fd) {
+    return fd.get() >= 0;
+}
+
+std::ostream& operator<<(std::ostream& os, const Fd& fd);
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_FD_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Handle.h b/staticlibs/netd/libnetdutils/include/netdutils/Handle.h
new file mode 100644
index 0000000..82083d4
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Handle.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_HANDLE_H
+#define NETUTILS_HANDLE_H
+
+#include <ostream>
+
+namespace android {
+namespace netdutils {
+
+// Opaque, strongly typed wrapper for integer-like handles.
+// Explicitly avoids implementing arithmetic operations.
+//
+// This class is intended to avoid common errors when reordering
+// arguments to functions, typos and other cases where plain integer
+// types would silently cover up the mistake.
+//
+// usage:
+// DEFINE_HANDLE(ProductId, uint64_t);
+// DEFINE_HANDLE(ThumbnailHash, uint64_t);
+// void foo(ProductId p, ThumbnailHash th) {...}
+//
+// void test() {
+//     ProductId p(88);
+//     ThumbnailHash th1(100), th2(200);
+//
+//     foo(p, th1);        <- ok!
+//     foo(th1, p);        <- disallowed!
+//     th1 += 10;          <- disallowed!
+//     p = th2;            <- disallowed!
+//     assert(th1 != th2); <- ok!
+// }
+template <typename T, typename TagT>
+class Handle {
+  public:
+    constexpr Handle() = default;
+    constexpr Handle(const T& value) : mValue(value) {}
+
+    const T get() const { return mValue; }
+
+    bool operator==(const Handle& that) const { return get() == that.get(); }
+    bool operator!=(const Handle& that) const { return get() != that.get(); }
+
+  private:
+    T mValue;
+};
+
+#define DEFINE_HANDLE(name, type) \
+    struct _##name##Tag {};       \
+    using name = ::android::netdutils::Handle<type, _##name##Tag>;
+
+template <typename T, typename TagT>
+inline std::ostream& operator<<(std::ostream& os, const Handle<T, TagT>& handle) {
+    return os << handle.get();
+}
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_HANDLE_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
new file mode 100644
index 0000000..d662739
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
@@ -0,0 +1,332 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <netdb.h>
+#include <netinet/in.h>
+#include <stdint.h>
+#include <cstring>
+#include <limits>
+#include <memory>
+#include <string>
+
+#include "netdutils/NetworkConstants.h"
+
+namespace android {
+namespace netdutils {
+
+namespace internal_ {
+
+// A structure to hold data for dealing with Internet addresses (IPAddress) and
+// related types such as IPSockAddr and IPPrefix.
+struct compact_ipdata {
+    uint8_t family{AF_UNSPEC};
+    uint8_t cidrlen{0U};  // written and read in host-byte order
+    in_port_t port{0U};   // written and read in host-byte order
+    uint32_t scope_id{0U};
+    union {
+        in_addr v4;
+        in6_addr v6;
+    } ip{.v6 = IN6ADDR_ANY_INIT};  // written and read in network-byte order
+
+    // Classes that use compact_ipdata and this method should be sure to clear
+    // (i.e. zero or make uniform) any fields not relevant to the class.
+    friend bool operator==(const compact_ipdata& a, const compact_ipdata& b) {
+        if ((a.family != b.family) || (a.cidrlen != b.cidrlen) || (a.port != b.port) ||
+            (a.scope_id != b.scope_id)) {
+            return false;
+        }
+        switch (a.family) {
+            case AF_UNSPEC:
+                // After the above checks, two AF_UNSPEC objects can be
+                // considered equal, for convenience.
+                return true;
+            case AF_INET: {
+                const in_addr v4a = a.ip.v4;
+                const in_addr v4b = b.ip.v4;
+                return (v4a.s_addr == v4b.s_addr);
+            }
+            case AF_INET6: {
+                const in6_addr v6a = a.ip.v6;
+                const in6_addr v6b = b.ip.v6;
+                return IN6_ARE_ADDR_EQUAL(&v6a, &v6b);
+            }
+        }
+        return false;
+    }
+
+    // Classes that use compact_ipdata and this method should be sure to clear
+    // (i.e. zero or make uniform) any fields not relevant to the class.
+    friend bool operator!=(const compact_ipdata& a, const compact_ipdata& b) { return !(a == b); }
+
+    // Classes that use compact_ipdata and this method should be sure to clear
+    // (i.e. zero or make uniform) any fields not relevant to the class.
+    friend bool operator<(const compact_ipdata& a, const compact_ipdata& b) {
+        if (a.family != b.family) return (a.family < b.family);
+        switch (a.family) {
+            case AF_INET: {
+                const in_addr v4a = a.ip.v4;
+                const in_addr v4b = b.ip.v4;
+                if (v4a.s_addr != v4b.s_addr) return (ntohl(v4a.s_addr) < ntohl(v4b.s_addr));
+                break;
+            }
+            case AF_INET6: {
+                const in6_addr v6a = a.ip.v6;
+                const in6_addr v6b = b.ip.v6;
+                const int cmp = std::memcmp(v6a.s6_addr, v6b.s6_addr, IPV6_ADDR_LEN);
+                if (cmp != 0) return cmp < 0;
+                break;
+            }
+        }
+        if (a.cidrlen != b.cidrlen) return (a.cidrlen < b.cidrlen);
+        if (a.port != b.port) return (a.port < b.port);
+        return (a.scope_id < b.scope_id);
+    }
+};
+
+static_assert(AF_UNSPEC <= std::numeric_limits<uint8_t>::max(), "AF_UNSPEC value too large");
+static_assert(AF_INET <= std::numeric_limits<uint8_t>::max(), "AF_INET value too large");
+static_assert(AF_INET6 <= std::numeric_limits<uint8_t>::max(), "AF_INET6 value too large");
+static_assert(sizeof(compact_ipdata) == 24U, "compact_ipdata unexpectedly large");
+
+}  // namespace internal_
+
+struct AddrinfoDeleter {
+    void operator()(struct addrinfo* p) const {
+        if (p != nullptr) {
+            freeaddrinfo(p);
+        }
+    }
+};
+
+typedef std::unique_ptr<struct addrinfo, struct AddrinfoDeleter> ScopedAddrinfo;
+
+inline bool usesScopedIds(const in6_addr& ipv6) {
+    return (IN6_IS_ADDR_LINKLOCAL(&ipv6) || IN6_IS_ADDR_MC_LINKLOCAL(&ipv6));
+}
+
+class IPPrefix;
+class IPSockAddr;
+
+class IPAddress {
+  public:
+    static bool forString(const std::string& repr, IPAddress* ip);
+    static IPAddress forString(const std::string& repr) {
+        IPAddress ip;
+        if (!forString(repr, &ip)) return IPAddress();
+        return ip;
+    }
+
+    IPAddress() = default;
+    IPAddress(const IPAddress&) = default;
+    IPAddress(IPAddress&&) = default;
+
+    explicit IPAddress(const in_addr& ipv4)
+        : mData({AF_INET, IPV4_ADDR_BITS, 0U, 0U, {.v4 = ipv4}}) {}
+    explicit IPAddress(const in6_addr& ipv6)
+        : mData({AF_INET6, IPV6_ADDR_BITS, 0U, 0U, {.v6 = ipv6}}) {}
+    IPAddress(const in6_addr& ipv6, uint32_t scope_id)
+        : mData({AF_INET6,
+                 IPV6_ADDR_BITS,
+                 0U,
+                 // Sanity check: scoped_ids only for link-local addresses.
+                 usesScopedIds(ipv6) ? scope_id : 0U,
+                 {.v6 = ipv6}}) {}
+    IPAddress(const IPAddress& ip, uint32_t scope_id) : IPAddress(ip) {
+        mData.scope_id = (family() == AF_INET6 && usesScopedIds(mData.ip.v6)) ? scope_id : 0U;
+    }
+
+    IPAddress& operator=(const IPAddress&) = default;
+    IPAddress& operator=(IPAddress&&) = default;
+
+    constexpr sa_family_t family() const noexcept { return mData.family; }
+    constexpr uint32_t scope_id() const noexcept { return mData.scope_id; }
+
+    std::string toString() const noexcept;
+
+    friend std::ostream& operator<<(std::ostream& os, const IPAddress& ip) {
+        os << ip.toString();
+        return os;
+    }
+    friend bool operator==(const IPAddress& a, const IPAddress& b) { return (a.mData == b.mData); }
+    friend bool operator!=(const IPAddress& a, const IPAddress& b) { return (a.mData != b.mData); }
+    friend bool operator<(const IPAddress& a, const IPAddress& b) { return (a.mData < b.mData); }
+    friend bool operator>(const IPAddress& a, const IPAddress& b) { return (b.mData < a.mData); }
+    friend bool operator<=(const IPAddress& a, const IPAddress& b) { return (a < b) || (a == b); }
+    friend bool operator>=(const IPAddress& a, const IPAddress& b) { return (b < a) || (a == b); }
+
+  private:
+    friend class IPPrefix;
+    friend class IPSockAddr;
+
+    explicit IPAddress(const internal_::compact_ipdata& ipdata) : mData(ipdata) {
+        mData.port = 0U;
+        switch (mData.family) {
+            case AF_INET:
+                mData.cidrlen = IPV4_ADDR_BITS;
+                mData.scope_id = 0U;
+                break;
+            case AF_INET6:
+                mData.cidrlen = IPV6_ADDR_BITS;
+                if (usesScopedIds(ipdata.ip.v6)) mData.scope_id = ipdata.scope_id;
+                break;
+            default:
+                mData.cidrlen = 0U;
+                mData.scope_id = 0U;
+                break;
+        }
+    }
+
+    internal_::compact_ipdata mData{};
+};
+
+class IPPrefix {
+  public:
+    static bool forString(const std::string& repr, IPPrefix* prefix);
+    static IPPrefix forString(const std::string& repr) {
+        IPPrefix prefix;
+        if (!forString(repr, &prefix)) return IPPrefix();
+        return prefix;
+    }
+
+    IPPrefix() = default;
+    IPPrefix(const IPPrefix&) = default;
+    IPPrefix(IPPrefix&&) = default;
+
+    explicit IPPrefix(const IPAddress& ip) : mData(ip.mData) {}
+
+    // Truncate the IP address |ip| at length |length|. Lengths greater than
+    // the address-family-relevant maximum, along with negative values, are
+    // interpreted as if the address-family-relevant maximum had been given.
+    IPPrefix(const IPAddress& ip, int length);
+
+    IPPrefix& operator=(const IPPrefix&) = default;
+    IPPrefix& operator=(IPPrefix&&) = default;
+
+    constexpr sa_family_t family() const noexcept { return mData.family; }
+    IPAddress ip() const noexcept { return IPAddress(mData); }
+    in_addr addr4() const noexcept { return mData.ip.v4; }
+    in6_addr addr6() const noexcept { return mData.ip.v6; }
+    constexpr int length() const noexcept { return mData.cidrlen; }
+    bool contains(const IPPrefix& other) {
+        return length() <= other.length() && IPPrefix(other.ip(), length()).ip() == ip();
+    }
+    bool contains(const IPAddress& other) {
+        return IPPrefix(other, length()).ip() == ip();
+    }
+
+    bool isUninitialized() const noexcept;
+    std::string toString() const noexcept;
+
+    friend std::ostream& operator<<(std::ostream& os, const IPPrefix& prefix) {
+        os << prefix.toString();
+        return os;
+    }
+    friend bool operator==(const IPPrefix& a, const IPPrefix& b) { return (a.mData == b.mData); }
+    friend bool operator!=(const IPPrefix& a, const IPPrefix& b) { return (a.mData != b.mData); }
+    friend bool operator<(const IPPrefix& a, const IPPrefix& b) { return (a.mData < b.mData); }
+    friend bool operator>(const IPPrefix& a, const IPPrefix& b) { return (b.mData < a.mData); }
+    friend bool operator<=(const IPPrefix& a, const IPPrefix& b) { return (a < b) || (a == b); }
+    friend bool operator>=(const IPPrefix& a, const IPPrefix& b) { return (b < a) || (a == b); }
+
+  private:
+    internal_::compact_ipdata mData{};
+};
+
+// An Internet socket address.
+//
+// Cannot represent other types of socket addresses (e.g. UNIX socket address, et cetera).
+class IPSockAddr {
+  public:
+    // TODO: static forString
+
+    static IPSockAddr toIPSockAddr(const std::string& repr, in_port_t port) {
+        return IPSockAddr(IPAddress::forString(repr), port);
+    }
+    static IPSockAddr toIPSockAddr(const sockaddr& sa) {
+        switch (sa.sa_family) {
+            case AF_INET:
+                return IPSockAddr(*reinterpret_cast<const sockaddr_in*>(&sa));
+            case AF_INET6:
+                return IPSockAddr(*reinterpret_cast<const sockaddr_in6*>(&sa));
+            default:
+                return IPSockAddr();
+        }
+    }
+    static IPSockAddr toIPSockAddr(const sockaddr_storage& ss) {
+        return toIPSockAddr(*reinterpret_cast<const sockaddr*>(&ss));
+    }
+
+    IPSockAddr() = default;
+    IPSockAddr(const IPSockAddr&) = default;
+    IPSockAddr(IPSockAddr&&) = default;
+
+    explicit IPSockAddr(const IPAddress& ip) : mData(ip.mData) {}
+    IPSockAddr(const IPAddress& ip, in_port_t port) : mData(ip.mData) { mData.port = port; }
+    explicit IPSockAddr(const sockaddr_in& ipv4sa)
+        : IPSockAddr(IPAddress(ipv4sa.sin_addr), ntohs(ipv4sa.sin_port)) {}
+    explicit IPSockAddr(const sockaddr_in6& ipv6sa)
+        : IPSockAddr(IPAddress(ipv6sa.sin6_addr, ipv6sa.sin6_scope_id), ntohs(ipv6sa.sin6_port)) {}
+
+    IPSockAddr& operator=(const IPSockAddr&) = default;
+    IPSockAddr& operator=(IPSockAddr&&) = default;
+
+    constexpr sa_family_t family() const noexcept { return mData.family; }
+    IPAddress ip() const noexcept { return IPAddress(mData); }
+    constexpr in_port_t port() const noexcept { return mData.port; }
+
+    // Implicit conversion to sockaddr_storage.
+    operator sockaddr_storage() const noexcept {
+        sockaddr_storage ss;
+        ss.ss_family = mData.family;
+        switch (mData.family) {
+            case AF_INET:
+                reinterpret_cast<sockaddr_in*>(&ss)->sin_addr = mData.ip.v4;
+                reinterpret_cast<sockaddr_in*>(&ss)->sin_port = htons(mData.port);
+                break;
+            case AF_INET6:
+                reinterpret_cast<sockaddr_in6*>(&ss)->sin6_addr = mData.ip.v6;
+                reinterpret_cast<sockaddr_in6*>(&ss)->sin6_port = htons(mData.port);
+                reinterpret_cast<sockaddr_in6*>(&ss)->sin6_scope_id = mData.scope_id;
+                break;
+        }
+        return ss;
+    }
+
+    std::string toString() const noexcept;
+
+    friend std::ostream& operator<<(std::ostream& os, const IPSockAddr& prefix) {
+        os << prefix.toString();
+        return os;
+    }
+    friend bool operator==(const IPSockAddr& a, const IPSockAddr& b) {
+        return (a.mData == b.mData);
+    }
+    friend bool operator!=(const IPSockAddr& a, const IPSockAddr& b) {
+        return (a.mData != b.mData);
+    }
+    friend bool operator<(const IPSockAddr& a, const IPSockAddr& b) { return (a.mData < b.mData); }
+    friend bool operator>(const IPSockAddr& a, const IPSockAddr& b) { return (b.mData < a.mData); }
+    friend bool operator<=(const IPSockAddr& a, const IPSockAddr& b) { return (a < b) || (a == b); }
+    friend bool operator>=(const IPSockAddr& a, const IPSockAddr& b) { return (b < a) || (a == b); }
+
+  private:
+    internal_::compact_ipdata mData{};
+};
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Log.h b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
new file mode 100644
index 0000000..2de5ed7
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
@@ -0,0 +1,212 @@
+/*
+ * 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.
+ */
+
+#ifndef NETUTILS_LOG_H
+#define NETUTILS_LOG_H
+
+#include <chrono>
+#include <deque>
+#include <functional>
+#include <shared_mutex>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+#include <android-base/stringprintf.h>
+#include <android-base/thread_annotations.h>
+
+#include <netdutils/Status.h>
+
+namespace android {
+namespace netdutils {
+
+class LogEntry {
+  public:
+    LogEntry() = default;
+    LogEntry(const LogEntry&) = default;
+    LogEntry(LogEntry&&) = default;
+    ~LogEntry() = default;
+    LogEntry& operator=(const LogEntry&) = default;
+    LogEntry& operator=(LogEntry&&) = default;
+
+    std::string toString() const;
+
+    ///
+    // Helper methods that make it easy to build up a LogEntry message.
+    // If performance becomes a factor the implementations could be inlined.
+    ///
+    LogEntry& message(const std::string& message);
+
+    // For calling with __FUNCTION__.
+    LogEntry& function(const std::string& function_name);
+    // For calling with __PRETTY_FUNCTION__.
+    LogEntry& prettyFunction(const std::string& pretty_function);
+
+    // Convenience methods for each of the common types of function arguments.
+    LogEntry& arg(const std::string& val);
+    // Intended for binary buffers, formats as hex
+    LogEntry& arg(const std::vector<uint8_t>& val);
+    LogEntry& arg(const std::vector<int32_t>& val);
+    LogEntry& arg(const std::vector<std::string>& val);
+    template <typename IntT, typename = std::enable_if_t<std::is_arithmetic_v<IntT>>>
+    LogEntry& arg(IntT val) {
+        mArgs.push_back(std::to_string(val));
+        return *this;
+    }
+    // Not using a plain overload here to avoid the implicit conversion from
+    // any pointer to bool, which causes string literals to print as 'true'.
+    template <>
+    LogEntry& arg<>(bool val);
+
+    template <typename... Args>
+    LogEntry& args(const Args&... a) {
+        // Cleverness ahead: we throw away the initializer_list filled with
+        // zeroes, all we care about is calling arg() for each argument.
+        (void) std::initializer_list<int>{(arg(a), 0)...};
+        return *this;
+    }
+
+    // Some things can return more than one value, or have multiple output
+    // parameters, so each of these adds to the mReturns vector.
+    LogEntry& returns(const std::string& rval);
+    LogEntry& returns(const Status& status);
+    LogEntry& returns(bool rval);
+    template <class T>
+    LogEntry& returns(T val) {
+        mReturns.push_back(std::to_string(val));
+        return *this;
+    }
+
+    LogEntry& withUid(uid_t uid);
+
+    // Append the duration computed since the creation of this instance.
+    LogEntry& withAutomaticDuration();
+    // Append the string-ified duration computed by some other means.
+    LogEntry& withDuration(const std::string& duration);
+
+  private:
+    std::chrono::steady_clock::time_point mStart = std::chrono::steady_clock::now();
+    std::string mMsg{};
+    std::string mFunc{};
+    std::vector<std::string> mArgs{};
+    std::vector<std::string> mReturns{};
+    std::string mUid{};
+    std::string mDuration{};
+};
+
+class Log {
+  public:
+    Log() = delete;
+    Log(const std::string& tag) : Log(tag, MAX_ENTRIES) {}
+    Log(const std::string& tag, size_t maxEntries) : mTag(tag), mMaxEntries(maxEntries) {}
+    Log(const Log&) = delete;
+    Log(Log&&) = delete;
+    ~Log();
+    Log& operator=(const Log&) = delete;
+    Log& operator=(Log&&) = delete;
+
+    LogEntry newEntry() const { return LogEntry(); }
+
+    // Record a log entry in internal storage only.
+    void log(const std::string& entry) { record(Level::LOG, entry); }
+    template <size_t n>
+    void log(const char entry[n]) { log(std::string(entry)); }
+    void log(const LogEntry& entry) { log(entry.toString()); }
+    void log(const char* fmt, ...) __attribute__((__format__(__printf__, 2, 3))) {
+        using ::android::base::StringAppendV;
+        std::string result;
+        va_list ap;
+        va_start(ap, fmt);
+        StringAppendV(&result, fmt, ap);
+        va_end(ap);
+        log(result);
+    }
+
+    // Record a log entry in internal storage and to ALOGI as well.
+    void info(const std::string& entry) { record(Level::INFO, entry); }
+    template <size_t n>
+    void info(const char entry[n]) { info(std::string(entry)); }
+    void info(const LogEntry& entry) { info(entry.toString()); }
+    void info(const char* fmt, ...) __attribute__((__format__(__printf__, 2, 3))) {
+        using ::android::base::StringAppendV;
+        std::string result;
+        va_list ap;
+        va_start(ap, fmt);
+        StringAppendV(&result, fmt, ap);
+        va_end(ap);
+        info(result);
+    }
+
+    // Record a log entry in internal storage and to ALOGW as well.
+    void warn(const std::string& entry) { record(Level::WARN, entry); }
+    template <size_t n>
+    void warn(const char entry[n]) { warn(std::string(entry)); }
+    void warn(const LogEntry& entry) { warn(entry.toString()); }
+    void warn(const char* fmt, ...) __attribute__((__format__(__printf__, 2, 3))) {
+        using ::android::base::StringAppendV;
+        std::string result;
+        va_list ap;
+        va_start(ap, fmt);
+        StringAppendV(&result, fmt, ap);
+        va_end(ap);
+        warn(result);
+    }
+
+    // Record a log entry in internal storage and to ALOGE as well.
+    void error(const std::string& entry) { record(Level::ERROR, entry); }
+    template <size_t n>
+    void error(const char entry[n]) { error(std::string(entry)); }
+    void error(const LogEntry& entry) { error(entry.toString()); }
+    void error(const char* fmt, ...) __attribute__((__format__(__printf__, 2, 3))) {
+        using ::android::base::StringAppendV;
+        std::string result;
+        va_list ap;
+        va_start(ap, fmt);
+        StringAppendV(&result, fmt, ap);
+        va_end(ap);
+        error(result);
+    }
+
+    // Iterates over every entry in the log in chronological order. Operates
+    // on a copy of the log entries, and so perEntryFn may itself call one of
+    // the logging functions if needed.
+    void forEachEntry(const std::function<void(const std::string&)>& perEntryFn) const;
+
+  private:
+    static constexpr const size_t MAX_ENTRIES = 750U;
+    const std::string mTag;
+    const size_t mMaxEntries;
+
+    // The LOG level adds an entry to mEntries but does not output the message
+    // to the system log. All other levels append to mEntries and output to the
+    // the system log.
+    enum class Level {
+        LOG,
+        INFO,
+        WARN,
+        ERROR,
+    };
+
+    void record(Level lvl, const std::string& entry);
+
+    mutable std::shared_mutex mLock;
+    std::deque<std::string> mEntries;  // GUARDED_BY(mLock), when supported
+};
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_LOG_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Math.h b/staticlibs/netd/libnetdutils/include/netdutils/Math.h
new file mode 100644
index 0000000..c41fbf5
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Math.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_MATH_H
+#define NETUTILS_MATH_H
+
+#include <algorithm>
+#include <cstdint>
+
+namespace android {
+namespace netdutils {
+
+template <class T>
+inline constexpr const T mask(const int shift) {
+    return (1 << shift) - 1;
+}
+
+// Align x up to the nearest integer multiple of 2^shift
+template <class T>
+inline constexpr const T align(const T& x, const int shift) {
+    return (x + mask<T>(shift)) & ~mask<T>(shift);
+}
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_MATH_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/MemBlock.h b/staticlibs/netd/libnetdutils/include/netdutils/MemBlock.h
new file mode 100644
index 0000000..fd4d612
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/MemBlock.h
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+#ifndef NETUTILS_MEMBLOCK_H
+#define NETUTILS_MEMBLOCK_H
+
+#include <memory>
+#include "netdutils/Slice.h"
+
+namespace android {
+namespace netdutils {
+
+// A class to encapsulate self-deleting byte arrays while preserving access
+// to the underlying length (without the length being part of the type, e.g.
+// std::array<>). By design, the only interface to the underlying bytes is
+// via Slice, to encourage safer memory access usage.
+//
+// No thread-safety guarantees whatsoever.
+class MemBlock {
+  public:
+    MemBlock() : MemBlock(0U) {}
+    explicit MemBlock(size_t len)
+            : mData((len > 0U) ? new uint8_t[len]{} : nullptr),
+              mLen(len) {}
+    // Allocate memory of size src.size() and copy src into this MemBlock.
+    explicit MemBlock(Slice src) : MemBlock(src.size()) {
+        copy(get(), src);
+    }
+
+    // No copy construction or assignment.
+    MemBlock(const MemBlock&) = delete;
+    MemBlock& operator=(const MemBlock&) = delete;
+
+    // Move construction and assignment are okay.
+    MemBlock(MemBlock&&) = default;
+    MemBlock& operator=(MemBlock&&) = default;
+
+    // Even though this method is const, the memory wrapped by the
+    // returned Slice is mutable.
+    Slice get() const noexcept { return Slice(mData.get(), mLen); }
+
+    // Implicit cast to Slice.
+    // NOLINTNEXTLINE(google-explicit-constructor)
+    operator const Slice() const noexcept { return get(); }
+
+  private:
+    std::unique_ptr<uint8_t[]> mData;
+    size_t mLen;
+};
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_MEMBLOCK_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Misc.h b/staticlibs/netd/libnetdutils/include/netdutils/Misc.h
new file mode 100644
index 0000000..d344f81
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Misc.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_MISC_H
+#define NETUTILS_MISC_H
+
+#include <map>
+
+namespace android {
+namespace netdutils {
+
+// Lookup key in map, returing a default value if key is not found
+template <typename U, typename V>
+inline const V& findWithDefault(const std::map<U, V>& map, const U& key, const V& dflt) {
+    auto it = map.find(key);
+    return (it == map.end()) ? dflt : it->second;
+}
+
+// Movable, copiable, scoped lambda (or std::function) runner. Useful
+// for running arbitrary cleanup or logging code when exiting a scope.
+//
+// Compare to defer in golang.
+template <typename FnT>
+class Cleanup {
+  public:
+    Cleanup() = delete;
+    explicit Cleanup(FnT fn) : mFn(fn) {}
+    ~Cleanup() { if (!mReleased) mFn(); }
+
+    void release() { mReleased = true; }
+
+  private:
+    bool mReleased{false};
+    FnT mFn;
+};
+
+// Helper to make a new Cleanup. Avoids complex or impossible syntax
+// when wrapping lambdas.
+//
+// Usage:
+// auto cleanup = makeCleanup([](){ your_code_here; });
+template <typename FnT>
+Cleanup<FnT> makeCleanup(FnT fn) {
+    return Cleanup<FnT>(fn);
+}
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_MISC_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/MockSyscalls.h b/staticlibs/netd/libnetdutils/include/netdutils/MockSyscalls.h
new file mode 100644
index 0000000..f57b55c
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/MockSyscalls.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_MOCK_SYSCALLS_H
+#define NETUTILS_MOCK_SYSCALLS_H
+
+#include <atomic>
+#include <cassert>
+#include <memory>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "netdutils/Syscalls.h"
+
+namespace android {
+namespace netdutils {
+
+class MockSyscalls : public Syscalls {
+  public:
+    virtual ~MockSyscalls() = default;
+    // Use Return(ByMove(...)) to deal with movable return types.
+    MOCK_CONST_METHOD3(open,
+                       StatusOr<UniqueFd>(const std::string& pathname, int flags, mode_t mode));
+    MOCK_CONST_METHOD3(socket, StatusOr<UniqueFd>(int domain, int type, int protocol));
+    MOCK_CONST_METHOD3(getsockname, Status(Fd sock, sockaddr* addr, socklen_t* addrlen));
+    MOCK_CONST_METHOD5(getsockopt, Status(Fd sock, int level, int optname, void* optval,
+                                          socklen_t *optlen));
+    MOCK_CONST_METHOD5(setsockopt, Status(Fd sock, int level, int optname, const void* optval,
+                                          socklen_t optlen));
+
+    MOCK_CONST_METHOD3(bind, Status(Fd sock, const sockaddr* addr, socklen_t addrlen));
+    MOCK_CONST_METHOD3(connect, Status(Fd sock, const sockaddr* addr, socklen_t addrlen));
+    MOCK_CONST_METHOD3(ioctl, StatusOr<ifreq>(Fd sock, unsigned long request, ifreq* ifr));
+
+    // Use Return(ByMove(...)) to deal with movable return types.
+    MOCK_CONST_METHOD2(eventfd, StatusOr<UniqueFd>(unsigned int initval, int flags));
+    MOCK_CONST_METHOD3(ppoll, StatusOr<int>(pollfd* fds, nfds_t nfds, double timeout));
+
+    MOCK_CONST_METHOD2(writev, StatusOr<size_t>(Fd fd, const std::vector<iovec>& iov));
+    MOCK_CONST_METHOD2(write, StatusOr<size_t>(Fd fd, const Slice buf));
+    MOCK_CONST_METHOD2(read, StatusOr<Slice>(Fd fd, const Slice buf));
+    MOCK_CONST_METHOD5(sendto, StatusOr<size_t>(Fd sock, const Slice buf, int flags,
+                                                const sockaddr* dst, socklen_t dstlen));
+    MOCK_CONST_METHOD5(recvfrom, StatusOr<Slice>(Fd sock, const Slice dst, int flags, sockaddr* src,
+                                                 socklen_t* srclen));
+    MOCK_CONST_METHOD2(shutdown, Status(Fd fd, int how));
+    MOCK_CONST_METHOD1(close, Status(Fd fd));
+
+    MOCK_CONST_METHOD2(fopen,
+                       StatusOr<UniqueFile>(const std::string& path, const std::string& mode));
+    MOCK_CONST_METHOD3(vfprintf, StatusOr<int>(FILE* file, const char* format, va_list ap));
+    MOCK_CONST_METHOD3(vfscanf, StatusOr<int>(FILE* file, const char* format, va_list ap));
+    MOCK_CONST_METHOD1(fclose, Status(FILE* file));
+    MOCK_CONST_METHOD0(fork, StatusOr<pid_t>());
+};
+
+// For the lifetime of this mock, replace the contents of sSyscalls
+// with a pointer to this mock. Behavior is undefined if multiple
+// ScopedMockSyscalls instances exist concurrently.
+class ScopedMockSyscalls : public MockSyscalls {
+  public:
+    ScopedMockSyscalls() : mOld(sSyscalls.swap(*this)) { assert((mRefcount++) == 1); }
+    virtual ~ScopedMockSyscalls() {
+        sSyscalls.swap(mOld);
+        assert((mRefcount--) == 0);
+    }
+
+  private:
+    std::atomic<int> mRefcount{0};
+    Syscalls& mOld;
+};
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_MOCK_SYSCALLS_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/NetNativeTestBase.h b/staticlibs/netd/libnetdutils/include/netdutils/NetNativeTestBase.h
new file mode 100644
index 0000000..c8b30c6
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/NetNativeTestBase.h
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ *
+ */
+#pragma once
+
+#include <android-base/format.h>
+#include <android-base/logging.h>
+#include "gtest/gtest.h"
+
+using ::testing::TestInfo;
+using ::testing::UnitTest;
+
+#define DBG 1
+
+/*
+ * Test base class for net native tests to support common usage.
+ */
+class NetNativeTestBase : public ::testing::Test {
+  public:
+    // TODO: update the logging when gtest supports logging the life cycle on each test.
+    NetNativeTestBase() {
+        if (DBG) LOG(INFO) << getTestCaseLog(true);
+    }
+    ~NetNativeTestBase() {
+        if (DBG) LOG(INFO) << getTestCaseLog(false);
+    }
+
+    std::string getTestCaseLog(bool running) {
+        const TestInfo* const test_info = UnitTest::GetInstance()->current_test_info();
+        return fmt::format("{}: {}#{}", (running ? "started" : "finished"),
+                           test_info->test_suite_name(), test_info->name());
+    }
+};
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Netfilter.h b/staticlibs/netd/libnetdutils/include/netdutils/Netfilter.h
new file mode 100644
index 0000000..22736f1
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Netfilter.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_NETFILTER_H
+#define NETUTILS_NETFILTER_H
+
+#include <ostream>
+
+#include <linux/netfilter.h>
+#include <linux/netfilter/nfnetlink.h>
+#include <linux/netlink.h>
+
+std::ostream& operator<<(std::ostream& os, const nfgenmsg& msg);
+
+#endif /* NETUTILS_NETFILTER_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Netlink.h b/staticlibs/netd/libnetdutils/include/netdutils/Netlink.h
new file mode 100644
index 0000000..ee5183a
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Netlink.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_NETLINK_H
+#define NETUTILS_NETLINK_H
+
+#include <functional>
+#include <ostream>
+#include <linux/netlink.h>
+
+#include "netdutils/Slice.h"
+
+namespace android {
+namespace netdutils {
+
+// Invoke onMsg once for each netlink message in buf. onMsg will be
+// invoked with an aligned and deserialized header along with a Slice
+// containing the message payload.
+//
+// Assume that the first message begins at offset zero within buf.
+void forEachNetlinkMessage(const Slice buf,
+                           const std::function<void(const nlmsghdr&, const Slice)>& onMsg);
+
+// Invoke onAttr once for each netlink attribute in buf. onAttr will be
+// invoked with an aligned and deserialized header along with a Slice
+// containing the attribute payload.
+//
+// Assume that the first attribute begins at offset zero within buf.
+void forEachNetlinkAttribute(const Slice buf,
+                             const std::function<void(const nlattr&, const Slice)>& onAttr);
+
+}  // namespace netdutils
+}  // namespace android
+
+bool operator==(const sockaddr_nl& lhs, const sockaddr_nl& rhs);
+bool operator!=(const sockaddr_nl& lhs, const sockaddr_nl& rhs);
+
+std::ostream& operator<<(std::ostream& os, const nlmsghdr& hdr);
+std::ostream& operator<<(std::ostream& os, const nlattr& attr);
+std::ostream& operator<<(std::ostream& os, const sockaddr_nl& addr);
+
+#endif /* NETUTILS_NETLINK_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/NetlinkListener.h b/staticlibs/netd/libnetdutils/include/netdutils/NetlinkListener.h
new file mode 100644
index 0000000..97f7bb2
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/NetlinkListener.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETLINK_LISTENER_H
+#define NETLINK_LISTENER_H
+
+#include <functional>
+#include <map>
+#include <mutex>
+#include <thread>
+
+#include <android-base/thread_annotations.h>
+#include <netdutils/Netlink.h>
+#include <netdutils/Slice.h>
+#include <netdutils/Status.h>
+#include <netdutils/UniqueFd.h>
+
+namespace android {
+namespace netdutils {
+
+class NetlinkListenerInterface {
+  public:
+    using DispatchFn = std::function<void(const nlmsghdr& nlmsg, const netdutils::Slice msg)>;
+
+    using SkErrorHandler = std::function<void(const int fd, const int err)>;
+
+    virtual ~NetlinkListenerInterface() = default;
+
+    // Send message to the kernel using the underlying netlink socket
+    virtual netdutils::Status send(const netdutils::Slice msg) = 0;
+
+    // Deliver future messages with nlmsghdr.nlmsg_type == type to fn.
+    //
+    // Threadsafe.
+    // All dispatch functions invoked on a single service thread.
+    // subscribe() and join() must not be called from the stack of fn().
+    virtual netdutils::Status subscribe(uint16_t type, const DispatchFn& fn) = 0;
+
+    // Halt delivery of future messages with nlmsghdr.nlmsg_type == type.
+    // Threadsafe.
+    virtual netdutils::Status unsubscribe(uint16_t type) = 0;
+
+    virtual void registerSkErrorHandler(const SkErrorHandler& handler) = 0;
+};
+
+// NetlinkListener manages a netlink socket and associated blocking
+// service thread.
+//
+// This class is written in a generic way to allow multiple different
+// netlink subsystems to share this common infrastructure. If multiple
+// subsystems share the same message delivery requirements (drops ok,
+// no drops) they may share a single listener by calling subscribe()
+// with multiple types.
+//
+// This class is suitable for moderate performance message
+// processing. In particular it avoids extra copies of received
+// message data and allows client code to control which message
+// attributes are processed.
+//
+// Note that NetlinkListener is capable of processing multiple batched
+// netlink messages in a single system call. This is useful to
+// netfilter extensions that allow batching of events like NFLOG.
+class NetlinkListener : public NetlinkListenerInterface {
+  public:
+    NetlinkListener(netdutils::UniqueFd event, netdutils::UniqueFd sock, const std::string& name);
+
+    ~NetlinkListener() override;
+
+    netdutils::Status send(const netdutils::Slice msg) override;
+
+    netdutils::Status subscribe(uint16_t type, const DispatchFn& fn) override EXCLUDES(mMutex);
+
+    netdutils::Status unsubscribe(uint16_t type) override EXCLUDES(mMutex);
+
+    void registerSkErrorHandler(const SkErrorHandler& handler) override;
+
+  private:
+    netdutils::Status run();
+
+    const netdutils::UniqueFd mEvent;
+    const netdutils::UniqueFd mSock;
+    const std::string mThreadName;
+    std::mutex mMutex;
+    std::map<uint16_t, DispatchFn> mDispatchMap GUARDED_BY(mMutex);
+    std::thread mWorker;
+    SkErrorHandler mErrorHandler;
+};
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETLINK_LISTENER_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/NetworkConstants.h b/staticlibs/netd/libnetdutils/include/netdutils/NetworkConstants.h
new file mode 100644
index 0000000..dead9a1
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/NetworkConstants.h
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+namespace android {
+namespace netdutils {
+
+// See also NetworkConstants.java in frameworks/base.
+constexpr int IPV4_ADDR_LEN = 4;
+constexpr int IPV4_ADDR_BITS = 32;
+constexpr int IPV6_ADDR_LEN = 16;
+constexpr int IPV6_ADDR_BITS = 128;
+
+// Referred from SHA256_DIGEST_LENGTH in boringssl
+constexpr size_t SHA256_SIZE = 32;
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/ResponseCode.h b/staticlibs/netd/libnetdutils/include/netdutils/ResponseCode.h
new file mode 100644
index 0000000..c170684
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/ResponseCode.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETDUTILS_RESPONSECODE_H
+#define NETDUTILS_RESPONSECODE_H
+
+namespace android {
+namespace netdutils {
+
+class ResponseCode {
+    // Keep in sync with
+    // frameworks/base/services/java/com/android/server/NetworkManagementService.java
+  public:
+    // 100 series - Requestion action was initiated; expect another reply
+    // before proceeding with a new command.
+    // clang-format off
+    static constexpr int ActionInitiated                = 100;
+    static constexpr int InterfaceListResult            = 110;
+    static constexpr int TetherInterfaceListResult      = 111;
+    static constexpr int TetherDnsFwdTgtListResult      = 112;
+    static constexpr int TtyListResult                  = 113;
+    static constexpr int TetheringStatsListResult       = 114;
+    static constexpr int TetherDnsFwdNetIdResult        = 115;
+
+    // 200 series - Requested action has been successfully completed
+    static constexpr int CommandOkay                    = 200;
+    static constexpr int TetherStatusResult             = 210;
+    static constexpr int IpFwdStatusResult              = 211;
+    static constexpr int InterfaceGetCfgResult          = 213;
+    // Formerly: int SoftapStatusResult                 = 214;
+    static constexpr int UsbRNDISStatusResult           = 215;
+    static constexpr int InterfaceRxCounterResult       = 216;
+    static constexpr int InterfaceTxCounterResult       = 217;
+    static constexpr int InterfaceRxThrottleResult      = 218;
+    static constexpr int InterfaceTxThrottleResult      = 219;
+    static constexpr int QuotaCounterResult             = 220;
+    static constexpr int TetheringStatsResult           = 221;
+    // NOTE: keep synced with bionic/libc/dns/net/gethnamaddr.c
+    static constexpr int DnsProxyQueryResult            = 222;
+    static constexpr int ClatdStatusResult              = 223;
+
+    // 400 series - The command was accepted but the requested action
+    // did not take place.
+    static constexpr int OperationFailed                = 400;
+    static constexpr int DnsProxyOperationFailed        = 401;
+    static constexpr int ServiceStartFailed             = 402;
+    static constexpr int ServiceStopFailed              = 403;
+
+    // 500 series - The command was not accepted and the requested
+    // action did not take place.
+    static constexpr int CommandSyntaxError             = 500;
+    static constexpr int CommandParameterError          = 501;
+
+    // 600 series - Unsolicited broadcasts
+    static constexpr int InterfaceChange                = 600;
+    static constexpr int BandwidthControl               = 601;
+    static constexpr int ServiceDiscoveryFailed         = 602;
+    static constexpr int ServiceDiscoveryServiceAdded   = 603;
+    static constexpr int ServiceDiscoveryServiceRemoved = 604;
+    static constexpr int ServiceRegistrationFailed      = 605;
+    static constexpr int ServiceRegistrationSucceeded   = 606;
+    static constexpr int ServiceResolveFailed           = 607;
+    static constexpr int ServiceResolveSuccess          = 608;
+    static constexpr int ServiceSetHostnameFailed       = 609;
+    static constexpr int ServiceSetHostnameSuccess      = 610;
+    static constexpr int ServiceGetAddrInfoFailed       = 611;
+    static constexpr int ServiceGetAddrInfoSuccess      = 612;
+    static constexpr int InterfaceClassActivity         = 613;
+    static constexpr int InterfaceAddressChange         = 614;
+    static constexpr int InterfaceDnsInfo               = 615;
+    static constexpr int RouteChange                    = 616;
+    static constexpr int StrictCleartext                = 617;
+    // clang-format on
+};
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif  // NETDUTILS_RESPONSECODE_H
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Slice.h b/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
new file mode 100644
index 0000000..aa12927
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_SLICE_H
+#define NETUTILS_SLICE_H
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <ostream>
+#include <tuple>
+#include <type_traits>
+#include <vector>
+
+namespace android {
+namespace netdutils {
+
+// Immutable wrapper for a linear region of unowned bytes.
+// Slice represents memory as a half-closed interval [base, limit).
+//
+// Note that without manually invoking the Slice() constructor, it is
+// impossible to increase the size of a slice. This guarantees that
+// applications that properly use the slice API will never access
+// memory outside of a slice.
+//
+// Note that const Slice still wraps mutable memory, however copy
+// assignment and move assignment to slice are disabled.
+class Slice {
+  public:
+    Slice() = default;
+
+    // Create a slice beginning at base and continuing to but not including limit
+    Slice(void* base, void* limit) : mBase(toUint8(base)), mLimit(toUint8(limit)) {}
+
+    // Create a slice beginning at base and continuing for size bytes
+    Slice(void* base, size_t size) : Slice(base, toUint8(base) + size) {}
+
+    // Return the address of the first byte in this slice
+    uint8_t* base() const { return mBase; }
+
+    // Return the address of the first byte following this slice
+    uint8_t* limit() const { return mLimit; }
+
+    // Return the size of this slice in bytes
+    size_t size() const { return limit() - base(); }
+
+    // Return true if size() == 0
+    bool empty() const { return base() == limit(); }
+
+  private:
+    static uint8_t* toUint8(void* ptr) { return reinterpret_cast<uint8_t*>(ptr); }
+
+    uint8_t* mBase = nullptr;
+    uint8_t* mLimit = nullptr;
+};
+
+// Return slice representation of ref which must be a POD type
+template <typename T>
+inline const Slice makeSlice(const T& ref) {
+    static_assert(std::is_pod<T>::value, "value must be a POD type");
+    static_assert(!std::is_pointer<T>::value, "value must not be a pointer type");
+    return {const_cast<T*>(&ref), sizeof(ref)};
+}
+
+// Return slice representation of string data()
+inline const Slice makeSlice(const std::string& s) {
+    using ValueT = std::string::value_type;
+    return {const_cast<ValueT*>(s.data()), s.size() * sizeof(ValueT)};
+}
+
+// Return slice representation of vector data()
+template <typename T>
+inline const Slice makeSlice(const std::vector<T>& v) {
+    return {const_cast<T*>(v.data()), v.size() * sizeof(T)};
+}
+
+// Return slice representation of array data()
+template <typename U, size_t V>
+inline const Slice makeSlice(const std::array<U, V>& a) {
+    return {const_cast<U*>(a.data()), a.size() * sizeof(U)};
+}
+
+// Return prefix and suffix of Slice s ending and starting at position cut
+inline std::pair<const Slice, const Slice> split(const Slice s, size_t cut) {
+    const size_t tmp = std::min(cut, s.size());
+    return {{s.base(), s.base() + tmp}, {s.base() + tmp, s.limit()}};
+}
+
+// Return prefix of Slice s ending at position cut
+inline const Slice take(const Slice s, size_t cut) {
+    return std::get<0>(split(s, cut));
+}
+
+// Return suffix of Slice s starting at position cut
+inline const Slice drop(const Slice s, size_t cut) {
+    return std::get<1>(split(s, cut));
+}
+
+// Copy from src into dst. Bytes copied is the lesser of dst.size() and src.size()
+inline size_t copy(const Slice dst, const Slice src) {
+    const auto min = std::min(dst.size(), src.size());
+    memcpy(dst.base(), src.base(), min);
+    return min;
+}
+
+// Base case for variadic extract below
+template <typename Head>
+inline size_t extract(const Slice src, Head& head) {
+    return copy(makeSlice(head), src);
+}
+
+// Copy from src into one or more pointers to POD data.  If src.size()
+// is less than the sum of all data pointers a suffix of data will be
+// left unmodified. Return the number of bytes copied.
+template <typename Head, typename... Tail>
+inline size_t extract(const Slice src, Head& head, Tail&... tail) {
+    const auto extracted = extract(src, head);
+    return extracted + extract(drop(src, extracted), tail...);
+}
+
+// Return a string containing a copy of the contents of s
+std::string toString(const Slice s);
+
+// Return a string containing a hexadecimal representation of the contents of s.
+// This function inserts a newline into its output every wrap bytes.
+std::string toHex(const Slice s, int wrap = INT_MAX);
+
+inline bool operator==(const Slice& lhs, const Slice& rhs) {
+    return (lhs.base() == rhs.base()) && (lhs.limit() == rhs.limit());
+}
+
+inline bool operator!=(const Slice& lhs, const Slice& rhs) {
+    return !(lhs == rhs);
+}
+
+std::ostream& operator<<(std::ostream& os, const Slice& slice);
+
+// Return suffix of Slice s starting at the first match of byte c. If no matched
+// byte, return an empty Slice.
+inline const Slice findFirstMatching(const Slice s, uint8_t c) {
+    uint8_t* match = (uint8_t*)memchr(s.base(), c, s.size());
+    if (!match) return Slice();
+    return drop(s, match - s.base());
+}
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_SLICE_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Socket.h b/staticlibs/netd/libnetdutils/include/netdutils/Socket.h
new file mode 100644
index 0000000..e5aaab9
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Socket.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETDUTILS_SOCKET_H
+#define NETDUTILS_SOCKET_H
+
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <string>
+
+#include "netdutils/StatusOr.h"
+
+namespace android {
+namespace netdutils {
+
+inline sockaddr* asSockaddrPtr(void* addr) {
+    return reinterpret_cast<sockaddr*>(addr);
+}
+
+inline const sockaddr* asSockaddrPtr(const void* addr) {
+    return reinterpret_cast<const sockaddr*>(addr);
+}
+
+// Return a string representation of addr or Status if there was a
+// failure during conversion.
+StatusOr<std::string> toString(const in6_addr& addr);
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETDUTILS_SOCKET_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/SocketOption.h b/staticlibs/netd/libnetdutils/include/netdutils/SocketOption.h
new file mode 100644
index 0000000..3b0aab7
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/SocketOption.h
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+#ifndef NETDUTILS_SOCKETOPTION_H
+#define NETDUTILS_SOCKETOPTION_H
+
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <string>
+
+#include "netdutils/Fd.h"
+#include "netdutils/Status.h"
+
+namespace android {
+namespace netdutils {
+
+// Turn on simple "boolean" socket options.
+//
+// This is simple wrapper for options that are enabled via code of the form:
+//
+//     int on = 1;
+//     setsockopt(..., &on, sizeof(on));
+Status enableSockopt(Fd sock, int level, int optname);
+
+// Turn on TCP keepalives, and set keepalive parameters for this socket.
+//
+// A parameter value of zero does not set that parameter.
+//
+// Typical system defaults are:
+//
+//     idleTime (in seconds)
+//     $ cat /proc/sys/net/ipv4/tcp_keepalive_time
+//     7200
+//
+//     numProbes
+//     $ cat /proc/sys/net/ipv4/tcp_keepalive_probes
+//     9
+//
+//     probeInterval (in seconds)
+//     $ cat /proc/sys/net/ipv4/tcp_keepalive_intvl
+//     75
+Status enableTcpKeepAlives(Fd sock, unsigned idleTime, unsigned numProbes, unsigned probeInterval);
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETDUTILS_SOCKETOPTION_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Status.h b/staticlibs/netd/libnetdutils/include/netdutils/Status.h
new file mode 100644
index 0000000..34f3bb2
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Status.h
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_STATUS_H
+#define NETUTILS_STATUS_H
+
+#include <cassert>
+#include <limits>
+#include <ostream>
+
+#include <android-base/result.h>
+
+namespace android {
+namespace netdutils {
+
+// Simple status implementation suitable for use on the stack in low
+// or moderate performance code. This can definitely be improved but
+// for now short string optimization is expected to keep the common
+// success case fast.
+//
+// Status is implicitly movable via the default noexcept move constructor
+// and noexcept move-assignment operator.
+class [[nodiscard]] Status {
+  public:
+    Status() = default;
+    explicit Status(int code) : mCode(code) {}
+
+    // Constructs an error Status, |code| must be non-zero.
+    Status(int code, std::string msg) : mCode(code), mMsg(std::move(msg)) { assert(!ok()); }
+
+    // Constructs an error Status with message. Error |code| is unspecified.
+    explicit Status(std::string msg) : Status(std::numeric_limits<int>::max(), std::move(msg)) {}
+
+    Status(android::base::Result<void> result)
+        : mCode(result.ok() ? 0 : static_cast<int>(result.error().code())),
+          mMsg(result.ok() ? "" : result.error().message()) {}
+
+    int code() const { return mCode; }
+
+    bool ok() const { return code() == 0; }
+
+    const std::string& msg() const { return mMsg; }
+
+    // Explicitly ignores the Status without triggering [[nodiscard]] errors.
+    void ignoreError() const {}
+
+    bool operator==(const Status& other) const { return code() == other.code(); }
+    bool operator!=(const Status& other) const { return !(*this == other); }
+
+  private:
+    int mCode = 0;
+    std::string mMsg;
+};
+
+namespace status {
+
+const Status ok{0};
+// EOF is not part of errno space, we'll place it far above the
+// highest existing value.
+const Status eof{0x10001, "end of file"};
+const Status undefined{std::numeric_limits<int>::max(), "undefined"};
+
+}  // namespace status
+
+// Return true if status is "OK". This is sometimes preferable to
+// status.ok() when we want to check the state of Status-like objects
+// that implicitly cast to Status.
+inline bool isOk(const Status& status) {
+    return status.ok();
+}
+
+// For use only in tests. Used for both Status and Status-like objects. See also isOk().
+#define EXPECT_OK(status) EXPECT_TRUE(isOk(status))
+#define ASSERT_OK(status) ASSERT_TRUE(isOk(status))
+
+// Documents that status is expected to be ok. This function may log
+// (or assert when running in debug mode) if status has an unexpected value.
+inline void expectOk(const Status& /*status*/) {
+    // TODO: put something here, for now this function serves solely as documentation.
+}
+
+// Convert POSIX errno to a Status object.
+// If Status is extended to have more features, this mapping may
+// become more complex.
+Status statusFromErrno(int err, const std::string& msg);
+
+// Helper that checks Status-like object (notably StatusOr) against a
+// value in the errno space.
+bool equalToErrno(const Status& status, int err);
+
+// Helper that converts Status-like object (notably StatusOr) to a
+// message.
+std::string toString(const Status& status);
+
+std::ostream& operator<<(std::ostream& os, const Status& s);
+
+// Evaluate 'stmt' to a Status object and if it results in an error, return that
+// error.  Use 'tmp' as a variable name to avoid shadowing any variables named
+// tmp.
+#define RETURN_IF_NOT_OK_IMPL(tmp, stmt)           \
+    do {                                           \
+        ::android::netdutils::Status tmp = (stmt); \
+        if (!isOk(tmp)) {                          \
+            return tmp;                            \
+        }                                          \
+    } while (false)
+
+// Create a unique variable name to avoid shadowing local variables.
+#define RETURN_IF_NOT_OK_CONCAT(line, stmt) RETURN_IF_NOT_OK_IMPL(__CONCAT(_status_, line), stmt)
+
+// Macro to allow exception-like handling of error return values.
+//
+// If the evaluation of stmt results in an error, return that error
+// from current function.
+//
+// Example usage:
+// Status bar() { ... }
+//
+// RETURN_IF_NOT_OK(status);
+// RETURN_IF_NOT_OK(bar());
+#define RETURN_IF_NOT_OK(stmt) RETURN_IF_NOT_OK_CONCAT(__LINE__, stmt)
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_STATUS_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/StatusOr.h b/staticlibs/netd/libnetdutils/include/netdutils/StatusOr.h
new file mode 100644
index 0000000..c7aa4e4
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/StatusOr.h
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_STATUSOR_H
+#define NETUTILS_STATUSOR_H
+
+#include <cassert>
+#include "netdutils/Status.h"
+
+namespace android {
+namespace netdutils {
+
+// Wrapper around a combination of Status and application value type.
+// T may be any copyable or movable type.
+template <typename T>
+class [[nodiscard]] StatusOr {
+  public:
+    // Constructs a new StatusOr with status::undefined status.
+    // This is marked 'explicit' to try to catch cases like 'return {};',
+    // where people think StatusOr<std::vector<int>> will be initialized
+    // with an empty vector, instead of a status::undefined.
+    explicit StatusOr() = default;
+
+    // Implicit copy constructor and construction from T.
+    // NOLINTNEXTLINE(google-explicit-constructor)
+    StatusOr(Status status) : mStatus(std::move(status)) { assert(!isOk(mStatus)); }
+
+    // Implicit construction from T. It is convenient and sensible to be able
+    // to do 'return T()' when the return type is StatusOr<T>.
+    // NOLINTNEXTLINE(google-explicit-constructor)
+    StatusOr(const T& value) : mStatus(status::ok), mValue(value) {}
+    // NOLINTNEXTLINE(google-explicit-constructor)
+    StatusOr(T&& value) : mStatus(status::ok), mValue(std::move(value)) {}
+
+    // Move constructor ok (if T supports move)
+    StatusOr(StatusOr&&) noexcept = default;
+    // Move assignment ok (if T supports move)
+    StatusOr& operator=(StatusOr&&) noexcept = default;
+    // Copy constructor ok (if T supports copy)
+    StatusOr(const StatusOr&) = default;
+    // Copy assignment ok (if T supports copy)
+    StatusOr& operator=(const StatusOr&) = default;
+
+    // Returns a const reference to wrapped type.
+    // It is an error to call value() when !isOk(status())
+    const T& value() const & { return mValue; }
+    const T&& value() const && { return mValue; }
+
+    // Returns an rvalue reference to wrapped type
+    // It is an error to call value() when !isOk(status())
+    //
+    // If T is expensive to copy but supports efficient move, it can be moved
+    // out of a StatusOr as follows:
+    //   T value = std::move(statusor).value();
+    T& value() & { return mValue; }
+    T&& value() && { return mValue; }
+
+    // Returns the Status object assigned at construction time.
+    const Status status() const { return mStatus; }
+
+    // Explicitly ignores the Status without triggering [[nodiscard]] errors.
+    void ignoreError() const {}
+
+    // Implicit cast to Status.
+    // NOLINTNEXTLINE(google-explicit-constructor)
+    operator Status() const { return status(); }
+
+  private:
+    Status mStatus = status::undefined;
+    T mValue;
+};
+
+template <typename T>
+inline std::ostream& operator<<(std::ostream& os, const StatusOr<T>& s) {
+    return os << "StatusOr[status: " << s.status() << "]";
+}
+
+#define ASSIGN_OR_RETURN_IMPL(tmp, lhs, stmt) \
+    auto tmp = (stmt);                        \
+    RETURN_IF_NOT_OK(tmp);                    \
+    lhs = std::move(tmp.value());
+
+#define ASSIGN_OR_RETURN_CONCAT(line, lhs, stmt) \
+    ASSIGN_OR_RETURN_IMPL(__CONCAT(_status_or_, line), lhs, stmt)
+
+// Macro to allow exception-like handling of error return values.
+//
+// If the evaluation of stmt results in an error, return that error
+// from the current function. Otherwise, assign the result to lhs.
+//
+// This macro supports both move and copy assignment operators. lhs
+// may be either a new local variable or an existing non-const
+// variable accessible in the current scope.
+//
+// Example usage:
+// StatusOr<MyType> foo() { ... }
+//
+// ASSIGN_OR_RETURN(auto myVar, foo());
+// ASSIGN_OR_RETURN(myExistingVar, foo());
+// ASSIGN_OR_RETURN(myMemberVar, foo());
+#define ASSIGN_OR_RETURN(lhs, stmt) ASSIGN_OR_RETURN_CONCAT(__LINE__, lhs, stmt)
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_STATUSOR_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Stopwatch.h b/staticlibs/netd/libnetdutils/include/netdutils/Stopwatch.h
new file mode 100644
index 0000000..e7b4326
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Stopwatch.h
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+#ifndef NETDUTILS_STOPWATCH_H
+#define NETDUTILS_STOPWATCH_H
+
+#include <chrono>
+
+namespace android {
+namespace netdutils {
+
+class Stopwatch {
+  private:
+    using clock = std::chrono::steady_clock;
+    using time_point = std::chrono::time_point<clock>;
+
+  public:
+    Stopwatch() : mStart(clock::now()) {}
+    virtual ~Stopwatch() = default;
+
+    int64_t timeTakenUs() const { return getElapsedUs(clock::now()); }
+    int64_t getTimeAndResetUs() {
+        const auto& now = clock::now();
+        int64_t elapsed = getElapsedUs(now);
+        mStart = now;
+        return elapsed;
+    }
+
+  private:
+    time_point mStart;
+
+    int64_t getElapsedUs(const time_point& now) const {
+        return (std::chrono::duration_cast<std::chrono::microseconds>(now - mStart)).count();
+    }
+};
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif  // NETDUTILS_STOPWATCH_H
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Syscalls.h b/staticlibs/netd/libnetdutils/include/netdutils/Syscalls.h
new file mode 100644
index 0000000..36fcd85
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Syscalls.h
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETDUTILS_SYSCALLS_H
+#define NETDUTILS_SYSCALLS_H
+
+#include <memory>
+
+#include <net/if.h>
+#include <poll.h>
+#include <sys/eventfd.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#include "netdutils/Fd.h"
+#include "netdutils/Slice.h"
+#include "netdutils/Socket.h"
+#include "netdutils/Status.h"
+#include "netdutils/StatusOr.h"
+#include "netdutils/UniqueFd.h"
+#include "netdutils/UniqueFile.h"
+
+namespace android {
+namespace netdutils {
+
+class Syscalls {
+  public:
+    virtual ~Syscalls() = default;
+
+    virtual StatusOr<UniqueFd> open(const std::string& pathname, int flags,
+                                    mode_t mode = 0) const = 0;
+
+    virtual StatusOr<UniqueFd> socket(int domain, int type, int protocol) const = 0;
+
+    virtual Status getsockname(Fd sock, sockaddr* addr, socklen_t* addrlen) const = 0;
+
+    virtual Status getsockopt(Fd sock, int level, int optname, void *optval,
+                              socklen_t *optlen) const = 0;
+
+    virtual Status setsockopt(Fd sock, int level, int optname, const void* optval,
+                              socklen_t optlen) const = 0;
+
+    virtual Status bind(Fd sock, const sockaddr* addr, socklen_t addrlen) const = 0;
+
+    virtual Status connect(Fd sock, const sockaddr* addr, socklen_t addrlen) const = 0;
+
+    virtual StatusOr<ifreq> ioctl(Fd sock, unsigned long request, ifreq* ifr) const = 0;
+
+    virtual StatusOr<UniqueFd> eventfd(unsigned int initval, int flags) const = 0;
+
+    virtual StatusOr<int> ppoll(pollfd* fds, nfds_t nfds, double timeout) const = 0;
+
+    virtual StatusOr<size_t> writev(Fd fd, const std::vector<iovec>& iov) const = 0;
+
+    virtual StatusOr<size_t> write(Fd fd, const Slice buf) const = 0;
+
+    virtual StatusOr<Slice> read(Fd fd, const Slice buf) const = 0;
+
+    virtual StatusOr<size_t> sendto(Fd sock, const Slice buf, int flags, const sockaddr* dst,
+                                    socklen_t dstlen) const = 0;
+
+    virtual StatusOr<Slice> recvfrom(Fd sock, const Slice dst, int flags, sockaddr* src,
+                                     socklen_t* srclen) const = 0;
+
+    virtual Status shutdown(Fd fd, int how) const = 0;
+
+    virtual Status close(Fd fd) const = 0;
+
+    virtual StatusOr<UniqueFile> fopen(const std::string& path, const std::string& mode) const = 0;
+
+    virtual StatusOr<int> vfprintf(FILE* file, const char* format, va_list ap) const = 0;
+
+    virtual StatusOr<int> vfscanf(FILE* file, const char* format, va_list ap) const = 0;
+
+    virtual Status fclose(FILE* file) const = 0;
+
+    virtual StatusOr<pid_t> fork() const = 0;
+
+    // va_args helpers
+    // va_start doesn't work when the preceding argument is a reference
+    // type so we're forced to use const char*.
+    StatusOr<int> fprintf(FILE* file, const char* format, ...) const {
+        va_list ap;
+        va_start(ap, format);
+        auto result = vfprintf(file, format, ap);
+        va_end(ap);
+        return result;
+    }
+
+    // va_start doesn't work when the preceding argument is a reference
+    // type so we're forced to use const char*.
+    StatusOr<int> fscanf(FILE* file, const char* format, ...) const {
+        va_list ap;
+        va_start(ap, format);
+        auto result = vfscanf(file, format, ap);
+        va_end(ap);
+        return result;
+    }
+
+    // Templated helpers that forward directly to methods declared above
+    template <typename SockaddrT>
+    StatusOr<SockaddrT> getsockname(Fd sock) const {
+        SockaddrT addr = {};
+        socklen_t addrlen = sizeof(addr);
+        RETURN_IF_NOT_OK(getsockname(sock, asSockaddrPtr(&addr), &addrlen));
+        return addr;
+    }
+
+    template <typename SockoptT>
+    Status getsockopt(Fd sock, int level, int optname, void* optval, socklen_t* optlen) const {
+        return getsockopt(sock, level, optname, optval, optlen);
+    }
+
+    template <typename SockoptT>
+    Status setsockopt(Fd sock, int level, int optname, const SockoptT& opt) const {
+        return setsockopt(sock, level, optname, &opt, sizeof(opt));
+    }
+
+    template <typename SockaddrT>
+    Status bind(Fd sock, const SockaddrT& addr) const {
+        return bind(sock, asSockaddrPtr(&addr), sizeof(addr));
+    }
+
+    template <typename SockaddrT>
+    Status connect(Fd sock, const SockaddrT& addr) const {
+        return connect(sock, asSockaddrPtr(&addr), sizeof(addr));
+    }
+
+    template <size_t size>
+    StatusOr<std::array<uint16_t, size>> ppoll(const std::array<Fd, size>& fds, uint16_t events,
+                                               double timeout) const {
+        std::array<pollfd, size> tmp;
+        for (size_t i = 0; i < size; ++i) {
+            tmp[i].fd = fds[i].get();
+            tmp[i].events = events;
+            tmp[i].revents = 0;
+        }
+        RETURN_IF_NOT_OK(ppoll(tmp.data(), tmp.size(), timeout).status());
+        std::array<uint16_t, size> out;
+        for (size_t i = 0; i < size; ++i) {
+            out[i] = tmp[i].revents;
+        }
+        return out;
+    }
+
+    template <typename SockaddrT>
+    StatusOr<size_t> sendto(Fd sock, const Slice buf, int flags, const SockaddrT& dst) const {
+        return sendto(sock, buf, flags, asSockaddrPtr(&dst), sizeof(dst));
+    }
+
+    // Ignore src sockaddr
+    StatusOr<Slice> recvfrom(Fd sock, const Slice dst, int flags) const {
+        return recvfrom(sock, dst, flags, nullptr, nullptr);
+    }
+
+    template <typename SockaddrT>
+    StatusOr<std::pair<Slice, SockaddrT>> recvfrom(Fd sock, const Slice dst, int flags) const {
+        SockaddrT addr = {};
+        socklen_t addrlen = sizeof(addr);
+        ASSIGN_OR_RETURN(auto used, recvfrom(sock, dst, flags, asSockaddrPtr(&addr), &addrlen));
+        return std::make_pair(used, addr);
+    }
+};
+
+// Specialized singleton that supports zero initialization and runtime
+// override of contained pointer.
+class SyscallsHolder {
+  public:
+    ~SyscallsHolder();
+
+    // Return a pointer to an unowned instance of Syscalls.
+    Syscalls& get();
+
+    // Testing only: set the value returned by getSyscalls. Return the old value.
+    // Callers are responsible for restoring the previous value returned
+    // by getSyscalls to avoid leaks.
+    Syscalls& swap(Syscalls& syscalls);
+
+  private:
+    std::atomic<Syscalls*> mSyscalls{nullptr};
+};
+
+// Syscalls instance used throughout netdutils
+extern SyscallsHolder sSyscalls;
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETDUTILS_SYSCALLS_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/ThreadUtil.h b/staticlibs/netd/libnetdutils/include/netdutils/ThreadUtil.h
new file mode 100644
index 0000000..62e6f70
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/ThreadUtil.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETDUTILS_THREADUTIL_H
+#define NETDUTILS_THREADUTIL_H
+
+#include <pthread.h>
+#include <memory>
+
+#include <android-base/logging.h>
+
+namespace android {
+namespace netdutils {
+
+struct scoped_pthread_attr {
+    scoped_pthread_attr() { pthread_attr_init(&attr); }
+    ~scoped_pthread_attr() { pthread_attr_destroy(&attr); }
+
+    int detach() { return -pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); }
+
+    pthread_attr_t attr;
+};
+
+inline void setThreadName(std::string name) {
+    // MAX_TASK_COMM_LEN=16 is not exported by bionic.
+    const size_t MAX_TASK_COMM_LEN = 16;
+
+    // Crop name to 16 bytes including the NUL byte, as required by pthread_setname_np()
+    if (name.size() >= MAX_TASK_COMM_LEN) name.resize(MAX_TASK_COMM_LEN - 1);
+
+    if (int ret = pthread_setname_np(pthread_self(), name.c_str()); ret != 0) {
+        LOG(WARNING) << "Unable to set thread name to " << name << ": " << strerror(ret);
+    }
+}
+
+template <typename T>
+inline void* runAndDelete(void* obj) {
+    std::unique_ptr<T> handler(reinterpret_cast<T*>(obj));
+    setThreadName(handler->threadName().c_str());
+    handler->run();
+    return nullptr;
+}
+
+template <typename T>
+inline int threadLaunch(T* obj) {
+    if (obj == nullptr) {
+        return -EINVAL;
+    }
+
+    scoped_pthread_attr scoped_attr;
+
+    int rval = scoped_attr.detach();
+    if (rval != 0) {
+        return rval;
+    }
+
+    pthread_t thread;
+    rval = pthread_create(&thread, &scoped_attr.attr, &runAndDelete<T>, obj);
+    if (rval != 0) {
+        LOG(WARNING) << __func__ << ": pthread_create failed: " << rval;
+        return -rval;
+    }
+
+    return rval;
+}
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif  // NETDUTILS_THREADUTIL_H
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/UidConstants.h b/staticlibs/netd/libnetdutils/include/netdutils/UidConstants.h
new file mode 100644
index 0000000..42c1090
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/UidConstants.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETDUTILS_UID_CONSTANTS_H
+#define NETDUTILS_UID_CONSTANTS_H
+
+// These are used by both eBPF kernel programs and netd, we cannot put them in NetdConstant.h since
+// we have to minimize the number of headers included by the BPF kernel program.
+#define MIN_SYSTEM_UID 0
+#define MAX_SYSTEM_UID 9999
+
+#define PER_USER_RANGE 100000
+
+#endif  // NETDUTILS_UID_CONSTANTS_H
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/UniqueFd.h b/staticlibs/netd/libnetdutils/include/netdutils/UniqueFd.h
new file mode 100644
index 0000000..61101f9
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/UniqueFd.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETUTILS_UNIQUEFD_H
+#define NETUTILS_UNIQUEFD_H
+
+#include <unistd.h>
+#include <ostream>
+
+#include "netdutils/Fd.h"
+
+namespace android {
+namespace netdutils {
+
+// Stricter unique_fd implementation that:
+// *) Does not implement release()
+// *) Does not implicitly cast to int
+// *) Uses a strongly typed wrapper (Fd) for the underlying file descriptor
+//
+// Users of UniqueFd should endeavor to treat this as a completely
+// opaque object. The only code that should interpret the wrapped
+// value is in Syscalls.h
+class UniqueFd {
+  public:
+    UniqueFd() = default;
+
+    UniqueFd(Fd fd) : mFd(fd) {}
+
+    ~UniqueFd() { reset(); }
+
+    // Disallow copy
+    UniqueFd(const UniqueFd&) = delete;
+    UniqueFd& operator=(const UniqueFd&) = delete;
+
+    // Allow move
+    UniqueFd(UniqueFd&& other) { std::swap(mFd, other.mFd); }
+    UniqueFd& operator=(UniqueFd&& other) {
+        std::swap(mFd, other.mFd);
+        return *this;
+    }
+
+    // Cleanup any currently owned Fd, replacing it with the optional
+    // parameter fd
+    void reset(Fd fd = Fd());
+
+    // Implict cast to Fd
+    operator const Fd &() const { return mFd; }
+
+  private:
+    Fd mFd;
+};
+
+std::ostream& operator<<(std::ostream& os, const UniqueFd& fd);
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_UNIQUEFD_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/UniqueFile.h b/staticlibs/netd/libnetdutils/include/netdutils/UniqueFile.h
new file mode 100644
index 0000000..6dd6d67
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/UniqueFile.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETDUTILS_UNIQUEFILE_H
+#define NETDUTILS_UNIQUEFILE_H
+
+#include <stdio.h>
+#include <memory>
+
+namespace android {
+namespace netdutils {
+
+struct UniqueFileDtor {
+    void operator()(FILE* file) const;
+};
+
+using UniqueFile = std::unique_ptr<FILE, UniqueFileDtor>;
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETDUTILS_UNIQUEFILE_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Utils.h b/staticlibs/netd/libnetdutils/include/netdutils/Utils.h
new file mode 100644
index 0000000..83c583b
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Utils.h
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+#ifndef NETUTILS_UTILS_H
+#define NETUTILS_UTILS_H
+
+#include "netdutils/StatusOr.h"
+
+namespace android {
+namespace netdutils {
+
+StatusOr<std::vector<std::string>> getIfaceNames();
+
+StatusOr<std::map<std::string, uint32_t>> getIfaceList();
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_UTILS_H */
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
new file mode 100644
index 0000000..91f94b5
--- /dev/null
+++ b/staticlibs/tests/unit/Android.bp
@@ -0,0 +1,90 @@
+//########################################################################
+// Build NetworkStaticLibTests package
+//########################################################################
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "NetworkStaticLibTestsLib",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    min_sdk_version: "30",
+    defaults: ["framework-connectivity-test-defaults"],
+    static_libs: [
+        "androidx.test.rules",
+        "mockito-target-extended-minus-junit4",
+        "netd-client",
+        "net-tests-utils",
+        "net-utils-framework-common",
+        "net-utils-device-common",
+        "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",
+        "android.test.base",
+    ],
+    visibility: [
+        "//frameworks/base/packages/Tethering/tests/integration",
+        "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
+        "//packages/modules/NetworkStack/tests/integration",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        test: true,
+    },
+}
+
+android_test {
+    name: "NetworkStaticLibTests",
+    certificate: "platform",
+    static_libs: [
+        "NetworkStaticLibTestsLib",
+    ],
+    jni_libs: [
+        // For mockito extended
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    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/AndroidManifest.xml b/staticlibs/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..84a20a2
--- /dev/null
+++ b/staticlibs/tests/unit/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.frameworks.libnet.tests">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.libnet.tests"
+        android:label="Network Static Library Tests" />
+</manifest>
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..caaf959
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -0,0 +1,175 @@
+#  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,
+    send_broadcast_empty_ethercat_packet,
+    send_raw_packet_downstream,
+)
+from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
+
+
+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.apf_utils.get_hardware_address")
+  @patch("net_tests_utils.host.python.apf_utils.send_raw_packet_downstream")
+  def test_send_broadcast_empty_ethercat_packet(
+      self,
+      mock_send_raw_packet_downstream: MagicMock,
+      mock_get_hardware_address: MagicMock,
+  ) -> None:
+    mock_get_hardware_address.return_value = "12:34:56:78:90:AB"
+    send_broadcast_empty_ethercat_packet(self.mock_ad, "eth0")
+    # Assuming you'll mock the packet construction part, verify calls to send_raw_packet_downstream.
+    mock_send_raw_packet_downstream.assert_called_once()
+
+  @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
+    iface_name = "eth0"
+    packet_in_hex = "AABBCCDDEEFF"
+    send_raw_packet_downstream(self.mock_ad, iface_name, packet_in_hex)
+    mock_adb_shell.assert_called_once_with(
+        self.mock_ad,
+        "cmd network_stack send-raw-packet-downstream"
+        f" {iface_name} {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, "eth0", "AABBCCDDEEFF")
+
+  @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, "eth0", "AABBCCDDEEFF")
+
+  @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/jarjar-rules.txt b/staticlibs/tests/unit/jarjar-rules.txt
new file mode 100644
index 0000000..e032ae5
--- /dev/null
+++ b/staticlibs/tests/unit/jarjar-rules.txt
@@ -0,0 +1 @@
+rule com.android.net.module.util.** com.android.net.moduletests.util.@1
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java
new file mode 100644
index 0000000..29e84c9
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.net.MacAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.arp.ArpPacket;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class ArpPacketTest {
+
+    private static final Inet4Address TEST_IPV4_ADDR =
+            (Inet4Address) InetAddresses.parseNumericAddress("192.168.1.2");
+    private static final Inet4Address INADDR_ANY =
+            (Inet4Address) InetAddresses.parseNumericAddress("0.0.0.0");
+    private static final byte[] TEST_SENDER_MAC_ADDR = new byte[] {
+            0x00, 0x1a, 0x11, 0x22, 0x33, 0x33 };
+    private static final byte[] TEST_TARGET_MAC_ADDR = new byte[] {
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+    private static final MacAddress TEST_DESTINATION_MAC = MacAddress.fromBytes(ETHER_BROADCAST);
+    private static final MacAddress TEST_SOURCE_MAC = MacAddress.fromBytes(TEST_SENDER_MAC_ADDR);
+    private static final byte[] TEST_ARP_PROBE = new byte[] {
+        // dst mac address
+        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+        // src mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33,
+        // ether type
+        (byte) 0x08, (byte) 0x06,
+        // hardware type
+        (byte) 0x00, (byte) 0x01,
+        // protocol type
+        (byte) 0x08, (byte) 0x00,
+        // hardware address size
+        (byte) 0x06,
+        // protocol address size
+        (byte) 0x04,
+        // opcode
+        (byte) 0x00, (byte) 0x01,
+        // sender mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33,
+        // sender IP address
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // target mac address
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // target IP address
+        (byte) 0xc0, (byte) 0xa8, (byte) 0x01, (byte) 0x02,
+    };
+
+    private static final byte[] TEST_ARP_ANNOUNCE = new byte[] {
+        // dst mac address
+        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+        // src mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33,
+        // ether type
+        (byte) 0x08, (byte) 0x06,
+        // hardware type
+        (byte) 0x00, (byte) 0x01,
+        // protocol type
+        (byte) 0x08, (byte) 0x00,
+        // hardware address size
+        (byte) 0x06,
+        // protocol address size
+        (byte) 0x04,
+        // opcode
+        (byte) 0x00, (byte) 0x01,
+        // sender mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33,
+        // sender IP address
+        (byte) 0xc0, (byte) 0xa8, (byte) 0x01, (byte) 0x02,
+        // target mac address
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // target IP address
+        (byte) 0xc0, (byte) 0xa8, (byte) 0x01, (byte) 0x02,
+    };
+
+    private static final byte[] TEST_ARP_PROBE_TRUNCATED = new byte[] {
+        // dst mac address
+        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+        // src mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33,
+        // ether type
+        (byte) 0x08, (byte) 0x06,
+        // hardware type
+        (byte) 0x00, (byte) 0x01,
+        // protocol type
+        (byte) 0x08, (byte) 0x00,
+        // hardware address size
+        (byte) 0x06,
+        // protocol address size
+        (byte) 0x04,
+        // opcode
+        (byte) 0x00,
+    };
+
+    private static final byte[] TEST_ARP_PROBE_TRUNCATED_MAC = new byte[] {
+         // dst mac address
+        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+        // src mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33,
+        // ether type
+        (byte) 0x08, (byte) 0x06,
+        // hardware type
+        (byte) 0x00, (byte) 0x01,
+        // protocol type
+        (byte) 0x08, (byte) 0x00,
+        // hardware address size
+        (byte) 0x06,
+        // protocol address size
+        (byte) 0x04,
+        // opcode
+        (byte) 0x00, (byte) 0x01,
+        // sender mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33,
+    };
+
+    @Test
+    public void testBuildArpProbePacket() throws Exception {
+        final ByteBuffer arpProbe = ArpPacket.buildArpPacket(ETHER_BROADCAST,
+                TEST_SENDER_MAC_ADDR, TEST_IPV4_ADDR.getAddress(), new byte[ETHER_ADDR_LEN],
+                INADDR_ANY.getAddress(), (short) ARP_REQUEST);
+        assertArrayEquals(arpProbe.array(), TEST_ARP_PROBE);
+    }
+
+    @Test
+    public void testBuildArpAnnouncePacket() throws Exception {
+        final ByteBuffer arpAnnounce = ArpPacket.buildArpPacket(ETHER_BROADCAST,
+                TEST_SENDER_MAC_ADDR, TEST_IPV4_ADDR.getAddress(), new byte[ETHER_ADDR_LEN],
+                TEST_IPV4_ADDR.getAddress(), (short) ARP_REQUEST);
+        assertArrayEquals(arpAnnounce.array(), TEST_ARP_ANNOUNCE);
+    }
+
+    @Test
+    public void testParseArpProbePacket() throws Exception {
+        final ArpPacket packet = ArpPacket.parseArpPacket(TEST_ARP_PROBE, TEST_ARP_PROBE.length);
+        assertEquals(packet.destination, TEST_DESTINATION_MAC);
+        assertEquals(packet.source, TEST_SOURCE_MAC);
+        assertEquals(packet.opCode, ARP_REQUEST);
+        assertEquals(packet.senderHwAddress, MacAddress.fromBytes(TEST_SENDER_MAC_ADDR));
+        assertEquals(packet.targetHwAddress, MacAddress.fromBytes(TEST_TARGET_MAC_ADDR));
+        assertEquals(packet.senderIp, INADDR_ANY);
+        assertEquals(packet.targetIp, TEST_IPV4_ADDR);
+    }
+
+    @Test
+    public void testParseArpAnnouncePacket() throws Exception {
+        final ArpPacket packet = ArpPacket.parseArpPacket(TEST_ARP_ANNOUNCE,
+                TEST_ARP_ANNOUNCE.length);
+        assertEquals(packet.destination, TEST_DESTINATION_MAC);
+        assertEquals(packet.source, TEST_SOURCE_MAC);
+        assertEquals(packet.opCode, ARP_REQUEST);
+        assertEquals(packet.senderHwAddress, MacAddress.fromBytes(TEST_SENDER_MAC_ADDR));
+        assertEquals(packet.targetHwAddress, MacAddress.fromBytes(TEST_TARGET_MAC_ADDR));
+        assertEquals(packet.senderIp, TEST_IPV4_ADDR);
+        assertEquals(packet.targetIp, TEST_IPV4_ADDR);
+    }
+
+    @Test
+    public void testParseArpPacket_invalidByteBufferParameters() throws Exception {
+        assertThrows(ArpPacket.ParseException.class, () -> ArpPacket.parseArpPacket(
+                TEST_ARP_PROBE, 0));
+    }
+
+    @Test
+    public void testParseArpPacket_truncatedPacket() throws Exception {
+        assertThrows(ArpPacket.ParseException.class, () -> ArpPacket.parseArpPacket(
+                TEST_ARP_PROBE_TRUNCATED, TEST_ARP_PROBE_TRUNCATED.length));
+    }
+
+    @Test
+    public void testParseArpPacket_truncatedMacAddress() throws Exception {
+        assertThrows(ArpPacket.ParseException.class, () -> ArpPacket.parseArpPacket(
+                TEST_ARP_PROBE_TRUNCATED_MAC, TEST_ARP_PROBE_TRUNCATED.length));
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt b/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt
new file mode 100644
index 0000000..49940ea
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.net.module.util
+
+import com.android.net.module.util.BitUtils.appendStringRepresentationOfBitMaskToStringBuilder
+import com.android.net.module.util.BitUtils.describeDifferences
+import com.android.net.module.util.BitUtils.packBits
+import com.android.net.module.util.BitUtils.unpackBits
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import org.junit.Test
+
+class BitUtilsTests {
+    @Test
+    fun testBitPackingTestCase() {
+        runBitPackingTestCase(0, intArrayOf())
+        runBitPackingTestCase(1, intArrayOf(0))
+        runBitPackingTestCase(3, intArrayOf(0, 1))
+        runBitPackingTestCase(4, intArrayOf(2))
+        runBitPackingTestCase(63, intArrayOf(0, 1, 2, 3, 4, 5))
+        runBitPackingTestCase(Long.MAX_VALUE.inv(), intArrayOf(63))
+        runBitPackingTestCase(Long.MAX_VALUE.inv() + 1, intArrayOf(0, 63))
+        runBitPackingTestCase(Long.MAX_VALUE.inv() + 2, intArrayOf(1, 63))
+    }
+
+    fun runBitPackingTestCase(packedBits: Long, bits: IntArray) {
+        assertEquals(packedBits, packBits(bits))
+        assertTrue(bits contentEquals unpackBits(packedBits))
+    }
+
+    @Test
+    fun testAppendStringRepresentationOfBitMaskToStringBuilder() {
+        runTestAppendStringRepresentationOfBitMaskToStringBuilder("", 0)
+        runTestAppendStringRepresentationOfBitMaskToStringBuilder("BIT0", 0b1)
+        runTestAppendStringRepresentationOfBitMaskToStringBuilder("BIT1&BIT2&BIT4", 0b10110)
+        runTestAppendStringRepresentationOfBitMaskToStringBuilder(
+                "BIT0&BIT60&BIT61&BIT62&BIT63",
+                (0b11110000_00000000_00000000_00000000 shl 32) +
+                        0b00000000_00000000_00000000_00000001)
+    }
+
+    fun runTestAppendStringRepresentationOfBitMaskToStringBuilder(expected: String, bitMask: Long) {
+        StringBuilder().let {
+            appendStringRepresentationOfBitMaskToStringBuilder(it, bitMask, { i -> "BIT$i" }, "&")
+            assertEquals(expected, it.toString())
+        }
+    }
+
+    @Test
+    fun testDescribeDifferences() {
+        fun describe(a: Long, b: Long) = describeDifferences(a, b, Integer::toString)
+        assertNull(describe(0, 0))
+        assertNull(describe(5, 5))
+        assertNull(describe(Long.MAX_VALUE, Long.MAX_VALUE))
+
+        assertEquals("+0", describe(0, 1))
+        assertEquals("-0", describe(1, 0))
+
+        assertEquals("+0+2", describe(0, 5))
+        assertEquals("+2", describe(1, 5))
+        assertEquals("-0+2", describe(1, 4))
+
+        fun makeField(vararg i: Int) = i.sumOf { 1L shl it }
+        assertEquals("-0-4-6-9+1+3+11", describe(makeField(0, 4, 6, 9), makeField(1, 3, 11)))
+        assertEquals("-1-5-9+6+8", describe(makeField(0, 1, 3, 4, 5, 9), makeField(0, 3, 4, 6, 8)))
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
new file mode 100644
index 0000000..a66dacd
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.net.module.util;
+
+import static android.system.OsConstants.EPERM;
+import static android.system.OsConstants.R_OK;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.TestBpfMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoSession;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BpfDumpTest {
+    private static final int TEST_KEY = 123;
+    private static final String TEST_KEY_BASE64 = "ewAAAA==";
+    private static final int TEST_VAL = 456;
+    private static final String TEST_VAL_BASE64 = "yAEAAA==";
+    private static final String BASE64_DELIMITER = ",";
+    private static final String TEST_KEY_VAL_BASE64 =
+            TEST_KEY_BASE64 + BASE64_DELIMITER + TEST_VAL_BASE64;
+    private static final String INVALID_BASE64_STRING = "Map is null";
+
+    @Test
+    public void testToBase64EncodedString() {
+        final Struct.S32 key = new Struct.S32(TEST_KEY);
+        final Struct.S32 value = new Struct.S32(TEST_VAL);
+
+        // Verified in python:
+        //   import base64
+        //   print(base64.b64encode(b'\x7b\x00\x00\x00')) # key: ewAAAA== (TEST_KEY_BASE64)
+        //   print(base64.b64encode(b'\xc8\x01\x00\x00')) # value: yAEAAA== (TEST_VAL_BASE64)
+        assertEquals("7B000000", HexDump.toHexString(key.writeToBytes()));
+        assertEquals("C8010000", HexDump.toHexString(value.writeToBytes()));
+        assertEquals(TEST_KEY_VAL_BASE64, BpfDump.toBase64EncodedString(key, value));
+    }
+
+    @Test
+    public void testFromBase64EncodedString() {
+        Pair<Struct.S32, Struct.S32> decodedKeyValue = BpfDump.fromBase64EncodedString(
+                Struct.S32.class, Struct.S32.class, TEST_KEY_VAL_BASE64);
+        assertEquals(TEST_KEY, decodedKeyValue.first.val);
+        assertEquals(TEST_VAL, decodedKeyValue.second.val);
+    }
+
+    private void assertThrowsIllegalArgumentException(final String testStr) {
+        assertThrows(IllegalArgumentException.class,
+                () -> BpfDump.fromBase64EncodedString(Struct.S32.class, Struct.S32.class, testStr));
+    }
+
+    @Test
+    public void testFromBase64EncodedStringInvalidString() {
+        assertThrowsIllegalArgumentException(INVALID_BASE64_STRING);
+        assertThrowsIllegalArgumentException(TEST_KEY_BASE64);
+        assertThrowsIllegalArgumentException(
+                TEST_KEY_BASE64 + BASE64_DELIMITER + INVALID_BASE64_STRING);
+        assertThrowsIllegalArgumentException(
+                INVALID_BASE64_STRING + BASE64_DELIMITER + TEST_VAL_BASE64);
+        assertThrowsIllegalArgumentException(
+                INVALID_BASE64_STRING + BASE64_DELIMITER + INVALID_BASE64_STRING);
+        assertThrowsIllegalArgumentException(
+                TEST_KEY_VAL_BASE64 + BASE64_DELIMITER + TEST_KEY_BASE64);
+    }
+
+    private String getDumpMap(final IBpfMap<Struct.S32, Struct.S32> map) {
+        final StringWriter sw = new StringWriter();
+        BpfDump.dumpMap(map, new PrintWriter(sw), "mapName", "header",
+                (key, val) -> "key=" + key.val + ", val=" + val.val);
+        return sw.toString();
+    }
+
+    @Test
+    public void testDumpMap() throws Exception {
+        final IBpfMap<Struct.S32, Struct.S32> map =
+                new TestBpfMap<>(Struct.S32.class, Struct.S32.class);
+        map.updateEntry(new Struct.S32(123), new Struct.S32(456));
+
+        final String dump = getDumpMap(map);
+        assertEquals(dump, "mapName:\n"
+                + "  header\n"
+                + "  key=123, val=456\n");
+    }
+
+    @Test
+    public void testDumpMapMultipleEntries() throws Exception {
+        final IBpfMap<Struct.S32, Struct.S32> map =
+                new TestBpfMap<>(Struct.S32.class, Struct.S32.class);
+        map.updateEntry(new Struct.S32(123), new Struct.S32(456));
+        map.updateEntry(new Struct.S32(789), new Struct.S32(123));
+
+        final String dump = getDumpMap(map);
+        assertTrue(dump.contains("mapName:"));
+        assertTrue(dump.contains("header"));
+        assertTrue(dump.contains("key=123, val=456"));
+        assertTrue(dump.contains("key=789, val=123"));
+    }
+
+    private String getDumpMapStatus(final IBpfMap<Struct.S32, Struct.S32> map) {
+        final StringWriter sw = new StringWriter();
+        BpfDump.dumpMapStatus(map, new PrintWriter(sw), "mapName", "mapPath");
+        return sw.toString();
+    }
+
+    @Test
+    public void testGetMapStatus() {
+        final IBpfMap<Struct.S32, Struct.S32> map =
+                new TestBpfMap<>(Struct.S32.class, Struct.S32.class);
+        assertEquals("mapName: OK\n", getDumpMapStatus(map));
+    }
+
+    @Test
+    public void testGetMapStatusNull() {
+        final MockitoSession session = mockitoSession()
+                .spyStatic(Os.class)
+                .startMocking();
+        try {
+            // Os.access succeeds
+            doReturn(true).when(() -> Os.access("mapPath", R_OK));
+            assertEquals("mapName: NULL(map is pinned to mapPath)\n", getDumpMapStatus(null));
+
+            // Os.access throws EPERM
+            doThrow(new ErrnoException("", EPERM)).when(() -> Os.access("mapPath", R_OK));
+            assertEquals("mapName: NULL(map is not pinned to mapPath: Operation not permitted)\n",
+                    getDumpMapStatus(null));
+        } finally {
+            session.finishMocking();
+        }
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ByteUtilsTests.kt b/staticlibs/tests/unit/src/com/android/net/module/util/ByteUtilsTests.kt
new file mode 100644
index 0000000..e58adad
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ByteUtilsTests.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.net.module.util
+
+import com.android.net.module.util.ByteUtils.indexOf
+import com.android.net.module.util.ByteUtils.concat
+import org.junit.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertNotSame
+
+class ByteUtilsTests {
+    private val EMPTY = byteArrayOf()
+    private val ARRAY1 = byteArrayOf(1)
+    private val ARRAY234 = byteArrayOf(2, 3, 4)
+
+    @Test
+    fun testIndexOf() {
+        assertEquals(-1, indexOf(EMPTY, 1))
+        assertEquals(-1, indexOf(ARRAY1, 2))
+        assertEquals(-1, indexOf(ARRAY234, 1))
+        assertEquals(0, indexOf(byteArrayOf(-1), -1))
+        assertEquals(0, indexOf(ARRAY234, 2))
+        assertEquals(1, indexOf(ARRAY234, 3))
+        assertEquals(2, indexOf(ARRAY234, 4))
+        assertEquals(1, indexOf(byteArrayOf(2, 3, 2, 3), 3))
+    }
+
+    @Test
+    fun testConcat() {
+        assertContentEquals(EMPTY, concat())
+        assertContentEquals(EMPTY, concat(EMPTY))
+        assertContentEquals(EMPTY, concat(EMPTY, EMPTY, EMPTY))
+        assertContentEquals(ARRAY1, concat(ARRAY1))
+        assertNotSame(ARRAY1, concat(ARRAY1))
+        assertContentEquals(ARRAY1, concat(EMPTY, ARRAY1, EMPTY))
+        assertContentEquals(byteArrayOf(1, 1, 1), concat(ARRAY1, ARRAY1, ARRAY1))
+        assertContentEquals(byteArrayOf(1, 2, 3, 4), concat(ARRAY1, ARRAY234))
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
new file mode 100644
index 0000000..851d09a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
@@ -0,0 +1,223 @@
+/*
+ * 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.net.module.util
+
+import android.util.Log
+import com.android.testutils.tryTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private val TAG = CleanupTest::class.simpleName
+
+@RunWith(JUnit4::class)
+class CleanupTest {
+    class TestException1 : Exception()
+    class TestException2 : Exception()
+    class TestException3 : Exception()
+
+    @Test
+    fun testNotThrow() {
+        var x = 1
+        val result = tryTest {
+            x = 2
+            Log.e(TAG, "Do nothing")
+            6
+        } cleanup {
+            assertTrue(x == 2)
+            x = 3
+            Log.e(TAG, "Do nothing")
+        }
+        assertTrue(x == 3)
+        assertTrue(result == 6)
+    }
+
+    @Test
+    fun testThrowTry() {
+        var x = 1
+        val thrown = assertFailsWith<TestException1> {
+            tryTest {
+                x = 2
+                throw TestException1()
+                x = 4
+            } cleanup {
+                assertTrue(x == 2)
+                x = 3
+                Log.e(TAG, "Do nothing")
+            }
+        }
+        assertTrue(thrown.suppressedExceptions.isEmpty())
+        assertTrue(x == 3)
+    }
+
+    @Test
+    fun testThrowCleanup() {
+        var x = 1
+        val thrown = assertFailsWith<TestException2> {
+            tryTest {
+                x = 2
+                Log.e(TAG, "Do nothing")
+            } cleanup {
+                assertTrue(x == 2)
+                x = 3
+                throw TestException2()
+                x = 4
+            }
+        }
+        assertTrue(thrown.suppressedExceptions.isEmpty())
+        assertTrue(x == 3)
+    }
+
+    @Test
+    fun testThrowBoth() {
+        var x = 1
+        val thrown = assertFailsWith<TestException1> {
+            tryTest {
+                x = 2
+                throw TestException1()
+                x = 3
+            } cleanup {
+                assertTrue(x == 2)
+                x = 4
+                throw TestException2()
+                x = 5
+            }
+        }
+        assertTrue(thrown.suppressedExceptions[0] is TestException2)
+        assertTrue(x == 4)
+    }
+
+    @Test
+    fun testReturn() {
+        val resultIfSuccess = 11
+        val resultIfException = 12
+        fun doTestReturn(crash: Boolean) = tryTest {
+            if (crash) throw RuntimeException() else resultIfSuccess
+        }.catch<RuntimeException> {
+            resultIfException
+        } cleanup {}
+
+        assertTrue(6 == tryTest { 6 } cleanup { Log.e(TAG, "tested") })
+        assertEquals(resultIfSuccess, doTestReturn(crash = false))
+        assertEquals(resultIfException, doTestReturn(crash = true))
+    }
+
+    @Test
+    fun testCatch() {
+        var x = 1
+        tryTest {
+            x = 2
+            throw TestException1()
+            x = 3
+        }.catch<TestException1> {
+            x = 4
+        }.catch<TestException2> {
+            x = 5
+        } cleanup {
+            assertTrue(x == 4)
+            x = 6
+        }
+        assertTrue(x == 6)
+    }
+
+    @Test
+    fun testNotCatch() {
+        var x = 1
+        assertFailsWith<TestException1> {
+            tryTest {
+                x = 2
+                throw TestException1()
+            }.catch<TestException2> {
+                fail("Caught TestException2 instead of TestException1")
+            } cleanup {
+                assertTrue(x == 2)
+                x = 3
+            }
+        }
+        assertTrue(x == 3)
+    }
+
+    @Test
+    fun testThrowInCatch() {
+        var x = 1
+        val thrown = assertFailsWith<TestException2> {
+            tryTest {
+                x = 2
+                throw TestException1()
+            }.catch<TestException1> {
+                x = 3
+                throw TestException2()
+            } cleanup {
+                assertTrue(x == 3)
+                x = 4
+            }
+        }
+        assertTrue(x == 4)
+        assertTrue(thrown.suppressedExceptions.isEmpty())
+    }
+
+    @Test
+    fun testAssertionErrorInCatch() {
+        var x = 1
+        val thrown = assertFailsWith<AssertionError> {
+            tryTest {
+                x = 2
+                throw TestException1()
+            }.catch<TestException1> {
+                x = 3
+                fail("Test failure in catch")
+            } cleanup {
+                assertTrue(x == 3)
+                x = 4
+            }
+        }
+        assertTrue(x == 4)
+        assertTrue(thrown.suppressedExceptions.isEmpty())
+    }
+
+    @Test
+    fun testMultipleCleanups() {
+        var x = 1
+        val thrown = assertFailsWith<TestException1> {
+            tryTest {
+                x = 2
+                throw TestException1()
+            } cleanupStep {
+                assertTrue(x == 2)
+                x = 3
+                throw TestException2()
+                x = 4
+            } cleanupStep {
+                assertTrue(x == 3)
+                x = 5
+                throw TestException3()
+                x = 6
+            } cleanup {
+                assertTrue(x == 5)
+                x = 7
+            }
+        }
+        assertEquals(2, thrown.suppressedExceptions.size)
+        assertTrue(thrown.suppressedExceptions[0] is TestException2)
+        assertTrue(thrown.suppressedExceptions[1] is TestException3)
+        assert(x == 7)
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTestJava.java b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTestJava.java
new file mode 100644
index 0000000..8a13397
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTestJava.java
@@ -0,0 +1,121 @@
+/*
+ * 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.net.module.util;
+
+import static com.android.testutils.Cleanup.testAndCleanup;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class CleanupTestJava {
+    private static final String TAG = CleanupTestJava.class.getSimpleName();
+    private static final class TestException1 extends Exception {}
+    private static final class TestException2 extends Exception {}
+    private static final class TestException3 extends Exception {}
+
+    @Test
+    public void testNotThrow() {
+        final AtomicInteger x = new AtomicInteger(1);
+        final int a = testAndCleanup(() -> {
+            x.compareAndSet(1, 2);
+            Log.e(TAG, "Do nothing");
+            return 6;
+        }, () -> {
+                x.compareAndSet(2, 3);
+                Log.e(TAG, "Do nothing");
+            });
+        assertEquals(3, x.get());
+        assertEquals(6, a);
+    }
+
+    @Test
+    public void testThrowTry() {
+        final AtomicInteger x = new AtomicInteger(1);
+        assertThrows(TestException1.class, () ->
+                testAndCleanup(() -> {
+                    x.compareAndSet(1, 2);
+                    throw new TestException1();
+                    // Java refuses to call x.set(3) here because this line is unreachable
+                }, () -> {
+                        x.compareAndSet(2, 3);
+                        Log.e(TAG, "Do nothing");
+                    })
+        );
+        assertEquals(3, x.get());
+    }
+
+    @Test
+    public void testThrowCleanup() {
+        final AtomicInteger x = new AtomicInteger(1);
+        assertThrows(TestException2.class, () ->
+                testAndCleanup(() -> {
+                    x.compareAndSet(1, 2);
+                    Log.e(TAG, "Do nothing");
+                }, () -> {
+                        x.compareAndSet(2, 3);
+                        throw new TestException2();
+                        // Java refuses to call x.set(4) here because this line is unreachable
+                    })
+        );
+        assertEquals(3, x.get());
+    }
+
+    @Test
+    public void testThrowBoth() {
+        final AtomicInteger x = new AtomicInteger(1);
+        assertThrows(TestException1.class, () ->
+                testAndCleanup(() -> {
+                    x.compareAndSet(1, 2);
+                    throw new TestException1();
+                }, () -> {
+                        x.compareAndSet(2, 3);
+                        throw new TestException2();
+                    })
+        );
+        assertEquals(3, x.get());
+    }
+
+    @Test
+    public void testMultipleCleanups() {
+        final AtomicInteger x = new AtomicInteger(1);
+        final TestException1 exception = assertThrows(TestException1.class, () ->
+                testAndCleanup(() -> {
+                    x.compareAndSet(1, 2);
+                    throw new TestException1();
+                }, () -> {
+                        x.compareAndSet(2, 3);
+                        throw new TestException2();
+                    }, () -> {
+                        x.compareAndSet(3, 4);
+                        throw new TestException3();
+                    }, () -> {
+                        x.compareAndSet(4, 5);
+                    })
+        );
+        assertEquals(2, exception.getSuppressed().length);
+        assertTrue(exception.getSuppressed()[0] instanceof TestException2);
+        assertTrue(exception.getSuppressed()[1] instanceof TestException3);
+        assertEquals(5, x.get());
+    }
+}
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
new file mode 100644
index 0000000..4ed3afd
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
@@ -0,0 +1,199 @@
+/*
+ * 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.net.module.util
+
+import android.util.SparseArray
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CollectionUtilsTest {
+    @Test
+    fun testAny() {
+        assertTrue(CollectionUtils.any(listOf("A", "B", "C", "D", "E")) { it == "E" })
+        assertFalse(CollectionUtils.any(listOf("A", "B", "C", "D", "E")) { it == "F" })
+        assertTrue(CollectionUtils.any(listOf("AA", "BBB")) { it.length >= 3 })
+        assertFalse(CollectionUtils.any(listOf("A", "BB", "CCC")) { it.length >= 4 })
+        assertFalse(CollectionUtils.any(listOf("A", "BB", "CCC")) { it.length < 0 })
+        assertFalse(CollectionUtils.any(listOf<String>()) { true })
+        assertFalse(CollectionUtils.any(listOf<String>()) { false })
+        assertTrue(CollectionUtils.any(listOf("A")) { true })
+        assertFalse(CollectionUtils.any(listOf("A")) { false })
+    }
+
+    @Test
+    fun testIndexOf() {
+        assertEquals(4, CollectionUtils.indexOf(listOf("A", "B", "C", "D", "E")) { it == "E" })
+        assertEquals(0, CollectionUtils.indexOf(listOf("A", "B", "C", "D", "E")) { it == "A" })
+        assertEquals(1, CollectionUtils.indexOf(listOf("AA", "BBB", "CCCC")) { it.length >= 3 })
+        assertEquals(1, CollectionUtils.indexOf(listOf("AA", null, "CCCC")) { it == null })
+        assertEquals(1, CollectionUtils.indexOf(listOf(null, "CCCC")) { it != null })
+    }
+
+    @Test
+    fun testIndexOfSubArray() {
+        val haystack = byteArrayOf(1, 2, 3, 4, 5)
+        assertEquals(2, CollectionUtils.indexOfSubArray(haystack, byteArrayOf(3, 4)))
+        assertEquals(3, CollectionUtils.indexOfSubArray(haystack, byteArrayOf(4, 5)))
+        assertEquals(4, CollectionUtils.indexOfSubArray(haystack, byteArrayOf(5)))
+        assertEquals(-1, CollectionUtils.indexOfSubArray(haystack, byteArrayOf(3, 2)))
+        assertEquals(0, CollectionUtils.indexOfSubArray(haystack, byteArrayOf()))
+        assertEquals(-1, CollectionUtils.indexOfSubArray(byteArrayOf(), byteArrayOf(3, 2)))
+        assertEquals(0, CollectionUtils.indexOfSubArray(byteArrayOf(), byteArrayOf()))
+    }
+
+    @Test
+    fun testAll() {
+        assertFalse(CollectionUtils.all(listOf("A", "B", "C", "D", "E")) { it != "E" })
+        assertTrue(CollectionUtils.all(listOf("A", "B", "C", "D", "E")) { it != "F" })
+        assertFalse(CollectionUtils.all(listOf("A", "BB", "CCC")) { it.length > 2 })
+        assertTrue(CollectionUtils.all(listOf("A", "BB", "CCC")) { it.length >= 1 })
+        assertTrue(CollectionUtils.all(listOf("A", "BB", "CCC")) { it.length < 4 })
+        assertTrue(CollectionUtils.all(listOf<String>()) { true })
+        assertTrue(CollectionUtils.all(listOf<String>()) { false })
+        assertTrue(CollectionUtils.all(listOf(1)) { true })
+        assertFalse(CollectionUtils.all(listOf(1)) { false })
+    }
+
+    @Test
+    fun testContains() {
+        assertTrue(CollectionUtils.contains(shortArrayOf(10, 20, 30), 10))
+        assertTrue(CollectionUtils.contains(shortArrayOf(10, 20, 30), 30))
+        assertFalse(CollectionUtils.contains(shortArrayOf(10, 20, 30), 40))
+        assertFalse(CollectionUtils.contains(null, 10.toShort()))
+        assertTrue(CollectionUtils.contains(intArrayOf(10, 20, 30), 10))
+        assertTrue(CollectionUtils.contains(intArrayOf(10, 20, 30), 30))
+        assertFalse(CollectionUtils.contains(intArrayOf(10, 20, 30), 40))
+        assertFalse(CollectionUtils.contains(null, 10.toInt()))
+        assertTrue(CollectionUtils.contains(arrayOf("A", "B", "C"), "A"))
+        assertTrue(CollectionUtils.contains(arrayOf("A", "B", "C"), "C"))
+        assertFalse(CollectionUtils.contains(arrayOf("A", "B", "C"), "D"))
+        assertFalse(CollectionUtils.contains(null, "A"))
+
+        val list = listOf("A", "B", "Ab", "C", "D", "E", "A", "E")
+        assertTrue(CollectionUtils.contains(list) { it.length == 2 })
+        assertFalse(CollectionUtils.contains(list) { it.length < 1 })
+        assertTrue(CollectionUtils.contains(list) { it > "A" })
+        assertFalse(CollectionUtils.contains(list) { it > "F" })
+    }
+
+    @Test
+    fun testTotal() {
+        assertEquals(10, CollectionUtils.total(longArrayOf(3, 6, 1)))
+        assertEquals(10, CollectionUtils.total(longArrayOf(6, 1, 3)))
+        assertEquals(10, CollectionUtils.total(longArrayOf(1, 3, 6)))
+        assertEquals(3, CollectionUtils.total(longArrayOf(1, 1, 1)))
+        assertEquals(0, CollectionUtils.total(null))
+    }
+
+    @Test
+    fun testFindFirstFindLast() {
+        val listAE = listOf("A", "B", "C", "D", "E")
+        assertSame(CollectionUtils.findFirst(listAE) { it == "A" }, listAE[0])
+        assertSame(CollectionUtils.findFirst(listAE) { it == "B" }, listAE[1])
+        assertSame(CollectionUtils.findFirst(listAE) { it == "E" }, listAE[4])
+        assertNull(CollectionUtils.findFirst(listAE) { it == "F" })
+        assertSame(CollectionUtils.findLast(listAE) { it == "A" }, listAE[0])
+        assertSame(CollectionUtils.findLast(listAE) { it == "B" }, listAE[1])
+        assertSame(CollectionUtils.findLast(listAE) { it == "E" }, listAE[4])
+        assertNull(CollectionUtils.findLast(listAE) { it == "F" })
+
+        val listMulti = listOf("A", "B", "A", "C", "D", "E", "A", "E")
+        assertSame(CollectionUtils.findFirst(listMulti) { it == "A" }, listMulti[0])
+        assertSame(CollectionUtils.findFirst(listMulti) { it == "B" }, listMulti[1])
+        assertSame(CollectionUtils.findFirst(listMulti) { it == "E" }, listMulti[5])
+        assertNull(CollectionUtils.findFirst(listMulti) { it == "F" })
+        assertSame(CollectionUtils.findLast(listMulti) { it == "A" }, listMulti[6])
+        assertSame(CollectionUtils.findLast(listMulti) { it == "B" }, listMulti[1])
+        assertSame(CollectionUtils.findLast(listMulti) { it == "E" }, listMulti[7])
+        assertNull(CollectionUtils.findLast(listMulti) { it == "F" })
+    }
+
+    @Test
+    fun testMap() {
+        val listAE = listOf("A", "B", "C", "D", "E", null)
+        assertEquals(listAE.map { "-$it-" }, CollectionUtils.map(listAE) { "-$it-" })
+    }
+
+    @Test
+    fun testZip() {
+        val listAE = listOf("A", "B", "C", "D", "E")
+        val list15 = listOf(1, 2, 3, 4, 5)
+        // Normal #zip returns kotlin.Pair, not android.util.Pair
+        assertEquals(list15.zip(listAE).map { android.util.Pair(it.first, it.second) },
+                CollectionUtils.zip(list15, listAE))
+        val listNull = listOf("A", null, "B", "C", "D")
+        assertEquals(list15.zip(listNull).map { android.util.Pair(it.first, it.second) },
+                CollectionUtils.zip(list15, listNull))
+        assertEquals(emptyList<android.util.Pair<Int, Int>>(),
+                CollectionUtils.zip(emptyList<Int>(), emptyList<Int>()))
+        assertFailsWith<IllegalArgumentException> {
+            // Different size
+            CollectionUtils.zip(listOf(1, 2), list15)
+        }
+    }
+
+    @Test
+    fun testAssoc() {
+        val listADA = listOf("A", "B", "C", "D", "A")
+        val list15 = listOf(1, 2, 3, 4, 5)
+        assertEquals(list15.zip(listADA).toMap(), CollectionUtils.assoc(list15, listADA))
+
+        // Null key is fine
+        val assoc = CollectionUtils.assoc(listOf(1, 2, null), listOf("A", "B", "C"))
+        assertEquals("C", assoc[null])
+
+        assertFailsWith<IllegalArgumentException> {
+            // Same key multiple times
+            CollectionUtils.assoc(listOf("A", "B", "A"), listOf(1, 2, 3))
+        }
+        assertFailsWith<IllegalArgumentException> {
+            // Same key multiple times, but it's null
+            CollectionUtils.assoc(listOf(null, "B", null), listOf(1, 2, 3))
+        }
+        assertFailsWith<IllegalArgumentException> {
+            // Different size
+            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/ConnectivityUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ConnectivityUtilsTest.java
new file mode 100644
index 0000000..8af0196
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ConnectivityUtilsTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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 static android.net.InetAddresses.parseNumericAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for ConnectivityUtils */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConnectivityUtilsTest {
+    @Test
+    public void testIsIPv6ULA() {
+        assertTrue(isIPv6ULA(parseNumericAddress("fc00::")));
+        assertTrue(isIPv6ULA(parseNumericAddress("fc00::1")));
+        assertTrue(isIPv6ULA(parseNumericAddress("fc00:1234::5678")));
+        assertTrue(isIPv6ULA(parseNumericAddress("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+
+        assertFalse(isIPv6ULA(parseNumericAddress("fe00::")));
+        assertFalse(isIPv6ULA(parseNumericAddress("2480:1248::123:456")));
+    }
+}
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
new file mode 100644
index 0000000..9fb61d9
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -0,0 +1,554 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import 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;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.net.module.util.FeatureVersions.CONNECTIVITY_MODULE_ID;
+import static com.android.net.module.util.FeatureVersions.NETWORK_STACK_MODULE_ID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.provider.DeviceConfig;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+
+import java.util.Arrays;
+
+
+/**
+ * Tests for DeviceConfigUtils.
+ *
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DeviceConfigUtilsTest {
+    private static final String TEST_NAME_SPACE = "connectivity";
+    private static final String TEST_EXPERIMENT_FLAG = "experiment_flag";
+    private static final int TEST_FLAG_VALUE = 28;
+    private static final String TEST_FLAG_VALUE_STRING = "28";
+    private static final int TEST_DEFAULT_FLAG_VALUE = 0;
+    private static final int TEST_MAX_FLAG_VALUE = 1000;
+    private static final int TEST_MIN_FLAG_VALUE = 100;
+    private static final long TEST_PACKAGE_VERSION = 290000000;
+    private static final String TEST_PACKAGE_NAME = "test.package.name";
+    // The APEX name is the name of the APEX module, as in android.content.pm.ModuleInfo, and is
+    // used for its mount point in /apex. APEX packages are actually APKs with a different
+    // file extension, so they have an AndroidManifest: the APEX package name is the package name in
+    // that manifest, and is reflected in android.content.pm.ApplicationInfo. Contrary to the APEX
+    // (module) name, different package names are typically used to identify the organization that
+    // built and signed the APEX modules.
+    private static final String TEST_APEX_PACKAGE_NAME = "com.prefix.android.tethering";
+    private static final String TEST_GO_APEX_PACKAGE_NAME = "com.prefix.android.go.tethering";
+    private static final String TEST_CONNRES_PACKAGE_NAME =
+            "com.prefix.android.connectivity.resources";
+    private static final String TEST_NETWORKSTACK_NAME = "com.prefix.android.networkstack";
+    private static final String TEST_GO_NETWORKSTACK_NAME = "com.prefix.android.go.networkstack";
+    private final PackageInfo mPackageInfo = new PackageInfo();
+    private final PackageInfo mApexPackageInfo = new PackageInfo();
+    private MockitoSession mSession;
+
+    @Mock private Context mContext;
+    @Mock private PackageManager mPm;
+    @Mock private Resources mResources;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mSession = mockitoSession().spyStatic(DeviceConfig.class).startMocking();
+
+        mPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+        mApexPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+
+        doReturn(mPm).when(mContext).getPackageManager();
+        doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
+        doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(anyString(), anyInt());
+        doReturn(mPackageInfo).when(mPm).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt());
+        doReturn(mApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_APEX_PACKAGE_NAME), anyInt());
+
+        doReturn(mResources).when(mContext).getResources();
+
+        final ResolveInfo ri = new ResolveInfo();
+        ri.activityInfo = new ActivityInfo();
+        ri.activityInfo.applicationInfo = new ApplicationInfo();
+        ri.activityInfo.applicationInfo.packageName = TEST_CONNRES_PACKAGE_NAME;
+        ri.activityInfo.applicationInfo.sourceDir =
+                "/apex/com.android.tethering/priv-app/ServiceConnectivityResources@version";
+        doReturn(Arrays.asList(ri)).when(mPm).queryIntentActivities(argThat(
+                intent -> intent.getAction().equals(DeviceConfigUtils.RESOURCES_APK_INTENT)),
+                eq(MATCH_SYSTEM_ONLY));
+    }
+
+    @After
+    public void tearDown() {
+        mSession.finishMocking();
+        DeviceConfigUtils.resetPackageVersionCacheForTest();
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_Null() {
+        doReturn(null).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_DEFAULT_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_NotNull() {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_NormalValue() {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, 0 /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_NullValue() {
+        doReturn(null).when(() -> DeviceConfig.getProperty(
+                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_DEFAULT_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, 0 /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_OverMaximumValue() {
+        doReturn(Integer.toString(TEST_MAX_FLAG_VALUE + 10)).when(() -> DeviceConfig.getProperty(
+                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_DEFAULT_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_MIN_FLAG_VALUE /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_EqualsMaximumValue() {
+        doReturn(Integer.toString(TEST_MAX_FLAG_VALUE)).when(() -> DeviceConfig.getProperty(
+                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_MAX_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_MIN_FLAG_VALUE /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_BelowMinimumValue() {
+        doReturn(Integer.toString(TEST_MIN_FLAG_VALUE - 10)).when(() -> DeviceConfig.getProperty(
+                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_DEFAULT_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_MIN_FLAG_VALUE /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_EqualsMinimumValue() {
+        doReturn(Integer.toString(TEST_MIN_FLAG_VALUE)).when(() -> DeviceConfig.getProperty(
+                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_MIN_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_MIN_FLAG_VALUE /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyBoolean_Null() {
+        doReturn(null).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertFalse(DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
+                false /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyBoolean_NotNull() {
+        doReturn("true").when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertTrue(DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
+                false /* default value */));
+    }
+
+    @Test
+    public void testIsFeatureEnabled() {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                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 {
+        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));
+
+        // If the flag is unset, package info is not queried
+        verify(mContext, never()).getPackageManager();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testIsFeatureEnabledFeatureForceEnabled() throws Exception {
+        doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                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();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testIsFeatureEnabledFeatureForceDisabled() throws Exception {
+        doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                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();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testFeatureIsEnabledWithException() throws Exception {
+        doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(anyString(), anyInt());
+
+        // Feature should be enabled by flag value "1".
+        doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                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
+    public void testFeatureIsEnabledOnGo() throws Exception {
+        doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(
+                eq(TEST_APEX_PACKAGE_NAME), anyInt());
+        doReturn(mApexPackageInfo).when(mPm).getPackageInfo(
+                eq(TEST_GO_APEX_PACKAGE_NAME), anyInt());
+        doReturn("0").when(() -> DeviceConfig.getProperty(
+                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));
+        assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+    }
+
+    @Test
+    public void testIsNetworkStackFeatureEnabledCaching() throws Exception {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(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 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));
+        assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+
+        // Package info is only queried once
+        verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+        verify(mContext, never()).getPackageName();
+    }
+
+    @Test
+    public void testGetResBooleanConfig() {
+        final int someResId = 1234;
+        doReturn(true).when(mResources).getBoolean(someResId);
+        assertTrue(DeviceConfigUtils.getResBooleanConfig(mContext, someResId, false));
+        doReturn(false).when(mResources).getBoolean(someResId);
+        assertFalse(DeviceConfigUtils.getResBooleanConfig(mContext, someResId, false));
+        doThrow(new Resources.NotFoundException()).when(mResources).getBoolean(someResId);
+        assertFalse(DeviceConfigUtils.getResBooleanConfig(mContext, someResId, false));
+    }
+
+    @Test
+    public void testGetResIntegerConfig() {
+        final int someResId = 1234;
+        doReturn(2097).when(mResources).getInteger(someResId);
+        assertEquals(2097, DeviceConfigUtils.getResIntegerConfig(mContext, someResId, 2098));
+        doThrow(new Resources.NotFoundException()).when(mResources).getInteger(someResId);
+        assertEquals(2098, DeviceConfigUtils.getResIntegerConfig(mContext, someResId, 2098));
+    }
+
+    @Test
+    public void testGetNetworkStackModuleVersionCaching() throws Exception {
+        final PackageInfo networkStackPackageInfo = new PackageInfo();
+        networkStackPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+        doReturn(networkStackPackageInfo).when(mPm).getPackageInfo(
+                eq(TEST_NETWORKSTACK_NAME), anyInt());
+        assertEquals(TEST_PACKAGE_VERSION,
+                DeviceConfigUtils.getNetworkStackModuleVersion(mContext));
+
+        assertEquals(TEST_PACKAGE_VERSION,
+                DeviceConfigUtils.getNetworkStackModuleVersion(mContext));
+        // Package info is only queried once
+        verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+        verify(mContext, never()).getPackageName();
+    }
+
+    @Test
+    public void testGetNetworkStackModuleVersionOnNonMainline() {
+        assertEquals(DeviceConfigUtils.DEFAULT_PACKAGE_VERSION,
+                DeviceConfigUtils.getNetworkStackModuleVersion(mContext));
+    }
+
+    @Test
+    public void testGetNetworkStackModuleVersion() throws Exception {
+        final PackageInfo networkStackPackageInfo = new PackageInfo();
+        final PackageInfo goNetworkStackPackageInfo = new PackageInfo();
+        networkStackPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+        goNetworkStackPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION + 1);
+        doReturn(goNetworkStackPackageInfo).when(mPm).getPackageInfo(
+                eq(TEST_NETWORKSTACK_NAME), anyInt());
+        // Verify the returned value is go module version.
+        assertEquals(TEST_PACKAGE_VERSION + 1,
+                DeviceConfigUtils.getNetworkStackModuleVersion(mContext));
+    }
+
+    @Test
+    public void testIsFeatureSupported_networkStackFeature() throws Exception {
+        // Supported for DEFAULT_PACKAGE_VERSION
+        assertTrue(DeviceConfigUtils.isFeatureSupported(
+                mContext, TEST_PACKAGE_VERSION + NETWORK_STACK_MODULE_ID));
+
+        final PackageInfo networkStackPackageInfo = new PackageInfo();
+        networkStackPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+        doReturn(networkStackPackageInfo).when(mPm).getPackageInfo(
+                eq(TEST_NETWORKSTACK_NAME), anyInt());
+
+        assertTrue(DeviceConfigUtils.isFeatureSupported(
+                mContext, TEST_PACKAGE_VERSION + NETWORK_STACK_MODULE_ID));
+        assertFalse(DeviceConfigUtils.isFeatureSupported(
+                mContext, TEST_PACKAGE_VERSION + NETWORK_STACK_MODULE_ID + 1));
+    }
+
+    @Test
+    public void testIsFeatureSupported_tetheringFeature() throws Exception {
+        assertTrue(DeviceConfigUtils.isFeatureSupported(
+                mContext, TEST_PACKAGE_VERSION + CONNECTIVITY_MODULE_ID));
+        // Return false because feature requires a future version.
+        assertFalse(DeviceConfigUtils.isFeatureSupported(
+                mContext, 889900000L + CONNECTIVITY_MODULE_ID));
+    }
+
+    @Test
+    public void testIsFeatureSupported_illegalModule() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> DeviceConfigUtils.isFeatureSupported(mContext, TEST_PACKAGE_VERSION));
+    }
+
+    @Test
+    public void testIsFeatureNotChickenedOut() {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
+                TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+    }
+
+    @Test
+    public void testIsFeatureNotChickenedOutFeatureDefaultEnabled() throws Exception {
+        doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
+                TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+
+        // If the flag is unset, package info is not queried
+        verify(mContext, never()).getPackageManager();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testIsFeatureNotChickenedOutFeatureForceEnabled() throws Exception {
+        doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
+                TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+
+        // If the feature is force enabled, package info is not queried
+        verify(mContext, never()).getPackageManager();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testIsFeatureNotChickenedOutFeatureForceDisabled() throws Exception {
+        doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
+                TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+
+        // If the feature is force disabled, package info is not queried
+        verify(mContext, never()).getPackageManager();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
new file mode 100644
index 0000000..88d9e1e
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.net.DnsResolver.CLASS_IN;
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+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 android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.net.InetAddressUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DnsPacketTest {
+    private static final int TEST_DNS_PACKET_ID = 0x7722;
+    private static final int TEST_DNS_PACKET_FLAGS = 0x8180;
+
+    private void assertHeaderParses(DnsPacket.DnsHeader header, int id, int flag,
+            int qCount, int aCount, int nsCount, int arCount) {
+        assertEquals(header.getId(), id);
+        assertEquals(header.getFlags(), flag);
+        assertEquals(header.getRecordCount(DnsPacket.QDSECTION), qCount);
+        assertEquals(header.getRecordCount(DnsPacket.ANSECTION), aCount);
+        assertEquals(header.getRecordCount(DnsPacket.NSSECTION), nsCount);
+        assertEquals(header.getRecordCount(DnsPacket.ARSECTION), arCount);
+    }
+
+    private void assertRecordParses(DnsPacket.DnsRecord record, String dname,
+            int dtype, int dclass, int ttl, byte[] rr) {
+        assertEquals(record.dName, dname);
+        assertEquals(record.nsType, dtype);
+        assertEquals(record.nsClass, dclass);
+        assertEquals(record.ttl, ttl);
+        assertTrue(Arrays.equals(record.getRR(), rr));
+    }
+
+    static class TestDnsPacket extends DnsPacket {
+        TestDnsPacket(byte[] data) throws DnsPacket.ParseException {
+            super(data);
+        }
+
+        TestDnsPacket(@NonNull DnsHeader header, @Nullable ArrayList<DnsRecord> qd,
+                @Nullable ArrayList<DnsRecord> an) {
+            super(header, qd, an);
+        }
+
+        public DnsHeader getHeader() {
+            return mHeader;
+        }
+        public List<DnsRecord> getRecordList(int secType) {
+            return mRecords[secType];
+        }
+    }
+
+    @Test
+    public void testNullDisallowed() {
+        try {
+            new TestDnsPacket(null);
+            fail("Exception not thrown for null byte array");
+        } catch (DnsPacket.ParseException e) {
+        }
+    }
+
+    @Test
+    public void testV4Answer() throws Exception {
+        final byte[] v4blob = new byte[] {
+            /* Header */
+            0x55, 0x66, /* Transaction ID */
+            (byte) 0x81, (byte) 0x80, /* Flags */
+            0x00, 0x01, /* Questions */
+            0x00, 0x01, /* Answer RRs */
+            0x00, 0x00, /* Authority RRs */
+            0x00, 0x00, /* Additional RRs */
+            /* Queries */
+            0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+            0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+            0x00, 0x01, /* Type */
+            0x00, 0x01, /* Class */
+            /* Answers */
+            (byte) 0xc0, 0x0c, /* Name */
+            0x00, 0x01, /* Type */
+            0x00, 0x01, /* Class */
+            0x00, 0x00, 0x01, 0x2b, /* TTL */
+            0x00, 0x04, /* Data length */
+            (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 /* Address */
+        };
+        TestDnsPacket packet = new TestDnsPacket(v4blob);
+
+        // Header part
+        assertHeaderParses(packet.getHeader(), 0x5566, 0x8180, 1, 1, 0, 0);
+
+        // Record part
+        List<DnsPacket.DnsRecord> qdRecordList =
+                packet.getRecordList(DnsPacket.QDSECTION);
+        assertEquals(qdRecordList.size(), 1);
+        assertRecordParses(qdRecordList.get(0), "www.google.com", 1, 1, 0, null);
+
+        List<DnsPacket.DnsRecord> anRecordList =
+                packet.getRecordList(DnsPacket.ANSECTION);
+        assertEquals(anRecordList.size(), 1);
+        assertRecordParses(anRecordList.get(0), "www.google.com", 1, 1, 0x12b,
+                new byte[]{ (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 });
+    }
+
+    @Test
+    public void testV6Answer() throws Exception {
+        final byte[] v6blob = new byte[] {
+            /* Header */
+            0x77, 0x22, /* Transaction ID */
+            (byte) 0x81, (byte) 0x80, /* Flags */
+            0x00, 0x01, /* Questions */
+            0x00, 0x01, /* Answer RRs */
+            0x00, 0x00, /* Authority RRs */
+            0x00, 0x00, /* Additional RRs */
+            /* Queries */
+            0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+            0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+            0x00, 0x1c, /* Type */
+            0x00, 0x01, /* Class */
+            /* Answers */
+            (byte) 0xc0, 0x0c, /* Name */
+            0x00, 0x1c, /* Type */
+            0x00, 0x01, /* Class */
+            0x00, 0x00, 0x00, 0x37, /* TTL */
+            0x00, 0x10, /* Data length */
+            0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x0d,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x04 /* Address */
+        };
+        TestDnsPacket packet = new TestDnsPacket(v6blob);
+
+        // Header part
+        assertHeaderParses(packet.getHeader(), 0x7722, 0x8180, 1, 1, 0, 0);
+
+        // Record part
+        List<DnsPacket.DnsRecord> qdRecordList =
+                packet.getRecordList(DnsPacket.QDSECTION);
+        assertEquals(qdRecordList.size(), 1);
+        assertRecordParses(qdRecordList.get(0), "www.google.com", 28, 1, 0, null);
+
+        List<DnsPacket.DnsRecord> anRecordList =
+                packet.getRecordList(DnsPacket.ANSECTION);
+        assertEquals(anRecordList.size(), 1);
+        assertRecordParses(anRecordList.get(0), "www.google.com", 28, 1, 0x37,
+                new byte[]{ 0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x0d,
+                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x04 });
+    }
+
+    /** Verifies that the synthesized {@link DnsPacket.DnsHeader} can be parsed correctly. */
+    @Test
+    public void testDnsHeaderSynthesize() {
+        final DnsPacket.DnsHeader testHeader = new DnsPacket.DnsHeader(TEST_DNS_PACKET_ID,
+                TEST_DNS_PACKET_FLAGS, 3 /* qcount */, 5 /* ancount */);
+        final DnsPacket.DnsHeader actualHeader = new DnsPacket.DnsHeader(
+                ByteBuffer.wrap(testHeader.getBytes()));
+        assertEquals(testHeader, actualHeader);
+    }
+
+    /** Verifies that the synthesized {@link DnsPacket.DnsRecord} can be parsed correctly. */
+    @Test
+    public void testDnsRecordSynthesize() throws IOException {
+        assertDnsRecordRoundTrip(
+                DnsPacket.DnsRecord.makeAOrAAAARecord(DnsPacket.ANSECTION,
+                        "test.com", CLASS_IN, 5 /* ttl */,
+                        InetAddressUtils.parseNumericAddress("abcd::fedc")));
+        assertDnsRecordRoundTrip(DnsPacket.DnsRecord.makeQuestion("test.com", TYPE_AAAA, CLASS_IN));
+        assertDnsRecordRoundTrip(DnsPacket.DnsRecord.makeCNameRecord(DnsPacket.ANSECTION,
+                "test.com", CLASS_IN, 0 /* ttl */, "example.com"));
+    }
+
+    /** Verifies that the type of implementation returned from DnsRecord#parse is correct */
+    @Test
+    public void testDnsRecordParse() throws IOException {
+        final byte[] svcbQuestionRecord = new byte[] {
+                0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, /* Name */
+                0x00, 0x40, /* Type */
+                0x00, 0x01, /* Class */
+        };
+        assertTrue(DnsPacket.DnsRecord.parse(DnsPacket.QDSECTION,
+                ByteBuffer.wrap(svcbQuestionRecord)) instanceof DnsSvcbRecord);
+
+        final byte[] svcbAnswerRecord = new byte[] {
+                0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, /* Name */
+                0x00, 0x40, /* Type */
+                0x00, 0x01, /* Class */
+                0x00, 0x00, 0x01, 0x2b, /* TTL */
+                0x00, 0x0b, /* Data length */
+                0x00, 0x01, /* SvcPriority */
+                0x03, 'd', 'o', 't', 0x03, 'c', 'o', 'm', 0x00, /* TargetName */
+        };
+        assertTrue(DnsPacket.DnsRecord.parse(DnsPacket.ANSECTION,
+                ByteBuffer.wrap(svcbAnswerRecord)) instanceof DnsSvcbRecord);
+    }
+
+    /**
+     * Verifies ttl/rData error handling when parsing
+     * {@link DnsPacket.DnsRecord} from bytes.
+     */
+    @Test
+    public void testDnsRecordTTLRDataErrorHandling() throws IOException {
+        // Verify the constructor ignore ttl/rData of questions even if they are supplied.
+        final byte[] qdWithTTLRData = new byte[]{
+                0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+                0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+                0x00, 0x00, /* Type */
+                0x00, 0x01, /* Class */
+                0x00, 0x00, 0x01, 0x2b, /* TTL */
+                0x00, 0x04, /* Data length */
+                (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 /* Address */};
+        final DnsPacket.DnsRecord questionsFromBytes =
+                DnsPacket.DnsRecord.parse(DnsPacket.QDSECTION, ByteBuffer.wrap(qdWithTTLRData));
+        assertEquals(0, questionsFromBytes.ttl);
+        assertNull(questionsFromBytes.getRR());
+
+        // Verify ANSECTION must have rData when constructing.
+        final byte[] anWithoutTTLRData = new byte[]{
+                0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+                0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+                0x00, 0x01, /* Type */
+                0x00, 0x01, /* Class */};
+        assertThrows(BufferUnderflowException.class, () ->
+                DnsPacket.DnsRecord.parse(DnsPacket.ANSECTION, ByteBuffer.wrap(anWithoutTTLRData)));
+    }
+
+    private void assertDnsRecordRoundTrip(DnsPacket.DnsRecord before)
+            throws IOException {
+        final DnsPacket.DnsRecord after = DnsPacket.DnsRecord.parse(before.rType,
+                ByteBuffer.wrap(before.getBytes()));
+        assertEquals(after, before);
+    }
+
+    /** Verifies that the synthesized {@link DnsPacket} can be parsed correctly. */
+    @Test
+    public void testDnsPacketSynthesize() throws IOException {
+        // Ipv4 dns response packet generated by scapy:
+        //   dns_r = scapy.DNS(
+        //      id=0xbeef,
+        //      qr=1,
+        //      qd=scapy.DNSQR(qname="hello.example.com"),
+        //      an=scapy.DNSRR(rrname="hello.example.com", type="CNAME", rdata='test.com') /
+        //      scapy.DNSRR(rrname="hello.example.com", rdata='1.2.3.4'))
+        //   scapy.hexdump(dns_r)
+        //   dns_r.show2()
+        // Note that since the synthesizing does not support name compression yet, the domain
+        // name of the sample need to be uncompressed when generating.
+        final byte[] v4BlobUncompressed = new byte[]{
+                /* Header */
+                (byte) 0xbe, (byte) 0xef, /* Transaction ID */
+                (byte) 0x81, 0x00, /* Flags */
+                0x00, 0x01, /* Questions */
+                0x00, 0x02, /* Answer RRs */
+                0x00, 0x00, /* Authority RRs */
+                0x00, 0x00, /* Additional RRs */
+                /* Queries */
+                0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+                0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name: hello.example.com */
+                0x00, 0x01, /* Type */
+                0x00, 0x01, /* Class */
+                /* Answers */
+                0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+                0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name: hello.example.com */
+                0x00, 0x05, /* Type */
+                0x00, 0x01, /* Class */
+                0x00, 0x00, 0x00, 0x00, /* TTL */
+                0x00, 0x0A, /* Data length */
+                0x04, 0x74, 0x65, 0x73, 0x74, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Alias: test.com */
+                0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+                0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name: hello.example.com */
+                0x00, 0x01, /* Type */
+                0x00, 0x01, /* Class */
+                0x00, 0x00, 0x00, 0x00, /* TTL */
+                0x00, 0x04, /* Data length */
+                0x01, 0x02, 0x03, 0x04, /* Address: 1.2.3.4 */
+        };
+
+        // Forge one via constructors.
+        final DnsPacket.DnsHeader testHeader = new DnsPacket.DnsHeader(0xbeef,
+                0x8100, 1 /* qcount */, 2 /* ancount */);
+        final ArrayList<DnsPacket.DnsRecord> qlist = new ArrayList<>();
+        final ArrayList<DnsPacket.DnsRecord> alist = new ArrayList<>();
+        qlist.add(DnsPacket.DnsRecord.makeQuestion(
+                "hello.example.com", TYPE_A, CLASS_IN));
+        alist.add(DnsPacket.DnsRecord.makeCNameRecord(
+                DnsPacket.ANSECTION, "hello.example.com", CLASS_IN, 0 /* ttl */, "test.com"));
+        alist.add(DnsPacket.DnsRecord.makeAOrAAAARecord(
+                DnsPacket.ANSECTION, "hello.example.com", CLASS_IN, 0 /* ttl */,
+                InetAddressUtils.parseNumericAddress("1.2.3.4")));
+        final TestDnsPacket testPacket = new TestDnsPacket(testHeader, qlist, alist);
+
+        // Assert content equals in both ways.
+        assertTrue(Arrays.equals(v4BlobUncompressed, testPacket.getBytes()));
+        assertEquals(new TestDnsPacket(v4BlobUncompressed), testPacket);
+    }
+
+    @Test
+    public void testDnsPacketSynthesize_recordCountMismatch() throws IOException {
+        final DnsPacket.DnsHeader testHeader = new DnsPacket.DnsHeader(0xbeef,
+                0x8100, 1 /* qcount */, 1 /* ancount */);
+        final ArrayList<DnsPacket.DnsRecord> qlist = new ArrayList<>();
+        final ArrayList<DnsPacket.DnsRecord> alist = new ArrayList<>();
+        qlist.add(DnsPacket.DnsRecord.makeQuestion(
+                "hello.example.com", TYPE_A, CLASS_IN));
+
+        // Assert throws if the supplied answer records fewer than the declared count.
+        assertThrows(IllegalArgumentException.class, () ->
+                new TestDnsPacket(testHeader, qlist, alist));
+
+        // Assert throws if the supplied answer records more than the declared count.
+        alist.add(DnsPacket.DnsRecord.makeCNameRecord(
+                DnsPacket.ANSECTION, "hello.example.com", CLASS_IN, 0 /* ttl */, "test.com"));
+        alist.add(DnsPacket.DnsRecord.makeAOrAAAARecord(
+                DnsPacket.ANSECTION, "hello.example.com", CLASS_IN, 0 /* ttl */,
+                InetAddressUtils.parseNumericAddress("1.2.3.4")));
+        assertThrows(IllegalArgumentException.class, () ->
+                new TestDnsPacket(testHeader, qlist, alist));
+
+        // Assert counts matched if the byte buffer still has data when parsing ended.
+        final byte[] blobTooMuchData = new byte[]{
+                /* Header */
+                (byte) 0xbe, (byte) 0xef, /* Transaction ID */
+                (byte) 0x81, 0x00, /* Flags */
+                0x00, 0x00, /* Questions */
+                0x00, 0x00, /* Answer RRs */
+                0x00, 0x00, /* Authority RRs */
+                0x00, 0x00, /* Additional RRs */
+                /* Queries */
+                0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+                0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name */
+                0x00, 0x01, /* Type */
+                0x00, 0x01, /* Class */
+        };
+        final TestDnsPacket packetFromTooMuchData = new TestDnsPacket(blobTooMuchData);
+        for (int i = 0; i < DnsPacket.NUM_SECTIONS; i++) {
+            assertEquals(0, packetFromTooMuchData.getRecordList(i).size());
+            assertEquals(0, packetFromTooMuchData.getHeader().getRecordCount(i));
+        }
+
+        // Assert throws if the byte buffer ended when expecting more records.
+        final byte[] blobNotEnoughData = new byte[]{
+                /* Header */
+                (byte) 0xbe, (byte) 0xef, /* Transaction ID */
+                (byte) 0x81, 0x00, /* Flags */
+                0x00, 0x01, /* Questions */
+                0x00, 0x02, /* Answer RRs */
+                0x00, 0x00, /* Authority RRs */
+                0x00, 0x00, /* Additional RRs */
+                /* Queries */
+                0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+                0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name */
+                0x00, 0x01, /* Type */
+                0x00, 0x01, /* Class */
+                /* Answers */
+                0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+                0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name */
+                0x00, 0x01, /* Type */
+                0x00, 0x01, /* Class */
+                0x00, 0x00, 0x00, 0x00, /* TTL */
+                0x00, 0x04, /* Data length */
+                0x01, 0x02, 0x03, 0x04, /* Address */
+        };
+        assertThrows(DnsPacket.ParseException.class, () -> new TestDnsPacket(blobNotEnoughData));
+    }
+
+    @Test
+    public void testEqualsAndHashCode() throws IOException {
+        // Verify DnsHeader equals and hashCode.
+        final DnsPacket.DnsHeader testHeader = new DnsPacket.DnsHeader(TEST_DNS_PACKET_ID,
+                TEST_DNS_PACKET_FLAGS, 1 /* qcount */, 1 /* ancount */);
+        final DnsPacket.DnsHeader emptyHeader = new DnsPacket.DnsHeader(TEST_DNS_PACKET_ID + 1,
+                TEST_DNS_PACKET_FLAGS + 0x08, 0 /* qcount */, 0 /* ancount */);
+        final DnsPacket.DnsHeader headerFromBytes =
+                new DnsPacket.DnsHeader(ByteBuffer.wrap(testHeader.getBytes()));
+        assertEquals(testHeader, headerFromBytes);
+        assertEquals(testHeader.hashCode(), headerFromBytes.hashCode());
+        assertNotEquals(testHeader, emptyHeader);
+        assertNotEquals(testHeader.hashCode(), emptyHeader.hashCode());
+        assertNotEquals(headerFromBytes, emptyHeader);
+        assertNotEquals(headerFromBytes.hashCode(), emptyHeader.hashCode());
+
+        // Verify DnsRecord equals and hashCode.
+        final DnsPacket.DnsRecord testQuestion = DnsPacket.DnsRecord.makeQuestion(
+                "test.com", TYPE_AAAA, CLASS_IN);
+        final DnsPacket.DnsRecord testAnswer = DnsPacket.DnsRecord.makeCNameRecord(
+                DnsPacket.ANSECTION, "test.com", CLASS_IN, 9, "www.test.com");
+        final DnsPacket.DnsRecord questionFromBytes = DnsPacket.DnsRecord.parse(DnsPacket.QDSECTION,
+                ByteBuffer.wrap(testQuestion.getBytes()));
+        assertEquals(testQuestion, questionFromBytes);
+        assertEquals(testQuestion.hashCode(), questionFromBytes.hashCode());
+        assertNotEquals(testQuestion, testAnswer);
+        assertNotEquals(testQuestion.hashCode(), testAnswer.hashCode());
+        assertNotEquals(questionFromBytes, testAnswer);
+        assertNotEquals(questionFromBytes.hashCode(), testAnswer.hashCode());
+
+        // Verify DnsPacket equals and hashCode.
+        final ArrayList<DnsPacket.DnsRecord> qlist = new ArrayList<>();
+        final ArrayList<DnsPacket.DnsRecord> alist = new ArrayList<>();
+        qlist.add(testQuestion);
+        alist.add(testAnswer);
+        final TestDnsPacket testPacket = new TestDnsPacket(testHeader, qlist, alist);
+        final TestDnsPacket emptyPacket = new TestDnsPacket(
+                emptyHeader, new ArrayList<>(), new ArrayList<>());
+        final TestDnsPacket packetFromBytes = new TestDnsPacket(testPacket.getBytes());
+        assertEquals(testPacket, packetFromBytes);
+        assertEquals(testPacket.hashCode(), packetFromBytes.hashCode());
+        assertNotEquals(testPacket, emptyPacket);
+        assertNotEquals(testPacket.hashCode(), emptyPacket.hashCode());
+        assertNotEquals(packetFromBytes, emptyPacket);
+        assertNotEquals(packetFromBytes.hashCode(), emptyPacket.hashCode());
+
+        // Verify DnsPacket with empty list.
+        final TestDnsPacket emptyPacketFromBytes = new TestDnsPacket(emptyPacket.getBytes());
+        assertEquals(emptyPacket, emptyPacketFromBytes);
+        assertEquals(emptyPacket.hashCode(), emptyPacketFromBytes.hashCode());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketUtilsTest.java
new file mode 100644
index 0000000..9e1ab82
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketUtilsTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static com.android.net.module.util.DnsPacketUtils.DnsRecordParser;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.net.ParseException;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DnsPacketUtilsTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+    /**
+     * Verifies that the compressed NAME field in the answer section of the DNS message is parsed
+     * successfully when name compression is permitted. Additionally, verifies that a
+     * {@link DnsPacket.ParseException} is thrown in a hypothetical scenario where name compression
+     * is not expected.
+     */
+    @Test
+    public void testParsingAnswerSectionNameCompressed() throws Exception {
+        final byte[] v4blobNameCompressedAnswer = new byte[] {
+                /* Header */
+                0x55, 0x66, /* Transaction ID */
+                (byte) 0x81, (byte) 0x80, /* Flags */
+                0x00, 0x01, /* Questions */
+                0x00, 0x01, /* Answer RRs */
+                0x00, 0x00, /* Authority RRs */
+                0x00, 0x00, /* Additional RRs */
+                /* Queries */
+                0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+                0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+                0x00, 0x01, /* Type */
+                0x00, 0x01, /* Class */
+                /* Answers */
+                (byte) 0xc0, 0x0c, /* Name */
+                0x00, 0x01, /* Type */
+                0x00, 0x01, /* Class */
+                0x00, 0x00, 0x01, 0x2b, /* TTL */
+                0x00, 0x04, /* Data length */
+                (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 /* Address */
+        };
+        final int answerOffsetBytePosition = 32;
+        final ByteBuffer nameCompressedBuf = ByteBuffer.wrap(v4blobNameCompressedAnswer);
+
+        nameCompressedBuf.position(answerOffsetBytePosition);
+        assertThrows(DnsPacket.ParseException.class, () -> DnsRecordParser.parseName(
+                nameCompressedBuf, /* depth= */ 0, /* isNameCompressionSupported= */false));
+
+        nameCompressedBuf.position(answerOffsetBytePosition);
+        String domainName = DnsRecordParser.parseName(
+                nameCompressedBuf, /* depth= */ 0, /* isNameCompressionSupported= */true);
+        assertEquals(domainName, "www.google.com");
+    }
+
+    /**
+     * Verifies that an uncompressed NAME field in the answer section of the DNS message is parsed
+     * successfully irrespective of whether name compression is permitted.
+     */
+    @Test
+    public void testParsingAnswerSectionNoNameCompression() throws Exception {
+        final byte[] v4blobNoNameCompression = new byte[] {
+                /* Header */
+                0x55, 0x66, /* Transaction ID */
+                (byte) 0x81, (byte) 0x80, /* Flags */
+                0x00, 0x01, /* Questions */
+                0x00, 0x01, /* Answer RRs */
+                0x00, 0x00, /* Authority RRs */
+                0x00, 0x00, /* Additional RRs */
+                /* Queries */
+                0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+                0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+                0x00, 0x01, /* Type */
+                0x00, 0x01, /* Class */
+                /* Answers */
+                0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+                0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+                0x00, 0x01, /* Type */
+                0x00, 0x01, /* Class */
+                0x00, 0x00, 0x01, 0x2b, /* TTL */
+                0x00, 0x04, /* Data length */
+                (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 /* Address */
+        };
+        final int answerOffsetBytePosition = 32;
+        final ByteBuffer notNameCompressedBuf = ByteBuffer.wrap(v4blobNoNameCompression);
+
+        notNameCompressedBuf.position(answerOffsetBytePosition);
+        String domainName = DnsRecordParser.parseName(
+                notNameCompressedBuf, /* depth= */ 0, /* isNameCompressionSupported= */ true);
+        assertEquals(domainName, "www.google.com");
+
+        notNameCompressedBuf.position(answerOffsetBytePosition);
+        domainName = DnsRecordParser.parseName(
+                notNameCompressedBuf, /* depth= */ 0, /* isNameCompressionSupported= */ false);
+        assertEquals(domainName, "www.google.com");
+    }
+
+    // Skip test on R- devices since ParseException only available on S+ devices.
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testDomainNameToLabels() throws Exception {
+        assertArrayEquals(
+                new byte[]{3, 'w', 'w', 'w', 6, 'g', 'o', 'o', 'g', 'l', 'e', 3, 'c', 'o', 'm', 0},
+                DnsRecordParser.domainNameToLabels("www.google.com"));
+        assertThrows(ParseException.class, () ->
+                DnsRecordParser.domainNameToLabels("aaa."));
+        assertThrows(ParseException.class, () ->
+                DnsRecordParser.domainNameToLabels("aaa"));
+        assertThrows(ParseException.class, () ->
+                DnsRecordParser.domainNameToLabels("."));
+        assertThrows(ParseException.class, () ->
+                DnsRecordParser.domainNameToLabels(""));
+    }
+
+    @Test
+    public void testIsHostName() {
+        Assert.assertTrue(DnsRecordParser.isHostName("www.google.com"));
+        Assert.assertFalse(DnsRecordParser.isHostName("com"));
+        Assert.assertFalse(DnsRecordParser.isHostName("1.2.3.4"));
+        Assert.assertFalse(DnsRecordParser.isHostName("1234::5678"));
+        Assert.assertFalse(DnsRecordParser.isHostName(null));
+        Assert.assertFalse(DnsRecordParser.isHostName(""));
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
new file mode 100644
index 0000000..d59795f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
@@ -0,0 +1,608 @@
+/*
+ * 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.net.module.util;
+
+import static android.net.DnsResolver.CLASS_IN;
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+
+import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.net.InetAddresses;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class DnsSvcbPacketTest {
+    private static final short TEST_TRANSACTION_ID = 0x4321;
+    private static final byte[] TEST_DNS_RESPONSE_HEADER_FLAG =  new byte[] { (byte) 0x81, 0x00 };
+
+    // A common DNS SVCB Question section with Name = "_dns.resolver.arpa".
+    private static final byte[] TEST_DNS_SVCB_QUESTION_SECTION = new byte[] {
+            0x04, '_', 'd', 'n', 's', 0x08, 'r', 'e', 's', 'o', 'l', 'v', 'e', 'r',
+            0x04, 'a', 'r', 'p', 'a', 0x00, 0x00, 0x40, 0x00, 0x01,
+    };
+
+    // mandatory=ipv4hint,alpn,key333
+    private static final byte[] TEST_SVC_PARAM_MANDATORY = new byte[] {
+            0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x01, 0x01, 0x4d,
+    };
+
+    // alpn=doq
+    private static final byte[] TEST_SVC_PARAM_ALPN_DOQ = new byte[] {
+            0x00, 0x01, 0x00, 0x04, 0x03, 'd', 'o', 'q'
+    };
+
+    // alpn=h2,http/1.1
+    private static final byte[] TEST_SVC_PARAM_ALPN_HTTPS = new byte[] {
+            0x00, 0x01, 0x00, 0x0c, 0x02, 'h', '2',
+            0x08, 'h', 't', 't', 'p', '/', '1', '.', '1',
+    };
+
+    // no-default-alpn
+    private static final byte[] TEST_SVC_PARAM_NO_DEFAULT_ALPN = new byte[] {
+            0x00, 0x02, 0x00, 0x00,
+    };
+
+    // port=5353
+    private static final byte[] TEST_SVC_PARAM_PORT = new byte[] {
+            0x00, 0x03, 0x00, 0x02, 0x14, (byte) 0xe9,
+    };
+
+    // ipv4hint=1.2.3.4,6.7.8.9
+    private static final byte[] TEST_SVC_PARAM_IPV4HINT_1 = new byte[] {
+            0x00, 0x04, 0x00, 0x08, 0x01, 0x02, 0x03, 0x04, 0x06, 0x07, 0x08, 0x09,
+    };
+
+    // ipv4hint=4.3.2.1
+    private static final byte[] TEST_SVC_PARAM_IPV4HINT_2 = new byte[] {
+            0x00, 0x04, 0x00, 0x04, 0x04, 0x03, 0x02, 0x01,
+    };
+
+    // ech=aBcDe
+    private static final byte[] TEST_SVC_PARAM_ECH = new byte[] {
+            0x00, 0x05, 0x00, 0x05, 'a', 'B', 'c', 'D', 'e',
+    };
+
+    // ipv6hint=2001:db8::1
+    private static final byte[] TEST_SVC_PARAM_IPV6HINT = new byte[] {
+            0x00, 0x06, 0x00, 0x10, 0x20, 0x01, 0x0d, (byte) 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    };
+
+    // dohpath=/some-path{?dns}
+    private static final byte[] TEST_SVC_PARAM_DOHPATH = new byte[] {
+            0x00, 0x07, 0x00, 0x10,
+            '/', 's', 'o', 'm', 'e', '-', 'p', 'a', 't', 'h', '{', '?', 'd', 'n', 's', '}',
+    };
+
+    // key12345=1A2B0C
+    private static final byte[] TEST_SVC_PARAM_GENERIC_WITH_VALUE = new byte[] {
+            0x30, 0x39, 0x00, 0x03, 0x1a, 0x2b, 0x0c,
+    };
+
+    // key12346
+    private static final byte[] TEST_SVC_PARAM_GENERIC_WITHOUT_VALUE = new byte[] {
+            0x30, 0x3a, 0x00, 0x00,
+    };
+
+    private static byte[] makeDnsResponseHeaderAsByteArray(int qdcount, int ancount, int nscount,
+                int arcount) {
+        final ByteBuffer buffer = ByteBuffer.wrap(new byte[12]);
+        buffer.putShort(TEST_TRANSACTION_ID); /* Transaction ID */
+        buffer.put(TEST_DNS_RESPONSE_HEADER_FLAG); /* Flags */
+        buffer.putShort((short) qdcount);
+        buffer.putShort((short) ancount);
+        buffer.putShort((short) nscount);
+        buffer.putShort((short) arcount);
+        return buffer.array();
+    }
+
+    private static DnsSvcbRecord makeDnsSvcbRecordFromByteArray(@NonNull byte[] data)
+                throws IOException {
+        return new DnsSvcbRecord(DnsPacket.ANSECTION, ByteBuffer.wrap(data));
+    }
+
+    private static DnsSvcbRecord makeDnsSvcbRecordWithSingleSvcParam(@NonNull byte[] svcParam)
+            throws IOException {
+        return makeDnsSvcbRecordFromByteArray(new TestDnsRecordByteArrayBuilder()
+                .setRRType(TYPE_SVCB)
+                .setTargetName("test.com")
+                .addRdata(svcParam)
+                .build());
+    }
+
+    // Converts a Short to a byte array in big endian.
+    private static byte[] shortToByteArray(short value) {
+        return new byte[] { (byte) (value >> 8), (byte) value };
+    }
+
+    private static byte[] getRemainingByteArray(@NonNull ByteBuffer buffer) {
+        final byte[] out = new byte[buffer.remaining()];
+        buffer.get(out);
+        return out;
+    }
+
+    // A utility to make a DNS record as byte array.
+    private static class TestDnsRecordByteArrayBuilder {
+        private static final byte[] NAME_COMPRESSION_POINTER = new byte[] { (byte) 0xc0, 0x0c };
+
+        private final String mRRName = "dns.com";
+        private short mRRType = 0;
+        private final short mRRClass = CLASS_IN;
+        private final int mRRTtl = 10;
+        private int mRdataLen = 0;
+        private final ArrayList<byte[]> mRdata = new ArrayList<>();
+        private String mTargetName = null;
+        private short mSvcPriority = 1;
+        private boolean mNameCompression = false;
+
+        TestDnsRecordByteArrayBuilder setNameCompression(boolean value) {
+            mNameCompression = value;
+            return this;
+        }
+
+        TestDnsRecordByteArrayBuilder setRRType(int value) {
+            mRRType = (short) value;
+            return this;
+        }
+
+        TestDnsRecordByteArrayBuilder setTargetName(@NonNull String value) throws IOException {
+            mTargetName = value;
+            return this;
+        }
+
+        TestDnsRecordByteArrayBuilder setSvcPriority(int value) {
+            mSvcPriority = (short) value;
+            return this;
+        }
+
+        TestDnsRecordByteArrayBuilder addRdata(@NonNull byte[] value) {
+            mRdata.add(value);
+            mRdataLen += value.length;
+            return this;
+        }
+
+        byte[] build() throws IOException {
+            final ByteArrayOutputStream os = new ByteArrayOutputStream();
+            final byte[] name = mNameCompression ? NAME_COMPRESSION_POINTER
+                    : DnsPacketUtils.DnsRecordParser.domainNameToLabels(mRRName);
+            os.write(name);
+            os.write(shortToByteArray(mRRType));
+            os.write(shortToByteArray(mRRClass));
+            os.write(HexDump.toByteArray(mRRTtl));
+            if (mTargetName == null) {
+                os.write(shortToByteArray((short) mRdataLen));
+            } else {
+                final byte[] targetNameLabels =
+                        DnsPacketUtils.DnsRecordParser.domainNameToLabels(mTargetName);
+                mRdataLen += (Short.BYTES + targetNameLabels.length);
+                os.write(shortToByteArray((short) mRdataLen));
+                os.write(shortToByteArray(mSvcPriority));
+                os.write(targetNameLabels);
+            }
+            for (byte[] data : mRdata) {
+                os.write(data);
+            }
+            return os.toByteArray();
+        }
+    }
+
+    @Test
+    public void testSliceAndAdvance() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9});
+        final ByteBuffer slice1 = DnsSvcbRecord.sliceAndAdvance(buffer, 3);
+        final ByteBuffer slice2 = DnsSvcbRecord.sliceAndAdvance(buffer, 4);
+        assertEquals(0, slice1.position());
+        assertEquals(3, slice1.capacity());
+        assertEquals(3, slice1.remaining());
+        assertTrue(slice1.isReadOnly());
+        assertArrayEquals(new byte[] {1, 2, 3}, getRemainingByteArray(slice1));
+        assertEquals(0, slice2.position());
+        assertEquals(4, slice2.capacity());
+        assertEquals(4, slice2.remaining());
+        assertTrue(slice2.isReadOnly());
+        assertArrayEquals(new byte[] {4, 5, 6, 7}, getRemainingByteArray(slice2));
+
+        // Nothing is read if out-of-bound access happens.
+        assertThrows(BufferUnderflowException.class,
+                () -> DnsSvcbRecord.sliceAndAdvance(buffer, 5));
+        assertEquals(7, buffer.position());
+        assertEquals(9, buffer.capacity());
+        assertEquals(2, buffer.remaining());
+        assertArrayEquals(new byte[] {8, 9}, getRemainingByteArray(buffer));
+    }
+
+    @Test
+    public void testDnsSvcbRecord_svcParamMandatory() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_MANDATORY);
+        // Check the content returned from toString() for now because the getter function for
+        // this SvcParam hasn't been implemented.
+        // TODO(b/240259333): Consider adding DnsSvcbRecord.isMandatory(String alpn) when needed.
+        assertTrue(record.toString().contains("ipv4hint"));
+        assertTrue(record.toString().contains("alpn"));
+        assertTrue(record.toString().contains("key333"));
+    }
+
+    @Test
+    public void testDnsSvcbRecord_svcParamAlpn() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_ALPN_HTTPS);
+        assertEquals(Arrays.asList("h2", "http/1.1"), record.getAlpns());
+    }
+
+    @Test
+    public void testDnsSvcbRecord_svcParamNoDefaultAlpn() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
+                TEST_SVC_PARAM_NO_DEFAULT_ALPN);
+        // Check the content returned from toString() for now because the getter function for
+        // this SvcParam hasn't been implemented.
+        // TODO(b/240259333): Consider adding DnsSvcbRecord.hasNoDefaultAlpn() when needed.
+        assertTrue(record.toString().contains("no-default-alpn"));
+    }
+
+    @Test
+    public void testDnsSvcbRecord_svcParamPort() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_PORT);
+        assertEquals(5353, record.getPort());
+    }
+
+    @Test
+    public void testDnsSvcbRecord_svcParamIpv4Hint() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_IPV4HINT_2);
+        assertEquals(Arrays.asList(InetAddresses.parseNumericAddress("4.3.2.1")),
+                record.getAddresses());
+    }
+
+    @Test
+    public void testDnsSvcbRecord_svcParamEch() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_ECH);
+        // Check the content returned from toString() for now because the getter function for
+        // this SvcParam hasn't been implemented.
+        // TODO(b/240259333): Consider adding DnsSvcbRecord.getEch() when needed.
+        assertTrue(record.toString().contains("ech=6142634465"));
+    }
+
+    @Test
+    public void testDnsSvcbRecord_svcParamIpv6Hint() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_IPV6HINT);
+        assertEquals(Arrays.asList(InetAddresses.parseNumericAddress("2001:db8::1")),
+                record.getAddresses());
+    }
+
+    @Test
+    public void testDnsSvcbRecord_svcParamDohPath() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_DOHPATH);
+        assertEquals("/some-path{?dns}", record.getDohPath());
+    }
+
+    @Test
+    public void testDnsSvcbRecord_svcParamGeneric_withValue() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
+                TEST_SVC_PARAM_GENERIC_WITH_VALUE);
+        // Check the content returned from toString() for now because the getter function for
+        // generic SvcParam hasn't been implemented.
+        // TODO(b/240259333): Consider adding DnsSvcbRecord.getValueFromGenericSvcParam(int key)
+        // when needed.
+        assertTrue(record.toString().contains("key12345=1A2B0C"));
+    }
+
+    @Test
+    public void testDnsSvcbRecord_svcParamGeneric_withoutValue() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
+                TEST_SVC_PARAM_GENERIC_WITHOUT_VALUE);
+        // Check the content returned from toString() for now because the getter function for
+        // generic SvcParam hasn't been implemented.
+        // TODO(b/240259333): Consider adding DnsSvcbRecord.getValueFromGenericSvcParam(int key)
+        // when needed.
+        assertTrue(record.toString().contains("key12346"));
+    }
+
+    @Test
+    public void testDnsSvcbRecord() throws Exception {
+        final DnsSvcbRecord record = makeDnsSvcbRecordFromByteArray(
+                new TestDnsRecordByteArrayBuilder()
+                .setRRType(TYPE_SVCB)
+                .setTargetName("doh.dns.com")
+                .addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
+                .addRdata(TEST_SVC_PARAM_IPV4HINT_1)
+                .addRdata(TEST_SVC_PARAM_IPV6HINT)
+                .addRdata(TEST_SVC_PARAM_PORT)
+                .addRdata(TEST_SVC_PARAM_DOHPATH)
+                .build());
+        assertEquals("doh.dns.com", record.getTargetName());
+        assertEquals(Arrays.asList("h2", "http/1.1"), record.getAlpns());
+        assertEquals(5353, record.getPort());
+        assertEquals(Arrays.asList(
+                InetAddresses.parseNumericAddress("1.2.3.4"),
+                InetAddresses.parseNumericAddress("6.7.8.9"),
+                InetAddresses.parseNumericAddress("2001:db8::1")), record.getAddresses());
+        assertEquals("/some-path{?dns}", record.getDohPath());
+    }
+
+    @Test
+    public void testDnsSvcbRecord_createdFromNullObject() throws Exception {
+        assertThrows(NullPointerException.class, () -> makeDnsSvcbRecordFromByteArray(null));
+    }
+
+    @Test
+    public void testDnsSvcbRecord_invalidDnsRecord() throws Exception {
+        // The type is not SVCB.
+        final byte[] bytes1 = new TestDnsRecordByteArrayBuilder()
+                .setRRType(TYPE_A)
+                .addRdata(InetAddresses.parseNumericAddress("1.2.3.4").getAddress())
+                .build();
+        assertThrows(IllegalStateException.class, () -> makeDnsSvcbRecordFromByteArray(bytes1));
+
+        // TargetName is missing.
+        final byte[] bytes2 = new TestDnsRecordByteArrayBuilder()
+                .setRRType(TYPE_SVCB)
+                .addRdata(new byte[] { 0x01, 0x01 })
+                .build();
+        assertThrows(BufferUnderflowException.class, () -> makeDnsSvcbRecordFromByteArray(bytes2));
+
+        // Rdata is empty.
+        final byte[] bytes3 = new TestDnsRecordByteArrayBuilder()
+                .setRRType(TYPE_SVCB)
+                .build();
+        assertThrows(BufferUnderflowException.class, () -> makeDnsSvcbRecordFromByteArray(bytes3));
+    }
+
+    @Test
+    public void testDnsSvcbRecord_repeatedKeyIsInvalid() throws Exception {
+        final byte[] bytes = new TestDnsRecordByteArrayBuilder()
+                .setRRType(TYPE_SVCB)
+                .addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
+                .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+                .build();
+        assertThrows(DnsPacket.ParseException.class, () -> makeDnsSvcbRecordFromByteArray(bytes));
+    }
+
+    @Test
+    public void testDnsSvcbRecord_invalidContent() throws Exception {
+        final List<byte[]> invalidContents = Arrays.asList(
+                // Invalid SvcParamValue for "mandatory":
+                // - SvcParamValue must not be empty.
+                // - SvcParamValue has less data than expected.
+                // - SvcParamValue has more data than expected.
+                // - SvcParamValue must be multiple of 2.
+                new byte[] { 0x00, 0x00, 0x00, 0x00},
+                new byte[] { 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06 },
+                new byte[] { 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, },
+                new byte[] { 0x00, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00 },
+
+                // Invalid SvcParamValue for "alpn":
+                // - SvcParamValue must not be empty.
+                // - SvcParamValue has less data than expected.
+                // - SvcParamValue has more data than expected.
+                // - Alpn length is less than the actual data size.
+                // - Alpn length is more than the actual data size.
+                // - Alpn must be a non-empty string.
+                new byte[] { 0x00, 0x01, 0x00, 0x00},
+                new byte[] { 0x00, 0x01, 0x00, 0x02, 0x02, 'h', '2' },
+                new byte[] { 0x00, 0x01, 0x00, 0x05, 0x02, 'h', '2' },
+                new byte[] { 0x00, 0x01, 0x00, 0x04, 0x02, 'd', 'o', 't' },
+                new byte[] { 0x00, 0x01, 0x00, 0x04, 0x08, 'd', 'o', 't' },
+                new byte[] { 0x00, 0x01, 0x00, 0x08, 0x02, 'h', '2', 0x00 },
+
+                // Invalid SvcParamValue for "no-default-alpn":
+                // - SvcParamValue must be empty.
+                // - SvcParamValue length must be 0.
+                new byte[] { 0x00, 0x02, 0x00, 0x04, 'd', 'a', 't', 'a' },
+                new byte[] { 0x00, 0x02, 0x00, 0x04 },
+
+                // Invalid SvcParamValue for "port":
+                // - SvcParamValue must not be empty.
+                // - SvcParamValue has less data than expected.
+                // - SvcParamValue has more data than expected.
+                // - SvcParamValue length must be multiple of 2.
+                new byte[] { 0x00, 0x03, 0x00, 0x00 },
+                new byte[] { 0x00, 0x03, 0x00, 0x02, 0x01 },
+                new byte[] { 0x00, 0x03, 0x00, 0x02, 0x01, 0x02, 0x03 },
+                new byte[] { 0x00, 0x03, 0x00, 0x03, 0x01, 0x02, 0x03 },
+
+                // Invalid SvcParamValue for "ipv4hint":
+                // - SvcParamValue must not be empty.
+                // - SvcParamValue has less data than expected.
+                // - SvcParamValue has more data than expected.
+                // - SvcParamValue must be multiple of 4.
+                new byte[] { 0x00, 0x04, 0x00, 0x00 },
+                new byte[] { 0x00, 0x04, 0x00, 0x04, 0x08 },
+                new byte[] { 0x00, 0x04, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08 },
+                new byte[] { 0x00, 0x04, 0x00, 0x05, 0x08, 0x08, 0x08, 0x08 },
+
+                // Invalid SvcParamValue for "ipv6hint":
+                // - SvcParamValue must not be empty.
+                // - SvcParamValue has less data than expected.
+                // - SvcParamValue has more data than expected.
+                // - SvcParamValue must be multiple of 16.
+                new byte[] { 0x00, 0x06, 0x00, 0x00 },
+                new byte[] { 0x00, 0x06, 0x00, 0x10, 0x01 },
+                new byte[] { 0x00, 0x06, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+                        0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17 },
+                new byte[] { 0x00, 0x06, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+                        0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 }
+        );
+
+        for (byte[] content : invalidContents) {
+            final byte[] bytes = new TestDnsRecordByteArrayBuilder()
+                        .setRRType(TYPE_SVCB)
+                        .addRdata(content)
+                        .build();
+            assertThrows(DnsPacket.ParseException.class,
+                        () -> makeDnsSvcbRecordFromByteArray(bytes));
+        }
+    }
+
+    @Test
+    public void testDnsSvcbPacket_createdFromNullObject() throws Exception {
+        assertThrows(DnsPacket.ParseException.class, () -> DnsSvcbPacket.fromResponse(null));
+    }
+
+    @Test
+    public void testDnsSvcbPacket() throws Exception {
+        final String dohTargetName = "https.dns.com";
+        final String doqTargetName = "doq.dns.com";
+        final InetAddress[] expectedIpAddressesForHttps = new InetAddress[] {
+                InetAddresses.parseNumericAddress("1.2.3.4"),
+                InetAddresses.parseNumericAddress("6.7.8.9"),
+                InetAddresses.parseNumericAddress("2001:db8::1"),
+        };
+        final InetAddress[] expectedIpAddressesForDoq = new InetAddress[] {
+                InetAddresses.parseNumericAddress("4.3.2.1"),
+        };
+
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 2 /* ancount */, 0 /* nscount */,
+                0 /* arcount */));
+        os.write(TEST_DNS_SVCB_QUESTION_SECTION);
+        // Add answer for alpn h2 and http/1.1.
+        os.write(new TestDnsRecordByteArrayBuilder()
+                .setNameCompression(true)
+                .setRRType(TYPE_SVCB)
+                .setTargetName(dohTargetName)
+                .addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
+                .addRdata(TEST_SVC_PARAM_IPV4HINT_1)
+                .addRdata(TEST_SVC_PARAM_IPV6HINT)
+                .addRdata(TEST_SVC_PARAM_PORT)
+                .addRdata(TEST_SVC_PARAM_DOHPATH)
+                .build());
+        // Add answer for alpn doq.
+        os.write(new TestDnsRecordByteArrayBuilder()
+                .setNameCompression(true)
+                .setRRType(TYPE_SVCB)
+                .setTargetName(doqTargetName)
+                .setSvcPriority(2)
+                .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+                .addRdata(TEST_SVC_PARAM_IPV4HINT_2)
+                .build());
+        final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
+
+        assertTrue(pkt.isSupported("http/1.1"));
+        assertTrue(pkt.isSupported("h2"));
+        assertTrue(pkt.isSupported("doq"));
+        assertFalse(pkt.isSupported("http"));
+        assertFalse(pkt.isSupported("h3"));
+        assertFalse(pkt.isSupported(""));
+
+        assertEquals(dohTargetName, pkt.getTargetName("http/1.1"));
+        assertEquals(dohTargetName, pkt.getTargetName("h2"));
+        assertEquals(doqTargetName, pkt.getTargetName("doq"));
+        assertEquals(null, pkt.getTargetName("http"));
+        assertEquals(null, pkt.getTargetName("h3"));
+        assertEquals(null, pkt.getTargetName(""));
+
+        assertEquals(5353, pkt.getPort("http/1.1"));
+        assertEquals(5353, pkt.getPort("h2"));
+        assertEquals(-1, pkt.getPort("doq"));
+        assertEquals(-1, pkt.getPort("http"));
+        assertEquals(-1, pkt.getPort("h3"));
+        assertEquals(-1, pkt.getPort(""));
+
+        assertArrayEquals(expectedIpAddressesForHttps, pkt.getAddresses("http/1.1").toArray());
+        assertArrayEquals(expectedIpAddressesForHttps, pkt.getAddresses("h2").toArray());
+        assertArrayEquals(expectedIpAddressesForDoq, pkt.getAddresses("doq").toArray());
+        assertTrue(pkt.getAddresses("http").isEmpty());
+        assertTrue(pkt.getAddresses("h3").isEmpty());
+        assertTrue(pkt.getAddresses("").isEmpty());
+
+        assertEquals("/some-path{?dns}", pkt.getDohPath("http/1.1"));
+        assertEquals("/some-path{?dns}", pkt.getDohPath("h2"));
+        assertEquals("", pkt.getDohPath("doq"));
+        assertEquals(null, pkt.getDohPath("http"));
+        assertEquals(null, pkt.getDohPath("h3"));
+        assertEquals(null, pkt.getDohPath(""));
+    }
+
+    @Test
+    public void testDnsSvcbPacket_noIpHint() throws Exception {
+        final String targetName = "doq.dns.com";
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 1 /* ancount */, 0 /* nscount */,
+                0 /* arcount */));
+        os.write(TEST_DNS_SVCB_QUESTION_SECTION);
+        // Add answer for alpn doq.
+        os.write(new TestDnsRecordByteArrayBuilder()
+                .setNameCompression(true)
+                .setRRType(TYPE_SVCB)
+                .setTargetName(targetName)
+                .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+                .build());
+        final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
+
+        assertTrue(pkt.isSupported("doq"));
+        assertEquals(targetName, pkt.getTargetName("doq"));
+        assertEquals(-1, pkt.getPort("doq"));
+        assertArrayEquals(new InetAddress[] {}, pkt.getAddresses("doq").toArray());
+        assertEquals("", pkt.getDohPath("doq"));
+    }
+
+    @Test
+    public void testDnsSvcbPacket_hasAnswerInAdditionalSection() throws Exception {
+        final InetAddress[] expectedIpAddresses = new InetAddress[] {
+                InetAddresses.parseNumericAddress("1.2.3.4"),
+                InetAddresses.parseNumericAddress("2001:db8::2"),
+        };
+
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 1 /* ancount */, 0 /* nscount */,
+                2 /* arcount */));
+        os.write(TEST_DNS_SVCB_QUESTION_SECTION);
+        // Add SVCB record in the Answer section.
+        os.write(new TestDnsRecordByteArrayBuilder()
+                .setNameCompression(true)
+                .setRRType(TYPE_SVCB)
+                .setTargetName("doq.dns.com")
+                .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+                .addRdata(TEST_SVC_PARAM_IPV4HINT_2)
+                .addRdata(TEST_SVC_PARAM_IPV6HINT)
+                .build());
+        // Add A/AAAA records in the Additional section.
+        os.write(new TestDnsRecordByteArrayBuilder()
+                .setNameCompression(true)
+                .setRRType(TYPE_A)
+                .addRdata(InetAddresses.parseNumericAddress("1.2.3.4").getAddress())
+                .build());
+        os.write(new TestDnsRecordByteArrayBuilder()
+                .setNameCompression(true)
+                .setRRType(TYPE_AAAA)
+                .addRdata(InetAddresses.parseNumericAddress("2001:db8::2").getAddress())
+                .build());
+        final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
+
+        // If there are A/AAAA records in the Additional section, getAddresses() returns the IP
+        // addresses in those records instead of the IP addresses in ipv4hint/ipv6hint.
+        assertArrayEquals(expectedIpAddresses, pkt.getAddresses("doq").toArray());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DomainUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DomainUtilsTest.java
new file mode 100644
index 0000000..5eaf2ad
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DomainUtilsTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.net.module.util;
+
+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 androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DomainUtilsTest {
+    @Test
+    public void testEncodeInvalidDomain() {
+        byte[] buffer = DomainUtils.encode(".google.com");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google.com.");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("-google.com");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google.com-");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google..com");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google!.com");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google.o");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google,com");
+        assertNull(buffer);
+    }
+
+    @Test
+    public void testEncodeValidDomainNamesWithoutCompression() {
+        // Single domain: "google.com"
+        String suffix = "06676F6F676C6503636F6D00";
+        byte[] buffer = DomainUtils.encode("google.com");
+        //assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // Single domain: "google-guest.com"
+        suffix = "0C676F6F676C652D677565737403636F6D00";
+        buffer = DomainUtils.encode("google-guest.com");
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "corp.google.com", "google.com"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "04636F727006676F6F676C6503636F6D00"                // corp.google.com
+                + "06676F6F676C6503636F6D00";                         // google.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "corp.google.com", "google.com"},
+                false /* compression */);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+
+        // domain search list: "example.corp.google.com", "corp..google.com"(invalid domain),
+        // "google.com"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "06676F6F676C6503636F6D00";                         // google.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "corp..google.com", "google.com"},
+                false /* compression */);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // Invalid domain search list: "corp..google.com", "..google.com"
+        buffer = DomainUtils.encode(new String[] {"corp..google.com", "..google.com"},
+                false /* compression */);
+        assertEquals(0, buffer.length);
+    }
+
+    @Test
+    public void testEncodeValidDomainNamesWithCompression() {
+        // domain search list: "example.corp.google.com", "corp.google.com", "google.com"
+        String suffix =
+                "076578616D706C6504636F727006676F6F676C6503636F6D00"  // example.corp.google.com
+                + "C008"                                              // corp.google.com
+                + "C00D";                                             // google.com
+        byte[] buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "corp.google.com", "google.com"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "a.example.corp.google.com", "google.com"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "0161C000"                                          // a.example.corp.google.com
+                + "C00D";                                             // google.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "a.example.corp.google.com", "google.com"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "google.com", "gle.com"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "C00D"                                              // google.com
+                + "03676C65C014";                                     // gle.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "google.com", "gle.com"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "google.com", "google"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "C00D";                                              // google.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "google.com", "google"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "..google.com"(invalid domain), "google"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00"; // example.corp.google.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "..google.com", "google"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "suffix.example.edu.cn", "edu.cn"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "06737566666978076578616D706C650365647502636E00"    // suffix.example.edu.cn
+                + "C028";                                             // edu.cn
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "suffix.example.edu.cn", "edu.cn"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "google.com", "example.com", "sub.example.com"
+        suffix = "06676F6F676C6503636F6D00"                           // google.com
+                + "076578616D706C65C007"                              // example.com
+                + "03737562C00C";                                     // sub.example.com
+        buffer = DomainUtils.encode(new String[] {
+                "google.com", "example.com", "sub.example.com"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+    }
+
+    @Test
+    public void testDecodeDomainNames() {
+        ArrayList<String> suffixStringList;
+        String suffixes = "06676F6F676C6503636F6D00" // google.com
+                + "076578616D706C6503636F6D00"       // example.com
+                + "06676F6F676C6500";                // google
+        List<String> expected = Arrays.asList("google.com", "example.com");
+        ByteBuffer buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
+        suffixStringList = DomainUtils.decode(buffer, false /* compression */);
+        assertEquals(expected, suffixStringList);
+
+        // include suffix with invalid length: 64
+        suffixes = "06676F6F676C6503636F6D00"        // google.com
+                + "406578616D706C6503636F6D00"       // example.com(length=64)
+                + "06676F6F676C6500";                // google
+        expected = Arrays.asList("google.com");
+        buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
+        suffixStringList = DomainUtils.decode(buffer, false /* compression */);
+        assertEquals(expected, suffixStringList);
+
+        // include suffix with invalid length: 0
+        suffixes = "06676F6F676C6503636F6D00"         // google.com
+                + "076578616D706C6503636F6D00"        // example.com
+                + "00676F6F676C6500";                 // google(length=0)
+        expected = Arrays.asList("google.com", "example.com");
+        buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
+        suffixStringList = DomainUtils.decode(buffer, false /* compression */);
+        assertEquals(expected, suffixStringList);
+
+        suffixes =
+                "076578616D706C6504636F727006676F6F676C6503636F6D00"  // example.corp.google.com
+                + "C008"                                              // corp.google.com
+                + "C00D";                                             // google.com
+        expected = Arrays.asList("example.corp.google.com", "corp.google.com", "google.com");
+        buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
+        suffixStringList = DomainUtils.decode(buffer, true /* compression */);
+        assertEquals(expected, suffixStringList);
+    }
+}
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..bdcb8c0
--- /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 testGetShrinkedBackingArray() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(2)
+
+        assertContentEquals(intArrayOf(-1, 2), array.shrinkedBackingArray)
+    }
+
+    @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/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
new file mode 100644
index 0000000..f2c902f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.net.module.util
+
+import android.os.HandlerThread
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.DevSdkIgnoreRunner.MonitorThreadLeak
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+const val THREAD_BLOCK_TIMEOUT_MS = 1000L
+const val TEST_REPEAT_COUNT = 100
+
+@MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class HandlerUtilsTest {
+    val handlerThread = HandlerThread("HandlerUtilsTestHandlerThread").also {
+        it.start()
+    }
+    val handler = handlerThread.threadHandler
+
+    @Test
+    fun testRunWithScissors() {
+        // Repeat the test a fair amount of times to ensure that it does not pass by chance.
+        repeat(TEST_REPEAT_COUNT) {
+            var result = false
+            HandlerUtils.runWithScissorsForDump(handler, {
+                assertEquals(Thread.currentThread(), handlerThread)
+                result = true
+            }, THREAD_BLOCK_TIMEOUT_MS)
+            // Assert that the result is modified on the handler thread, but can also be seen from
+            // the current thread. The assertion should pass if the runWithScissors provides
+            // the guarantee where the assignment happens-before the assertion.
+            assertTrue(result)
+        }
+    }
+
+    @After
+    fun tearDown() {
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
new file mode 100644
index 0000000..5a15585
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.net.module.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class HexDumpTest {
+    @Test
+    public void testBytesToHexString() {
+        assertEquals("abcdef", HexDump.toHexString(
+                new byte[]{(byte) 0xab, (byte) 0xcd, (byte) 0xef}, false));
+        assertEquals("ABCDEF", HexDump.toHexString(
+                new byte[]{(byte) 0xab, (byte) 0xcd, (byte) 0xef}, true));
+    }
+
+    @Test
+    public void testNullArray() {
+        assertEquals("(null)", HexDump.dumpHexString(null));
+    }
+
+    @Test
+    public void testHexStringToByteArray() {
+        assertArrayEquals(new byte[]{(byte) 0xab, (byte) 0xcd, (byte) 0xef},
+                HexDump.hexStringToByteArray("abcdef"));
+        assertArrayEquals(new byte[]{(byte) 0xAB, (byte) 0xCD, (byte) 0xEF},
+                HexDump.hexStringToByteArray("ABCDEF"));
+    }
+
+    @Test
+    public void testIntegerToByteArray() {
+        assertArrayEquals(new byte[]{(byte) 0xff, (byte) 0x00, (byte) 0x00, (byte) 0x04},
+                HexDump.toByteArray((int) 0xff000004));
+    }
+
+    @Test
+    public void testByteToByteArray() {
+        assertArrayEquals(new byte[]{(byte) 0x7f}, HexDump.toByteArray((byte) 0x7f));
+    }
+
+    @Test
+    public void testIntegerToHexString() {
+        assertEquals("FF000004", HexDump.toHexString((int) 0xff000004));
+    }
+
+    @Test
+    public void testByteToHexString() {
+        assertEquals("7F", HexDump.toHexString((byte) 0x7f));
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/Inet4AddressUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/Inet4AddressUtilsTest.java
new file mode 100644
index 0000000..702bdaf
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/Inet4AddressUtilsTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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 static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
+import static com.android.net.module.util.Inet4AddressUtils.getImplicitNetmask;
+import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
+import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
+import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTL;
+import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTL;
+import static com.android.net.module.util.Inet4AddressUtils.netmaskToPrefixLength;
+import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTH;
+import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTL;
+import static com.android.net.module.util.Inet4AddressUtils.trimAddressZeros;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import android.net.InetAddresses;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class Inet4AddressUtilsTest {
+
+    @Test
+    public void testInet4AddressToIntHTL() {
+        assertEquals(0, inet4AddressToIntHTL(ipv4Address("0.0.0.0")));
+        assertEquals(0x000080ff, inet4AddressToIntHTL(ipv4Address("255.128.0.0")));
+        assertEquals(0x0080ff0a, inet4AddressToIntHTL(ipv4Address("10.255.128.0")));
+        assertEquals(0x00feff0a, inet4AddressToIntHTL(ipv4Address("10.255.254.0")));
+        assertEquals(0xfeffa8c0, inet4AddressToIntHTL(ipv4Address("192.168.255.254")));
+        assertEquals(0xffffa8c0, inet4AddressToIntHTL(ipv4Address("192.168.255.255")));
+    }
+
+    @Test
+    public void testIntToInet4AddressHTL() {
+        assertEquals(ipv4Address("0.0.0.0"), intToInet4AddressHTL(0));
+        assertEquals(ipv4Address("255.128.0.0"), intToInet4AddressHTL(0x000080ff));
+        assertEquals(ipv4Address("10.255.128.0"), intToInet4AddressHTL(0x0080ff0a));
+        assertEquals(ipv4Address("10.255.254.0"), intToInet4AddressHTL(0x00feff0a));
+        assertEquals(ipv4Address("192.168.255.254"), intToInet4AddressHTL(0xfeffa8c0));
+        assertEquals(ipv4Address("192.168.255.255"), intToInet4AddressHTL(0xffffa8c0));
+    }
+
+    @Test
+    public void testInet4AddressToIntHTH() {
+        assertEquals(0, inet4AddressToIntHTH(ipv4Address("0.0.0.0")));
+        assertEquals(0xff800000, inet4AddressToIntHTH(ipv4Address("255.128.0.0")));
+        assertEquals(0x0aff8000, inet4AddressToIntHTH(ipv4Address("10.255.128.0")));
+        assertEquals(0x0afffe00, inet4AddressToIntHTH(ipv4Address("10.255.254.0")));
+        assertEquals(0xc0a8fffe, inet4AddressToIntHTH(ipv4Address("192.168.255.254")));
+        assertEquals(0xc0a8ffff, inet4AddressToIntHTH(ipv4Address("192.168.255.255")));
+    }
+
+    @Test
+    public void testIntToInet4AddressHTH() {
+        assertEquals(ipv4Address("0.0.0.0"), intToInet4AddressHTH(0));
+        assertEquals(ipv4Address("255.128.0.0"), intToInet4AddressHTH(0xff800000));
+        assertEquals(ipv4Address("10.255.128.0"), intToInet4AddressHTH(0x0aff8000));
+        assertEquals(ipv4Address("10.255.254.0"), intToInet4AddressHTH(0x0afffe00));
+        assertEquals(ipv4Address("192.168.255.254"), intToInet4AddressHTH(0xc0a8fffe));
+        assertEquals(ipv4Address("192.168.255.255"), intToInet4AddressHTH(0xc0a8ffff));
+    }
+
+
+    @Test
+    public void testPrefixLengthToV4NetmaskIntHTL() {
+        assertEquals(0, prefixLengthToV4NetmaskIntHTL(0));
+        assertEquals(0x000080ff /* 255.128.0.0 */, prefixLengthToV4NetmaskIntHTL(9));
+        assertEquals(0x0080ffff /* 255.255.128.0 */, prefixLengthToV4NetmaskIntHTL(17));
+        assertEquals(0x00feffff /* 255.255.254.0 */, prefixLengthToV4NetmaskIntHTL(23));
+        assertEquals(0xfeffffff /* 255.255.255.254 */, prefixLengthToV4NetmaskIntHTL(31));
+        assertEquals(0xffffffff /* 255.255.255.255 */, prefixLengthToV4NetmaskIntHTL(32));
+    }
+
+    @Test
+    public void testPrefixLengthToV4NetmaskIntHTH() {
+        assertEquals(0, prefixLengthToV4NetmaskIntHTH(0));
+        assertEquals(0xff800000 /* 255.128.0.0 */, prefixLengthToV4NetmaskIntHTH(9));
+        assertEquals(0xffff8000 /* 255.255.128.0 */, prefixLengthToV4NetmaskIntHTH(17));
+        assertEquals(0xfffffe00 /* 255.255.254.0 */, prefixLengthToV4NetmaskIntHTH(23));
+        assertEquals(0xfffffffe /* 255.255.255.254 */, prefixLengthToV4NetmaskIntHTH(31));
+        assertEquals(0xffffffff /* 255.255.255.255 */, prefixLengthToV4NetmaskIntHTH(32));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testPrefixLengthToV4NetmaskIntHTH_NegativeLength() {
+        prefixLengthToV4NetmaskIntHTH(-1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testPrefixLengthToV4NetmaskIntHTH_LengthTooLarge() {
+        prefixLengthToV4NetmaskIntHTH(33);
+    }
+
+    private void checkAddressMasking(String expectedAddr, String addr, int prefixLength) {
+        final int prefix = prefixLengthToV4NetmaskIntHTH(prefixLength);
+        final int addrInt = inet4AddressToIntHTH(ipv4Address(addr));
+        assertEquals(ipv4Address(expectedAddr), intToInet4AddressHTH(prefix & addrInt));
+    }
+
+    @Test
+    public void testPrefixLengthToV4NetmaskIntHTH_MaskAddr() {
+        checkAddressMasking("192.168.0.0", "192.168.128.1", 16);
+        checkAddressMasking("255.240.0.0", "255.255.255.255", 12);
+        checkAddressMasking("255.255.255.255", "255.255.255.255", 32);
+        checkAddressMasking("0.0.0.0", "255.255.255.255", 0);
+    }
+
+    @Test
+    public void testGetImplicitNetmask() {
+        assertEquals(8, getImplicitNetmask(ipv4Address("4.2.2.2")));
+        assertEquals(8, getImplicitNetmask(ipv4Address("10.5.6.7")));
+        assertEquals(16, getImplicitNetmask(ipv4Address("173.194.72.105")));
+        assertEquals(16, getImplicitNetmask(ipv4Address("172.23.68.145")));
+        assertEquals(24, getImplicitNetmask(ipv4Address("192.0.2.1")));
+        assertEquals(24, getImplicitNetmask(ipv4Address("192.168.5.1")));
+        assertEquals(32, getImplicitNetmask(ipv4Address("224.0.0.1")));
+        assertEquals(32, getImplicitNetmask(ipv4Address("255.6.7.8")));
+    }
+
+    private void assertInvalidNetworkMask(Inet4Address addr) {
+        try {
+            netmaskToPrefixLength(addr);
+            fail("Invalid netmask " + addr.getHostAddress() + " did not cause exception");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testNetmaskToPrefixLength() {
+        assertEquals(0, netmaskToPrefixLength(ipv4Address("0.0.0.0")));
+        assertEquals(9, netmaskToPrefixLength(ipv4Address("255.128.0.0")));
+        assertEquals(17, netmaskToPrefixLength(ipv4Address("255.255.128.0")));
+        assertEquals(23, netmaskToPrefixLength(ipv4Address("255.255.254.0")));
+        assertEquals(31, netmaskToPrefixLength(ipv4Address("255.255.255.254")));
+        assertEquals(32, netmaskToPrefixLength(ipv4Address("255.255.255.255")));
+
+        assertInvalidNetworkMask(ipv4Address("0.0.0.1"));
+        assertInvalidNetworkMask(ipv4Address("255.255.255.253"));
+        assertInvalidNetworkMask(ipv4Address("255.255.0.255"));
+    }
+
+    @Test
+    public void testGetPrefixMaskAsAddress() {
+        assertEquals("255.255.240.0", getPrefixMaskAsInet4Address(20).getHostAddress());
+        assertEquals("255.0.0.0", getPrefixMaskAsInet4Address(8).getHostAddress());
+        assertEquals("0.0.0.0", getPrefixMaskAsInet4Address(0).getHostAddress());
+        assertEquals("255.255.255.255", getPrefixMaskAsInet4Address(32).getHostAddress());
+    }
+
+    @Test
+    public void testGetBroadcastAddress() {
+        assertEquals("192.168.15.255",
+                getBroadcastAddress(ipv4Address("192.168.0.123"), 20).getHostAddress());
+        assertEquals("192.255.255.255",
+                getBroadcastAddress(ipv4Address("192.168.0.123"), 8).getHostAddress());
+        assertEquals("192.168.0.123",
+                getBroadcastAddress(ipv4Address("192.168.0.123"), 32).getHostAddress());
+        assertEquals("255.255.255.255",
+                getBroadcastAddress(ipv4Address("192.168.0.123"), 0).getHostAddress());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testGetBroadcastAddress_PrefixTooLarge() {
+        getBroadcastAddress(ipv4Address("192.168.0.123"), 33);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testGetBroadcastAddress_NegativePrefix() {
+        getBroadcastAddress(ipv4Address("192.168.0.123"), -1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testGetPrefixMaskAsAddress_PrefixTooLarge() {
+        getPrefixMaskAsInet4Address(33);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testGetPrefixMaskAsAddress_NegativePrefix() {
+        getPrefixMaskAsInet4Address(-1);
+    }
+
+    @Test
+    public void testTrimAddressZeros() {
+        assertNull(trimAddressZeros(null));
+        assertEquals("$invalid&", trimAddressZeros("$invalid&"));
+        assertEquals("example.com", trimAddressZeros("example.com"));
+        assertEquals("a.b.c.d", trimAddressZeros("a.b.c.d"));
+
+        assertEquals("192.0.2.2", trimAddressZeros("192.000.02.2"));
+        assertEquals("192.0.2.2", trimAddressZeros("192.0.2.2"));
+    }
+
+    private Inet4Address ipv4Address(String addr) {
+        return (Inet4Address) InetAddresses.parseNumericAddress(addr);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
new file mode 100644
index 0000000..66427fc
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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 static junit.framework.Assert.assertEquals;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InetAddressUtilsTest {
+
+    private InetAddress parcelUnparcelAddress(InetAddress addr) {
+        Parcel p = Parcel.obtain();
+        InetAddressUtils.parcelInetAddress(p, addr, 0 /* flags */);
+        p.setDataPosition(0);
+        byte[] marshalled = p.marshall();
+        p.recycle();
+        p = Parcel.obtain();
+        p.unmarshall(marshalled, 0, marshalled.length);
+        p.setDataPosition(0);
+        InetAddress out = InetAddressUtils.unparcelInetAddress(p);
+        p.recycle();
+        return out;
+    }
+
+    @Test
+    public void testParcelUnparcelIpv4Address() throws Exception {
+        InetAddress ipv4 = InetAddress.getByName("192.0.2.1");
+        assertEquals(ipv4, parcelUnparcelAddress(ipv4));
+    }
+
+    @Test
+    public void testParcelUnparcelIpv6Address() throws Exception {
+        InetAddress ipv6 = InetAddress.getByName("2001:db8::1");
+        assertEquals(ipv6, parcelUnparcelAddress(ipv6));
+    }
+
+    @Test
+    public void testParcelUnparcelScopedIpv6Address() throws Exception {
+        InetAddress ipv6 = InetAddress.getByName("fe80::1%42");
+        assertEquals(42, ((Inet6Address) ipv6).getScopeId());
+        Inet6Address out = (Inet6Address) parcelUnparcelAddress(ipv6);
+        assertEquals(ipv6, out);
+        assertEquals(42, out.getScopeId());
+    }
+
+    @Test
+    public void testWithScopeId() {
+        final int scopeId = 999;
+
+        final String globalAddrStr = "2401:fa00:49c:484:dc41:e6ff:fefd:f180";
+        final Inet6Address globalAddr = (Inet6Address) InetAddresses
+                .parseNumericAddress(globalAddrStr);
+        final Inet6Address updatedGlobalAddr = InetAddressUtils.withScopeId(globalAddr, scopeId);
+        assertFalse(updatedGlobalAddr.isLinkLocalAddress());
+        assertEquals(globalAddrStr, updatedGlobalAddr.getHostAddress());
+        assertEquals(0, updatedGlobalAddr.getScopeId());
+
+        final String localAddrStr = "fe80::4735:9628:d038:2087";
+        final Inet6Address localAddr = (Inet6Address) InetAddresses
+                .parseNumericAddress(localAddrStr);
+        final Inet6Address updatedLocalAddr = InetAddressUtils.withScopeId(localAddr, scopeId);
+        assertTrue(updatedLocalAddr.isLinkLocalAddress());
+        assertEquals(localAddrStr + "%" + scopeId, updatedLocalAddr.getHostAddress());
+        assertEquals(scopeId, updatedLocalAddr.getScopeId());
+    }
+
+    @Test
+    public void testV4MappedV6Address() throws Exception {
+        final Inet4Address v4Addr = (Inet4Address) InetAddress.getByName("192.0.2.1");
+        final Inet6Address v4MappedV6Address = InetAddressUtils.v4MappedV6Address(v4Addr);
+        final byte[] expectedAddrBytes = new byte[]{
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+        };
+        assertArrayEquals(expectedAddrBytes, v4MappedV6Address.getAddress());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/InterfaceParamsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/InterfaceParamsTest.java
new file mode 100644
index 0000000..a1d8c10
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/InterfaceParamsTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.net.module.util;
+
+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 androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InterfaceParamsTest {
+    @Test
+    public void testNullInterfaceReturnsNull() {
+        assertNull(InterfaceParams.getByName(null));
+    }
+
+    @Test
+    public void testNonExistentInterfaceReturnsNull() {
+        assertNull(InterfaceParams.getByName("doesnotexist0"));
+    }
+
+    @Test
+    public void testLoopback() {
+        final InterfaceParams ifParams = InterfaceParams.getByName("lo");
+        assertNotNull(ifParams);
+        assertEquals("lo", ifParams.name);
+        assertTrue(ifParams.index > 0);
+        assertNotNull(ifParams.macAddr);
+        assertFalse(ifParams.hasMacAddress);
+        assertTrue(ifParams.defaultMtu >= NetworkStackConstants.ETHER_MTU);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/IpRangeTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/IpRangeTest.java
new file mode 100644
index 0000000..20bbd4a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/IpRangeTest.java
@@ -0,0 +1,282 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.annotation.SuppressLint;
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpRangeTest {
+
+    private static InetAddress address(String addr) {
+        return InetAddresses.parseNumericAddress(addr);
+    }
+
+    private static final Inet4Address IPV4_ADDR = (Inet4Address) address("192.0.2.4");
+    private static final Inet4Address IPV4_RANGE_END = (Inet4Address) address("192.0.3.1");
+    private static final Inet6Address IPV6_ADDR = (Inet6Address) address("2001:db8::");
+    private static final Inet6Address IPV6_RANGE_END = (Inet6Address) address("2001:db9:010f::");
+
+    private static final byte[] IPV4_BYTES = IPV4_ADDR.getAddress();
+    private static final byte[] IPV6_BYTES = IPV6_ADDR.getAddress();
+
+    @Test
+    public void testConstructorBadArguments() {
+        try {
+            new IpRange(null, IPV6_ADDR);
+            fail("Expected NullPointerException: null start address");
+        } catch (NullPointerException expected) {
+        }
+
+        try {
+            new IpRange(IPV6_ADDR, null);
+            fail("Expected NullPointerException: null end address");
+        } catch (NullPointerException expected) {
+        }
+
+        try {
+            new IpRange(null, null);
+            fail("Expected NullPointerException: null addresses");
+        } catch (NullPointerException expected) {
+        }
+
+        try {
+            new IpRange(null);
+            fail("Expected NullPointerException: null start address");
+        } catch (NullPointerException expected) {
+        }
+
+        try {
+            new IpRange(address("10.10.10.10"), address("1.2.3.4"));
+            fail("Expected IllegalArgumentException: start address after end address");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        try {
+            new IpRange(address("ffff::"), address("abcd::"));
+            fail("Expected IllegalArgumentException: start address after end address");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testConstructor() {
+        IpRange r = new IpRange(new IpPrefix(IPV4_ADDR, 32));
+        assertEquals(IPV4_ADDR, r.getStartAddr());
+        assertEquals(IPV4_ADDR, r.getEndAddr());
+
+        r = new IpRange(new IpPrefix(IPV4_ADDR, 16));
+        assertEquals(address("192.0.0.0"), r.getStartAddr());
+        assertEquals(address("192.0.255.255"), r.getEndAddr());
+
+        r = new IpRange(IPV4_ADDR, IPV4_RANGE_END);
+        assertEquals(IPV4_ADDR, r.getStartAddr());
+        assertEquals(IPV4_RANGE_END, r.getEndAddr());
+
+        r = new IpRange(new IpPrefix(IPV6_ADDR, 128));
+        assertEquals(IPV6_ADDR, r.getStartAddr());
+        assertEquals(IPV6_ADDR, r.getEndAddr());
+
+        r = new IpRange(new IpPrefix(IPV6_ADDR, 16));
+        assertEquals(address("2001::"), r.getStartAddr());
+        assertEquals(address("2001:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), r.getEndAddr());
+
+        r = new IpRange(IPV6_ADDR, IPV6_RANGE_END);
+        assertEquals(IPV6_ADDR, r.getStartAddr());
+        assertEquals(IPV6_RANGE_END, r.getEndAddr());
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testContainsRangeEqualRanges() {
+        final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 35));
+        final IpRange r2 = new IpRange(new IpPrefix(IPV6_ADDR, 35));
+
+        assertTrue(r1.containsRange(r2));
+        assertTrue(r2.containsRange(r1));
+        assertEquals(r1, r2);
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testContainsRangeSubset() {
+        final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 64));
+        final IpRange r2 = new IpRange(new IpPrefix(address("2001:db8::0101"), 128));
+
+        assertTrue(r1.containsRange(r2));
+        assertFalse(r2.containsRange(r1));
+        assertNotEquals(r1, r2);
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testContainsRangeTruncatesLowerOrderBits() {
+        final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 100));
+        final IpRange r2 = new IpRange(new IpPrefix(address("2001:db8::0101"), 100));
+
+        assertTrue(r1.containsRange(r2));
+        assertTrue(r2.containsRange(r1));
+        assertEquals(r1, r2);
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testContainsRangeSubsetSameStartAddr() {
+        final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 35));
+        final IpRange r2 = new IpRange(new IpPrefix(IPV6_ADDR, 39));
+
+        assertTrue(r1.containsRange(r2));
+        assertFalse(r2.containsRange(r1));
+        assertNotEquals(r1, r2);
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testContainsRangeOverlapping() {
+        final IpRange r1 = new IpRange(new IpPrefix(address("2001:db9::"), 32));
+        final IpRange r2 = new IpRange(address("2001:db8::"), address("2001:db9::1"));
+
+        assertFalse(r1.containsRange(r2));
+        assertFalse(r2.containsRange(r1));
+        assertNotEquals(r1, r2);
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testOverlapsRangeEqualRanges() {
+        final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 35));
+        final IpRange r2 = new IpRange(new IpPrefix(IPV6_ADDR, 35));
+
+        assertTrue(r1.overlapsRange(r2));
+        assertTrue(r2.overlapsRange(r1));
+        assertEquals(r1, r2);
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testOverlapsRangeSubset() {
+        final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 35));
+        final IpRange r2 = new IpRange(new IpPrefix(IPV6_ADDR, 39));
+
+        assertTrue(r1.overlapsRange(r2));
+        assertTrue(r2.overlapsRange(r1));
+        assertNotEquals(r1, r2);
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testOverlapsRangeDisjoint() {
+        final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 32));
+        final IpRange r2 = new IpRange(new IpPrefix(address("2001:db9::"), 32));
+
+        assertFalse(r1.overlapsRange(r2));
+        assertFalse(r2.overlapsRange(r1));
+        assertNotEquals(r1, r2);
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testOverlapsRangePartialOverlapLow() {
+        final IpRange r1 = new IpRange(new IpPrefix(address("2001:db9::"), 32));
+        final IpRange r2 = new IpRange(address("2001:db8::"), address("2001:db9::1"));
+
+        assertTrue(r1.overlapsRange(r2));
+        assertTrue(r2.overlapsRange(r1));
+        assertNotEquals(r1, r2);
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    public void testOverlapsRangePartialOverlapHigh() {
+        final IpRange r1 = new IpRange(new IpPrefix(address("2001:db7::"), 32));
+        final IpRange r2 = new IpRange(address("2001:db7::ffff"), address("2001:db8::"));
+
+        assertTrue(r1.overlapsRange(r2));
+        assertTrue(r2.overlapsRange(r1));
+        assertNotEquals(r1, r2);
+    }
+
+    @Test
+    public void testIpRangeToPrefixesIpv4FullRange() throws Exception {
+        final IpRange range = new IpRange(address("0.0.0.0"), address("255.255.255.255"));
+        final List<IpPrefix> prefixes = new ArrayList<>();
+        prefixes.add(new IpPrefix("0.0.0.0/0"));
+
+        assertEquals(prefixes, range.asIpPrefixes());
+    }
+
+    @Test
+    public void testIpRangeToPrefixesIpv4() throws Exception {
+        final IpRange range = new IpRange(IPV4_ADDR, IPV4_RANGE_END);
+        final List<IpPrefix> prefixes = new ArrayList<>();
+        prefixes.add(new IpPrefix("192.0.2.128/25"));
+        prefixes.add(new IpPrefix("192.0.2.64/26"));
+        prefixes.add(new IpPrefix("192.0.2.32/27"));
+        prefixes.add(new IpPrefix("192.0.2.16/28"));
+        prefixes.add(new IpPrefix("192.0.2.8/29"));
+        prefixes.add(new IpPrefix("192.0.2.4/30"));
+        prefixes.add(new IpPrefix("192.0.3.0/31"));
+
+        assertEquals(prefixes, range.asIpPrefixes());
+    }
+
+    @Test
+    public void testIpRangeToPrefixesIpv6FullRange() throws Exception {
+        final IpRange range =
+                new IpRange(address("::"), address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+        final List<IpPrefix> prefixes = new ArrayList<>();
+        prefixes.add(new IpPrefix("::/0"));
+
+        assertEquals(prefixes, range.asIpPrefixes());
+    }
+
+    @Test
+    public void testIpRangeToPrefixesIpv6() throws Exception {
+        final IpRange range = new IpRange(IPV6_ADDR, IPV6_RANGE_END);
+        final List<IpPrefix> prefixes = new ArrayList<>();
+        prefixes.add(new IpPrefix("2001:db8::/32"));
+        prefixes.add(new IpPrefix("2001:db9::/40"));
+        prefixes.add(new IpPrefix("2001:db9:100::/45"));
+        prefixes.add(new IpPrefix("2001:db9:108::/46"));
+        prefixes.add(new IpPrefix("2001:db9:10c::/47"));
+        prefixes.add(new IpPrefix("2001:db9:10e::/48"));
+        prefixes.add(new IpPrefix("2001:db9:10f::/128"));
+
+        assertEquals(prefixes, range.asIpPrefixes());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java
new file mode 100644
index 0000000..d57023c
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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 static com.android.net.module.util.NetworkStackConstants.ICMP_CHECKSUM_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpUtilsTest {
+
+    private static final int IPV4_HEADER_LENGTH = 20;
+    private static final int IPV6_HEADER_LENGTH = 40;
+    private static final int TCP_HEADER_LENGTH = 20;
+    private static final int UDP_HEADER_LENGTH = 8;
+    private static final int TCP_CHECKSUM_OFFSET = 16;
+    private static final int UDP_CHECKSUM_OFFSET = 6;
+
+    private int getUnsignedByte(ByteBuffer buf, int offset) {
+        return buf.get(offset) & 0xff;
+    }
+
+    private int getChecksum(ByteBuffer buf, int offset) {
+        return getUnsignedByte(buf, offset) * 256 + getUnsignedByte(buf, offset + 1);
+    }
+
+    private void assertChecksumEquals(int expected, short actual) {
+        assertEquals(Integer.toHexString(expected), Integer.toHexString(actual & 0xffff));
+    }
+
+    // Generate test packets using Python code like this::
+    //
+    // from scapy import all as scapy
+    //
+    // def JavaPacketDefinition(bytes):
+    //   out = "        ByteBuffer packet = ByteBuffer.wrap(new byte[] {\n            "
+    //   for i in xrange(len(bytes)):
+    //     out += "(byte) 0x%02x" % ord(bytes[i])
+    //     if i < len(bytes) - 1:
+    //       if i % 4 == 3:
+    //         out += ",\n            "
+    //       else:
+    //         out += ", "
+    //   out += "\n        });"
+    //   return out
+    //
+    // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2") /
+    //           scapy.UDP(sport=12345, dport=7) /
+    //           "hello")
+    // print JavaPacketDefinition(str(packet))
+
+    @Test
+    public void testEmptyAndZeroBufferChecksum() throws Exception {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[] { (byte) 0x00, (byte) 0x00, });
+        // the following should *not* return 0xFFFF
+        assertEquals(0, IpUtils.checksum(packet, 0, 0, 0));
+        assertEquals(0, IpUtils.checksum(packet, 0, 0, 1));
+        assertEquals(0, IpUtils.checksum(packet, 0, 0, 2));
+        assertEquals(0, IpUtils.checksum(packet, 0, 1, 2));
+        assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 0, 0));
+        assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 0, 1));
+        assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 0, 2));
+        assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 1, 2));
+    }
+
+    @Test
+    public void testIpv6TcpChecksum() throws Exception {
+        // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80) /
+        //           scapy.TCP(sport=12345, dport=7,
+        //                     seq=1692871236, ack=128376451, flags=16,
+        //                     window=32768) /
+        //           "hello, world")
+        ByteBuffer packet = ByteBuffer.wrap(new byte[] {
+            (byte) 0x68, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x20, (byte) 0x06, (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,
+            (byte) 0x30, (byte) 0x39, (byte) 0x00, (byte) 0x07,
+            (byte) 0x64, (byte) 0xe7, (byte) 0x2a, (byte) 0x44,
+            (byte) 0x07, (byte) 0xa6, (byte) 0xde, (byte) 0x83,
+            (byte) 0x50, (byte) 0x10, (byte) 0x80, (byte) 0x00,
+            (byte) 0xee, (byte) 0x71, (byte) 0x00, (byte) 0x00,
+            (byte) 0x68, (byte) 0x65, (byte) 0x6c, (byte) 0x6c,
+            (byte) 0x6f, (byte) 0x2c, (byte) 0x20, (byte) 0x77,
+            (byte) 0x6f, (byte) 0x72, (byte) 0x6c, (byte) 0x64
+        });
+
+        // Check that a valid packet has checksum 0.
+        int transportLen = packet.limit() - IPV6_HEADER_LENGTH;
+        assertEquals(0, IpUtils.tcpChecksum(packet, 0, IPV6_HEADER_LENGTH, transportLen));
+
+        // Check that we can calculate the checksum from scratch.
+        int sumOffset = IPV6_HEADER_LENGTH + TCP_CHECKSUM_OFFSET;
+        int sum = getUnsignedByte(packet, sumOffset) * 256 + getUnsignedByte(packet, sumOffset + 1);
+        assertEquals(0xee71, sum);
+
+        packet.put(sumOffset, (byte) 0);
+        packet.put(sumOffset + 1, (byte) 0);
+        assertChecksumEquals(sum, IpUtils.tcpChecksum(packet, 0, IPV6_HEADER_LENGTH, transportLen));
+
+        // Check that writing the checksum back into the packet results in a valid packet.
+        packet.putShort(
+            sumOffset,
+            IpUtils.tcpChecksum(packet, 0, IPV6_HEADER_LENGTH, transportLen));
+        assertEquals(0, IpUtils.tcpChecksum(packet, 0, IPV6_HEADER_LENGTH, transportLen));
+    }
+
+    @Test
+    public void testIpv4UdpChecksum() {
+        // packet = (scapy.IP(src="192.0.2.1", dst="192.0.2.2", tos=0x40) /
+        //           scapy.UDP(sport=32012, dport=4500) /
+        //           "\xff")
+        ByteBuffer packet = ByteBuffer.wrap(new byte[] {
+            (byte) 0x45, (byte) 0x40, (byte) 0x00, (byte) 0x1d,
+            (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00,
+            (byte) 0x40, (byte) 0x11, (byte) 0xf6, (byte) 0x8b,
+            (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+            (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x02,
+            (byte) 0x7d, (byte) 0x0c, (byte) 0x11, (byte) 0x94,
+            (byte) 0x00, (byte) 0x09, (byte) 0xee, (byte) 0x36,
+            (byte) 0xff
+        });
+
+        // Check that a valid packet has IP checksum 0 and UDP checksum 0xffff (0 is not a valid
+        // UDP checksum, so the udpChecksum rewrites 0 to 0xffff).
+        assertEquals(0, IpUtils.ipChecksum(packet, 0));
+        assertEquals((short) 0xffff, IpUtils.udpChecksum(packet, 0, IPV4_HEADER_LENGTH));
+
+        // Check that we can calculate the checksums from scratch.
+        final int ipSumOffset = IPV4_CHECKSUM_OFFSET;
+        final int ipSum = getChecksum(packet, ipSumOffset);
+        assertEquals(0xf68b, ipSum);
+
+        packet.put(ipSumOffset, (byte) 0);
+        packet.put(ipSumOffset + 1, (byte) 0);
+        assertChecksumEquals(ipSum, IpUtils.ipChecksum(packet, 0));
+
+        final int udpSumOffset = IPV4_HEADER_LENGTH + UDP_CHECKSUM_OFFSET;
+        final int udpSum = getChecksum(packet, udpSumOffset);
+        assertEquals(0xee36, udpSum);
+
+        packet.put(udpSumOffset, (byte) 0);
+        packet.put(udpSumOffset + 1, (byte) 0);
+        assertChecksumEquals(udpSum, IpUtils.udpChecksum(packet, 0, IPV4_HEADER_LENGTH));
+
+        // Check that writing the checksums back into the packet results in a valid packet.
+        packet.putShort(ipSumOffset, IpUtils.ipChecksum(packet, 0));
+        packet.putShort(udpSumOffset, IpUtils.udpChecksum(packet, 0, IPV4_HEADER_LENGTH));
+        assertEquals(0, IpUtils.ipChecksum(packet, 0));
+        assertEquals((short) 0xffff, IpUtils.udpChecksum(packet, 0, IPV4_HEADER_LENGTH));
+    }
+
+    @Test
+    public void testIpv4IcmpChecksum() throws Exception {
+        // packet = (scapy.IP(src="192.0.2.1", dst="192.0.2.2", tos=0x40) /
+        //           scapy.ICMP(type=0x8, id=0x1234, seq=0x5678) /
+        //           "hello, world")
+        ByteBuffer packet = ByteBuffer.wrap(new byte[] {
+            /* IPv4 */
+            (byte) 0x45, (byte) 0x40, (byte) 0x00, (byte) 0x28,
+            (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00,
+            (byte) 0x40, (byte) 0x01, (byte) 0xf6, (byte) 0x90,
+            (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+            (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x02,
+            /* ICMP */
+            (byte) 0x08,                                         /* type: echo-request */
+            (byte) 0x00,                                         /* code: 0 */
+            (byte) 0x4f, (byte) 0x07,                            /* chksum: 0x4f07 */
+            (byte) 0x12, (byte) 0x34,                            /* id: 0x1234 */
+            (byte) 0x56, (byte) 0x78,                            /* seq: 0x5678 */
+            (byte) 0x68, (byte) 0x65, (byte) 0x6c, (byte) 0x6c,  /* data: hello, world */
+            (byte) 0x6f, (byte) 0x2c, (byte) 0x20, (byte) 0x77,
+            (byte) 0x6f, (byte) 0x72, (byte) 0x6c, (byte) 0x64
+        });
+
+        // Check that a valid packet has checksum 0.
+        int transportLen = packet.limit() - IPV4_HEADER_LENGTH;
+        assertEquals(0, IpUtils.icmpChecksum(packet, IPV4_HEADER_LENGTH, transportLen));
+
+        // Check that we can calculate the checksum from scratch.
+        int sumOffset = IPV4_HEADER_LENGTH + ICMP_CHECKSUM_OFFSET;
+        int sum = getUnsignedByte(packet, sumOffset) * 256 + getUnsignedByte(packet, sumOffset + 1);
+        assertEquals(0x4f07, sum);
+
+        packet.put(sumOffset, (byte) 0);
+        packet.put(sumOffset + 1, (byte) 0);
+        assertChecksumEquals(sum, IpUtils.icmpChecksum(packet, IPV4_HEADER_LENGTH, transportLen));
+
+        // Check that writing the checksum back into the packet results in a valid packet.
+        packet.putShort(
+            sumOffset,
+            IpUtils.icmpChecksum(packet, IPV4_HEADER_LENGTH, transportLen));
+        assertEquals(0, IpUtils.icmpChecksum(packet, IPV4_HEADER_LENGTH, transportLen));
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/Ipv6UtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/Ipv6UtilsTest.java
new file mode 100644
index 0000000..4866a48
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/Ipv6UtilsTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class Ipv6UtilsTest {
+
+    private static final MacAddress MAC1 = MacAddress.fromString("11:22:33:44:55:66");
+    private static final MacAddress MAC2 = MacAddress.fromString("aa:bb:cc:dd:ee:ff");
+    private static final Inet6Address LINK_LOCAL = addr("fe80::1");
+    private static final Inet6Address ROUTER_LINK_LOCAL = addr("fe80::cafe:d00d");
+    private static final Inet6Address ALL_ROUTERS =
+            NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+    private static final Inet6Address ALL_NODES =
+            NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+
+    @Test
+    public void testBuildRsPacket() {
+        ByteBuffer b = Ipv6Utils.buildRsPacket(MAC1, MAC2, LINK_LOCAL, ALL_ROUTERS /* no opts */);
+
+        EthernetHeader eth = Struct.parse(EthernetHeader.class, b);
+        assertEquals(MAC1, eth.srcMac);
+        assertEquals(MAC2, eth.dstMac);
+
+        Ipv6Header ipv6 = Struct.parse(Ipv6Header.class, b);
+        assertEquals(255, ipv6.hopLimit);
+        assertEquals(OsConstants.IPPROTO_ICMPV6, ipv6.nextHeader);
+        assertEquals(LINK_LOCAL, ipv6.srcIp);
+        assertEquals(ALL_ROUTERS, ipv6.dstIp);
+
+        Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, b);
+        assertEquals(NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION, icmpv6.type);
+        assertEquals(0, icmpv6.code);
+    }
+
+    @Test
+    public void testBuildRaPacket() {
+        final byte pioFlags =
+                NetworkStackConstants.PIO_FLAG_AUTONOMOUS | NetworkStackConstants.PIO_FLAG_ON_LINK;
+        ByteBuffer pio1 = PrefixInformationOption.build(new IpPrefix("2001:db8:1::/64"),
+                pioFlags, 3600 /* validLifetime */, 1800 /* preferredLifetime */);
+        ByteBuffer pio2 = PrefixInformationOption.build(new IpPrefix("fdcd:a17f:6502:1::/64"),
+                pioFlags, 86400 /* validLifetime */, 86400 /* preferredLifetime */);
+
+        ByteBuffer b = Ipv6Utils.buildRaPacket(MAC2, MAC1, ROUTER_LINK_LOCAL, ALL_NODES,
+                (byte) 0 /* flags */, 7200 /* lifetime */,
+                30_000 /* reachableTime */, 750 /* retransTimer */,
+                pio1, pio2);
+
+        EthernetHeader eth = Struct.parse(EthernetHeader.class, b);
+        assertEquals(MAC2, eth.srcMac);
+        assertEquals(MAC1, eth.dstMac);
+
+        Ipv6Header ipv6 = Struct.parse(Ipv6Header.class, b);
+        assertEquals(255, ipv6.hopLimit);
+        assertEquals(OsConstants.IPPROTO_ICMPV6, ipv6.nextHeader);
+        assertEquals(ROUTER_LINK_LOCAL, ipv6.srcIp);
+        assertEquals(ALL_NODES, ipv6.dstIp);
+
+        Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, b);
+        assertEquals(NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT, icmpv6.type);
+        assertEquals(0, icmpv6.code);
+
+        RaHeader ra = Struct.parse(RaHeader.class, b);
+        assertEquals(0, ra.hopLimit);  // Hop limit: unspecified.
+        assertEquals(0, ra.flags);
+        assertEquals(7200, ra.lifetime);
+        assertEquals(30_000, ra.reachableTime);
+        assertEquals(750, ra.retransTimer);
+
+        PrefixInformationOption pio = Struct.parse(PrefixInformationOption.class, b);
+        assertPioEquals(pio, "2001:db8:1::/64", pioFlags, 3600, 1800);
+        pio = Struct.parse(PrefixInformationOption.class, b);
+        assertPioEquals(pio, "fdcd:a17f:6502:1::/64", pioFlags, 86400, 86400);
+    }
+
+    @Test
+    public void testBuildEchoRequestPacket() {
+        final ByteBuffer b = Ipv6Utils.buildEchoRequestPacket(MAC2, MAC1, LINK_LOCAL, ALL_NODES);
+
+        EthernetHeader eth = Struct.parse(EthernetHeader.class, b);
+        assertEquals(MAC2, eth.srcMac);
+        assertEquals(MAC1, eth.dstMac);
+
+        Ipv6Header ipv6 = Struct.parse(Ipv6Header.class, b);
+        assertEquals(255, ipv6.hopLimit);
+        assertEquals(OsConstants.IPPROTO_ICMPV6, ipv6.nextHeader);
+        assertEquals(LINK_LOCAL, ipv6.srcIp);
+        assertEquals(ALL_NODES, ipv6.dstIp);
+
+        Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, b);
+        assertEquals(NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE, icmpv6.type);
+        assertEquals(0, icmpv6.code);
+    }
+
+    @Test
+    public void testBuildEchoReplyPacket() {
+        final ByteBuffer b = Ipv6Utils.buildEchoReplyPacket(LINK_LOCAL, ALL_NODES);
+
+        Ipv6Header ipv6 = Struct.parse(Ipv6Header.class, b);
+        assertEquals(255, ipv6.hopLimit);
+        assertEquals(OsConstants.IPPROTO_ICMPV6, ipv6.nextHeader);
+        assertEquals(LINK_LOCAL, ipv6.srcIp);
+        assertEquals(ALL_NODES, ipv6.dstIp);
+
+        Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, b);
+        assertEquals(NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE, icmpv6.type);
+        assertEquals(0, icmpv6.code);
+    }
+
+    private void assertPioEquals(PrefixInformationOption pio, String prefix, byte flags,
+            long valid, long preferred) {
+        assertEquals(NetworkStackConstants.ICMPV6_ND_OPTION_PIO, pio.type);
+        assertEquals(4, pio.length);
+        assertEquals(flags, pio.flags);
+        assertEquals(valid, pio.validLifetime);
+        assertEquals(preferred, pio.preferredLifetime);
+        IpPrefix expected = new IpPrefix(prefix);
+        IpPrefix actual = new IpPrefix(pio.prefix, pio.prefixLen);
+        assertEquals(expected, actual);
+    }
+
+    private static Inet6Address addr(String addr) {
+        return (Inet6Address) InetAddresses.parseNumericAddress(addr);
+    }
+
+    private byte[] slice(byte[] array, int length) {
+        return Arrays.copyOf(array, length);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/JniUtilTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/JniUtilTest.kt
new file mode 100644
index 0000000..7574087
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/JniUtilTest.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.net.module.util
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+public final class JniUtilTest {
+    private val TEST_JAVA_UTIL_NAME = "java_util_jni"
+    private val TEST_ORG_JUNIT_NAME = "org_junit_jni"
+
+    @Test
+    fun testGetJniLibraryName() {
+        assertEquals(TEST_JAVA_UTIL_NAME,
+                JniUtil.getJniLibraryName(java.util.Set::class.java.getPackage()))
+        assertEquals(TEST_ORG_JUNIT_NAME,
+                JniUtil.getJniLibraryName(org.junit.Before::class.java.getPackage()))
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java
new file mode 100644
index 0000000..80ab618
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static com.android.testutils.MiscAsserts.assertSameElements;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.SuppressLint;
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.ProxyInfo;
+import android.net.RouteInfo;
+import android.util.ArraySet;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
+import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+@RunWith(AndroidJUnit4.class)
+public final class LinkPropertiesUtilsTest {
+    @SuppressLint("NewApi")
+    private static final IpPrefix PREFIX = new IpPrefix(toInetAddress("75.208.6.0"), 24);
+    private static final InetAddress V4_ADDR = toInetAddress("75.208.6.1");
+    private static final InetAddress V6_ADDR  = toInetAddress(
+            "2001:0db8:85a3:0000:0000:8a2e:0370:7334");
+    private static final InetAddress DNS1 = toInetAddress("75.208.7.1");
+    private static final InetAddress DNS2 = toInetAddress("69.78.7.1");
+
+    private static final InetAddress GATEWAY1 = toInetAddress("75.208.8.1");
+    private static final InetAddress GATEWAY2 = toInetAddress("69.78.8.1");
+
+    private static final String IF_NAME = "wlan0";
+    private static final LinkAddress V4_LINKADDR = new LinkAddress(V4_ADDR, 32);
+    private static final LinkAddress V6_LINKADDR = new LinkAddress(V6_ADDR, 128);
+    private static final RouteInfo RT_INFO1 = new RouteInfo(PREFIX, GATEWAY1, IF_NAME);
+    private static final RouteInfo RT_INFO2 = new RouteInfo(PREFIX, GATEWAY2, IF_NAME);
+    private static final String TEST_DOMAIN = "link.properties.com";
+
+    private static InetAddress toInetAddress(String addrString) {
+        return InetAddresses.parseNumericAddress(addrString);
+    }
+
+    private LinkProperties createTestObject() {
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(IF_NAME);
+        lp.addLinkAddress(V4_LINKADDR);
+        lp.addLinkAddress(V6_LINKADDR);
+        lp.addDnsServer(DNS1);
+        lp.addDnsServer(DNS2);
+        lp.setDomains(TEST_DOMAIN);
+        lp.addRoute(RT_INFO1);
+        lp.addRoute(RT_INFO2);
+        lp.setHttpProxy(ProxyInfo.buildDirectProxy("test", 8888));
+        return lp;
+    }
+
+    @Test
+    public void testLinkPropertiesIdenticalEqual() {
+        final LinkProperties source = createTestObject();
+        final LinkProperties target = new LinkProperties(source);
+
+        assertTrue(LinkPropertiesUtils.isIdenticalInterfaceName(source, target));
+        assertTrue(LinkPropertiesUtils.isIdenticalInterfaceName(target, source));
+
+        assertTrue(LinkPropertiesUtils.isIdenticalAddresses(source, target));
+        assertTrue(LinkPropertiesUtils.isIdenticalAddresses(target, source));
+
+        assertTrue(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+        assertTrue(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
+
+        assertTrue(LinkPropertiesUtils.isIdenticalDnses(source, target));
+        assertTrue(LinkPropertiesUtils.isIdenticalDnses(target, source));
+
+        assertTrue(LinkPropertiesUtils.isIdenticalRoutes(source, target));
+        assertTrue(LinkPropertiesUtils.isIdenticalRoutes(target, source));
+
+        assertTrue(LinkPropertiesUtils.isIdenticalHttpProxy(source, target));
+        assertTrue(LinkPropertiesUtils.isIdenticalHttpProxy(target, source));
+
+        // Test different interface name.
+        target.setInterfaceName("lo");
+        assertFalse(LinkPropertiesUtils.isIdenticalInterfaceName(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalInterfaceName(target, source));
+        // Restore interface name
+        target.setInterfaceName(IF_NAME);
+
+        // Compare addresses.size() not equals.
+        final LinkAddress testLinkAddr = new LinkAddress(toInetAddress("75.208.6.2"), 32);
+        target.addLinkAddress(testLinkAddr);
+        assertFalse(LinkPropertiesUtils.isIdenticalAddresses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalAddresses(target, source));
+
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
+
+        // Currently, target contains V4_LINKADDR, V6_LINKADDR and testLinkAddr.
+        // Compare addresses.size() equals but contains different address.
+        target.removeLinkAddress(V4_LINKADDR);
+        assertEquals(source.getAddresses().size(), target.getAddresses().size());
+        assertFalse(LinkPropertiesUtils.isIdenticalAddresses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalAddresses(target, source));
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
+        // Restore link address
+        target.addLinkAddress(V4_LINKADDR);
+        target.removeLinkAddress(testLinkAddr);
+
+        // Compare size not equals.
+        target.addDnsServer(toInetAddress("75.208.10.1"));
+        assertFalse(LinkPropertiesUtils.isIdenticalDnses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalDnses(target, source));
+
+        // Compare the same servers but target has different domains.
+        target.removeDnsServer(toInetAddress("75.208.10.1"));
+        target.setDomains("test.com");
+        assertFalse(LinkPropertiesUtils.isIdenticalDnses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalDnses(target, source));
+
+        // Test null domain.
+        target.setDomains(null);
+        assertFalse(LinkPropertiesUtils.isIdenticalDnses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalDnses(target, source));
+        // Restore domain
+        target.setDomains(TEST_DOMAIN);
+
+        // Compare size not equals.
+        final RouteInfo testRoute = new RouteInfo(toInetAddress("75.208.7.1"));
+        target.addRoute(testRoute);
+        assertFalse(LinkPropertiesUtils.isIdenticalRoutes(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalRoutes(target, source));
+
+        // Currently, target contains RT_INFO1, RT_INFO2 and testRoute.
+        // Compare size equals but different routes.
+        target.removeRoute(RT_INFO1);
+        assertEquals(source.getRoutes().size(), target.getRoutes().size());
+        assertFalse(LinkPropertiesUtils.isIdenticalRoutes(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalRoutes(target, source));
+        // Restore route
+        target.addRoute(RT_INFO1);
+        target.removeRoute(testRoute);
+
+        // Test different proxy.
+        target.setHttpProxy(ProxyInfo.buildDirectProxy("hello", 8888));
+        assertFalse(LinkPropertiesUtils.isIdenticalHttpProxy(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalHttpProxy(target, source));
+
+        // Test null proxy.
+        target.setHttpProxy(null);
+        assertFalse(LinkPropertiesUtils.isIdenticalHttpProxy(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalHttpProxy(target, source));
+
+        final LinkProperties stacked = new LinkProperties();
+        stacked.setInterfaceName("v4-" + target.getInterfaceName());
+        stacked.addLinkAddress(testLinkAddr);
+        target.addStackedLink(stacked);
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
+    }
+
+    private <T> void compareResult(List<T> oldItems, List<T> newItems, List<T> expectRemoved,
+            List<T> expectAdded) {
+        CompareResult<T> result = new CompareResult<>(oldItems, newItems);
+        assertEquals(new ArraySet<>(expectAdded), new ArraySet<>(result.added));
+        assertEquals(new ArraySet<>(expectRemoved), (new ArraySet<>(result.removed)));
+    }
+
+    @Test
+    public void testCompareResult() {
+        // Either adding or removing items
+        compareResult(Arrays.asList(1, 2, 3, 4), Arrays.asList(1),
+                Arrays.asList(2, 3, 4), new ArrayList<>());
+        compareResult(Arrays.asList(1, 2), Arrays.asList(3, 2, 1, 4),
+                new ArrayList<>(), Arrays.asList(3, 4));
+
+
+        // adding and removing items at the same time
+        compareResult(Arrays.asList(1, 2, 3, 4), Arrays.asList(2, 3, 4, 5),
+                Arrays.asList(1), Arrays.asList(5));
+        compareResult(Arrays.asList(1, 2, 3), Arrays.asList(4, 5, 6),
+                Arrays.asList(1, 2, 3), Arrays.asList(4, 5, 6));
+
+        // null cases
+        compareResult(Arrays.asList(1, 2, 3), null, Arrays.asList(1, 2, 3), new ArrayList<>());
+        compareResult(null, Arrays.asList(3, 2, 1), new ArrayList<>(), Arrays.asList(1, 2, 3));
+        compareResult(null, null, new ArrayList<>(), new ArrayList<>());
+
+        // Some more tests with strings
+        final ArrayList<String> list1 = new ArrayList<>();
+        list1.add("string1");
+
+        final ArrayList<String> list2 = new ArrayList<>(list1);
+        final CompareResult<String> cr1 = new CompareResult<>(list1, list2);
+        assertTrue(cr1.added.isEmpty());
+        assertTrue(cr1.removed.isEmpty());
+
+        list2.add("string2");
+        final CompareResult<String> cr2 = new CompareResult<>(list1, list2);
+        assertEquals(Arrays.asList("string2"), cr2.added);
+        assertTrue(cr2.removed.isEmpty());
+
+        list2.remove("string1");
+        final CompareResult<String> cr3 = new CompareResult<>(list1, list2);
+        assertEquals(Arrays.asList("string2"), cr3.added);
+        assertEquals(Arrays.asList("string1"), cr3.removed);
+
+        list1.add("string2");
+        final CompareResult<String> cr4 = new CompareResult<>(list1, list2);
+        assertTrue(cr4.added.isEmpty());
+        assertEquals(Arrays.asList("string1"), cr3.removed);
+    }
+
+    @Test
+    public void testCompareAddresses() {
+        final LinkProperties source = createTestObject();
+        final LinkProperties target = new LinkProperties(source);
+        final InetAddress addr1 = toInetAddress("75.208.6.2");
+        final LinkAddress linkAddr1 = new LinkAddress(addr1, 32);
+
+        CompareResult<LinkAddress> results = LinkPropertiesUtils.compareAddresses(source, target);
+        assertEquals(0, results.removed.size());
+        assertEquals(0, results.added.size());
+
+        source.addLinkAddress(linkAddr1);
+        results = LinkPropertiesUtils.compareAddresses(source, target);
+        assertEquals(1, results.removed.size());
+        assertEquals(linkAddr1, results.removed.get(0));
+        assertEquals(0, results.added.size());
+
+        final InetAddress addr2 = toInetAddress("75.208.6.3");
+        final LinkAddress linkAddr2 = new LinkAddress(addr2, 32);
+
+        target.addLinkAddress(linkAddr2);
+        results = LinkPropertiesUtils.compareAddresses(source, target);
+        assertEquals(linkAddr1, results.removed.get(0));
+        assertEquals(linkAddr2, results.added.get(0));
+    }
+
+    private void assertCompareOrUpdateResult(CompareOrUpdateResult result,
+            List<String> expectedAdded, List<String> expectedRemoved,
+            List<String> expectedUpdated) {
+        assertSameElements(expectedAdded, result.added);
+        assertSameElements(expectedRemoved, result.removed);
+        assertSameElements(expectedUpdated, result.updated);
+    }
+
+    private List<String> strArray(String... strs) {
+        return Arrays.asList(strs);
+    }
+
+    @Test
+    public void testCompareOrUpdateResult() {
+        // As the item type, use a simple string. An item is defined to be an update of another item
+        // if the string starts with the same alphabetical characters.
+        // Extracting the key from the object is just a regexp.
+        Function<String, String> extractPrefix = (s) -> s.replaceFirst("^([a-z]+).*", "$1");
+        assertEquals("goodbye", extractPrefix.apply("goodbye1234"));
+
+        List<String> oldItems = strArray("hello123", "goodbye5678", "howareyou669");
+        List<String> newItems = strArray("hello123", "goodbye000", "verywell");
+
+        final List<String> emptyList = new ArrayList<>();
+
+        // Items -> empty: everything removed.
+        CompareOrUpdateResult<String, String> result =
+                new CompareOrUpdateResult<String, String>(oldItems, emptyList, extractPrefix);
+        assertCompareOrUpdateResult(result,
+                emptyList, strArray("hello123", "howareyou669", "goodbye5678"), emptyList);
+
+        // Empty -> items: everything added.
+        result = new CompareOrUpdateResult<String, String>(emptyList, newItems, extractPrefix);
+        assertCompareOrUpdateResult(result,
+                strArray("hello123", "goodbye000", "verywell"), emptyList,  emptyList);
+
+        // Empty -> empty: no change.
+        result = new CompareOrUpdateResult<String, String>(newItems, newItems, extractPrefix);
+        assertCompareOrUpdateResult(result,  emptyList,  emptyList, emptyList);
+
+        // Added, removed, updated at the same time.
+        result =  new CompareOrUpdateResult<>(oldItems, newItems, extractPrefix);
+        assertCompareOrUpdateResult(result,
+                strArray("verywell"), strArray("howareyou669"), strArray("goodbye000"));
+
+        // Null -> items: everything added.
+        result = new CompareOrUpdateResult<String, String>(null, newItems, extractPrefix);
+        assertCompareOrUpdateResult(result,
+                strArray("hello123", "goodbye000", "verywell"), emptyList,  emptyList);
+
+        // Items -> null: everything removed.
+        result = new CompareOrUpdateResult<String, String>(oldItems, null, extractPrefix);
+        assertCompareOrUpdateResult(result,
+                emptyList, strArray("hello123", "howareyou669", "goodbye5678"), emptyList);
+
+        // Null -> null: all lists empty.
+        result = new CompareOrUpdateResult<String, String>(null, null, extractPrefix);
+        assertCompareOrUpdateResult(result, emptyList, emptyList, emptyList);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
new file mode 100644
index 0000000..84018a5
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
@@ -0,0 +1,315 @@
+/*
+ * 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 static android.Manifest.permission.NETWORK_SETTINGS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.HashMap;
+
+/** Unit tests for {@link LocationPermissionChecker}. */
+@RequiresApi(Build.VERSION_CODES.R)
+public class LocationPermissionCheckerTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(
+            Build.VERSION_CODES.Q /* ignoreClassUpTo */);
+
+    // Mock objects for testing
+    @Mock private Context mMockContext;
+    @Mock private PackageManager mMockPkgMgr;
+    @Mock private ApplicationInfo mMockApplInfo;
+    @Mock private AppOpsManager mMockAppOps;
+    @Mock private UserManager mMockUserManager;
+    @Mock private LocationManager mLocationManager;
+
+    private static final String TEST_PKG_NAME = "com.google.somePackage";
+    private static final String TEST_FEATURE_ID = "com.google.someFeature";
+    private static final int MANAGED_PROFILE_UID = 1100000;
+    private static final int OTHER_USER_UID = 1200000;
+
+    private final String mInteractAcrossUsersFullPermission =
+            "android.permission.INTERACT_ACROSS_USERS_FULL";
+    private final String mManifestStringCoarse =
+            Manifest.permission.ACCESS_COARSE_LOCATION;
+    private final String mManifestStringFine =
+            Manifest.permission.ACCESS_FINE_LOCATION;
+
+    // Test variables
+    private int mWifiScanAllowApps;
+    private int mUid;
+    private int mCoarseLocationPermission;
+    private int mAllowCoarseLocationApps;
+    private int mFineLocationPermission;
+    private int mAllowFineLocationApps;
+    private int mNetworkSettingsPermission;
+    private int mCurrentUid;
+    private boolean mIsLocationEnabled;
+    private boolean mThrowSecurityException;
+    private Answer<Integer> mReturnPermission;
+    private HashMap<String, Integer> mPermissionsList = new HashMap<String, Integer>();
+    private LocationPermissionChecker mChecker;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        initTestVars();
+    }
+
+    private void setupMocks() throws Exception {
+        when(mMockPkgMgr.getApplicationInfoAsUser(eq(TEST_PKG_NAME), eq(0), any()))
+                .thenReturn(mMockApplInfo);
+        when(mMockContext.getPackageManager()).thenReturn(mMockPkgMgr);
+        when(mMockAppOps.noteOp(AppOpsManager.OPSTR_WIFI_SCAN, mUid, TEST_PKG_NAME,
+                TEST_FEATURE_ID, null)).thenReturn(mWifiScanAllowApps);
+        when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_COARSE_LOCATION), eq(mUid),
+                eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class)))
+                .thenReturn(mAllowCoarseLocationApps);
+        when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_FINE_LOCATION), eq(mUid),
+                eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class)))
+                .thenReturn(mAllowFineLocationApps);
+        if (mThrowSecurityException) {
+            doThrow(new SecurityException("Package " + TEST_PKG_NAME + " doesn't belong"
+                    + " to application bound to user " + mUid))
+                    .when(mMockAppOps).checkPackage(mUid, TEST_PKG_NAME);
+        }
+        mockSystemService(Context.APP_OPS_SERVICE, AppOpsManager.class, mMockAppOps);
+        mockSystemService(Context.USER_SERVICE, UserManager.class, mMockUserManager);
+        mockSystemService(Context.LOCATION_SERVICE, LocationManager.class, mLocationManager);
+    }
+
+    private <T> void mockSystemService(String name, Class<T> clazz, T service) {
+        when(mMockContext.getSystemService(name)).thenReturn(service);
+        when(mMockContext.getSystemServiceName(clazz)).thenReturn(name);
+        // Do not use mockito extended final method mocking
+        when(mMockContext.getSystemService(clazz)).thenCallRealMethod();
+    }
+
+    private void setupTestCase() throws Exception {
+        setupMocks();
+        setupMockInterface();
+        mChecker = new LocationPermissionChecker(mMockContext) {
+            @Override
+            protected int getCurrentUser() {
+                // Get the user ID of the process running the test rather than the foreground user
+                // id: ActivityManager.getCurrentUser() requires privileged permissions.
+                return UserHandle.getUserHandleForUid(Process.myUid()).getIdentifier();
+            }
+        };
+    }
+
+    private void initTestVars() {
+        mPermissionsList.clear();
+        mReturnPermission = createPermissionAnswer();
+        mWifiScanAllowApps = AppOpsManager.MODE_ERRORED;
+        mUid = OTHER_USER_UID;
+        mThrowSecurityException = true;
+        mMockApplInfo.targetSdkVersion = Build.VERSION_CODES.M;
+        mIsLocationEnabled = false;
+        mCurrentUid = Process.myUid();
+        mCoarseLocationPermission = PackageManager.PERMISSION_DENIED;
+        mFineLocationPermission = PackageManager.PERMISSION_DENIED;
+        mAllowCoarseLocationApps = AppOpsManager.MODE_ERRORED;
+        mAllowFineLocationApps = AppOpsManager.MODE_ERRORED;
+        mNetworkSettingsPermission = PackageManager.PERMISSION_DENIED;
+    }
+
+    private void setupMockInterface() {
+        Binder.restoreCallingIdentity((((long) mUid) << 32) | Binder.getCallingPid());
+        doAnswer(mReturnPermission).when(mMockContext).checkPermission(
+                anyString(), anyInt(), anyInt());
+        when(mMockUserManager.isSameProfileGroup(UserHandle.SYSTEM,
+                UserHandle.getUserHandleForUid(MANAGED_PROFILE_UID)))
+                .thenReturn(true);
+        when(mMockContext.checkPermission(mManifestStringCoarse, -1, mUid))
+                .thenReturn(mCoarseLocationPermission);
+        when(mMockContext.checkPermission(mManifestStringFine, -1, mUid))
+                .thenReturn(mFineLocationPermission);
+        when(mMockContext.checkPermission(NETWORK_SETTINGS, -1, mUid))
+                .thenReturn(mNetworkSettingsPermission);
+        when(mLocationManager.isLocationEnabledForUser(any())).thenReturn(mIsLocationEnabled);
+    }
+
+    private Answer<Integer> createPermissionAnswer() {
+        return new Answer<Integer>() {
+            @Override
+            public Integer answer(InvocationOnMock invocation) {
+                int myUid = (int) invocation.getArguments()[1];
+                String myPermission = (String) invocation.getArguments()[0];
+                mPermissionsList.get(myPermission);
+                if (mPermissionsList.containsKey(myPermission)) {
+                    int uid = mPermissionsList.get(myPermission);
+                    if (myUid == uid) {
+                        return PackageManager.PERMISSION_GRANTED;
+                    }
+                }
+                return PackageManager.PERMISSION_DENIED;
+            }
+        };
+    }
+
+    @Test
+    public void testEnforceLocationPermission_HasAllPermissions_BeforeQ() throws Exception {
+        mIsLocationEnabled = true;
+        mThrowSecurityException = false;
+        mCoarseLocationPermission = PackageManager.PERMISSION_GRANTED;
+        mAllowCoarseLocationApps = AppOpsManager.MODE_ALLOWED;
+        mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
+        mUid = mCurrentUid;
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.SUCCEEDED, result);
+    }
+
+    @Test
+    public void testEnforceLocationPermission_HasAllPermissions_AfterQ() throws Exception {
+        mMockApplInfo.targetSdkVersion = Build.VERSION_CODES.Q;
+        mIsLocationEnabled = true;
+        mThrowSecurityException = false;
+        mUid = mCurrentUid;
+        mFineLocationPermission = PackageManager.PERMISSION_GRANTED;
+        mAllowFineLocationApps = AppOpsManager.MODE_ALLOWED;
+        mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.SUCCEEDED, result);
+    }
+
+    @Test
+    public void testEnforceLocationPermission_PkgNameAndUidMismatch() throws Exception {
+        mThrowSecurityException = true;
+        mIsLocationEnabled = true;
+        mFineLocationPermission = PackageManager.PERMISSION_GRANTED;
+        mAllowFineLocationApps = AppOpsManager.MODE_ALLOWED;
+        mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
+        setupTestCase();
+
+        assertThrows(SecurityException.class,
+                () -> mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
+    }
+
+    @Test
+    public void testenforceCanAccessScanResults_NoCoarseLocationPermission() throws Exception {
+        mThrowSecurityException = false;
+        mIsLocationEnabled = true;
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
+    }
+
+    @Test
+    public void testenforceCanAccessScanResults_NoFineLocationPermission() throws Exception {
+        mThrowSecurityException = false;
+        mMockApplInfo.targetSdkVersion = Build.VERSION_CODES.Q;
+        mIsLocationEnabled = true;
+        mCoarseLocationPermission = PackageManager.PERMISSION_GRANTED;
+        mAllowFineLocationApps = AppOpsManager.MODE_ERRORED;
+        mUid = MANAGED_PROFILE_UID;
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
+        verify(mMockAppOps, never()).noteOp(anyInt(), anyInt(), anyString());
+    }
+
+    @Test
+    public void testenforceCanAccessScanResults_LocationModeDisabled() throws Exception {
+        mThrowSecurityException = false;
+        mUid = MANAGED_PROFILE_UID;
+        mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
+        mPermissionsList.put(mInteractAcrossUsersFullPermission, mUid);
+        mIsLocationEnabled = false;
+
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.ERROR_LOCATION_MODE_OFF, result);
+    }
+
+    @Test
+    public void testenforceCanAccessScanResults_LocationModeDisabledHasNetworkSettings()
+            throws Exception {
+        mThrowSecurityException = false;
+        mIsLocationEnabled = false;
+        mNetworkSettingsPermission = PackageManager.PERMISSION_GRANTED;
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.SUCCEEDED, result);
+    }
+
+
+    private static void assertThrows(Class<? extends Exception> exceptionClass, Runnable r) {
+        try {
+            r.run();
+            Assert.fail("Expected " + exceptionClass + " to be thrown.");
+        } catch (Exception exception) {
+            assertTrue(exceptionClass.isInstance(exception));
+        }
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/MacAddressUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/MacAddressUtilsTest.java
new file mode 100644
index 0000000..2550756
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/MacAddressUtilsTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.MacAddress;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public final class MacAddressUtilsTest {
+
+    // Matches WifiInfo.DEFAULT_MAC_ADDRESS
+    private static final MacAddress DEFAULT_MAC_ADDRESS =
+            MacAddress.fromString("02:00:00:00:00:00");
+
+    @Test
+    public void testIsMulticastAddress() {
+        MacAddress[] multicastAddresses = {
+            // broadcast address
+            MacAddress.fromString("ff:ff:ff:ff:ff:ff"),
+            MacAddress.fromString("07:00:d3:56:8a:c4"),
+            MacAddress.fromString("33:33:aa:bb:cc:dd"),
+        };
+        MacAddress[] unicastAddresses = {
+            // all zero address
+            MacAddress.fromString("00:00:00:00:00:00"),
+            MacAddress.fromString("00:01:44:55:66:77"),
+            MacAddress.fromString("08:00:22:33:44:55"),
+            MacAddress.fromString("06:00:00:00:00:00"),
+        };
+
+        for (MacAddress mac : multicastAddresses) {
+            String msg = mac.toString() + " expected to be a multicast address";
+            assertTrue(msg, MacAddressUtils.isMulticastAddress(mac));
+        }
+        for (MacAddress mac : unicastAddresses) {
+            String msg = mac.toString() + " expected not to be a multicast address";
+            assertFalse(msg, MacAddressUtils.isMulticastAddress(mac));
+        }
+    }
+
+    @Test
+    public void testMacAddressRandomGeneration() {
+        final int iterations = 1000;
+
+        for (int i = 0; i < iterations; i++) {
+            MacAddress mac = MacAddressUtils.createRandomUnicastAddress();
+            String stringRepr = mac.toString();
+
+            assertTrue(stringRepr + " expected to be a locally assigned address",
+                    mac.isLocallyAssigned());
+            assertEquals(MacAddress.TYPE_UNICAST, mac.getAddressType());
+            assertFalse(mac.equals(DEFAULT_MAC_ADDRESS));
+        }
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/NetUtilsTest.java
new file mode 100644
index 0000000..9e635c2
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetUtilsTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.RouteInfo;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public final class NetUtilsTest {
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+    private static final InetAddress V4_ADDR1 = toInetAddress("75.208.7.1");
+    private static final InetAddress V4_ADDR2 = toInetAddress("75.208.7.2");
+    private static final InetAddress V6_ADDR1 = toInetAddress("2001:0db8:85a3::8a2e:0370:7334");
+    private static final InetAddress V6_ADDR2 = toInetAddress("2001:0db8:85a3::8a2e:0370:7335");
+
+    private static final InetAddress V4_GATEWAY = toInetAddress("75.208.8.1");
+    private static final InetAddress V6_GATEWAY = toInetAddress("fe80::6:0000:613");
+
+    private static final InetAddress V4_DEST = toInetAddress("75.208.8.15");
+    private static final InetAddress V6_DEST = toInetAddress("2001:db8:cafe::123");
+
+    private static final RouteInfo V4_EXPECTED = new RouteInfo(new IpPrefix("75.208.8.0/24"),
+            V4_GATEWAY, "wlan0");
+    private static final RouteInfo V6_EXPECTED = new RouteInfo(new IpPrefix("2001:db8:cafe::/64"),
+            V6_GATEWAY, "wlan0");
+
+    private static InetAddress toInetAddress(String addr) {
+        return InetAddresses.parseNumericAddress(addr);
+    }
+
+    @Test
+    public void testAddressTypeMatches() {
+        assertTrue(NetUtils.addressTypeMatches(V4_ADDR1, V4_ADDR2));
+        assertTrue(NetUtils.addressTypeMatches(V6_ADDR1, V6_ADDR2));
+        assertFalse(NetUtils.addressTypeMatches(V4_ADDR1, V6_ADDR1));
+        assertFalse(NetUtils.addressTypeMatches(V6_ADDR1, V4_ADDR1));
+    }
+
+    @Test
+    public void testSelectBestRoute() {
+        final List<RouteInfo> routes = new ArrayList<>();
+
+        RouteInfo route = NetUtils.selectBestRoute(null, V4_DEST);
+        assertNull(route);
+        route = NetUtils.selectBestRoute(routes, null);
+        assertNull(route);
+
+        route = NetUtils.selectBestRoute(routes, V4_DEST);
+        assertNull(route);
+
+        routes.add(V4_EXPECTED);
+        // "75.208.0.0/16" is not an expected result since it is not the longest prefix.
+        routes.add(new RouteInfo(new IpPrefix("75.208.0.0/16"), V4_GATEWAY, "wlan0"));
+        routes.add(new RouteInfo(new IpPrefix("75.208.7.0/24"), V4_GATEWAY, "wlan0"));
+
+        routes.add(V6_EXPECTED);
+        // "2001:db8::/32" is not an expected result since it is not the longest prefix.
+        routes.add(new RouteInfo(new IpPrefix("2001:db8::/32"), V6_GATEWAY, "wlan0"));
+        routes.add(new RouteInfo(new IpPrefix("2001:db8:beef::/64"), V6_GATEWAY, "wlan0"));
+
+        // Verify expected v4 route is selected
+        route = NetUtils.selectBestRoute(routes, V4_DEST);
+        assertEquals(V4_EXPECTED, route);
+
+        // Verify expected v6 route is selected
+        route = NetUtils.selectBestRoute(routes, V6_DEST);
+        assertEquals(V6_EXPECTED, route);
+
+        // Remove expected v4 route
+        routes.remove(V4_EXPECTED);
+        route = NetUtils.selectBestRoute(routes, V4_DEST);
+        assertNotEquals(V4_EXPECTED, route);
+
+        // Remove expected v6 route
+        routes.remove(V6_EXPECTED);
+        route = NetUtils.selectBestRoute(routes, V4_DEST);
+        assertNotEquals(V6_EXPECTED, route);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testSelectBestRouteWithExcludedRoutes() {
+        final List<RouteInfo> routes = new ArrayList<>();
+
+        routes.add(V4_EXPECTED);
+        routes.add(new RouteInfo(new IpPrefix("75.208.0.0/16"), V4_GATEWAY, "wlan0"));
+        routes.add(new RouteInfo(new IpPrefix("75.208.7.0/24"), V4_GATEWAY, "wlan0"));
+
+        routes.add(V6_EXPECTED);
+        routes.add(new RouteInfo(new IpPrefix("2001:db8::/32"), V6_GATEWAY, "wlan0"));
+        routes.add(new RouteInfo(new IpPrefix("2001:db8:beef::/64"), V6_GATEWAY, "wlan0"));
+
+        // After adding excluded v4 route with longer prefix, expected result is null.
+        routes.add(new RouteInfo(new IpPrefix("75.208.8.0/28"), null /* gateway */, "wlan0",
+                RouteInfo.RTN_THROW));
+        RouteInfo route = NetUtils.selectBestRoute(routes, V4_DEST);
+        assertNull(route);
+
+        // After adding excluded v6 route with longer prefix, expected result is null.
+        routes.add(new RouteInfo(new IpPrefix("2001:db8:cafe::/96"), null /* gateway */, "wlan0",
+                RouteInfo.RTN_THROW));
+        route = NetUtils.selectBestRoute(routes, V6_DEST);
+        assertNull(route);
+    }
+}
+
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt
new file mode 100644
index 0000000..958f45f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.annotation.TargetApi
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_BIP
+import android.net.NetworkCapabilities.NET_CAPABILITY_CBS
+import android.net.NetworkCapabilities.NET_CAPABILITY_EIMS
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE
+import android.os.Build
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.modules.utils.build.SdkLevel
+import com.android.net.module.util.NetworkCapabilitiesUtils.RESTRICTED_CAPABILITIES
+import com.android.net.module.util.NetworkCapabilitiesUtils.UNRESTRICTED_CAPABILITIES
+import com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.lang.IllegalArgumentException
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetworkCapabilitiesUtilsTest {
+
+    @Test
+    fun testGetAccountingTransport() {
+        assertEquals(TRANSPORT_WIFI, getDisplayTransport(intArrayOf(TRANSPORT_WIFI)))
+        assertEquals(TRANSPORT_CELLULAR, getDisplayTransport(intArrayOf(TRANSPORT_CELLULAR)))
+        assertEquals(TRANSPORT_BLUETOOTH, getDisplayTransport(intArrayOf(TRANSPORT_BLUETOOTH)))
+        assertEquals(TRANSPORT_ETHERNET, getDisplayTransport(intArrayOf(TRANSPORT_ETHERNET)))
+        assertEquals(TRANSPORT_WIFI_AWARE, getDisplayTransport(intArrayOf(TRANSPORT_WIFI_AWARE)))
+
+        assertEquals(TRANSPORT_VPN, getDisplayTransport(
+                intArrayOf(TRANSPORT_VPN, TRANSPORT_WIFI)))
+        assertEquals(TRANSPORT_VPN, getDisplayTransport(
+                intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_VPN)))
+
+        assertEquals(TRANSPORT_WIFI, getDisplayTransport(
+                intArrayOf(TRANSPORT_ETHERNET, TRANSPORT_WIFI)))
+        assertEquals(TRANSPORT_ETHERNET, getDisplayTransport(
+                intArrayOf(TRANSPORT_ETHERNET, TRANSPORT_TEST)))
+
+        assertFailsWith(IllegalArgumentException::class) {
+            getDisplayTransport(intArrayOf())
+        }
+    }
+
+    // NetworkCapabilities constructor and Builder are not available until R. Mark TargetApi to
+    // ignore the linter error since it's used in only unit test.
+    @Test @TargetApi(Build.VERSION_CODES.R)
+    fun testInferRestrictedCapability() {
+        val nc = NetworkCapabilities()
+        // Default capabilities don't have restricted capability.
+        assertFalse(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
+        // If there is a force restricted capability, then the network capabilities is restricted.
+        nc.addCapability(NET_CAPABILITY_OEM_PAID)
+        nc.addCapability(NET_CAPABILITY_INTERNET)
+        assertTrue(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
+        // Except for the force restricted capability, if there is any unrestricted capability in
+        // capabilities, then the network capabilities is not restricted.
+        nc.removeCapability(NET_CAPABILITY_OEM_PAID)
+        nc.addCapability(NET_CAPABILITY_CBS)
+        assertFalse(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
+        // Except for the force restricted capability, the network capabilities will only be treated
+        // as restricted when there is no any unrestricted capability.
+        nc.removeCapability(NET_CAPABILITY_INTERNET)
+        assertTrue(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
+        if (!SdkLevel.isAtLeastS()) return
+        // BIP deserves its specific test because it's the first capability over 30, meaning the
+        // shift will overflow
+        nc.removeCapability(NET_CAPABILITY_CBS)
+        nc.addCapability(NET_CAPABILITY_BIP)
+        assertTrue(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
+    }
+
+    @Test
+    fun testRestrictedUnrestrictedCapabilities() {
+        // verify EIMS is restricted
+        assertEquals((1 shl NET_CAPABILITY_EIMS).toLong() and RESTRICTED_CAPABILITIES,
+                (1 shl NET_CAPABILITY_EIMS).toLong())
+
+        // verify CBS is also restricted
+        assertEquals((1 shl NET_CAPABILITY_CBS).toLong() and RESTRICTED_CAPABILITIES,
+                (1 shl NET_CAPABILITY_CBS).toLong())
+
+        // verify BIP is also restricted
+        // BIP is not available in R and before, but the BIP constant is inlined so
+        // this test can still run on R.
+        assertEquals((1L shl NET_CAPABILITY_BIP) and RESTRICTED_CAPABILITIES,
+                (1L shl NET_CAPABILITY_BIP))
+
+        // verify default is not restricted
+        assertEquals((1 shl NET_CAPABILITY_INTERNET).toLong() and RESTRICTED_CAPABILITIES, 0)
+
+        assertTrue(RESTRICTED_CAPABILITIES > 0)
+
+        // just to see
+        assertEquals(RESTRICTED_CAPABILITIES and UNRESTRICTED_CAPABILITIES, 0)
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkIdentityUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkIdentityUtilsTest.kt
new file mode 100644
index 0000000..2904e12
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkIdentityUtilsTest.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.net.module.util
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.net.module.util.NetworkIdentityUtils.scrubSubscriberId
+import com.android.net.module.util.NetworkIdentityUtils.scrubSubscriberIds
+import com.android.testutils.assertContainsStringsExactly
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetworkIdentityUtilsTest {
+    @Test
+    fun testScrubSubscriberId() {
+        assertEquals("123456...", scrubSubscriberId("1234567890123"))
+        assertEquals("123456...", scrubSubscriberId("1234567"))
+        assertEquals("123...", scrubSubscriberId("123"))
+        assertEquals("...", scrubSubscriberId(""))
+        assertEquals("null", scrubSubscriberId(null))
+    }
+
+    @Test
+    fun testScrubSubscriberIds() {
+        assertContainsStringsExactly(scrubSubscriberIds(arrayOf("1234567", "", null))!!,
+                "123456...", "...", "null")
+        assertContainsStringsExactly(scrubSubscriberIds(arrayOf("12345"))!!, "12345...")
+        assertContainsStringsExactly(scrubSubscriberIds(arrayOf())!!)
+        assertNull(scrubSubscriberIds(null))
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..2785ea9
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.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 kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetworkStatsUtilsTest {
+    @Test
+    fun testMultiplySafeByRational() {
+        // Verify basic cases that the method equals to a * b / c.
+        assertEquals(3 * 5 / 2, NetworkStatsUtils.multiplySafeByRational(3, 5, 2))
+
+        // Verify input with zeros.
+        assertEquals(0 * 7 / 3, NetworkStatsUtils.multiplySafeByRational(0, 7, 3))
+        assertEquals(7 * 0 / 3, NetworkStatsUtils.multiplySafeByRational(7, 0, 3))
+        assertEquals(0 * 0 / 1, NetworkStatsUtils.multiplySafeByRational(0, 0, 1))
+        assertEquals(0, NetworkStatsUtils.multiplySafeByRational(0, Long.MAX_VALUE, Long.MAX_VALUE))
+        assertEquals(0, NetworkStatsUtils.multiplySafeByRational(Long.MAX_VALUE, 0, Long.MAX_VALUE))
+        assertFailsWith<ArithmeticException> {
+            NetworkStatsUtils.multiplySafeByRational(7, 3, 0)
+        }
+        assertFailsWith<ArithmeticException> {
+            NetworkStatsUtils.multiplySafeByRational(0, 0, 0)
+        }
+
+        // Verify cases where a * b overflows.
+        assertEquals(101, NetworkStatsUtils.multiplySafeByRational(
+                101, Long.MAX_VALUE, Long.MAX_VALUE))
+        assertEquals(721, NetworkStatsUtils.multiplySafeByRational(
+                Long.MAX_VALUE, 721, Long.MAX_VALUE))
+        assertEquals(Long.MAX_VALUE, NetworkStatsUtils.multiplySafeByRational(
+                Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE))
+        assertFailsWith<ArithmeticException> {
+            NetworkStatsUtils.multiplySafeByRational(Long.MAX_VALUE, Long.MAX_VALUE, 0)
+        }
+    }
+
+    @Test
+    fun testConstrain() {
+        assertFailsWith<IllegalArgumentException> {
+            NetworkStatsUtils.constrain(5, 6, 3) // low > high
+        }
+        assertEquals(3, NetworkStatsUtils.constrain(5, 1, 3))
+        assertEquals(3, NetworkStatsUtils.constrain(3, 1, 3))
+        assertEquals(2, NetworkStatsUtils.constrain(2, 1, 3))
+        assertEquals(1, NetworkStatsUtils.constrain(1, 1, 3))
+        assertEquals(1, NetworkStatsUtils.constrain(0, 1, 3))
+
+        assertEquals(11, NetworkStatsUtils.constrain(15, 11, 11))
+        assertEquals(11, NetworkStatsUtils.constrain(11, 11, 11))
+        assertEquals(11, NetworkStatsUtils.constrain(1, 11, 11))
+    }
+
+    @Test
+    fun testBucketToEntry() {
+        val bucket = makeMockBucket(android.app.usage.NetworkStats.Bucket.UID_ALL,
+                android.app.usage.NetworkStats.Bucket.TAG_NONE,
+                android.app.usage.NetworkStats.Bucket.STATE_DEFAULT,
+                android.app.usage.NetworkStats.Bucket.METERED_YES,
+                android.app.usage.NetworkStats.Bucket.ROAMING_NO,
+                android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12)
+        val entry = NetworkStatsUtils.fromBucket(bucket)
+        val expectedEntry = NetworkStats.Entry(null /* IFACE_ALL */, NetworkStats.UID_ALL,
+            NetworkStats.SET_DEFAULT, NetworkStats.TAG_NONE, NetworkStats.METERED_YES,
+            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)
+    }
+
+    private fun makeMockBucket(
+        uid: Int,
+        tag: Int,
+        state: Int,
+        metered: Int,
+        roaming: Int,
+        defaultNetwork: Int,
+        rxBytes: Long,
+        rxPackets: Long,
+        txBytes: Long,
+        txPackets: Long
+    ): android.app.usage.NetworkStats.Bucket {
+        val ret: android.app.usage.NetworkStats.Bucket =
+                mock(android.app.usage.NetworkStats.Bucket::class.java)
+        doReturn(uid).`when`(ret).getUid()
+        doReturn(tag).`when`(ret).getTag()
+        doReturn(state).`when`(ret).getState()
+        doReturn(metered).`when`(ret).getMetered()
+        doReturn(roaming).`when`(ret).getRoaming()
+        doReturn(defaultNetwork).`when`(ret).getDefaultNetworkStatus()
+        doReturn(rxBytes).`when`(ret).getRxBytes()
+        doReturn(rxPackets).`when`(ret).getRxPackets()
+        doReturn(txBytes).`when`(ret).getTxBytes()
+        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
new file mode 100644
index 0000000..886336c
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
@@ -0,0 +1,1103 @@
+/*
+ * 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.net.module.util;
+
+import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_IPV6;
+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;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.net.InetAddresses;
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Ipv4Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.TcpHeader;
+import com.android.net.module.util.structs.UdpHeader;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PacketBuilderTest {
+    private static final MacAddress SRC_MAC = MacAddress.fromString("11:22:33:44:55:66");
+    private static final MacAddress DST_MAC = MacAddress.fromString("aa:bb:cc:dd:ee:ff");
+    private static final Inet4Address IPV4_SRC_ADDR = addr4("192.0.2.1");
+    private static final Inet4Address IPV4_DST_ADDR = addr4("198.51.100.1");
+    private static final Inet6Address IPV6_SRC_ADDR = addr6("2001:db8::1");
+    private static final Inet6Address IPV6_DST_ADDR = addr6("2001:db8::2");
+    private static final short SRC_PORT = 9876;
+    private static final short DST_PORT = 433;
+    private static final short SEQ_NO = 13579;
+    private static final short ACK_NO = 24680;
+    private static final byte TYPE_OF_SERVICE = 0;
+    private static final short ID = 27149;
+    private static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0
+    private static final byte TIME_TO_LIVE = (byte) 0x40;
+    private static final short WINDOW = (short) 0x2000;
+    private static final short URGENT_POINTER = 0;
+    // version=6, traffic class=0x80, flowlabel=0x515ca;
+    private static final int VERSION_TRAFFICCLASS_FLOWLABEL = 0x680515ca;
+    private static final short HOP_LIMIT = 0x40;
+    private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[] {
+            (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+    });
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                       type='IPv4') /
+                //           scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                    tos=0, id=27149, flags='DF') /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0))
+                // 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) 0x08, (byte) 0x00,
+                // IPv4 header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x28,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x8c,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0xe5, (byte) 0xe5, (byte) 0x00, (byte) 0x00
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                       type='IPv4') /
+                //           scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                    tos=0, id=27149, flags='DF') /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0) /
+                //           b'\xde\xad\xbe\xef')
+                // 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) 0x08, (byte) 0x00,
+                // IPv4 header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x2c,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x88,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0x48, (byte) 0x44, (byte) 0x00, (byte) 0x00,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_IPV4HDR_TCPHDR =
+            new byte[] {
+                // packet = (scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                    tos=0, id=27149, flags='DF') /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0))
+                // IPv4 header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x28,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x8c,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0xe5, (byte) 0xe5, (byte) 0x00, (byte) 0x00
+            };
+
+    private static final byte[] TEST_PACKET_IPV4HDR_TCPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                    tos=0, id=27149, flags='DF') /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0) /
+                //           b'\xde\xad\xbe\xef')
+                // IPv4 header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x2c,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x88,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0x48, (byte) 0x44, (byte) 0x00, (byte) 0x00,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV4HDR_UDPHDR =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                 type='IPv4') /
+                //           scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                 tos=0, id=27149, flags='DF') /
+                //           scapy.UDP(sport=9876, dport=433))
+                // 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) 0x08, (byte) 0x00,
+                // IP header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x1c,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x11, (byte) 0xe4, (byte) 0x8d,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x08, (byte) 0xeb, (byte) 0x62
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV4HDR_UDPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                 type='IPv4') /
+                //           scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                 tos=0, id=27149, flags='DF') /
+                //           scapy.UDP(sport=9876, dport=433) /
+                //           b'\xde\xad\xbe\xef')
+                // 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) 0x08, (byte) 0x00,
+                // IP header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x20,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x11, (byte) 0xe4, (byte) 0x89,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x0c, (byte) 0x4d, (byte) 0xbd,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_IPV4HDR_UDPHDR =
+            new byte[] {
+                // packet = (scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                 tos=0, id=27149, flags='DF') /
+                //           scapy.UDP(sport=9876, dport=433))
+                // IP header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x1c,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x11, (byte) 0xe4, (byte) 0x8d,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x08, (byte) 0xeb, (byte) 0x62
+            };
+
+    private static final byte[] TEST_PACKET_IPV4HDR_UDPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                 tos=0, id=27149, flags='DF') /
+                //           scapy.UDP(sport=9876, dport=433) /
+                //           b'\xde\xad\xbe\xef')
+                // IP header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x20,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x11, (byte) 0xe4, (byte) 0x89,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x0c, (byte) 0x4d, (byte) 0xbd,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                     type='IPv6') /
+                //           scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433))
+                // 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,
+                // IP header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x08, (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) 0x00, (byte) 0x08, (byte) 0x7c, (byte) 0x24
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                       type='IPv6') /
+                //           scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433) /
+                //           b'\xde\xad\xbe\xef')
+                // 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,
+                // IP header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x0c, (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) 0x00, (byte) 0x0c, (byte) 0xde, (byte) 0x7e,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_TCPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                       type='IPv6') /
+                //           scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0) /
+                //           b'\xde\xad\xbe\xef')
+                // 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) 0x18, (byte) 0x06, (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,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0xd9, (byte) 0x05, (byte) 0x00, (byte) 0x00,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_TCPHDR =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                       type='IPv6') /
+                //           scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0))
+                // 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) 0x14, (byte) 0x06, (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,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0x76, (byte) 0xa7, (byte) 0x00, (byte) 0x00
+            };
+
+    private static final byte[] TEST_PACKET_IPV6HDR_TCPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0) /
+                //           b'\xde\xad\xbe\xef')
+                // IPv6 header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x18, (byte) 0x06, (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,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0xd9, (byte) 0x05, (byte) 0x00, (byte) 0x00,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_IPV6HDR_TCPHDR =
+            new byte[] {
+                // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0))
+                // IPv6 header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x14, (byte) 0x06, (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,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0x76, (byte) 0xa7, (byte) 0x00, (byte) 0x00
+            };
+
+    private static final byte[] TEST_PACKET_IPV6HDR_UDPHDR =
+            new byte[] {
+                // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433))
+                // IP header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x08, (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) 0x00, (byte) 0x08, (byte) 0x7c, (byte) 0x24
+            };
+
+    private static final byte[] TEST_PACKET_IPV6HDR_UDPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433) /
+                //           b'\xde\xad\xbe\xef')
+                // IP header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x0c, (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) 0x00, (byte) 0x0c, (byte) 0xde, (byte) 0x7e,
+                // Data
+                (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. The packet will be fragmented into
+     * multiple smaller packets if the packet size exceeds L2 mtu.
+     *
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |                Layer 2 header (EthernetHeader)                | (optional)
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |           Layer 3 header (Ipv4Header, Ipv6Header)             |
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |           Layer 4 header (TcpHeader, UdpHeader)               |
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |                          Payload                              | (optional)
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     *
+     * @param srcMac source MAC address. used by L2 ether header.
+     * @param dstMac destination MAC address. used by L2 ether header.
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} and {@code IPPROTO_IPV6}
+     *        currently supported.
+     * @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 List<ByteBuffer> buildPackets(@Nullable final MacAddress srcMac,
+            @Nullable final MacAddress dstMac, final int l3proto, final int l4proto,
+            @Nullable final ByteBuffer payload, int l2mtu)
+            throws Exception {
+        if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
+            fail("Unsupported layer 3 protocol " + l3proto);
+        }
+
+        if (l4proto != IPPROTO_TCP && l4proto != IPPROTO_UDP) {
+            fail("Unsupported layer 4 protocol " + l4proto);
+        }
+
+        final boolean hasEther = (srcMac != null && dstMac != null);
+        final int payloadLen = (payload == null) ? 0 : payload.limit();
+        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, l3proto, l4proto,
+                payloadLen);
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+
+        // [1] Build ether header.
+        if (hasEther) {
+            final int etherType = (l3proto == IPPROTO_IP) ? ETHER_TYPE_IPV4 : ETHER_TYPE_IPV6;
+            packetBuilder.writeL2Header(srcMac, dstMac, (short) etherType);
+        }
+
+        // [2] Build IP header.
+        if (l3proto == IPPROTO_IP) {
+            packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                    TIME_TO_LIVE, (byte) l4proto, IPV4_SRC_ADDR, IPV4_DST_ADDR);
+        } else if (l3proto == IPPROTO_IPV6) {
+            packetBuilder.writeIpv6Header(VERSION_TRAFFICCLASS_FLOWLABEL,
+                    (byte) l4proto, HOP_LIMIT, IPV6_SRC_ADDR, IPV6_DST_ADDR);
+        }
+
+        // [3] Build TCP or UDP header.
+        if (l4proto == IPPROTO_TCP) {
+            packetBuilder.writeTcpHeader(SRC_PORT, DST_PORT, SEQ_NO, ACK_NO,
+                    TCPHDR_ACK, WINDOW, URGENT_POINTER);
+        } else if (l4proto == IPPROTO_UDP) {
+            packetBuilder.writeUdpHeader(SRC_PORT, DST_PORT);
+        }
+
+        // [4] Build payload.
+        if (payload != null) {
+            buffer.put(payload);
+            // in case data might be reused by caller, restore the position and
+            // limit of bytebuffer.
+            payload.clear();
+        }
+
+        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);
+    }
+
+    /**
+     * Check ethernet header.
+     *
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} and {@code IPPROTO_IPV6}
+     *        currently supported.
+     * @param actual the packet to check.
+     */
+    private void checkEtherHeader(final int l3proto, final ByteBuffer actual) {
+        if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
+            fail("Unsupported layer 3 protocol " + l3proto);
+        }
+
+        final EthernetHeader eth = Struct.parse(EthernetHeader.class, actual);
+        assertEquals(SRC_MAC, eth.srcMac);
+        assertEquals(DST_MAC, eth.dstMac);
+        final int expectedEtherType = (l3proto == IPPROTO_IP) ? ETHER_TYPE_IPV4 : ETHER_TYPE_IPV6;
+        assertEquals(expectedEtherType, eth.etherType);
+    }
+
+    /**
+     * Check IPv4 header.
+     *
+     * @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} and {@code IPPROTO_UDP}
+     *        currently supported.
+     * @param hasData true if the packet has data payload; false otherwise.
+     * @param actual the packet to check.
+     */
+    private void checkIpv4Header(final int l4proto, final boolean hasData,
+            final ByteBuffer actual) {
+        if (l4proto != IPPROTO_TCP && l4proto != IPPROTO_UDP) {
+            fail("Unsupported layer 4 protocol " + l4proto);
+        }
+
+        final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, actual);
+        assertEquals(Ipv4Header.IPHDR_VERSION_IHL, ipv4Header.vi);
+        assertEquals(TYPE_OF_SERVICE, ipv4Header.tos);
+        assertEquals(ID, ipv4Header.id);
+        assertEquals(FLAGS_AND_FRAGMENT_OFFSET, ipv4Header.flagsAndFragmentOffset);
+        assertEquals(TIME_TO_LIVE, ipv4Header.ttl);
+        assertEquals(IPV4_SRC_ADDR, ipv4Header.srcIp);
+        assertEquals(IPV4_DST_ADDR, ipv4Header.dstIp);
+
+        final int dataLength = hasData ? DATA.limit() : 0;
+        if (l4proto == IPPROTO_TCP) {
+            assertEquals(IPV4_HEADER_MIN_LEN + TCP_HEADER_MIN_LEN + dataLength,
+                    ipv4Header.totalLength);
+            assertEquals((byte) IPPROTO_TCP, ipv4Header.protocol);
+            assertEquals(hasData ? (short) 0xe488 : (short) 0xe48c, ipv4Header.checksum);
+        } else if (l4proto == IPPROTO_UDP) {
+            assertEquals(IPV4_HEADER_MIN_LEN + UDP_HEADER_LEN + dataLength,
+                    ipv4Header.totalLength);
+            assertEquals((byte) IPPROTO_UDP, ipv4Header.protocol);
+            assertEquals(hasData ? (short) 0xe489 : (short) 0xe48d, ipv4Header.checksum);
+        }
+    }
+
+    /**
+     * Check IPv6 header.
+     *
+     * @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} and {@code IPPROTO_UDP}
+     *        currently supported.
+     * @param hasData true if the packet has data payload; false otherwise.
+     * @param actual the packet to check.
+     */
+    private void checkIpv6Header(final int l4proto, final boolean hasData,
+            final ByteBuffer actual) {
+        if (l4proto != IPPROTO_TCP && l4proto != IPPROTO_UDP) {
+            fail("Unsupported layer 4 protocol " + l4proto);
+        }
+
+        final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, actual);
+
+        assertEquals(VERSION_TRAFFICCLASS_FLOWLABEL, ipv6Header.vtf);
+        assertEquals(HOP_LIMIT, ipv6Header.hopLimit);
+        assertEquals(IPV6_SRC_ADDR, ipv6Header.srcIp);
+        assertEquals(IPV6_DST_ADDR, ipv6Header.dstIp);
+
+        final int dataLength = hasData ? DATA.limit() : 0;
+        if (l4proto == IPPROTO_TCP) {
+            assertEquals(TCP_HEADER_MIN_LEN + dataLength, ipv6Header.payloadLength);
+            assertEquals((byte) IPPROTO_TCP, ipv6Header.nextHeader);
+        } else if (l4proto == IPPROTO_UDP) {
+            assertEquals(UDP_HEADER_LEN + dataLength, ipv6Header.payloadLength);
+            assertEquals((byte) IPPROTO_UDP, ipv6Header.nextHeader);
+        }
+    }
+
+    /**
+     * Check TCP packet.
+     *
+     * @param hasEther true if the packet has ether header; false otherwise.
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} and {@code IPPROTO_IPV6}
+     *        currently supported.
+     * @param hasData true if the packet has data payload; false otherwise.
+     * @param actual the packet to check.
+     */
+    private void checkTcpPacket(final boolean hasEther, final int l3proto, final boolean hasData,
+            final ByteBuffer actual) {
+        if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
+            fail("Unsupported layer 3 protocol " + l3proto);
+        }
+
+        // [1] Check ether header.
+        if (hasEther) {
+            checkEtherHeader(l3proto, actual);
+        }
+
+        // [2] Check IP header.
+        if (l3proto == IPPROTO_IP) {
+            checkIpv4Header(IPPROTO_TCP, hasData, actual);
+        } else if (l3proto == IPPROTO_IPV6) {
+            checkIpv6Header(IPPROTO_TCP, hasData, actual);
+        }
+
+        // [3] Check TCP header.
+        final TcpHeader tcpHeader = Struct.parse(TcpHeader.class, actual);
+        assertEquals(SRC_PORT, tcpHeader.srcPort);
+        assertEquals(DST_PORT, tcpHeader.dstPort);
+        assertEquals(SEQ_NO, tcpHeader.seq);
+        assertEquals(ACK_NO, tcpHeader.ack);
+        assertEquals((short) 0x5010 /* offset=5(*4bytes), control bits=ACK */,
+                tcpHeader.dataOffsetAndControlBits);
+        assertEquals(WINDOW, tcpHeader.window);
+        assertEquals(URGENT_POINTER, tcpHeader.urgentPointer);
+        if (l3proto == IPPROTO_IP) {
+            assertEquals(hasData ? (short) 0x4844 : (short) 0xe5e5, tcpHeader.checksum);
+        } else if (l3proto == IPPROTO_IPV6) {
+            assertEquals(hasData ? (short) 0xd905 : (short) 0x76a7, tcpHeader.checksum);
+        }
+
+        // [4] Check payload.
+        if (hasData) {
+            assertEquals(0xdeadbeef, actual.getInt());
+        }
+    }
+
+    /**
+     * Check UDP packet.
+     *
+     * @param hasEther true if the packet has ether header; false otherwise.
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} and {@code IPPROTO_IPV6}
+     *        currently supported.
+     * @param hasData true if the packet has data payload; false otherwise.
+     * @param actual the packet to check.
+     */
+    private void checkUdpPacket(final boolean hasEther, final int l3proto, final boolean hasData,
+            final ByteBuffer actual) {
+        if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
+            fail("Unsupported layer 3 protocol " + l3proto);
+        }
+
+        // [1] Check ether header.
+        if (hasEther) {
+            checkEtherHeader(l3proto, actual);
+        }
+
+        // [2] Check IP header.
+        if (l3proto == IPPROTO_IP) {
+            checkIpv4Header(IPPROTO_UDP, hasData, actual);
+        } else if (l3proto == IPPROTO_IPV6) {
+            checkIpv6Header(IPPROTO_UDP, hasData, actual);
+        }
+
+        // [3] Check UDP header.
+        final UdpHeader udpHeader = Struct.parse(UdpHeader.class, actual);
+        assertEquals(SRC_PORT, udpHeader.srcPort);
+        assertEquals(DST_PORT, udpHeader.dstPort);
+        final int dataLength = hasData ? DATA.limit() : 0;
+        assertEquals(UDP_HEADER_LEN + dataLength, udpHeader.length);
+        if (l3proto == IPPROTO_IP) {
+            assertEquals(hasData ? (short) 0x4dbd : (short) 0xeb62, udpHeader.checksum);
+        } else if (l3proto == IPPROTO_IPV6) {
+            assertEquals(hasData ? (short) 0xde7e : (short) 0x7c24, udpHeader.checksum);
+        }
+
+        // [4] Check payload.
+        if (hasData) {
+            assertEquals(0xdeadbeef, actual.getInt());
+        }
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv4Tcp() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IP, IPPROTO_TCP,
+                null /* data */);
+        checkTcpPacket(true /* hasEther */, IPPROTO_IP, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv4TcpData() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IP, IPPROTO_TCP, DATA);
+        checkTcpPacket(true /* hasEther */, IPPROTO_IP, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR_DATA,
+                packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv4Tcp() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IP, IPPROTO_TCP, null /* data */);
+        checkTcpPacket(false /* hasEther */, IPPROTO_IP, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV4HDR_TCPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv4TcpData() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IP, IPPROTO_TCP, DATA);
+        checkTcpPacket(false /* hasEther */, IPPROTO_IP, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV4HDR_TCPHDR_DATA, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv4Udp() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IP, IPPROTO_UDP,
+                null /* data */);
+        checkUdpPacket(true /* hasEther */, IPPROTO_IP, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_UDPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv4UdpData() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IP, IPPROTO_UDP, DATA);
+        checkUdpPacket(true /* hasEther */, IPPROTO_IP, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_UDPHDR_DATA, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv4Udp() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IP, IPPROTO_UDP, null /*data*/);
+        checkUdpPacket(false /* hasEther */, IPPROTO_IP, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV4HDR_UDPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv4UdpData() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IP, IPPROTO_UDP, DATA);
+        checkUdpPacket(false /* hasEther */, IPPROTO_IP, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV4HDR_UDPHDR_DATA, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv6TcpData() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_TCP, DATA);
+        checkTcpPacket(true /* hasEther */, IPPROTO_IPV6, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV6HDR_TCPHDR_DATA,
+                packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv6Tcp() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_TCP,
+                null /*data*/);
+        checkTcpPacket(true /* hasEther */, IPPROTO_IPV6, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV6HDR_TCPHDR,
+                packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv6TcpData() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */, IPPROTO_IPV6,
+                IPPROTO_TCP, DATA);
+        checkTcpPacket(false /* hasEther */, IPPROTO_IPV6, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV6HDR_TCPHDR_DATA, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv6Tcp() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */, IPPROTO_IPV6,
+                IPPROTO_TCP, null /*data*/);
+        checkTcpPacket(false /* hasEther */, IPPROTO_IPV6, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV6HDR_TCPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv6Udp() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_UDP,
+                null /* data */);
+        checkUdpPacket(true /* hasEther */, IPPROTO_IPV6, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv6UdpData() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_UDP,
+                DATA);
+        checkUdpPacket(true /* hasEther */, IPPROTO_IPV6, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv6Udp() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IPV6, IPPROTO_UDP, null /*data*/);
+        checkUdpPacket(false /* hasEther */, IPPROTO_IPV6, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV6HDR_UDPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv6UdpData() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IPV6, IPPROTO_UDP, DATA);
+        checkUdpPacket(false /* hasEther */, IPPROTO_IPV6, true /* hasData */, packet);
+        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,
+                IPPROTO_TCP, 0 /* payloadLen */);
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+        packetBuilder.writeTcpHeader(SRC_PORT, DST_PORT, SEQ_NO, ACK_NO,
+                TCPHDR_ACK, WINDOW, URGENT_POINTER);
+        assertThrows("java.io.IOException: Packet is missing IPv4 header", IOException.class,
+                () -> packetBuilder.finalizePacket());
+    }
+
+    @Test
+    public void testFinalizePacketWithoutL4Header() throws Exception {
+        final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */, IPPROTO_IP,
+                IPPROTO_TCP, 0 /* payloadLen */);
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+        packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                TIME_TO_LIVE, (byte) IPPROTO_TCP, IPV4_SRC_ADDR, IPV4_DST_ADDR);
+        assertThrows("java.io.IOException: Packet is missing neither TCP nor UDP header",
+                IOException.class, () -> packetBuilder.finalizePacket());
+    }
+
+    @Test
+    public void testWriteL2HeaderToInsufficientBuffer() throws Exception {
+        final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1));
+        assertThrows(IOException.class,
+                () -> packetBuilder.writeL2Header(SRC_MAC, DST_MAC, (short) ETHER_TYPE_IPV4));
+    }
+
+    @Test
+    public void testWriteIpv4HeaderToInsufficientBuffer() throws Exception {
+        final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1));
+        assertThrows(IOException.class,
+                () -> packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                        TIME_TO_LIVE, (byte) IPPROTO_TCP, IPV4_SRC_ADDR, IPV4_DST_ADDR));
+    }
+
+    @Test
+    public void testWriteTcpHeaderToInsufficientBuffer() throws Exception {
+        final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1));
+        assertThrows(IOException.class,
+                () -> packetBuilder.writeTcpHeader(SRC_PORT, DST_PORT, SEQ_NO, ACK_NO,
+                        TCPHDR_ACK, WINDOW, URGENT_POINTER));
+    }
+
+    @Test
+    public void testWriteUdpHeaderToInsufficientBuffer() throws Exception {
+        final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1));
+        assertThrows(IOException.class, () -> packetBuilder.writeUdpHeader(SRC_PORT, DST_PORT));
+    }
+
+    private static Inet4Address addr4(String addr) {
+        return (Inet4Address) InetAddresses.parseNumericAddress(addr);
+    }
+
+    private static Inet6Address addr6(String addr) {
+        return (Inet6Address) InetAddresses.parseNumericAddress(addr);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PacketReaderTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/PacketReaderTest.java
new file mode 100644
index 0000000..459801c
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PacketReaderTest.java
@@ -0,0 +1,245 @@
+/*
+ * 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.net.module.util;
+
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_SNDTIMEO;
+
+import static com.android.net.module.util.PacketReader.DEFAULT_RECV_BUF_SIZE;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileDescriptor;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for PacketReader.
+ *
+ * @hide
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PacketReaderTest {
+    static final InetAddress LOOPBACK6 = Inet6Address.getLoopbackAddress();
+    static final StructTimeval TIMEO = StructTimeval.fromMillis(500);
+
+    // TODO : reassigning the latch voids its synchronization properties, which means this
+    // scheme just doesn't work. Latches almost always need to be final to work.
+    protected CountDownLatch mLatch;
+    protected FileDescriptor mLocalSocket;
+    protected InetSocketAddress mLocalSockName;
+    protected byte[] mLastRecvBuf;
+    protected boolean mStopped;
+    protected HandlerThread mHandlerThread;
+    protected PacketReader mReceiver;
+
+    class UdpLoopbackReader extends PacketReader {
+        UdpLoopbackReader(Handler h) {
+            super(h);
+        }
+
+        @Override
+        protected FileDescriptor createFd() {
+            FileDescriptor s = null;
+            try {
+                s = Os.socket(AF_INET6, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP);
+                Os.bind(s, LOOPBACK6, 0);
+                mLocalSockName = (InetSocketAddress) Os.getsockname(s);
+                Os.setsockoptTimeval(s, SOL_SOCKET, SO_SNDTIMEO, TIMEO);
+            } catch (ErrnoException | SocketException e) {
+                closeFd(s);
+                throw new RuntimeException("Failed to create FD", e);
+            }
+
+            mLocalSocket = s;
+            return s;
+        }
+
+        @Override
+        protected void handlePacket(byte[] recvbuf, int length) {
+            mLastRecvBuf = Arrays.copyOf(recvbuf, length);
+            mLatch.countDown();
+        }
+
+        @Override
+        protected void onStart() {
+            mStopped = false;
+            mLatch.countDown();
+        }
+
+        @Override
+        protected void onStop() {
+            mStopped = true;
+            mLatch.countDown();
+        }
+    };
+
+    @Before
+    public void setUp() {
+        resetLatch();
+        mLocalSocket = null;
+        mLocalSockName = null;
+        mLastRecvBuf = null;
+        mStopped = false;
+
+        mHandlerThread = new HandlerThread(PacketReaderTest.class.getSimpleName());
+        mHandlerThread.start();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mReceiver != null) {
+            mHandlerThread.getThreadHandler().post(() -> mReceiver.stop());
+            waitForActivity();
+        }
+        mReceiver = null;
+        mHandlerThread.quit();
+        mHandlerThread = null;
+    }
+
+    void resetLatch() {
+        mLatch = new CountDownLatch(1);
+    }
+
+    void waitForActivity() throws Exception {
+        try {
+            mLatch.await(1000, TimeUnit.MILLISECONDS);
+        } finally {
+            resetLatch();
+        }
+    }
+
+    void sendPacket(byte[] contents) throws Exception {
+        final DatagramSocket sender = new DatagramSocket();
+        sender.connect(mLocalSockName);
+        sender.send(new DatagramPacket(contents, contents.length));
+        sender.close();
+    }
+
+    @Test
+    public void testBasicWorking() throws Exception {
+        final Handler h = mHandlerThread.getThreadHandler();
+        mReceiver = new UdpLoopbackReader(h);
+
+        h.post(() -> mReceiver.start());
+        waitForActivity();
+        assertTrue(mLocalSockName != null);
+        assertEquals(LOOPBACK6, mLocalSockName.getAddress());
+        assertTrue(0 < mLocalSockName.getPort());
+        assertTrue(mLocalSocket != null);
+        assertFalse(mStopped);
+
+        final byte[] one = "one 1".getBytes("UTF-8");
+        sendPacket(one);
+        waitForActivity();
+        assertEquals(1, mReceiver.numPacketsReceived());
+        assertTrue(Arrays.equals(one, mLastRecvBuf));
+        assertFalse(mStopped);
+
+        final byte[] two = "two 2".getBytes("UTF-8");
+        sendPacket(two);
+        waitForActivity();
+        assertEquals(2, mReceiver.numPacketsReceived());
+        assertTrue(Arrays.equals(two, mLastRecvBuf));
+        assertFalse(mStopped);
+
+        h.post(() -> mReceiver.stop());
+        waitForActivity();
+        assertEquals(2, mReceiver.numPacketsReceived());
+        assertTrue(Arrays.equals(two, mLastRecvBuf));
+        assertTrue(mStopped);
+        mReceiver = null;
+    }
+
+    class NullPacketReader extends PacketReader {
+        NullPacketReader(Handler h, int recvbufsize) {
+            super(h, recvbufsize);
+        }
+
+        @Override
+        public FileDescriptor createFd() {
+            return null;
+        }
+    }
+
+    @Test
+    public void testMinimalRecvBufSize() throws Exception {
+        final Handler h = mHandlerThread.getThreadHandler();
+
+        for (int i : new int[] { -1, 0, 1, DEFAULT_RECV_BUF_SIZE - 1 }) {
+            final PacketReader b = new NullPacketReader(h, i);
+            assertEquals(DEFAULT_RECV_BUF_SIZE, b.recvBufSize());
+        }
+    }
+
+    @Test
+    public void testStartingFromWrongThread() throws Exception {
+        final Handler h = mHandlerThread.getThreadHandler();
+        final PacketReader b = new NullPacketReader(h, DEFAULT_RECV_BUF_SIZE);
+        assertThrows(IllegalStateException.class, () -> b.start());
+    }
+
+    @Test
+    public void testStoppingFromWrongThread() throws Exception {
+        final Handler h = mHandlerThread.getThreadHandler();
+        final PacketReader b = new NullPacketReader(h, DEFAULT_RECV_BUF_SIZE);
+        assertThrows(IllegalStateException.class, () -> b.stop());
+    }
+
+    @Test
+    public void testSuccessToCreateSocket() throws Exception {
+        final Handler h = mHandlerThread.getThreadHandler();
+        final PacketReader b = new UdpLoopbackReader(h);
+        h.post(() -> assertTrue(b.start()));
+    }
+
+    @Test
+    public void testFailToCreateSocket() throws Exception {
+        final Handler h = mHandlerThread.getThreadHandler();
+        final PacketReader b = new NullPacketReader(h, DEFAULT_RECV_BUF_SIZE);
+        h.post(() -> assertFalse(b.start()));
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PerUidCounterTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/PerUidCounterTest.kt
new file mode 100644
index 0000000..321fe59
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PerUidCounterTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.net.module.util
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class PerUidCounterTest {
+    private val UID_A = 1000
+    private val UID_B = 1001
+    private val UID_C = 1002
+
+    @Test
+    fun testCounterMaximum() {
+        assertFailsWith<IllegalArgumentException> {
+            PerUidCounter(-1)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            PerUidCounter(0)
+        }
+
+        val testLimit = 1000
+        val testCounter = PerUidCounter(testLimit)
+        assertEquals(0, testCounter[UID_A])
+        repeat(testLimit) {
+            testCounter.incrementCountOrThrow(UID_A)
+        }
+        assertEquals(testLimit, testCounter[UID_A])
+        assertFailsWith<IllegalStateException> {
+            testCounter.incrementCountOrThrow(UID_A)
+        }
+        assertEquals(testLimit, testCounter[UID_A])
+    }
+
+    @Test
+    fun testIncrementCountOrThrow() {
+        val counter = PerUidCounter(3)
+
+        // Verify the counters work independently.
+        counter.incrementCountOrThrow(UID_A)
+        counter.incrementCountOrThrow(UID_B)
+        counter.incrementCountOrThrow(UID_B)
+        counter.incrementCountOrThrow(UID_A)
+        counter.incrementCountOrThrow(UID_A)
+        assertEquals(3, counter[UID_A])
+        assertEquals(2, counter[UID_B])
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_A)
+        }
+        counter.incrementCountOrThrow(UID_B)
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_B)
+        }
+
+        // Verify exception can be triggered again.
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_A)
+        }
+        assertFailsWith<IllegalStateException> {
+            repeat(3) {
+                counter.incrementCountOrThrow(UID_A)
+            }
+        }
+        assertEquals(3, counter[UID_A])
+        assertEquals(3, counter[UID_B])
+        assertEquals(0, counter[UID_C])
+    }
+
+    @Test
+    fun testDecrementCountOrThrow() {
+        val counter = PerUidCounter(3)
+
+        // Verify the count cannot go below zero.
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A)
+        }
+        assertFailsWith<IllegalStateException> {
+            repeat(5) {
+                counter.decrementCountOrThrow(UID_A)
+            }
+        }
+
+        // Verify the counters work independently.
+        counter.incrementCountOrThrow(UID_A)
+        counter.incrementCountOrThrow(UID_B)
+        assertEquals(1, counter[UID_A])
+        assertEquals(1, counter[UID_B])
+        assertFailsWith<IllegalStateException> {
+            repeat(3) {
+                counter.decrementCountOrThrow(UID_A)
+            }
+        }
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A)
+        }
+        assertEquals(0, counter[UID_A])
+        assertEquals(1, counter[UID_B])
+
+        // Verify mixing increment and decrement.
+        val largeCounter = PerUidCounter(100)
+        repeat(90) {
+            largeCounter.incrementCountOrThrow(UID_A)
+        }
+        repeat(70) {
+            largeCounter.decrementCountOrThrow(UID_A)
+        }
+        repeat(80) {
+            largeCounter.incrementCountOrThrow(UID_A)
+        }
+        assertFailsWith<IllegalStateException> {
+            largeCounter.incrementCountOrThrow(UID_A)
+        }
+        assertEquals(100, largeCounter[UID_A])
+        repeat(100) {
+            largeCounter.decrementCountOrThrow(UID_A)
+        }
+        assertFailsWith<IllegalStateException> {
+            largeCounter.decrementCountOrThrow(UID_A)
+        }
+        assertEquals(0, largeCounter[UID_A])
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
new file mode 100644
index 0000000..8586e82
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.net.module.util
+
+import android.Manifest.permission.NETWORK_STACK
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.pm.PackageManager.PERMISSION_DENIED
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf
+import com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission
+import com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr
+import com.android.net.module.util.PermissionUtils.enforcePackageNameMatchesUid
+import com.android.net.module.util.PermissionUtils.enforceSystemFeature
+import com.android.net.module.util.PermissionUtils.hasAnyPermissionOf
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.mock
+
+/** Tests for PermissionUtils */
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+class PermissionUtilsTest {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+    private val TEST_PERMISSION1 = "android.permission.TEST_PERMISSION1"
+    private val TEST_PERMISSION2 = "android.permission.TEST_PERMISSION2"
+    private val TEST_UID1 = 1234
+    private val TEST_UID2 = 1235
+    private val TEST_PACKAGE_NAME = "test.package"
+    private val mockContext = mock(Context::class.java)
+    private val mockPackageManager = mock(PackageManager::class.java)
+
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+
+    @Before
+    fun setup() {
+        doReturn(mockPackageManager).`when`(mockContext).packageManager
+        doReturn(mockContext).`when`(mockContext).createContextAsUser(any(), anyInt())
+    }
+
+    @Test
+    fun testEnforceAnyPermissionOf() {
+        doReturn(PERMISSION_GRANTED).`when`(mockContext)
+            .checkCallingOrSelfPermission(TEST_PERMISSION1)
+        doReturn(PERMISSION_DENIED).`when`(mockContext)
+            .checkCallingOrSelfPermission(TEST_PERMISSION2)
+        assertTrue(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+        enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
+
+        doReturn(PERMISSION_DENIED).`when`(mockContext)
+            .checkCallingOrSelfPermission(TEST_PERMISSION1)
+        doReturn(PERMISSION_GRANTED).`when`(mockContext)
+            .checkCallingOrSelfPermission(TEST_PERMISSION2)
+        assertTrue(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+        enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
+
+        doReturn(PERMISSION_DENIED).`when`(mockContext).checkCallingOrSelfPermission(any())
+        assertFalse(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+        assertFailsWith<SecurityException>("Expect fail but permission granted.") {
+            enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
+        }
+    }
+
+    @Test
+    fun testEnforceNetworkStackPermissionOr() {
+        doReturn(PERMISSION_GRANTED).`when`(mockContext).checkCallingOrSelfPermission(NETWORK_STACK)
+        doReturn(PERMISSION_DENIED).`when`(mockContext)
+            .checkCallingOrSelfPermission(PERMISSION_MAINLINE_NETWORK_STACK)
+        enforceNetworkStackPermission(mockContext)
+        enforceNetworkStackPermissionOr(mockContext, TEST_PERMISSION1)
+
+        doReturn(PERMISSION_DENIED).`when`(mockContext).checkCallingOrSelfPermission(NETWORK_STACK)
+        doReturn(PERMISSION_GRANTED).`when`(mockContext)
+            .checkCallingOrSelfPermission(PERMISSION_MAINLINE_NETWORK_STACK)
+        enforceNetworkStackPermission(mockContext)
+        enforceNetworkStackPermissionOr(mockContext, TEST_PERMISSION2)
+
+        doReturn(PERMISSION_DENIED).`when`(mockContext).checkCallingOrSelfPermission(NETWORK_STACK)
+        doReturn(PERMISSION_DENIED).`when`(mockContext)
+            .checkCallingOrSelfPermission(PERMISSION_MAINLINE_NETWORK_STACK)
+        doReturn(PERMISSION_GRANTED).`when`(mockContext)
+            .checkCallingOrSelfPermission(TEST_PERMISSION1)
+        assertFailsWith<SecurityException>("Expect fail but permission granted.") {
+            enforceNetworkStackPermission(mockContext)
+        }
+        enforceNetworkStackPermissionOr(mockContext, TEST_PERMISSION1)
+
+        doReturn(PERMISSION_DENIED).`when`(mockContext).checkCallingOrSelfPermission(any())
+        assertFailsWith<SecurityException>("Expect fail but permission granted.") {
+            enforceNetworkStackPermission(mockContext)
+        }
+        assertFailsWith<SecurityException>("Expect fail but permission granted.") {
+            enforceNetworkStackPermissionOr(mockContext, TEST_PERMISSION2)
+        }
+    }
+
+    private fun mockHasSystemFeature(featureName: String, hasFeature: Boolean) {
+        doReturn(hasFeature).`when`(mockPackageManager)
+            .hasSystemFeature(ArgumentMatchers.eq(featureName))
+    }
+
+    @Test
+    fun testEnforceSystemFeature() {
+        val systemFeature = "test.system.feature"
+        val exceptionMessage = "test exception message"
+        mockHasSystemFeature(featureName = systemFeature, hasFeature = false)
+        val e = assertFailsWith<UnsupportedOperationException>("Should fail without feature") {
+            enforceSystemFeature(mockContext, systemFeature, exceptionMessage)
+        }
+        assertEquals(exceptionMessage, e.message)
+
+        mockHasSystemFeature(featureName = systemFeature, hasFeature = true)
+        try {
+            enforceSystemFeature(mockContext, systemFeature, "")
+        } catch (e: UnsupportedOperationException) {
+            Assert.fail("Exception should have not been thrown with system feature enabled")
+        }
+    }
+
+    @Test
+    fun testEnforcePackageNameMatchesUid() {
+        // Verify name not found throws.
+        doThrow(NameNotFoundException()).`when`(mockPackageManager)
+            .getPackageUid(eq(TEST_PACKAGE_NAME), anyInt())
+        assertFailsWith<SecurityException> {
+            enforcePackageNameMatchesUid(mockContext, TEST_UID1, TEST_PACKAGE_NAME)
+        }
+
+        // Verify uid mismatch throws.
+        doReturn(TEST_UID1).`when`(mockPackageManager)
+            .getPackageUid(eq(TEST_PACKAGE_NAME), anyInt())
+        assertFailsWith<SecurityException> {
+            enforcePackageNameMatchesUid(mockContext, TEST_UID2, TEST_PACKAGE_NAME)
+        }
+
+        // Verify uid match passes.
+        enforcePackageNameMatchesUid(mockContext, TEST_UID1, TEST_PACKAGE_NAME)
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
new file mode 100644
index 0000000..b04561c
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.net.module.util
+
+import android.net.INetd
+import android.os.Build
+import android.util.Log
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class RoutingCoordinatorServiceTest {
+    val mNetd = mock(INetd::class.java)
+    val mService = RoutingCoordinatorService(mNetd)
+
+    @Test
+    fun testInterfaceForward() {
+        val inOrder = inOrder(mNetd)
+
+        mService.addInterfaceForward("from1", "to1")
+        inOrder.verify(mNetd).ipfwdEnableForwarding(any())
+        inOrder.verify(mNetd).tetherAddForward("from1", "to1")
+        inOrder.verify(mNetd).ipfwdAddInterfaceForward("from1", "to1")
+
+        mService.addInterfaceForward("from2", "to1")
+        inOrder.verify(mNetd).tetherAddForward("from2", "to1")
+        inOrder.verify(mNetd).ipfwdAddInterfaceForward("from2", "to1")
+
+        val hasFailed = AtomicBoolean(false)
+        val prevHandler = Log.setWtfHandler { tag, what, system ->
+            hasFailed.set(true)
+        }
+        tryTest {
+            mService.addInterfaceForward("from2", "to1")
+            assertTrue(hasFailed.get())
+        } cleanup {
+            Log.setWtfHandler(prevHandler)
+        }
+
+        mService.removeInterfaceForward("from1", "to1")
+        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward("from1", "to1")
+        inOrder.verify(mNetd).tetherRemoveForward("from1", "to1")
+
+        mService.removeInterfaceForward("from2", "to1")
+        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward("from2", "to1")
+        inOrder.verify(mNetd).tetherRemoveForward("from2", "to1")
+
+        inOrder.verify(mNetd).ipfwdDisableForwarding(any())
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
new file mode 100644
index 0000000..aa1bfee
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SharedLogTest {
+    private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}";
+    private static final String TIMESTAMP = "HH:MM:SS";
+    private static final String TAG = "top";
+
+    @Test
+    public void testBasicOperation() {
+        final SharedLog logTop = new SharedLog(TAG);
+        assertTrue(TAG.equals(logTop.getTag()));
+
+        logTop.mark("first post!");
+
+        final SharedLog logLevel2a = logTop.forSubComponent("twoA");
+        final SharedLog logLevel2b = logTop.forSubComponent("twoB");
+        logLevel2b.e("2b or not 2b");
+        logLevel2b.e("No exception", null);
+        logLevel2b.e("Wait, here's one", new Exception("Test"));
+        logLevel2a.w("second post?");
+
+        final SharedLog logLevel3 = logLevel2a.forSubComponent("three");
+        logTop.log("still logging");
+        logLevel2b.e(new Exception("Got another exception"));
+        logLevel3.i("3 >> 2");
+        logLevel2a.mark("ok: last post");
+        logTop.logf("finished!");
+
+        final String[] expected = {
+            " - MARK first post!",
+            " - [twoB] ERROR 2b or not 2b",
+            " - [twoB] ERROR No exception",
+            // No stacktrace in shared log, only in logcat
+            " - [twoB] ERROR Wait, here's one: Test",
+            " - [twoA] WARN second post?",
+            " - still logging",
+            " - [twoB] ERROR java.lang.Exception: Got another exception",
+            " - [twoA.three] 3 >> 2",
+            " - [twoA] MARK ok: last post",
+            " - finished!",
+        };
+        // Verify the logs are all there and in the correct order.
+        assertDumpLogs(expected, logTop);
+
+        // In fact, because they all share the same underlying LocalLog,
+        // every subcomponent SharedLog's dump() is identical.
+        assertDumpLogs(expected, logLevel2a);
+        assertDumpLogs(expected, logLevel2b);
+        assertDumpLogs(expected, logLevel3);
+    }
+
+    private static void assertDumpLogs(String[] expected, SharedLog log) {
+        verifyLogLines(expected, dump(log));
+        verifyLogLines(reverse(expected), reverseDump(log));
+    }
+
+    private static String dump(SharedLog log) {
+        return getSharedLogString(pw -> log.dump(null /* fd */, pw, null /* args */));
+    }
+
+    private static String reverseDump(SharedLog log) {
+        return getSharedLogString(pw -> log.reverseDump(pw));
+    }
+
+    private static String[] reverse(String[] ary) {
+        final List<String> ls = new ArrayList<>(Arrays.asList(ary));
+        Collections.reverse(ls);
+        return ls.toArray(new String[ary.length]);
+    }
+
+    private static String getSharedLogString(Consumer<PrintWriter> functor) {
+        final ByteArrayOutputStream ostream = new ByteArrayOutputStream();
+        final PrintWriter pw = new PrintWriter(ostream, true);
+        functor.accept(pw);
+
+        final String dumpOutput = ostream.toString();
+        assertNotNull(dumpOutput);
+        assertFalse("".equals(dumpOutput));
+        return dumpOutput;
+    }
+
+    private static void verifyLogLines(String[] expected, String gottenLogs) {
+        final String[] lines = gottenLogs.split("\n");
+        assertEquals(expected.length, lines.length);
+
+        for (int i = 0; i < expected.length; i++) {
+            String got = lines[i];
+            String want = expected[i];
+            assertTrue(String.format("'%s' did not contain '%s'", got, want), got.endsWith(want));
+            assertTrue(String.format("'%s' did not contain a %s timestamp", got, TIMESTAMP),
+                    got.replaceFirst(TIMESTAMP_PATTERN, TIMESTAMP).contains(TIMESTAMP));
+        }
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
new file mode 100644
index 0000000..a39b7a3
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
@@ -0,0 +1,1083 @@
+/*
+ * 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 static com.android.testutils.MiscAsserts.assertThrows;
+
+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.assertTrue;
+
+import android.annotation.SuppressLint;
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructTest {
+
+    // IPv6, 0 bytes of options, ifindex 15715755: 0x00efcdab, type 134 (RA), code 0, padding.
+    private static final String HDR_EMPTY = "0a00" + "0000" + "abcdef00" + "8600000000000000";
+
+    // UBE16: 0xfeff, UBE32: 0xfeffffff, UBE64: 0xfeffffffffffffff, UBE63: 0x7effffffffffffff
+    private static final String NETWORK_ORDER_MSG = "feff" + "feffffff" + "feffffffffffffff"
+            + "7effffffffffffff";
+
+    // S8: 0x7f, S16: 0x7fff, S32: 0x7fffffff, S64: 0x7fffffffffffffff
+    private static final String SIGNED_DATA = "7f" + "ff7f" + "ffffff7f" + "ffffffffffffff7f";
+
+    // nS8: 0x81, nS16: 0x8001, nS32: 0x80000001, nS64: 800000000000000001
+    private static final String SIGNED_NEGATIVE_DATA = "81" + "0180" + "01000080"
+            + "0100000000000080";
+
+    // U8: 0xff, U16: 0xffff, U32: 0xffffffff, U64: 0xffffffffffffffff, U63: 0x7fffffffffffffff,
+    // U63: 0xffffffffffffffff(-1L)
+    private static final String UNSIGNED_DATA = "ff" + "ffff" + "ffffffff" + "ffffffffffffffff"
+            + "ffffffffffffff7f" + "ffffffffffffffff";
+
+    // PREF64 option, 2001:db8:3:4:5:6::/96, lifetime: 10064
+    private static final String OPT_PREF64 = "2750" + "20010db80003000400050006";
+    private static final byte[] TEST_PREFIX64 = new byte[]{
+            (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, (byte) 0x00, (byte) 0x03,
+            (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x05, (byte) 0x00, (byte) 0x06,
+    };
+
+    private static final Inet4Address TEST_IPV4_ADDRESS =
+            (Inet4Address) InetAddresses.parseNumericAddress("192.168.100.1");
+    private static final Inet6Address TEST_IPV6_ADDRESS =
+            (Inet6Address) InetAddresses.parseNumericAddress("2001:db8:3:4:5:6:7:8");
+
+    private <T> T doParsingMessageTest(final String hexString, final Class<T> clazz,
+            final ByteOrder order) {
+        final ByteBuffer buf = toByteBuffer(hexString);
+        buf.order(order);
+        return Struct.parse(clazz, buf);
+    }
+
+    public static class HeaderMsgWithConstructor extends Struct {
+        static int sType;
+        static int sLength;
+
+        @Field(order = 0, type = Type.U8, padding = 1)
+        public final short mFamily;
+        @Field(order = 1, type = Type.U16)
+        public final int mLen;
+        @Field(order = 2, type = Type.S32)
+        public final int mIfindex;
+        @Field(order = 3, type = Type.U8)
+        public final short mIcmpType;
+        @Field(order = 4, type = Type.U8, padding = 6)
+        public final short mIcmpCode;
+
+        HeaderMsgWithConstructor(final short family, final int len, final int ifindex,
+                final short type, final short code) {
+            mFamily = family;
+            mLen = len;
+            mIfindex = ifindex;
+            mIcmpType = type;
+            mIcmpCode = code;
+        }
+    }
+
+    private void verifyHeaderParsing(final HeaderMsgWithConstructor msg) {
+        assertEquals(10, msg.mFamily);
+        assertEquals(0, msg.mLen);
+        assertEquals(15715755, msg.mIfindex);
+        assertEquals(134, msg.mIcmpType);
+        assertEquals(0, msg.mIcmpCode);
+
+        assertEquals(16, Struct.getSize(HeaderMsgWithConstructor.class));
+        assertArrayEquals(toByteBuffer(HDR_EMPTY).array(),
+                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
+    @Test
+    public void testClassWithExplicitConstructor() {
+        final HeaderMsgWithConstructor msg = doParsingMessageTest(HDR_EMPTY,
+                HeaderMsgWithConstructor.class, ByteOrder.LITTLE_ENDIAN);
+        verifyHeaderParsing(msg);
+    }
+
+    public static class HeaderMsgWithoutConstructor extends Struct {
+        static int sType;
+        static int sLength;
+
+        @Field(order = 0, type = Type.U8, padding = 1)
+        public short mFamily;
+        @Field(order = 1, type = Type.U16)
+        public int mLen;
+        @Field(order = 2, type = Type.S32)
+        public int mIfindex;
+        @Field(order = 3, type = Type.U8)
+        public short mIcmpType;
+        @Field(order = 4, type = Type.U8, padding = 6)
+        public short mIcmpCode;
+    }
+
+    @Test
+    public void testClassWithDefaultConstructor() {
+        final HeaderMsgWithoutConstructor msg = doParsingMessageTest(HDR_EMPTY,
+                HeaderMsgWithoutConstructor.class, ByteOrder.LITTLE_ENDIAN);
+        assertEquals(10, msg.mFamily);
+        assertEquals(0, msg.mLen);
+        assertEquals(15715755, msg.mIfindex);
+        assertEquals(134, msg.mIcmpType);
+        assertEquals(0, msg.mIcmpCode);
+
+        assertEquals(16, Struct.getSize(HeaderMsgWithoutConstructor.class));
+        assertArrayEquals(toByteBuffer(HDR_EMPTY).array(),
+                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
+    public static class HeaderMessage {
+        @Field(order = 0, type = Type.U8, padding = 1)
+        short mFamily;
+        @Field(order = 1, type = Type.U16)
+        int mLen;
+        @Field(order = 2, type = Type.S32)
+        int mIfindex;
+        @Field(order = 3, type = Type.U8)
+        short mIcmpType;
+        @Field(order = 4, type = Type.U8, padding = 6)
+        short mIcmpCode;
+    }
+
+    @Test
+    public void testInvalidClass_NotSubClass() {
+        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+        assertThrows(IllegalArgumentException.class, () -> Struct.parse(HeaderMessage.class, buf));
+    }
+
+    public static class HeaderMessageMissingAnnotation extends Struct {
+        @Field(order = 0, type = Type.U8, padding = 1)
+        short mFamily;
+        @Field(order = 1, type = Type.U16)
+        int mLen;
+        int mIfindex;
+        @Field(order = 2, type = Type.U8)
+        short mIcmpType;
+        @Field(order = 3, type = Type.U8, padding = 6)
+        short mIcmpCode;
+    }
+
+    @Test
+    public void testInvalidClass_MissingAnnotationField() {
+        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(HeaderMessageMissingAnnotation.class, buf));
+    }
+
+    public static class NetworkOrderMessage extends Struct {
+        @Field(order = 0, type = Type.UBE16)
+        public final int mUBE16;
+        @Field(order = 1, type = Type.UBE32)
+        public final long mUBE32;
+        @Field(order = 2, type = Type.UBE64)
+        public final BigInteger mUBE64;
+        @Field(order = 3, type = Type.UBE63)
+        public final long mUBE63;
+
+        NetworkOrderMessage(final int be16, final long be32, final BigInteger be64,
+                final long be63) {
+            mUBE16 = be16;
+            mUBE32 = be32;
+            mUBE64 = be64;
+            mUBE63 = be63;
+        }
+    }
+
+    @Test
+    public void testNetworkOrder() {
+        final NetworkOrderMessage msg = doParsingMessageTest(NETWORK_ORDER_MSG,
+                NetworkOrderMessage.class, ByteOrder.LITTLE_ENDIAN);
+        assertEquals(65279, msg.mUBE16);
+        assertEquals(4278190079L, msg.mUBE32);
+        assertEquals(new BigInteger("18374686479671623679"), msg.mUBE64);
+        assertEquals(9151314442816847871L, msg.mUBE63);
+
+        assertEquals(22, Struct.getSize(NetworkOrderMessage.class));
+        assertArrayEquals(toByteBuffer(NETWORK_ORDER_MSG).array(),
+                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
+    public static class UnsignedDataMessage extends Struct {
+        @Field(order = 0, type = Type.U8)
+        public final short mU8;
+        @Field(order = 1, type = Type.U16)
+        public final int mU16;
+        @Field(order = 2, type = Type.U32)
+        public final long mU32;
+        @Field(order = 3, type = Type.U64)
+        public final BigInteger mU64;
+        @Field(order = 4, type = Type.U63)
+        public final long mU63;
+        @Field(order = 5, type = Type.U63)
+        public final long mLU64; // represent U64 data with U63 type
+
+        UnsignedDataMessage(final short u8, final int u16, final long u32, final BigInteger u64,
+                final long u63, final long lu64) {
+            mU8 = u8;
+            mU16 = u16;
+            mU32 = u32;
+            mU64 = u64;
+            mU63 = u63;
+            mLU64 = lu64;
+        }
+    }
+
+    @Test
+    public void testUnsignedData() {
+        final UnsignedDataMessage msg = doParsingMessageTest(UNSIGNED_DATA,
+                UnsignedDataMessage.class, ByteOrder.LITTLE_ENDIAN);
+        assertEquals(255, msg.mU8);
+        assertEquals(65535, msg.mU16);
+        assertEquals(4294967295L, msg.mU32);
+        assertEquals(new BigInteger("18446744073709551615"), msg.mU64);
+        assertEquals(9223372036854775807L, msg.mU63);
+        assertEquals(-1L, msg.mLU64);
+
+        assertEquals(31, Struct.getSize(UnsignedDataMessage.class));
+        assertArrayEquals(toByteBuffer(UNSIGNED_DATA).array(),
+                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
+    public static class U64DataMessage extends Struct {
+        @Field(order = 0, type = Type.U64) long mU64;
+    }
+
+    @Test
+    public void testInvalidType_U64WithLongPrimitive() {
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(U64DataMessage.class, toByteBuffer("ffffffffffffffff")));
+    }
+
+    // BigInteger U64: 0x0000000000001234, BigInteger UBE64: 0x0000000000001234, BigInteger U64: 0
+    private static final String SMALL_VALUE_BIGINTEGER = "3412000000000000" + "0000000000001234"
+            + "0000000000000000";
+
+    public static class SmallValueBigInteger extends Struct {
+        @Field(order = 0, type = Type.U64) public final BigInteger mSmallValue;
+        @Field(order = 1, type = Type.UBE64) public final BigInteger mBSmallValue;
+        @Field(order = 2, type = Type.U64) public final BigInteger mZero;
+
+        SmallValueBigInteger(final BigInteger smallValue, final BigInteger bSmallValue,
+                final BigInteger zero) {
+            mSmallValue = smallValue;
+            mBSmallValue = bSmallValue;
+            mZero = zero;
+        }
+    }
+
+    @Test
+    public void testBigIntegerSmallValueOrZero() {
+        final SmallValueBigInteger msg = doParsingMessageTest(SMALL_VALUE_BIGINTEGER,
+                SmallValueBigInteger.class, ByteOrder.LITTLE_ENDIAN);
+        assertEquals(new BigInteger("4660"), msg.mSmallValue);
+        assertEquals(new BigInteger("4660"), msg.mBSmallValue);
+        assertEquals(new BigInteger("0"), msg.mZero);
+
+        assertEquals(24, Struct.getSize(SmallValueBigInteger.class));
+        assertArrayEquals(toByteBuffer(SMALL_VALUE_BIGINTEGER).array(),
+                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
+    public static class SignedDataMessage extends Struct {
+        @Field(order = 0, type = Type.S8)
+        public final byte mS8;
+        @Field(order = 1, type = Type.S16)
+        public final short mS16;
+        @Field(order = 2, type = Type.S32)
+        public final int mS32;
+        @Field(order = 3, type = Type.S64)
+        public final long mS64;
+
+        SignedDataMessage(final byte s8, final short s16, final int s32, final long s64) {
+            mS8 = s8;
+            mS16 = s16;
+            mS32 = s32;
+            mS64 = s64;
+        }
+    }
+
+    @Test
+    public void testSignedPositiveData() {
+        final SignedDataMessage msg = doParsingMessageTest(SIGNED_DATA, SignedDataMessage.class,
+                ByteOrder.LITTLE_ENDIAN);
+        assertEquals(127, msg.mS8);
+        assertEquals(32767, msg.mS16);
+        assertEquals(2147483647, msg.mS32);
+        assertEquals(9223372036854775807L, msg.mS64);
+
+        assertEquals(15, Struct.getSize(SignedDataMessage.class));
+        assertArrayEquals(toByteBuffer(SIGNED_DATA).array(),
+                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
+    @Test
+    public void testSignedNegativeData() {
+        final SignedDataMessage msg = doParsingMessageTest(SIGNED_NEGATIVE_DATA,
+                SignedDataMessage.class, ByteOrder.LITTLE_ENDIAN);
+        assertEquals(-127, msg.mS8);
+        assertEquals(-32767, msg.mS16);
+        assertEquals(-2147483647, msg.mS32);
+        assertEquals(-9223372036854775807L, msg.mS64);
+
+        assertEquals(15, Struct.getSize(SignedDataMessage.class));
+        assertArrayEquals(toByteBuffer(SIGNED_NEGATIVE_DATA).array(),
+                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
+    public static class HeaderMessageWithDuplicateOrder extends Struct {
+        @Field(order = 0, type = Type.U8, padding = 1)
+        short mFamily;
+        @Field(order = 1, type = Type.U16)
+        int mLen;
+        @Field(order = 2, type = Type.S32)
+        int mIfindex;
+        @Field(order = 2, type = Type.U8)
+        short mIcmpType;
+        @Field(order = 3, type = Type.U8, padding = 6)
+        short mIcmpCode;
+    }
+
+    @Test
+    public void testInvalidClass_DuplicateFieldOrder() {
+        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(HeaderMessageWithDuplicateOrder.class, buf));
+    }
+
+    public static class HeaderMessageWithNegativeOrder extends Struct {
+        @Field(order = 0, type = Type.U8, padding = 1)
+        short mFamily;
+        @Field(order = 1, type = Type.U16)
+        int mLen;
+        @Field(order = 2, type = Type.S32)
+        int mIfindex;
+        @Field(order = 3, type = Type.U8)
+        short mIcmpType;
+        @Field(order = -4, type = Type.U8, padding = 6)
+        short mIcmpCode;
+    }
+
+    @Test
+    public void testInvalidClass_NegativeFieldOrder() {
+        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(HeaderMessageWithNegativeOrder.class, buf));
+    }
+
+    public static class HeaderMessageOutOfIndexBounds extends Struct {
+        @Field(order = 0, type = Type.U8, padding = 1)
+        short mFamily;
+        @Field(order = 1, type = Type.U16)
+        int mLen;
+        @Field(order = 2, type = Type.S32)
+        int mIfindex;
+        @Field(order = 3, type = Type.U8)
+        short mIcmpType;
+        @Field(order = 5, type = Type.U8, padding = 6)
+        short mIcmpCode;
+    }
+
+    @Test
+    public void testInvalidClass_OutOfIndexBounds() {
+        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(HeaderMessageOutOfIndexBounds.class, buf));
+    }
+
+    public static class HeaderMessageMismatchedPrimitiveType extends Struct {
+        @Field(order = 0, type = Type.U8, padding = 1)
+        short mFamily;
+        @Field(order = 1, type = Type.U16)
+        short mLen; // should be integer
+        @Field(order = 2, type = Type.S32)
+        int mIfindex;
+        @Field(order = 3, type = Type.U8)
+        short mIcmpType;
+        @Field(order = 4, type = Type.U8, padding = 6)
+        short mIcmpCode;
+    }
+
+    @Test
+    public void testInvalidClass_MismatchedPrimitiveDataType() {
+        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(HeaderMessageMismatchedPrimitiveType.class, buf));
+    }
+
+    public static class PrefixMessage extends Struct {
+        @Field(order = 0, type = Type.UBE16)
+        public final int mLifetime;
+        @Field(order = 1, type = Type.ByteArray, arraysize = 12)
+        public final byte[] mPrefix;
+
+        PrefixMessage(final int lifetime, final byte[] prefix) {
+            mLifetime = lifetime;
+            mPrefix = prefix;
+        }
+    }
+
+    @SuppressLint("NewApi")
+    private void verifyPrefixByteArrayParsing(final PrefixMessage msg) throws Exception {
+        // The original PREF64 option message has just 12 bytes for prefix byte array
+        // (Highest 96 bits of the Prefix), copyOf pads the 128-bits IPv6 address with
+        // prefix and 4-bytes zeros.
+        final InetAddress addr = InetAddress.getByAddress(Arrays.copyOf(msg.mPrefix, 16));
+        final IpPrefix prefix = new IpPrefix(addr, 96);
+        assertEquals(10064, msg.mLifetime);
+        assertTrue(prefix.equals(new IpPrefix("2001:db8:3:4:5:6::/96")));
+
+        assertEquals(14, Struct.getSize(PrefixMessage.class));
+        assertArrayEquals(toByteBuffer(OPT_PREF64).array(),
+                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
+    public static class PrefixMessageWithZeroLengthArray extends Struct {
+        @Field(order = 0, type = Type.UBE16)
+        final int mLifetime;
+        @Field(order = 1, type = Type.ByteArray, arraysize = 0)
+        final byte[] mPrefix;
+
+        PrefixMessageWithZeroLengthArray(final int lifetime, final byte[] prefix) {
+            mLifetime = lifetime;
+            mPrefix = prefix;
+        }
+    }
+
+    @Test
+    public void testInvalidClass_ZeroLengthByteArray() {
+        final ByteBuffer buf = toByteBuffer(OPT_PREF64);
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(PrefixMessageWithZeroLengthArray.class, buf));
+    }
+
+    @Test
+    public void testPrefixArrayField() throws Exception {
+        final PrefixMessage msg = doParsingMessageTest(OPT_PREF64, PrefixMessage.class,
+                ByteOrder.LITTLE_ENDIAN);
+        verifyPrefixByteArrayParsing(msg);
+    }
+
+    public static class HeaderMessageWithMutableField extends Struct {
+        @Field(order = 0, type = Type.U8, padding = 1)
+        final short mFamily;
+        @Field(order = 1, type = Type.U16)
+        final int mLen;
+        @Field(order = 2, type = Type.S32)
+        int mIfindex;
+        @Field(order = 3, type = Type.U8)
+        short mIcmpType;
+        @Field(order = 4, type = Type.U8, padding = 6)
+        final short mIcmpCode;
+
+        HeaderMessageWithMutableField(final short family, final int len, final short code) {
+            mFamily = family;
+            mLen = len;
+            mIcmpCode = code;
+        }
+    }
+
+    @Test
+    public void testMixMutableAndImmutableFields() {
+        final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(HeaderMessageWithMutableField.class, buf));
+    }
+
+    public static class HeaderMsgWithStaticConstant extends Struct {
+        private static final String TAG = "HeaderMessage";
+        private static final int FIELD_COUNT = 5;
+
+        @Field(order = 0, type = Type.U8, padding = 1)
+        public final short mFamily;
+        @Field(order = 1, type = Type.U16)
+        public final int mLen;
+        @Field(order = 2, type = Type.S32)
+        public final int mIfindex;
+        @Field(order = 3, type = Type.U8)
+        public final short mIcmpType;
+        @Field(order = 4, type = Type.U8, padding = 6)
+        public final short mIcmpCode;
+
+        HeaderMsgWithStaticConstant(final short family, final int len, final int ifindex,
+                final short type, final short code) {
+            mFamily = family;
+            mLen = len;
+            mIfindex = ifindex;
+            mIcmpType = type;
+            mIcmpCode = code;
+        }
+    }
+
+    @Test
+    public void testStaticConstantField() {
+        final HeaderMsgWithStaticConstant msg = doParsingMessageTest(HDR_EMPTY,
+                HeaderMsgWithStaticConstant.class, ByteOrder.LITTLE_ENDIAN);
+        assertEquals(10, msg.mFamily);
+        assertEquals(0, msg.mLen);
+        assertEquals(15715755, msg.mIfindex);
+        assertEquals(134, msg.mIcmpType);
+        assertEquals(0, msg.mIcmpCode);
+
+        assertEquals(16, Struct.getSize(HeaderMsgWithStaticConstant.class));
+        assertArrayEquals(toByteBuffer(HDR_EMPTY).array(),
+                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
+    public static class MismatchedConstructor extends Struct {
+        @Field(order = 0, type = Type.U16) final int mInt1;
+        @Field(order = 1, type = Type.U16) final int mInt2;
+        MismatchedConstructor(String int1, String int2) {
+            mInt1 = Integer.valueOf(int1);
+            mInt2 = Integer.valueOf(int2);
+        }
+    }
+
+    @Test
+    public void testMisMatchedConstructor() {
+        final ByteBuffer buf = toByteBuffer("1234" + "5678");
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(MismatchedConstructor.class, buf));
+    }
+
+    public static class ClassWithTwoConstructors extends Struct {
+        @Field(order = 0, type = Type.U16) public final int mInt1;
+        @Field(order = 1, type = Type.U16) public final int mInt2;
+        ClassWithTwoConstructors(String int1, String int2) {
+            mInt1 = Integer.valueOf(int1);
+            mInt2 = Integer.valueOf(int2);
+        }
+        ClassWithTwoConstructors(int int1, int int2) {
+            mInt1 = int1;
+            mInt2 = int2;
+        }
+    }
+
+    @Test
+    public void testClassWithTwoConstructors() {
+        final ClassWithTwoConstructors msg = doParsingMessageTest("1234" + "5678",
+                ClassWithTwoConstructors.class, ByteOrder.LITTLE_ENDIAN);
+        assertEquals(13330 /* 0x3412 */, msg.mInt1);
+        assertEquals(30806 /* 0x7856 */, msg.mInt2);
+
+        assertEquals(4, Struct.getSize(ClassWithTwoConstructors.class));
+        assertArrayEquals(toByteBuffer("1234" + "5678").array(),
+                msg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
+    @Test
+    public void testInvalidOutputByteBuffer_ZeroCapacity() {
+        final ByteBuffer output = ByteBuffer.allocate(0);
+        output.order(ByteOrder.LITTLE_ENDIAN);
+        final HeaderMsgWithConstructor msg = doParsingMessageTest(HDR_EMPTY,
+                HeaderMsgWithConstructor.class, ByteOrder.LITTLE_ENDIAN);
+        assertThrows(BufferOverflowException.class, () -> msg.writeToByteBuffer(output));
+    }
+
+    @Test
+    public void testConsecutiveWrites() {
+        final HeaderMsgWithConstructor msg1 = doParsingMessageTest(HDR_EMPTY,
+                HeaderMsgWithConstructor.class, ByteOrder.LITTLE_ENDIAN);
+        final PrefixMessage msg2 = doParsingMessageTest(OPT_PREF64, PrefixMessage.class,
+                ByteOrder.LITTLE_ENDIAN);
+
+        int size = Struct.getSize(HeaderMsgWithConstructor.class)
+                + Struct.getSize(PrefixMessage.class);
+        final ByteBuffer output = ByteBuffer.allocate(size);
+        output.order(ByteOrder.LITTLE_ENDIAN);
+
+        msg1.writeToByteBuffer(output);
+        msg2.writeToByteBuffer(output);
+        output.flip();
+
+        final ByteBuffer concat = ByteBuffer.allocate(size).put(toByteBuffer(HDR_EMPTY))
+                .put(toByteBuffer(OPT_PREF64));
+        assertArrayEquals(output.array(), concat.array());
+    }
+
+    @Test
+    public void testClassesParsedFromCache() throws Exception {
+        for (int i = 0; i < 100; i++) {
+            final HeaderMsgWithConstructor msg1 = doParsingMessageTest(HDR_EMPTY,
+                    HeaderMsgWithConstructor.class, ByteOrder.LITTLE_ENDIAN);
+            verifyHeaderParsing(msg1);
+
+            final PrefixMessage msg2 = doParsingMessageTest(OPT_PREF64, PrefixMessage.class,
+                    ByteOrder.LITTLE_ENDIAN);
+            verifyPrefixByteArrayParsing(msg2);
+        }
+    }
+
+    public static class BigEndianDataMessage extends Struct {
+        @Field(order = 0, type = Type.S32) public int mInt1;
+        @Field(order = 1, type = Type.S32) public int mInt2;
+        @Field(order = 2, type = Type.UBE16) public int mInt3;
+        @Field(order = 3, type = Type.U16) public int mInt4;
+        @Field(order = 4, type = Type.U64) public BigInteger mBigInteger1;
+        @Field(order = 5, type = Type.UBE64) public BigInteger mBigInteger2;
+        @Field(order = 6, type = Type.S64) public long mLong;
+    }
+
+    private static final String BIG_ENDIAN_DATA = "00000001" + "fffffffe" + "fffe" + "fffe"
+            + "ff00004500002301" + "ff00004500002301" + "ff00004500002301";
+
+    @Test
+    public void testBigEndianByteBuffer() {
+        final BigEndianDataMessage msg = doParsingMessageTest(BIG_ENDIAN_DATA,
+                BigEndianDataMessage.class, ByteOrder.BIG_ENDIAN);
+
+        assertEquals(1, msg.mInt1);
+        assertEquals(-2, msg.mInt2);
+        assertEquals(65534, msg.mInt3);
+        assertEquals(65534, msg.mInt4);
+        assertEquals(new BigInteger("18374686776024376065"), msg.mBigInteger1);
+        assertEquals(new BigInteger("18374686776024376065"), msg.mBigInteger2);
+        assertEquals(0xff00004500002301L, msg.mLong);
+
+        assertEquals(36, Struct.getSize(BigEndianDataMessage.class));
+        assertArrayEquals(toByteBuffer(BIG_ENDIAN_DATA).array(),
+                msg.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+
+    private ByteBuffer toByteBuffer(final String hexString) {
+        return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+    }
+
+    public static class MacAddressMessage extends Struct {
+        public @Field(order = 0, type = Type.EUI48) final MacAddress mMac1;
+        public @Field(order = 1, type = Type.EUI48) final MacAddress mMac2;
+
+        MacAddressMessage(final MacAddress mac1, final MacAddress mac2) {
+            this.mMac1 = mac1;
+            this.mMac2 = mac2;
+        }
+    }
+
+    @Test
+    public void testMacAddressType() {
+        final MacAddressMessage msg = doParsingMessageTest("001122334455" + "ffffffffffff",
+                MacAddressMessage.class, ByteOrder.BIG_ENDIAN);
+
+        assertEquals(MacAddress.fromString("00:11:22:33:44:55"), msg.mMac1);
+        assertEquals(MacAddress.fromString("ff:ff:ff:ff:ff:ff"), msg.mMac2);
+
+        assertEquals(12, Struct.getSize(MacAddressMessage.class));
+        assertArrayEquals(toByteBuffer("001122334455" + "ffffffffffff").array(),
+                msg.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+
+    public static class BadMacAddressType extends Struct {
+        @Field(order = 0, type = Type.EUI48) byte[] mMac;
+    }
+
+    @Test
+    public void testIncorrectType_EUI48WithByteArray() {
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(BadMacAddressType.class, toByteBuffer("ffffffffffff")));
+    }
+
+    @Test
+    public void testStructToByteArrayRoundTrip() {
+        final SignedDataMessage littleEndianMsg = doParsingMessageTest(SIGNED_DATA,
+                SignedDataMessage.class, ByteOrder.LITTLE_ENDIAN);
+        assertArrayEquals(toByteBuffer(SIGNED_DATA).array(),
+                littleEndianMsg.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+
+        final SignedDataMessage bigEndianMsg = doParsingMessageTest(SIGNED_DATA,
+                SignedDataMessage.class, ByteOrder.BIG_ENDIAN);
+        assertArrayEquals(toByteBuffer(SIGNED_DATA).array(),
+                bigEndianMsg.writeToBytes(ByteOrder.BIG_ENDIAN));
+
+        final SignedDataMessage nativeOrderMsg = ByteOrder.nativeOrder().equals(
+                ByteOrder.LITTLE_ENDIAN) ? littleEndianMsg : bigEndianMsg;
+        assertArrayEquals(toByteBuffer(SIGNED_DATA).array(),
+                nativeOrderMsg.writeToBytes());
+    }
+
+    @Test
+    public void testStructToByteArray() {
+        final SignedDataMessage msg = new SignedDataMessage((byte) -5, (short) 42, (int) 0xff000004,
+                (long) 0xff000004ff000005L);
+        final String leHexString = "fb" + "2a00" + "040000ff" + "050000ff040000ff";
+        final String beHexString = "fb" + "002a" + "ff000004" + "ff000004ff000005";
+        final String hexString = ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)
+                ? leHexString : beHexString;
+        assertArrayEquals(toByteBuffer(hexString).array(), msg.writeToBytes());
+    }
+
+    public static class IpAddressMessage extends Struct {
+        @Field(order = 0, type = Type.Ipv4Address) public final Inet4Address ipv4Address;
+        @Field(order = 1, type = Type.Ipv6Address) public final Inet6Address ipv6Address;
+
+        IpAddressMessage(final Inet4Address ipv4Address, final Inet6Address ipv6Address) {
+            this.ipv4Address = ipv4Address;
+            this.ipv6Address = ipv6Address;
+        }
+    }
+
+    @Test
+    public void testIpAddressType() {
+        final IpAddressMessage msg = doParsingMessageTest("c0a86401"
+                + "20010db8000300040005000600070008", IpAddressMessage.class, ByteOrder.BIG_ENDIAN);
+
+        assertEquals(TEST_IPV4_ADDRESS, msg.ipv4Address);
+        assertEquals(TEST_IPV6_ADDRESS, msg.ipv6Address);
+
+        assertEquals(20, Struct.getSize(IpAddressMessage.class));
+        assertArrayEquals(toByteBuffer("c0a86401" + "20010db8000300040005000600070008").array(),
+                msg.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+
+    @Test
+    public void testV4MappedV6Address() {
+        final IpAddressMessage msg = doParsingMessageTest("c0a86401"
+                + "00000000000000000000ffffc0a86401", IpAddressMessage.class, ByteOrder.BIG_ENDIAN);
+        assertEquals(TEST_IPV4_ADDRESS, msg.ipv4Address);
+        assertEquals(InetAddressUtils.v4MappedV6Address(TEST_IPV4_ADDRESS), msg.ipv6Address);
+    }
+
+    public static class WrongIpAddressType extends Struct {
+        @Field(order = 0, type = Type.Ipv4Address) public byte[] ipv4Address;
+        @Field(order = 1, type = Type.Ipv6Address) public byte[] ipv6Address;
+    }
+
+    @Test
+    public void testIncorrectType_IpAddressWithByteArray() {
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(WrongIpAddressType.class,
+                                   toByteBuffer("c0a86401" + "20010db8000300040005000600070008")));
+    }
+
+    public static class FullTypeMessage extends Struct {
+        @Field(order = 0, type = Type.U8) public final short u8;
+        @Field(order = 1, type = Type.U16) public final int u16;
+        @Field(order = 2, type = Type.U32) public final long u32;
+        @Field(order = 3, type = Type.U63) public final long u63;
+        @Field(order = 4, type = Type.U64) public final BigInteger u64;
+        @Field(order = 5, type = Type.S8) public final byte s8;
+        @Field(order = 6, type = Type.S16) public final short s16;
+        @Field(order = 7, type = Type.S32) public final int s32;
+        @Field(order = 8, type = Type.S64) public final long s64;
+        @Field(order = 9, type = Type.UBE16) public final int ube16;
+        @Field(order = 10, type = Type.UBE32) public final long ube32;
+        @Field(order = 11, type = Type.UBE63) public final long ube63;
+        @Field(order = 12, type = Type.UBE64) public final BigInteger ube64;
+        @Field(order = 13, type = Type.ByteArray, arraysize = 12) public final byte[] bytes;
+        @Field(order = 14, type = Type.EUI48) public final MacAddress eui48;
+        @Field(order = 15, type = Type.Ipv4Address) public final Inet4Address ipv4Address;
+        @Field(order = 16, type = Type.Ipv6Address) public final Inet6Address ipv6Address;
+
+        FullTypeMessage(final short u8, final int u16, final long u32, final long u63,
+                final BigInteger u64, final byte s8, final short s16, final int s32, final long s64,
+                final int ube16, final long ube32, final long ube63, final BigInteger ube64,
+                final byte[] bytes, final MacAddress eui48, final Inet4Address ipv4Address,
+                final Inet6Address ipv6Address) {
+            this.u8 = u8;
+            this.u16 = u16;
+            this.u32 = u32;
+            this.u63 = u63;
+            this.u64 = u64;
+            this.s8 = s8;
+            this.s16 = s16;
+            this.s32 = s32;
+            this.s64 = s64;
+            this.ube16 = ube16;
+            this.ube32 = ube32;
+            this.ube63 = ube63;
+            this.ube64 = ube64;
+            this.bytes = bytes;
+            this.eui48 = eui48;
+            this.ipv4Address = ipv4Address;
+            this.ipv6Address = ipv6Address;
+        }
+    }
+
+    private static final String FULL_TYPE_DATA = "ff" + "ffff" + "ffffffff" + "7fffffffffffffff"
+            + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff" + "7fffffffffffffff" + "7fff"
+            + "7fffffff" + "7fffffffffffffff" + "ffffffffffffffff" + "20010db80003000400050006"
+            + "001122334455" + "c0a86401" + "20010db8000300040005000600070008";
+    private static final String FULL_TYPE_DATA_DIFF_MAC = "ff" + "ffff" + "ffffffff"
+            + "7fffffffffffffff" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
+            + "7fffffffffffffff" + "7fff" + "7fffffff" + "7fffffffffffffff" + "ffffffffffffffff"
+            + "20010db80003000400050006" + "112233445566"
+            + "c0a86401" + "20010db8000300040005000600070008";
+    private static final String FULL_TYPE_DATA_DIFF_LONG = "ff" + "ffff" + "ffffffff"
+            + "7ffffffffffffffe" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
+            + "7fffffffffffffff" + "7fff" + "7fffffff" + "7fffffffffffffff" + "ffffffffffffffff"
+            + "20010db80003000400050006" + "001122334455"
+            + "c0a86401" + "20010db8000300040005000600070008";
+    private static final String FULL_TYPE_DATA_DIFF_INTEGER = "ff" + "ffff" + "ffffffff"
+            + "7fffffffffffffff" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
+            + "7fffffffffffffff" + "7fff" + "ffffff7f" + "7fffffffffffffff" + "ffffffffffffffff"
+            + "20010db80003000400050006" + "001122334455"
+            + "c0a86401" + "20010db8000300040005000600070008";
+    private static final String FULL_TYPE_DATA_DIFF_IPV4 = "ff" + "ffff" + "ffffffff"
+            + "7fffffffffffffff" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
+            + "7fffffffffffffff" + "7fff" + "ffffff7f" + "7fffffffffffffff" + "ffffffffffffffff"
+            + "20010db80003000400050006" + "001122334455"
+            + "c0a81010" + "20010db8000300040005000600070008";
+    private static final String FULL_TYPE_DATA_DIFF_IPV6 = "ff" + "ffff" + "ffffffff"
+            + "7fffffffffffffff" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
+            + "7fffffffffffffff" + "7fff" + "ffffff7f" + "7fffffffffffffff" + "ffffffffffffffff"
+            + "20010db80003000400050006" + "001122334455"
+            + "c0a86401" + "20010db800030004000500060007000a";
+    @Test
+    public void testStructClass_equals() {
+        final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+                ByteOrder.BIG_ENDIAN);
+
+        assertEquals(255, msg.u8);
+        assertEquals(65535, msg.u16);
+        assertEquals(4294967295L, msg.u32);
+        assertEquals(9223372036854775807L, msg.u63);
+        assertEquals(new BigInteger("18446744073709551615"), msg.u64);
+        assertEquals(127, msg.s8);
+        assertEquals(32767, msg.s16);
+        assertEquals(2147483647, msg.s32);
+        assertEquals(9223372036854775807L, msg.s64);
+        assertEquals(32767, msg.ube16);
+        assertEquals(2147483647, msg.ube32);
+        assertEquals(9223372036854775807L, msg.ube63);
+        assertEquals(new BigInteger("18446744073709551615"), msg.ube64);
+        assertArrayEquals(TEST_PREFIX64, msg.bytes);
+        assertEquals(MacAddress.fromString("00:11:22:33:44:55"), msg.eui48);
+        assertEquals(TEST_IPV4_ADDRESS, msg.ipv4Address);
+        assertEquals(TEST_IPV6_ADDRESS, msg.ipv6Address);
+
+        assertEquals(98, msg.getSize(FullTypeMessage.class));
+        assertArrayEquals(toByteBuffer(FULL_TYPE_DATA).array(),
+                msg.writeToBytes(ByteOrder.BIG_ENDIAN));
+
+        final FullTypeMessage msg1 = new FullTypeMessage((short) 0xff, (int) 0xffff,
+                (long) 0xffffffffL, (long) 0x7fffffffffffffffL,
+                new BigInteger("18446744073709551615"), (byte) 0x7f, (short) 0x7fff,
+                (int) 0x7fffffff, (long) 0x7fffffffffffffffL, (int) 0x7fff, (long) 0x7fffffffL,
+                (long) 0x7fffffffffffffffL, new BigInteger("18446744073709551615"), TEST_PREFIX64,
+                MacAddress.fromString("00:11:22:33:44:55"), TEST_IPV4_ADDRESS, TEST_IPV6_ADDRESS);
+        assertTrue(msg.equals(msg1));
+    }
+
+    public static class FullTypeMessageWithDupType extends Struct {
+        @Field(order = 0, type = Type.U8) public final short u8;
+        @Field(order = 1, type = Type.U16) public final int u16;
+        @Field(order = 2, type = Type.U32) public final long u32;
+        @Field(order = 3, type = Type.S64) public final long u63; // old: U63, new: S64
+        @Field(order = 4, type = Type.UBE64) public final BigInteger u64; // old: U64, new: UBE64
+        @Field(order = 5, type = Type.S8) public final byte s8;
+        @Field(order = 6, type = Type.S16) public final short s16;
+        @Field(order = 7, type = Type.S32) public final int s32;
+        @Field(order = 8, type = Type.S64) public final long s64;
+        @Field(order = 9, type = Type.U16) public final int ube16; // old:UBE16, new: U16
+        @Field(order = 10, type = Type.UBE32) public final long ube32;
+        @Field(order = 11, type = Type.UBE63) public final long ube63;
+        @Field(order = 12, type = Type.UBE64) public final BigInteger ube64;
+        @Field(order = 13, type = Type.ByteArray, arraysize = 12) public final byte[] bytes;
+        @Field(order = 14, type = Type.EUI48) public final MacAddress eui48;
+        @Field(order = 15, type = Type.Ipv4Address) public final Inet4Address ipv4Address;
+        @Field(order = 16, type = Type.Ipv6Address) public final Inet6Address ipv6Address;
+
+        FullTypeMessageWithDupType(final short u8, final int u16, final long u32, final long u63,
+                final BigInteger u64, final byte s8, final short s16, final int s32, final long s64,
+                final int ube16, final long ube32, final long ube63, final BigInteger ube64,
+                final byte[] bytes, final MacAddress eui48, final Inet4Address ipv4Address,
+                final Inet6Address ipv6Address) {
+            this.u8 = u8;
+            this.u16 = u16;
+            this.u32 = u32;
+            this.u63 = u63;
+            this.u64 = u64;
+            this.s8 = s8;
+            this.s16 = s16;
+            this.s32 = s32;
+            this.s64 = s64;
+            this.ube16 = ube16;
+            this.ube32 = ube32;
+            this.ube63 = ube63;
+            this.ube64 = ube64;
+            this.bytes = bytes;
+            this.eui48 = eui48;
+            this.ipv4Address = ipv4Address;
+            this.ipv6Address = ipv6Address;
+        }
+    }
+
+    @Test
+    public void testStructClass_notEqualWithDifferentClass() {
+        final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+                ByteOrder.BIG_ENDIAN);
+        final FullTypeMessageWithDupType msg1 = doParsingMessageTest(FULL_TYPE_DATA,
+                FullTypeMessageWithDupType.class, ByteOrder.BIG_ENDIAN);
+
+        assertFalse(msg.equals(msg1));
+    }
+
+    @Test
+    public void testStructClass_notEqualWithDifferentValue() {
+        final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+                ByteOrder.BIG_ENDIAN);
+
+        // With different MAC address.
+        final FullTypeMessage msg1 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_MAC,
+                FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
+        assertNotEquals(msg.eui48, msg1.eui48);
+        assertFalse(msg.equals(msg1));
+
+        // With different byte array.
+        final FullTypeMessage msg2 = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+                ByteOrder.BIG_ENDIAN);
+        msg2.bytes[5] = (byte) 42; // change one byte in the array.
+        assertFalse(msg.equals(msg2));
+
+        // With different Long primitive.
+        final FullTypeMessage msg3 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_LONG,
+                FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
+        assertNotEquals(msg.u63, msg3.u63);
+        assertFalse(msg.equals(msg3));
+
+        // With different Integer primitive.
+        final FullTypeMessage msg4 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_INTEGER,
+                FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
+        assertNotEquals(msg.ube32, msg4.ube32);
+        assertFalse(msg.equals(msg4));
+
+        // With different IPv4 address.
+        final FullTypeMessage msg5 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_IPV4,
+                FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
+        assertNotEquals(msg.ipv4Address, msg5.ipv4Address);
+        assertFalse(msg.equals(msg5));
+
+        // With different IPv6 address.
+        final FullTypeMessage msg6 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_IPV6,
+                FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
+        assertNotEquals(msg.ipv6Address, msg6.ipv6Address);
+        assertFalse(msg.equals(msg6));
+    }
+
+    @Test
+    public void testStructClass_toString() {
+        final String expected = "u8: 255, u16: 65535, u32: 4294967295,"
+                + " u63: 9223372036854775807, u64: 18446744073709551615, s8: 127, s16: 32767,"
+                + " s32: 2147483647, s64: 9223372036854775807, ube16: 32767, ube32: 2147483647,"
+                + " ube63: 9223372036854775807, ube64: 18446744073709551615,"
+                + " bytes: 0x20010DB80003000400050006,"
+                + " eui48: 00:11:22:33:44:55,"
+                + " ipv4Address: 192.168.100.1,"
+                + " ipv6Address: 2001:db8:3:4:5:6:7:8";
+
+        final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+                ByteOrder.BIG_ENDIAN);
+        assertEquals(expected, msg.toString());
+    }
+
+    @Test
+    public void testStructClass_toStringWithNullMember() {
+        final String expected = "u8: 255, u16: 65535, u32: 4294967295,"
+                + " u63: 9223372036854775807, u64: null, s8: 127, s16: 32767,"
+                + " s32: 2147483647, s64: 9223372036854775807, ube16: 32767, ube32: 2147483647,"
+                + " ube63: 9223372036854775807, ube64: 18446744073709551615,"
+                + " bytes: null, eui48: null, ipv4Address: 192.168.100.1,"
+                + " ipv6Address: null";
+
+        final FullTypeMessage msg = new FullTypeMessage((short) 0xff, (int) 0xffff,
+                (long) 0xffffffffL, (long) 0x7fffffffffffffffL,
+                null /* u64 */, (byte) 0x7f, (short) 0x7fff,
+                (int) 0x7fffffff, (long) 0x7fffffffffffffffL, (int) 0x7fff, (long) 0x7fffffffL,
+                (long) 0x7fffffffffffffffL, new BigInteger("18446744073709551615"),
+                null /* bytes */, null /* eui48 */, TEST_IPV4_ADDRESS, null /* ipv6Address */);
+        assertEquals(expected, msg.toString());
+    }
+
+    @Test
+    public void testStructClass_hashcode() {
+        final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+                ByteOrder.BIG_ENDIAN);
+        final FullTypeMessage msg1 = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+                ByteOrder.BIG_ENDIAN);
+
+        assertNotEquals(0, msg.hashCode());
+        assertNotEquals(0, msg1.hashCode());
+        assertTrue(msg.equals(msg1));
+        assertEquals(msg.hashCode(), msg1.hashCode());
+    }
+
+    public static class InvalidByteArray extends Struct {
+        @Field(order = 0, type = Type.ByteArray, arraysize = 12) public byte[] bytes;
+    }
+
+    @Test
+    public void testStructClass_WrongByteArraySize() {
+        final InvalidByteArray msg = doParsingMessageTest("20010db80003000400050006",
+                InvalidByteArray.class, ByteOrder.BIG_ENDIAN);
+
+        // Actual byte array size doesn't match the size declared in the annotation.
+        msg.bytes = new byte[16];
+        assertThrows(IllegalStateException.class, () -> msg.writeToBytes());
+
+        final ByteBuffer output = ByteBuffer.allocate(Struct.getSize(InvalidByteArray.class));
+        output.order(ByteOrder.LITTLE_ENDIAN);
+        assertThrows(IllegalStateException.class, () -> msg.writeToByteBuffer(output));
+    }
+
+    @Test
+    public void testStructClass_NullByteArray() {
+        final InvalidByteArray msg = doParsingMessageTest("20010db80003000400050006",
+                InvalidByteArray.class, ByteOrder.BIG_ENDIAN);
+
+        msg.bytes = null;
+        assertThrows(NullPointerException.class, () -> msg.writeToBytes());
+
+        final ByteBuffer output = ByteBuffer.allocate(Struct.getSize(InvalidByteArray.class));
+        output.order(ByteOrder.LITTLE_ENDIAN);
+        assertThrows(NullPointerException.class, () -> msg.writeToByteBuffer(output));
+    }
+
+    @Test
+    public void testStructClass_ParsingByteArrayAfterInitialization() {
+        InvalidByteArray msg = new InvalidByteArray();
+        msg.bytes = new byte[]{(byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03};
+
+        // Although bytes member has been initialized with the length different with
+        // annotation size, parsing from ByteBuffer will get bytes member have the
+        // reference to byte array with correct size.
+        msg = doParsingMessageTest("20010db80003000400050006", InvalidByteArray.class,
+                ByteOrder.BIG_ENDIAN);
+        assertArrayEquals(TEST_PREFIX64, msg.bytes);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
new file mode 100644
index 0000000..d534054
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
@@ -0,0 +1,294 @@
+/**
+ * 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.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.net.module.util.SyncStateMachine.StateInfo
+import java.util.ArrayDeque
+import java.util.ArrayList
+import kotlin.test.assertFailsWith
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+private const val MSG_INVALID = -1
+private const val MSG_1 = 1
+private const val MSG_2 = 2
+private const val MSG_3 = 3
+private const val MSG_4 = 4
+private const val MSG_5 = 5
+private const val MSG_6 = 6
+private const val MSG_7 = 7
+private const val ARG_1 = 100
+private const val ARG_2 = 200
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+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) {})
+    private val mState4 = spy(object : TestState(MSG_4) {})
+    private val mState5 = spy(object : TestState(MSG_5) {})
+    private val mState6 = spy(object : TestState(MSG_6) {})
+    private val mState7 = spy(object : TestState(MSG_7) {})
+    private val mInOrder = inOrder(mState1, mState2, mState3, mState4, mState5, mState6, mState7)
+    // Lazy initialize to make sure running in test thread.
+    private val mSM by lazy {
+        SyncStateMachine("TestSyncStateMachine", Thread.currentThread(), true /* debug */)
+    }
+    private val mAllStates = ArrayList<StateInfo>()
+
+    private val mMsgProcessedResults = ArrayDeque<Pair<State, Int>>()
+
+    open inner class TestState(val expected: Int) : State() {
+        // Control destination state in obj field for testing.
+        override fun processMessage(msg: Message): Boolean {
+            mMsgProcessedResults.add(this to msg.what)
+            assertEquals(ARG_1, msg.arg1)
+            assertEquals(ARG_2, msg.arg2)
+
+            if (msg.what == expected) {
+                msg.obj?.let { mSM.transitionTo(it as State) }
+                return true
+            }
+
+            return false
+        }
+    }
+
+    private fun verifyNoMoreInteractions() {
+        verifyNoMoreInteractions(mState1, mState2, mState3, mState4, mState5, mState6)
+    }
+
+    private fun processMessage(what: Int, toState: State?) {
+        mSM.processMessage(what, ARG_1, ARG_2, toState)
+    }
+
+    private fun verifyMessageProcessedBy(what: Int, vararg processedStates: State) {
+        for (state in processedStates) {
+            // InOrder.verify can't check the Message content here because SyncSM will recycle the
+            // message after it's been processed. SyncSM reuses the same Message instance for all
+            // messages it processes. So, if using InOrder.verify to verify the content of a message
+            // after SyncSM has processed it, the content would be wrong.
+            mInOrder.verify(state).processMessage(any())
+            val (processedState, msgWhat) = mMsgProcessedResults.remove()
+            assertEquals(state, processedState)
+            assertEquals(what, msgWhat)
+        }
+        assertTrue(mMsgProcessedResults.isEmpty())
+    }
+
+    @Test
+    fun testInitialState() {
+        // mState1 -> initial
+        //    |
+        // mState2
+        mAllStates.add(StateInfo(mState1, null))
+        mAllStates.add(StateInfo(mState2, mState1))
+        mSM.addAllStates(mAllStates)
+
+        mSM.start(mState1)
+        mInOrder.verify(mState1).enter()
+        verifyNoMoreInteractions()
+    }
+
+    @Test
+    fun testStartFromLeafState() {
+        // mState1 -> initial
+        //    |
+        // mState2
+        //    |
+        // mState3
+        mAllStates.add(StateInfo(mState1, null))
+        mAllStates.add(StateInfo(mState2, mState1))
+        mAllStates.add(StateInfo(mState3, mState2))
+        mSM.addAllStates(mAllStates)
+
+        mSM.start(mState3)
+        mInOrder.verify(mState1).enter()
+        mInOrder.verify(mState2).enter()
+        mInOrder.verify(mState3).enter()
+        verifyNoMoreInteractions()
+    }
+
+    private fun verifyStart() {
+        mSM.addAllStates(mAllStates)
+        mSM.start(mState1)
+        mInOrder.verify(mState1).enter()
+        verifyNoMoreInteractions()
+    }
+
+    fun addState(state: State, parent: State? = null) {
+        mAllStates.add(StateInfo(state, parent))
+    }
+
+    @Test
+    fun testAddState() {
+        // Add duplicated states.
+        mAllStates.add(StateInfo(mState1, null))
+        mAllStates.add(StateInfo(mState1, null))
+        assertFailsWith(IllegalStateException::class) {
+            mSM.addAllStates(mAllStates)
+        }
+    }
+
+    @Test
+    fun testProcessMessage() {
+        // mState1
+        //    |
+        // mState2
+        addState(mState1)
+        addState(mState2, mState1)
+        verifyStart()
+
+        processMessage(MSG_1, null)
+        verifyMessageProcessedBy(MSG_1, mState1)
+        verifyNoMoreInteractions()
+    }
+
+    @Test
+    fun testTwoStates() {
+        // mState1 <-initial, mState2
+        addState(mState1)
+        addState(mState2)
+        verifyStart()
+
+        // Test transition to mState2
+        processMessage(MSG_1, mState2)
+        verifyMessageProcessedBy(MSG_1, mState1)
+        mInOrder.verify(mState1).exit()
+        mInOrder.verify(mState2).enter()
+        verifyNoMoreInteractions()
+
+        // If set destState to mState2 (current state), no state transition.
+        processMessage(MSG_2, mState2)
+        verifyMessageProcessedBy(MSG_2, mState2)
+        verifyNoMoreInteractions()
+    }
+
+    @Test
+    fun testTwoStateTrees() {
+        //    mState1 -> initial  mState4
+        //    /     \             /     \
+        // mState2 mState3     mState5 mState6
+        addState(mState1)
+        addState(mState2, mState1)
+        addState(mState3, mState1)
+        addState(mState4)
+        addState(mState5, mState4)
+        addState(mState6, mState4)
+        verifyStart()
+
+        //    mState1 -> current     mState4
+        //    /     \                /     \
+        // mState2 mState3 -> dest mState5 mState6
+        processMessage(MSG_1, mState3)
+        verifyMessageProcessedBy(MSG_1, mState1)
+        mInOrder.verify(mState3).enter()
+        verifyNoMoreInteractions()
+
+        //           mState1                     mState4
+        //           /     \                     /     \
+        // dest <- mState2 mState3 -> current mState5 mState6
+        processMessage(MSG_1, mState2)
+        verifyMessageProcessedBy(MSG_1, mState3, mState1)
+        mInOrder.verify(mState3).exit()
+        mInOrder.verify(mState2).enter()
+        verifyNoMoreInteractions()
+
+        //               mState1          mState4
+        //               /     \          /     \
+        // current <- mState2 mState3 mState5 mState6 -> dest
+        processMessage(MSG_2, mState6)
+        verifyMessageProcessedBy(MSG_2, mState2)
+        mInOrder.verify(mState2).exit()
+        mInOrder.verify(mState1).exit()
+        mInOrder.verify(mState4).enter()
+        mInOrder.verify(mState6).enter()
+        verifyNoMoreInteractions()
+    }
+
+    @Test
+    fun testMultiDepthTransition() {
+        //      mState1 -> current
+        //    |          \
+        //  mState2         mState6
+        //    |   \           |
+        //  mState3 mState5  mState7
+        //    |
+        //  mState4
+        addState(mState1)
+        addState(mState2, mState1)
+        addState(mState6, mState1)
+        addState(mState3, mState2)
+        addState(mState5, mState2)
+        addState(mState7, mState6)
+        addState(mState4, mState3)
+        verifyStart()
+
+        //      mState1 -> current
+        //    |          \
+        //  mState2         mState6
+        //    |   \           |
+        //  mState3 mState5  mState7
+        //    |
+        //  mState4 -> dest
+        processMessage(MSG_1, mState4)
+        verifyMessageProcessedBy(MSG_1, mState1)
+        mInOrder.verify(mState2).enter()
+        mInOrder.verify(mState3).enter()
+        mInOrder.verify(mState4).enter()
+        verifyNoMoreInteractions()
+
+        //            mState1
+        //        /            \
+        //  mState2             mState6
+        //    |   \                 \
+        //  mState3 mState5 -> dest  mState7
+        //    |
+        //  mState4 -> current
+        processMessage(MSG_1, mState5)
+        verifyMessageProcessedBy(MSG_1, mState4, mState3, mState2, mState1)
+        mInOrder.verify(mState4).exit()
+        mInOrder.verify(mState3).exit()
+        mInOrder.verify(mState5).enter()
+        verifyNoMoreInteractions()
+
+        //            mState1
+        //        /              \
+        //  mState2               mState6
+        //    |   \                    \
+        //  mState3 mState5 -> current  mState7 -> dest
+        //    |
+        //  mState4
+        processMessage(MSG_2, mState7)
+        verifyMessageProcessedBy(MSG_2, mState5, mState2)
+        mInOrder.verify(mState5).exit()
+        mInOrder.verify(mState2).exit()
+        mInOrder.verify(mState6).enter()
+        mInOrder.verify(mState7).enter()
+        verifyNoMoreInteractions()
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
new file mode 100644
index 0000000..8e320d0
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import com.android.testutils.ConcurrentInterpreter
+import com.android.testutils.INTERPRET_TIME_UNIT
+import com.android.testutils.InterpretException
+import com.android.testutils.InterpretMatcher
+import com.android.testutils.SyntaxException
+import com.android.testutils.__FILE__
+import com.android.testutils.__LINE__
+import com.android.testutils.intArg
+import com.android.testutils.strArg
+import com.android.testutils.timeArg
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.util.concurrent.CyclicBarrier
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.system.measureTimeMillis
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+val TEST_VALUES = listOf(4, 13, 52, 94, 41, 68, 11, 13, 51, 0, 91, 94, 33, 98, 14)
+const val ABSENT_VALUE = 2
+// Caution in changing these : some tests rely on the fact that TEST_TIMEOUT > 2 * SHORT_TIMEOUT
+// and LONG_TIMEOUT > 2 * TEST_TIMEOUT
+const val SHORT_TIMEOUT = 40L // ms
+const val TEST_TIMEOUT = 200L // ms
+const val LONG_TIMEOUT = 5000L // ms
+
+@RunWith(JUnit4::class)
+class TrackRecordTest {
+    @Test
+    fun testAddAndSizeAndGet() {
+        val repeats = 22 // arbitrary
+        val record = ArrayTrackRecord<Int>()
+        assertEquals(0, record.size)
+        repeat(repeats) { i -> record.add(i + 2) }
+        assertEquals(repeats, record.size)
+        record.add(2)
+        assertEquals(repeats + 1, record.size)
+
+        assertEquals(11, record[9])
+        assertEquals(11, record.getOrNull(9))
+        assertEquals(2, record[record.size - 1])
+        assertEquals(2, record.getOrNull(record.size - 1))
+
+        assertFailsWith<IndexOutOfBoundsException> { record[800] }
+        assertFailsWith<IndexOutOfBoundsException> { record[-1] }
+        assertFailsWith<IndexOutOfBoundsException> { record[repeats + 1] }
+        assertNull(record.getOrNull(800))
+        assertNull(record.getOrNull(-1))
+        assertNull(record.getOrNull(repeats + 1))
+        assertNull(record.getOrNull(800) { true })
+        assertNull(record.getOrNull(-1) { true })
+        assertNull(record.getOrNull(repeats + 1) { true })
+    }
+
+    @Test
+    fun testIndexOf() {
+        val record = ArrayTrackRecord<Int>()
+        TEST_VALUES.forEach { record.add(it) }
+        with(record) {
+            assertEquals(9, indexOf(0))
+            assertEquals(9, lastIndexOf(0))
+            assertEquals(1, indexOf(13))
+            assertEquals(7, lastIndexOf(13))
+            assertEquals(3, indexOf(94))
+            assertEquals(11, lastIndexOf(94))
+            assertEquals(-1, indexOf(ABSENT_VALUE))
+            assertEquals(-1, lastIndexOf(ABSENT_VALUE))
+        }
+    }
+
+    @Test
+    fun testContains() {
+        val record = ArrayTrackRecord<Int>()
+        TEST_VALUES.forEach { record.add(it) }
+        TEST_VALUES.forEach { assertTrue(record.contains(it)) }
+        assertFalse(record.contains(ABSENT_VALUE))
+        assertTrue(record.containsAll(TEST_VALUES))
+        assertTrue(record.containsAll(TEST_VALUES.sorted()))
+        assertTrue(record.containsAll(TEST_VALUES.sortedDescending()))
+        assertTrue(record.containsAll(TEST_VALUES.distinct()))
+        assertTrue(record.containsAll(TEST_VALUES.subList(0, TEST_VALUES.size / 2)))
+        assertTrue(record.containsAll(TEST_VALUES.subList(0, TEST_VALUES.size / 2).sorted()))
+        assertTrue(record.containsAll(listOf()))
+        assertFalse(record.containsAll(listOf(ABSENT_VALUE)))
+        assertFalse(record.containsAll(TEST_VALUES + listOf(ABSENT_VALUE)))
+    }
+
+    @Test
+    fun testEmpty() {
+        val record = ArrayTrackRecord<Int>()
+        assertTrue(record.isEmpty())
+        record.add(1)
+        assertFalse(record.isEmpty())
+    }
+
+    @Test
+    fun testIterate() {
+        val record = ArrayTrackRecord<Int>()
+        record.forEach { fail("Expected nothing to iterate") }
+        TEST_VALUES.forEach { record.add(it) }
+        // zip relies on the iterator (this calls extension function Iterable#zip(Iterable))
+        record.zip(TEST_VALUES).forEach { assertEquals(it.first, it.second) }
+        // Also test reverse iteration (to test hasPrevious() and friends)
+        record.reversed().zip(TEST_VALUES.reversed()).forEach { assertEquals(it.first, it.second) }
+    }
+
+    @Test
+    fun testIteratorIsSnapshot() {
+        val record = ArrayTrackRecord<Int>()
+        TEST_VALUES.forEach { record.add(it) }
+        val iterator = record.iterator()
+        val expectedSize = record.size
+        record.add(ABSENT_VALUE)
+        record.add(ABSENT_VALUE)
+        var measuredSize = 0
+        iterator.forEach {
+            ++measuredSize
+            assertNotEquals(ABSENT_VALUE, it)
+        }
+        assertEquals(expectedSize, measuredSize)
+    }
+
+    @Test
+    fun testSublist() {
+        val record = ArrayTrackRecord<Int>()
+        TEST_VALUES.forEach { record.add(it) }
+        assertEquals(record.subList(3, record.size - 3),
+                TEST_VALUES.subList(3, TEST_VALUES.size - 3))
+    }
+
+    fun testPollReturnsImmediately(record: TrackRecord<Int>) {
+        record.add(4)
+        val elapsed = measureTimeMillis { assertEquals(4, record.poll(LONG_TIMEOUT, 0)) }
+        // Should not have waited at all, in fact.
+        assertTrue(elapsed < LONG_TIMEOUT)
+        record.add(7)
+        record.add(9)
+        // Can poll multiple times for the same position, in whatever order
+        assertEquals(9, record.poll(0, 2))
+        assertEquals(7, record.poll(Long.MAX_VALUE, 1))
+        assertEquals(9, record.poll(0, 2))
+        assertEquals(4, record.poll(0, 0))
+        assertEquals(9, record.poll(0, 2) { it > 5 })
+        assertEquals(7, record.poll(0, 0) { it > 5 })
+    }
+
+    @Test
+    fun testPollReturnsImmediately() {
+        testPollReturnsImmediately(ArrayTrackRecord())
+        testPollReturnsImmediately(ArrayTrackRecord<Int>().newReadHead())
+    }
+
+    @Test
+    fun testPollTimesOut() {
+        val record = ArrayTrackRecord<Int>()
+        var delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 0)) }
+        assertTrue(delay >= SHORT_TIMEOUT, "Delay $delay < $SHORT_TIMEOUT")
+        delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 0) { it < 10 }) }
+        assertTrue(delay >= SHORT_TIMEOUT)
+    }
+
+    @Test
+    fun testConcurrentPollDisallowed() {
+        val failures = AtomicInteger(0)
+        val readHead = ArrayTrackRecord<Int>().newReadHead()
+        val barrier = CyclicBarrier(2)
+        Thread {
+            barrier.await(LONG_TIMEOUT, TimeUnit.MILLISECONDS) // barrier 1
+            try {
+                readHead.poll(LONG_TIMEOUT)
+            } catch (e: ConcurrentModificationException) {
+                failures.incrementAndGet()
+                // Unblock the other thread
+                readHead.add(0)
+            }
+        }.start()
+        barrier.await() // barrier 1
+        try {
+            readHead.poll(LONG_TIMEOUT)
+        } catch (e: ConcurrentModificationException) {
+            failures.incrementAndGet()
+            // Unblock the other thread
+            readHead.add(0)
+        }
+        // One of the threads must have gotten an exception.
+        assertEquals(failures.get(), 1)
+    }
+
+    @Test
+    fun testPollWakesUp() {
+        val record = ArrayTrackRecord<Int>()
+        val barrier = CyclicBarrier(2)
+        Thread {
+            barrier.await(LONG_TIMEOUT, TimeUnit.MILLISECONDS) // barrier 1
+            barrier.await() // barrier 2
+            Thread.sleep(SHORT_TIMEOUT * 2)
+            record.add(31)
+        }.start()
+        barrier.await() // barrier 1
+        // Should find the element in more than SHORT_TIMEOUT but less than TEST_TIMEOUT
+        var delay = measureTimeMillis {
+            barrier.await() // barrier 2
+            assertEquals(31, record.poll(TEST_TIMEOUT, 0))
+        }
+        assertTrue(delay in SHORT_TIMEOUT..TEST_TIMEOUT)
+        // Polling for an element already added in anothe thread (pos 0) : should return immediately
+        delay = measureTimeMillis { assertEquals(31, record.poll(TEST_TIMEOUT, 0)) }
+        assertTrue(delay < TEST_TIMEOUT, "Delay $delay > $TEST_TIMEOUT")
+        // Waiting for an element that never comes
+        delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 1)) }
+        assertTrue(delay >= SHORT_TIMEOUT, "Delay $delay < $SHORT_TIMEOUT")
+        // Polling for an element that doesn't match what is already there
+        delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 0) { it < 10 }) }
+        assertTrue(delay >= SHORT_TIMEOUT)
+    }
+
+    // Just make sure the interpreter actually throws an exception when the spec
+    // does not conform to the behavior. The interpreter is just a tool to test a
+    // tool used for a tool for test, let's not have hundreds of tests for it ;
+    // if it's broken one of the tests using it will break.
+    @Test
+    fun testInterpreter() {
+        val interpretLine = __LINE__ + 2
+        try {
+            TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+                add(4) | poll(1, 0) = 5
+            """)
+            fail("This spec should have thrown")
+        } catch (e: InterpretException) {
+            assertTrue(e.cause is AssertionError)
+            assertEquals(interpretLine + 1, e.stackTrace[0].lineNumber)
+            assertTrue(e.stackTrace[0].fileName.contains(__FILE__))
+            assertTrue(e.stackTrace[0].methodName.contains("testInterpreter"))
+            assertTrue(e.stackTrace[0].methodName.contains("thread1"))
+        }
+    }
+
+    @Test
+    fun testMultipleAdds() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
+            add(2)         |                |                |
+                           | add(4)         |                |
+                           |                | add(6)         |
+                           |                |                | add(8)
+            poll(0, 0) = 2 time 0..1 | poll(0, 0) = 2 | poll(0, 0) = 2 | poll(0, 0) = 2
+            poll(0, 1) = 4 time 0..1 | poll(0, 1) = 4 | poll(0, 1) = 4 | poll(0, 1) = 4
+            poll(0, 2) = 6 time 0..1 | poll(0, 2) = 6 | poll(0, 2) = 6 | poll(0, 2) = 6
+            poll(0, 3) = 8 time 0..1 | poll(0, 3) = 8 | poll(0, 3) = 8 | poll(0, 3) = 8
+        """)
+    }
+
+    @Test
+    fun testConcurrentAdds() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
+            add(2)             | add(4)             | add(6)             | add(8)
+            add(1)             | add(3)             | add(5)             | add(7)
+            poll(0, 1) is even | poll(0, 0) is even | poll(0, 3) is even | poll(0, 2) is even
+            poll(0, 5) is odd  | poll(0, 4) is odd  | poll(0, 7) is odd  | poll(0, 6) is odd
+        """)
+    }
+
+    @Test
+    fun testMultiplePoll() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
+            add(4)         | poll(1, 0) = 4
+                           | poll(0, 1) = null time 0..1
+                           | poll(1, 1) = null time 1..2
+            sleep; add(7)  | poll(2, 1) = 7 time 1..2
+            sleep; add(18) | poll(2, 2) = 18 time 1..2
+        """)
+    }
+
+    @Test
+    fun testMultiplePollWithPredicate() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
+                     | poll(1, 0) = null          | poll(1, 0) = null
+            add(6)   | poll(1, 0) = 6             |
+            add(11)  | poll(1, 0) { > 20 } = null | poll(1, 0) { = 11 } = 11
+                     | poll(1, 0) { > 8 } = 11    |
+        """)
+    }
+
+    @Test
+    fun testMultipleReadHeads() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+                   | poll() = null | poll() = null | poll() = null
+            add(5) |               | poll() = 5    |
+                   | poll() = 5    |               |
+            add(8) | poll() = 8    | poll() = 8    |
+                   |               |               | poll() = 5
+                   |               |               | poll() = 8
+                   |               |               | poll() = null
+                   |               | poll() = null |
+        """)
+    }
+
+    @Test
+    fun testReadHeadPollWithPredicate() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+            add(5)  | poll() { < 0 } = null
+                    | poll() { > 5 } = null
+            add(10) |
+                    | poll() { = 5 } = null   // The "5" was skipped in the previous line
+            add(15) | poll() { > 8 } = 15     // The "10" was skipped in the previous line
+                    | poll(1, 0) { > 8 } = 10 // 10 is the first element after pos 0 matching > 8
+        """)
+    }
+
+    @Test
+    fun testPollImmediatelyAdvancesReadhead() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+            add(1)                  | add(2)              | add(3)   | add(4)
+            mark = 0                | poll(0) { > 3 } = 4 |          |
+            poll(0) { > 10 } = null |                     |          |
+            mark = 4                |                     |          |
+            poll() = null           |                     |          |
+        """)
+    }
+
+    @Test
+    fun testParallelReadHeads() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+            mark = 0   | mark = 0   | mark = 0   | mark = 0
+            add(2)     |            |            |
+                       | add(4)     |            |
+                       |            | add(6)     |
+                       |            |            | add(8)
+            poll() = 2 | poll() = 2 | poll() = 2 | poll() = 2
+            poll() = 4 | poll() = 4 | poll() = 4 | poll() = 4
+            poll() = 6 | poll() = 6 | poll() = 6 | mark = 2
+            poll() = 8 | poll() = 8 | mark = 3   | poll() = 6
+            mark = 4   | mark = 4   | poll() = 8 | poll() = 8
+        """)
+    }
+
+    @Test
+    fun testPeek() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+            add(2)     |            |               |
+                       | add(4)     |               |
+                       |            | add(6)        |
+                       |            |               | add(8)
+            peek() = 2 | poll() = 2 | poll() = 2    | peek() = 2
+            peek() = 2 | peek() = 4 | poll() = 4    | peek() = 2
+            peek() = 2 | peek() = 4 | peek() = 6    | poll() = 2
+            peek() = 2 | mark = 1   | mark = 2      | poll() = 4
+            mark = 0   | peek() = 4 | peek() = 6    | peek() = 6
+            poll() = 2 | poll() = 4 | poll() = 6    | poll() = 6
+            poll() = 4 | mark = 2   | poll() = 8    | peek() = 8
+            peek() = 6 | peek() = 6 | peek() = null | mark = 3
+        """)
+    }
+}
+
+private object TRTInterpreter : ConcurrentInterpreter<TrackRecord<Int>>(interpretTable) {
+    fun interpretTestSpec(spec: String, useReadHeads: Boolean) = if (useReadHeads) {
+        interpretTestSpec(spec, initial = ArrayTrackRecord(),
+                threadTransform = { (it as ArrayTrackRecord).newReadHead() })
+    } else {
+        interpretTestSpec(spec, ArrayTrackRecord())
+    }
+}
+
+/*
+ * Quick ref of supported expressions :
+ * sleep(x) : sleeps for x time units and returns Unit ; sleep alone means sleep(1)
+ * add(x) : calls and returns TrackRecord#add.
+ * poll(time, pos) [{ predicate }] : calls and returns TrackRecord#poll(x time units, pos).
+ *   Optionally, a predicate may be specified.
+ * poll() [{ predicate }] : calls and returns ReadHead#poll(1 time unit). Optionally, a predicate
+ *   may be specified.
+ * EXPR = VALUE : asserts that EXPR equals VALUE. EXPR is interpreted. VALUE can either be the
+ *   string "null" or an int. Returns Unit.
+ * EXPR time x..y : measures the time taken by EXPR and asserts it took at least x and at most
+ *   y time units.
+ * predicate must be one of "= x", "< x" or "> x".
+ */
+private val interpretTable = listOf<InterpretMatcher<TrackRecord<Int>>>(
+    // Interpret "XXX is odd" : run XXX and assert its return value is odd ("even" works too)
+    Regex("(.*)\\s+is\\s+(even|odd)") to { i, t, r ->
+        i.interpret(r.strArg(1), t).also {
+            assertEquals((it as Int) % 2, if ("even" == r.strArg(2)) 0 else 1)
+        }
+    },
+    // Interpret "add(XXX)" as TrackRecord#add(int)
+    Regex("""add\((\d+)\)""") to { i, t, r ->
+        t.add(r.intArg(1))
+    },
+    // Interpret "poll(x, y)" as TrackRecord#poll(timeout = x * INTERPRET_TIME_UNIT, pos = y)
+    // Accepts an optional {} argument for the predicate (see makePredicate for syntax)
+    Regex("""poll\((\d+),\s*(\d+)\)\s*(\{.*\})?""") to { i, t, r ->
+        t.poll(r.timeArg(1), r.intArg(2), makePredicate(r.strArg(3)))
+    },
+    // ReadHead#poll. If this throws in the cast, the code is malformed and has passed "poll()"
+    // in a test that takes a TrackRecord that is not a ReadHead. It's technically possible to get
+    // the test code to not compile instead of throw, but it's vastly more complex and this will
+    // fail 100% at runtime any test that would not have compiled.
+    Regex("""poll\((\d+)?\)\s*(\{.*\})?""") to { i, t, r ->
+        (if (r.strArg(1).isEmpty()) INTERPRET_TIME_UNIT else r.timeArg(1)).let { time ->
+            (t as ArrayTrackRecord<Int>.ReadHead).poll(time, makePredicate(r.strArg(2)))
+        }
+    },
+    // ReadHead#mark. The same remarks apply as with ReadHead#poll.
+    Regex("mark") to { i, t, _ -> (t as ArrayTrackRecord<Int>.ReadHead).mark },
+    // ReadHead#peek. The same remarks apply as with ReadHead#poll.
+    Regex("peek\\(\\)") to { i, t, _ -> (t as ArrayTrackRecord<Int>.ReadHead).peek() }
+)
+
+// Parses a { = x } or { < x } or { > x } string and returns the corresponding predicate
+// Returns an always-true predicate for empty and null arguments
+private fun makePredicate(spec: String?): (Int) -> Boolean {
+    if (spec.isNullOrEmpty()) return { true }
+    val match = Regex("""\{\s*([<>=])\s*(\d+)\s*\}""").matchEntire(spec)
+            ?: throw SyntaxException("Predicate \"${spec}\"")
+    val arg = match.intArg(2)
+    return when (match.strArg(1)) {
+        ">" -> { i -> i > arg }
+        "<" -> { i -> i < arg }
+        "=" -> { i -> i == arg }
+        else -> throw RuntimeException("How did \"${spec}\" match this regexp ?")
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java
new file mode 100644
index 0000000..11a74f2
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java
@@ -0,0 +1,376 @@
+/*
+ * 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.net.module.util.async;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+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.ignoreStubs;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.async.ReadableDataAnswer;
+
+import org.junit.After;
+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 org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BufferedFileTest {
+    @Mock EventManager mockEventManager;
+    @Mock BufferedFile.Listener mockFileListener;
+    @Mock AsyncFile mockAsyncFile;
+    @Mock ParcelFileDescriptor mockParcelFileDescriptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager));
+    }
+
+    @Test
+    public void onClosed() throws Exception {
+        final int inboundBufferSize = 1024;
+        final int outboundBufferSize = 768;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        file.onClosed(mockAsyncFile);
+
+        verify(mockFileListener).onBufferedFileClosed();
+    }
+
+    @Test
+    public void continueReadingAndClose() throws Exception {
+        final int inboundBufferSize = 1024;
+        final int outboundBufferSize = 768;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        assertEquals(inboundBufferSize, file.getInboundBufferFreeSizeForTest());
+        assertEquals(outboundBufferSize, file.getOutboundBufferFreeSize());
+
+        file.continueReading();
+        verify(mockAsyncFile).enableReadEvents(true);
+
+        file.close();
+        verify(mockAsyncFile).close();
+    }
+
+    @Test
+    public void enqueueOutboundData() throws Exception {
+        final int inboundBufferSize = 10;
+        final int outboundBufferSize = 250;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data1 = new byte[101];
+        final byte[] data2 = new byte[102];
+        data1[0] = (byte) 1;
+        data2[0] = (byte) 2;
+
+        assertEquals(0, file.getOutboundBufferSize());
+
+        final int totalLen = data1.length + data2.length;
+
+        when(mockAsyncFile.write(any(), anyInt(), anyInt())).thenReturn(0);
+        assertTrue(file.enqueueOutboundData(data1, 0, data1.length, null, 0, 0));
+        verify(mockAsyncFile).enableWriteEvents(true);
+
+        assertEquals(data1.length, file.getOutboundBufferSize());
+
+        checkAndResetMocks();
+
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+        when(mockAsyncFile.write(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
+
+        assertTrue(file.enqueueOutboundData(data2, 0, data2.length, null, 0, 0));
+
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(0, file.getOutboundBufferSize());
+
+        assertEquals(0, posCaptor.getValue().intValue());
+        assertEquals(totalLen, lenCaptor.getValue().intValue());
+        assertEquals(data1[0], arrayCaptor.getValue()[0]);
+        assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
+    }
+
+    @Test
+    public void enqueueOutboundData_combined() throws Exception {
+        final int inboundBufferSize = 10;
+        final int outboundBufferSize = 250;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data1 = new byte[101];
+        final byte[] data2 = new byte[102];
+        data1[0] = (byte) 1;
+        data2[0] = (byte) 2;
+
+        assertEquals(0, file.getOutboundBufferSize());
+
+        final int totalLen = data1.length + data2.length;
+
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+        when(mockAsyncFile.write(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
+
+        assertTrue(file.enqueueOutboundData(data1, 0, data1.length, data2, 0, data2.length));
+
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(0, file.getOutboundBufferSize());
+
+        assertEquals(0, posCaptor.getValue().intValue());
+        assertEquals(totalLen, lenCaptor.getValue().intValue());
+        assertEquals(data1[0], arrayCaptor.getValue()[0]);
+        assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
+    }
+
+    @Test
+    public void enableWriteEvents() throws Exception {
+        final int inboundBufferSize = 10;
+        final int outboundBufferSize = 250;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data1 = new byte[101];
+        final byte[] data2 = new byte[102];
+        final byte[] data3 = new byte[103];
+        data1[0] = (byte) 1;
+        data2[0] = (byte) 2;
+        data3[0] = (byte) 3;
+
+        assertEquals(0, file.getOutboundBufferSize());
+
+        // Write first 2 buffers, but fail to flush them, causing async write request.
+        final int data1And2Len = data1.length + data2.length;
+        when(mockAsyncFile.write(any(), eq(0), eq(data1And2Len))).thenReturn(0);
+        assertTrue(file.enqueueOutboundData(data1, 0, data1.length, data2, 0, data2.length));
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(data1And2Len, file.getOutboundBufferSize());
+        verify(mockAsyncFile).enableWriteEvents(true);
+
+        // Try to write 3rd buffers, which won't fit, then fail to flush.
+        when(mockAsyncFile.write(any(), eq(0), eq(data1And2Len))).thenReturn(0);
+        assertFalse(file.enqueueOutboundData(data3, 0, data3.length, null, 0, 0));
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(data1And2Len, file.getOutboundBufferSize());
+        verify(mockAsyncFile, times(2)).enableWriteEvents(true);
+
+        checkAndResetMocks();
+
+        // Simulate writeability event, and successfully flush.
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+        when(mockAsyncFile.write(arrayCaptor.capture(),
+                posCaptor.capture(), lenCaptor.capture())).thenReturn(data1And2Len);
+        file.onWriteReady(mockAsyncFile);
+        verify(mockAsyncFile).enableWriteEvents(false);
+        verify(mockFileListener).onBufferedFileOutboundSpace();
+        assertEquals(0, file.getOutboundBufferSize());
+
+        assertEquals(0, posCaptor.getValue().intValue());
+        assertEquals(data1And2Len, lenCaptor.getValue().intValue());
+        assertEquals(data1[0], arrayCaptor.getValue()[0]);
+        assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
+
+        checkAndResetMocks();
+
+        // Now write, but fail to flush the third buffer.
+        when(mockAsyncFile.write(arrayCaptor.capture(),
+                posCaptor.capture(), lenCaptor.capture())).thenReturn(0);
+        assertTrue(file.enqueueOutboundData(data3, 0, data3.length, null, 0, 0));
+        verify(mockAsyncFile).enableWriteEvents(true);
+        assertEquals(data3.length, file.getOutboundBufferSize());
+
+        assertEquals(data1And2Len, posCaptor.getValue().intValue());
+        assertEquals(outboundBufferSize - data1And2Len, lenCaptor.getValue().intValue());
+        assertEquals(data3[0], arrayCaptor.getValue()[data1And2Len]);
+    }
+
+    @Test
+    public void read() throws Exception {
+        final int inboundBufferSize = 250;
+        final int outboundBufferSize = 10;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data1 = new byte[101];
+        final byte[] data2 = new byte[102];
+        data1[0] = (byte) 1;
+        data2[0] = (byte) 2;
+
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data1, data2);
+        final ReadableByteBuffer inboundBuffer = file.getInboundBuffer();
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        file.onReadReady(mockAsyncFile);
+        verify(mockAsyncFile).enableReadEvents(true);
+        verify(mockFileListener).onBufferedFileInboundData(eq(data1.length + data2.length));
+
+        assertEquals(0, file.getOutboundBufferSize());
+        assertEquals(data1.length + data2.length, inboundBuffer.size());
+        assertEquals((byte) 1, inboundBuffer.peek(0));
+        assertEquals((byte) 2, inboundBuffer.peek(data1.length));
+    }
+
+    @Test
+    public void enableReadEvents() throws Exception {
+        final int inboundBufferSize = 250;
+        final int outboundBufferSize = 10;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data1 = new byte[101];
+        final byte[] data2 = new byte[102];
+        final byte[] data3 = new byte[103];
+        data1[0] = (byte) 1;
+        data2[0] = (byte) 2;
+        data3[0] = (byte) 3;
+
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data1, data2, data3);
+        final ReadableByteBuffer inboundBuffer = file.getInboundBuffer();
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        file.onReadReady(mockAsyncFile);
+        verify(mockAsyncFile).enableReadEvents(false);
+        verify(mockFileListener).onBufferedFileInboundData(eq(inboundBufferSize));
+
+        assertEquals(0, file.getOutboundBufferSize());
+        assertEquals(inboundBufferSize, inboundBuffer.size());
+        assertEquals((byte) 1, inboundBuffer.peek(0));
+        assertEquals((byte) 2, inboundBuffer.peek(data1.length));
+        assertEquals((byte) 3, inboundBuffer.peek(data1.length + data2.length));
+
+        checkAndResetMocks();
+
+        // Cannot enable read events since the buffer is full.
+        file.continueReading();
+
+        checkAndResetMocks();
+
+        final byte[] tmp = new byte[inboundBufferSize];
+        inboundBuffer.readBytes(tmp, 0, data1.length);
+        assertEquals(inboundBufferSize - data1.length, inboundBuffer.size());
+
+        file.continueReading();
+
+        inboundBuffer.readBytes(tmp, 0, data2.length);
+        assertEquals(inboundBufferSize - data1.length - data2.length, inboundBuffer.size());
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        file.onReadReady(mockAsyncFile);
+        verify(mockAsyncFile, times(2)).enableReadEvents(true);
+        verify(mockFileListener).onBufferedFileInboundData(
+            eq(data1.length + data2.length + data3.length - inboundBufferSize));
+
+        assertEquals(data3.length, inboundBuffer.size());
+        assertEquals((byte) 3, inboundBuffer.peek(0));
+    }
+
+    @Test
+    public void shutdownReading() throws Exception {
+        final int inboundBufferSize = 250;
+        final int outboundBufferSize = 10;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data = new byte[100];
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+
+        file.shutdownReading();
+        file.onReadReady(mockAsyncFile);
+
+        verify(mockAsyncFile).enableReadEvents(false);
+
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(data.length, dataAnswer.getRemainingSize());
+    }
+
+    @Test
+    public void shutdownReading_inCallback() throws Exception {
+        final int inboundBufferSize = 250;
+        final int outboundBufferSize = 10;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data = new byte[100];
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+
+        doAnswer(new Answer() {
+            @Override public Object answer(InvocationOnMock invocation) {
+                file.shutdownReading();
+                return null;
+            }}).when(mockFileListener).onBufferedFileInboundData(anyInt());
+
+        file.onReadReady(mockAsyncFile);
+
+        verify(mockAsyncFile).enableReadEvents(false);
+
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(0, dataAnswer.getRemainingSize());
+    }
+
+    private void checkAndResetMocks() {
+        verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager,
+            mockParcelFileDescriptor));
+        reset(mockFileListener, mockAsyncFile, mockEventManager);
+    }
+
+    private BufferedFile createFile(
+            int inboundBufferSize, int outboundBufferSize) throws Exception {
+        when(mockEventManager.registerFile(any(), any())).thenReturn(mockAsyncFile);
+        return BufferedFile.create(
+            mockEventManager,
+            FileHandle.fromFileDescriptor(mockParcelFileDescriptor),
+            mockFileListener,
+            inboundBufferSize,
+            outboundBufferSize);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/async/CircularByteBufferTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/async/CircularByteBufferTest.java
new file mode 100644
index 0000000..01abee2
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/async/CircularByteBufferTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.net.module.util.async;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CircularByteBufferTest {
+    @Test
+    public void writeBytes() {
+        final int capacity = 23;
+        CircularByteBuffer buffer = new CircularByteBuffer(capacity);
+        assertEquals(0, buffer.size());
+        assertEquals(0, buffer.getDirectReadSize());
+        assertEquals(capacity, buffer.freeSize());
+        assertEquals(capacity, buffer.getDirectWriteSize());
+
+        final byte[] writeBuffer = new byte[15];
+        buffer.writeBytes(writeBuffer, 0, writeBuffer.length);
+
+        assertEquals(writeBuffer.length, buffer.size());
+        assertEquals(writeBuffer.length, buffer.getDirectReadSize());
+        assertEquals(capacity - writeBuffer.length, buffer.freeSize());
+        assertEquals(capacity - writeBuffer.length, buffer.getDirectWriteSize());
+
+        buffer.clear();
+        assertEquals(0, buffer.size());
+        assertEquals(0, buffer.getDirectReadSize());
+        assertEquals(capacity, buffer.freeSize());
+        assertEquals(capacity, buffer.getDirectWriteSize());
+    }
+
+    @Test
+    public void writeBytes_withRollover() {
+        doTestReadWriteWithRollover(new BufferAccessor(false, false));
+    }
+
+    @Test
+    public void writeBytes_withFullBuffer() {
+        doTestReadWriteWithFullBuffer(new BufferAccessor(false, false));
+    }
+
+    @Test
+    public void directWriteBytes_withRollover() {
+        doTestReadWriteWithRollover(new BufferAccessor(true, true));
+    }
+
+    @Test
+    public void directWriteBytes_withFullBuffer() {
+        doTestReadWriteWithFullBuffer(new BufferAccessor(true, true));
+    }
+
+    private void doTestReadWriteWithFullBuffer(BufferAccessor accessor) {
+        CircularByteBuffer buffer = doTestReadWrite(accessor, 20, 5, 4);
+
+        assertEquals(0, buffer.size());
+        assertEquals(20, buffer.freeSize());
+    }
+
+    private void doTestReadWriteWithRollover(BufferAccessor accessor) {
+        // All buffer sizes are prime numbers to ensure that some read or write
+        // operations will roll over the end of the internal buffer.
+        CircularByteBuffer buffer = doTestReadWrite(accessor, 31, 13, 7);
+
+        assertNotEquals(0, buffer.size());
+    }
+
+    private CircularByteBuffer doTestReadWrite(BufferAccessor accessor,
+            final int capacity, final int writeLen, final int readLen) {
+        CircularByteBuffer buffer = new CircularByteBuffer(capacity);
+
+        final byte[] writeBuffer = new byte[writeLen + 2];
+        final byte[] peekBuffer = new byte[readLen + 2];
+        final byte[] readBuffer = new byte[readLen + 2];
+
+        final int numIterations = 1011;
+        final int maxRemaining = readLen - 1;
+
+        int currentWriteSymbol = 0;
+        int expectedReadSymbol = 0;
+        int expectedSize = 0;
+        int totalWritten = 0;
+        int totalRead = 0;
+
+        for (int i = 0; i < numIterations; i++) {
+            // Fill in with write buffers as much as possible.
+            while (buffer.freeSize() >= writeLen) {
+                currentWriteSymbol = fillTestBytes(writeBuffer, 1, writeLen, currentWriteSymbol);
+                accessor.writeBytes(buffer, writeBuffer, 1, writeLen);
+
+                expectedSize += writeLen;
+                totalWritten += writeLen;
+                assertEquals(expectedSize, buffer.size());
+                assertEquals(capacity - expectedSize, buffer.freeSize());
+            }
+
+            // Keep reading into read buffers while there's still data.
+            while (buffer.size() >= readLen) {
+                peekBuffer[1] = 0;
+                peekBuffer[2] = 0;
+                buffer.peekBytes(2, peekBuffer, 3, readLen - 2);
+                assertEquals(0, peekBuffer[1]);
+                assertEquals(0, peekBuffer[2]);
+
+                peekBuffer[2] = buffer.peek(1);
+
+                accessor.readBytes(buffer, readBuffer, 1, readLen);
+                peekBuffer[1] = readBuffer[1];
+
+                expectedReadSymbol = checkTestBytes(
+                    readBuffer, 1, readLen, expectedReadSymbol, totalRead);
+
+                assertArrayEquals(peekBuffer, readBuffer);
+
+                expectedSize -= readLen;
+                totalRead += readLen;
+                assertEquals(expectedSize, buffer.size());
+                assertEquals(capacity - expectedSize, buffer.freeSize());
+            }
+
+            if (buffer.size() > maxRemaining) {
+                fail("Too much data remaining: " + buffer.size());
+            }
+        }
+
+        final int maxWritten = capacity * numIterations;
+        final int minWritten = maxWritten / 2;
+        if (totalWritten < minWritten || totalWritten > maxWritten
+                || (totalWritten - totalRead) > maxRemaining) {
+            fail("Unexpected counts: read=" + totalRead + ", written=" + totalWritten
+                    + ", minWritten=" + minWritten + ", maxWritten=" + maxWritten);
+        }
+
+        return buffer;
+    }
+
+    @Test
+    public void readBytes_overflow() {
+        CircularByteBuffer buffer = new CircularByteBuffer(23);
+
+        final byte[] dataBuffer = new byte[15];
+        buffer.writeBytes(dataBuffer, 0, dataBuffer.length - 2);
+
+        try {
+            buffer.readBytes(dataBuffer, 0, dataBuffer.length);
+            assertTrue(false);
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        assertEquals(13, buffer.size());
+        assertEquals(10, buffer.freeSize());
+    }
+
+    @Test
+    public void writeBytes_overflow() {
+        CircularByteBuffer buffer = new CircularByteBuffer(23);
+
+        final byte[] dataBuffer = new byte[15];
+        buffer.writeBytes(dataBuffer, 0, dataBuffer.length);
+
+        try {
+            buffer.writeBytes(dataBuffer, 0, dataBuffer.length);
+            assertTrue(false);
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        assertEquals(15, buffer.size());
+        assertEquals(8, buffer.freeSize());
+    }
+
+    private static int fillTestBytes(byte[] buffer, int pos, int len, int startValue) {
+        for (int i = 0; i < len; i++) {
+            buffer[pos + i] = (byte) (startValue & 0xFF);
+            startValue = (startValue + 1) % 256;
+        }
+        return startValue;
+    }
+
+    private static int checkTestBytes(
+            byte[] buffer, int pos, int len, int startValue, int totalRead) {
+        for (int i = 0; i < len; i++) {
+            byte expectedValue = (byte) (startValue & 0xFF);
+            if (expectedValue != buffer[pos + i]) {
+                fail("Unexpected byte=" + (((int) buffer[pos + i]) & 0xFF)
+                        + ", expected=" + (((int) expectedValue) & 0xFF)
+                        + ", pos=" + (totalRead + i));
+            }
+            startValue = (startValue + 1) % 256;
+        }
+        return startValue;
+    }
+
+    private static final class BufferAccessor {
+        private final boolean mDirectRead;
+        private final boolean mDirectWrite;
+
+        BufferAccessor(boolean directRead, boolean directWrite) {
+            mDirectRead = directRead;
+            mDirectWrite = directWrite;
+        }
+
+        void writeBytes(CircularByteBuffer buffer, byte[] src, int pos, int len) {
+            if (mDirectWrite) {
+                while (len > 0) {
+                    if (buffer.getDirectWriteSize() == 0) {
+                        fail("Direct write size is zero: free=" + buffer.freeSize()
+                                + ", size=" + buffer.size());
+                    }
+                    int copyLen = Math.min(len, buffer.getDirectWriteSize());
+                    System.arraycopy(src, pos, buffer.getDirectWriteBuffer(),
+                        buffer.getDirectWritePos(), copyLen);
+                    buffer.accountForDirectWrite(copyLen);
+                    len -= copyLen;
+                    pos += copyLen;
+                }
+            } else {
+                buffer.writeBytes(src, pos, len);
+            }
+        }
+
+        void readBytes(CircularByteBuffer buffer, byte[] dst, int pos, int len) {
+            if (mDirectRead) {
+                while (len > 0) {
+                    if (buffer.getDirectReadSize() == 0) {
+                        fail("Direct read size is zero: free=" + buffer.freeSize()
+                                + ", size=" + buffer.size());
+                    }
+                    int copyLen = Math.min(len, buffer.getDirectReadSize());
+                    System.arraycopy(
+                        buffer.getDirectReadBuffer(), buffer.getDirectReadPos(), dst, pos, copyLen);
+                    buffer.accountForDirectRead(copyLen);
+                    len -= copyLen;
+                    pos += copyLen;
+                }
+            } else {
+                buffer.readBytes(dst, pos, len);
+            }
+        }
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java
new file mode 100644
index 0000000..7ee376c
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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.ip;
+
+import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static com.android.net.module.util.netlink.ConntrackMessage.Tuple;
+import static com.android.net.module.util.netlink.ConntrackMessage.TupleIpv4;
+import static com.android.net.module.util.netlink.ConntrackMessage.TupleProto;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.net.InetAddresses;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkUtils;
+
+import libcore.util.HexEncoding;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.net.Inet4Address;
+
+/**
+ * Tests for ConntrackMonitor.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConntrackMonitorTest {
+    private static final long TIMEOUT_MS = 10_000L;
+
+    @Mock private SharedLog mLog;
+    @Mock private ConntrackMonitor.ConntrackEventConsumer mConsumer;
+
+    private final HandlerThread mHandlerThread = new HandlerThread(
+            ConntrackMonitorTest.class.getSimpleName());
+
+    // Late init since the handler thread has been started.
+    private Handler mHandler;
+    private TestConntrackMonitor mConntrackMonitor;
+
+    // A version of [ConntrackMonitor] that reads packets from the socket pair, and instead
+    // allows the test to write test packets to the socket pair via [sendMessage].
+    private class TestConntrackMonitor extends ConntrackMonitor {
+        TestConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
+                @NonNull ConntrackEventConsumer cb) {
+            super(h, log, cb);
+
+            mReadFd = new FileDescriptor();
+            mWriteFd = new FileDescriptor();
+            try {
+                Os.socketpair(AF_UNIX, SOCK_DGRAM, 0, mWriteFd, mReadFd);
+            } catch (ErrnoException e) {
+                fail("Could not create socket pair: " + e);
+            }
+        }
+
+        @Override
+        protected FileDescriptor createFd() {
+            return mReadFd;
+        }
+
+        private void sendMessage(byte[] msg) {
+            mHandler.post(() -> {
+                try {
+                    NetlinkUtils.sendMessage(mWriteFd, msg, 0 /* offset */, msg.length,
+                                              TIMEOUT_MS);
+                } catch (ErrnoException | InterruptedIOException e) {
+                    fail("Unable to send netfilter message: " + e);
+                }
+            });
+        }
+
+        private final FileDescriptor mReadFd;
+        private final FileDescriptor mWriteFd;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        // ConntrackMonitor needs to be started from the handler thread.
+        final ConditionVariable initDone = new ConditionVariable();
+        mHandler.post(() -> {
+            TestConntrackMonitor m = new TestConntrackMonitor(mHandler, mLog, mConsumer);
+            m.start();
+            mConntrackMonitor = m;
+
+            initDone.open();
+        });
+        if (!initDone.block(TIMEOUT_MS)) {
+            fail("... init monitor timed-out after " + TIMEOUT_MS + "ms");
+        }
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mHandlerThread.quitSafely();
+    }
+
+    public static final String CT_V4NEW_TCP_HEX =
+            // CHECKSTYLE:OFF IndentationCheck
+            // struct nlmsghdr
+            "8C000000" +      // length = 140
+            "0001" +          // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
+            "0006" +          // flags = NLM_F_CREATE (1 << 10) | NLM_F_EXCL (1 << 9)
+            "00000000" +      // seqno = 0
+            "00000000" +      // pid = 0
+            // struct nfgenmsg
+            "02" +            // nfgen_family = AF_INET
+            "00" +            // version = NFNETLINK_V0
+            "1234" +          // res_id = 0x1234 (big endian)
+             // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 C0A8500C" +  // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+                    "0800 0200 8C700874" +  // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 F3F1 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0280" +          // nla_type = nested CTA_TUPLE_REPLY
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 8C700874" +  // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+                    "0800 0200 6451B301" +  // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 01BB 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=443 (big endian)
+                    "0600 0300 F3F1 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0300" +          // nla_type = CTA_STATUS
+            "0000019e" +      // nla_value = 0b110011110 (big endian)
+                              // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+                              // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+                              // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0700" +          // nla_type = CTA_TIMEOUT
+            "00000078";       // nla_value = 120 (big endian)
+            // CHECKSTYLE:ON IndentationCheck
+    public static final byte[] CT_V4NEW_TCP_BYTES =
+            HexEncoding.decode(CT_V4NEW_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @NonNull
+    private ConntrackEvent makeTestConntrackEvent(short msgType, int status, int timeoutSec) {
+        final Inet4Address privateIp =
+                (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
+        final Inet4Address remoteIp =
+                (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
+        final Inet4Address publicIp =
+                (Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
+
+        return new ConntrackEvent(
+                (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
+                new Tuple(new TupleIpv4(privateIp, remoteIp),
+                        new TupleProto((byte) IPPROTO_TCP, (short) 62449, (short) 443)),
+                new Tuple(new TupleIpv4(remoteIp, publicIp),
+                        new TupleProto((byte) IPPROTO_TCP, (short) 443, (short) 62449)),
+                status,
+                timeoutSec);
+    }
+
+    @Test
+    public void testConntrackEventNew() throws Exception {
+        final ConntrackEvent expectedEvent = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+                0x19e /* status */, 120 /* timeoutSec */);
+        mConntrackMonitor.sendMessage(CT_V4NEW_TCP_BYTES);
+        verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+    }
+
+    @Test
+    public void testConntrackEventEquals() {
+        final ConntrackEvent event1 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+                5678 /* timeoutSec*/);
+        final ConntrackEvent event2 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+                5678 /* timeoutSec*/);
+        assertEquals(event1, event2);
+    }
+
+    @Test
+    public void testConntrackEventNotEquals() {
+        final ConntrackEvent e = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+                5678 /* timeoutSec*/);
+
+        final ConntrackEvent typeNotEqual = new ConntrackEvent((short) (e.msgType + 1) /* diff */,
+                e.tupleOrig, e.tupleReply, e.status, e.timeoutSec);
+        assertNotEquals(e, typeNotEqual);
+
+        final ConntrackEvent tupleOrigNotEqual = new ConntrackEvent(e.msgType,
+                null /* diff */, e.tupleReply, e.status, e.timeoutSec);
+        assertNotEquals(e, tupleOrigNotEqual);
+
+        final ConntrackEvent tupleReplyNotEqual = new ConntrackEvent(e.msgType,
+                e.tupleOrig, null /* diff */, e.status, e.timeoutSec);
+        assertNotEquals(e, tupleReplyNotEqual);
+
+        final ConntrackEvent statusNotEqual = new ConntrackEvent(e.msgType,
+                e.tupleOrig, e.tupleReply, e.status + 1 /* diff */, e.timeoutSec);
+        assertNotEquals(e, statusNotEqual);
+
+        final ConntrackEvent timeoutSecNotEqual = new ConntrackEvent(e.msgType,
+                e.tupleOrig, e.tupleReply, e.status, e.timeoutSec + 1 /* diff */);
+        assertNotEquals(e, timeoutSecNotEqual);
+    }
+
+    @Test
+    public void testToString() {
+        final ConntrackEvent event = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+                0x198 /* status */, 120 /* timeoutSec */);
+        final String expected = ""
+                + "ConntrackEvent{"
+                + "msg_type{IPCTNL_MSG_CT_NEW}, "
+                + "tuple_orig{Tuple{IPPROTO_TCP: 192.168.80.12:62449 -> 140.112.8.116:443}}, "
+                + "tuple_reply{Tuple{IPPROTO_TCP: 140.112.8.116:443 -> 100.81.179.1:62449}}, "
+                + "status{408(IPS_CONFIRMED|IPS_SRC_NAT|IPS_SRC_NAT_DONE|IPS_DST_NAT_DONE)}, "
+                + "timeout_sec{120}}";
+        assertEquals(expected, event.toString());
+    }
+
+    public static final String CT_V4DELETE_TCP_HEX =
+            // CHECKSTYLE:OFF IndentationCheck
+            // struct nlmsghdr
+            "84000000" +      // length = 132
+            "0201" +          // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_DELETE (2)
+            "0000" +          // flags = 0
+            "00000000" +      // seqno = 0
+            "00000000" +      // pid = 0
+            // struct nfgenmsg
+            "02" +            // nfgen_family  = AF_INET
+            "00" +            // version = NFNETLINK_V0
+            "1234" +          // res_id = 0x1234 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 C0A8500C" +  // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+                    "0800 0200 8C700874" +  // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 F3F1 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=433 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0280" +          // nla_type = nested CTA_TUPLE_REPLY
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 8C700874" +  // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+                    "0800 0200 6451B301" +  // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 01BB 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=433 (big endian)
+                    "0600 0300 F3F1 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0300" +          // nla_type = CTA_STATUS
+            "0000039E";       // nla_value = 0b1110011110 (big endian)
+                              // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+                              // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+                              // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8) |
+                              // IPS_DYING (1 << 9)
+            // CHECKSTYLE:ON IndentationCheck
+    public static final byte[] CT_V4DELETE_TCP_BYTES =
+            HexEncoding.decode(CT_V4DELETE_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @Test
+    public void testConntrackEventDelete() throws Exception {
+        final ConntrackEvent expectedEvent =
+                makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, 0x39e /* status */,
+                        0 /* timeoutSec (absent) */);
+        mConntrackMonitor.sendMessage(CT_V4DELETE_TCP_BYTES);
+        verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java
new file mode 100644
index 0000000..dea667d
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.ip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.SharedLog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InterfaceControllerTest {
+    private static final String TEST_IFACE = "testif";
+    private static final String TEST_IPV4_ADDR = "192.168.123.28";
+    private static final int TEST_PREFIXLENGTH = 31;
+
+    @Mock private INetd mNetd;
+    @Mock private SharedLog mLog;
+    @Captor private ArgumentCaptor<InterfaceConfigurationParcel> mConfigCaptor;
+
+    private InterfaceController mController;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mController = new InterfaceController(TEST_IFACE, mNetd, mLog);
+
+        doNothing().when(mNetd).interfaceSetCfg(mConfigCaptor.capture());
+    }
+
+    @Test
+    public void testSetIPv4Address() throws Exception {
+        mController.setIPv4Address(
+                new LinkAddress(InetAddresses.parseNumericAddress(TEST_IPV4_ADDR),
+                        TEST_PREFIXLENGTH));
+        verify(mNetd, times(1)).interfaceSetCfg(any());
+        final InterfaceConfigurationParcel parcel = mConfigCaptor.getValue();
+        assertEquals(TEST_IFACE, parcel.ifName);
+        assertEquals(TEST_IPV4_ADDR, parcel.ipv4Addr);
+        assertEquals(TEST_PREFIXLENGTH, parcel.prefixLength);
+        assertEquals("", parcel.hwAddr);
+        assertArrayEquals(new String[0], parcel.flags);
+    }
+
+    @Test
+    public void testClearIPv4Address() throws Exception {
+        mController.clearIPv4Address();
+        verify(mNetd, times(1)).interfaceSetCfg(any());
+        final InterfaceConfigurationParcel parcel = mConfigCaptor.getValue();
+        assertEquals(TEST_IFACE, parcel.ifName);
+        assertEquals("0.0.0.0", parcel.ipv4Addr);
+        assertEquals(0, parcel.prefixLength);
+        assertEquals("", parcel.hwAddr);
+        assertArrayEquals(new String[0], parcel.flags);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/ConntrackMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/ConntrackMessageTest.java
new file mode 100644
index 0000000..f02b4cb
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/ConntrackMessageTest.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+import static com.android.net.module.util.netlink.NetlinkConstants.NFNL_SUBSYS_CTNETLINK;
+
+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.Assume.assumeTrue;
+
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConntrackMessageTest {
+    private static final boolean USING_LE = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN);
+
+    private short makeCtType(short msgType) {
+        return (short) (NFNL_SUBSYS_CTNETLINK << 8 | (byte) msgType);
+    }
+
+    // Example 1: TCP (192.168.43.209, 44333) -> (23.211.13.26, 443)
+    public static final String CT_V4UPDATE_TCP_HEX =
+            // struct nlmsghdr
+            "50000000" +      // length = 80
+            "0001" +          // type = (1 << 8) | 0
+            "0501" +          // flags
+            "01000000" +      // seqno = 1
+            "00000000" +      // pid = 0
+            // struct nfgenmsg
+            "02" +            // nfgen_family  = AF_INET
+            "00" +            // version = NFNETLINK_V0
+            "0000" +          // res_id
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 C0A82BD1" +  // nla_type=CTA_IP_V4_SRC, ip=192.168.43.209
+                    "0800 0200 17D30D1A" +  // nla_type=CTA_IP_V4_DST, ip=23.211.13.26
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=6
+                    "0600 0200 AD2D 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=44333 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0700" +          // nla_type = CTA_TIMEOUT
+            "00069780";       // nla_value = 432000 (big endian)
+    public static final byte[] CT_V4UPDATE_TCP_BYTES =
+            HexEncoding.decode(CT_V4UPDATE_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    private byte[] makeIPv4TimeoutUpdateRequestTcp() throws Exception {
+        return ConntrackMessage.newIPv4TimeoutUpdateRequest(
+                OsConstants.IPPROTO_TCP,
+                (Inet4Address) InetAddress.getByName("192.168.43.209"), 44333,
+                (Inet4Address) InetAddress.getByName("23.211.13.26"), 443,
+                432000);
+    }
+
+    // Example 2: UDP (100.96.167.146, 37069) -> (216.58.197.10, 443)
+    public static final String CT_V4UPDATE_UDP_HEX =
+            // struct nlmsghdr
+            "50000000" +      // length = 80
+            "0001" +          // type = (1 << 8) | 0
+            "0501" +          // flags
+            "01000000" +      // seqno = 1
+            "00000000" +      // pid = 0
+            // struct nfgenmsg
+            "02" +            // nfgen_family  = AF_INET
+            "00" +            // version = NFNETLINK_V0
+            "0000" +          // res_id
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 6460A792" +  // nla_type=CTA_IP_V4_SRC, ip=100.96.167.146
+                    "0800 0200 D83AC50A" +  // nla_type=CTA_IP_V4_DST, ip=216.58.197.10
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 11 000000" +  // nla_type=CTA_PROTO_NUM, proto=17
+                    "0600 0200 90CD 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=37069 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0700" +          // nla_type = CTA_TIMEOUT
+            "000000B4";       // nla_value = 180 (big endian)
+    public static final byte[] CT_V4UPDATE_UDP_BYTES =
+            HexEncoding.decode(CT_V4UPDATE_UDP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    private byte[] makeIPv4TimeoutUpdateRequestUdp() throws Exception {
+        return ConntrackMessage.newIPv4TimeoutUpdateRequest(
+                OsConstants.IPPROTO_UDP,
+                (Inet4Address) InetAddress.getByName("100.96.167.146"), 37069,
+                (Inet4Address) InetAddress.getByName("216.58.197.10"), 443,
+                180);
+    }
+
+    @Test
+    public void testConntrackMakeIPv4TcpTimeoutUpdate() throws Exception {
+        assumeTrue(USING_LE);
+
+        final byte[] tcp = makeIPv4TimeoutUpdateRequestTcp();
+        assertArrayEquals(CT_V4UPDATE_TCP_BYTES, tcp);
+    }
+
+    @Test
+    public void testConntrackParseIPv4TcpTimeoutUpdate() throws Exception {
+        assumeTrue(USING_LE);
+
+        final byte[] tcp = makeIPv4TimeoutUpdateRequestTcp();
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(tcp);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
+        assertNotNull(msg);
+        assertTrue(msg instanceof ConntrackMessage);
+        final ConntrackMessage conntrackMessage = (ConntrackMessage) msg;
+
+        final StructNlMsgHdr hdr = conntrackMessage.getHeader();
+        assertNotNull(hdr);
+        assertEquals(80, hdr.nlmsg_len);
+        assertEquals(makeCtType(IPCTNL_MSG_CT_NEW), hdr.nlmsg_type);
+        assertEquals((short) (StructNlMsgHdr.NLM_F_REPLACE | StructNlMsgHdr.NLM_F_REQUEST
+                | StructNlMsgHdr.NLM_F_ACK), hdr.nlmsg_flags);
+        assertEquals(1, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructNfGenMsg nfmsgHdr = conntrackMessage.nfGenMsg;
+        assertNotNull(nfmsgHdr);
+        assertEquals((byte) OsConstants.AF_INET, nfmsgHdr.nfgen_family);
+        assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
+        assertEquals((short) 0, nfmsgHdr.res_id);
+
+        assertEquals(InetAddress.parseNumericAddress("192.168.43.209"),
+                conntrackMessage.tupleOrig.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("23.211.13.26"),
+                conntrackMessage.tupleOrig.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_TCP, conntrackMessage.tupleOrig.protoNum);
+        assertEquals((short) 44333, conntrackMessage.tupleOrig.srcPort);
+        assertEquals((short) 443, conntrackMessage.tupleOrig.dstPort);
+
+        assertNull(conntrackMessage.tupleReply);
+
+        assertEquals(0 /* absent */, conntrackMessage.status);
+        assertEquals(432000, conntrackMessage.timeoutSec);
+    }
+
+    @Test
+    public void testConntrackMakeIPv4UdpTimeoutUpdate() throws Exception {
+        assumeTrue(USING_LE);
+
+        final byte[] udp = makeIPv4TimeoutUpdateRequestUdp();
+        assertArrayEquals(CT_V4UPDATE_UDP_BYTES, udp);
+    }
+
+    @Test
+    public void testConntrackParseIPv4UdpTimeoutUpdate() throws Exception {
+        assumeTrue(USING_LE);
+
+        final byte[] udp = makeIPv4TimeoutUpdateRequestUdp();
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(udp);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
+        assertNotNull(msg);
+        assertTrue(msg instanceof ConntrackMessage);
+        final ConntrackMessage conntrackMessage = (ConntrackMessage) msg;
+
+        final StructNlMsgHdr hdr = conntrackMessage.getHeader();
+        assertNotNull(hdr);
+        assertEquals(80, hdr.nlmsg_len);
+        assertEquals(makeCtType(IPCTNL_MSG_CT_NEW), hdr.nlmsg_type);
+        assertEquals((short) (StructNlMsgHdr.NLM_F_REPLACE | StructNlMsgHdr.NLM_F_REQUEST
+                | StructNlMsgHdr.NLM_F_ACK), hdr.nlmsg_flags);
+        assertEquals(1, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructNfGenMsg nfmsgHdr = conntrackMessage.nfGenMsg;
+        assertNotNull(nfmsgHdr);
+        assertEquals((byte) OsConstants.AF_INET, nfmsgHdr.nfgen_family);
+        assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
+        assertEquals((short) 0, nfmsgHdr.res_id);
+
+        assertEquals(InetAddress.parseNumericAddress("100.96.167.146"),
+                conntrackMessage.tupleOrig.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("216.58.197.10"),
+                conntrackMessage.tupleOrig.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_UDP, conntrackMessage.tupleOrig.protoNum);
+        assertEquals((short) 37069, conntrackMessage.tupleOrig.srcPort);
+        assertEquals((short) 443, conntrackMessage.tupleOrig.dstPort);
+
+        assertNull(conntrackMessage.tupleReply);
+
+        assertEquals(0 /* absent */, conntrackMessage.status);
+        assertEquals(180, conntrackMessage.timeoutSec);
+    }
+
+    public static final String CT_V4NEW_TCP_HEX =
+            // CHECKSTYLE:OFF IndentationCheck
+            // struct nlmsghdr
+            "8C000000" +      // length = 140
+            "0001" +          // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
+            "0006" +          // flags = NLM_F_CREATE (1 << 10) | NLM_F_EXCL (1 << 9)
+            "00000000" +      // seqno = 0
+            "00000000" +      // pid = 0
+            // struct nfgenmsg
+            "02" +            // nfgen_family = AF_INET
+            "00" +            // version = NFNETLINK_V0
+            "1234" +          // res_id = 0x1234 (big endian)
+             // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 C0A8500C" +  // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+                    "0800 0200 8C700874" +  // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 F3F1 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0280" +          // nla_type = nested CTA_TUPLE_REPLY
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 8C700874" +  // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+                    "0800 0200 6451B301" +  // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 01BB 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=443 (big endian)
+                    "0600 0300 F3F1 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0300" +          // nla_type = CTA_STATUS
+            "00000198" +      // nla_value = 0b110011000 (big endian)
+                              // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+                              // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0700" +          // nla_type = CTA_TIMEOUT
+            "00000078";       // nla_value = 120 (big endian)
+            // CHECKSTYLE:ON IndentationCheck
+    public static final byte[] CT_V4NEW_TCP_BYTES =
+            HexEncoding.decode(CT_V4NEW_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @Test
+    public void testParseCtNew() {
+        assumeTrue(USING_LE);
+
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(CT_V4NEW_TCP_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
+        assertNotNull(msg);
+        assertTrue(msg instanceof ConntrackMessage);
+        final ConntrackMessage conntrackMessage = (ConntrackMessage) msg;
+
+        final StructNlMsgHdr hdr = conntrackMessage.getHeader();
+        assertNotNull(hdr);
+        assertEquals(140, hdr.nlmsg_len);
+        assertEquals(makeCtType(IPCTNL_MSG_CT_NEW), hdr.nlmsg_type);
+        assertEquals((short) (StructNlMsgHdr.NLM_F_CREATE | StructNlMsgHdr.NLM_F_EXCL),
+                hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructNfGenMsg nfmsgHdr = conntrackMessage.nfGenMsg;
+        assertNotNull(nfmsgHdr);
+        assertEquals((byte) OsConstants.AF_INET, nfmsgHdr.nfgen_family);
+        assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
+        assertEquals((short) 0x1234, nfmsgHdr.res_id);
+
+        assertEquals(InetAddress.parseNumericAddress("192.168.80.12"),
+                conntrackMessage.tupleOrig.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("140.112.8.116"),
+                conntrackMessage.tupleOrig.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_TCP, conntrackMessage.tupleOrig.protoNum);
+        assertEquals((short) 62449, conntrackMessage.tupleOrig.srcPort);
+        assertEquals((short) 443, conntrackMessage.tupleOrig.dstPort);
+
+        assertEquals(InetAddress.parseNumericAddress("140.112.8.116"),
+                conntrackMessage.tupleReply.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("100.81.179.1"),
+                conntrackMessage.tupleReply.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_TCP, conntrackMessage.tupleReply.protoNum);
+        assertEquals((short) 443, conntrackMessage.tupleReply.srcPort);
+        assertEquals((short) 62449, conntrackMessage.tupleReply.dstPort);
+
+        assertEquals(0x198, conntrackMessage.status);
+        assertEquals(120, conntrackMessage.timeoutSec);
+    }
+
+    @Test
+    public void testParseTruncation() {
+        assumeTrue(USING_LE);
+
+        // Expect no crash while parsing the truncated message which has been truncated to every
+        // length between 0 and its full length - 1.
+        for (int len = 0; len < CT_V4NEW_TCP_BYTES.length; len++) {
+            final byte[] truncated = Arrays.copyOfRange(CT_V4NEW_TCP_BYTES, 0, len);
+
+            final ByteBuffer byteBuffer = ByteBuffer.wrap(truncated);
+            byteBuffer.order(ByteOrder.nativeOrder());
+            final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer,
+                    OsConstants.NETLINK_NETFILTER);
+        }
+    }
+
+    @Test
+    public void testParseTruncationWithInvalidByte() {
+        assumeTrue(USING_LE);
+
+        // Expect no crash while parsing the message which is truncated by invalid bytes. The
+        // message has been truncated to every length between 0 and its full length - 1.
+        for (byte invalid : new byte[]{(byte) 0x00, (byte) 0xff}) {
+            for (int len = 0; len < CT_V4NEW_TCP_BYTES.length; len++) {
+                final byte[] truncated = new byte[CT_V4NEW_TCP_BYTES.length];
+                Arrays.fill(truncated, (byte) invalid);
+                System.arraycopy(CT_V4NEW_TCP_BYTES, 0, truncated, 0, len);
+
+                final ByteBuffer byteBuffer = ByteBuffer.wrap(truncated);
+                byteBuffer.order(ByteOrder.nativeOrder());
+                final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer,
+                        OsConstants.NETLINK_NETFILTER);
+            }
+        }
+    }
+
+    // Malformed conntrack messages.
+    public static final String CT_MALFORMED_HEX =
+            // CHECKSTYLE:OFF IndentationCheck
+            // <--           nlmsghr           -->|<-nfgenmsg->|<--    CTA_TUPLE_ORIG     -->|
+            // CTA_TUPLE_ORIG has no nla_value.
+            "18000000 0001 0006 00000000 00000000   02 00 0000 0400 0180"
+            // nested CTA_TUPLE_IP has no nla_value.
+            + "1C000000 0001 0006 00000000 00000000 02 00 0000 0800 0180 0400 0180"
+            // nested CTA_IP_V4_SRC has no nla_value.
+            + "20000000 0001 0006 00000000 00000000 02 00 0000 0C00 0180 0800 0180 0400 0100"
+            // nested CTA_TUPLE_PROTO has no nla_value.
+            // <--           nlmsghr           -->|<-nfgenmsg->|<--    CTA_TUPLE_ORIG
+            + "30000000 0001 0006 00000000 00000000 02 00 0000 1C00 0180 1400 0180 0800 0100"
+            //                                  -->|
+            + "C0A8500C 0800 0200 8C700874 0400 0280";
+            // CHECKSTYLE:ON IndentationCheck
+    public static final byte[] CT_MALFORMED_BYTES =
+            HexEncoding.decode(CT_MALFORMED_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @Test
+    public void testParseMalformation() {
+        assumeTrue(USING_LE);
+
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(CT_MALFORMED_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        // Expect no crash while parsing the malformed message.
+        int messageCount = 0;
+        while (byteBuffer.remaining() > 0) {
+            final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer,
+                    OsConstants.NETLINK_NETFILTER);
+            messageCount++;
+        }
+        assertEquals(4, messageCount);
+    }
+
+    @Test
+    public void testToString() {
+        assumeTrue(USING_LE);
+
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(CT_V4NEW_TCP_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
+        assertNotNull(msg);
+        assertTrue(msg instanceof ConntrackMessage);
+        final ConntrackMessage conntrackMessage = (ConntrackMessage) msg;
+
+        // Bug: "nlmsg_flags{1536(NLM_F_MATCH))" is not correct because StructNlMsgHdr
+        // #stringForNlMsgFlags can't convert all flags (ex: NLM_F_CREATE) and can't distinguish
+        // the flags which have the same value (ex: NLM_F_MATCH <0x200> and NLM_F_EXCL <0x200>).
+        // The flags output string should be "NLM_F_CREATE|NLM_F_EXCL" in this case.
+        // TODO: correct the flag converted string once #stringForNlMsgFlags does.
+        final String expected = ""
+                + "ConntrackMessage{"
+                + "nlmsghdr{StructNlMsgHdr{ nlmsg_len{140}, nlmsg_type{256(IPCTNL_MSG_CT_NEW)}, "
+                + "nlmsg_flags{1536(NLM_F_MATCH)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "nfgenmsg{NfGenMsg{ nfgen_family{AF_INET}, version{0}, res_id{4660} }}, "
+                + "tuple_orig{Tuple{IPPROTO_TCP: 192.168.80.12:62449 -> 140.112.8.116:443}}, "
+                + "tuple_reply{Tuple{IPPROTO_TCP: 140.112.8.116:443 -> 100.81.179.1:62449}}, "
+                + "status{408(IPS_CONFIRMED|IPS_SRC_NAT|IPS_SRC_NAT_DONE|IPS_DST_NAT_DONE)}, "
+                + "timeout_sec{120}}";
+        assertEquals(expected, conntrackMessage.toString());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
new file mode 100644
index 0000000..b44e428
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
@@ -0,0 +1,803 @@
+/*
+ * 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.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static android.os.Process.ROOT_UID;
+import static android.os.Process.SHELL_UID;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.NETLINK_INET_DIAG;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DESTROY;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+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.InetAddresses;
+import android.util.ArraySet;
+import android.util.Range;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InetDiagSocketTest {
+    // ::FFFF:192.0.2.1
+    private static final byte[] SRC_V4_MAPPED_V6_ADDRESS_BYTES = {
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+            (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+    };
+    // ::FFFF:192.0.2.2
+    private static final byte[] DST_V4_MAPPED_V6_ADDRESS_BYTES = {
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+            (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x02,
+    };
+
+    // Hexadecimal representation of InetDiagReqV2 request.
+    private static final String INET_DIAG_REQ_V2_UDP_INET4_HEX =
+            // struct nlmsghdr
+            "48000000" +     // length = 72
+            "1400" +         // type = SOCK_DIAG_BY_FAMILY
+            "0103" +         // flags = NLM_F_REQUEST | NLM_F_DUMP
+            "00000000" +     // seqno
+            "00000000" +     // pid (0 == kernel)
+            // struct inet_diag_req_v2
+            "02" +           // family = AF_INET
+            "11" +           // protcol = IPPROTO_UDP
+            "00" +           // idiag_ext
+            "00" +           // pad
+            "ffffffff" +     // idiag_states
+            // inet_diag_sockid
+            "a5de" +         // idiag_sport = 42462
+            "b971" +         // idiag_dport = 47473
+            "0a006402000000000000000000000000" + // idiag_src = 10.0.100.2
+            "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8
+            "00000000" +     // idiag_if
+            "ffffffffffffffff"; // idiag_cookie = INET_DIAG_NOCOOKIE
+    private static final byte[] INET_DIAG_REQ_V2_UDP_INET4_BYTES =
+            HexEncoding.decode(INET_DIAG_REQ_V2_UDP_INET4_HEX.toCharArray(), false);
+
+    @Test
+    public void testInetDiagReqV2UdpInet4() throws Exception {
+        InetSocketAddress local = new InetSocketAddress(InetAddress.getByName("10.0.100.2"),
+                42462);
+        InetSocketAddress remote = new InetSocketAddress(InetAddress.getByName("8.8.8.8"),
+                47473);
+        final byte[] msg = InetDiagMessage.inetDiagReqV2(IPPROTO_UDP, local, remote, AF_INET,
+                (short) (NLM_F_REQUEST | NLM_F_DUMP));
+        assertArrayEquals(INET_DIAG_REQ_V2_UDP_INET4_BYTES, msg);
+    }
+
+    // Hexadecimal representation of InetDiagReqV2 request.
+    private static final String INET_DIAG_REQ_V2_TCP_INET6_HEX =
+            // struct nlmsghdr
+            "48000000" +     // length = 72
+            "1400" +         // type = SOCK_DIAG_BY_FAMILY
+            "0100" +         // flags = NLM_F_REQUEST
+            "00000000" +     // seqno
+            "00000000" +     // pid (0 == kernel)
+            // struct inet_diag_req_v2
+            "0a" +           // family = AF_INET6
+            "06" +           // protcol = IPPROTO_TCP
+            "00" +           // idiag_ext
+            "00" +           // pad
+            "ffffffff" +     // idiag_states
+                // inet_diag_sockid
+                "a5de" +         // idiag_sport = 42462
+                "b971" +         // idiag_dport = 47473
+                "fe8000000000000086c9b2fffe6aed4b" + // idiag_src = fe80::86c9:b2ff:fe6a:ed4b
+                "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8
+                "00000000" +     // idiag_if
+                "ffffffffffffffff"; // idiag_cookie = INET_DIAG_NOCOOKIE
+    private static final byte[] INET_DIAG_REQ_V2_TCP_INET6_BYTES =
+            HexEncoding.decode(INET_DIAG_REQ_V2_TCP_INET6_HEX.toCharArray(), false);
+
+    @Test
+    public void testInetDiagReqV2TcpInet6() throws Exception {
+        InetSocketAddress local = new InetSocketAddress(
+                InetAddress.getByName("fe80::86c9:b2ff:fe6a:ed4b"), 42462);
+        InetSocketAddress remote = new InetSocketAddress(InetAddress.getByName("8.8.8.8"),
+                47473);
+        byte[] msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, local, remote, AF_INET6,
+                NLM_F_REQUEST);
+
+        assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_BYTES, msg);
+    }
+
+    // Hexadecimal representation of InetDiagReqV2 request with extension, INET_DIAG_INFO.
+    private static final String INET_DIAG_REQ_V2_TCP_INET_INET_DIAG_HEX =
+            // struct nlmsghdr
+            "48000000" +     // length = 72
+            "1400" +         // type = SOCK_DIAG_BY_FAMILY
+            "0100" +         // flags = NLM_F_REQUEST
+            "00000000" +     // seqno
+            "00000000" +     // pid (0 == kernel)
+            // struct inet_diag_req_v2
+            "02" +           // family = AF_INET
+            "06" +           // protcol = IPPROTO_TCP
+            "02" +           // idiag_ext = INET_DIAG_INFO
+            "00" +           // pad
+            "ffffffff" +   // idiag_states
+            // inet_diag_sockid
+            "3039" +         // idiag_sport = 12345
+            "d431" +         // idiag_dport = 54321
+            "01020304000000000000000000000000" + // idiag_src = 1.2.3.4
+            "08080404000000000000000000000000" + // idiag_dst = 8.8.4.4
+            "00000000" +     // idiag_if
+            "ffffffffffffffff"; // idiag_cookie = INET_DIAG_NOCOOKIE
+
+    private static final byte[] INET_DIAG_REQ_V2_TCP_INET_INET_DIAG_BYTES =
+            HexEncoding.decode(INET_DIAG_REQ_V2_TCP_INET_INET_DIAG_HEX.toCharArray(), false);
+    private static final int TCP_ALL_STATES = 0xffffffff;
+    @Test
+    public void testInetDiagReqV2TcpInetWithExt() throws Exception {
+        InetSocketAddress local = new InetSocketAddress(
+                InetAddress.getByName("1.2.3.4"), 12345);
+        InetSocketAddress remote = new InetSocketAddress(InetAddress.getByName("8.8.4.4"),
+                54321);
+        byte[] msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, local, remote, AF_INET,
+                NLM_F_REQUEST, 0 /* pad */, 2 /* idiagExt */, TCP_ALL_STATES);
+
+        assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET_INET_DIAG_BYTES, msg);
+
+        local = new InetSocketAddress(
+                InetAddress.getByName("fe80::86c9:b2ff:fe6a:ed4b"), 42462);
+        remote = new InetSocketAddress(InetAddress.getByName("8.8.8.8"),
+                47473);
+        msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, local, remote, AF_INET6,
+                NLM_F_REQUEST, 0 /* pad */, 0 /* idiagExt */, TCP_ALL_STATES);
+
+        assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_BYTES, msg);
+    }
+
+    // Hexadecimal representation of InetDiagReqV2 request with no socket specified.
+    private static final String INET_DIAG_REQ_V2_TCP_INET6_NO_ID_SPECIFIED_HEX =
+            // struct nlmsghdr
+            "48000000" +     // length = 72
+            "1400" +         // type = SOCK_DIAG_BY_FAMILY
+            "0100" +         // flags = NLM_F_REQUEST
+            "00000000" +     // seqno
+            "00000000" +     // pid (0 == kernel)
+            // struct inet_diag_req_v2
+            "0a" +           // family = AF_INET6
+            "06" +           // protcol = IPPROTO_TCP
+            "00" +           // idiag_ext
+            "00" +           // pad
+            "ffffffff" +     // idiag_states
+            // inet_diag_sockid
+            "0000" +         // idiag_sport
+            "0000" +         // idiag_dport
+            "00000000000000000000000000000000" + // idiag_src
+            "00000000000000000000000000000000" + // idiag_dst
+            "00000000" +     // idiag_if
+            "0000000000000000"; // idiag_cookie
+
+    private static final byte[] INET_DIAG_REQ_V2_TCP_INET6_NO_ID_SPECIFIED_BYTES =
+            HexEncoding.decode(INET_DIAG_REQ_V2_TCP_INET6_NO_ID_SPECIFIED_HEX.toCharArray(), false);
+
+    @Test
+    public void testInetDiagReqV2TcpInet6NoIdSpecified() throws Exception {
+        InetSocketAddress local = new InetSocketAddress(
+                InetAddress.getByName("fe80::fe6a:ed4b"), 12345);
+        InetSocketAddress remote = new InetSocketAddress(InetAddress.getByName("8.8.4.4"),
+                54321);
+        // Verify no socket specified if either local or remote socket address is null.
+        byte[] msgExt = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, null, null, AF_INET6,
+                NLM_F_REQUEST, 0 /* pad */, 0 /* idiagExt */, TCP_ALL_STATES);
+        byte[] msg;
+        try {
+            msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, null, remote, AF_INET6,
+                    NLM_F_REQUEST);
+            fail("Both remote and local should be null, expected UnknownHostException");
+        } catch (IllegalArgumentException e) {
+        }
+
+        try {
+            msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, local, null, AF_INET6,
+                    NLM_F_REQUEST, 0 /* pad */, 0 /* idiagExt */, TCP_ALL_STATES);
+            fail("Both remote and local should be null, expected UnknownHostException");
+        } catch (IllegalArgumentException e) {
+        }
+
+        msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, null, null, AF_INET6,
+                NLM_F_REQUEST, 0 /* pad */, 0 /* idiagExt */, TCP_ALL_STATES);
+        assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_NO_ID_SPECIFIED_BYTES, msg);
+        assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_NO_ID_SPECIFIED_BYTES, msgExt);
+    }
+
+    // Hexadecimal representation of InetDiagReqV2 request with v4-mapped v6 address
+    private static final String INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_HEX =
+            // struct nlmsghdr
+            "48000000" +     // length = 72
+            "1400" +         // type = SOCK_DIAG_BY_FAMILY
+            "0100" +         // flags = NLM_F_REQUEST
+            "00000000" +     // seqno
+            "00000000" +     // pid (0 == kernel)
+            // struct inet_diag_req_v2
+            "0a" +           // family = AF_INET6
+            "06" +           // protcol = IPPROTO_TCP
+            "00" +           // idiag_ext
+            "00" +           // pad
+            "ffffffff" +     // idiag_states
+            // inet_diag_sockid
+            "a817" +     // idiag_sport = 43031
+            "960f" +     // idiag_dport = 38415
+            "00000000000000000000ffffc0000201" + // idiag_src = ::FFFF:192.0.2.1
+            "00000000000000000000ffffc0000202" + // idiag_dst = ::FFFF:192.0.2.2
+            "00000000" +     // idiag_if
+            "ffffffffffffffff"; // idiag_cookie = INET_DIAG_NOCOOKIE
+
+    private static final byte[] INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_BYTES =
+            HexEncoding.decode(INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_HEX.toCharArray(), false);
+
+    @Test
+    public void testInetDiagReqV2TcpInet6V4Mapped() throws Exception {
+        final Inet6Address srcAddr = Inet6Address.getByAddress(
+                null /* host */, SRC_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+        final Inet6Address dstAddr = Inet6Address.getByAddress(
+                null /* host */, DST_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+        final byte[] msg = InetDiagMessage.inetDiagReqV2(
+                IPPROTO_TCP,
+                new InetSocketAddress(srcAddr, 43031),
+                new InetSocketAddress(dstAddr, 38415),
+                AF_INET6,
+                NLM_F_REQUEST);
+        assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_BYTES, msg);
+    }
+
+    // Hexadecimal representation of InetDiagReqV2 request with SOCK_DESTROY
+    private static final String INET_DIAG_REQ_V2_TCP_INET6_DESTROY_HEX =
+            // struct nlmsghdr
+            "48000000" +     // length = 72
+            "1500" +         // type = SOCK_DESTROY
+            "0500" +         // flags = NLM_F_REQUEST | NLM_F_ACK
+            "00000000" +     // seqno
+            "00000000" +     // pid (0 == kernel)
+            // struct inet_diag_req_v2
+            "0a" +           // family = AF_INET6
+            "06" +           // protcol = IPPROTO_TCP
+            "00" +           // idiag_ext
+            "00" +           // pad
+            "ffffffff" +     // idiag_states = TCP_ALL_STATES
+            // inet_diag_sockid
+            "a817" +     // idiag_sport = 43031
+            "960f" +     // idiag_dport = 38415
+            "20010db8000000000000000000000001" + // idiag_src = 2001:db8::1
+            "20010db8000000000000000000000002" + // idiag_dst = 2001:db8::2
+            "07000000" + // idiag_if = 7
+            "5800000000000000"; // idiag_cookie = 88
+
+    private static final byte[] INET_DIAG_REQ_V2_TCP_INET6_DESTROY_BYTES =
+            HexEncoding.decode(INET_DIAG_REQ_V2_TCP_INET6_DESTROY_HEX.toCharArray(), false);
+
+    @Test
+    public void testInetDiagReqV2TcpInet6Destroy() throws Exception {
+        final StructInetDiagSockId sockId = new StructInetDiagSockId(
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::1"), 43031),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::2"), 38415),
+                7  /* ifIndex */,
+                88 /* cookie */);
+        final byte[] msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, sockId, AF_INET6,
+                SOCK_DESTROY, (short) (NLM_F_REQUEST | NLM_F_ACK), 0 /* pad */, 0 /* idiagExt */,
+                TCP_ALL_STATES);
+
+        assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_DESTROY_BYTES, msg);
+    }
+
+    private void assertNlMsgHdr(StructNlMsgHdr hdr, short type, short flags, int seq, int pid) {
+        assertNotNull(hdr);
+        assertEquals(type, hdr.nlmsg_type);
+        assertEquals(flags, hdr.nlmsg_flags);
+        assertEquals(seq, hdr.nlmsg_seq);
+        assertEquals(pid, hdr.nlmsg_pid);
+    }
+
+    private void assertInetDiagSockId(StructInetDiagSockId sockId,
+            InetSocketAddress locSocketAddress, InetSocketAddress remSocketAddress,
+            int ifIndex, long cookie) {
+        assertEquals(locSocketAddress, sockId.locSocketAddress);
+        assertEquals(remSocketAddress, sockId.remSocketAddress);
+        assertEquals(ifIndex, sockId.ifIndex);
+        assertEquals(cookie, sockId.cookie);
+    }
+
+    // Hexadecimal representation of InetDiagMessage
+    private static final String INET_DIAG_MSG_HEX1 =
+            // struct nlmsghdr
+            "58000000"     // length = 88
+            + "1400"         // type = SOCK_DIAG_BY_FAMILY
+            + "0200"         // flags = NLM_F_MULTI
+            + "00000000"     // seqno
+            + "f5220000"     // pid
+            // struct inet_diag_msg
+            + "0a"           // family = AF_INET6
+            + "01"           // idiag_state = 1
+            + "02"           // idiag_timer = 2
+            + "ff"           // idiag_retrans = 255
+                // inet_diag_sockid
+                + "a817"     // idiag_sport = 43031
+                + "960f"     // idiag_dport = 38415
+                + "20010db8000000000000000000000001" // idiag_src = 2001:db8::1
+                + "20010db8000000000000000000000002" // idiag_dst = 2001:db8::2
+                + "07000000" // idiag_if = 7
+                + "5800000000000000" // idiag_cookie = 88
+            + "04000000"     // idiag_expires = 4
+            + "05000000"     // idiag_rqueue = 5
+            + "06000000"     // idiag_wqueue = 6
+            + "a3270000"     // idiag_uid = 10147
+            + "a57e19f0";    // idiag_inode = 4028202661
+
+    private void assertInetDiagMsg1(final NetlinkMessage msg) {
+        assertNotNull(msg);
+
+        assertTrue(msg instanceof InetDiagMessage);
+        final InetDiagMessage inetDiagMsg = (InetDiagMessage) msg;
+
+        assertNlMsgHdr(inetDiagMsg.getHeader(),
+                NetlinkConstants.SOCK_DIAG_BY_FAMILY,
+                StructNlMsgHdr.NLM_F_MULTI,
+                0    /* seq */,
+                8949 /* pid */);
+
+        assertEquals(AF_INET6, inetDiagMsg.inetDiagMsg.idiag_family);
+        assertEquals(1, inetDiagMsg.inetDiagMsg.idiag_state);
+        assertEquals(2, inetDiagMsg.inetDiagMsg.idiag_timer);
+        assertEquals(255, inetDiagMsg.inetDiagMsg.idiag_retrans);
+        assertInetDiagSockId(inetDiagMsg.inetDiagMsg.id,
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::1"), 43031),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::2"), 38415),
+                7  /* ifIndex */,
+                88 /* cookie */);
+        assertEquals(4, inetDiagMsg.inetDiagMsg.idiag_expires);
+        assertEquals(5, inetDiagMsg.inetDiagMsg.idiag_rqueue);
+        assertEquals(6, inetDiagMsg.inetDiagMsg.idiag_wqueue);
+        assertEquals(10147, inetDiagMsg.inetDiagMsg.idiag_uid);
+        assertEquals(4028202661L, inetDiagMsg.inetDiagMsg.idiag_inode);
+
+        // Verify the length of attribute list is 0 as expected since message doesn't
+        // take any attributes
+        assertEquals(0, inetDiagMsg.nlAttrs.size());
+    }
+
+    // Hexadecimal representation of InetDiagMessage
+    private static final String INET_DIAG_MSG_HEX2 =
+            // struct nlmsghdr
+            "6C000000"       // length = 108
+            + "1400"         // type = SOCK_DIAG_BY_FAMILY
+            + "0200"         // flags = NLM_F_MULTI
+            + "00000000"     // seqno
+            + "f5220000"     // pid
+            // struct inet_diag_msg
+            + "0a"           // family = AF_INET6
+            + "02"           // idiag_state = 2
+            + "10"           // idiag_timer = 16
+            + "20"           // idiag_retrans = 32
+                // inet_diag_sockid
+                + "a845"     // idiag_sport = 43077
+                + "01bb"     // idiag_dport = 443
+                + "20010db8000000000000000000000003" // idiag_src = 2001:db8::3
+                + "20010db8000000000000000000000004" // idiag_dst = 2001:db8::4
+                + "08000000" // idiag_if = 8
+                + "6300000000000000" // idiag_cookie = 99
+            + "30000000"     // idiag_expires = 48
+            + "40000000"     // idiag_rqueue = 64
+            + "50000000"     // idiag_wqueue = 80
+            + "39300000"     // idiag_uid = 12345
+            + "851a0000"     // idiag_inode = 6789
+            + "0500"           // len = 5
+            + "0800"         // type = 8
+            + "00000000"     // data
+            + "0800"         // len = 8
+            + "0F00"         // type = 15(INET_DIAG_MARK)
+            + "850A0C00"     // data, socket mark=789125
+            + "0400"         // len = 4
+            + "0200";        // type = 2
+
+    private void assertInetDiagMsg2(final NetlinkMessage msg) {
+        assertNotNull(msg);
+
+        assertTrue(msg instanceof InetDiagMessage);
+        final InetDiagMessage inetDiagMsg = (InetDiagMessage) msg;
+
+        assertNlMsgHdr(inetDiagMsg.getHeader(),
+                NetlinkConstants.SOCK_DIAG_BY_FAMILY,
+                StructNlMsgHdr.NLM_F_MULTI,
+                0    /* seq */,
+                8949 /* pid */);
+
+        assertEquals(AF_INET6, inetDiagMsg.inetDiagMsg.idiag_family);
+        assertEquals(2, inetDiagMsg.inetDiagMsg.idiag_state);
+        assertEquals(16, inetDiagMsg.inetDiagMsg.idiag_timer);
+        assertEquals(32, inetDiagMsg.inetDiagMsg.idiag_retrans);
+        assertInetDiagSockId(inetDiagMsg.inetDiagMsg.id,
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::3"), 43077),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::4"), 443),
+                8  /* ifIndex */,
+                99 /* cookie */);
+        assertEquals(48, inetDiagMsg.inetDiagMsg.idiag_expires);
+        assertEquals(64, inetDiagMsg.inetDiagMsg.idiag_rqueue);
+        assertEquals(80, inetDiagMsg.inetDiagMsg.idiag_wqueue);
+        assertEquals(12345, inetDiagMsg.inetDiagMsg.idiag_uid);
+        assertEquals(6789, inetDiagMsg.inetDiagMsg.idiag_inode);
+
+        // Verify the number of nlAttr and their content.
+        assertEquals(3, inetDiagMsg.nlAttrs.size());
+
+        assertEquals(5, inetDiagMsg.nlAttrs.get(0).nla_len);
+        assertEquals(8, inetDiagMsg.nlAttrs.get(0).nla_type);
+        assertArrayEquals(
+                HexEncoding.decode("00".toCharArray(), false),
+                inetDiagMsg.nlAttrs.get(0).nla_value);
+        assertEquals(8, inetDiagMsg.nlAttrs.get(1).nla_len);
+        assertEquals(15, inetDiagMsg.nlAttrs.get(1).nla_type);
+        assertArrayEquals(
+                HexEncoding.decode("850A0C00".toCharArray(), false),
+                inetDiagMsg.nlAttrs.get(1).nla_value);
+        assertEquals(4, inetDiagMsg.nlAttrs.get(2).nla_len);
+        assertEquals(2, inetDiagMsg.nlAttrs.get(2).nla_type);
+        assertNull(inetDiagMsg.nlAttrs.get(2).nla_value);
+    }
+
+    // Hexadecimal representation of InetDiagMessage
+    private static final String INET_DIAG_MSG_HEX_MALFORMED =
+            // struct nlmsghdr
+            "6E000000"       // length = 110
+            + "1400"         // type = SOCK_DIAG_BY_FAMILY
+            + "0200"         // flags = NLM_F_MULTI
+            + "00000000"     // seqno
+            + "f5220000"     // pid
+            // struct inet_diag_msg
+            + "0a"           // family = AF_INET6
+            + "02"           // idiag_state = 2
+            + "10"           // idiag_timer = 16
+            + "20"           // idiag_retrans = 32
+            // inet_diag_sockid
+            + "a845"     // idiag_sport = 43077
+            + "01bb"     // idiag_dport = 443
+            + "20010db8000000000000000000000005" // idiag_src = 2001:db8::5
+            + "20010db8000000000000000000000006" // idiag_dst = 2001:db8::6
+            + "08000000" // idiag_if = 8
+            + "6300000000000000" // idiag_cookie = 99
+            + "30000000"     // idiag_expires = 48
+            + "40000000"     // idiag_rqueue = 64
+            + "50000000"     // idiag_wqueue = 80
+            + "39300000"     // idiag_uid = 12345
+            + "851a0000"     // idiag_inode = 6789
+            + "0500"           // len = 5
+            + "0800"         // type = 8
+            + "00000000"     // data
+            + "0800"         // len = 8
+            + "0F00"         // type = 15(INET_DIAG_MARK)
+            + "850A0C00"     // data, socket mark=789125
+            + "0400"         // len = 4
+            + "0200"         // type = 2
+            + "0100"         // len = 1, malformed value
+            + "0100";        // type = 1
+
+    @Test
+    public void testParseInetDiagResponseMalformedNlAttr() throws Exception {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(
+                HexEncoding.decode((INET_DIAG_MSG_HEX_MALFORMED).toCharArray(), false));
+        byteBuffer.order(ByteOrder.nativeOrder());
+        assertNull(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+    }
+
+    // Hexadecimal representation of InetDiagMessage
+    private static final String INET_DIAG_MSG_HEX_TRUNCATED =
+            // struct nlmsghdr
+            "5E000000"       // length = 96
+            + "1400"         // type = SOCK_DIAG_BY_FAMILY
+            + "0200"         // flags = NLM_F_MULTI
+            + "00000000"     // seqno
+            + "f5220000"     // pid
+            // struct inet_diag_msg
+            + "0a"           // family = AF_INET6
+            + "02"           // idiag_state = 2
+            + "10"           // idiag_timer = 16
+            + "20"           // idiag_retrans = 32
+            // inet_diag_sockid
+            + "a845"     // idiag_sport = 43077
+            + "01bb"     // idiag_dport = 443
+            + "20010db8000000000000000000000005" // idiag_src = 2001:db8::5
+            + "20010db8000000000000000000000006" // idiag_dst = 2001:db8::6
+            + "08000000" // idiag_if = 8
+            + "6300000000000000" // idiag_cookie = 99
+            + "30000000"     // idiag_expires = 48
+            + "40000000"     // idiag_rqueue = 64
+            + "50000000"     // idiag_wqueue = 80
+            + "39300000"     // idiag_uid = 12345
+            + "851a0000"     // idiag_inode = 6789
+            + "0800"         // len = 8
+            + "0100"         // type = 1
+            + "000000";      // data, less than the expected length
+
+    @Test
+    public void testParseInetDiagResponseTruncatedNlAttr() throws Exception {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(
+                HexEncoding.decode((INET_DIAG_MSG_HEX_TRUNCATED).toCharArray(), false));
+        byteBuffer.order(ByteOrder.nativeOrder());
+        assertNull(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+    }
+
+    private static final byte[] INET_DIAG_MSG_BYTES =
+            HexEncoding.decode(INET_DIAG_MSG_HEX1.toCharArray(), false);
+
+    @Test
+    public void testParseInetDiagResponse() throws Exception {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        assertInetDiagMsg1(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+    }
+
+
+    private static final byte[] INET_DIAG_MSG_BYTES_MULTIPLE =
+            HexEncoding.decode((INET_DIAG_MSG_HEX1 + INET_DIAG_MSG_HEX2).toCharArray(), false);
+
+    @Test
+    public void testParseInetDiagResponseMultiple() {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES_MULTIPLE);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        assertInetDiagMsg1(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+        assertInetDiagMsg2(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+    }
+
+    private static final String INET_DIAG_SOCK_ID_V4_MAPPED_V6_HEX =
+            "a845" +     // idiag_sport = 43077
+            "01bb" +     // idiag_dport = 443
+            "00000000000000000000ffffc0000201" + // idiag_src = ::FFFF:192.0.2.1
+            "00000000000000000000ffffc0000202" + // idiag_dst = ::FFFF:192.0.2.2
+            "08000000" + // idiag_if = 8
+            "6300000000000000"; // idiag_cookie = 99
+
+    private static final byte[] INET_DIAG_SOCK_ID_V4_MAPPED_V6_BYTES =
+            HexEncoding.decode(INET_DIAG_SOCK_ID_V4_MAPPED_V6_HEX.toCharArray(), false);
+
+    @Test
+    public void testParseAndPackInetDiagSockIdV4MappedV6() {
+        final ByteBuffer parseByteBuffer = ByteBuffer.wrap(INET_DIAG_SOCK_ID_V4_MAPPED_V6_BYTES);
+        parseByteBuffer.order(ByteOrder.nativeOrder());
+        final StructInetDiagSockId diagSockId =
+                StructInetDiagSockId.parse(parseByteBuffer, (short) AF_INET6);
+        assertNotNull(diagSockId);
+
+        final ByteBuffer packByteBuffer =
+                ByteBuffer.allocate(INET_DIAG_SOCK_ID_V4_MAPPED_V6_BYTES.length);
+        diagSockId.pack(packByteBuffer);
+
+        // Move position to the head since ByteBuffer#equals compares the values from the current
+        // position.
+        parseByteBuffer.position(0);
+        packByteBuffer.position(0);
+        assertEquals(parseByteBuffer, packByteBuffer);
+    }
+
+    // Hexadecimal representation of InetDiagMessage with v4-mapped v6 address
+    private static final String INET_DIAG_MSG_V4_MAPPED_V6_HEX =
+            // struct nlmsghdr
+            "58000000" +     // length = 88
+            "1400" +         // type = SOCK_DIAG_BY_FAMILY
+            "0200" +         // flags = NLM_F_MULTI
+            "00000000" +     // seqno
+            "f5220000" +     // pid
+            // struct inet_diag_msg
+            "0a" +           // family = AF_INET6
+            "01" +           // idiag_state = 1
+            "02" +           // idiag_timer = 2
+            "03" +           // idiag_retrans = 3
+                // inet_diag_sockid
+                "a817" +     // idiag_sport = 43031
+                "960f" +     // idiag_dport = 38415
+                "00000000000000000000ffffc0000201" + // idiag_src = ::FFFF:192.0.2.1
+                "00000000000000000000ffffc0000202" + // idiag_dst = ::FFFF:192.0.2.2
+                "07000000" + // idiag_if = 7
+                "5800000000000000" + // idiag_cookie = 88
+            "04000000" +     // idiag_expires = 4
+            "05000000" +     // idiag_rqueue = 5
+            "06000000" +     // idiag_wqueue = 6
+            "a3270000" +     // idiag_uid = 10147
+            "A57E1900";      // idiag_inode = 1670821
+
+    private static final byte[] INET_DIAG_MSG_V4_MAPPED_V6_BYTES =
+            HexEncoding.decode(INET_DIAG_MSG_V4_MAPPED_V6_HEX.toCharArray(), false);
+
+    @Test
+    public void testParseInetDiagResponseV4MappedV6() throws Exception {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_V4_MAPPED_V6_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG);
+
+        assertNotNull(msg);
+        assertTrue(msg instanceof InetDiagMessage);
+        final InetDiagMessage inetDiagMsg = (InetDiagMessage) msg;
+        final Inet6Address srcAddr = Inet6Address.getByAddress(
+                null /* host */, SRC_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+        final Inet6Address dstAddr = Inet6Address.getByAddress(
+                null /* host */, DST_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+        assertInetDiagSockId(inetDiagMsg.inetDiagMsg.id,
+                new InetSocketAddress(srcAddr, 43031),
+                new InetSocketAddress(dstAddr, 38415),
+                7  /* ifIndex */,
+                88 /* cookie */);
+    }
+
+    private void doTestIsLoopback(InetAddress srcAddr, InetAddress dstAddr, boolean expected) {
+        final InetDiagMessage inetDiagMsg = new InetDiagMessage(new StructNlMsgHdr());
+        inetDiagMsg.inetDiagMsg.id = new StructInetDiagSockId(
+                new InetSocketAddress(srcAddr, 43031),
+                new InetSocketAddress(dstAddr, 38415)
+        );
+
+        assertEquals(expected, InetDiagMessage.isLoopback(inetDiagMsg));
+    }
+
+    @Test
+    public void testIsLoopback() {
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("127.0.0.1"),
+                InetAddresses.parseNumericAddress("192.0.2.1"),
+                true
+        );
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("192.0.2.1"),
+                InetAddresses.parseNumericAddress("127.7.7.7"),
+                true
+        );
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("::1"),
+                InetAddresses.parseNumericAddress("::1"),
+                true
+        );
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("::1"),
+                InetAddresses.parseNumericAddress("2001:db8::1"),
+                true
+        );
+    }
+
+    @Test
+    public void testIsLoopbackSameSrcDstAddress()  {
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("192.0.2.1"),
+                InetAddresses.parseNumericAddress("192.0.2.1"),
+                true
+        );
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("2001:db8::1"),
+                InetAddresses.parseNumericAddress("2001:db8::1"),
+                true
+        );
+    }
+
+    @Test
+    public void testIsLoopbackNonLoopbackSocket()  {
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("192.0.2.1"),
+                InetAddresses.parseNumericAddress("192.0.2.2"),
+                false
+        );
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("2001:db8::1"),
+                InetAddresses.parseNumericAddress("2001:db8::2"),
+                false
+        );
+    }
+
+    @Test
+    public void testIsLoopbackV4MappedV6() throws UnknownHostException {
+        // ::FFFF:127.1.2.3
+        final byte[] addrLoopbackByte = {
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+                (byte) 0x7f, (byte) 0x01, (byte) 0x02, (byte) 0x03,
+        };
+        // ::FFFF:192.0.2.1
+        final byte[] addrNonLoopbackByte1 = {
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+        };
+        // ::FFFF:192.0.2.2
+        final byte[] addrNonLoopbackByte2 = {
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x02,
+        };
+
+        final Inet6Address addrLoopback = Inet6Address.getByAddress(null, addrLoopbackByte, -1);
+        final Inet6Address addrNonLoopback1 =
+                Inet6Address.getByAddress(null, addrNonLoopbackByte1, -1);
+        final Inet6Address addrNonLoopback2 =
+                Inet6Address.getByAddress(null, addrNonLoopbackByte2, -1);
+
+        doTestIsLoopback(addrLoopback, addrNonLoopback1, true);
+        doTestIsLoopback(addrNonLoopback1, addrNonLoopback2, false);
+        doTestIsLoopback(addrNonLoopback1, addrNonLoopback1, true);
+    }
+
+    private void doTestContainsUid(final int uid, final Set<Range<Integer>> ranges,
+            final boolean expected) {
+        final InetDiagMessage inetDiagMsg = new InetDiagMessage(new StructNlMsgHdr());
+        inetDiagMsg.inetDiagMsg.idiag_uid = uid;
+        assertEquals(expected, InetDiagMessage.containsUid(inetDiagMsg, ranges));
+    }
+
+    @Test
+    public void testContainsUid() {
+        doTestContainsUid(77 /* uid */,
+                new ArraySet<>(List.of(new Range<>(0, 100))),
+                true /* expected */);
+        doTestContainsUid(77 /* uid */,
+                new ArraySet<>(List.of(new Range<>(77, 77), new Range<>(100, 200))),
+                true /* expected */);
+
+        doTestContainsUid(77 /* uid */,
+                new ArraySet<>(List.of(new Range<>(100, 200))),
+                false /* expected */);
+        doTestContainsUid(77 /* uid */,
+                new ArraySet<>(List.of(new Range<>(0, 76), new Range<>(78, 100))),
+                false /* expected */);
+    }
+
+    private void doTestIsAdbSocket(final int uid, final boolean expected) {
+        final InetDiagMessage inetDiagMsg = new InetDiagMessage(new StructNlMsgHdr());
+        inetDiagMsg.inetDiagMsg.idiag_uid = uid;
+        inetDiagMsg.inetDiagMsg.id = new StructInetDiagSockId(
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::1"), 38417),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::2"), 38415)
+        );
+        assertEquals(expected, InetDiagMessage.isAdbSocket(inetDiagMsg));
+    }
+
+    @Test
+    public void testIsAdbSocket() {
+        final int appUid = 10108;
+        doTestIsAdbSocket(SHELL_UID,  true /* expected */);
+        doTestIsAdbSocket(ROOT_UID, false /* expected */);
+        doTestIsAdbSocket(appUid, false /* expected */);
+    }
+}
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
new file mode 100644
index 0000000..d1df3a6
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
@@ -0,0 +1,297 @@
+/*
+ * 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.netlink;
+
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+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 android.net.InetAddresses;
+import android.net.IpPrefix;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NduseroptMessageTest {
+
+    private static final byte ICMP_TYPE_RA = (byte) 134;
+
+    private static final int IFINDEX1 = 15715755;
+    private static final int IFINDEX2 = 1431655765;
+
+    // IPv6, 0 bytes of options, interface index 15715755, type 134 (RA), code 0, padding.
+    private static final String HDR_EMPTY = "0a00" + "0000" + "abcdef00" + "8600000000000000";
+
+    // IPv6, 16 bytes of options, interface index 1431655765, type 134 (RA), code 0, padding.
+    private static final String HDR_16BYTE = "0a00" + "1000" + "55555555" + "8600000000000000";
+
+    // IPv6, 32 bytes of options, interface index 1431655765, type 134 (RA), code 0, padding.
+    private static final String HDR_32BYTE = "0a00" + "2000" + "55555555" + "8600000000000000";
+
+    // PREF64 option, 2001:db8:3:4:5:6::/96, lifetime=10064
+    private static final String OPT_PREF64 = "2602" + "2750" + "20010db80003000400050006";
+
+    // Length 20, NDUSEROPT_SRCADDR, fe80:2:3:4:5:6:7:8
+    private static final String NLA_SRCADDR = "1400" + "0100" + "fe800002000300040005000600070008";
+
+    private static final InetAddress SADDR1 = parseNumericAddress("fe80:2:3:4:5:6:7:8%" + IFINDEX1);
+    private static final InetAddress SADDR2 = parseNumericAddress("fe80:2:3:4:5:6:7:8%" + IFINDEX2);
+
+    private static final String MSG_EMPTY = HDR_EMPTY + NLA_SRCADDR;
+    private static final String MSG_PREF64 = HDR_16BYTE + OPT_PREF64 + NLA_SRCADDR;
+
+    @Test
+    public void testParsing() {
+        NduseroptMessage msg = parseNduseroptMessage(toBuffer(MSG_EMPTY));
+        assertMatches(AF_INET6, 0, IFINDEX1, ICMP_TYPE_RA, (byte) 0, SADDR1, msg);
+        assertNull(msg.option);
+
+        msg = parseNduseroptMessage(toBuffer(MSG_PREF64));
+        assertMatches(AF_INET6, 16, IFINDEX2, ICMP_TYPE_RA, (byte) 0, SADDR2, msg);
+        assertPref64Option("2001:db8:3:4:5:6::/96", msg.option);
+    }
+
+    @Test
+    public void testParseWithinNetlinkMessage() throws Exception {
+        // A NduseroptMessage inside a netlink message. Ensure that it parses the same way both by
+        // parsing the netlink message via NetlinkMessage.parse() and by parsing the option itself
+        // with NduseroptMessage.parse().
+        final String hexBytes =
+                "44000000440000000000000000000000"             // len=68, RTM_NEWNDUSEROPT
+                + "0A0010001E0000008600000000000000"           // IPv6, opt_bytes=16, ifindex=30, RA
+                + "260202580064FF9B0000000000000000"           // pref64, prefix=64:ff9b::/96, 600
+                + "14000100FE800000000000000250B6FFFEB7C499";  // srcaddr=fe80::250:b6ff:feb7:c499
+
+        ByteBuffer buf = toBuffer(hexBytes);
+        assertEquals(68, buf.limit());
+        buf.order(ByteOrder.nativeOrder());
+
+        NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_ROUTE);
+        assertNotNull(nlMsg);
+        assertTrue(nlMsg instanceof NduseroptMessage);
+
+        NduseroptMessage msg = (NduseroptMessage) nlMsg;
+        InetAddress srcaddr = InetAddress.getByName("fe80::250:b6ff:feb7:c499%30");
+        assertMatches(AF_INET6, 16, 30, ICMP_TYPE_RA, (byte) 0, srcaddr, msg);
+        assertPref64Option("64:ff9b::/96", msg.option);
+
+        final String hexBytesWithoutHeader = hexBytes.substring(StructNlMsgHdr.STRUCT_SIZE * 2);
+        ByteBuffer bufWithoutHeader = toBuffer(hexBytesWithoutHeader);
+        assertEquals(52, bufWithoutHeader.limit());
+        msg = parseNduseroptMessage(bufWithoutHeader);
+        assertMatches(AF_INET6, 16, 30, ICMP_TYPE_RA, (byte) 0, srcaddr, msg);
+        assertPref64Option("64:ff9b::/96", msg.option);
+    }
+
+    @Test
+    public void testParseRdnssOptionWithinNetlinkMessage() throws Exception {
+        final String hexBytes =
+                "4C000000440000000000000000000000"
+                + "0A0018001E0000008600000000000000"
+                + "1903000000001770FD123456789000000000000000000001"  // RDNSS option
+                + "14000100FE800000000000000250B6FFFEB7C499";
+
+        ByteBuffer buf = toBuffer(hexBytes);
+        assertEquals(76, buf.limit());
+        buf.order(ByteOrder.nativeOrder());
+
+        NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_ROUTE);
+        assertNotNull(nlMsg);
+        assertTrue(nlMsg instanceof NduseroptMessage);
+
+        NduseroptMessage msg = (NduseroptMessage) nlMsg;
+        InetAddress srcaddr = InetAddress.getByName("fe80::250:b6ff:feb7:c499%30");
+        assertMatches(AF_INET6, 24, 30, ICMP_TYPE_RA, (byte) 0, srcaddr, msg);
+        assertRdnssOption(msg.option, 6000 /* lifetime */,
+                (Inet6Address) InetAddresses.parseNumericAddress("fd12:3456:7890::1"));
+    }
+
+    @Test
+    public void testParseTruncatedRdnssOptionWithinNetlinkMessage() throws Exception {
+        final String truncatedHexBytes =
+                "38000000440000000000000000000000"
+                + "0A0018001E0000008600000000000000"
+                + "1903000000001770FD123456789000000000000000000001";  // RDNSS option
+
+        ByteBuffer buf = toBuffer(truncatedHexBytes);
+        buf.order(ByteOrder.nativeOrder());
+        NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_ROUTE);
+        assertNull(nlMsg);
+    }
+
+    @Test
+    public void testParseUnknownOptionWithinNetlinkMessage() throws Exception {
+        final String hexBytes =
+                "4C000000440000000000000000000000"
+                + "0A0018001E0000008600000000000000"
+                + "310300000000177006676F6F676C652E03636F6D00000000"  // DNSSL option: "google.com"
+                + "14000100FE800000000000000250B6FFFEB7C499";
+
+        ByteBuffer buf = toBuffer(hexBytes);
+        assertEquals(76, buf.limit());
+        buf.order(ByteOrder.nativeOrder());
+
+        NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_ROUTE);
+        assertNotNull(nlMsg);
+        assertTrue(nlMsg instanceof NduseroptMessage);
+
+        NduseroptMessage msg = (NduseroptMessage) nlMsg;
+        InetAddress srcaddr = InetAddress.getByName("fe80::250:b6ff:feb7:c499%30");
+        assertMatches(AF_INET6, 24, 30, ICMP_TYPE_RA, (byte) 0, srcaddr, msg);
+        assertEquals(NdOption.UNKNOWN, msg.option);
+    }
+
+    @Test
+    public void testUnknownOption() {
+        ByteBuffer buf = toBuffer(MSG_PREF64);
+        // Replace the PREF64 option type (38) with an unknown option number.
+        final int optionStart = NduseroptMessage.STRUCT_SIZE;
+        assertEquals(38, buf.get(optionStart));
+        buf.put(optionStart, (byte) 42);
+
+        NduseroptMessage msg = parseNduseroptMessage(buf);
+        assertMatches(AF_INET6, 16, IFINDEX2, ICMP_TYPE_RA, (byte) 0, SADDR2, msg);
+        assertEquals(NdOption.UNKNOWN, msg.option);
+
+        buf.flip();
+        assertEquals(42, buf.get(optionStart));
+        buf.put(optionStart, (byte) 38);
+
+        msg = parseNduseroptMessage(buf);
+        assertMatches(AF_INET6, 16, IFINDEX2, ICMP_TYPE_RA, (byte) 0, SADDR2, msg);
+        assertPref64Option("2001:db8:3:4:5:6::/96", msg.option);
+    }
+
+    @Test
+    public void testZeroLengthOption() {
+        // Make sure an unknown option with a 0-byte length is ignored and parsing continues with
+        // the address, which comes after it.
+        final String hexString = HDR_16BYTE + "00000000000000000000000000000000" + NLA_SRCADDR;
+        ByteBuffer buf = toBuffer(hexString);
+        assertEquals(52, buf.limit());
+        NduseroptMessage msg = parseNduseroptMessage(buf);
+        assertMatches(AF_INET6, 16, IFINDEX2, ICMP_TYPE_RA, (byte) 0, SADDR2, msg);
+        assertNull(msg.option);
+    }
+
+    @Test
+    public void testTooLongOption() {
+        // Make sure that if an option's length is too long, it's ignored and parsing continues with
+        // the address, which comes after it.
+        final String hexString = HDR_16BYTE + "26030000000000000000000000000000" + NLA_SRCADDR;
+        ByteBuffer buf = toBuffer(hexString);
+        assertEquals(52, buf.limit());
+        NduseroptMessage msg = parseNduseroptMessage(buf);
+        assertMatches(AF_INET6, 16, IFINDEX2, ICMP_TYPE_RA, (byte) 0, SADDR2, msg);
+        assertNull(msg.option);
+    }
+
+    @Test
+    public void testOptionsTooLong() {
+        // Header claims 32 bytes of options. Buffer ends before options end.
+        String hexString = HDR_32BYTE + OPT_PREF64;
+        ByteBuffer buf = toBuffer(hexString);
+        assertEquals(32, buf.limit());
+        assertNull(NduseroptMessage.parse(toBuffer(hexString), NETLINK_ROUTE));
+
+        // Header claims 32 bytes of options. Buffer ends at end of options with no source address.
+        hexString = HDR_32BYTE + OPT_PREF64 + OPT_PREF64;
+        buf = toBuffer(hexString);
+        assertEquals(48, buf.limit());
+        assertNull(NduseroptMessage.parse(toBuffer(hexString), NETLINK_ROUTE));
+    }
+
+    @Test
+    public void testTruncation() {
+        final int optLen = MSG_PREF64.length() / 2;  // 1 byte = 2 hex chars
+        for (int len = 0; len < optLen; len++) {
+            ByteBuffer buf = toBuffer(MSG_PREF64.substring(0, len * 2));
+            NduseroptMessage msg = parseNduseroptMessage(buf);
+            if (len < optLen) {
+                assertNull(msg);
+            } else {
+                assertNotNull(msg);
+                assertPref64Option("2001:db8:3:4:5:6::/96", msg.option);
+            }
+        }
+    }
+
+    @Test
+    public void testToString() {
+        NduseroptMessage msg = parseNduseroptMessage(toBuffer(MSG_PREF64));
+        assertNotNull(msg);
+        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.
+    private NduseroptMessage parseNduseroptMessage(ByteBuffer buf) {
+        return NduseroptMessage.parse(null, buf);
+    }
+
+    private ByteBuffer toBuffer(String hexString) {
+        return ByteBuffer.wrap(HexEncoding.decode(hexString));
+    }
+
+    private void assertMatches(int family, int optsLen, int ifindex, byte icmpType,
+            byte icmpCode, InetAddress srcaddr, NduseroptMessage msg) {
+        assertNotNull(msg);
+        assertEquals(family, msg.family);
+        assertEquals(ifindex, msg.ifindex);
+        assertEquals(optsLen, msg.opts_len);
+        assertEquals(icmpType, msg.icmp_type);
+        assertEquals(icmpCode, msg.icmp_code);
+        assertEquals(srcaddr, msg.srcaddr);
+    }
+
+    private void assertPref64Option(String prefix, NdOption opt) {
+        assertNotNull(opt);
+        assertTrue(opt instanceof StructNdOptPref64);
+        StructNdOptPref64 pref64Opt = (StructNdOptPref64) opt;
+        assertEquals(new IpPrefix(prefix), pref64Opt.prefix);
+    }
+
+    private void assertRdnssOption(NdOption opt, long lifetime, Inet6Address... servers) {
+        assertNotNull(opt);
+        assertTrue(opt instanceof StructNdOptRdnss);
+        StructNdOptRdnss rdnss = (StructNdOptRdnss) opt;
+        assertEquals(StructNdOptRdnss.TYPE, rdnss.type);
+        assertEquals((byte) (servers.length * 2 + 1), rdnss.header.length);
+        assertEquals(lifetime, rdnss.header.lifetime);
+        assertArrayEquals(servers, rdnss.servers);
+    }
+}
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
new file mode 100644
index 0000000..e42c552
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkConstantsTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.netlink;
+
+import static android.system.OsConstants.NETLINK_INET_DIAG;
+import static android.system.OsConstants.NETLINK_NETFILTER;
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET_CTRZERO;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET_DYING;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET_STATS;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET_STATS_CPU;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET_UNCONFIRMED;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+import static com.android.net.module.util.netlink.NetlinkConstants.NFNL_SUBSYS_CTNETLINK;
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_ERROR;
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_NOOP;
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_OVERRUN;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELADDR;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELLINK;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELROUTE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELRULE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_GETADDR;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_GETLINK;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_GETNEIGH;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_GETROUTE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_GETRULE;
+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;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_SETLINK;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForNlMsgType;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetlinkConstantsTest {
+    private static final short UNKNOWN_FAMILY = 1234;
+
+    private short makeCtType(short msgType) {
+        return (short) (NFNL_SUBSYS_CTNETLINK << 8 | (byte) msgType);
+    }
+
+    @Test
+    public void testStringForNlMsgType() {
+        assertEquals("RTM_NEWLINK", stringForNlMsgType(RTM_NEWLINK, NETLINK_ROUTE));
+        assertEquals("RTM_DELLINK", stringForNlMsgType(RTM_DELLINK, NETLINK_ROUTE));
+        assertEquals("RTM_GETLINK", stringForNlMsgType(RTM_GETLINK, NETLINK_ROUTE));
+        assertEquals("RTM_SETLINK", stringForNlMsgType(RTM_SETLINK, NETLINK_ROUTE));
+        assertEquals("RTM_NEWADDR", stringForNlMsgType(RTM_NEWADDR, NETLINK_ROUTE));
+        assertEquals("RTM_DELADDR", stringForNlMsgType(RTM_DELADDR, NETLINK_ROUTE));
+        assertEquals("RTM_GETADDR", stringForNlMsgType(RTM_GETADDR, NETLINK_ROUTE));
+        assertEquals("RTM_NEWROUTE", stringForNlMsgType(RTM_NEWROUTE, NETLINK_ROUTE));
+        assertEquals("RTM_DELROUTE", stringForNlMsgType(RTM_DELROUTE, NETLINK_ROUTE));
+        assertEquals("RTM_GETROUTE", stringForNlMsgType(RTM_GETROUTE, NETLINK_ROUTE));
+        assertEquals("RTM_NEWNEIGH", stringForNlMsgType(RTM_NEWNEIGH, NETLINK_ROUTE));
+        assertEquals("RTM_DELNEIGH", stringForNlMsgType(RTM_DELNEIGH, NETLINK_ROUTE));
+        assertEquals("RTM_GETNEIGH", stringForNlMsgType(RTM_GETNEIGH, NETLINK_ROUTE));
+        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",
+                stringForNlMsgType(SOCK_DIAG_BY_FAMILY, NETLINK_INET_DIAG));
+
+        assertEquals("IPCTNL_MSG_CT_NEW",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_NEW), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_DELETE",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_DELETE), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET_CTRZERO",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET_CTRZERO), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET_STATS_CPU",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET_STATS_CPU), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET_STATS",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET_STATS), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET_DYING",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET_DYING), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET_UNCONFIRMED",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET_UNCONFIRMED), NETLINK_NETFILTER));
+    }
+
+    @Test
+    public void testStringForNlMsgType_ControlMessage() {
+        for (int family : new int[]{NETLINK_ROUTE, NETLINK_INET_DIAG, NETLINK_NETFILTER}) {
+            assertEquals("NLMSG_NOOP", stringForNlMsgType(NLMSG_NOOP, family));
+            assertEquals("NLMSG_ERROR", stringForNlMsgType(NLMSG_ERROR, family));
+            assertEquals("NLMSG_DONE", stringForNlMsgType(NLMSG_DONE, family));
+            assertEquals("NLMSG_OVERRUN", stringForNlMsgType(NLMSG_OVERRUN, family));
+        }
+    }
+
+    @Test
+    public void testStringForNlMsgType_UnknownFamily() {
+        assertTrue(stringForNlMsgType(RTM_NEWLINK, UNKNOWN_FAMILY).startsWith("unknown"));
+        assertTrue(stringForNlMsgType(SOCK_DIAG_BY_FAMILY, UNKNOWN_FAMILY).startsWith("unknown"));
+        assertTrue(stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_NEW), UNKNOWN_FAMILY)
+                .startsWith("unknown"));
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkErrorMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkErrorMessageTest.java
new file mode 100644
index 0000000..ab7d9cf
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkErrorMessageTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 android.system.OsConstants.NETLINK_ROUTE;
+
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REPLACE;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+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 NetlinkErrorMessageTest {
+    private static final String TAG = "NetlinkErrorMessageTest";
+
+    // Hexadecimal representation of packet capture.
+    public static final String NLM_ERROR_OK_HEX =
+            // struct nlmsghdr
+            "24000000" +     // length = 36
+            "0200"     +     // type = 2 (NLMSG_ERROR)
+            "0000"     +     // flags
+            "26350000" +     // seqno
+            "64100000" +     // pid = userspace process
+            // error integer
+            "00000000" +     // "errno" (0 == OK)
+            // struct nlmsghdr
+            "30000000" +     // length (48) of original request
+            "1C00"     +     // type = 28 (RTM_NEWNEIGH)
+            "0501"     +     // flags (NLM_F_REQUEST | NLM_F_ACK | NLM_F_REPLACE)
+            "26350000" +     // seqno
+            "00000000";      // pid = kernel
+    public static final byte[] NLM_ERROR_OK =
+            HexEncoding.decode(NLM_ERROR_OK_HEX.toCharArray(), false);
+
+    @Test
+    public void testParseNlmErrorOk() {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(NLM_ERROR_OK);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof NetlinkErrorMessage);
+        final NetlinkErrorMessage errorMsg = (NetlinkErrorMessage) msg;
+
+        final StructNlMsgHdr hdr = errorMsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(36, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.NLMSG_ERROR, hdr.nlmsg_type);
+        assertEquals(0, hdr.nlmsg_flags);
+        assertEquals(13606, hdr.nlmsg_seq);
+        assertEquals(4196, hdr.nlmsg_pid);
+
+        final StructNlMsgErr err = errorMsg.getNlMsgError();
+        assertNotNull(err);
+        assertEquals(0, err.error);
+        assertNotNull(err.msg);
+        assertEquals(48, err.msg.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWNEIGH, err.msg.nlmsg_type);
+        assertEquals((NLM_F_REQUEST | NLM_F_ACK | NLM_F_REPLACE), err.msg.nlmsg_flags);
+        assertEquals(13606, err.msg.nlmsg_seq);
+        assertEquals(0, err.msg.nlmsg_pid);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
new file mode 100644
index 0000000..f64adb8
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.AF_UNSPEC;
+import static android.system.OsConstants.EACCES;
+import static android.system.OsConstants.NETLINK_ROUTE;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
+import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import static org.junit.Assert.assertEquals;
+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 android.content.Context;
+import android.net.util.SocketUtils;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.system.NetlinkSocketAddress;
+import android.system.Os;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.Struct;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import libcore.io.IoUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetlinkUtilsTest {
+    private static final String TAG = "NetlinkUtilsTest";
+    private static final int TEST_SEQNO = 5;
+    private static final int TEST_TIMEOUT_MS = 500;
+
+    @Test
+    public void testGetNeighborsQuery() throws Exception {
+        final byte[] req = RtNetlinkNeighborMessage.newGetNeighborsRequest(TEST_SEQNO);
+        assertNotNull(req);
+
+        List<RtNetlinkNeighborMessage> msgs = new ArrayList<>();
+        Consumer<RtNetlinkNeighborMessage> handleNlDumpMsg = (msg) -> {
+            msgs.add(msg);
+        };
+
+        final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
+        final int targetSdk =
+                ctx.getPackageManager()
+                        .getApplicationInfo(ctx.getPackageName(), 0)
+                        .targetSdkVersion;
+
+        // Apps targeting an SDK version > S are not allowed to send RTM_GETNEIGH{TBL} messages
+        if (SdkLevel.isAtLeastT() && targetSdk > 31) {
+            var ctxt = new String(Files.readAllBytes(Paths.get("/proc/thread-self/attr/current")));
+            assumeFalse("must not be platform app", ctxt.startsWith("u:r:platform_app:s0:"));
+            // NetworkStackCoverageTests uses the same UID with NetworkStack module, which
+            // still has the permission to send RTM_GETNEIGH message (sepolicy just blocks the
+            // access from untrusted_apps), also exclude the NetworkStackCoverageTests.
+            assumeFalse("network_stack context is expected to have permission to send RTM_GETNEIGH",
+                    ctxt.startsWith("u:r:network_stack:s0"));
+            try {
+                NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+                        NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
+                fail("RTM_GETNEIGH is not allowed for apps targeting SDK > 31 on T+ platforms,"
+                        + " target SDK version: " + targetSdk);
+            } catch (ErrnoException e) {
+                // Expected
+                assertEquals(e.errno, EACCES);
+                return;
+            }
+        }
+
+        // Check that apps targeting lower API levels / running on older platforms succeed
+        NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+                NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
+
+        for (var msg : msgs) {
+            assertNotNull(msg);
+            final StructNlMsgHdr hdr = msg.getHeader();
+            assertNotNull(hdr);
+            assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
+            assertTrue((hdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
+            assertEquals(TEST_SEQNO, hdr.nlmsg_seq);
+        }
+
+        // TODO: make sure this test passes sanely in airplane mode.
+        assertTrue(msgs.size() > 0);
+    }
+
+    @Test
+    public void testBasicWorkingGetAddrQuery() throws Exception {
+        final int testSeqno = 8;
+        final byte[] req = newGetAddrRequest(testSeqno);
+        assertNotNull(req);
+
+        List<RtNetlinkAddressMessage> msgs = new ArrayList<>();
+        Consumer<RtNetlinkAddressMessage> handleNlDumpMsg = (msg) -> {
+            msgs.add(msg);
+        };
+        NetlinkUtils.<RtNetlinkAddressMessage>getAndProcessNetlinkDumpMessages(req, NETLINK_ROUTE,
+                RtNetlinkAddressMessage.class, handleNlDumpMsg);
+
+        boolean ipv4LoopbackAddressFound = false;
+        boolean ipv6LoopbackAddressFound = false;
+        final InetAddress loopbackIpv4 = InetAddress.getByName("127.0.0.1");
+        final InetAddress loopbackIpv6 = InetAddress.getByName("::1");
+
+        for (var msg : msgs) {
+            assertNotNull(msg);
+            final StructNlMsgHdr nlmsghdr = msg.getHeader();
+            assertNotNull(nlmsghdr);
+            assertEquals(NetlinkConstants.RTM_NEWADDR, nlmsghdr.nlmsg_type);
+            assertTrue((nlmsghdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
+            assertEquals(testSeqno, nlmsghdr.nlmsg_seq);
+            assertTrue(msg instanceof RtNetlinkAddressMessage);
+            // When parsing the full response we can see the RTM_NEWADDR messages representing for
+            // IPv4 and IPv6 loopback address: 127.0.0.1 and ::1 and non-loopback addresses.
+            final StructIfaddrMsg ifaMsg = ((RtNetlinkAddressMessage) msg).getIfaddrHeader();
+            final InetAddress ipAddress = ((RtNetlinkAddressMessage) msg).getIpAddress();
+            assertTrue(
+                    "Non-IP address family: " + ifaMsg.family,
+                    ifaMsg.family == AF_INET || ifaMsg.family == AF_INET6);
+            assertNotNull(ipAddress);
+
+            if (ipAddress.equals(loopbackIpv4)) {
+                ipv4LoopbackAddressFound = true;
+                assertTrue(ipAddress.isLoopbackAddress());
+            }
+            if (ipAddress.equals(loopbackIpv6)) {
+                ipv6LoopbackAddressFound = true;
+                assertTrue(ipAddress.isLoopbackAddress());
+            }
+        }
+
+        assertTrue(msgs.size() > 0);
+        // Check ipv4 and ipv6 loopback addresses are in the output
+        assertTrue(ipv4LoopbackAddressFound && ipv6LoopbackAddressFound);
+    }
+
+    /** A convenience method to create an RTM_GETADDR request message. */
+    private static byte[] newGetAddrRequest(int seqNo) {
+        final int length = StructNlMsgHdr.STRUCT_SIZE + Struct.getSize(StructIfaddrMsg.class);
+        final byte[] bytes = new byte[length];
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+        nlmsghdr.nlmsg_len = length;
+        nlmsghdr.nlmsg_type = NetlinkConstants.RTM_GETADDR;
+        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+        nlmsghdr.nlmsg_seq = seqNo;
+        nlmsghdr.pack(byteBuffer);
+
+        final StructIfaddrMsg addrMsg = new StructIfaddrMsg((byte) AF_UNSPEC /* family */,
+                (short) 0 /* prefixLen */, (short) 0 /* flags */, (short) 0 /* scope */,
+                0 /* index */);
+        addrMsg.pack(byteBuffer);
+
+        return bytes;
+    }
+
+    @Test
+    public void testGetIpv6MulticastRoutes_doesNotThrow() {
+        var multicastRoutes = NetlinkUtils.getIpv6MulticastRoutes();
+
+        for (var route : multicastRoutes) {
+            assertNotNull(route);
+            assertEquals("Route is not IP6MR: " + route,
+                    RTNL_FAMILY_IP6MR, route.getRtmFamily());
+            assertNotNull("Route doesn't contain source: " + route, route.getSource());
+            assertNotNull("Route doesn't contain destination: " + route, route.getDestination());
+        }
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R) // getsockoptInt requires > R
+    public void testNetlinkSocketForProto_defaultBufferSize() throws Exception {
+        final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
+        final int bufferSize = Os.getsockoptInt(fd, SOL_SOCKET, SO_RCVBUF) / 2;
+
+        assertTrue("bufferSize: " + bufferSize, bufferSize > 0); // whatever the default value is
+        SocketUtils.closeSocket(fd);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R) // getsockoptInt requires > R
+    public void testNetlinkSocketForProto_setBufferSize() throws Exception {
+        final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE,
+                8000);
+        final int bufferSize = Os.getsockoptInt(fd, SOL_SOCKET, SO_RCVBUF) / 2;
+
+        assertEquals(8000, bufferSize);
+        SocketUtils.closeSocket(fd);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
new file mode 100644
index 0000000..1d08525
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
@@ -0,0 +1,341 @@
+/*
+ * 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.net.module.util.netlink;
+
+import static android.system.OsConstants.IFA_F_PERMANENT;
+import static android.system.OsConstants.NETLINK_ROUTE;
+import static android.system.OsConstants.RT_SCOPE_LINK;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
+
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+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 android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkAddressMessageTest {
+    private static final Inet6Address TEST_LINK_LOCAL =
+            (Inet6Address) InetAddresses.parseNumericAddress("FE80::2C41:5CFF:FE09:6665");
+    private static final Inet6Address TEST_GLOBAL_ADDRESS =
+            (Inet6Address) InetAddresses.parseNumericAddress("2001:DB8:1::100");
+
+    // An example of the full RTM_NEWADDR message.
+    private static final String RTM_NEWADDR_HEX =
+            "48000000140000000000000000000000"            // struct nlmsghr
+            + "0A4080FD1E000000"                          // struct ifaddrmsg
+            + "14000100FE800000000000002C415CFFFE096665"  // IFA_ADDRESS
+            + "14000600100E0000201C00002A70000045700000"  // IFA_CACHEINFO
+            + "0800080080000000";                         // IFA_FLAGS
+
+    private ByteBuffer toByteBuffer(final String hexString) {
+        return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+    }
+
+    @Test
+    public void testParseRtmNewAddress() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWADDR_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkAddressMessage);
+        final RtNetlinkAddressMessage addrMsg = (RtNetlinkAddressMessage) msg;
+
+        final StructNlMsgHdr hdr = addrMsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(72, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWADDR, hdr.nlmsg_type);
+        assertEquals(0, hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructIfaddrMsg ifaddrMsgHdr = addrMsg.getIfaddrHeader();
+        assertNotNull(ifaddrMsgHdr);
+        assertEquals((byte) OsConstants.AF_INET6, ifaddrMsgHdr.family);
+        assertEquals(64, ifaddrMsgHdr.prefixLen);
+        assertEquals(0x80, ifaddrMsgHdr.flags);
+        assertEquals(0xFD, ifaddrMsgHdr.scope);
+        assertEquals(30, ifaddrMsgHdr.index);
+
+        assertEquals((Inet6Address) addrMsg.getIpAddress(), TEST_LINK_LOCAL);
+        assertEquals(3600L, addrMsg.getIfacacheInfo().preferred);
+        assertEquals(7200L, addrMsg.getIfacacheInfo().valid);
+        assertEquals(28714, addrMsg.getIfacacheInfo().cstamp);
+        assertEquals(28741, addrMsg.getIfacacheInfo().tstamp);
+        assertEquals(0x80, addrMsg.getFlags());
+    }
+
+    private static final String RTM_NEWADDR_PACK_HEX =
+            "48000000140000000000000000000000"             // struct nlmsghr
+            + "0A4080FD1E000000"                           // struct ifaddrmsg
+            + "14000100FE800000000000002C415CFFFE096665"   // IFA_ADDRESS
+            + "14000600FFFFFFFFFFFFFFFF2A7000002A700000"   // IFA_CACHEINFO
+            + "0800080081000000";                          // IFA_FLAGS(override ifa_flags)
+
+    @Test
+    public void testPackRtmNewAddr() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWADDR_PACK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkAddressMessage);
+        final RtNetlinkAddressMessage addrMsg = (RtNetlinkAddressMessage) msg;
+
+        final ByteBuffer packBuffer = ByteBuffer.allocate(72);
+        packBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        addrMsg.pack(packBuffer);
+        assertEquals(RTM_NEWADDR_PACK_HEX, HexDump.toHexString(packBuffer.array()));
+    }
+
+    private static final String RTM_NEWADDR_TRUNCATED_HEX =
+            "44000000140000000000000000000000"            // struct nlmsghr
+            + "0A4080FD1E000000"                          // struct ifaddrmsg
+            + "10000100FE800000000000002C415CFF"          // IFA_ADDRESS(truncated)
+            + "14000600FFFFFFFFFFFFFFFF2A7000002A700000"  // IFA_CACHEINFO
+            + "0800080080000000";                         // IFA_FLAGS
+
+    @Test
+    public void testTruncatedRtmNewAddr() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWADDR_TRUNCATED_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        // Parsing RTM_NEWADDR with truncated IFA_ADDRESS attribute returns null.
+        assertNull(msg);
+    }
+
+    @Test
+    public void testCreateRtmNewAddressMessage() {
+        // Hexadecimal representation of our created packet.
+        final String expectedNewAddressHex =
+                // struct nlmsghdr
+                "48000000" +    // length = 72
+                "1400" +        // type = 20 (RTM_NEWADDR)
+                "0501" +        // flags = NLM_F_ACK | NLM_F_REQUEST | NLM_F_REPLACE
+                "01000000" +    // seqno = 1
+                "00000000" +    // pid = 0 (send to kernel)
+                // struct IfaddrMsg
+                "0A" +          // family = inet6
+                "40" +          // prefix len = 64
+                "00" +          // flags = 0
+                "FD" +          // scope = RT_SCOPE_LINK
+                "17000000" +    // ifindex = 23
+                // struct nlattr: IFA_ADDRESS
+                "1400" +        // len
+                "0100" +        // type
+                "FE800000000000002C415CFFFE096665" + // IP address = fe80::2C41:5cff:fe09:6665
+                // struct nlattr: IFA_CACHEINFO
+                "1400" +        // len
+                "0600" +        // type
+                "FFFFFFFF" +    // preferred = infinite
+                "FFFFFFFF" +    // valid = infinite
+                "00000000" +    // cstamp
+                "00000000" +    // tstamp
+                // struct nlattr: IFA_FLAGS
+                "0800" +        // len
+                "0800" +        // type
+                "80000000";     // flags = IFA_F_PERMANENT
+        final byte[] expectedNewAddress =
+                HexEncoding.decode(expectedNewAddressHex.toCharArray(), false);
+
+        final byte[] bytes = RtNetlinkAddressMessage.newRtmNewAddressMessage(1 /* seqno */,
+                TEST_LINK_LOCAL, (short) 64 /* prefix len */, IFA_F_PERMANENT /* flags */,
+                (byte) RT_SCOPE_LINK /* scope */, 23 /* ifindex */,
+                (long) 0xFFFFFFFF /* preferred */, (long) 0xFFFFFFFF /* valid */);
+        assertArrayEquals(expectedNewAddress, bytes);
+    }
+
+    @Test
+    public void testCreateRtmNewAddressMessage_IPv4Address() {
+        // Hexadecimal representation of our created packet.
+        final String expectedNewAddressHex =
+                // struct nlmsghdr
+                "4c000000"      // length = 76
+                + "1400"        // type = 20 (RTM_NEWADDR)
+                + "0501"        // flags = NLM_F_ACK | NLM_F_REQUEST | NLM_F_REPLACE
+                + "01000000"    // seqno = 1
+                + "00000000"    // pid = 0 (send to kernel)
+                // struct IfaddrMsg
+                + "02"          // family = inet
+                + "18"          // prefix len = 24
+                + "00"          // flags = 0
+                + "00"          // scope = RT_SCOPE_UNIVERSE
+                + "14000000"    // ifindex = 20
+                // struct nlattr: IFA_ADDRESS
+                + "0800"        // len
+                + "0100"        // type
+                + "C0A80491"    // IPv4 address = 192.168.4.145
+                // struct nlattr: IFA_CACHEINFO
+                + "1400"        // len
+                + "0600"        // type
+                + "C0A80000"    // preferred = 43200s
+                + "C0A80000"    // valid = 43200s
+                + "00000000"    // cstamp
+                + "00000000"    // tstamp
+                // struct nlattr: IFA_FLAGS
+                + "0800"        // len
+                + "0800"        // type
+                + "00000000"    // flags = 0
+                // struct nlattr: IFA_LOCAL
+                + "0800"        // len
+                + "0200"        // type
+                + "C0A80491"    // local address = 192.168.4.145
+                // struct nlattr: IFA_BROADCAST
+                + "0800"        // len
+                + "0400"        // type
+                + "C0A804FF";   // broadcast address = 192.168.4.255
+        final byte[] expectedNewAddress =
+                HexEncoding.decode(expectedNewAddressHex.toCharArray(), false);
+
+        final Inet4Address ipAddress =
+                (Inet4Address) InetAddresses.parseNumericAddress("192.168.4.145");
+        final byte[] bytes = RtNetlinkAddressMessage.newRtmNewAddressMessage(1 /* seqno */,
+                ipAddress, (short) 24 /* prefix len */, 0 /* flags */,
+                (byte) RT_SCOPE_UNIVERSE /* scope */, 20 /* ifindex */,
+                (long) 0xA8C0 /* preferred */, (long) 0xA8C0 /* valid */);
+        assertArrayEquals(expectedNewAddress, bytes);
+    }
+
+    @Test
+    public void testCreateRtmDelAddressMessage() {
+        // Hexadecimal representation of our created packet.
+        final String expectedDelAddressHex =
+                // struct nlmsghdr
+                "2C000000" + // length = 44
+                "1500" +     // type = 21 (RTM_DELADDR)
+                "0500" +     // flags = NLM_F_ACK | NLM_F_REQUEST
+                "01000000" + // seqno = 1
+                "00000000" + // pid = 0 (send to kernel)
+                // struct IfaddrMsg
+                "0A" +       // family = inet6
+                "40" +       // prefix len = 64
+                "00" +       // flags = 0
+                "00" +       // scope = RT_SCOPE_UNIVERSE
+                "3B000000" + // ifindex = 59
+                // struct nlattr: IFA_ADDRESS
+                "1400" +     // len
+                "0100" +     // type
+                "20010DB8000100000000000000000100"; // IP address = 2001:db8:1::100
+        final byte[] expectedDelAddress =
+                HexEncoding.decode(expectedDelAddressHex.toCharArray(), false);
+
+        final byte[] bytes = RtNetlinkAddressMessage.newRtmDelAddressMessage(1 /* seqno */,
+                TEST_GLOBAL_ADDRESS, (short) 64 /* prefix len */, 59 /* ifindex */);
+        assertArrayEquals(expectedDelAddress, bytes);
+    }
+
+    @Test
+    public void testCreateRtmNewAddressMessage_nullIpAddress() {
+        assertThrows(NullPointerException.class,
+                () -> RtNetlinkAddressMessage.newRtmNewAddressMessage(1 /* seqno */,
+                        null /* IP address */, (short) 0 /* prefix len */,
+                        IFA_F_PERMANENT /* flags */, (byte) RT_SCOPE_LINK /* scope */,
+                        23 /* ifindex */, (long) 0xFFFFFFFF /* preferred */,
+                        (long) 0xFFFFFFFF /* valid */));
+    }
+
+    @Test
+    public void testCreateRtmDelAddressMessage_nullIpAddress() {
+        assertThrows(NullPointerException.class,
+                () -> RtNetlinkAddressMessage.newRtmDelAddressMessage(1 /* seqno */,
+                        null /* IP address */, (short) 0 /* prefix len */, 59 /* ifindex */));
+    }
+
+    @Test
+    public void testCreateRtmNewAddressMessage_u32Flags() {
+        // Hexadecimal representation of our created packet.
+        final String expectedNewAddressHex =
+                // struct nlmsghdr
+                "48000000" +    // length = 72
+                "1400" +        // type = 20 (RTM_NEWADDR)
+                "0501" +        // flags = NLM_F_ACK | NLM_F_REQUEST | NLM_F_REPLACE
+                "01000000" +    // seqno = 1
+                "00000000" +    // pid = 0 (send to kernel)
+                // struct IfaddrMsg
+                "0A" +          // family = inet6
+                "80" +          // prefix len = 128
+                "00" +          // flags = 0
+                "00" +          // scope = RT_SCOPE_UNIVERSE
+                "17000000" +    // ifindex = 23
+                // struct nlattr: IFA_ADDRESS
+                "1400" +        // len
+                "0100" +        // type
+                "20010DB8000100000000000000000100" + // IP address = 2001:db8:1::100
+                // struct nlattr: IFA_CACHEINFO
+                "1400" +        // len
+                "0600" +        // type
+                "FFFFFFFF" +    // preferred = infinite
+                "FFFFFFFF" +    // valid = infinite
+                "00000000" +    // cstamp
+                "00000000" +    // tstamp
+                // struct nlattr: IFA_FLAGS
+                "0800" +        // len
+                "0800" +        // type
+                "00030000";     // flags = IFA_F_MANAGETEMPADDR | IFA_F_NOPREFIXROUTE
+        final byte[] expectedNewAddress =
+                HexEncoding.decode(expectedNewAddressHex.toCharArray(), false);
+
+        final byte[] bytes = RtNetlinkAddressMessage.newRtmNewAddressMessage(1 /* seqno */,
+                TEST_GLOBAL_ADDRESS, (short) 128 /* prefix len */,
+                (int) 0x300 /* flags: IFA_F_MANAGETEMPADDR | IFA_F_NOPREFIXROUTE */,
+                (byte) RT_SCOPE_UNIVERSE /* scope */, 23 /* ifindex */,
+                (long) 0xFFFFFFFF /* preferred */, (long) 0xFFFFFFFF /* valid */);
+        assertArrayEquals(expectedNewAddress, bytes);
+    }
+
+    @Test
+    public void testToString() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWADDR_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkAddressMessage);
+        final RtNetlinkAddressMessage addrMsg = (RtNetlinkAddressMessage) msg;
+        final String expected = "RtNetlinkAddressMessage{ "
+                + "nlmsghdr{"
+                + "StructNlMsgHdr{ nlmsg_len{72}, nlmsg_type{20(RTM_NEWADDR)}, nlmsg_flags{0()}, "
+                + "nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "Ifaddrmsg{"
+                + "family: 10, prefixLen: 64, flags: 128, scope: 253, index: 30}, "
+                + "IP Address{fe80::2c41:5cff:fe09:6665}, "
+                + "IfacacheInfo{"
+                + "preferred: 3600, valid: 7200, cstamp: 28714, tstamp: 28741}, "
+                + "Address Flags{00000080} "
+                + "}";
+        assertEquals(expected, addrMsg.toString());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
new file mode 100644
index 0000000..9db63db
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.net.module.util.netlink;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
+
+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 android.net.MacAddress;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkLinkMessageTest {
+
+    // An example of the full RTM_NEWLINK message.
+    private static final String RTM_NEWLINK_HEX =
+            "64000000100000000000000000000000"   // struct nlmsghr
+            + "000001001E0000000210000000000000" // struct ifinfo
+            + "0A000300776C616E30000000"         // IFLA_IFNAME(wlan0)
+            + "08000D00B80B0000"                 // IFLA_PROTINFO
+            + "0500100002000000"                 // IFLA_OPERSTATE
+            + "0500110001000000"                 // IFLA_LINKMODE
+            + "08000400DC050000"                 // IFLA_MTU
+            + "0A00010092C3E3C9374E0000"         // IFLA_ADDRESS
+            + "0A000200FFFFFFFFFFFF0000";        // IFLA_BROADCAST
+
+    private ByteBuffer toByteBuffer(final String hexString) {
+        return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+    }
+
+    @Test
+    public void testParseRtmNewLink() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkLinkMessage);
+        final RtNetlinkLinkMessage linkMsg = (RtNetlinkLinkMessage) msg;
+
+        final StructNlMsgHdr hdr = linkMsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(100, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWLINK, hdr.nlmsg_type);
+        assertEquals(0, hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructIfinfoMsg ifinfomsgHdr = linkMsg.getIfinfoHeader();
+        assertNotNull(ifinfomsgHdr);
+        assertEquals((byte) OsConstants.AF_UNSPEC, ifinfomsgHdr.family);
+        assertEquals(OsConstants.ARPHRD_ETHER, ifinfomsgHdr.type);
+        assertEquals(30, ifinfomsgHdr.index);
+        assertEquals(0, ifinfomsgHdr.change);
+
+        assertEquals(ETHER_MTU, linkMsg.getMtu());
+        assertEquals(MacAddress.fromString("92:C3:E3:C9:37:4E"), linkMsg.getHardwareAddress());
+        assertTrue(linkMsg.getInterfaceName().equals("wlan0"));
+    }
+
+    /**
+     * Example:
+     * # adb shell ip tunnel add トン0 mode sit local any remote 8.8.8.8
+     * # adb shell ip link show | grep トン
+     * 33: トン0@NONE: <POINTOPOINT,NOARP> mtu 1480 qdisc noop state DOWN mode DEFAULT group
+     *     default qlen 1000
+     *
+     * IFLA_IFNAME attribute: \x0c\x00\x03\x00\xe3\x83\x88\xe3\x83\xb3\x30\x00
+     *     length: 0x000c
+     *     type: 0x0003
+     *     value: \xe3\x83\x88\xe3\x83\xb3\x30\x00
+     *            ト (\xe3\x83\x88)
+     *            ン (\xe3\x83\xb3)
+     *            0  (\x30)
+     *            null terminated (\x00)
+     */
+    private static final String RTM_NEWLINK_UTF8_HEX =
+            "34000000100000000000000000000000"   // struct nlmsghr
+            + "000001001E0000000210000000000000" // struct ifinfo
+            + "08000400DC050000"                 // IFLA_MTU
+            + "0A00010092C3E3C9374E0000"         // IFLA_ADDRESS
+            + "0C000300E38388E383B33000";        // IFLA_IFNAME(トン0)
+
+    @Test
+    public void testParseRtmNewLink_utf8Ifname() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_UTF8_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkLinkMessage);
+        final RtNetlinkLinkMessage linkMsg = (RtNetlinkLinkMessage) msg;
+
+        assertTrue(linkMsg.getInterfaceName().equals("トン0"));
+    }
+
+    private static final String RTM_NEWLINK_PACK_HEX =
+            "34000000100000000000000000000000"   // struct nlmsghr
+            + "000001001E0000000210000000000000" // struct ifinfo
+            + "08000400DC050000"                 // IFLA_MTU
+            + "0A00010092C3E3C9374E0000"         // IFLA_ADDRESS
+            + "0A000300776C616E30000000";        // IFLA_IFNAME(wlan0)
+
+    @Test
+    public void testPackRtmNewLink() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_PACK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkLinkMessage);
+        final RtNetlinkLinkMessage linkMsg = (RtNetlinkLinkMessage) msg;
+
+        final ByteBuffer packBuffer = ByteBuffer.allocate(64);
+        packBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        linkMsg.pack(packBuffer);
+        assertEquals(RTM_NEWLINK_PACK_HEX, HexDump.toHexString(packBuffer.array()));
+    }
+
+    private static final String RTM_NEWLINK_TRUNCATED_HEX =
+            "54000000100000000000000000000000"   // struct nlmsghr
+            + "000001001E0000000210000000000000" // struct ifinfo
+            + "08000D00B80B0000"                 // IFLA_PROTINFO
+            + "0500100002000000"                 // IFLA_OPERSTATE
+            + "0800010092C3E3C9"                 // IFLA_ADDRESS(truncated)
+            + "0500110001000000"                 // IFLA_LINKMODE
+            + "0A000300776C616E30000000"         // IFLA_IFNAME(wlan0)
+            + "08000400DC050000";                // IFLA_MTU
+
+    @Test
+    public void testTruncatedRtmNewLink() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_TRUNCATED_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkLinkMessage);
+        final RtNetlinkLinkMessage linkMsg = (RtNetlinkLinkMessage) msg;
+
+        // Truncated IFLA_ADDRESS attribute doesn't affect parsing other attrs.
+        assertNull(linkMsg.getHardwareAddress());
+        assertEquals(ETHER_MTU, linkMsg.getMtu());
+        assertTrue(linkMsg.getInterfaceName().equals("wlan0"));
+    }
+
+    @Test
+    public void testToString() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkLinkMessage);
+        final RtNetlinkLinkMessage linkMsg = (RtNetlinkLinkMessage) msg;
+        final String expected = "RtNetlinkLinkMessage{ "
+                + "nlmsghdr{"
+                + "StructNlMsgHdr{ nlmsg_len{100}, nlmsg_type{16(RTM_NEWLINK)}, nlmsg_flags{0()}, "
+                + "nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "Ifinfomsg{"
+                + "family: 0, type: 1, index: 30, flags: 4098, change: 0}, "
+                + "Hardware Address{92:c3:e3:c9:37:4e}, " + "MTU{1500}, "
+                + "Ifname{wlan0} "
+                + "}";
+        assertEquals(expected, linkMsg.toString());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkNeighborMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkNeighborMessageTest.java
new file mode 100644
index 0000000..4d8900c
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkNeighborMessageTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static com.android.net.module.util.netlink.StructNdMsg.NUD_STALE;
+import static com.android.testutils.NetlinkTestUtils.makeDelNeighMessage;
+import static com.android.testutils.NetlinkTestUtils.makeNewNeighMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkNeighborMessageTest {
+    private static final String TAG = "RtNetlinkNeighborMessageTest";
+
+    public static final byte[] RTM_DELNEIGH = makeDelNeighMessage(
+            InetAddresses.parseNumericAddress("192.168.159.254"), NUD_STALE);
+
+    public static final byte[] RTM_NEWNEIGH = makeNewNeighMessage(
+            InetAddresses.parseNumericAddress("fe80::86c9:b2ff:fe6a:ed4b"), NUD_STALE);
+
+    // An example of the full response from an RTM_GETNEIGH query.
+    private static final String RTM_GETNEIGH_RESPONSE_HEX =
+            // <-- struct nlmsghr             -->|<-- struct ndmsg           -->|<-- struct nlattr: NDA_DST             -->|<-- NDA_LLADDR          -->|<-- NDA_PROBES -->|<-- NDA_CACHEINFO                         -->|
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 15000000 4000 00 05 1400 0100 ff020000000000000000000000000001 0a00 0200 333300000001 0000 0800 0400 00000000 1400 0300 a2280000 32110000 32110000 01000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 15000000 4000 00 05 1400 0100 ff0200000000000000000001ff000001 0a00 0200 3333ff000001 0000 0800 0400 00000000 1400 0300 0d280000 9d100000 9d100000 00000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 15000000 0400 80 01 1400 0100 20010db800040ca00000000000000001 0a00 0200 84c9b26aed4b 0000 0800 0400 04000000 1400 0300 90100000 90100000 90080000 01000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 15000000 4000 00 05 1400 0100 ff0200000000000000000001ff47da19 0a00 0200 3333ff47da19 0000 0800 0400 00000000 1400 0300 a1280000 31110000 31110000 01000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 14000000 4000 00 05 1400 0100 ff020000000000000000000000000016 0a00 0200 333300000016 0000 0800 0400 00000000 1400 0300 912a0000 21130000 21130000 00000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 14000000 4000 00 05 1400 0100 ff0200000000000000000001ffeace3b 0a00 0200 3333ffeace3b 0000 0800 0400 00000000 1400 0300 922a0000 22130000 22130000 00000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 15000000 4000 00 05 1400 0100 ff0200000000000000000001ff5c2a83 0a00 0200 3333ff5c2a83 0000 0800 0400 00000000 1400 0300 391c0000 c9040000 c9040000 01000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 01000000 4000 00 02 1400 0100 00000000000000000000000000000000 0a00 0200 000000000000 0000 0800 0400 00000000 1400 0300 cd180200 5d010200 5d010200 08000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 15000000 4000 00 05 1400 0100 ff020000000000000000000000000002 0a00 0200 333300000002 0000 0800 0400 00000000 1400 0300 352a0000 c5120000 c5120000 00000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 15000000 4000 00 05 1400 0100 ff020000000000000000000000000016 0a00 0200 333300000016 0000 0800 0400 00000000 1400 0300 982a0000 28130000 28130000 00000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 15000000 0800 80 01 1400 0100 fe8000000000000086c9b2fffe6aed4b 0a00 0200 84c9b26aed4b 0000 0800 0400 00000000 1400 0300 23000000 24000000 57000000 13000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 15000000 4000 00 05 1400 0100 ff0200000000000000000001ffeace3b 0a00 0200 3333ffeace3b 0000 0800 0400 00000000 1400 0300 992a0000 29130000 29130000 01000000" +
+            "58000000 1c00 0200 00000000 3e2b0000 0a 00 0000 14000000 4000 00 05 1400 0100 ff020000000000000000000000000002 0a00 0200 333300000002 0000 0800 0400 00000000 1400 0300 2e2a0000 be120000 be120000 00000000" +
+            "44000000 1c00 0200 00000000 3e2b0000 02 00 0000 18000000 4000 00 03 0800 0100 00000000                         0400 0200                   0800 0400 00000000 1400 0300 75280000 05110000 05110000 22000000";
+    public static final byte[] RTM_GETNEIGH_RESPONSE =
+            HexEncoding.decode(RTM_GETNEIGH_RESPONSE_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @Test
+    public void testParseRtmDelNeigh() {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(RTM_DELNEIGH);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkNeighborMessage);
+        final RtNetlinkNeighborMessage neighMsg = (RtNetlinkNeighborMessage) msg;
+
+        final StructNlMsgHdr hdr = neighMsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(76, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_DELNEIGH, hdr.nlmsg_type);
+        assertEquals(0, hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructNdMsg ndmsgHdr = neighMsg.getNdHeader();
+        assertNotNull(ndmsgHdr);
+        assertEquals((byte) OsConstants.AF_INET, ndmsgHdr.ndm_family);
+        assertEquals(21, ndmsgHdr.ndm_ifindex);
+        assertEquals(NUD_STALE, ndmsgHdr.ndm_state);
+        final InetAddress destination = neighMsg.getDestination();
+        assertNotNull(destination);
+        assertEquals(InetAddress.parseNumericAddress("192.168.159.254"), destination);
+    }
+
+    @Test
+    public void testParseRtmNewNeigh() {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(RTM_NEWNEIGH);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkNeighborMessage);
+        final RtNetlinkNeighborMessage neighMsg = (RtNetlinkNeighborMessage) msg;
+
+        final StructNlMsgHdr hdr = neighMsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(88, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
+        assertEquals(0, hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructNdMsg ndmsgHdr = neighMsg.getNdHeader();
+        assertNotNull(ndmsgHdr);
+        assertEquals((byte) OsConstants.AF_INET6, ndmsgHdr.ndm_family);
+        assertEquals(21, ndmsgHdr.ndm_ifindex);
+        assertEquals(NUD_STALE, ndmsgHdr.ndm_state);
+        final InetAddress destination = neighMsg.getDestination();
+        assertNotNull(destination);
+        assertEquals(InetAddress.parseNumericAddress("fe80::86c9:b2ff:fe6a:ed4b"), destination);
+    }
+
+    @Test
+    public void testParseRtmGetNeighResponse() {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(RTM_GETNEIGH_RESPONSE);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+
+        int messageCount = 0;
+        while (byteBuffer.remaining() > 0) {
+            final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+            assertNotNull(msg);
+            assertTrue(msg instanceof RtNetlinkNeighborMessage);
+            final RtNetlinkNeighborMessage neighMsg = (RtNetlinkNeighborMessage) msg;
+
+            final StructNlMsgHdr hdr = neighMsg.getHeader();
+            assertNotNull(hdr);
+            assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
+            assertEquals(StructNlMsgHdr.NLM_F_MULTI, hdr.nlmsg_flags);
+            assertEquals(0, hdr.nlmsg_seq);
+            assertEquals(11070, hdr.nlmsg_pid);
+
+            final int probes = neighMsg.getProbes();
+            assertTrue("Unexpected number of probes. Got " +  probes + ", max=5",
+                    probes < 5);
+            final int ndm_refcnt = neighMsg.getCacheInfo().ndm_refcnt;
+            assertTrue("nda_cacheinfo has unexpectedly high ndm_refcnt: " + ndm_refcnt,
+                    ndm_refcnt < 0x100);
+
+            messageCount++;
+        }
+        // TODO: add more detailed spot checks.
+        assertEquals(14, messageCount);
+    }
+
+    @Test
+    public void testCreateRtmNewNeighMessage() {
+        final int seqNo = 2635;
+        final int ifIndex = 14;
+        final byte[] llAddr =
+                new byte[] { (byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5, (byte) 6 };
+
+        // Hexadecimal representation of our created packet.
+        final String expectedNewNeighHex =
+                // struct nlmsghdr
+                "30000000" +     // length = 48
+                "1c00" +         // type = 28 (RTM_NEWNEIGH)
+                "0501" +         // flags (NLM_F_REQUEST | NLM_F_ACK | NLM_F_REPLACE)
+                "4b0a0000" +     // seqno
+                "00000000" +     // pid (0 == kernel)
+                // struct ndmsg
+                "02" +           // family
+                "00" +           // pad1
+                "0000" +         // pad2
+                "0e000000" +     // interface index (14)
+                "0800" +         // NUD state (0x08 == NUD_DELAY)
+                "00" +           // flags
+                "00" +           // type
+                // struct nlattr: NDA_DST
+                "0800" +         // length = 8
+                "0100" +         // type (1 == NDA_DST, for neighbor messages)
+                "7f000001" +     // IPv4 address (== 127.0.0.1)
+                // struct nlattr: NDA_LLADDR
+                "0a00" +         // length = 10
+                "0200" +         // type (2 == NDA_LLADDR, for neighbor messages)
+                "010203040506" + // MAC Address (== 01:02:03:04:05:06)
+                "0000";          // padding, for 4 byte alignment
+        final byte[] expectedNewNeigh =
+                HexEncoding.decode(expectedNewNeighHex.toCharArray(), false);
+
+        final byte[] bytes = RtNetlinkNeighborMessage.newNewNeighborMessage(
+            seqNo, Inet4Address.LOOPBACK, StructNdMsg.NUD_DELAY, ifIndex, llAddr);
+        if (!Arrays.equals(expectedNewNeigh, bytes)) {
+            assertEquals(expectedNewNeigh.length, bytes.length);
+            for (int i = 0; i < Math.min(expectedNewNeigh.length, bytes.length); i++) {
+                assertEquals(expectedNewNeigh[i], bytes[i]);
+            }
+        }
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
new file mode 100644
index 0000000..50b8278
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
@@ -0,0 +1,367 @@
+/*
+ * 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.net.module.util.netlink;
+
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.NETLINK_ROUTE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
+
+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 android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkRouteMessageTest {
+    private static final IpPrefix TEST_IPV6_GLOBAL_PREFIX = new IpPrefix("2001:db8:1::/64");
+    private static final Inet6Address TEST_IPV6_LINK_LOCAL_GATEWAY =
+            (Inet6Address) InetAddresses.parseNumericAddress("fe80::1");
+
+    // An example of the full RTM_NEWROUTE message.
+    private static final String RTM_NEWROUTE_HEX =
+            "88000000180000060000000000000000"            // struct nlmsghr
+            + "0A400000FC02000100000000"                  // struct rtmsg
+            + "08000F00C7060000"                          // RTA_TABLE
+            + "1400010020010DB8000100000000000000000000"  // RTA_DST
+            + "08000400DF020000"                          // RTA_OIF
+            + "0800060000010000"                          // RTA_PRIORITY
+            + "24000C0000000000000000005EEA000000000000"  // RTA_CACHEINFO
+            + "00000000000000000000000000000000"
+            + "14000500FE800000000000000000000000000001"  // RTA_GATEWAY
+            + "0500140000000000";                         // RTA_PREF
+
+    private ByteBuffer toByteBuffer(final String hexString) {
+        return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+    }
+
+    private void assertRtmRouteMessage(final RtNetlinkRouteMessage routeMsg) {
+        final StructNlMsgHdr hdr = routeMsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(136, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWROUTE, hdr.nlmsg_type);
+        assertEquals(0x600, hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructRtMsg rtmsg = routeMsg.getRtMsgHeader();
+        assertNotNull(rtmsg);
+        assertEquals((byte) OsConstants.AF_INET6, rtmsg.family);
+        assertEquals(64, rtmsg.dstLen);
+        assertEquals(0, rtmsg.srcLen);
+        assertEquals(0, rtmsg.tos);
+        assertEquals(0xFC, rtmsg.table);
+        assertEquals(NetlinkConstants.RTPROT_KERNEL, rtmsg.protocol);
+        assertEquals(NetlinkConstants.RT_SCOPE_UNIVERSE, rtmsg.scope);
+        assertEquals(NetlinkConstants.RTN_UNICAST, rtmsg.type);
+        assertEquals(0, rtmsg.flags);
+
+        assertEquals(routeMsg.getDestination(), TEST_IPV6_GLOBAL_PREFIX);
+        assertEquals(735, routeMsg.getInterfaceIndex());
+        assertEquals((Inet6Address) routeMsg.getGateway(), TEST_IPV6_LINK_LOCAL_GATEWAY);
+
+        assertNotNull(routeMsg.getRtaCacheInfo());
+    }
+
+    @Test
+    public void testParseRtmRouteMessage() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        assertRtmRouteMessage(routeMsg);
+    }
+
+    private static final String RTM_NEWROUTE_PACK_HEX =
+            "4C000000180000060000000000000000"             // struct nlmsghr
+            + "0A400000FC02000100000000"                   // struct rtmsg
+            + "1400010020010DB8000100000000000000000000"   // RTA_DST
+            + "14000500FE800000000000000000000000000001"   // RTA_GATEWAY
+            + "08000400DF020000"                           // RTA_OIF
+            + "24000C0000000000000000005EEA000000000000"   // RTA_CACHEINFO
+            + "00000000000000000000000000000000";
+
+    @Test
+    public void testPackRtmNewRoute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_PACK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+        final ByteBuffer packBuffer = ByteBuffer.allocate(112);
+        packBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        routeMsg.pack(packBuffer);
+        assertEquals(RTM_NEWROUTE_PACK_HEX, HexDump.toHexString(packBuffer.array()));
+    }
+
+    private static final String RTM_GETROUTE_MULTICAST_IPV6_HEX =
+            "1C0000001A0001030000000000000000"             // struct nlmsghr
+            + "810000000000000000000000";                  // struct rtmsg
+
+    private static final String RTM_NEWROUTE_MULTICAST_IPV6_HEX =
+            "88000000180002000000000000000000"             // struct nlmsghr
+            + "81808000FE11000500000000"                   // struct rtmsg
+            + "08000F00FE000000"                           // RTA_TABLE
+            + "14000200FDACC0F1DBDB000195B7C1A464F944EA"   // RTA_SRC
+            + "14000100FF040000000000000000000000001234"   // RTA_DST
+            + "0800030014000000"                           // RTA_IIF
+            + "0C0009000800000111000000"                   // RTA_MULTIPATH
+            + "1C00110001000000000000009400000000000000"   // RTA_STATS
+            + "0000000000000000"
+            + "0C0017007617000000000000";                  // RTA_EXPIRES
+
+    @Test
+    public void testParseRtmNewRoute_MulticastIpv6() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        final StructNlMsgHdr hdr = routeMsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(136, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWROUTE, hdr.nlmsg_type);
+
+        final StructRtMsg rtmsg = routeMsg.getRtMsgHeader();
+        assertNotNull(rtmsg);
+        assertEquals((byte) 129, (byte) rtmsg.family);
+        assertEquals(128, rtmsg.dstLen);
+        assertEquals(128, rtmsg.srcLen);
+        assertEquals(0xFE, rtmsg.table);
+
+        assertEquals(routeMsg.getSource(),
+                new IpPrefix("fdac:c0f1:dbdb:1:95b7:c1a4:64f9:44ea/128"));
+        assertEquals(routeMsg.getDestination(), new IpPrefix("ff04::1234/128"));
+        assertEquals(20, routeMsg.getIifIndex());
+        assertEquals(60060, routeMsg.getSinceLastUseMillis());
+    }
+
+    // NEWROUTE message for multicast IPv6 with the packed attributes
+    private static final String RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX =
+            "58000000180002000000000000000000"             // struct nlmsghr
+            + "81808000FE11000500000000"                   // struct rtmsg
+            + "14000200FDACC0F1DBDB000195B7C1A464F944EA"   // RTA_SRC
+            + "14000100FF040000000000000000000000001234"   // RTA_DST
+            + "0800030014000000"                           // RTA_IIF
+            + "0C0017007617000000000000";                  // RTA_EXPIRES
+    @Test
+    public void testPackRtmNewRoute_MulticastIpv6() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+        final ByteBuffer packBuffer = ByteBuffer.allocate(88);
+        packBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        routeMsg.pack(packBuffer);
+        assertEquals(RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX,
+                HexDump.toHexString(packBuffer.array()));
+    }
+
+    private static final String RTM_NEWROUTE_TRUNCATED_HEX =
+            "48000000180000060000000000000000"             // struct nlmsghr
+            + "0A400000FC02000100000000"                   // struct rtmsg
+            + "1400010020010DB8000100000000000000000000"   // RTA_DST
+            + "10000500FE8000000000000000000000"           // RTA_GATEWAY(truncated)
+            + "08000400DF020000";                          // RTA_OIF
+
+    @Test
+    public void testTruncatedRtmNewRoute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_TRUNCATED_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        // Parsing RTM_NEWROUTE with truncated RTA_GATEWAY attribute returns null.
+        assertNull(msg);
+    }
+
+    private static final String RTM_NEWROUTE_IPV4_MAPPED_IPV6_GATEWAY_HEX =
+            "4C000000180000060000000000000000"             // struct nlmsghr
+            + "0A400000FC02000100000000"                   // struct rtmsg
+            + "1400010020010DB8000100000000000000000000"   // RTA_DST(2001:db8:1::/64)
+            + "1400050000000000000000000000FFFF0A010203"   // RTA_GATEWAY(::ffff:10.1.2.3)
+            + "08000400DF020000";                          // RTA_OIF
+
+    @Test
+    public void testParseRtmRouteMessage_IPv4MappedIPv6Gateway() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_IPV4_MAPPED_IPV6_GATEWAY_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        // Parsing RTM_NEWROUTE with IPv4-mapped IPv6 gateway address, which doesn't match
+        // rtm_family after address parsing.
+        assertNull(msg);
+    }
+
+    private static final String RTM_NEWROUTE_IPV4_MAPPED_IPV6_DST_HEX =
+            "4C000000180000060000000000000000"             // struct nlmsghr
+            + "0A780000FC02000100000000"                   // struct rtmsg
+            + "1400010000000000000000000000FFFF0A000000"   // RTA_DST(::ffff:10.0.0.0/120)
+            + "14000500FE800000000000000000000000000001"   // RTA_GATEWAY(fe80::1)
+            + "08000400DF020000";                          // RTA_OIF
+
+    @Test
+    public void testParseRtmRouteMessage_IPv4MappedIPv6Destination() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_IPV4_MAPPED_IPV6_DST_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        // Parsing RTM_NEWROUTE with IPv4-mapped IPv6 destination prefix, which doesn't match
+        // rtm_family after address parsing.
+        assertNull(msg);
+    }
+
+    // An example of the full RTM_NEWADDR message.
+    private static final String RTM_NEWADDR_HEX =
+            "48000000140000000000000000000000"            // struct nlmsghr
+            + "0A4080FD1E000000"                          // struct ifaddrmsg
+            + "14000100FE800000000000002C415CFFFE096665"  // IFA_ADDRESS
+            + "14000600100E0000201C00002A70000045700000"  // IFA_CACHEINFO
+            + "0800080080000000";                         // IFA_FLAGS
+
+    @Test
+    public void testParseMultipleRtmMessagesInOneByteBuffer() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX + RTM_NEWADDR_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+
+        // Try to parse the RTM_NEWROUTE message.
+        NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        assertRtmRouteMessage(routeMsg);
+
+        // Try to parse the RTM_NEWADDR message.
+        msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkAddressMessage);
+    }
+
+    @Test
+    public void testToString() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        final String expected = "RtNetlinkRouteMessage{ "
+                + "nlmsghdr{"
+                + "StructNlMsgHdr{ nlmsg_len{136}, nlmsg_type{24(RTM_NEWROUTE)}, "
+                + "nlmsg_flags{1536(NLM_F_MATCH)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "Rtmsg{"
+                + "family: 10, dstLen: 64, srcLen: 0, tos: 0, table: 252, protocol: 2, "
+                + "scope: 0, type: 1, flags: 0}, "
+                + "destination{2001:db8:1::}, "
+                + "gateway{fe80::1}, "
+                + "oifindex{735}, "
+                + "rta_cacheinfo{clntref: 0, lastuse: 0, expires: 59998, error: 0, used: 0, "
+                + "id: 0, ts: 0, tsage: 0} "
+                + "}";
+        assertEquals(expected, routeMsg.toString());
+    }
+
+    @Test
+    public void testToString_RtmGetRoute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_GETROUTE_MULTICAST_IPV6_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        final String expected = "RtNetlinkRouteMessage{ "
+                + "nlmsghdr{"
+                + "StructNlMsgHdr{ nlmsg_len{28}, nlmsg_type{26(RTM_GETROUTE)}, "
+                + "nlmsg_flags{769(NLM_F_REQUEST|NLM_F_DUMP)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "Rtmsg{"
+                + "family: 129, dstLen: 0, srcLen: 0, tos: 0, table: 0, protocol: 0, "
+                + "scope: 0, type: 0, flags: 0}, "
+                + "destination{::}, "
+                + "gateway{}, "
+                + "oifindex{0}, "
+                + "rta_cacheinfo{} "
+                + "}";
+        assertEquals(expected, routeMsg.toString());
+    }
+
+    @Test
+    public void testToString_RtmNewRouteMulticastIpv6() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        final String expected = "RtNetlinkRouteMessage{ "
+                + "nlmsghdr{"
+                + "StructNlMsgHdr{ nlmsg_len{136}, nlmsg_type{24(RTM_NEWROUTE)}, "
+                + "nlmsg_flags{2(NLM_F_MULTI)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "Rtmsg{"
+                + "family: 129, dstLen: 128, srcLen: 128, tos: 0, table: 254, protocol: 17, "
+                + "scope: 0, type: 5, flags: 0}, "
+                + "source{fdac:c0f1:dbdb:1:95b7:c1a4:64f9:44ea}, "
+                + "destination{ff04::1234}, "
+                + "gateway{}, "
+                + "iifindex{20}, "
+                + "oifindex{0}, "
+                + "rta_cacheinfo{} "
+                + "sinceLastUseMillis{60060}"
+                + "}";
+        assertEquals(expected, routeMsg.toString());
+    }
+
+    @Test
+    public void testGetRtmFamily_RTNL_FAMILY_IP6MR() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+        assertEquals(RTNL_FAMILY_IP6MR, routeMsg.getRtmFamily());
+    }
+
+    @Test
+    public void testGetRtmFamily_AF_INET6() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+        assertEquals(AF_INET6, routeMsg.getRtmFamily());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructInetDiagSockIdTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructInetDiagSockIdTest.java
new file mode 100644
index 0000000..ce190f2
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructInetDiagSockIdTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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.net.module.util.netlink;
+
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructInetDiagSockIdTest {
+    private static final Inet4Address IPV4_SRC_ADDR =
+            (Inet4Address) InetAddresses.parseNumericAddress("192.0.2.1");
+    private static final Inet4Address IPV4_DST_ADDR =
+            (Inet4Address) InetAddresses.parseNumericAddress("198.51.100.1");
+    private static final Inet6Address IPV6_SRC_ADDR =
+            (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+    private static final Inet6Address IPV6_DST_ADDR =
+            (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::2");
+    private static final int SRC_PORT = 65297;
+    private static final int DST_PORT = 443;
+    private static final int IF_INDEX = 7;
+    private static final long COOKIE = 561;
+
+    private static final byte[] INET_DIAG_SOCKET_ID_IPV4 =
+            new byte[] {
+                    // src port, dst port
+                    (byte) 0xff, (byte) 0x11, (byte) 0x01, (byte) 0xbb,
+                    // src address
+                    (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    // dst address
+                    (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    // if index
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    // cookie
+                    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+                    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff
+            };
+
+    private static final byte[] INET_DIAG_SOCKET_ID_IPV4_IF_COOKIE =
+            new byte[] {
+                    // src port, dst port
+                    (byte) 0xff, (byte) 0x11, (byte) 0x01, (byte) 0xbb,
+                    // src address
+                    (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    // dst address
+                    (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    // if index
+                    (byte) 0x07, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    // cookie
+                    (byte) 0x31, (byte) 0x02, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            };
+
+    private static final byte[] INET_DIAG_SOCKET_ID_IPV6 =
+            new byte[] {
+                    // src port, dst port
+                    (byte) 0xff, (byte) 0x11, (byte) 0x01, (byte) 0xbb,
+                    // src address
+                    (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,
+                    // dst address
+                    (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,
+                    // if index
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    // cookie
+                    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+                    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff
+            };
+
+    private static final byte[] INET_DIAG_SOCKET_ID_IPV6_IF_COOKIE =
+            new byte[] {
+                    // src port, dst port
+                    (byte) 0xff, (byte) 0x11, (byte) 0x01, (byte) 0xbb,
+                    // src address
+                    (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,
+                    // dst address
+                    (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,
+                    // if index
+                    (byte) 0x07, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    // cookie
+                    (byte) 0x31, (byte) 0x02, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            };
+
+    @Test
+    public void testPackStructInetDiagSockIdWithIpv4() {
+        final InetSocketAddress srcAddr = new InetSocketAddress(IPV4_SRC_ADDR, SRC_PORT);
+        final InetSocketAddress dstAddr = new InetSocketAddress(IPV4_DST_ADDR, DST_PORT);
+        final StructInetDiagSockId sockId = new StructInetDiagSockId(srcAddr, dstAddr);
+        final ByteBuffer buffer = ByteBuffer.allocate(StructInetDiagSockId.STRUCT_SIZE);
+        sockId.pack(buffer);
+        assertArrayEquals(INET_DIAG_SOCKET_ID_IPV4, buffer.array());
+    }
+
+    @Test
+    public void testPackStructInetDiagSockIdWithIpv6() {
+        final InetSocketAddress srcAddr = new InetSocketAddress(IPV6_SRC_ADDR, SRC_PORT);
+        final InetSocketAddress dstAddr = new InetSocketAddress(IPV6_DST_ADDR, DST_PORT);
+        final StructInetDiagSockId sockId = new StructInetDiagSockId(srcAddr, dstAddr);
+        final ByteBuffer buffer = ByteBuffer.allocate(StructInetDiagSockId.STRUCT_SIZE);
+        sockId.pack(buffer);
+        assertArrayEquals(INET_DIAG_SOCKET_ID_IPV6, buffer.array());
+    }
+
+    @Test
+    public void testPackStructInetDiagSockIdWithIpv4IfIndexCookie() {
+        final InetSocketAddress srcAddr = new InetSocketAddress(IPV4_SRC_ADDR, SRC_PORT);
+        final InetSocketAddress dstAddr = new InetSocketAddress(IPV4_DST_ADDR, DST_PORT);
+        final StructInetDiagSockId sockId =
+                new StructInetDiagSockId(srcAddr, dstAddr, IF_INDEX, COOKIE);
+        final ByteBuffer buffer = ByteBuffer.allocate(StructInetDiagSockId.STRUCT_SIZE);
+        sockId.pack(buffer);
+        assertArrayEquals(INET_DIAG_SOCKET_ID_IPV4_IF_COOKIE, buffer.array());
+    }
+
+    @Test
+    public void testPackStructInetDiagSockIdWithIpv6IfIndexCookie() {
+        final InetSocketAddress srcAddr = new InetSocketAddress(IPV6_SRC_ADDR, SRC_PORT);
+        final InetSocketAddress dstAddr = new InetSocketAddress(IPV6_DST_ADDR, DST_PORT);
+        final StructInetDiagSockId sockId =
+                new StructInetDiagSockId(srcAddr, dstAddr, IF_INDEX, COOKIE);
+        final ByteBuffer buffer = ByteBuffer.allocate(StructInetDiagSockId.STRUCT_SIZE);
+        sockId.pack(buffer);
+        assertArrayEquals(INET_DIAG_SOCKET_ID_IPV6_IF_COOKIE, buffer.array());
+    }
+
+    @Test
+    public void testParseStructInetDiagSockIdWithIpv4() {
+        final ByteBuffer buffer = ByteBuffer.wrap(INET_DIAG_SOCKET_ID_IPV4_IF_COOKIE);
+        final StructInetDiagSockId sockId = StructInetDiagSockId.parse(buffer, (byte) AF_INET);
+
+        assertEquals(SRC_PORT, sockId.locSocketAddress.getPort());
+        assertEquals(IPV4_SRC_ADDR, sockId.locSocketAddress.getAddress());
+        assertEquals(DST_PORT, sockId.remSocketAddress.getPort());
+        assertEquals(IPV4_DST_ADDR, sockId.remSocketAddress.getAddress());
+        assertEquals(IF_INDEX, sockId.ifIndex);
+        assertEquals(COOKIE, sockId.cookie);
+    }
+
+    @Test
+    public void testParseStructInetDiagSockIdWithIpv6() {
+        final ByteBuffer buffer = ByteBuffer.wrap(INET_DIAG_SOCKET_ID_IPV6_IF_COOKIE);
+        final StructInetDiagSockId sockId = StructInetDiagSockId.parse(buffer, (byte) AF_INET6);
+
+        assertEquals(SRC_PORT, sockId.locSocketAddress.getPort());
+        assertEquals(IPV6_SRC_ADDR, sockId.locSocketAddress.getAddress());
+        assertEquals(DST_PORT, sockId.remSocketAddress.getPort());
+        assertEquals(IPV6_DST_ADDR, sockId.remSocketAddress.getAddress());
+        assertEquals(IF_INDEX, sockId.ifIndex);
+        assertEquals(COOKIE, sockId.cookie);
+    }
+
+    @Test
+    public void testToStringStructInetDiagSockIdWithIpv4() {
+        final InetSocketAddress srcAddr = new InetSocketAddress(IPV4_SRC_ADDR, SRC_PORT);
+        final InetSocketAddress dstAddr = new InetSocketAddress(IPV4_DST_ADDR, DST_PORT);
+        final StructInetDiagSockId sockId = new StructInetDiagSockId(srcAddr, dstAddr);
+        assertEquals("StructInetDiagSockId{ idiag_sport{65297}, idiag_dport{443},"
+                + " idiag_src{192.0.2.1}, idiag_dst{198.51.100.1}, idiag_if{0},"
+                + " idiag_cookie{INET_DIAG_NOCOOKIE}}", sockId.toString());
+    }
+
+    @Test
+    public void testToStringStructInetDiagSockIdWithIpv6() {
+        final InetSocketAddress srcAddr = new InetSocketAddress(IPV6_SRC_ADDR, SRC_PORT);
+        final InetSocketAddress dstAddr = new InetSocketAddress(IPV6_DST_ADDR, DST_PORT);
+        final StructInetDiagSockId sockId = new StructInetDiagSockId(srcAddr, dstAddr);
+        assertEquals("StructInetDiagSockId{ idiag_sport{65297}, idiag_dport{443},"
+                + " idiag_src{2001:db8::1}, idiag_dst{2001:db8::2}, idiag_if{0},"
+                + " idiag_cookie{INET_DIAG_NOCOOKIE}}", sockId.toString());
+    }
+}
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/StructNdOptPref64Test.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPref64Test.java
new file mode 100644
index 0000000..beed838
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPref64Test.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static com.android.net.module.util.netlink.StructNdOptPref64.getScaledLifetimePlc;
+import static com.android.net.module.util.netlink.StructNdOptPref64.plcToPrefixLength;
+import static com.android.net.module.util.netlink.StructNdOptPref64.prefixLengthToPlc;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.annotation.SuppressLint;
+import android.net.IpPrefix;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructNdOptPref64Test {
+
+    private static final String PREFIX1 = "64:ff9b::";
+    private static final String PREFIX2 = "2001:db8:1:2:3:64::";
+
+    private static byte[] prefixBytes(String addrString) throws Exception {
+        InetAddress addr = InetAddress.getByName(addrString);
+        byte[] prefixBytes = new byte[12];
+        System.arraycopy(addr.getAddress(), 0, prefixBytes, 0, 12);
+        return prefixBytes;
+    }
+
+    @SuppressLint("NewApi")
+    private static IpPrefix prefix(String addrString, int prefixLength) throws Exception {
+        return new IpPrefix(InetAddress.getByName(addrString), prefixLength);
+    }
+
+    private void assertPref64OptMatches(int lifetime, IpPrefix prefix, StructNdOptPref64 opt) {
+        assertEquals(StructNdOptPref64.TYPE, opt.type);
+        assertEquals(2, opt.length);
+        assertEquals(lifetime, opt.lifetime);
+        assertEquals(prefix, opt.prefix);
+    }
+
+    private void assertToByteBufferMatches(StructNdOptPref64 opt, String expected) {
+        String actual = HexEncoding.encodeToString(opt.toByteBuffer().array());
+        assertEquals(expected, actual);
+    }
+
+    private ByteBuffer makeNdOptPref64(int lifetime, byte[] prefix, int prefixLengthCode) {
+        if (prefix.length != 12) throw new IllegalArgumentException("Prefix must be 12 bytes");
+
+        ByteBuffer buf = ByteBuffer.allocate(16)
+                .put((byte) StructNdOptPref64.TYPE)
+                .put((byte) StructNdOptPref64.LENGTH)
+                .putShort(getScaledLifetimePlc(lifetime, prefixLengthCode))
+                .put(prefix, 0, 12);
+
+        buf.flip();
+        return buf;
+    }
+
+    @Test
+    public void testParseCannedOption() throws Exception {
+        String hexBytes = "2602"               // type=38, len=2 (16 bytes)
+                + "0088"                       // lifetime=136, PLC=0 (/96)
+                + "20010DB80003000400050006";  // 2001:db8:3:4:5:6/96
+        byte[] rawBytes = HexEncoding.decode(hexBytes);
+        StructNdOptPref64 opt = StructNdOptPref64.parse(ByteBuffer.wrap(rawBytes));
+        assertPref64OptMatches(136, prefix("2001:DB8:3:4:5:6::", 96), opt);
+        assertToByteBufferMatches(opt, hexBytes);
+
+        hexBytes = "2602"                      // type=38, len=2 (16 bytes)
+                + "2752"                       // lifetime=10064, PLC=2 (/56)
+                + "0064FF9B0000000000000000";  // 64:ff9b::/56
+        rawBytes = HexEncoding.decode(hexBytes);
+        opt = StructNdOptPref64.parse(ByteBuffer.wrap(rawBytes));
+        assertPref64OptMatches(10064, prefix("64:FF9B::", 56), opt);
+        assertToByteBufferMatches(opt, hexBytes);
+    }
+
+    @Test
+    public void testParsing() throws Exception {
+        // Valid.
+        ByteBuffer buf = makeNdOptPref64(600, prefixBytes(PREFIX1), 0);
+        StructNdOptPref64 opt = StructNdOptPref64.parse(buf);
+        assertPref64OptMatches(600, prefix(PREFIX1, 96), opt);
+
+        // Valid, zero lifetime, /64.
+        buf = makeNdOptPref64(0, prefixBytes(PREFIX1), 1);
+        opt = StructNdOptPref64.parse(buf);
+        assertPref64OptMatches(0, prefix(PREFIX1, 64), opt);
+
+        // Valid, low lifetime, /56.
+        buf = makeNdOptPref64(8, prefixBytes(PREFIX2), 2);
+        opt = StructNdOptPref64.parse(buf);
+        assertPref64OptMatches(8, prefix(PREFIX2, 56), opt);
+        assertEquals(new IpPrefix("2001:db8:1::/56"), opt.prefix);  // Prefix is truncated.
+
+        // Valid, maximum lifetime, /32.
+        buf = makeNdOptPref64(65528, prefixBytes(PREFIX2), 5);
+        opt = StructNdOptPref64.parse(buf);
+        assertPref64OptMatches(65528, prefix(PREFIX2, 32), opt);
+        assertEquals(new IpPrefix("2001:db8::/32"), opt.prefix);  // Prefix is truncated.
+
+        // Lifetime not divisible by 8.
+        buf = makeNdOptPref64(300, prefixBytes(PREFIX2), 0);
+        opt = StructNdOptPref64.parse(buf);
+        assertPref64OptMatches(296, prefix(PREFIX2, 96), opt);
+
+        // Invalid prefix length codes.
+        buf = makeNdOptPref64(600, prefixBytes(PREFIX1), 6);
+        assertNull(StructNdOptPref64.parse(buf));
+        buf = makeNdOptPref64(600, prefixBytes(PREFIX1), 7);
+        assertNull(StructNdOptPref64.parse(buf));
+
+        // Truncated to varying lengths...
+        buf = makeNdOptPref64(600, prefixBytes(PREFIX1), 3);
+        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",
+                    StructNdOptPref64.parse(buf));
+        }
+        buf.flip();
+        buf.limit(len);
+        // ... but otherwise OK.
+        opt = StructNdOptPref64.parse(buf);
+        assertPref64OptMatches(600, prefix(PREFIX1, 48), opt);
+    }
+
+    @Test
+    public void testToString() throws Exception {
+        ByteBuffer buf = makeNdOptPref64(600, prefixBytes(PREFIX1), 4);
+        StructNdOptPref64 opt = StructNdOptPref64.parse(buf);
+        assertPref64OptMatches(600, prefix(PREFIX1, 40), opt);
+        assertEquals("NdOptPref64(64:ff9b::/40, 600)", opt.toString());
+    }
+
+    private void assertInvalidPlc(int plc) {
+        assertThrows(IllegalArgumentException.class, () -> plcToPrefixLength(plc));
+    }
+
+    @Test
+    public void testPrefixLengthToPlc() {
+        for (int i = 0; i < 6; i++) {
+            assertEquals(i, prefixLengthToPlc(plcToPrefixLength(i)));
+        }
+        assertInvalidPlc(-1);
+        assertInvalidPlc(6);
+        assertInvalidPlc(7);
+        assertEquals(0, prefixLengthToPlc(96));
+    }
+
+
+    private void assertInvalidParameters(IpPrefix prefix, int lifetime) {
+        assertThrows(IllegalArgumentException.class, () -> new StructNdOptPref64(prefix, lifetime));
+    }
+
+    @Test
+    public void testToByteBuffer() throws Exception {
+        final IpPrefix prefix1 = prefix(PREFIX1, 56);
+        final IpPrefix prefix2 = prefix(PREFIX2, 96);
+
+        StructNdOptPref64 opt = new StructNdOptPref64(prefix1, 600);
+        assertToByteBufferMatches(opt, "2602025A0064FF9B0000000000000000");
+        assertEquals(new IpPrefix("64:ff9b::/56"), opt.prefix);
+        assertEquals(600, opt.lifetime);
+
+        opt = new StructNdOptPref64(prefix2, 65519);
+        assertToByteBufferMatches(opt, "2602FFE820010DB80001000200030064");
+        assertEquals(new IpPrefix("2001:db8:1:2:3:64::/96"), opt.prefix);
+        assertEquals(65512, opt.lifetime);
+
+        assertInvalidParameters(prefix1, 65535);
+        assertInvalidParameters(prefix2, -1);
+        assertInvalidParameters(prefix("1.2.3.4", 32), 600);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptRdnssTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptRdnssTest.java
new file mode 100644
index 0000000..1dcb9b5
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptRdnssTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.net.module.util.netlink;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_RDNSS;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.net.InetAddresses;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.structs.RdnssOption;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructNdOptRdnssTest {
+    private static final String DNS_SERVER1 = "2001:4860:4860::64";
+    private static final String DNS_SERVER2 = "2001:4860:4860::6464";
+
+    private static final Inet6Address[] DNS_SERVER_ADDRESSES = new Inet6Address[] {
+            (Inet6Address) InetAddresses.parseNumericAddress(DNS_SERVER1),
+            (Inet6Address) InetAddresses.parseNumericAddress(DNS_SERVER2),
+    };
+
+    private static final String RDNSS_OPTION_BYTES =
+            "1905"                                 // type=25, len=5 (40 bytes)
+            + "0000"                               // reserved
+            + "00000E10"                           // lifetime=3600
+            + "20014860486000000000000000000064"   // 2001:4860:4860::64
+            + "20014860486000000000000000006464";  // 2001:4860:4860::6464
+
+    private static final String RDNSS_INFINITY_LIFETIME_OPTION_BYTES =
+            "1905"                                 // type=25, len=3 (24 bytes)
+            + "0000"                               // reserved
+            + "FFFFFFFF"                           // lifetime=0xffffffff
+            + "20014860486000000000000000000064"   // 2001:4860:4860::64
+            + "20014860486000000000000000006464";  // 2001:4860:4860::6464
+
+    private void assertRdnssOptMatches(final StructNdOptRdnss opt, int length, long lifetime,
+            final Inet6Address[] servers) {
+        assertEquals(StructNdOptRdnss.TYPE, opt.type);
+        assertEquals(length, opt.length);
+        assertEquals(lifetime, opt.header.lifetime);
+        assertEquals(servers, opt.servers);
+    }
+
+    private ByteBuffer makeRdnssOption(byte type, byte length, long lifetime, String... servers)
+            throws Exception {
+        final ByteBuffer buf = ByteBuffer.allocate(8 + servers.length * 16)
+                .put(type)
+                .put(length)
+                .putShort((short) 0) // Reserved
+                .putInt((int) (lifetime & 0xFFFFFFFFL));
+        for (int i = 0; i < servers.length; i++) {
+            final byte[] rawBytes =
+                    ((Inet6Address) InetAddresses.parseNumericAddress(servers[i])).getAddress();
+            buf.put(rawBytes);
+        }
+        buf.flip();
+        return buf;
+    }
+
+    private void assertToByteBufferMatches(StructNdOptRdnss opt, String expected) {
+        String actual = HexEncoding.encodeToString(opt.toByteBuffer().array());
+        assertEquals(expected, actual);
+    }
+
+    private void doRdnssOptionParsing(final String optionHexString, int length, long lifetime,
+            final Inet6Address[] servers) {
+        final byte[] rawBytes = HexEncoding.decode(optionHexString);
+        final StructNdOptRdnss opt = StructNdOptRdnss.parse(ByteBuffer.wrap(rawBytes));
+        assertRdnssOptMatches(opt, length, lifetime, servers);
+        assertToByteBufferMatches(opt, optionHexString);
+    }
+
+    @Test
+    public void testParsing() throws Exception {
+        doRdnssOptionParsing(RDNSS_OPTION_BYTES, 5 /* length */, 3600 /* lifetime */,
+                DNS_SERVER_ADDRESSES);
+    }
+
+    @Test
+    public void testParsing_infinityLifetime() throws Exception {
+        doRdnssOptionParsing(RDNSS_INFINITY_LIFETIME_OPTION_BYTES, 5 /* length */,
+                0xffffffffL /* lifetime */, DNS_SERVER_ADDRESSES);
+    }
+
+    @Test
+    public void testToByteBuffer() {
+        final StructNdOptRdnss rdnss = new StructNdOptRdnss(DNS_SERVER_ADDRESSES, 3600);
+        assertToByteBufferMatches(rdnss, RDNSS_OPTION_BYTES);
+    }
+
+    @Test
+    public void testToByteBuffer_infinityLifetime() {
+        final StructNdOptRdnss rdnss = new StructNdOptRdnss(DNS_SERVER_ADDRESSES, 0xffffffffL);
+        assertToByteBufferMatches(rdnss, RDNSS_INFINITY_LIFETIME_OPTION_BYTES);
+    }
+
+    @Test
+    public void testParsing_invalidType() throws Exception {
+        final ByteBuffer buf = makeRdnssOption((byte) 38, (byte) 5 /* length */,
+                3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        assertNull(StructNdOptRdnss.parse(buf));
+    }
+
+    @Test
+    public void testParsing_smallOptionLength() throws Exception {
+        final ByteBuffer buf = makeRdnssOption((byte) ICMPV6_ND_OPTION_RDNSS,
+                (byte) 2 /* length */, 3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        assertNull(StructNdOptRdnss.parse(buf));
+    }
+
+    @Test
+    public void testParsing_oddOptionLength() throws Exception {
+        final ByteBuffer buf = makeRdnssOption((byte) ICMPV6_ND_OPTION_RDNSS,
+                (byte) 6 /* length */, 3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        assertNull(StructNdOptRdnss.parse(buf));
+    }
+
+    @Test
+    public void testParsing_truncatedByteBuffer() throws Exception {
+        ByteBuffer buf = makeRdnssOption((byte) ICMPV6_ND_OPTION_RDNSS,
+                (byte) 5 /* length */, 3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        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",
+                    StructNdOptRdnss.parse(buf));
+        }
+        buf.flip();
+        buf.limit(len);
+
+        final StructNdOptRdnss opt = StructNdOptRdnss.parse(buf);
+        assertRdnssOptMatches(opt, 5 /* length */, 3600 /* lifetime */, DNS_SERVER_ADDRESSES);
+    }
+
+    @Test
+    public void testParsing_invalidByteBufferLength() throws Exception {
+        final ByteBuffer buf = makeRdnssOption((byte) ICMPV6_ND_OPTION_RDNSS,
+                (byte) 5 /* length */, 3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        buf.limit(20); // less than MIN_OPT_LEN * 8
+        assertNull(StructNdOptRdnss.parse(buf));
+    }
+
+    @Test
+    public void testConstructor_nullDnsServerAddressArray() {
+        assertThrows(NullPointerException.class,
+                () -> new StructNdOptRdnss(null /* servers */, 3600 /* lifetime */));
+    }
+
+    @Test
+    public void testConstructor_emptyDnsServerAddressArray() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new StructNdOptRdnss(new Inet6Address[0] /* empty server array */,
+                                           3600 /* lifetime*/));
+    }
+
+    @Test
+    public void testToString() {
+        final ByteBuffer buf = RdnssOption.build(3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        final StructNdOptRdnss opt = StructNdOptRdnss.parse(buf);
+        final String expected = "NdOptRdnss(type: 25, length: 5, reserved: 0, lifetime: 3600,"
+                + "servers:[2001:4860:4860::64,2001:4860:4860::6464])";
+        assertRdnssOptMatches(opt, 5 /* length */, 3600 /* lifetime */, DNS_SERVER_ADDRESSES);
+        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
new file mode 100644
index 0000000..b5e3dff
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.net.module.util.netlink;
+
+import static com.android.net.module.util.netlink.RtNetlinkAddressMessage.IFA_FLAGS;
+import static com.android.net.module.util.netlink.RtNetlinkLinkMessage.IFLA_ADDRESS;
+import static com.android.net.module.util.netlink.RtNetlinkLinkMessage.IFLA_IFNAME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.net.MacAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructNlAttrTest {
+    private static final MacAddress TEST_MAC_ADDRESS = MacAddress.fromString("00:11:22:33:44:55");
+    private static final String TEST_INTERFACE_NAME = "wlan0";
+    private static final int TEST_ADDR_FLAGS = 0x80;
+
+    @Test
+    public void testGetValueAsMacAddress() {
+        final StructNlAttr attr1 = new StructNlAttr(IFLA_ADDRESS, TEST_MAC_ADDRESS);
+        final MacAddress address1 = attr1.getValueAsMacAddress();
+        assertEquals(address1, TEST_MAC_ADDRESS);
+
+        // Invalid mac address byte array.
+        final byte[] array = new byte[] {
+                (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33,
+                (byte) 0x44, (byte) 0x55, (byte) 0x66,
+        };
+        final StructNlAttr attr2 = new StructNlAttr(IFLA_ADDRESS, array);
+        final MacAddress address2 = attr2.getValueAsMacAddress();
+        assertNull(address2);
+    }
+
+    @Test
+    public void testGetValueAsString() {
+        final StructNlAttr attr1 = new StructNlAttr(IFLA_IFNAME, TEST_INTERFACE_NAME);
+        final String str1 = attr1.getValueAsString();
+        assertEquals(str1, TEST_INTERFACE_NAME);
+
+        final byte[] array = new byte[] {
+                (byte) 0x77, (byte) 0x6c, (byte) 0x61, (byte) 0x6E, (byte) 0x30, (byte) 0x00,
+        };
+        final StructNlAttr attr2 = new StructNlAttr(IFLA_IFNAME, array);
+        final String str2 = attr2.getValueAsString();
+        assertEquals(str2, TEST_INTERFACE_NAME);
+    }
+
+    @Test
+    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, Integer.valueOf(TEST_ADDR_FLAGS));
+        assertEquals(int1, TEST_ADDR_FLAGS);
+
+        // Malformed attribute.
+        final byte[] malformed_int = new byte[] { (byte) 0x0, (byte) 0x0, (byte) 0x80, };
+        final StructNlAttr attr2 = new StructNlAttr(IFA_FLAGS, malformed_int);
+        final Integer integer2 = attr2.getValueAsInteger();
+        final int int2 = attr2.getValueAsInt(0x08 /* default value */);
+        assertNull(integer2);
+        assertEquals(int2, 0x08 /* default value */);
+
+        // Null attribute value.
+        final byte[] null_int = null;
+        final StructNlAttr attr3 = new StructNlAttr(IFA_FLAGS, null_int);
+        final Integer integer3 = attr3.getValueAsInteger();
+        final int int3 = attr3.getValueAsInt(0x08 /* default value */);
+        assertNull(integer3);
+        assertEquals(int3, 0x08 /* default value */);
+    }
+
+    @Test
+    public void testGetValueAsLong() {
+        final Long input = 1234567L;
+        // Not a real netlink attribute, just for testing
+        final StructNlAttr attr = new StructNlAttr(IFA_FLAGS, input);
+
+        final Long output = attr.getValueAsLong();
+
+        assertEquals(input, output);
+    }
+
+    @Test
+    public void testGetValueAsLong_malformed() {
+        final int input = 1234567;
+        // Not a real netlink attribute, just for testing
+        final StructNlAttr attr = new StructNlAttr(IFA_FLAGS, input);
+
+        final Long output = attr.getValueAsLong();
+
+        assertNull(output);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
new file mode 100644
index 0000000..a0d8b8c
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.netlink;
+
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static org.junit.Assert.fail;
+
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructNlMsgHdrTest {
+
+    public static final short TEST_NLMSG_LEN = 16;
+    public static final short TEST_NLMSG_FLAGS = StructNlMsgHdr.NLM_F_REQUEST
+            | StructNlMsgHdr.NLM_F_MULTI | StructNlMsgHdr.NLM_F_ACK | StructNlMsgHdr.NLM_F_ECHO;
+    public static final short TEST_NLMSG_SEQ = 1234;
+    public static final short TEST_NLMSG_PID = 5678;
+
+    // Checking the header string nlmsg_{len, ..} of the number can make sure that the checking
+    // number comes from the expected element.
+    // TODO: Verify more flags once StructNlMsgHdr can distinguish the flags which have the same
+    // value. For example, NLM_F_MATCH (0x200) and NLM_F_EXCL (0x200) can't be distinguished.
+    // See StructNlMsgHdrTest#stringForNlMsgFlags.
+    public static final String TEST_NLMSG_LEN_STR = "nlmsg_len{16}";
+    public static final String TEST_NLMSG_FLAGS_STR =
+            "NLM_F_REQUEST|NLM_F_MULTI|NLM_F_ACK|NLM_F_ECHO";
+    public static final String TEST_NLMSG_SEQ_STR = "nlmsg_seq{1234}";
+    public static final String TEST_NLMSG_PID_STR = "nlmsg_pid{5678}";
+
+    private StructNlMsgHdr makeStructNlMsgHdr(short type) {
+        return makeStructNlMsgHdr(type, TEST_NLMSG_FLAGS);
+    }
+
+    private StructNlMsgHdr makeStructNlMsgHdr(short type, short flags) {
+        final StructNlMsgHdr struct = new StructNlMsgHdr();
+        struct.nlmsg_len = TEST_NLMSG_LEN;
+        struct.nlmsg_type = type;
+        struct.nlmsg_flags = flags;
+        struct.nlmsg_seq = TEST_NLMSG_SEQ;
+        struct.nlmsg_pid = TEST_NLMSG_PID;
+        return struct;
+    }
+
+    private static void assertContains(String actualValue, String expectedSubstring) {
+        if (actualValue.contains(expectedSubstring)) return;
+        fail("\"" + actualValue + "\" does not contain \"" + expectedSubstring + "\"");
+    }
+
+    private static void assertNotContains(String actualValue, String unexpectedSubstring) {
+        if (!actualValue.contains(unexpectedSubstring)) return;
+        fail("\"" + actualValue + "\" contains \"" + unexpectedSubstring + "\"");
+    }
+
+    @Test
+    public void testToString() {
+        StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_NEWADDR);
+        String s = struct.toString();
+        assertContains(s, TEST_NLMSG_LEN_STR);
+        assertContains(s, TEST_NLMSG_FLAGS_STR);
+        assertContains(s, TEST_NLMSG_SEQ_STR);
+        assertContains(s, TEST_NLMSG_PID_STR);
+        assertContains(s, "nlmsg_type{20()}");
+
+        struct = makeStructNlMsgHdr(NetlinkConstants.SOCK_DIAG_BY_FAMILY);
+        s = struct.toString();
+        assertContains(s, TEST_NLMSG_LEN_STR);
+        assertContains(s, TEST_NLMSG_FLAGS_STR);
+        assertContains(s, TEST_NLMSG_SEQ_STR);
+        assertContains(s, TEST_NLMSG_PID_STR);
+        assertContains(s, "nlmsg_type{20()}");
+    }
+
+    @Test
+    public void testToStringWithNetlinkFamily() {
+        StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_NEWADDR);
+        String s = struct.toString(OsConstants.NETLINK_ROUTE);
+        assertContains(s, TEST_NLMSG_LEN_STR);
+        assertContains(s, TEST_NLMSG_FLAGS_STR);
+        assertContains(s, TEST_NLMSG_SEQ_STR);
+        assertContains(s, TEST_NLMSG_PID_STR);
+        assertContains(s, "nlmsg_type{20(RTM_NEWADDR)}");
+
+        struct = makeStructNlMsgHdr(NetlinkConstants.SOCK_DIAG_BY_FAMILY);
+        s = struct.toString(OsConstants.NETLINK_INET_DIAG);
+        assertContains(s, TEST_NLMSG_LEN_STR);
+        assertContains(s, TEST_NLMSG_FLAGS_STR);
+        assertContains(s, TEST_NLMSG_SEQ_STR);
+        assertContains(s, TEST_NLMSG_PID_STR);
+        assertContains(s, "nlmsg_type{20(SOCK_DIAG_BY_FAMILY)}");
+    }
+
+    @Test
+    public void testToString_flags_dumpRequest() {
+        final short flags = StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP;
+        StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_GETROUTE, flags);
+
+        String s = struct.toString(OsConstants.NETLINK_ROUTE);
+
+        assertContains(s, "RTM_GETROUTE");
+        assertContains(s, "NLM_F_REQUEST");
+        assertContains(s, "NLM_F_DUMP");
+        // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH;
+        assertNotContains(s, "NLM_F_MATCH");
+        assertNotContains(s, "NLM_F_ROOT");
+    }
+
+    @Test
+    public void testToString_flags_root() {
+        final short flags = StructNlMsgHdr.NLM_F_ROOT;
+        StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_GETROUTE, flags);
+
+        String s = struct.toString(OsConstants.NETLINK_ROUTE);
+
+        assertContains(s, "NLM_F_ROOT");
+        // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH;
+        assertNotContains(s, "NLM_F_DUMP");
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/OWNERS b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/OWNERS
new file mode 100644
index 0000000..fca70aa
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 685852
+file:platform/frameworks/base:main:/services/core/java/com/android/server/vcn/OWNERS
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmIdTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmIdTest.java
new file mode 100644
index 0000000..c9741cf
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmIdTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmIdTest {
+    private static final String EXPECTED_HEX_STRING =
+            "C0000201000000000000000000000000" + "53FA0FDD32000000";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+    private static final InetAddress DEST_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
+    private static final long SPI = 0x53fa0fdd;
+    private static final short PROTO = IPPROTO_ESP;
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmId struct = new StructXfrmId(DEST_ADDRESS, SPI, PROTO);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final StructXfrmId struct = StructXfrmId.parse(StructXfrmId.class, buffer);
+
+        assertEquals(DEST_ADDRESS, struct.getDestAddress(OsConstants.AF_INET));
+        assertEquals(SPI, struct.spi);
+        assertEquals(PROTO, struct.proto);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfgTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfgTest.java
new file mode 100644
index 0000000..69360f6
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCfgTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_INF;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmLifetimeCfgTest {
+    private static final String EXPECTED_HEX_STRING =
+            "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmLifetimeCfg struct = new StructXfrmLifetimeCfg();
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final StructXfrmLifetimeCfg struct =
+                StructXfrmLifetimeCfg.parse(StructXfrmLifetimeCfg.class, buffer);
+
+        assertEquals(XFRM_INF, struct.softByteLimit);
+        assertEquals(XFRM_INF, struct.hardByteLimit);
+        assertEquals(XFRM_INF, struct.softPacketLimit);
+        assertEquals(XFRM_INF, struct.hardPacketLimit);
+        assertEquals(BigInteger.ZERO, struct.softAddExpiresSeconds);
+        assertEquals(BigInteger.ZERO, struct.hardAddExpiresSeconds);
+        assertEquals(BigInteger.ZERO, struct.softUseExpiresSeconds);
+        assertEquals(BigInteger.ZERO, struct.hardUseExpiresSeconds);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCurTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCurTest.java
new file mode 100644
index 0000000..008c922
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmLifetimeCurTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Calendar;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmLifetimeCurTest {
+    private static final String EXPECTED_HEX_STRING =
+            "00000000000000000000000000000000" + "8CFE4265000000000000000000000000";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+    private static final BigInteger ADD_TIME;
+
+    static {
+        final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
+        cal.set(2023, Calendar.NOVEMBER, 2, 1, 42, 36);
+        final long timestampSeconds = TimeUnit.MILLISECONDS.toSeconds(cal.getTimeInMillis());
+        ADD_TIME = BigInteger.valueOf(timestampSeconds);
+    }
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmLifetimeCur struct =
+                new StructXfrmLifetimeCur(
+                        BigInteger.ZERO, BigInteger.ZERO, ADD_TIME, BigInteger.ZERO);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final StructXfrmLifetimeCur struct =
+                StructXfrmLifetimeCur.parse(StructXfrmLifetimeCur.class, buffer);
+
+        assertEquals(BigInteger.ZERO, struct.bytes);
+        assertEquals(BigInteger.ZERO, struct.packets);
+        assertEquals(ADD_TIME, struct.addTime);
+        assertEquals(BigInteger.ZERO, struct.useTime);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsnTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsnTest.java
new file mode 100644
index 0000000..1eb968d
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsnTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmReplayStateEsnTest {
+    private static final String EXPECTED_HEX_STRING =
+            "80000000000000000000000000000000"
+                    + "00000000001000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "0000000000000000";
+
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    private static final long BMP_LEN = 128;
+    private static final long REPLAY_WINDOW = 4096;
+    private static final byte[] BITMAP = new byte[512];
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmReplayStateEsn struct =
+                new StructXfrmReplayStateEsn(BMP_LEN, 0L, 0L, 0L, 0L, REPLAY_WINDOW, BITMAP);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(struct.getStructSize());
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+
+        final StructXfrmReplayStateEsn struct = StructXfrmReplayStateEsn.parse(buffer);
+
+        assertEquals(BMP_LEN, struct.getBmpLen());
+        assertEquals(REPLAY_WINDOW, struct.getReplayWindow());
+        assertArrayEquals(BITMAP, struct.getBitmap());
+        assertEquals(0L, struct.getRxSequenceNumber());
+        assertEquals(0L, struct.getTxSequenceNumber());
+    }
+
+    @Test
+    public void testGetSequenceNumber() throws Exception {
+        final long low = 0x00ab112233L;
+        final long hi = 0x01L;
+
+        assertEquals(0x01ab112233L, StructXfrmReplayStateEsn.getSequenceNumber(hi, low));
+        assertEquals(0xab11223300000001L, StructXfrmReplayStateEsn.getSequenceNumber(low, hi));
+    }
+
+    // TODO: Add test cases that the test bitmap is not all zeros
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmSelectorTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmSelectorTest.java
new file mode 100644
index 0000000..99f3b2a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmSelectorTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmSelectorTest {
+    private static final String EXPECTED_HEX_STRING =
+            "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000200000000000000"
+                    + "0000000000000000";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    private static final byte[] XFRM_ADDRESS_T_ANY_BYTES = new byte[16];
+    private static final int FAMILY = OsConstants.AF_INET;
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmSelector struct = new StructXfrmSelector(FAMILY);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final StructXfrmSelector struct =
+                StructXfrmSelector.parse(StructXfrmSelector.class, buffer);
+
+        assertArrayEquals(XFRM_ADDRESS_T_ANY_BYTES, struct.nestedStructDAddr);
+        assertArrayEquals(XFRM_ADDRESS_T_ANY_BYTES, struct.nestedStructSAddr);
+        assertEquals(0, struct.dPort);
+        assertEquals(0, struct.dPortMask);
+        assertEquals(0, struct.sPort);
+        assertEquals(0, struct.sPortMask);
+        assertEquals(FAMILY, struct.selectorFamily);
+        assertEquals(0, struct.prefixlenD);
+        assertEquals(0, struct.prefixlenS);
+        assertEquals(0, struct.proto);
+        assertEquals(0, struct.ifIndex);
+        assertEquals(0, struct.user);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaIdTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaIdTest.java
new file mode 100644
index 0000000..b659f62
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaIdTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmUsersaIdTest {
+    private static final String EXPECTED_HEX_STRING =
+            "C0000201000000000000000000000000" + "7768440002003200";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    private static final InetAddress DEST_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
+    private static final long SPI = 0x77684400;
+    private static final int FAMILY = OsConstants.AF_INET;
+    private static final short PROTO = IPPROTO_ESP;
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmUsersaId struct = new StructXfrmUsersaId(DEST_ADDRESS, SPI, FAMILY, PROTO);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+
+        final StructXfrmUsersaId struct =
+                StructXfrmUsersaId.parse(StructXfrmUsersaId.class, buffer);
+
+        assertEquals(DEST_ADDRESS, struct.getDestAddress());
+        assertEquals(SPI, struct.spi);
+        assertEquals(FAMILY, struct.family);
+        assertEquals(PROTO, struct.proto);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfoTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfoTest.java
new file mode 100644
index 0000000..94161ff
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaInfoTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MODE_TRANSPORT;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Calendar;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmUsersaInfoTest {
+    private static final String EXPECTED_HEX_STRING =
+            "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000A00000000000000"
+                    + "000000000000000020010DB800000000"
+                    + "0000000000000111AABBCCDD32000000"
+                    + "20010DB8000000000000000000000222"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "FD464C65000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "024000000A0000000000000000000000";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    private static final InetAddress DEST_ADDRESS =
+            InetAddresses.parseNumericAddress("2001:db8::111");
+    private static final InetAddress SOURCE_ADDRESS =
+            InetAddresses.parseNumericAddress("2001:db8::222");
+    private static final BigInteger ADD_TIME;
+    private static final int SELECTOR_FAMILY = OsConstants.AF_INET6;
+    private static final int FAMILY = OsConstants.AF_INET6;
+    private static final long SPI = 0xaabbccddL;
+    private static final long SEQ = 0L;
+    private static final long REQ_ID = 16386L;
+    private static final short PROTO = IPPROTO_ESP;
+    private static final short MODE = XFRM_MODE_TRANSPORT;
+    private static final short REPLAY_WINDOW_LEGACY = 0;
+    private static final short FLAGS = 0;
+
+    static {
+        final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
+        cal.set(2023, Calendar.NOVEMBER, 9, 2, 42, 05);
+        final long timestampSeconds = TimeUnit.MILLISECONDS.toSeconds(cal.getTimeInMillis());
+        ADD_TIME = BigInteger.valueOf(timestampSeconds);
+    }
+
+    @Test
+    public void testEncode() throws Exception {
+        final StructXfrmUsersaInfo struct =
+                new StructXfrmUsersaInfo(
+                        DEST_ADDRESS,
+                        SOURCE_ADDRESS,
+                        ADD_TIME,
+                        SELECTOR_FAMILY,
+                        SPI,
+                        SEQ,
+                        REQ_ID,
+                        PROTO,
+                        MODE,
+                        REPLAY_WINDOW_LEGACY,
+                        FLAGS);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+        buffer.order(ByteOrder.nativeOrder());
+        struct.writeToByteBuffer(buffer);
+
+        assertArrayEquals(EXPECTED_HEX, buffer.array());
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+
+        final StructXfrmUsersaInfo struct =
+                StructXfrmUsersaInfo.parse(StructXfrmUsersaInfo.class, buffer);
+
+        assertEquals(DEST_ADDRESS, struct.getDestAddress());
+        assertEquals(SOURCE_ADDRESS, struct.getSrcAddress());
+        assertEquals(SPI, struct.getSpi());
+        assertEquals(SEQ, struct.seq);
+        assertEquals(REQ_ID, struct.reqId);
+        assertEquals(FAMILY, struct.family);
+        assertEquals(MODE, struct.mode);
+        assertEquals(REPLAY_WINDOW_LEGACY, struct.replayWindowLegacy);
+        assertEquals(FLAGS, struct.flags);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessageTest.java
new file mode 100644
index 0000000..0ab36e7
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkGetSaMessageTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class XfrmNetlinkGetSaMessageTest {
+    private static final String EXPECTED_HEX_STRING =
+            "28000000120001000000000000000000"
+                    + "C0000201000000000000000000000000"
+                    + "7768440002003200";
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+    private static final InetAddress DEST_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
+    private static final long SPI = 0x77684400;
+    private static final int FAMILY = OsConstants.AF_INET;
+    private static final short PROTO = IPPROTO_ESP;
+
+    @Test
+    public void testEncode() throws Exception {
+        final byte[] result =
+                XfrmNetlinkGetSaMessage.newXfrmNetlinkGetSaMessage(DEST_ADDRESS, SPI, PROTO);
+        assertArrayEquals(EXPECTED_HEX, result);
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final XfrmNetlinkGetSaMessage message =
+                (XfrmNetlinkGetSaMessage) NetlinkMessage.parse(buffer, NETLINK_XFRM);
+        final StructXfrmUsersaId struct = message.getStructXfrmUsersaId();
+
+        assertEquals(DEST_ADDRESS, struct.getDestAddress());
+        assertEquals(SPI, struct.spi);
+        assertEquals(FAMILY, struct.family);
+        assertEquals(PROTO, struct.proto);
+        assertEquals(0, buffer.remaining());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessageTest.java
new file mode 100644
index 0000000..3d0ce2c
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/XfrmNetlinkNewSaMessageTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MODE_TRANSPORT;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class XfrmNetlinkNewSaMessageTest {
+    private static final String EXPECTED_HEX_STRING =
+            "2004000010000000000000003FE1D4B6"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000A00000000000000"
+                    + "000000000000000020010DB800000000"
+                    + "0000000000000111AABBCCDD32000000"
+                    + "20010DB8000000000000000000000222"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "FD464C65000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "024000000A0000000000000000000000"
+                    + "5C000100686D61632873686131290000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000A000000055F01AC07E15E437"
+                    + "115DDE0AEDD18A822BA9F81E60001400"
+                    + "686D6163287368613129000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "A00000006000000055F01AC07E15E437"
+                    + "115DDE0AEDD18A822BA9F81E58000200"
+                    + "63626328616573290000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "800000006AED4975ADF006D65C76F639"
+                    + "23A6265B1C0217008000000000000000"
+                    + "00000000000000000000000000100000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000";
+
+    private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+    private static final InetAddress DEST_ADDRESS =
+            InetAddresses.parseNumericAddress("2001:db8::111");
+    private static final InetAddress SOURCE_ADDRESS =
+            InetAddresses.parseNumericAddress("2001:db8::222");
+    private static final int FAMILY = OsConstants.AF_INET6;
+    private static final long SPI = 0xaabbccddL;
+    private static final long SEQ = 0L;
+    private static final long REQ_ID = 16386L;
+    private static final short MODE = XFRM_MODE_TRANSPORT;
+    private static final short REPLAY_WINDOW_LEGACY = 0;
+    private static final short FLAGS = 0;
+    private static final byte[] BITMAP = new byte[512];
+
+    @Test
+    public void testDecode() throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+        buffer.order(ByteOrder.nativeOrder());
+        final XfrmNetlinkNewSaMessage message =
+                (XfrmNetlinkNewSaMessage) NetlinkMessage.parse(buffer, NETLINK_XFRM);
+        final StructXfrmUsersaInfo xfrmUsersaInfo = message.getXfrmUsersaInfo();
+
+        assertEquals(DEST_ADDRESS, xfrmUsersaInfo.getDestAddress());
+        assertEquals(SOURCE_ADDRESS, xfrmUsersaInfo.getSrcAddress());
+        assertEquals(SPI, xfrmUsersaInfo.getSpi());
+        assertEquals(SEQ, xfrmUsersaInfo.seq);
+        assertEquals(REQ_ID, xfrmUsersaInfo.reqId);
+        assertEquals(FAMILY, xfrmUsersaInfo.family);
+        assertEquals(MODE, xfrmUsersaInfo.mode);
+        assertEquals(REPLAY_WINDOW_LEGACY, xfrmUsersaInfo.replayWindowLegacy);
+        assertEquals(FLAGS, xfrmUsersaInfo.flags);
+
+        assertArrayEquals(BITMAP, message.getBitmap());
+        assertEquals(0L, message.getRxSequenceNumber());
+        assertEquals(0L, message.getTxSequenceNumber());
+    }
+}
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/net/module/util/structs/StructMf6cctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java
new file mode 100644
index 0000000..a83fc36
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java
@@ -0,0 +1,102 @@
+package com.android.net.module.util.structs;
+
+import static android.system.OsConstants.AF_INET6;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.util.ArraySet;
+import androidx.test.runner.AndroidJUnit4;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMf6cctlTest {
+    private static final byte[] MSG_BYTES = new byte[] {
+        10, 0, /* AF_INET6 */
+        0, 0, /* originPort */
+        0, 0, 0, 0, /* originFlowinfo */
+        32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* originAddress */
+        0, 0, 0, 0, /* originScopeId */
+        10, 0, /* AF_INET6 */
+        0, 0, /* groupPort */
+        0, 0, 0, 0, /* groupFlowinfo*/
+        -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, /*groupAddress*/
+        0, 0, 0, 0, /* groupScopeId*/
+        1, 0, /* mf6ccParent */
+        0, 0, /* padding */
+        0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 /* mf6ccIfset */
+    };
+
+    private static final int OIF = 10;
+    private static final byte[] OIFSET_BYTES = new byte[] {
+        0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+    };
+
+    private static final Inet6Address SOURCE =
+            (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+    private static final Inet6Address DESTINATION =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+
+    @Test
+    public void testConstructor() {
+        final Set<Integer> oifset = new ArraySet<>();
+        oifset.add(OIF);
+
+        StructMf6cctl mf6cctl = new StructMf6cctl(SOURCE, DESTINATION,
+                1 /* mf6ccParent */, oifset);
+
+        assertTrue(Arrays.equals(SOURCE.getAddress(), mf6cctl.originAddress));
+        assertTrue(Arrays.equals(DESTINATION.getAddress(), mf6cctl.groupAddress));
+        assertEquals(1, mf6cctl.mf6ccParent);
+        assertArrayEquals(OIFSET_BYTES, mf6cctl.mf6ccIfset);
+    }
+
+    @Test
+    public void testConstructor_tooBigOifIndex_throwsIllegalArgumentException()
+            throws UnknownHostException {
+        final Set<Integer> oifset = new ArraySet<>();
+        oifset.add(1000);
+
+        assertThrows(IllegalArgumentException.class,
+            () -> new StructMf6cctl(SOURCE, DESTINATION, 1, oifset));
+    }
+
+    @Test
+    public void testParseMf6cctl() {
+        final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+        buf.order(ByteOrder.nativeOrder());
+        StructMf6cctl mf6cctl = StructMf6cctl.parse(StructMf6cctl.class, buf);
+
+        assertEquals(AF_INET6, mf6cctl.originFamily);
+        assertEquals(AF_INET6, mf6cctl.groupFamily);
+        assertArrayEquals(SOURCE.getAddress(), mf6cctl.originAddress);
+        assertArrayEquals(DESTINATION.getAddress(), mf6cctl.groupAddress);
+        assertEquals(1, mf6cctl.mf6ccParent);
+        assertArrayEquals("mf6ccIfset = " + Arrays.toString(mf6cctl.mf6ccIfset),
+                OIFSET_BYTES, mf6cctl.mf6ccIfset);
+    }
+
+    @Test
+    public void testWriteToBytes() {
+        final Set<Integer> oifset = new ArraySet<>();
+        oifset.add(OIF);
+
+        StructMf6cctl mf6cctl = new StructMf6cctl(SOURCE, DESTINATION,
+                1 /* mf6ccParent */, oifset);
+        byte[] bytes = mf6cctl.writeToBytes();
+
+        assertArrayEquals("bytes = " + Arrays.toString(bytes), MSG_BYTES, bytes);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java
new file mode 100644
index 0000000..75196e4
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java
@@ -0,0 +1,70 @@
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.util.ArraySet;
+import androidx.test.runner.AndroidJUnit4;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMif6ctlTest {
+    private static final byte[] MSG_BYTES = new byte[] {
+        1, 0,  /* mif6cMifi */
+        0, /* mif6cFlags */
+        1, /* vifcThreshold*/
+        20, 0, /* mif6cPifi */
+        0, 0, 0, 0, /* vifcRateLimit */
+        0, 0 /* padding */
+    };
+
+    @Test
+    public void testConstructor() {
+        StructMif6ctl mif6ctl = new StructMif6ctl(10 /* mif6cMifi */,
+                (short) 11 /* mif6cFlags */,
+                (short) 12 /* vifcThreshold */,
+                13 /* mif6cPifi */,
+                14L /* vifcRateLimit */);
+
+        assertEquals(10, mif6ctl.mif6cMifi);
+        assertEquals(11, mif6ctl.mif6cFlags);
+        assertEquals(12, mif6ctl.vifcThreshold);
+        assertEquals(13, mif6ctl.mif6cPifi);
+        assertEquals(14, mif6ctl.vifcRateLimit);
+    }
+
+    @Test
+    public void testParseMif6ctl() {
+        final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+        buf.order(ByteOrder.nativeOrder());
+        StructMif6ctl mif6ctl = StructMif6ctl.parse(StructMif6ctl.class, buf);
+
+        assertEquals(1, mif6ctl.mif6cMifi);
+        assertEquals(0, mif6ctl.mif6cFlags);
+        assertEquals(1, mif6ctl.vifcThreshold);
+        assertEquals(20, mif6ctl.mif6cPifi);
+        assertEquals(0, mif6ctl.vifcRateLimit);
+    }
+
+    @Test
+    public void testWriteToBytes() {
+        StructMif6ctl mif6ctl = new StructMif6ctl(1 /* mif6cMifi */,
+                (short) 0 /* mif6cFlags */,
+                (short) 1 /* vifcThreshold */,
+                20 /* mif6cPifi */,
+                (long) 0 /* vifcRateLimit */);
+
+        byte[] bytes = mif6ctl.writeToBytes();
+
+        assertArrayEquals("bytes = " + Arrays.toString(bytes), MSG_BYTES, bytes);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java
new file mode 100644
index 0000000..f1b75a0
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java
@@ -0,0 +1,58 @@
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMrt6MsgTest {
+
+    private static final byte[] MSG_BYTES = new byte[] {
+        0, /* mbz = 0 */
+        1, /* message type = MRT6MSG_NOCACHE */
+        1, 0, /* mif u16 = 1 */
+        0, 0, 0, 0, /* padding */
+        32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* source=2001:db8::1 */
+        -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, /* destination=ff05::1234 */
+    };
+
+    private static final Inet6Address SOURCE =
+            (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+    private static final Inet6Address GROUP =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+
+    @Test
+    public void testParseMrt6Msg() {
+        final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+        StructMrt6Msg mrt6Msg = StructMrt6Msg.parse(buf);
+
+        assertEquals(1, mrt6Msg.mif);
+        assertEquals(StructMrt6Msg.MRT6MSG_NOCACHE, mrt6Msg.msgType);
+        assertEquals(SOURCE, mrt6Msg.src);
+        assertEquals(GROUP, mrt6Msg.dst);
+    }
+
+    @Test
+    public void testWriteToBytes() {
+        StructMrt6Msg msg = new StructMrt6Msg((byte) 0 /* mbz must be 0 */,
+                StructMrt6Msg.MRT6MSG_NOCACHE,
+                1 /* mif */,
+                SOURCE,
+                GROUP);
+        byte[] bytes = msg.writeToBytes();
+
+        assertArrayEquals(MSG_BYTES, bytes);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java
new file mode 100644
index 0000000..23e7b15
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.net.module.util.wear;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.android.net.module.util.async.CircularByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetPacketHelpersTest {
+    @Test
+    public void decodeNetworkUnsignedInt16() {
+        final byte[] data = new byte[4];
+        data[0] = (byte) 0xFF;
+        data[1] = (byte) 1;
+        data[2] = (byte) 2;
+        data[3] = (byte) 0xFF;
+
+        assertEquals(0x0102, NetPacketHelpers.decodeNetworkUnsignedInt16(data, 1));
+
+        CircularByteBuffer buffer = new CircularByteBuffer(100);
+        buffer.writeBytes(data, 0, data.length);
+
+        assertEquals(0x0102, NetPacketHelpers.decodeNetworkUnsignedInt16(buffer, 1));
+    }
+
+    @Test
+    public void encodeNetworkUnsignedInt16() {
+        final byte[] data = new byte[4];
+        data[0] = (byte) 0xFF;
+        data[3] = (byte) 0xFF;
+        NetPacketHelpers.encodeNetworkUnsignedInt16(0x0102, data, 1);
+
+        assertEquals((byte) 0xFF, data[0]);
+        assertEquals((byte) 1, data[1]);
+        assertEquals((byte) 2, data[2]);
+        assertEquals((byte) 0xFF, data[3]);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java
new file mode 100644
index 0000000..1fcca70
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java
@@ -0,0 +1,291 @@
+/*
+ * 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.net.module.util.wear;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+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.ignoreStubs;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.async.AsyncFile;
+import com.android.net.module.util.async.BufferedFile;
+import com.android.net.module.util.async.EventManager;
+import com.android.net.module.util.async.FileHandle;
+import com.android.net.module.util.async.ReadableByteBuffer;
+import com.android.testutils.async.ReadableDataAnswer;
+
+import org.junit.After;
+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 org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StreamingPacketFileTest {
+    private static final int MAX_PACKET_SIZE = 100;
+
+    @Mock EventManager mockEventManager;
+    @Mock PacketFile.Listener mockFileListener;
+    @Mock AsyncFile mockAsyncFile;
+    @Mock ParcelFileDescriptor mockParcelFileDescriptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager));
+    }
+
+    @Test
+    public void continueReadingAndClose() throws Exception {
+        final int maxBufferedInboundPackets = 3;
+        final int maxBufferedOutboundPackets = 5;
+
+        final StreamingPacketFile file =
+            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+
+        assertEquals(maxBufferedInboundPackets * (MAX_PACKET_SIZE + 2),
+            bufferedFile.getInboundBufferFreeSizeForTest());
+        assertEquals(maxBufferedOutboundPackets * (MAX_PACKET_SIZE + 2),
+            bufferedFile.getOutboundBufferFreeSize());
+        assertEquals(bufferedFile.getOutboundBufferFreeSize() - 2,
+            file.getOutboundFreeSize());
+
+        file.continueReading();
+        verify(mockAsyncFile).enableReadEvents(true);
+
+        file.close();
+        verify(mockAsyncFile).close();
+    }
+
+    @Test
+    public void enqueueOutboundPacket() throws Exception {
+        final int maxBufferedInboundPackets = 10;
+        final int maxBufferedOutboundPackets = 20;
+
+        final StreamingPacketFile file =
+            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+
+        final byte[] packet1 = new byte[11];
+        final byte[] packet2 = new byte[12];
+        packet1[0] = (byte) 1;
+        packet2[0] = (byte) 2;
+
+        assertEquals(0, bufferedFile.getOutboundBufferSize());
+
+        when(mockAsyncFile.write(any(), anyInt(), anyInt())).thenReturn(0);
+        assertTrue(file.enqueueOutboundPacket(packet1, 0, packet1.length));
+        verify(mockAsyncFile).enableWriteEvents(true);
+
+        assertEquals(packet1.length + 2, bufferedFile.getOutboundBufferSize());
+
+        checkAndResetMocks();
+
+        final int totalLen = packet1.length + packet2.length + 4;
+
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+        when(mockAsyncFile.write(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
+
+        assertTrue(file.enqueueOutboundPacket(packet2, 0, packet2.length));
+
+        assertEquals(0, bufferedFile.getInboundBuffer().size());
+        assertEquals(0, bufferedFile.getOutboundBufferSize());
+
+        assertEquals(0, posCaptor.getValue().intValue());
+        assertEquals(totalLen, lenCaptor.getValue().intValue());
+
+        final byte[] capturedData = arrayCaptor.getValue();
+        assertEquals(packet1.length, NetPacketHelpers.decodeNetworkUnsignedInt16(capturedData, 0));
+        assertEquals(packet2.length,
+            NetPacketHelpers.decodeNetworkUnsignedInt16(capturedData, packet1.length + 2));
+        assertEquals(packet1[0], capturedData[2]);
+        assertEquals(packet2[0], capturedData[packet1.length + 4]);
+    }
+
+    @Test
+    public void onInboundPacket() throws Exception {
+        final int maxBufferedInboundPackets = 10;
+        final int maxBufferedOutboundPackets = 20;
+
+        final StreamingPacketFile file =
+            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+        final ReadableByteBuffer inboundBuffer = bufferedFile.getInboundBuffer();
+
+        final int len1 = 11;
+        final int len2 = 12;
+        final byte[] data = new byte[len1 + len2 + 4];
+        NetPacketHelpers.encodeNetworkUnsignedInt16(len1, data, 0);
+        NetPacketHelpers.encodeNetworkUnsignedInt16(len2, data, 11 + 2);
+        data[2] = (byte) 1;
+        data[len1 + 4] = (byte) 2;
+
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        when(mockFileListener.onPreambleData(any(), eq(0), eq(data.length))).thenReturn(0);
+        bufferedFile.onReadReady(mockAsyncFile);
+        verify(mockAsyncFile).enableReadEvents(true);
+        verify(mockFileListener).onInboundBuffered(data.length, data.length);
+        verify(mockFileListener).onInboundPacket(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
+        verify(mockEventManager).execute(any());
+
+        byte[] capturedData = arrayCaptor.getValue();
+        assertEquals(2, posCaptor.getValue().intValue());
+        assertEquals(len1, lenCaptor.getValue().intValue());
+        assertEquals((byte) 1, capturedData[2]);
+
+        checkAndResetMocks();
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        file.onBufferedFileInboundData(0);
+        verify(mockFileListener).onInboundPacket(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
+        verify(mockEventManager).execute(any());
+
+        capturedData = arrayCaptor.getValue();
+        assertEquals(2, posCaptor.getValue().intValue());
+        assertEquals(len2, lenCaptor.getValue().intValue());
+        assertEquals((byte) 2, capturedData[2]);
+
+        assertEquals(0, bufferedFile.getOutboundBufferSize());
+        assertEquals(0, inboundBuffer.size());
+    }
+
+    @Test
+    public void onReadReady_preambleData() throws Exception {
+        final int maxBufferedInboundPackets = 10;
+        final int maxBufferedOutboundPackets = 20;
+
+        final StreamingPacketFile file =
+            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+        final ReadableByteBuffer inboundBuffer = bufferedFile.getInboundBuffer();
+
+        final int preambleLen = 23;
+        final int len1 = 11;
+        final byte[] data = new byte[preambleLen + 2 + len1];
+        NetPacketHelpers.encodeNetworkUnsignedInt16(len1, data, preambleLen);
+        data[preambleLen + 2] = (byte) 1;
+
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        when(mockFileListener.onPreambleData(any(), eq(0), eq(data.length))).thenReturn(5);
+        when(mockFileListener.onPreambleData(
+            any(), eq(0), eq(data.length - 5))).thenReturn(preambleLen - 5);
+        when(mockFileListener.onPreambleData(
+            any(), eq(0), eq(data.length - preambleLen))).thenReturn(0);
+
+        bufferedFile.onReadReady(mockAsyncFile);
+
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+
+        verify(mockFileListener).onInboundBuffered(data.length, data.length);
+        verify(mockFileListener).onInboundPacket(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
+        verify(mockEventManager).execute(any());
+        verify(mockAsyncFile).enableReadEvents(true);
+
+        final byte[] capturedData = arrayCaptor.getValue();
+        assertEquals(2, posCaptor.getValue().intValue());
+        assertEquals(len1, lenCaptor.getValue().intValue());
+        assertEquals((byte) 1, capturedData[2]);
+
+        assertEquals(0, bufferedFile.getOutboundBufferSize());
+        assertEquals(0, inboundBuffer.size());
+    }
+
+    @Test
+    public void shutdownReading() throws Exception {
+        final int maxBufferedInboundPackets = 10;
+        final int maxBufferedOutboundPackets = 20;
+
+        final StreamingPacketFile file =
+            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+
+        final byte[] data = new byte[100];
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+
+        doAnswer(new Answer() {
+            @Override public Object answer(InvocationOnMock invocation) {
+                file.shutdownReading();
+                return Integer.valueOf(-1);
+            }}).when(mockFileListener).onPreambleData(any(), anyInt(), anyInt());
+
+        bufferedFile.onReadReady(mockAsyncFile);
+
+        verify(mockFileListener).onInboundBuffered(data.length, data.length);
+        verify(mockAsyncFile).enableReadEvents(false);
+
+        assertEquals(0, bufferedFile.getInboundBuffer().size());
+    }
+
+    private void checkAndResetMocks() {
+        verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager,
+            mockParcelFileDescriptor));
+        reset(mockFileListener, mockAsyncFile, mockEventManager);
+    }
+
+    private StreamingPacketFile createFile(
+            int maxBufferedInboundPackets, int maxBufferedOutboundPackets) throws Exception {
+        when(mockEventManager.registerFile(any(), any())).thenReturn(mockAsyncFile);
+        return new StreamingPacketFile(
+            mockEventManager,
+            FileHandle.fromFileDescriptor(mockParcelFileDescriptor),
+            mockFileListener,
+            MAX_PACKET_SIZE,
+            maxBufferedInboundPackets,
+            maxBufferedOutboundPackets);
+    }
+}
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/DeviceInfoUtilsTest.java b/staticlibs/tests/unit/src/com/android/testutils/DeviceInfoUtilsTest.java
new file mode 100644
index 0000000..e46dd59
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/DeviceInfoUtilsTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public final class DeviceInfoUtilsTest {
+    /**
+     * Verifies that version string compare logic returns expected result for various cases.
+     * Note that only major and minor number are compared.
+     */
+    @Test
+    public void testMajorMinorVersionCompare() {
+        assertEquals(0, DeviceInfoUtils.compareMajorMinorVersion("4.8.1", "4.8"));
+        assertEquals(1, DeviceInfoUtils.compareMajorMinorVersion("4.9", "4.8.1"));
+        assertEquals(1, DeviceInfoUtils.compareMajorMinorVersion("5.0", "4.8"));
+        assertEquals(1, DeviceInfoUtils.compareMajorMinorVersion("5", "4.8"));
+        assertEquals(0, DeviceInfoUtils.compareMajorMinorVersion("5", "5.0"));
+        assertEquals(1, DeviceInfoUtils.compareMajorMinorVersion("5-beta1", "4.8"));
+        assertEquals(0, DeviceInfoUtils.compareMajorMinorVersion("4.8.0.0", "4.8"));
+        assertEquals(0, DeviceInfoUtils.compareMajorMinorVersion("4.8-RC1", "4.8"));
+        assertEquals(0, DeviceInfoUtils.compareMajorMinorVersion("4.8", "4.8"));
+        assertEquals(-1, DeviceInfoUtils.compareMajorMinorVersion("3.10", "4.8.0"));
+        assertEquals(-1, DeviceInfoUtils.compareMajorMinorVersion("4.7.10.10", "4.8"));
+    }
+
+    @Test
+    public void testGetMajorMinorSubminorVersion() throws Exception {
+        final DeviceInfoUtils.KVersion expected = new DeviceInfoUtils.KVersion(4, 19, 220);
+        assertEquals(expected, DeviceInfoUtils.getMajorMinorSubminorVersion("4.19.220"));
+        assertEquals(expected, DeviceInfoUtils.getMajorMinorSubminorVersion("4.19.220.50"));
+        assertEquals(expected, DeviceInfoUtils.getMajorMinorSubminorVersion(
+                "4.19.220-g500ede0aed22-ab8272303"));
+
+        final DeviceInfoUtils.KVersion expected2 = new DeviceInfoUtils.KVersion(5, 17, 0);
+        assertEquals(expected2, DeviceInfoUtils.getMajorMinorSubminorVersion("5.17"));
+        assertEquals(expected2, DeviceInfoUtils.getMajorMinorSubminorVersion("5.17."));
+        assertEquals(expected2, DeviceInfoUtils.getMajorMinorSubminorVersion("5.17.beta"));
+        assertEquals(expected2, DeviceInfoUtils.getMajorMinorSubminorVersion(
+                "5.17-rc6-g52099515ca00-ab8032400"));
+
+        final DeviceInfoUtils.KVersion invalid = new DeviceInfoUtils.KVersion(0, 0, 0);
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion(""));
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion("4"));
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion("4."));
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion("4-beta"));
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion("1.x.1"));
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion("x.1.1"));
+    }
+
+    @Test
+    public void testVersion() throws Exception {
+        final DeviceInfoUtils.KVersion v1 = new DeviceInfoUtils.KVersion(4, 8, 1);
+        final DeviceInfoUtils.KVersion v2 = new DeviceInfoUtils.KVersion(4, 8, 1);
+        final DeviceInfoUtils.KVersion v3 = new DeviceInfoUtils.KVersion(4, 8, 2);
+        final DeviceInfoUtils.KVersion v4 = new DeviceInfoUtils.KVersion(4, 9, 1);
+        final DeviceInfoUtils.KVersion v5 = new DeviceInfoUtils.KVersion(5, 8, 1);
+
+        assertEquals(v1, v2);
+        assertNotEquals(v1, v3);
+        assertNotEquals(v1, v4);
+        assertNotEquals(v1, v5);
+
+        assertEquals(0, v1.compareTo(v2));
+        assertEquals(-1, v1.compareTo(v3));
+        assertEquals(1, v3.compareTo(v1));
+        assertEquals(-1, v1.compareTo(v4));
+        assertEquals(1, v4.compareTo(v1));
+        assertEquals(-1, v1.compareTo(v5));
+        assertEquals(1, v5.compareTo(v1));
+
+        assertTrue(v2.isInRange(v1, v5));
+        assertTrue(v3.isInRange(v1, v5));
+        assertTrue(v4.isInRange(v1, v5));
+        assertFalse(v5.isInRange(v1, v5));
+        assertFalse(v1.isInRange(v3, v5));
+        assertFalse(v5.isInRange(v2, v4));
+
+        assertTrue(v2.isAtLeast(v1));
+        assertTrue(v3.isAtLeast(v1));
+        assertTrue(v4.isAtLeast(v1));
+        assertTrue(v5.isAtLeast(v1));
+        assertFalse(v1.isAtLeast(v3));
+        assertFalse(v1.isAtLeast(v4));
+        assertFalse(v1.isAtLeast(v5));
+    }
+
+    @Test
+    public void testKernelVersionIsAtLeast() {
+        // Pick a lower kernel version 4.0 which was released at April 2015, the kernel
+        // version running on all test devices nowadays should be higher than it.
+        assertTrue(DeviceInfoUtils.isKernelVersionAtLeast("4.0"));
+
+        // Invalid kernel version.
+        assertTrue(DeviceInfoUtils.isKernelVersionAtLeast("0.0.0"));
+
+        // Pick a higher kernel version which isn't released yet, comparison should return false.
+        // Need to update the target version in the future to make sure the test still passes.
+        assertFalse(DeviceInfoUtils.isKernelVersionAtLeast("20.0.0"));
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
new file mode 100644
index 0000000..440b836
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.os.Handler
+import android.os.HandlerThread
+import com.android.testutils.FunctionalUtils.ThrowingSupplier
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val ATTEMPTS = 50 // Causes testWaitForIdle to take about 150ms on aosp_crosshatch-eng
+private const val TIMEOUT_MS = 1000
+
+@RunWith(JUnit4::class)
+class HandlerUtilsTest {
+    @Test
+    fun testWaitForIdle() {
+        val handlerThread = HandlerThread("testHandler").apply { start() }
+
+        // Tests that waitForIdle can be called many times without ill impact if the service is
+        // already idle.
+        repeat(ATTEMPTS) {
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+
+        // Tests that calling waitForIdle waits for messages to be processed. Use both an
+        // inline runnable that's instantiated at each loop run and a runnable that's instantiated
+        // once for all.
+        val tempRunnable = object : Runnable {
+            // Use StringBuilder preferentially to StringBuffer because StringBuilder is NOT
+            // thread-safe. It's part of the point that both runnables run on the same thread
+            // so if anything is wrong in that space it's better to opportunistically use a class
+            // where things might go wrong, even if there is no guarantee of failure.
+            var memory = StringBuilder()
+            override fun run() {
+                memory.append("b")
+            }
+        }
+        repeat(ATTEMPTS) { i ->
+            handlerThread.threadHandler.post { tempRunnable.memory.append("a"); }
+            handlerThread.threadHandler.post(tempRunnable)
+            handlerThread.waitForIdle(TIMEOUT_MS)
+            assertEquals(tempRunnable.memory.toString(), "ab".repeat(i + 1))
+        }
+    }
+
+    // Statistical test : even if visibleOnHandlerThread doesn't work this is likely to succeed,
+    // but it will be at least flaky.
+    @Test
+    fun testVisibleOnHandlerThread() {
+        val handlerThread = HandlerThread("testHandler").apply { start() }
+        val handler = Handler(handlerThread.looper)
+
+        repeat(ATTEMPTS) { attempt ->
+            var x = -10
+            var y = -11
+            y = visibleOnHandlerThread(handler, ThrowingSupplier<Int> { x = attempt; attempt })
+            assertEquals(attempt, x)
+            assertEquals(attempt, y)
+            handler.post { assertEquals(attempt, x) }
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            visibleOnHandlerThread(handler) { throw IllegalArgumentException() }
+        }
+
+        // Null values may be returned by the supplier
+        assertNull(visibleOnHandlerThread(handler, ThrowingSupplier<Nothing?> { null }))
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/TestDnsServerTest.kt b/staticlibs/tests/unit/src/com/android/testutils/TestDnsServerTest.kt
new file mode 100644
index 0000000..6f4587b
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/TestDnsServerTest.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.DnsResolver.CLASS_IN
+import android.net.DnsResolver.TYPE_AAAA
+import android.net.Network
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.DnsPacket
+import com.android.net.module.util.DnsPacket.DnsRecord
+import libcore.net.InetAddressUtils
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+val TEST_V6_ADDR = InetAddressUtils.parseNumericAddress("2001:db8::3")
+const val TEST_DOMAIN = "hello.example.com"
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TestDnsServerTest {
+    private val network = Mockito.mock(Network::class.java)
+    private val localAddr = InetSocketAddress(InetAddress.getLocalHost(), 0 /* port */)
+    private val testServer: TestDnsServer = TestDnsServer(network, localAddr)
+
+    @After
+    fun tearDown() {
+        if (testServer.isAlive) testServer.stop()
+    }
+
+    @Test
+    fun testStartStop() {
+        repeat(100) {
+            val server = TestDnsServer(network, localAddr)
+            server.start()
+            assertTrue(server.isAlive)
+            server.stop()
+            assertFalse(server.isAlive)
+        }
+
+        // Test illegal start/stop.
+        assertFailsWith<IllegalStateException> { testServer.stop() }
+        testServer.start()
+        assertTrue(testServer.isAlive)
+        assertFailsWith<IllegalStateException> { testServer.start() }
+        testServer.stop()
+        assertFalse(testServer.isAlive)
+        assertFailsWith<IllegalStateException> { testServer.stop() }
+        // TestDnsServer rejects start after stop.
+        assertFailsWith<IllegalStateException> { testServer.start() }
+    }
+
+    @Test
+    fun testHandleDnsQuery() {
+        testServer.setAnswer(TEST_DOMAIN, listOf(TEST_V6_ADDR))
+        testServer.start()
+
+        // Mock query and send it to the test server.
+        val queryHeader = DnsPacket.DnsHeader(0xbeef /* id */,
+                0x0 /* flag */, 1 /* qcount */, 0 /* ancount */)
+        val qlist = listOf(DnsRecord.makeQuestion(TEST_DOMAIN, TYPE_AAAA, CLASS_IN))
+        val queryPacket = TestDnsServer.DnsQueryPacket(queryHeader, qlist, emptyList())
+        val response = resolve(queryPacket, testServer.port)
+
+        // Verify expected answer packet. Set QR bit of flag to 1 for response packet
+        // according to RFC 1035 section 4.1.1.
+        val answerHeader = DnsPacket.DnsHeader(0xbeef,
+            1 shl 15 /* flag */, 1 /* qcount */, 1 /* ancount */)
+        val alist = listOf(DnsRecord.makeAOrAAAARecord(DnsPacket.ANSECTION, TEST_DOMAIN,
+                    CLASS_IN, DEFAULT_TTL_S, TEST_V6_ADDR))
+        val expectedAnswerPacket = TestDnsServer.DnsAnswerPacket(answerHeader, qlist, alist)
+        assertEquals(expectedAnswerPacket, response)
+
+        // Clean up the server in tearDown.
+    }
+
+    private fun resolve(queryDnsPacket: DnsPacket, serverPort: Int): TestDnsServer.DnsAnswerPacket {
+        val bytes = queryDnsPacket.bytes
+        // Create a new client socket, the socket will be bound to a
+        // random port other than the server port.
+        val socket = DatagramSocket(localAddr).also { it.soTimeout = 100 }
+        val queryPacket = DatagramPacket(bytes, bytes.size, localAddr.address, serverPort)
+
+        // Send query and wait for the reply.
+        socket.send(queryPacket)
+        val buffer = ByteArray(MAX_BUF_SIZE)
+        val reply = DatagramPacket(buffer, buffer.size)
+        socket.receive(reply)
+        return TestDnsServer.DnsAnswerPacket(reply.data)
+    }
+
+    // TODO: Add more tests, which includes:
+    //  * Empty question RR packet (or more unexpected states)
+    //  * No answer found (setAnswer empty list at L.78)
+    //  * Test one or multi A record(s)
+    //  * Test multi AAAA records
+    //  * Test CNAME records
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
new file mode 100644
index 0000000..e838bdc
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.annotation.SuppressLint
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkCapabilities
+import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.AVAILABLE
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.BLOCKED_STATUS
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LINK_PROPERTIES_CHANGED
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LOSING
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LOST
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.NETWORK_CAPS_UPDATED
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.RESUMED
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.SUSPENDED
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.UNAVAILABLE
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import kotlin.reflect.KClass
+import kotlin.test.assertEquals
+import kotlin.test.assertFails
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+const val SHORT_TIMEOUT_MS = 20L
+const val DEFAULT_LINGER_DELAY_MS = 30000
+const val NOT_METERED = NetworkCapabilities.NET_CAPABILITY_NOT_METERED
+const val WIFI = NetworkCapabilities.TRANSPORT_WIFI
+const val CELLULAR = NetworkCapabilities.TRANSPORT_CELLULAR
+const val TEST_INTERFACE_NAME = "testInterfaceName"
+
+@RunWith(JUnit4::class)
+@SuppressLint("NewApi") // Uses hidden APIs, which the linter would identify as missing APIs.
+class TestableNetworkCallbackTest {
+    private lateinit var mCallback: TestableNetworkCallback
+
+    private fun makeHasNetwork(netId: Int) = object : TestableNetworkCallback.HasNetwork {
+        override val network: Network = Network(netId)
+    }
+
+    @Before
+    fun setUp() {
+        mCallback = TestableNetworkCallback()
+    }
+
+    @Test
+    fun testLastAvailableNetwork() {
+        // Make sure there is no last available network at first, then the last available network
+        // is returned after onAvailable is called.
+        val net2097 = Network(2097)
+        assertNull(mCallback.lastAvailableNetwork)
+        mCallback.onAvailable(net2097)
+        assertEquals(mCallback.lastAvailableNetwork, net2097)
+
+        // Make sure calling onCapsChanged/onLinkPropertiesChanged don't affect the last available
+        // network.
+        mCallback.onCapabilitiesChanged(net2097, NetworkCapabilities())
+        mCallback.onLinkPropertiesChanged(net2097, LinkProperties())
+        assertEquals(mCallback.lastAvailableNetwork, net2097)
+
+        // Make sure onLost clears the last available network.
+        mCallback.onLost(net2097)
+        assertNull(mCallback.lastAvailableNetwork)
+
+        // Do the same but with a different network after onLost : make sure the last available
+        // network is the new one, not the original one.
+        val net2098 = Network(2098)
+        mCallback.onAvailable(net2098)
+        mCallback.onCapabilitiesChanged(net2098, NetworkCapabilities())
+        mCallback.onLinkPropertiesChanged(net2098, LinkProperties())
+        assertEquals(mCallback.lastAvailableNetwork, net2098)
+
+        // Make sure onAvailable changes the last available network even if onLost was not called.
+        val net2099 = Network(2099)
+        mCallback.onAvailable(net2099)
+        assertEquals(mCallback.lastAvailableNetwork, net2099)
+
+        // For legacy reasons, lastAvailableNetwork is null as soon as any is lost, not necessarily
+        // the last available one. Check that behavior.
+        mCallback.onLost(net2098)
+        assertNull(mCallback.lastAvailableNetwork)
+
+        // Make sure that losing the really last available one still results in null.
+        mCallback.onLost(net2099)
+        assertNull(mCallback.lastAvailableNetwork)
+
+        // Make sure multiple onAvailable in a row then onLost still results in null.
+        mCallback.onAvailable(net2097)
+        mCallback.onAvailable(net2098)
+        mCallback.onAvailable(net2099)
+        mCallback.onLost(net2097)
+        assertNull(mCallback.lastAvailableNetwork)
+    }
+
+    @Test
+    fun testAssertNoCallback() {
+        mCallback.assertNoCallback(SHORT_TIMEOUT_MS)
+        mCallback.onAvailable(Network(100))
+        assertFails { mCallback.assertNoCallback(SHORT_TIMEOUT_MS) }
+        val net = Network(101)
+        mCallback.assertNoCallback { it is Available }
+        mCallback.onAvailable(net)
+        // Expect no blocked status change. Receive other callback does not fail the test.
+        mCallback.assertNoCallback { it is BlockedStatus }
+        mCallback.onBlockedStatusChanged(net, true)
+        assertFails { mCallback.assertNoCallback { it is BlockedStatus } }
+        mCallback.onBlockedStatusChanged(net, false)
+        mCallback.onCapabilitiesChanged(net, NetworkCapabilities())
+        assertFails { mCallback.assertNoCallback { it is CapabilitiesChanged } }
+    }
+
+    @Test
+    fun testCapabilitiesWithAndWithout() {
+        val net = Network(101)
+        val matcher = makeHasNetwork(101)
+        val meteredNc = NetworkCapabilities()
+        val unmeteredNc = NetworkCapabilities().addCapability(NOT_METERED)
+        // Check that expecting caps (with or without) fails when no callback has been received.
+        assertFails {
+            mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { it.hasCapability(NOT_METERED) }
+        }
+        assertFails {
+            mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { !it.hasCapability(NOT_METERED) }
+        }
+
+        // Add NOT_METERED and check that With succeeds and Without fails.
+        mCallback.onCapabilitiesChanged(net, unmeteredNc)
+        mCallback.expectCaps(matcher) { it.hasCapability(NOT_METERED) }
+        mCallback.onCapabilitiesChanged(net, unmeteredNc)
+        assertFails {
+            mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { !it.hasCapability(NOT_METERED) }
+        }
+
+        // Don't add NOT_METERED and check that With fails and Without succeeds.
+        mCallback.onCapabilitiesChanged(net, meteredNc)
+        assertFails {
+            mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { it.hasCapability(NOT_METERED) }
+        }
+        mCallback.onCapabilitiesChanged(net, meteredNc)
+        mCallback.expectCaps(matcher) { !it.hasCapability(NOT_METERED) }
+    }
+
+    @Test
+    fun testExpectWithPredicate() {
+        val net = Network(193)
+        val netCaps = NetworkCapabilities().addTransportType(CELLULAR)
+        // Check that expecting callbackThat anything fails when no callback has been received.
+        assertFails { mCallback.expect<CallbackEntry>(timeoutMs = SHORT_TIMEOUT_MS) { true } }
+
+        // Basic test for true and false
+        mCallback.onAvailable(net)
+        mCallback.expect<Available> { true }
+        mCallback.onAvailable(net)
+        assertFails { mCallback.expect<CallbackEntry>(timeoutMs = SHORT_TIMEOUT_MS) { false } }
+
+        // Try a positive and a negative case
+        mCallback.onBlockedStatusChanged(net, true)
+        mCallback.expect<CallbackEntry> { cb -> cb is BlockedStatus && cb.blocked }
+        mCallback.onCapabilitiesChanged(net, netCaps)
+        assertFails { mCallback.expect<CallbackEntry>(timeoutMs = SHORT_TIMEOUT_MS) { cb ->
+            cb is CapabilitiesChanged && cb.caps.hasTransport(WIFI)
+        } }
+    }
+
+    @Test
+    fun testExpectCaps() {
+        val net = Network(101)
+        val netCaps = NetworkCapabilities().addCapability(NOT_METERED).addTransportType(WIFI)
+        // Check that expecting capabilitiesThat anything fails when no callback has been received.
+        assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { true } }
+
+        // Basic test for true and false
+        mCallback.onCapabilitiesChanged(net, netCaps)
+        mCallback.expectCaps(net) { true }
+        mCallback.onCapabilitiesChanged(net, netCaps)
+        assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { false } }
+
+        // Try a positive and a negative case
+        mCallback.onCapabilitiesChanged(net, netCaps)
+        mCallback.expectCaps(net) {
+            it.hasCapability(NOT_METERED) && it.hasTransport(WIFI) && !it.hasTransport(CELLULAR)
+        }
+        mCallback.onCapabilitiesChanged(net, netCaps)
+        assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { it.hasTransport(CELLULAR) } }
+
+        // Try a matching callback on the wrong network
+        mCallback.onCapabilitiesChanged(net, netCaps)
+        assertFails {
+            mCallback.expectCaps(Network(100), SHORT_TIMEOUT_MS) { true }
+        }
+    }
+
+    @Test
+    fun testLinkPropertiesCallbacks() {
+        val net = Network(112)
+        val linkAddress = LinkAddress("fe80::ace:d00d/64")
+        val mtu = 1984
+        val linkProps = LinkProperties().apply {
+            this.mtu = mtu
+            interfaceName = TEST_INTERFACE_NAME
+            addLinkAddress(linkAddress)
+        }
+
+        // Check that expecting linkPropsThat anything fails when no callback has been received.
+        assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { true } }
+
+        // Basic test for true and false
+        mCallback.onLinkPropertiesChanged(net, linkProps)
+        mCallback.expect<LinkPropertiesChanged>(net) { true }
+        mCallback.onLinkPropertiesChanged(net, linkProps)
+        assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { false } }
+
+        // Try a positive and negative case
+        mCallback.onLinkPropertiesChanged(net, linkProps)
+        mCallback.expect<LinkPropertiesChanged>(net) {
+            it.lp.interfaceName == TEST_INTERFACE_NAME &&
+                    it.lp.linkAddresses.contains(linkAddress) &&
+                    it.lp.mtu == mtu
+        }
+        mCallback.onLinkPropertiesChanged(net, linkProps)
+        assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) {
+            it.lp.interfaceName != TEST_INTERFACE_NAME
+        } }
+
+        // Try a matching callback on the wrong network
+        mCallback.onLinkPropertiesChanged(net, linkProps)
+        assertFails { mCallback.expect<LinkPropertiesChanged>(Network(114), SHORT_TIMEOUT_MS) {
+            it.lp.interfaceName == TEST_INTERFACE_NAME
+        } }
+    }
+
+    @Test
+    fun testExpect() {
+        val net = Network(103)
+        // Test expectCallback fails when nothing was sent.
+        assertFails { mCallback.expect<BlockedStatus>(net, SHORT_TIMEOUT_MS) }
+
+        // Test onAvailable is seen and can be expected
+        mCallback.onAvailable(net)
+        mCallback.expect<Available>(net, SHORT_TIMEOUT_MS)
+
+        // Test onAvailable won't return calls with a different network
+        mCallback.onAvailable(Network(106))
+        assertFails { mCallback.expect<Available>(net, SHORT_TIMEOUT_MS) }
+
+        // Test onAvailable won't return calls with a different callback
+        mCallback.onAvailable(net)
+        assertFails { mCallback.expect<BlockedStatus>(net, SHORT_TIMEOUT_MS) }
+    }
+
+    @Test
+    fun testAllExpectOverloads() {
+        // This test should never run, it only checks that all overloads exist and build
+        assumeTrue(false)
+        val hn = object : TestableNetworkCallback.HasNetwork { override val network = ANY_NETWORK }
+
+        // Method with all arguments (version that takes a Network)
+        mCallback.expect(AVAILABLE, ANY_NETWORK, 10, "error") { true }
+
+        // Java overloads omitting one argument. One line for omitting each argument, in positional
+        // order. Versions that take a Network.
+        mCallback.expect(AVAILABLE, 10, "error") { true }
+        mCallback.expect(AVAILABLE, ANY_NETWORK, "error") { true }
+        mCallback.expect(AVAILABLE, ANY_NETWORK, 10) { true }
+        mCallback.expect(AVAILABLE, ANY_NETWORK, 10, "error")
+
+        // Java overloads for omitting two arguments. One line for omitting each pair of arguments.
+        // Versions that take a Network.
+        mCallback.expect(AVAILABLE, "error") { true }
+        mCallback.expect(AVAILABLE, 10) { true }
+        mCallback.expect(AVAILABLE, 10, "error")
+        mCallback.expect(AVAILABLE, ANY_NETWORK) { true }
+        mCallback.expect(AVAILABLE, ANY_NETWORK, "error")
+        mCallback.expect(AVAILABLE, ANY_NETWORK, 10)
+
+        // Java overloads for omitting three arguments. One line for each remaining argument.
+        // Versions that take a Network.
+        mCallback.expect(AVAILABLE) { true }
+        mCallback.expect(AVAILABLE, "error")
+        mCallback.expect(AVAILABLE, 10)
+        mCallback.expect(AVAILABLE, ANY_NETWORK)
+
+        // Java overload for omitting all four arguments.
+        mCallback.expect(AVAILABLE)
+
+        // Same orders as above, but versions that take a HasNetwork. Except overloads that
+        // were already tested because they omitted the Network argument
+        mCallback.expect(AVAILABLE, hn, 10, "error") { true }
+        mCallback.expect(AVAILABLE, hn, "error") { true }
+        mCallback.expect(AVAILABLE, hn, 10) { true }
+        mCallback.expect(AVAILABLE, hn, 10, "error")
+
+        mCallback.expect(AVAILABLE, hn) { true }
+        mCallback.expect(AVAILABLE, hn, "error")
+        mCallback.expect(AVAILABLE, hn, 10)
+
+        mCallback.expect(AVAILABLE, hn)
+
+        // Same as above but for reified versions.
+        mCallback.expect<Available>(ANY_NETWORK, 10, "error") { true }
+        mCallback.expect<Available>(timeoutMs = 10, errorMsg = "error") { true }
+        mCallback.expect<Available>(network = ANY_NETWORK, errorMsg = "error") { true }
+        mCallback.expect<Available>(network = ANY_NETWORK, timeoutMs = 10) { true }
+        mCallback.expect<Available>(network = ANY_NETWORK, timeoutMs = 10, errorMsg = "error")
+
+        mCallback.expect<Available>(errorMsg = "error") { true }
+        mCallback.expect<Available>(timeoutMs = 10) { true }
+        mCallback.expect<Available>(timeoutMs = 10, errorMsg = "error")
+        mCallback.expect<Available>(network = ANY_NETWORK) { true }
+        mCallback.expect<Available>(network = ANY_NETWORK, errorMsg = "error")
+        mCallback.expect<Available>(network = ANY_NETWORK, timeoutMs = 10)
+
+        mCallback.expect<Available> { true }
+        mCallback.expect<Available>(errorMsg = "error")
+        mCallback.expect<Available>(timeoutMs = 10)
+        mCallback.expect<Available>(network = ANY_NETWORK)
+        mCallback.expect<Available>()
+
+        mCallback.expect<Available>(hn, 10, "error") { true }
+        mCallback.expect<Available>(network = hn, errorMsg = "error") { true }
+        mCallback.expect<Available>(network = hn, timeoutMs = 10) { true }
+        mCallback.expect<Available>(network = hn, timeoutMs = 10, errorMsg = "error")
+
+        mCallback.expect<Available>(network = hn) { true }
+        mCallback.expect<Available>(network = hn, errorMsg = "error")
+        mCallback.expect<Available>(network = hn, timeoutMs = 10)
+
+        mCallback.expect<Available>(network = hn)
+    }
+
+    @Test
+    fun testExpectClass() {
+        val net = Network(1)
+        mCallback.onAvailable(net)
+        assertFails { mCallback.expect(LOST, net) }
+    }
+
+    @Test
+    fun testPoll() {
+        assertNull(mCallback.poll(SHORT_TIMEOUT_MS))
+        TNCInterpreter.interpretTestSpec(initial = mCallback, lineShift = 1,
+                threadTransform = { cb -> cb.createLinkedCopy() }, spec = """
+            sleep; onAvailable(133)    | poll(2) = Available(133) time 1..4
+                                       | poll(1) = null
+            onCapabilitiesChanged(108) | poll(1) = CapabilitiesChanged(108) time 0..3
+            onBlockedStatus(199)       | poll(1) = BlockedStatus(199) time 0..3
+        """)
+    }
+
+    @Test
+    fun testEventuallyExpect() {
+        // TODO: Current test does not verify the inline one. Also verify the behavior after
+        // aligning two eventuallyExpect()
+        val net1 = Network(100)
+        val net2 = Network(101)
+        mCallback.onAvailable(net1)
+        mCallback.onCapabilitiesChanged(net1, NetworkCapabilities())
+        mCallback.onLinkPropertiesChanged(net1, LinkProperties())
+        mCallback.eventuallyExpect(LINK_PROPERTIES_CHANGED) {
+            net1.equals(it.network)
+        }
+        // No further new callback. Expect no callback.
+        assertFails { mCallback.eventuallyExpect(LINK_PROPERTIES_CHANGED, SHORT_TIMEOUT_MS) }
+
+        // Verify no predicate set.
+        mCallback.onAvailable(net2)
+        mCallback.onLinkPropertiesChanged(net2, LinkProperties())
+        mCallback.onBlockedStatusChanged(net1, false)
+        mCallback.eventuallyExpect(BLOCKED_STATUS) { net1.equals(it.network) }
+        // Verify no callback received if the callback does not happen.
+        assertFails { mCallback.eventuallyExpect(LOSING, SHORT_TIMEOUT_MS) }
+    }
+
+    @Test
+    fun testEventuallyExpectOnMultiThreads() {
+        TNCInterpreter.interpretTestSpec(initial = mCallback, lineShift = 1,
+                threadTransform = { cb -> cb.createLinkedCopy() }, spec = """
+                onAvailable(100)                   | eventually(CapabilitiesChanged(100), 1) fails
+                sleep ; onCapabilitiesChanged(100) | eventually(CapabilitiesChanged(100), 3)
+                onAvailable(101) ; onBlockedStatus(101) | eventually(BlockedStatus(100), 2) fails
+                onSuspended(100) ; sleep ; onLost(100)  | eventually(Lost(100), 3)
+        """)
+    }
+}
+
+private object TNCInterpreter : ConcurrentInterpreter<TestableNetworkCallback>(interpretTable)
+
+val EntryList = CallbackEntry::class.sealedSubclasses.map { it.simpleName }.joinToString("|")
+private fun callbackEntryFromString(name: String): KClass<out CallbackEntry> {
+    return CallbackEntry::class.sealedSubclasses.first { it.simpleName == name }
+}
+
+@SuppressLint("NewApi") // Uses hidden APIs, which the linter would identify as missing APIs.
+private val interpretTable = listOf<InterpretMatcher<TestableNetworkCallback>>(
+    // Interpret "Available(xx)" as "call to onAvailable with netId xx", and likewise for
+    // all callback types. This is implemented above by enumerating the subclasses of
+    // CallbackEntry and reading their simpleName.
+    Regex("""(.*)\s+=\s+($EntryList)\((\d+)\)""") to { i, cb, t ->
+        val record = i.interpret(t.strArg(1), cb)
+        assertTrue(callbackEntryFromString(t.strArg(2)).isInstance(record))
+        // Strictly speaking testing for is CallbackEntry is useless as it's been tested above
+        // but the compiler can't figure things out from the isInstance call. It does understand
+        // from the assertTrue(is CallbackEntry) that this is true, which allows to access
+        // the 'network' member below.
+        assertTrue(record is CallbackEntry)
+        assertEquals(record.network.netId, t.intArg(3))
+    },
+    // Interpret "onAvailable(xx)" as calling "onAvailable" with a netId of xx, and likewise for
+    // all callback types. NetworkCapabilities and LinkProperties just get an empty object
+    // as their argument. Losing gets the default linger timer. Blocked gets false.
+    Regex("""on($EntryList)\((\d+)\)""") to { i, cb, t ->
+        val net = Network(t.intArg(2))
+        when (t.strArg(1)) {
+            "Available" -> cb.onAvailable(net)
+            // PreCheck not used in tests. Add it here if it becomes useful.
+            "CapabilitiesChanged" -> cb.onCapabilitiesChanged(net, NetworkCapabilities())
+            "LinkPropertiesChanged" -> cb.onLinkPropertiesChanged(net, LinkProperties())
+            "Suspended" -> cb.onNetworkSuspended(net)
+            "Resumed" -> cb.onNetworkResumed(net)
+            "Losing" -> cb.onLosing(net, DEFAULT_LINGER_DELAY_MS)
+            "Lost" -> cb.onLost(net)
+            "Unavailable" -> cb.onUnavailable()
+            "BlockedStatus" -> cb.onBlockedStatusChanged(net, false)
+            else -> fail("Unknown callback type")
+        }
+    },
+    Regex("""poll\((\d+)\)""") to { i, cb, t -> cb.poll(t.timeArg(1)) },
+    // Interpret "eventually(Available(xx), timeout)" as calling eventuallyExpect that expects
+    // CallbackEntry.AVAILABLE with netId of xx within timeout*INTERPRET_TIME_UNIT timeout, and
+    // likewise for all callback types.
+    Regex("""eventually\(($EntryList)\((\d+)\),\s+(\d+)\)""") to { i, cb, t ->
+        val net = Network(t.intArg(2))
+        val timeout = t.timeArg(3)
+        when (t.strArg(1)) {
+            "Available" -> cb.eventuallyExpect(AVAILABLE, timeout) { net == it.network }
+            "Suspended" -> cb.eventuallyExpect(SUSPENDED, timeout) { net == it.network }
+            "Resumed" -> cb.eventuallyExpect(RESUMED, timeout) { net == it.network }
+            "Losing" -> cb.eventuallyExpect(LOSING, timeout) { net == it.network }
+            "Lost" -> cb.eventuallyExpect(LOST, timeout) { net == it.network }
+            "Unavailable" -> cb.eventuallyExpect(UNAVAILABLE, timeout) { net == it.network }
+            "BlockedStatus" -> cb.eventuallyExpect(BLOCKED_STATUS, timeout) { net == it.network }
+            "CapabilitiesChanged" ->
+                cb.eventuallyExpect(NETWORK_CAPS_UPDATED, timeout) { net == it.network }
+            "LinkPropertiesChanged" ->
+                cb.eventuallyExpect(LINK_PROPERTIES_CHANGED, timeout) { net == it.network }
+            else -> fail("Unknown callback type")
+        }
+    }
+)
diff --git a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTestJava.java b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTestJava.java
new file mode 100644
index 0000000..4570d0a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTestJava.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import static com.android.testutils.RecorderCallback.CallbackEntry.AVAILABLE;
+import static com.android.testutils.TestableNetworkCallbackKt.anyNetwork;
+
+import static org.junit.Assume.assumeTrue;
+
+import org.junit.Test;
+
+public class TestableNetworkCallbackTestJava {
+    @Test
+    void testAllExpectOverloads() {
+        // This test should never run, it only checks that all overloads exist and build
+        assumeTrue(false);
+        final TestableNetworkCallback callback = new TestableNetworkCallback();
+        TestableNetworkCallback.HasNetwork hn = TestableNetworkCallbackKt::anyNetwork;
+
+        // Method with all arguments (version that takes a Network)
+        callback.expect(AVAILABLE, anyNetwork(), 10, "error", cb -> true);
+
+        // Overloads omitting one argument. One line for omitting each argument, in positional
+        // order. Versions that take a Network.
+        callback.expect(AVAILABLE, 10, "error", cb -> true);
+        callback.expect(AVAILABLE, anyNetwork(), "error", cb -> true);
+        callback.expect(AVAILABLE, anyNetwork(), 10, cb -> true);
+        callback.expect(AVAILABLE, anyNetwork(), 10, "error");
+
+        // Overloads for omitting two arguments. One line for omitting each pair of arguments.
+        // Versions that take a Network.
+        callback.expect(AVAILABLE, "error", cb -> true);
+        callback.expect(AVAILABLE, 10, cb -> true);
+        callback.expect(AVAILABLE, 10, "error");
+        callback.expect(AVAILABLE, anyNetwork(), cb -> true);
+        callback.expect(AVAILABLE, anyNetwork(), "error");
+        callback.expect(AVAILABLE, anyNetwork(), 10);
+
+        // Overloads for omitting three arguments. One line for each remaining argument.
+        // Versions that take a Network.
+        callback.expect(AVAILABLE, cb -> true);
+        callback.expect(AVAILABLE, "error");
+        callback.expect(AVAILABLE, 10);
+        callback.expect(AVAILABLE, anyNetwork());
+
+        // Java overload for omitting all four arguments.
+        callback.expect(AVAILABLE);
+
+        // Same orders as above, but versions that take a HasNetwork. Except overloads that
+        // were already tested because they omitted the Network argument
+        callback.expect(AVAILABLE, hn, 10, "error", cb -> true);
+        callback.expect(AVAILABLE, hn, "error", cb -> true);
+        callback.expect(AVAILABLE, hn, 10, cb -> true);
+        callback.expect(AVAILABLE, hn, 10, "error");
+
+        callback.expect(AVAILABLE, hn, cb -> true);
+        callback.expect(AVAILABLE, hn, "error");
+        callback.expect(AVAILABLE, hn, 10);
+
+        callback.expect(AVAILABLE, hn);
+    }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
new file mode 100644
index 0000000..440f5ca
--- /dev/null
+++ b/staticlibs/testutils/Android.bp
@@ -0,0 +1,112 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "net-tests-utils",
+    srcs: [
+        "devicetests/**/*.java",
+        "devicetests/**/*.kt",
+    ],
+    defaults: [
+        "framework-connectivity-test-defaults",
+        "lib_mockito_extended",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+        "net-utils-device-common-bpf", // TestBpfMap extends IBpfMap.
+    ],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "kotlin-reflect",
+        "libnanohttpd",
+        "net-tests-utils-host-device-common",
+        "net-utils-device-common",
+        "net-utils-device-common-async",
+        "net-utils-device-common-netlink",
+        "net-utils-device-common-struct",
+        "net-utils-device-common-struct-base",
+        "net-utils-device-common-wear",
+        "net-utils-framework-connectivity",
+        "modules-utils-build_system",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+    },
+}
+
+java_library {
+    // Consider using net-tests-utils instead if writing device code.
+    // That library has a lot more useful tools into it for users that
+    // work on Android and includes this lib.
+    name: "net-tests-utils-host-device-common",
+    srcs: [
+        "hostdevice/**/*.java",
+        "hostdevice/**/*.kt",
+    ],
+    host_supported: true,
+    visibility: [
+        "//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
+        "//packages/modules/Connectivity/staticlibs/client-libs/tests:__subpackages__",
+        "//packages/modules/Connectivity/tests/cts/hostside",
+    ],
+    // There are downstream branches using an old version of Kotlin
+    // that used to reserve the right to make breaking changes to the
+    // Result type and disallowed returning an instance of it.
+    // Later versions allowed this and there was never a change,
+    // so no matter the version returning Result is always fine,
+    // but on sc-mainline-prod the compiler rejects it without
+    // the following flag.
+    kotlincflags: ["-Xallow-result-return-type"],
+    libs: [
+        "jsr305",
+    ],
+    static_libs: [
+        "kotlin-test",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+    },
+}
+
+java_test_host {
+    name: "net-tests-utils-host-common",
+    srcs: [
+        "host/java/**/*.java",
+        "host/java/**/*.kt",
+    ],
+    libs: ["tradefed"],
+    test_suites: [
+        "ats",
+        "device-tests",
+        "general-tests",
+        "cts",
+        "mts-networking",
+        "mts-tethering",
+        "mcts-tethering",
+    ],
+    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
new file mode 100644
index 0000000..394c6be
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -0,0 +1,36 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_team: "trendy_team_fwk_core_networking",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "ConnectivityTestPreparer",
+    srcs: ["src/**/*.kt"],
+    sdk_version: "system_current",
+    // Allow running the test on any device with SDK R+, even when built from a branch that uses
+    // an unstable SDK, by targeting a stable SDK regardless of the build SDK.
+    min_sdk_version: "30",
+    target_sdk_version: "30",
+    static_libs: [
+        "androidx.test.rules",
+        "modules-utils-build_system",
+        "net-tests-utils",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+    },
+}
diff --git a/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml b/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml
new file mode 100644
index 0000000..015b41f
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.testutils.connectivitypreparer">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <!-- For wifi scans -->
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.testutils.connectivitypreparer"
+                     android:label="Connectivity test target preparer" />
+</manifest>
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
new file mode 100644
index 0000000..e634f0e
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.connectivitypreparer
+
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.telephony.TelephonyManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectUtil
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ConnectivityCheckTest {
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+    private val pm by lazy { context.packageManager }
+    private val connectUtil by lazy { ConnectUtil(context) }
+
+    @Test
+    fun testCheckWifiSetup() {
+        if (!pm.hasSystemFeature(FEATURE_WIFI)) return
+        connectUtil.ensureWifiValidated()
+    }
+
+    @Test
+    fun testCheckTelephonySetup() {
+        if (!pm.hasSystemFeature(FEATURE_TELEPHONY)) return
+        val tm = context.getSystemService(TelephonyManager::class.java)
+                ?: fail("Could not get telephony service")
+
+        val commonError = "Check the test bench. To run the tests anyway for quick & dirty local " +
+                "testing, you can use atest X -- " +
+                "--test-arg com.android.testutils.ConnectivityTestTargetPreparer" +
+                ":ignore-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")
+        } else if (tm.simState != TelephonyManager.SIM_STATE_READY) {
+            fail("The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
+                    commonError)
+        }
+        assertTrue(tm.isDataConnectivityPossible,
+            "The device has a SIM card, but it does not supports data connectivity. " +
+            "Check the data plan, and verify that mobile data is working. " + commonError)
+        connectUtil.ensureCellularValidated()
+    }
+}
diff --git a/staticlibs/testutils/devicetests/NSResponder.kt b/staticlibs/testutils/devicetests/NSResponder.kt
new file mode 100644
index 0000000..f7619cd
--- /dev/null
+++ b/staticlibs/testutils/devicetests/NSResponder.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.MacAddress
+import android.util.Log
+import com.android.net.module.util.Ipv6Utils
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA
+import com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
+import com.android.net.module.util.Struct
+import com.android.net.module.util.structs.Icmpv6Header
+import com.android.net.module.util.structs.Ipv6Header
+import com.android.net.module.util.structs.LlaOption
+import com.android.net.module.util.structs.NsHeader
+import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
+import java.lang.IllegalArgumentException
+import java.net.Inet6Address
+import java.nio.ByteBuffer
+
+private const val NS_TYPE = 135.toShort()
+
+/**
+ * A class that can be used to reply to Neighbor Solicitation packets on a [TapPacketReader].
+ */
+class NSResponder(
+    reader: TapPacketReader,
+    table: Map<Inet6Address, MacAddress>,
+    name: String = NSResponder::class.java.simpleName
+) : PacketResponder(reader, Icmpv6Filter(), name) {
+    companion object {
+        private val TAG = NSResponder::class.simpleName
+    }
+
+    // Copy the map if not already immutable (toMap) to make sure it is not modified
+    private val table = table.toMap()
+
+    override fun replyToPacket(packet: ByteArray, reader: TapPacketReader) {
+        if (packet.size < IPV6_HEADER_LENGTH) {
+            return
+        }
+        val buf = ByteBuffer.wrap(packet, ETHER_HEADER_LEN, packet.size - ETHER_HEADER_LEN)
+        val ipv6Header = parseOrLog(Ipv6Header::class.java, buf) ?: return
+        val icmpHeader = parseOrLog(Icmpv6Header::class.java, buf) ?: return
+        if (icmpHeader.type != NS_TYPE) {
+            return
+        }
+        val ns = parseOrLog(NsHeader::class.java, buf) ?: return
+        val replyMacAddr = table[ns.target] ?: return
+        val slla = parseOrLog(LlaOption::class.java, buf) ?: return
+        val requesterMac = slla.linkLayerAddress
+
+        val tlla = LlaOption.build(ICMPV6_ND_OPTION_TLLA.toByte(), replyMacAddr)
+        reader.sendResponse(Ipv6Utils.buildNaPacket(
+            replyMacAddr /* srcMac */,
+            requesterMac /* dstMac */,
+            ns.target /* srcIp */,
+            ipv6Header.srcIp /* dstIp */,
+            NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED,
+            ns.target,
+            tlla))
+    }
+
+    private fun <T> parseOrLog(clazz: Class<T>, buf: ByteBuffer): T? where T : Struct {
+        return try {
+            Struct.parse(clazz, buf)
+        } catch (e: IllegalArgumentException) {
+            Log.e(TAG, "Invalid ${clazz.simpleName} in ICMPv6 packet", e)
+            null
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt b/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt
new file mode 100644
index 0000000..cf0490c
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.MacAddress
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import java.net.Inet4Address
+import java.net.InetAddress
+import java.nio.ByteBuffer
+
+private const val ARP_SENDER_MAC_OFFSET = ETHER_HEADER_LEN + 8
+private const val ARP_TARGET_IPADDR_OFFSET = ETHER_HEADER_LEN + 24
+
+private val TYPE_ARP = byteArrayOf(0x08, 0x06)
+// Arp reply header for IPv4 over ethernet
+private val ARP_REPLY_IPV4 = byteArrayOf(0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x02)
+
+/**
+ * A class that can be used to reply to ARP packets on a [TapPacketReader].
+ */
+class ArpResponder(
+    reader: TapPacketReader,
+    table: Map<Inet4Address, MacAddress>,
+    name: String = ArpResponder::class.java.simpleName
+) : PacketResponder(reader, ArpRequestFilter(), name) {
+    // Copy the map if not already immutable (toMap) to make sure it is not modified
+    private val table = table.toMap()
+
+    override fun replyToPacket(packet: ByteArray, reader: TapPacketReader) {
+        val targetIp = InetAddress.getByAddress(
+                packet.copyFromIndexWithLength(ARP_TARGET_IPADDR_OFFSET, 4))
+                as Inet4Address
+
+        val macAddr = table[targetIp]?.toByteArray() ?: return
+        val senderMac = packet.copyFromIndexWithLength(ARP_SENDER_MAC_OFFSET, 6)
+        reader.sendResponse(ByteBuffer.wrap(
+                // Ethernet header
+                senderMac + macAddr + TYPE_ARP +
+                        // ARP message
+                        ARP_REPLY_IPV4 +
+                        macAddr /* sender MAC */ +
+                        targetIp.address /* sender IP addr */ +
+                        macAddr /* target mac */ +
+                        targetIp.address /* target IP addr */
+        ))
+    }
+}
+
+private fun ByteArray.copyFromIndexWithLength(start: Int, len: Int) =
+        copyOfRange(start, start + len)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/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/CompatUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt
new file mode 100644
index 0000000..82f1d9b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.NetworkSpecifier
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
+
+/**
+ * Test utility to create [NetworkSpecifier]s on different SDK versions.
+ */
+object CompatUtil {
+    @JvmStatic
+    fun makeTestNetworkSpecifier(ifName: String): NetworkSpecifier {
+        // Until R, there was no TestNetworkSpecifier, StringNetworkSpecifier was used instead
+        if (!isAtLeastS()) {
+            return makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
+        }
+        // TestNetworkSpecifier is not part of the SDK in some branches using this utility
+        // TODO: replace with a direct call to the constructor
+        return makeNetworkSpecifierInternal("android.net.TestNetworkSpecifier", ifName)
+    }
+
+    @JvmStatic
+    fun makeEthernetNetworkSpecifier(ifName: String): NetworkSpecifier {
+        // Until R, there was no EthernetNetworkSpecifier, StringNetworkSpecifier was used instead
+        if (!isAtLeastS()) {
+            return makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
+        }
+        // EthernetNetworkSpecifier is not part of the SDK in some branches using this utility
+        // TODO: replace with a direct call to the constructor
+        return makeNetworkSpecifierInternal("android.net.EthernetNetworkSpecifier", ifName)
+    }
+
+    private fun makeNetworkSpecifierInternal(clazz: String, specifier: String): NetworkSpecifier {
+        // StringNetworkSpecifier was removed after R (and was hidden API before that)
+        return Class.forName(clazz)
+                .getConstructor(String::class.java).newInstance(specifier) as NetworkSpecifier
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt
new file mode 100644
index 0000000..9e72f4b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt
@@ -0,0 +1,240 @@
+package com.android.testutils
+
+import android.os.SystemClock
+import java.util.concurrent.CyclicBarrier
+import kotlin.test.assertEquals
+import kotlin.test.assertFails
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+// The table contains pairs associating a regexp with the code to run. The statement is matched
+// against each matcher in sequence and when a match is found the associated code is run, passing
+// it the TrackRecord under test and the result of the regexp match.
+typealias InterpretMatcher<T> = Pair<Regex, (ConcurrentInterpreter<T>, T, MatchResult) -> Any?>
+
+// The default unit of time for interpreted tests
+const val INTERPRET_TIME_UNIT = 60L // ms
+
+/**
+ * A small interpreter for testing parallel code.
+ *
+ * The interpreter will read a list of lines consisting of "|"-separated statements, e.g. :
+ *   sleep 2 ; unblock thread2 | wait thread2 time 2..5
+ *   sendMessage "x"           | obtainMessage = "x" time 0..1
+ *
+ * Each column runs in a different concurrent thread and all threads wait for each other in
+ * between lines. Each statement is split on ";" then matched with regular expressions in the
+ * instructionTable constant, which contains the code associated with each statement. The
+ * interpreter supports an object being passed to the interpretTestSpec() method to be passed
+ * in each lambda (think about the object under test), and an optional transform function to be
+ * executed on the object at the start of every thread.
+ *
+ * The time unit is defined in milliseconds by the interpretTimeUnit member, which has a default
+ * value but can be passed to the constructor. Whitespace is ignored.
+ *
+ * The interpretation table has to be passed as an argument. It's a table associating a regexp
+ * with the code that should execute, as a function taking three arguments : the interpreter,
+ * the regexp match, and the object. See the individual tests for the DSL of that test.
+ * Implementors for new interpreting languages are encouraged to look at the defaultInterpretTable
+ * constant below for an example of how to write an interpreting table.
+ * Some expressions already exist by default and can be used by all interpreters. Refer to
+ * getDefaultInstructions() below for a list and documentation.
+ */
+open class ConcurrentInterpreter<T>(localInterpretTable: List<InterpretMatcher<T>>) {
+    private val interpretTable: List<InterpretMatcher<T>> =
+            localInterpretTable + getDefaultInstructions()
+    // The last time the thread became blocked, with base System.currentTimeMillis(). This should
+    // be set immediately before any time the thread gets blocked.
+    internal val lastBlockedTime = ThreadLocal<Long>()
+
+    // Split the line into multiple statements separated by ";" and execute them. Return whatever
+    // the last statement returned.
+    fun interpretMultiple(instr: String, r: T): Any? {
+        return instr.split(";").map { interpret(it.trim(), r) }.last()
+    }
+
+    // Match the statement to a regex and interpret it.
+    fun interpret(instr: String, r: T): Any? {
+        val (matcher, code) =
+                interpretTable.find { instr matches it.first } ?: throw SyntaxException(instr)
+        val match = matcher.matchEntire(instr) ?: throw SyntaxException(instr)
+        return code(this, r, match)
+    }
+
+    /**
+     * Spins as many threads as needed by the test spec and interpret each program concurrently.
+     *
+     * All threads wait on a CyclicBarrier after each line.
+     * |lineShift| says how many lines after the call the spec starts. This is used for error
+     * reporting. Unfortunately AFAICT there is no way to get the line of an argument rather
+     * than the line at which the expression starts.
+     *
+     * This method is mostly meant for implementations that extend the ConcurrentInterpreter
+     * class to add their own directives and instructions. These may need to operate on some
+     * data, which can be passed in |initial|. For example, an interpreter specialized in callbacks
+     * may want to pass the callback there. In some cases, it's necessary that each thread
+     * performs a transformation *after* it starts on that value before starting ; in this case,
+     * the transformation can be passed to |threadTransform|. The default is to return |initial| as
+     * is. Look at some existing child classes of this interpreter for some examples of how this
+     * can be used.
+     *
+     * @param spec The test spec, as a string of lines separated by pipes.
+     * @param initial An initial value passed to all threads.
+     * @param lineShift How many lines after the call the spec starts, for error reporting.
+     * @param threadTransform an optional transformation that each thread will apply to |initial|
+     */
+    fun interpretTestSpec(
+        spec: String,
+        initial: T,
+        lineShift: Int = 0,
+        threadTransform: (T) -> T = { it }
+    ) {
+        // For nice stack traces
+        val callSite = getCallingMethod()
+        val lines = spec.trim().trim('\n').split("\n").map { it.split("|") }
+        // |lines| contains arrays of strings that make up the statements of a thread : in other
+        // words, it's an array that contains a list of statements for each column in the spec.
+        // E.g. if the string is """
+        //   a | b | c
+        //   d | e | f
+        // """, then lines is [ [ "a", "b", "c" ], [ "d", "e", "f" ] ].
+        val threadCount = lines[0].size
+        assertTrue(lines.all { it.size == threadCount })
+        val threadInstructions = (0 until threadCount).map { i -> lines.map { it[i].trim() } }
+        // |threadInstructions| is a list where each element is the list of instructions for the
+        // thread at the index. In other words, it's just |lines| transposed. In the example
+        // above, it would be [ [ "a", "d" ], [ "b", "e" ], [ "c", "f" ] ]
+        // mapIndexed below will pass in |instructions| the list of instructions for this thread.
+        val barrier = CyclicBarrier(threadCount)
+        var crash: InterpretException? = null
+        threadInstructions.mapIndexed { threadIndex, instructions ->
+            Thread {
+                val threadLocal = threadTransform(initial)
+                lastBlockedTime.set(System.currentTimeMillis())
+                barrier.await()
+                var lineNum = 0
+                instructions.forEach {
+                    if (null != crash) return@Thread
+                    lineNum += 1
+                    try {
+                        interpretMultiple(it, threadLocal)
+                    } catch (e: Throwable) {
+                        // If fail() or some exception was called, the thread will come here ; if
+                        // the exception isn't caught the process will crash, which is not nice for
+                        // testing. Instead, catch the exception, cancel other threads, and report
+                        // nicely. Catch throwable because fail() is AssertionError, which inherits
+                        // from Error.
+                        crash = InterpretException(threadIndex, it,
+                                callSite.lineNumber + lineNum + lineShift,
+                                callSite.className, callSite.methodName, callSite.fileName, e)
+                    }
+                    lastBlockedTime.set(System.currentTimeMillis())
+                    barrier.await()
+                }
+            }.also { it.start() }
+        }.forEach { it.join() }
+        // If the test failed, crash with line number
+        crash?.let { throw it }
+    }
+
+    // Helper to get the stack trace for a calling method
+    private fun getCallingStackTrace(): Array<StackTraceElement> {
+        try {
+            throw RuntimeException()
+        } catch (e: RuntimeException) {
+            return e.stackTrace
+        }
+    }
+
+    // Find the calling method. This is the first method in the stack trace that is annotated
+    // with @Test.
+    fun getCallingMethod(): StackTraceElement {
+        val stackTrace = getCallingStackTrace()
+        return stackTrace.find { element ->
+            val clazz = Class.forName(element.className)
+            // Because the stack trace doesn't list the formal arguments, find all methods with
+            // this name and return this name if any of them is annotated with @Test.
+            clazz.declaredMethods
+                    .filter { method -> method.name == element.methodName }
+                    .any { method -> method.getAnnotation(org.junit.Test::class.java) != null }
+        } ?: stackTrace[3]
+        // If no method is annotated return the 4th one, because that's what it usually is :
+        // 0 is getCallingStackTrace, 1 is this method, 2 is ConcurrentInterpreter#interpretTestSpec
+    }
+}
+
+/**
+ * Default instructions available to all interpreters.
+ * sleep(x) : sleeps for x time units and returns Unit ; sleep alone means sleep(1)
+ * EXPR = VALUE : asserts that EXPR equals VALUE. EXPR is interpreted. VALUE can either be the
+ *   string "null" or an int. Returns Unit.
+ * EXPR time x..y : measures the time taken by EXPR and asserts it took at least x and at most
+ *   y time units.
+ * EXPR // any text : comments are ignored.
+ * EXPR fails : checks that EXPR throws some exception.
+ */
+private fun <T> getDefaultInstructions() = listOf<InterpretMatcher<T>>(
+    // Interpret an empty line as doing nothing.
+    Regex("") to { _, _, _ -> null },
+    // Ignore comments.
+    Regex("(.*)//.*") to { i, t, r -> i.interpret(r.strArg(1), t) },
+    // Interpret "XXX time x..y" : run XXX and check it took at least x and not more than y
+    Regex("""(.*)\s*time\s*(\d+)\.\.(\d+)""") to { i, t, r ->
+        val lateStart = System.currentTimeMillis()
+        i.interpret(r.strArg(1), t)
+        val end = System.currentTimeMillis()
+        // There is uncertainty in measuring time.
+        // It takes some (small) time for the thread to even measure the time at which it
+        // starts interpreting the instruction. It is therefore possible that thread A sleeps for
+        // n milliseconds, and B expects to have waited for at least n milliseconds, but because
+        // B started measuring after 1ms or so, B thinks it didn't wait long enough.
+        // To avoid this, when the `time` instruction tests the instruction took at least X and
+        // at most Y, it tests X against a time measured since *before* the thread blocked but
+        // Y against a time measured as late as possible. This ensures that the timer is
+        // sufficiently lenient in both directions that there are no flaky measures.
+        val minTime = end - lateStart
+        val maxTime = end - i.lastBlockedTime.get()!!
+
+        assertTrue(maxTime >= r.timeArg(2),
+                "Should have taken at least ${r.timeArg(2)} but took less than $maxTime")
+        assertTrue(minTime <= r.timeArg(3),
+                "Should have taken at most ${r.timeArg(3)} but took more than $minTime")
+    },
+    // Interpret "XXX = YYY" : run XXX and assert its return value is equal to YYY. "null" supported
+    Regex("""(.*)\s*=\s*(null|\d+)""") to { i, t, r ->
+        i.interpret(r.strArg(1), t).also {
+            if ("null" == r.strArg(2)) assertNull(it) else assertEquals(r.intArg(2), it)
+        }
+    },
+    // Interpret sleep. Optional argument for the count, in INTERPRET_TIME_UNIT units.
+    Regex("""sleep(\((\d+)\))?""") to { i, t, r ->
+        SystemClock.sleep(if (r.strArg(2).isEmpty()) INTERPRET_TIME_UNIT else r.timeArg(2))
+    },
+    Regex("""(.*)\s*fails""") to { i, t, r ->
+        assertFails { i.interpret(r.strArg(1), t) }
+    }
+)
+
+class SyntaxException(msg: String, cause: Throwable? = null) : RuntimeException(msg, cause)
+class InterpretException(
+    threadIndex: Int,
+    instr: String,
+    lineNum: Int,
+    className: String,
+    methodName: String,
+    fileName: String,
+    cause: Throwable
+) : RuntimeException("Failure: $instr", cause) {
+    init {
+        stackTrace = arrayOf(StackTraceElement(
+                className,
+                "$methodName:thread$threadIndex",
+                fileName,
+                lineNum)) + super.getStackTrace()
+    }
+}
+
+// Some small helpers to avoid to say the large ".groupValues[index].trim()" every time
+fun MatchResult.strArg(index: Int) = this.groupValues[index].trim()
+fun MatchResult.intArg(index: Int) = strArg(index).toInt()
+fun MatchResult.timeArg(index: Int) = INTERPRET_TIME_UNIT * intArg(index)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
new file mode 100644
index 0000000..3857810
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.Manifest.permission
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.ScanResult
+import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiManager
+import android.os.ParcelFileDescriptor
+import android.os.SystemClock
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val MAX_WIFI_CONNECT_RETRIES = 10
+private const val WIFI_CONNECT_INTERVAL_MS = 500L
+private const val WIFI_CONNECT_TIMEOUT_MS = 30_000L
+
+// Constants used by WifiManager.ActionListener#onFailure. Although onFailure is SystemApi,
+// the error code constants are not (b/204277752)
+private const val WIFI_ERROR_IN_PROGRESS = 1
+private const val WIFI_ERROR_BUSY = 2
+
+class ConnectUtil(private val context: Context) {
+    private val TAG = ConnectUtil::class.java.simpleName
+
+    private val cm = context.getSystemService(ConnectivityManager::class.java)
+            ?: fail("Could not find ConnectivityManager")
+    private val wifiManager = context.getSystemService(WifiManager::class.java)
+            ?: fail("Could not find WifiManager")
+
+    fun ensureWifiConnected(): Network = ensureWifiConnected(requireValidated = false)
+    fun ensureWifiValidated(): Network = ensureWifiConnected(requireValidated = true)
+
+    fun ensureCellularValidated(): Network {
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(
+            NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
+        return tryTest {
+            val errorMsg = "The device does not have mobile data available. Check that it is " +
+                    "setup with a SIM card that has a working data plan, that the APN " +
+                    "configuration is valid, and that the device can access the internet through " +
+                    "mobile data."
+            cb.eventuallyExpect<CapabilitiesChanged>(errorMsg) {
+                it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+            }.network
+        } cleanup {
+            cm.unregisterNetworkCallback(cb)
+        }
+    }
+
+    private fun ensureWifiConnected(requireValidated: Boolean): Network {
+        val callback = TestableNetworkCallback(timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
+        cm.registerNetworkCallback(NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build(), callback)
+
+        return tryTest {
+            val connInfo = wifiManager.connectionInfo
+            Log.d(TAG, "connInfo=" + connInfo)
+            if (connInfo == null || connInfo.networkId == -1) {
+                clearWifiBlocklist()
+                val pfd = getInstrumentation().uiAutomation.executeShellCommand("svc wifi enable")
+                // Read the output stream to ensure the command has completed
+                ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() }
+                val config = getOrCreateWifiConfiguration()
+                connectToWifiConfig(config)
+            }
+            val errorMsg = if (requireValidated) {
+                "The wifi access point did not have access to the internet after " +
+                        "$WIFI_CONNECT_TIMEOUT_MS ms. Check that it has a working connection."
+            } else {
+                "Could not connect to a wifi access point within $WIFI_CONNECT_TIMEOUT_MS ms. " +
+                        "Check that the test device has a wifi network configured, and that the " +
+                        "test access point is functioning properly."
+            }
+            val cb = callback.eventuallyExpect<CapabilitiesChanged>(errorMsg) {
+                (!requireValidated || it.caps.hasCapability(NET_CAPABILITY_VALIDATED))
+            }
+            cb.network
+        } cleanup {
+            cm.unregisterNetworkCallback(callback)
+        }
+    }
+
+    // Suppress warning because WifiManager methods to connect to a config are
+    // documented not to be deprecated for privileged users.
+    @Suppress("DEPRECATION")
+    fun connectToWifiConfig(config: WifiConfiguration) {
+        repeat(MAX_WIFI_CONNECT_RETRIES) {
+            val error = runAsShell(permission.NETWORK_SETTINGS) {
+                val listener = ConnectWifiListener()
+                wifiManager.connect(config, listener)
+                listener.connectFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+            } ?: return // Connect succeeded
+
+            // Only retry for IN_PROGRESS and BUSY
+            if (error != WIFI_ERROR_IN_PROGRESS && error != WIFI_ERROR_BUSY) {
+                fail("Failed to connect to " + config.SSID + ": " + error)
+            }
+            Log.w(TAG, "connect failed with $error; waiting before retry")
+            SystemClock.sleep(WIFI_CONNECT_INTERVAL_MS)
+        }
+        fail("Failed to connect to ${config.SSID} after $MAX_WIFI_CONNECT_RETRIES retries")
+    }
+
+    private class ConnectWifiListener : WifiManager.ActionListener {
+        /**
+         * Future completed when the connect process ends. Provides the error code or null if none.
+         */
+        val connectFuture = CompletableFuture<Int?>()
+        override fun onSuccess() {
+            connectFuture.complete(null)
+        }
+
+        override fun onFailure(reason: Int) {
+            connectFuture.complete(reason)
+        }
+    }
+
+    private fun getOrCreateWifiConfiguration(): WifiConfiguration {
+        val configs = runAsShell(permission.NETWORK_SETTINGS) {
+            wifiManager.getConfiguredNetworks()
+        }
+        // If no network is configured, add a config for virtual access points if applicable
+        if (configs.size == 0) {
+            val scanResults = getWifiScanResults()
+            val virtualConfig = maybeConfigureVirtualNetwork(scanResults)
+            assertNotNull(virtualConfig, "The device has no configured wifi network")
+            return virtualConfig
+        }
+        // No need to add a configuration: there is already one.
+        if (configs.size > 1) {
+            // For convenience in case of local testing on devices with multiple saved configs,
+            // prefer the first configuration that is in range.
+            // In actual tests, there should only be one configuration, and it should be usable as
+            // assumed by WifiManagerTest.testConnect.
+            Log.w(TAG, "Multiple wifi configurations found: " +
+                    configs.joinToString(", ") { it.SSID })
+            val scanResultsList = getWifiScanResults()
+            Log.i(TAG, "Scan results: " + scanResultsList.joinToString(", ") {
+                "${it.SSID} (${it.level})"
+            })
+
+            val scanResults = scanResultsList.map { "\"${it.SSID}\"" }.toSet()
+            return configs.firstOrNull { scanResults.contains(it.SSID) } ?: configs[0]
+        }
+        return configs[0]
+    }
+
+    private fun getWifiScanResults(): List<ScanResult> {
+        val scanResultsFuture = CompletableFuture<List<ScanResult>>()
+        runAsShell(permission.NETWORK_SETTINGS) {
+            val receiver: BroadcastReceiver = object : BroadcastReceiver() {
+                override fun onReceive(context: Context, intent: Intent) {
+                    scanResultsFuture.complete(wifiManager.scanResults)
+                }
+            }
+            context.registerReceiver(receiver,
+                    IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
+            wifiManager.startScan()
+        }
+        return try {
+            scanResultsFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        } catch (e: Exception) {
+            throw AssertionError("Wifi scan results not received within timeout", e)
+        }
+    }
+
+    /**
+     * If a virtual wifi network is detected, add a configuration for that network.
+     * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
+     */
+    private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
+        // Virtual wifi networks used on the emulator and cloud testing infrastructure
+        val virtualSsids = listOf("VirtWifi", "AndroidWifi")
+        Log.d(TAG, "Wifi scan results: $scanResults")
+        val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) }
+                ?: return null
+
+        // Only add the virtual configuration if the virtual AP is detected in scans
+        val virtualConfig = WifiConfiguration()
+        // ASCII SSIDs need to be surrounded by double quotes
+        virtualConfig.SSID = "\"${virtualScanResult.SSID}\""
+        virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
+        runAsShell(permission.NETWORK_SETTINGS) {
+            val networkId = wifiManager.addNetwork(virtualConfig)
+            assertTrue(networkId >= 0)
+            assertTrue(wifiManager.enableNetwork(networkId, false /* attemptConnect */))
+        }
+        return virtualConfig
+    }
+
+    /**
+     * Re-enable wifi networks that were blocked, typically because no internet connection was
+     * detected the last time they were connected. This is necessary to make sure wifi can reconnect
+     * to them.
+     */
+    private fun clearWifiBlocklist() {
+        runAsShell(permission.NETWORK_SETTINGS, permission.ACCESS_WIFI_STATE) {
+            for (cfg in wifiManager.configuredNetworks) {
+                assertTrue(wifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */))
+            }
+        }
+    }
+}
+
+private inline fun <reified T : CallbackEntry> TestableNetworkCallback.eventuallyExpect(
+    errorMsg: String,
+    crossinline predicate: (T) -> Boolean = { true }
+): T = history.poll(defaultTimeoutMs, mark) { it is T && predicate(it) }.also {
+    assertNotNull(it, errorMsg)
+} as T
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ContextUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/ContextUtils.kt
new file mode 100644
index 0000000..936b568
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ContextUtils.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("ContextUtils")
+
+package com.android.testutils
+
+import android.content.Context
+import android.os.UserHandle
+import org.mockito.AdditionalAnswers.delegatesTo
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import java.util.function.BiConsumer
+
+// Helper function so that Java doesn't have to pass a method that returns Unit
+fun mockContextAsUser(context: Context, functor: BiConsumer<Context, UserHandle>? = null) =
+    mockContextAsUser(context) { c, h -> functor?.accept(c, h) }
+
+/**
+ * Return a context with assigned user and delegate to original context.
+ *
+ * @param context the mock context to set up createContextAsUser on. After this function
+ *                is called, client code can call createContextAsUser and expect a context that
+ *                will return the correct user and userId.
+ *
+ * @param functor additional code to run on the created context-as-user instances, for example to
+ *                set up further mocks on these contexts.
+ */
+fun mockContextAsUser(context: Context, functor: ((Context, UserHandle) -> Unit)? = null) {
+    doAnswer { invocation ->
+        val asUserContext = mock(Context::class.java, delegatesTo<Context>(context))
+        val user = invocation.arguments[0] as UserHandle
+        val userId = user.identifier
+        doReturn(user).`when`(asUserContext).user
+        doReturn(userId).`when`(asUserContext).userId
+        functor?.let { it(asUserContext, user) }
+        asUserContext
+    }.`when`(context).createContextAsUser(any(UserHandle::class.java), anyInt() /* flags */)
+}
+
+/**
+ * Helper function to mock the desired system service.
+ *
+ * @param context the mock context to set up the getSystemService and getSystemServiceName.
+ * @param clazz the system service class that intents to mock.
+ * @param service the system service name that intents to mock.
+ */
+fun <T> mockService(context: Context, clazz: Class<T>, name: String, service: T) {
+    doReturn(service).`when`(context).getSystemService(name)
+    doReturn(name).`when`(context).getSystemServiceName(clazz)
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
new file mode 100644
index 0000000..1b709b2
--- /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 = 3000
+) {
+    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")
+            } 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(100, 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/DevSdkIgnoreRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
new file mode 100644
index 0000000..46229b0
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
@@ -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.testutils
+
+import android.os.Build
+import androidx.test.InstrumentationRegistry
+import com.android.modules.utils.build.UnboundedSdkLevel
+import java.util.regex.Pattern
+import org.junit.Assume.assumeTrue
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+@Deprecated("Use Build.VERSION_CODES", ReplaceWith("Build.VERSION_CODES.S_V2"))
+const val SC_V2 = Build.VERSION_CODES.S_V2
+// TODO: Remove this when Build.VERSION_CODES.VANILLA_ICE_CREAM is available in all branches
+// where this code builds
+const val VANILLA_ICE_CREAM = 35 // Bui1ld.VERSION_CODES.VANILLA_ICE_CREAM
+
+private val MAX_TARGET_SDK_ANNOTATION_RE = Pattern.compile("MaxTargetSdk([0-9]+)$")
+private val targetSdk = InstrumentationRegistry.getContext().applicationInfo.targetSdkVersion
+
+private fun isDevSdkInRange(minExclusive: String?, maxInclusive: String?): Boolean {
+    return (minExclusive == null || !isAtMost(minExclusive)) &&
+            (maxInclusive == null || isAtMost(maxInclusive))
+}
+
+private fun isAtMost(sdkVersionOrCodename: String): Boolean {
+    // UnboundedSdkLevel does not support builds < Q, and may stop supporting Q as well since it
+    // is intended for mainline modules that are now R+.
+    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+        // Assume that any codename passed as argument from current code is a more recent build than
+        // Q: this util did not exist before Q, and codenames are only used before the corresponding
+        // build is finalized. This util could list 28 older codenames to check against (as per
+        // ro.build.version.known_codenames in more recent builds), but this does not seem valuable.
+        val intVersion = sdkVersionOrCodename.toIntOrNull() ?: return true
+        return Build.VERSION.SDK_INT <= intVersion
+    }
+    return UnboundedSdkLevel.isAtMost(sdkVersionOrCodename)
+}
+
+/**
+ * Returns true if the development SDK version of the device is in the provided annotation range.
+ *
+ * If the device is not using a release SDK, the development SDK differs from
+ * [Build.VERSION.SDK_INT], and is indicated by the device codenames; see [UnboundedSdkLevel].
+ */
+fun isDevSdkInRange(
+    ignoreUpTo: DevSdkIgnoreRule.IgnoreUpTo?,
+    ignoreAfter: DevSdkIgnoreRule.IgnoreAfter?
+): Boolean {
+    val minExclusive =
+            if (ignoreUpTo?.value == 0) ignoreUpTo.codename
+            else ignoreUpTo?.value?.toString()
+    val maxInclusive =
+            if (ignoreAfter?.value == 0) ignoreAfter.codename
+            else ignoreAfter?.value?.toString()
+    return isDevSdkInRange(minExclusive, maxInclusive)
+}
+
+private fun getMaxTargetSdk(description: Description): Int? {
+    return description.annotations.firstNotNullOfOrNull {
+        MAX_TARGET_SDK_ANNOTATION_RE.matcher(it.annotationClass.simpleName).let { m ->
+            if (m.find()) m.group(1).toIntOrNull() else null
+        }
+    }
+}
+
+/**
+ * A test rule to ignore tests based on the development SDK level.
+ *
+ * If the device is not using a release SDK, the development SDK is considered to be higher than
+ * [Build.VERSION.SDK_INT].
+ *
+ * @param ignoreClassUpTo Skip all tests in the class if the device dev SDK is <= this codename or
+ *                        SDK level.
+ * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this codename or
+ *                         SDK level.
+ */
+class DevSdkIgnoreRule @JvmOverloads constructor(
+    private val ignoreClassUpTo: String? = null,
+    private val ignoreClassAfter: String? = null
+) : TestRule {
+    /**
+     * @param ignoreClassUpTo Skip all tests in the class if the device dev SDK is <= this value.
+     * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this value.
+     */
+    @JvmOverloads
+    constructor(ignoreClassUpTo: Int?, ignoreClassAfter: Int? = null) : this(
+            ignoreClassUpTo?.toString(), ignoreClassAfter?.toString())
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return IgnoreBySdkStatement(base, description)
+    }
+
+    /**
+     * Ignore the test for any development SDK that is strictly after [value].
+     *
+     * If the device is not using a release SDK, the development SDK is considered to be higher
+     * than [Build.VERSION.SDK_INT].
+     */
+    annotation class IgnoreAfter(val value: Int = 0, val codename: String = "")
+
+    /**
+     * Ignore the test for any development SDK that lower than or equal to [value].
+     *
+     * If the device is not using a release SDK, the development SDK is considered to be higher
+     * than [Build.VERSION.SDK_INT].
+     */
+    annotation class IgnoreUpTo(val value: Int = 0, val codename: String = "")
+
+    private inner class IgnoreBySdkStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            val ignoreAfter = description.getAnnotation(IgnoreAfter::class.java)
+            val ignoreUpTo = description.getAnnotation(IgnoreUpTo::class.java)
+
+            val devSdkMessage = "Skipping test for build ${Build.VERSION.CODENAME} " +
+                    "with SDK ${Build.VERSION.SDK_INT}"
+            assumeTrue(devSdkMessage, isDevSdkInRange(ignoreClassUpTo, ignoreClassAfter))
+            assumeTrue(devSdkMessage, isDevSdkInRange(ignoreUpTo, ignoreAfter))
+
+            val maxTargetSdk = getMaxTargetSdk(description)
+            if (maxTargetSdk != null) {
+                assumeTrue("Skipping test, target SDK $targetSdk greater than $maxTargetSdk",
+                        targetSdk <= maxTargetSdk)
+            }
+            base.evaluate()
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
new file mode 100644
index 0000000..d00ae52
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import 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
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import java.lang.reflect.Modifier
+import org.junit.runner.Description
+import org.junit.runner.Runner
+import org.junit.runner.manipulation.Filter
+import org.junit.runner.manipulation.Filterable
+import org.junit.runner.manipulation.NoTestsRemainException
+import org.junit.runner.manipulation.Sortable
+import org.junit.runner.manipulation.Sorter
+import org.junit.runner.notification.Failure
+import org.junit.runner.notification.RunNotifier
+import org.junit.runners.Parameterized
+import org.mockito.Mockito
+
+/**
+ * A runner that can skip tests based on the development SDK as defined in [DevSdkIgnoreRule].
+ *
+ * Generally [DevSdkIgnoreRule] should be used for that purpose (using rules is preferable over
+ * replacing the test runner), however JUnit runners inspect all methods in the test class before
+ * processing test rules. This may cause issues if the test methods are referencing classes that do
+ * not exist on the SDK of the device the test is run on.
+ *
+ * This runner inspects [IgnoreAfter] and [IgnoreUpTo] annotations on the test class, and will skip
+ * the whole class if they do not match the development SDK as defined in [DevSdkIgnoreRule].
+ * Otherwise, it will delegate to [AndroidJUnit4] to run the test as usual.
+ *
+ * This class automatically uses the Parameterized runner as its base runner when needed, so the
+ * @Parameterized.Parameters annotation and its friends can be used in tests using this runner.
+ *
+ * Example usage:
+ *
+ *     @RunWith(DevSdkIgnoreRunner::class)
+ *     @IgnoreUpTo(Build.VERSION_CODES.Q)
+ *     class MyTestClass { ... }
+ */
+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")
+    val ctx = ApplicationProvider.getApplicationContext<Context>()
+    private val restoreDefaultNetwork =
+            klass.isAnnotationPresent(RestoreDefaultNetwork::class.java) &&
+            !ctx.applicationInfo.isInstantApp()
+
+    // 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.
+    private class RunnerWrapper<T>(private val wrapped: T) :
+            Runner(), Filterable by wrapped, Sortable by wrapped
+            where T : Runner, T : Filterable, T : Sortable {
+        override fun getDescription(): Description = wrapped.description
+        override fun run(notifier: RunNotifier?) = wrapped.run(notifier)
+    }
+
+    // Annotation for test classes to indicate the test runner should monitor thread leak.
+    // 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)
+
+        if (!isDevSdkInRange(ignoreUpTo, ignoreAfter)) {
+            null
+        } else if (it.hasParameterizedMethod()) {
+            // Parameterized throws if there is no static method annotated with @Parameters, which
+            // isn't too useful. Use it if there are, otherwise use its base AndroidJUnit4 runner.
+            RunnerWrapper(Parameterized(klass))
+        } else {
+            RunnerWrapper(AndroidJUnit4(klass))
+        }
+    }
+
+    private fun <T> Class<T>.hasParameterizedMethod(): Boolean = methods.any {
+        Modifier.isStatic(it.modifiers) &&
+                it.isAnnotationPresent(Parameterized.Parameters::class.java) }
+
+    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.
+        val threadsDiff = CompareOrUpdateResult(
+                threadCountsBeforeTest.entries,
+                threadCountsAfterTest.entries
+        ) { it.key }
+        // Ignore removed threads, which typically are generated by previous tests.
+        // Because this is in the threadsDiff.updated member, for sure there is a
+        // corresponding key in threadCountsBeforeTest.
+        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.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()
+    }
+
+    private fun getAllThreadNameCounts(): Map<String, Int> {
+        // Get the counts of threads in the group per name.
+        // Filter system thread groups.
+        // Also ignore threads with 1 count, this effectively filtered out threads created by the
+        // test runner or other system components. e.g. hwuiTask*, queued-work-looper,
+        // SurfaceSyncGroupTimer, RenderThread, Time-limited test, etc.
+        return Thread.getAllStackTraces().keys
+                .filter { it.threadGroup?.name != "system" }
+                .groupingBy { it.name }.eachCount()
+                .filter { it.value != 1 }
+    }
+
+    override fun getDescription(): Description {
+        if (baseRunner == null) {
+            return Description.createSuiteDescription(klass)
+        }
+
+        return baseRunner.description.also {
+            if (shouldThreadLeakFailTest) {
+                it.addChild(leakMonitorDesc)
+            }
+            if (restoreDefaultNetwork) {
+                it.addChild(restoreDefaultNetworkDesc)
+            }
+        }
+    }
+
+    /**
+     * Get the test count before applying the [Filterable] implementation.
+     */
+    override fun testCount(): Int {
+        // When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
+        if (baseRunner == null) return 1
+
+        var testCount = baseRunner.testCount()
+        if (shouldThreadLeakFailTest) {
+            testCount += 1
+        }
+        if (restoreDefaultNetwork) {
+            testCount += 1
+        }
+        return testCount
+    }
+
+    @Throws(NoTestsRemainException::class)
+    override fun filter(filter: Filter?) {
+        baseRunner?.filter(filter) ?: throw NoTestsRemainException()
+    }
+
+    override fun sort(sorter: Sorter?) {
+        baseRunner?.sort(sorter)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
new file mode 100644
index 0000000..68248ca
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.provider.DeviceConfig
+import android.util.Log
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.FunctionalUtils.ThrowingRunnable
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+private val TAG = DeviceConfigRule::class.simpleName
+
+private const val TIMEOUT_MS = 20_000L
+
+/**
+ * A [TestRule] that helps set [DeviceConfig] for tests and clean up the test configuration
+ * automatically on teardown.
+ *
+ * The rule can also optionally retry tests when they fail following an external change of
+ * DeviceConfig before S; this typically happens because device config flags are synced while the
+ * test is running, and DisableConfigSyncTargetPreparer is only usable starting from S.
+ *
+ * @param retryCountBeforeSIfConfigChanged if > 0, when the test fails before S, check if
+ *        the configs that were set through this rule were changed, and retry the test
+ *        up to the specified number of times if yes.
+ */
+class DeviceConfigRule @JvmOverloads constructor(
+    val retryCountBeforeSIfConfigChanged: Int = 0
+) : TestRule {
+    // Maps (namespace, key) -> value
+    private val originalConfig = mutableMapOf<Pair<String, String>, String?>()
+    private val usedConfig = mutableMapOf<Pair<String, String>, String?>()
+
+    /**
+     * Actions to be run after cleanup of the config, for the current test only.
+     */
+    private val currentTestCleanupActions = mutableListOf<ThrowingRunnable>()
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return TestValidationUrlStatement(base, description)
+    }
+
+    private inner class TestValidationUrlStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            var retryCount = if (SdkLevel.isAtLeastS()) 1 else retryCountBeforeSIfConfigChanged + 1
+            while (retryCount > 0) {
+                retryCount--
+                tryTest {
+                    base.evaluate()
+                    // Can't use break/return out of a loop here because this is a tryTest lambda,
+                    // so set retryCount to exit instead
+                    retryCount = 0
+                }.catch<Throwable> { e -> // junit AssertionFailedError does not extend Exception
+                    if (retryCount == 0) throw e
+                    usedConfig.forEach { (key, value) ->
+                        val currentValue = runAsShell(READ_DEVICE_CONFIG) {
+                            DeviceConfig.getProperty(key.first, key.second)
+                        }
+                        if (currentValue != value) {
+                            Log.w(TAG, "Test failed with unexpected device config change, retrying")
+                            return@catch
+                        }
+                    }
+                    throw e
+                } cleanupStep {
+                    runAsShell(WRITE_DEVICE_CONFIG) {
+                        originalConfig.forEach { (key, value) ->
+                            DeviceConfig.setProperty(
+                                    key.first, key.second, value, false /* makeDefault */)
+                        }
+                    }
+                } cleanupStep {
+                    originalConfig.clear()
+                    usedConfig.clear()
+                } cleanup {
+                    // Fold all cleanup actions into cleanup steps of an empty tryTest, so they are
+                    // all run even if exceptions are thrown, and exceptions are reported properly.
+                    currentTestCleanupActions.fold(tryTest { }) {
+                        tryBlock, action -> tryBlock.cleanupStep { action.run() }
+                    }.cleanup {
+                        currentTestCleanupActions.clear()
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Set a configuration key/value. After the test case ends, it will be restored to the value it
+     * had when this method was first called.
+     */
+    fun setConfig(namespace: String, key: String, value: String?): String? {
+        Log.i(TAG, "Setting config \"$key\" to \"$value\"")
+        val readWritePermissions = arrayOf(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+
+        val keyPair = Pair(namespace, key)
+        val existingValue = runAsShell(*readWritePermissions) {
+            DeviceConfig.getProperty(namespace, key)
+        }
+        if (!originalConfig.containsKey(keyPair)) {
+            originalConfig[keyPair] = existingValue
+        }
+        usedConfig[keyPair] = value
+        if (existingValue == value) {
+            // Already the correct value. There may be a race if a change is already in flight,
+            // but if multiple threads update the config there is no way to fix that anyway.
+            Log.i(TAG, "\"$key\" already had value \"$value\"")
+            return value
+        }
+
+        val future = CompletableFuture<String>()
+        val listener = DeviceConfig.OnPropertiesChangedListener {
+            // The listener receives updates for any change to any key, so don't react to
+            // changes that do not affect the relevant key
+            if (!it.keyset.contains(key)) return@OnPropertiesChangedListener
+            // "null" means absent in DeviceConfig : there is no such thing as a present but
+            // null value, so the following works even if |value| is null.
+            if (it.getString(key, null) == value) {
+                future.complete(value)
+            }
+        }
+
+        return tryTest {
+            runAsShell(*readWritePermissions) {
+                DeviceConfig.addOnPropertiesChangedListener(
+                        namespace,
+                        inlineExecutor,
+                        listener)
+                DeviceConfig.setProperty(
+                        namespace,
+                        key,
+                        value,
+                        false /* makeDefault */)
+                // Don't drop the permission until the config is applied, just in case
+                future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+            }.also {
+                Log.i(TAG, "Config \"$key\" successfully set to \"$value\"")
+            }
+        } cleanup {
+            DeviceConfig.removeOnPropertiesChangedListener(listener)
+        }
+    }
+
+    private val inlineExecutor get() = Executor { r -> r.run() }
+
+    /**
+     * Add an action to be run after config cleanup when the current test case ends.
+     */
+    fun runAfterNextCleanup(action: ThrowingRunnable) {
+        currentTestCleanupActions.add(action)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
new file mode 100644
index 0000000..ce55fdc
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import android.os.VintfRuntimeInfo;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for device information.
+ */
+public class DeviceInfoUtils {
+    /**
+     * Class for a three-part kernel version number.
+     */
+    public static class KVersion {
+        public final int major;
+        public final int minor;
+        public final int sub;
+
+        public KVersion(int major, int minor, int sub) {
+            this.major = major;
+            this.minor = minor;
+            this.sub = sub;
+        }
+
+        /**
+         * Compares with other version numerically.
+         *
+         * @param  other the other version to compare
+         * @return the value 0 if this == other;
+         *         a value less than 0 if this < other and
+         *         a value greater than 0 if this > other.
+         */
+        public int compareTo(final KVersion other) {
+            int res = Integer.compare(this.major, other.major);
+            if (res == 0) {
+                res = Integer.compare(this.minor, other.minor);
+            }
+            if (res == 0) {
+                res = Integer.compare(this.sub, other.sub);
+            }
+            return res;
+        }
+
+        /**
+         * At least satisfied with the given version.
+         *
+         * @param  from the start version to compare
+         * @return return true if this version is at least satisfied with the given version.
+         *         otherwise, return false.
+         */
+        public boolean isAtLeast(final KVersion from) {
+            return compareTo(from) >= 0;
+        }
+
+        /**
+         * Falls within the given range [from, to).
+         *
+         * @param  from the start version to compare
+         * @param  to   the end version to compare
+         * @return return true if this version falls within the given range.
+         *         otherwise, return false.
+         */
+        public boolean isInRange(final KVersion from, final KVersion to) {
+            return isAtLeast(from) && !isAtLeast(to);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof KVersion)) return false;
+            KVersion that = (KVersion) o;
+            return this.major == that.major
+                    && this.minor == that.minor
+                    && this.sub == that.sub;
+        }
+    };
+
+    /**
+     * Get a two-part kernel version number (major and minor) from a given string.
+     *
+     * TODO: use class KVersion.
+     */
+    private static Pair<Integer, Integer> getMajorMinorVersion(String version) {
+        // Only gets major and minor number of the version string.
+        final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*");
+        final Matcher m = versionPattern.matcher(version);
+        if (m.matches()) {
+            final int major = Integer.parseInt(m.group(1));
+            final int minor = TextUtils.isEmpty(m.group(3)) ? 0 : Integer.parseInt(m.group(3));
+            return new Pair<>(major, minor);
+        } else {
+            return new Pair<>(0, 0);
+        }
+    }
+
+    /**
+     * Compares two version strings numerically. Compare only major and minor number of the
+     * version string. The version comparison uses #Integer.compare. Possible version
+     * 5, 5.10, 5-beta1, 4.8-RC1, 4.7.10.10 and so on.
+     *
+     * @param  s1 the first version string to compare
+     * @param  s2 the second version string to compare
+     * @return the value 0 if s1 == s2;
+     *         a value less than 0 if s1 < s2 and
+     *         a value greater than 0 if s1 > s2.
+     *
+     * TODO: use class KVersion.
+     */
+    public static int compareMajorMinorVersion(final String s1, final String s2) {
+        final Pair<Integer, Integer> v1 = getMajorMinorVersion(s1);
+        final Pair<Integer, Integer> v2 = getMajorMinorVersion(s2);
+
+        if (Objects.equals(v1.first, v2.first)) {
+            return Integer.compare(v1.second, v2.second);
+        } else {
+            return Integer.compare(v1.first, v2.first);
+        }
+    }
+
+    /**
+     * Get a three-part kernel version number (major, minor and subminor) from a given string.
+     * Any version string must at least have major and minor number. If the subminor number can't
+     * be parsed from string. Assign zero as subminor number. Invalid version is treated as
+     * version 0.0.0.
+     */
+    public static KVersion getMajorMinorSubminorVersion(final String version) {
+        // The kernel version is a three-part version number (major, minor and subminor). Get
+        // the three-part version numbers and discard the remaining stuff if any.
+        // For example:
+        //   4.19.220-g500ede0aed22-ab8272303 --> 4.19.220
+        //   5.17-rc6-g52099515ca00-ab8032400 --> 5.17.0
+        final Pattern versionPattern = Pattern.compile("^(\\d+)\\.(\\d+)(\\.(\\d+))?.*");
+        final Matcher m = versionPattern.matcher(version);
+        if (m.matches()) {
+            final int major = Integer.parseInt(m.group(1));
+            final int minor = Integer.parseInt(m.group(2));
+            final int sub = TextUtils.isEmpty(m.group(4)) ? 0 : Integer.parseInt(m.group(4));
+            return new KVersion(major, minor, sub);
+        } else {
+            return new KVersion(0, 0, 0);
+        }
+    }
+
+    /**
+     * Check if the current kernel version is at least satisfied with the given version.
+     *
+     * @param  version the start version to compare
+     * @return return true if the current version is at least satisfied with the given version.
+     *         otherwise, return false.
+     */
+    public static boolean isKernelVersionAtLeast(final String version) {
+        final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
+        final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
+        final KVersion from = DeviceInfoUtils.getMajorMinorSubminorVersion(version);
+        return current.isAtLeast(from);
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DnsAnswerProvider.kt b/staticlibs/testutils/devicetests/com/android/testutils/DnsAnswerProvider.kt
new file mode 100644
index 0000000..6a804bf
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DnsAnswerProvider.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.DnsResolver.CLASS_IN
+import com.android.net.module.util.DnsPacket
+import com.android.net.module.util.DnsPacket.ANSECTION
+import java.net.InetAddress
+import java.util.concurrent.ConcurrentHashMap
+
+const val DEFAULT_TTL_S = 5L
+
+/**
+ * Helper class to store the mapping of DNS queries.
+ *
+ * DnsAnswerProvider is built atop a ConcurrentHashMap and as such it provides the same
+ * guarantees as ConcurrentHashMap between writing and reading elements. Specifically :
+ * - Setting an answer happens-before reading the same answer.
+ * - Callers can read and write concurrently from DnsAnswerProvider and expect no
+ *   ConcurrentModificationException.
+ * Freshness of the answers depends on ordering of the threads ; if callers need a
+ * freshness guarantee, they need to provide the happens-before relationship from a
+ * write that they want to observe to the read that they need to be observed.
+ */
+class DnsAnswerProvider {
+    private val mDnsKeyToRecords = ConcurrentHashMap<String, List<DnsPacket.DnsRecord>>()
+
+    /**
+     * Get answer for the specified hostname.
+     *
+     * @param query the target hostname.
+     * @param type type of record, could be A or AAAA.
+     *
+     * @return list of [DnsPacket.DnsRecord] associated to the query. Empty if no record matches.
+     */
+    fun getAnswer(query: String, type: Int) = mDnsKeyToRecords[query]
+            .orEmpty().filter { it.nsType == type }
+
+    /** Set answer for the specified {@code query}.
+     *
+     * @param query the target hostname
+     * @param addresses [List<InetAddress>] which could be used to generate multiple A or AAAA
+     *                  RRs with the corresponding addresses.
+     */
+    fun setAnswer(query: String, hosts: List<InetAddress>) = mDnsKeyToRecords.put(query, hosts.map {
+            DnsPacket.DnsRecord.makeAOrAAAARecord(ANSECTION, query, CLASS_IN, DEFAULT_TTL_S, it)
+        })
+
+    fun clearAnswer(query: String) = mDnsKeyToRecords.remove(query)
+    fun clearAll() = mDnsKeyToRecords.clear()
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DumpTestUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DumpTestUtils.java
new file mode 100644
index 0000000..d103748
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DumpTestUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Utilities for testing output of service dumps.
+ */
+public class DumpTestUtils {
+
+    private static String dumpService(String serviceName, boolean adoptPermission, String... args)
+            throws RemoteException, InterruptedException, ErrnoException {
+        final IBinder ib = ServiceManager.getService(serviceName);
+        FileDescriptor[] pipe = Os.pipe();
+
+        // Start a thread to read the dump output, or dump might block if it fills the pipe.
+        final CountDownLatch latch = new CountDownLatch(1);
+        AtomicReference<String> output = new AtomicReference<>();
+        // Used to send exceptions back to the main thread to ensure that the test fails cleanly.
+        AtomicReference<Exception> exception = new AtomicReference<>();
+        new Thread(() -> {
+            try {
+                output.set(Streams.readFully(
+                        new InputStreamReader(new FileInputStream(pipe[0]),
+                                StandardCharsets.UTF_8)));
+                latch.countDown();
+            } catch (Exception e) {
+                exception.set(e);
+                latch.countDown();
+            }
+        }).start();
+
+        final int timeoutMs = 5_000;
+        final String what = "service '" + serviceName + "' with args: " + Arrays.toString(args);
+        try {
+            if (adoptPermission) {
+                runAsShell(android.Manifest.permission.DUMP, () -> ib.dump(pipe[1], args));
+            } else {
+                ib.dump(pipe[1], args);
+            }
+            IoUtils.closeQuietly(pipe[1]);
+            assertTrue("Dump of " + what + " timed out after " + timeoutMs + "ms",
+                    latch.await(timeoutMs, TimeUnit.MILLISECONDS));
+        } finally {
+            // Closing the fds will terminate the thread if it's blocked on read.
+            IoUtils.closeQuietly(pipe[0]);
+            if (pipe[1].valid()) IoUtils.closeQuietly(pipe[1]);
+        }
+        if (exception.get() != null) {
+            fail("Exception dumping " + what + ": " + exception.get());
+        }
+        return output.get();
+    }
+
+    /**
+     * Dumps the specified service and returns a string. Sends a dump IPC to the given service
+     * with the specified args and a pipe, then reads from the pipe in a separate thread.
+     * The current process must already have the DUMP permission.
+     *
+     * @param serviceName the service to dump.
+     * @param args the arguments to pass to the dump function.
+     * @return The dump text.
+     * @throws RemoteException dumping the service failed.
+     * @throws InterruptedException the dump timed out.
+     * @throws ErrnoException opening or closing the pipe for the dump failed.
+     */
+    public static String dumpService(String serviceName, String... args)
+            throws RemoteException, InterruptedException, ErrnoException {
+        return dumpService(serviceName, false, args);
+    }
+
+    /**
+     * Dumps the specified service and returns a string. Sends a dump IPC to the given service
+     * with the specified args and a pipe, then reads from the pipe in a separate thread.
+     * Adopts the {@code DUMP} permission via {@code adoptShellPermissionIdentity} and then releases
+     * it. This method should not be used if the caller already has the shell permission identity.
+     * TODO: when Q and R are no longer supported, use
+     * {@link android.app.UiAutomation#getAdoptedShellPermissions} to automatically acquire the
+     * shell permission if the caller does not already have it.
+     *
+     * @param serviceName the service to dump.
+     * @param args the arguments to pass to the dump function.
+     * @return The dump text.
+     * @throws RemoteException dumping the service failed.
+     * @throws InterruptedException the dump timed out.
+     * @throws ErrnoException opening or closing the pipe for the dump failed.
+     */
+    public static String dumpServiceWithShellPermission(String serviceName, String... args)
+            throws RemoteException, InterruptedException, ErrnoException {
+        return dumpService(serviceName, true, args);
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt
new file mode 100644
index 0000000..36eb795
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt
@@ -0,0 +1,44 @@
+/*
+ * 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 java.io.FileDescriptor
+
+class ExternalPacketForwarder(
+    srcFd: FileDescriptor,
+    mtu: Int,
+    dstFd: FileDescriptor,
+    forwardMap: Map<Int, Int>
+) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
+
+    /**
+     * Prepares a packet for forwarding by potentially updating the
+     * source port based on the specified port remapping rules.
+     *
+     * @param buf The packet data as a byte array.
+     * @param version The IP version of the packet (e.g., 4 for IPv4).
+     */
+    override fun remapPort(buf: ByteArray, version: Int) {
+        val transportOffset = getTransportOffset(version)
+        val intPort = getRemappedPort(buf, transportOffset)
+
+        // Copy remapped source port.
+        if (intPort != 0) {
+            setPortAt(intPort, buf, transportOffset)
+        }
+   }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
new file mode 100644
index 0000000..1f82a35
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.DnsResolver
+import android.net.InetAddresses
+import android.os.Looper
+import android.os.Handler
+import com.android.internal.annotations.GuardedBy
+import java.net.InetAddress
+import java.util.concurrent.Executor
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doAnswer
+
+const val TYPE_UNSPECIFIED = -1
+// TODO: Integrate with NetworkMonitorTest.
+class FakeDns(val mockResolver: DnsResolver) {
+    class DnsEntry(val hostname: String, val type: Int, val addresses: List<InetAddress>) {
+        fun match(host: String, type: Int) = hostname.equals(host) && type == type
+    }
+
+    @GuardedBy("answers")
+    val answers = ArrayList<DnsEntry>()
+
+    fun getAnswer(hostname: String, type: Int): DnsEntry? = synchronized(answers) {
+        return answers.firstOrNull { it.match(hostname, type) }
+    }
+
+    fun setAnswer(hostname: String, answer: Array<String>, type: Int) = synchronized(answers) {
+        val ans = DnsEntry(hostname, type, generateAnswer(answer))
+        // Replace or remove the existing one.
+        when (val index = answers.indexOfFirst { it.match(hostname, type) }) {
+            -1 -> answers.add(ans)
+            else -> answers[index] = ans
+        }
+    }
+
+    private fun generateAnswer(answer: Array<String>) =
+            answer.filterNotNull().map { InetAddresses.parseNumericAddress(it) }
+
+    fun startMocking() {
+        // Mock DnsResolver.query() w/o type
+        doAnswer {
+            mockAnswer(it, 1, -1, 3, 5)
+        }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* flags */,
+                any() /* executor */, any() /* cancellationSignal */, any() /*callback*/)
+        // Mock DnsResolver.query() w/ type
+        doAnswer {
+            mockAnswer(it, 1, 2, 4, 6)
+        }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* nsType */,
+                anyInt() /* flags */, any() /* executor */, any() /* cancellationSignal */,
+        any() /*callback*/)
+    }
+
+    private fun mockAnswer(
+        it: InvocationOnMock,
+        posHos: Int,
+        posType: Int,
+        posExecutor: Int,
+        posCallback: Int
+    ) {
+        val hostname = it.arguments[posHos] as String
+        val executor = it.arguments[posExecutor] as Executor
+        val callback = it.arguments[posCallback] as DnsResolver.Callback<List<InetAddress>>
+        var type = if (posType != -1) it.arguments[posType] as Int else TYPE_UNSPECIFIED
+        val answer = getAnswer(hostname, type)
+
+        if (answer != null && !answer.addresses.isNullOrEmpty()) {
+            Handler(Looper.getMainLooper()).post({ executor.execute({
+                    callback.onAnswer(answer.addresses, 0); }) })
+        }
+    }
+
+    /** Clears all entries. */
+    fun clearAll() = synchronized(answers) {
+        answers.clear()
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
new file mode 100644
index 0000000..f00ca11
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("HandlerUtils")
+
+package com.android.testutils
+
+import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Log
+import com.android.testutils.FunctionalUtils.ThrowingRunnable
+import com.android.testutils.FunctionalUtils.ThrowingSupplier
+import java.lang.Exception
+import java.util.concurrent.Executor
+import kotlin.test.fail
+
+private const val TAG = "HandlerUtils"
+
+/**
+ * Block until the specified Handler or HandlerThread becomes idle, or until timeoutMs has passed.
+ */
+fun HandlerThread.waitForIdle(timeoutMs: Int) = threadHandler.waitForIdle(timeoutMs.toLong())
+fun HandlerThread.waitForIdle(timeoutMs: Long) = threadHandler.waitForIdle(timeoutMs)
+fun Handler.waitForIdle(timeoutMs: Int) = waitForIdle(timeoutMs.toLong())
+fun Handler.waitForIdle(timeoutMs: Long) {
+    val cv = ConditionVariable(false)
+    post(cv::open)
+    if (!cv.block(timeoutMs)) {
+        fail("Handler did not become idle after ${timeoutMs}ms")
+    }
+}
+
+/**
+ * Block until the given Serial Executor becomes idle, or until timeoutMs has passed.
+ */
+fun waitForIdleSerialExecutor(executor: Executor, timeoutMs: Long) {
+    val cv = ConditionVariable()
+    executor.execute(cv::open)
+    if (!cv.block(timeoutMs)) {
+        fail("Executor did not become idle after ${timeoutMs}ms")
+    }
+}
+
+/**
+ * Executes a block of code that returns a value, making its side effects visible on the caller and
+ * the handler thread.
+ *
+ * After this function returns, the side effects of the passed block of code are guaranteed to be
+ * observed both on the thread running the handler and on the thread running this method.
+ * To achieve this, this method runs the passed block on the handler and blocks this thread
+ * until it's executed, so keep in mind this method will block, (including, if the handler isn't
+ * running, blocking forever).
+ */
+fun <T> visibleOnHandlerThread(handler: Handler, supplier: ThrowingSupplier<T>): T {
+    val cv = ConditionVariable()
+    var rv: Result<T> = Result.failure(RuntimeException("Not run"))
+    handler.post {
+        try {
+            rv = Result.success(supplier.get())
+        } catch (exception: Exception) {
+            Log.e(TAG, "visibleOnHandlerThread caught exception", exception)
+            rv = Result.failure(exception)
+        }
+        cv.open()
+    }
+    // After block() returns, the handler thread has seen the change (since it ran it)
+    // and this thread also has seen the change (since cv.open() happens-before cv.block()
+    // returns).
+    cv.block()
+    return rv.getOrThrow()
+}
+
+/** Overload of visibleOnHandlerThread but executes a block of code that does not return a value. */
+inline fun visibleOnHandlerThread(handler: Handler, r: ThrowingRunnable){
+    visibleOnHandlerThread(handler, ThrowingSupplier<Unit> { r.run() })
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt
new file mode 100644
index 0000000..58829dc
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt
@@ -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 com.android.testutils
+
+import java.io.FileDescriptor
+
+class InternalPacketForwarder(
+    srcFd: FileDescriptor,
+    mtu: Int,
+    dstFd: FileDescriptor,
+    forwardMap: Map<Int, Int>
+) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
+    /**
+     * Prepares a packet for forwarding by potentially updating the
+     * destination port based on the specified port remapping rules.
+     *
+     * @param buf The packet data as a byte array.
+     * @param version The IP version of the packet (e.g., 4 for IPv4).
+     */
+    override fun remapPort(buf: ByteArray, version: Int) {
+        val transportOffset = getTransportOffset(version) + DESTINATION_PORT_OFFSET
+        val extPort = getRemappedPort(buf, transportOffset)
+
+        // Copy remapped destination port.
+        if (extPort != 0) {
+            setPortAt(extPort, buf, transportOffset)
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
new file mode 100644
index 0000000..8b88224
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.testutils
+
+import android.net.DnsResolver
+import android.net.Network
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdServiceInfo
+import android.os.Process
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.net.module.util.DnsPacket
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_DST_ADDR_OFFSET
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
+import com.android.net.module.util.TrackRecord
+import com.android.testutils.IPv6UdpFilter
+import com.android.testutils.TapPacketReader
+import java.net.Inet6Address
+import java.net.InetAddress
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val MDNS_REGISTRATION_TIMEOUT_MS = 10_000L
+private const val MDNS_PORT = 5353.toShort()
+const val MDNS_CALLBACK_TIMEOUT = 2000L
+const val MDNS_NO_CALLBACK_TIMEOUT_MS = 200L
+
+interface NsdEvent
+open class NsdRecord<T : NsdEvent> private constructor(
+    private val history: ArrayTrackRecord<T>,
+    private val expectedThreadId: Int? = null
+) : TrackRecord<T> by history {
+    constructor(expectedThreadId: Int? = null) : this(ArrayTrackRecord(), expectedThreadId)
+
+    val nextEvents = history.newReadHead()
+
+    override fun add(e: T): Boolean {
+        if (expectedThreadId != null) {
+            assertEquals(
+                expectedThreadId, Process.myTid(),
+                "Callback is running on the wrong thread"
+            )
+        }
+        return history.add(e)
+    }
+
+    inline fun <reified V : NsdEvent> expectCallbackEventually(
+        timeoutMs: Long = MDNS_CALLBACK_TIMEOUT,
+        crossinline predicate: (V) -> Boolean = { true }
+    ): V = nextEvents.poll(timeoutMs) { e -> e is V && predicate(e) } as V?
+        ?: fail("Callback for ${V::class.java.simpleName} not seen after $timeoutMs ms")
+
+    inline fun <reified V : NsdEvent> expectCallback(timeoutMs: Long = MDNS_CALLBACK_TIMEOUT): V {
+        val nextEvent = nextEvents.poll(timeoutMs)
+        assertNotNull(
+            nextEvent, "No callback received after $timeoutMs ms, expected " +
+                    "${V::class.java.simpleName}"
+        )
+        assertTrue(
+            nextEvent is V, "Expected ${V::class.java.simpleName} but got " +
+                    nextEvent.javaClass.simpleName
+        )
+        return nextEvent
+    }
+
+    inline fun assertNoCallback(timeoutMs: Long = MDNS_NO_CALLBACK_TIMEOUT_MS) {
+        val cb = nextEvents.poll(timeoutMs)
+        assertNull(cb, "Expected no callback but got $cb")
+    }
+}
+
+class NsdDiscoveryRecord(expectedThreadId: Int? = null) :
+    NsdManager.DiscoveryListener, NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>(expectedThreadId) {
+    sealed class DiscoveryEvent : NsdEvent {
+        data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int) :
+            DiscoveryEvent()
+
+        data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int) :
+            DiscoveryEvent()
+
+        data class DiscoveryStarted(val serviceType: String) : DiscoveryEvent()
+        data class DiscoveryStopped(val serviceType: String) : DiscoveryEvent()
+        data class ServiceFound(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
+        data class ServiceLost(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
+    }
+
+    override fun onStartDiscoveryFailed(serviceType: String, err: Int) {
+        add(DiscoveryEvent.StartDiscoveryFailed(serviceType, err))
+    }
+
+    override fun onStopDiscoveryFailed(serviceType: String, err: Int) {
+        add(DiscoveryEvent.StopDiscoveryFailed(serviceType, err))
+    }
+
+    override fun onDiscoveryStarted(serviceType: String) {
+        add(DiscoveryEvent.DiscoveryStarted(serviceType))
+    }
+
+    override fun onDiscoveryStopped(serviceType: String) {
+        add(DiscoveryEvent.DiscoveryStopped(serviceType))
+    }
+
+    override fun onServiceFound(si: NsdServiceInfo) {
+        add(DiscoveryEvent.ServiceFound(si))
+    }
+
+    override fun onServiceLost(si: NsdServiceInfo) {
+        add(DiscoveryEvent.ServiceLost(si))
+    }
+
+    fun waitForServiceDiscovered(
+        serviceName: String,
+        serviceType: String,
+        expectedNetwork: Network? = null
+    ): NsdServiceInfo {
+        val serviceFound = expectCallbackEventually<DiscoveryEvent.ServiceFound> {
+            it.serviceInfo.serviceName == serviceName &&
+                    (expectedNetwork == null ||
+                            expectedNetwork == it.serviceInfo.network)
+        }.serviceInfo
+        // Discovered service types have a dot at the end
+        assertEquals("$serviceType.", serviceFound.serviceType)
+        return serviceFound
+    }
+}
+
+class NsdRegistrationRecord(expectedThreadId: Int? = null) : NsdManager.RegistrationListener,
+    NsdRecord<NsdRegistrationRecord.RegistrationEvent>(expectedThreadId) {
+    sealed class RegistrationEvent : NsdEvent {
+        abstract val serviceInfo: NsdServiceInfo
+
+        data class RegistrationFailed(
+            override val serviceInfo: NsdServiceInfo,
+            val errorCode: Int
+        ) : RegistrationEvent()
+
+        data class UnregistrationFailed(
+            override val serviceInfo: NsdServiceInfo,
+            val errorCode: Int
+        ) : RegistrationEvent()
+
+        data class ServiceRegistered(override val serviceInfo: NsdServiceInfo) :
+            RegistrationEvent()
+
+        data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo) :
+            RegistrationEvent()
+    }
+
+    override fun onRegistrationFailed(si: NsdServiceInfo, err: Int) {
+        add(RegistrationEvent.RegistrationFailed(si, err))
+    }
+
+    override fun onUnregistrationFailed(si: NsdServiceInfo, err: Int) {
+        add(RegistrationEvent.UnregistrationFailed(si, err))
+    }
+
+    override fun onServiceRegistered(si: NsdServiceInfo) {
+        add(RegistrationEvent.ServiceRegistered(si))
+    }
+
+    override fun onServiceUnregistered(si: NsdServiceInfo) {
+        add(RegistrationEvent.ServiceUnregistered(si))
+    }
+}
+
+class NsdResolveRecord : NsdManager.ResolveListener,
+    NsdRecord<NsdResolveRecord.ResolveEvent>() {
+    sealed class ResolveEvent : NsdEvent {
+        data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
+            ResolveEvent()
+
+        data class ServiceResolved(val serviceInfo: NsdServiceInfo) : ResolveEvent()
+        data class ResolutionStopped(val serviceInfo: NsdServiceInfo) : ResolveEvent()
+        data class StopResolutionFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
+            ResolveEvent()
+    }
+
+    override fun onResolveFailed(si: NsdServiceInfo, err: Int) {
+        add(ResolveEvent.ResolveFailed(si, err))
+    }
+
+    override fun onServiceResolved(si: NsdServiceInfo) {
+        add(ResolveEvent.ServiceResolved(si))
+    }
+
+    override fun onResolutionStopped(si: NsdServiceInfo) {
+        add(ResolveEvent.ResolutionStopped(si))
+    }
+
+    override fun onStopResolutionFailed(si: NsdServiceInfo, err: Int) {
+        super.onStopResolutionFailed(si, err)
+        add(ResolveEvent.StopResolutionFailed(si, err))
+    }
+}
+
+class NsdServiceInfoCallbackRecord : NsdManager.ServiceInfoCallback,
+    NsdRecord<NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent>() {
+    sealed class ServiceInfoCallbackEvent : NsdEvent {
+        data class RegisterCallbackFailed(val errorCode: Int) : ServiceInfoCallbackEvent()
+        data class ServiceUpdated(val serviceInfo: NsdServiceInfo) : ServiceInfoCallbackEvent()
+        object ServiceUpdatedLost : ServiceInfoCallbackEvent()
+        object UnregisterCallbackSucceeded : ServiceInfoCallbackEvent()
+    }
+
+    override fun onServiceInfoCallbackRegistrationFailed(err: Int) {
+        add(ServiceInfoCallbackEvent.RegisterCallbackFailed(err))
+    }
+
+    override fun onServiceUpdated(si: NsdServiceInfo) {
+        add(ServiceInfoCallbackEvent.ServiceUpdated(si))
+    }
+
+    override fun onServiceLost() {
+        add(ServiceInfoCallbackEvent.ServiceUpdatedLost)
+    }
+
+    override fun onServiceInfoCallbackUnregistered() {
+        add(ServiceInfoCallbackEvent.UnregisterCallbackSucceeded)
+    }
+}
+
+private fun getMdnsPayload(packet: ByteArray) = packet.copyOfRange(
+    ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, packet.size)
+
+private fun getDstAddr(packet: ByteArray): Inet6Address {
+    val v6AddrPos = ETHER_HEADER_LEN + IPV6_DST_ADDR_OFFSET
+    return Inet6Address.getByAddress(packet.copyOfRange(v6AddrPos, v6AddrPos + IPV6_ADDR_LEN))
+            as Inet6Address
+}
+
+fun TapPacketReader.pollForMdnsPacket(
+    timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS,
+    predicate: (TestDnsPacket) -> Boolean
+): TestDnsPacket? {
+    val mdnsProbeFilter = IPv6UdpFilter(srcPort = MDNS_PORT, dstPort = MDNS_PORT).and {
+        val dst = getDstAddr(it)
+        val mdnsPayload = getMdnsPayload(it)
+        try {
+            predicate(TestDnsPacket(mdnsPayload, dst))
+        } catch (e: DnsPacket.ParseException) {
+            false
+        }
+    }
+    return poll(timeoutMs, mdnsProbeFilter)?.let {
+        TestDnsPacket(getMdnsPayload(it), getDstAddr(it))
+    }
+}
+
+fun TapPacketReader.pollForProbe(
+    serviceName: String,
+    serviceType: String,
+    timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) {
+    it.isProbeFor("$serviceName.$serviceType.local")
+}
+
+fun TapPacketReader.pollForAdvertisement(
+    serviceName: String,
+    serviceType: String,
+    timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) {
+    it.isReplyFor("$serviceName.$serviceType.local")
+}
+
+fun TapPacketReader.pollForQuery(
+    recordName: String,
+    vararg requiredTypes: Int,
+    timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
+): 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
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) {
+    it.isReplyFor("$serviceName.$serviceType.local")
+}
+
+class TestDnsPacket(data: ByteArray, val dstAddr: InetAddress) : DnsPacket(data) {
+    val header: DnsHeader
+        get() = mHeader
+    val records: Array<List<DnsRecord>>
+        get() = mRecords
+    fun isProbeFor(name: String): Boolean = mRecords[QDSECTION].any {
+        it.dName == name && it.nsType == DnsResolver.TYPE_ANY
+    }
+
+    fun isReplyFor(name: String, type: Int = DnsResolver.TYPE_SRV): Boolean =
+        mRecords[ANSECTION].any {
+            it.dName == name && it.nsType == type
+        }
+
+    fun isQueryFor(name: String, vararg requiredTypes: Int): Boolean = requiredTypes.all { type ->
+        mRecords[QDSECTION].any {
+            it.dName == name && it.nsType == type
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetlinkTestUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/NetlinkTestUtils.kt
new file mode 100644
index 0000000..3f5460b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetlinkTestUtils.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("NetlinkTestUtils")
+
+package com.android.testutils
+
+import com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH
+import com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH
+import libcore.util.HexEncoding
+import libcore.util.HexEncoding.encodeToString
+import java.net.Inet6Address
+import java.net.InetAddress
+
+private const val VRRP_MAC_ADDR = "00005e000164"
+
+/**
+ * Make a RTM_NEWNEIGH netlink message.
+ */
+@JvmOverloads
+fun makeNewNeighMessage(
+    neighAddr: InetAddress,
+    nudState: Short,
+    linkLayerAddr: String = VRRP_MAC_ADDR
+) = makeNeighborMessage(
+        neighAddr = neighAddr,
+        type = RTM_NEWNEIGH,
+        nudState = nudState,
+        linkLayerAddr = linkLayerAddr
+)
+
+/**
+ * Make a RTM_DELNEIGH netlink message.
+ */
+fun makeDelNeighMessage(
+    neighAddr: InetAddress,
+    nudState: Short
+) = makeNeighborMessage(
+        neighAddr = neighAddr,
+        type = RTM_DELNEIGH,
+        nudState = nudState
+)
+
+private fun makeNeighborMessage(
+    neighAddr: InetAddress,
+    type: Short,
+    nudState: Short,
+    linkLayerAddr: String = VRRP_MAC_ADDR
+) = HexEncoding.decode(
+    /* ktlint-disable indent */
+    // -- struct nlmsghdr --
+                         // length = 88 or 76:
+    (if (neighAddr is Inet6Address) "58000000" else "4c000000") +
+    type.toLEHex() +     // type
+    "0000" +             // flags
+    "00000000" +         // seqno
+    "00000000" +         // pid (0 == kernel)
+    // struct ndmsg
+                         // family (AF_INET6 or AF_INET)
+    (if (neighAddr is Inet6Address) "0a" else "02") +
+    "00" +               // pad1
+    "0000" +             // pad2
+    "15000000" +         // interface index (21 == wlan0, on test device)
+    nudState.toLEHex() + // NUD state
+    "00" +               // flags
+    "01" +               // type
+    // -- struct nlattr: NDA_DST --
+                         // length = 20 or 8:
+    (if (neighAddr is Inet6Address) "1400" else "0800") +
+    "0100" +             // type (1 == NDA_DST, for neighbor messages)
+                         // IP address:
+    encodeToString(neighAddr.address) +
+    // -- struct nlattr: NDA_LLADDR --
+    "0a00" +             // length = 10
+    "0200" +             // type (2 == NDA_LLADDR, for neighbor messages)
+    linkLayerAddr +      // MAC Address(default == 00:00:5e:00:01:64)
+    "0000" +             // padding, for 4 byte alignment
+    // -- struct nlattr: NDA_PROBES --
+    "0800" +             // length = 8
+    "0400" +             // type (4 == NDA_PROBES, for neighbor messages)
+    "01000000" +         // number of probes
+    // -- struct nlattr: NDA_CACHEINFO --
+    "1400" +             // length = 20
+    "0300" +             // type (3 == NDA_CACHEINFO, for neighbor messages)
+    "05190000" +         // ndm_used, as "clock ticks ago"
+    "05190000" +         // ndm_confirmed, as "clock ticks ago"
+    "190d0000" +         // ndm_updated, as "clock ticks ago"
+    "00000000",          // ndm_refcnt
+    false /* allowSingleChar */)
+    /* ktlint-enable indent */
+
+/**
+ * Convert a [Short] to a little-endian hex string.
+ */
+private fun Short.toLEHex() = String.format("%04x", java.lang.Short.reverseBytes(this))
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderCbStubCompat.java b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderCbStubCompat.java
new file mode 100644
index 0000000..642da7a
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderCbStubCompat.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import android.net.NetworkStats;
+import android.net.netstats.provider.INetworkStatsProviderCallback;
+import android.os.RemoteException;
+
+/**
+ * A shim class that allows {@link TestableNetworkStatsProviderCbBinder} to be built against
+ * different SDK versions.
+ */
+public class NetworkStatsProviderCbStubCompat extends INetworkStatsProviderCallback.Stub {
+    @Override
+    public void notifyStatsUpdated(int token, NetworkStats ifaceStats, NetworkStats uidStats)
+            throws RemoteException {}
+
+    @Override
+    public void notifyAlertReached() throws RemoteException {}
+
+    /** Added in T. */
+    public void notifyLimitReached() throws RemoteException {}
+
+    /** Added in T. */
+    public void notifyWarningReached() throws RemoteException {}
+
+    /** Added in S, removed in T. */
+    public void notifyWarningOrLimitReached() throws RemoteException {}
+
+    @Override
+    public void unregister() throws RemoteException {}
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderStubCompat.java b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderStubCompat.java
new file mode 100644
index 0000000..a77aa02
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderStubCompat.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import android.net.netstats.provider.INetworkStatsProvider;
+
+/**
+ * A shim class that allows {@link TestableNetworkStatsProviderBinder} to be built against
+ * different SDK versions.
+ */
+public class NetworkStatsProviderStubCompat extends INetworkStatsProvider.Stub {
+    @Override
+    public void onRequestStatsUpdate(int token) {}
+
+    // Removed and won't be called in S+.
+    public void onSetLimit(String iface, long quotaBytes) {}
+
+    @Override
+    public void onSetAlert(long bytes) {}
+
+    // Added in S.
+    public void onSetWarningAndLimit(String iface, long warningBytes, long limitBytes) {}
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
new file mode 100644
index 0000000..8324b25
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.NetworkStats
+import kotlin.test.assertTrue
+
+@JvmOverloads
+fun orderInsensitiveEquals(
+    leftStats: NetworkStats,
+    rightStats: NetworkStats,
+    compareTime: Boolean = false
+): Boolean {
+    if (leftStats == rightStats) return true
+    if (compareTime && leftStats.getElapsedRealtime() != rightStats.getElapsedRealtime()) {
+        return false
+    }
+
+    // While operations such as add/subtract will preserve empty entries. This will make
+    // the result be hard to verify during test. Remove them before comparing since they
+    // are not really affect correctness.
+    // TODO (b/152827872): Remove empty entries after addition/subtraction.
+    val leftTrimmedEmpty = leftStats.removeEmptyEntries()
+    val rightTrimmedEmpty = rightStats.removeEmptyEntries()
+
+    if (leftTrimmedEmpty.size() != rightTrimmedEmpty.size()) return false
+    val left = NetworkStats.Entry()
+    val right = NetworkStats.Entry()
+    // Order insensitive compare.
+    for (i in 0 until leftTrimmedEmpty.size()) {
+        leftTrimmedEmpty.getValues(i, left)
+        val j: Int = rightTrimmedEmpty.findIndexHinted(left.iface, left.uid, left.set, left.tag,
+                left.metered, left.roaming, left.defaultNetwork, i)
+        if (j == -1) return false
+        rightTrimmedEmpty.getValues(j, right)
+        if (left != right) return false
+    }
+    return true
+}
+
+/**
+ * Assert that two {@link NetworkStats} are equals, assuming the order of the records are not
+ * necessarily the same.
+ *
+ * @note {@code elapsedRealtime} is not compared by default, given that in test cases that is not
+ *       usually used.
+ */
+@JvmOverloads
+fun assertNetworkStatsEquals(
+    expected: NetworkStats,
+    actual: NetworkStats,
+    compareTime: Boolean = false
+) {
+    assertTrue(orderInsensitiveEquals(expected, actual, compareTime),
+            "expected: " + expected + " but was: " + actual)
+}
+
+/**
+ * Assert that after being parceled then unparceled, {@link NetworkStats} is equal to the original
+ * object.
+ */
+fun assertParcelingIsLossless(stats: NetworkStats) {
+    assertParcelingIsLossless(stats, { a, b -> orderInsensitiveEquals(a, b) })
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NonNullTestUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/NonNullTestUtils.java
new file mode 100644
index 0000000..463c470
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NonNullTestUtils.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import android.annotation.NonNull;
+
+/**
+ * Utilities to help Kotlin test to verify java @NonNull code
+ */
+public class NonNullTestUtils {
+    /**
+     * This method allows Kotlin to pass nullable to @NonNull java code for testing.
+     * For Foo(@NonNull arg) java method, Kotlin can pass nullable variable by
+     * Foo(NonNullTestUtils.nullUnsafe(nullableVar)).
+     */
+    @NonNull public static <T> T nullUnsafe(T v) {
+        return v;
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
new file mode 100644
index 0000000..0b736d1
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.TestNetworkSpecifier
+import android.os.Binder
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import java.net.InetAddress
+import libcore.io.IoUtils
+
+/**
+ * A class that set up two {@link TestNetworkInterface}, and forward packets between them.
+ *
+ * See {@link PacketForwarder} for more detailed information.
+ */
+class PacketBridge(
+    context: Context,
+    addresses: List<LinkAddress>,
+    dnsAddr: InetAddress,
+    portMapping: List<Pair<Int, Int>>
+) {
+    private val binder = Binder()
+
+    private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+    private val tnm = context.getSystemService(TestNetworkManager::class.java)!!
+
+    // Create test networks. The needed permissions should be supplied by the callers.
+    @SuppressLint("MissingPermission")
+    private val internalIface = tnm.createTunInterface(addresses)
+    @SuppressLint("MissingPermission")
+    private val externalIface = tnm.createTunInterface(addresses)
+
+    // Register test networks to ConnectivityService.
+    private val internalNetworkCallback: TestableNetworkCallback
+    private val externalNetworkCallback: TestableNetworkCallback
+
+    private val internalForwardMap = HashMap<Int, Int>()
+    private val externalForwardMap = HashMap<Int, Int>()
+
+    val internalNetwork: Network
+    val externalNetwork: Network
+    init {
+        val (inCb, inNet) = createTestNetwork(internalIface, addresses, dnsAddr)
+        val (exCb, exNet) = createTestNetwork(externalIface, addresses, dnsAddr)
+        internalNetworkCallback = inCb
+        externalNetworkCallback = exCb
+        internalNetwork = inNet
+        externalNetwork = exNet
+        for (mapping in portMapping) {
+            internalForwardMap[mapping.first] = mapping.second
+            externalForwardMap[mapping.second] = mapping.first
+        }
+    }
+
+    // Set up the packet bridge.
+    private val internalFd = internalIface.fileDescriptor.fileDescriptor
+    private val externalFd = externalIface.fileDescriptor.fileDescriptor
+
+    private val pr1 = InternalPacketForwarder(
+        internalFd,
+        1500,
+        externalFd,
+        internalForwardMap
+    )
+    private val pr2 = ExternalPacketForwarder(
+        externalFd,
+        1500,
+        internalFd,
+        externalForwardMap
+    )
+
+    fun start() {
+        IoUtils.setBlocking(internalFd, true /* blocking */)
+        IoUtils.setBlocking(externalFd, true /* blocking */)
+        pr1.start()
+        pr2.start()
+    }
+
+    fun stop() {
+        pr1.interrupt()
+        pr2.interrupt()
+        cm.unregisterNetworkCallback(internalNetworkCallback)
+        cm.unregisterNetworkCallback(externalNetworkCallback)
+    }
+
+    /**
+     * Creates a test network with given test TUN interface and addresses.
+     */
+    private fun createTestNetwork(
+        testIface: TestNetworkInterface,
+        addresses: List<LinkAddress>,
+        dnsAddr: InetAddress
+    ): Pair<TestableNetworkCallback, Network> {
+        // Make a network request to hold the test network
+        val nr = NetworkRequest.Builder()
+            .clearCapabilities()
+            .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+            .setNetworkSpecifier(TestNetworkSpecifier(testIface.interfaceName))
+            .build()
+        val testCb = TestableNetworkCallback()
+        cm.requestNetwork(nr, testCb)
+
+        val lp = LinkProperties().apply {
+            setLinkAddresses(addresses)
+            interfaceName = testIface.interfaceName
+            addDnsServer(dnsAddr)
+        }
+        tnm.setupTestNetwork(lp, true /* isMetered */, binder)
+
+        // Wait for available before return.
+        val network = testCb.expect<Available>().network
+        return testCb to network
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
new file mode 100644
index 0000000..5c79eb0
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import static com.android.testutils.PacketReflector.IPPROTO_TCP;
+import static com.android.testutils.PacketReflector.IPPROTO_UDP;
+import static com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_PROTO_OFFSET;
+import static com.android.testutils.PacketReflector.TCP_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.UDP_HEADER_LENGTH;
+
+import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A class that forwards packets from a {@link TestNetworkInterface} to another
+ * {@link TestNetworkInterface}.
+ *
+ * For testing purposes, a {@link TestNetworkInterface} provides a {@link FileDescriptor}
+ * which allows content injection on the test network. However, this could be hard to use
+ * because the callers need to compose IP packets in order to inject content to the
+ * test network.
+ *
+ * In order to remove the need of composing the IP packets, this class forwards IP packets to
+ * the {@link FileDescriptor} of another {@link TestNetworkInterface} instance. Thus,
+ * the TCP/IP headers could be parsed/composed automatically by the protocol stack of this
+ * additional {@link TestNetworkInterface}, while the payload is supplied by the
+ * servers run on the interface.
+ *
+ * To make it work, an internal interface and an external interface are defined, where
+ * the client might send packets from the internal interface which are originated from
+ * multiple addresses to a server that listens on the different port.
+ *
+ * For the incoming packet received from external interface, for example a http response sent
+ * from the http server, the same mechanism is applied but in a different direction,
+ * where the source and destination will be swapped.
+ */
+public abstract class PacketForwarderBase extends Thread {
+    private static final String TAG = "PacketForwarder";
+    static final int DESTINATION_PORT_OFFSET = 2;
+
+    // The source fd to read packets from.
+    @NonNull
+    final FileDescriptor mSrcFd;
+    // The buffer to temporarily hold the entire packet after receiving.
+    @NonNull
+    final byte[] mBuf;
+    // The destination fd to write packets to.
+    @NonNull
+    final FileDescriptor mDstFd;
+
+    @NonNull
+    final Map<Integer, Integer> mPortRemapRules;
+    /**
+     * Construct a {@link PacketForwarderBase}.
+     *
+     * This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
+     * forwards them to the {@code dstFd} of another {@link TestNetworkInterface}.
+     *
+     * Note that this class is not useful if the instance is not managed by a
+     * {@link PacketBridge} to set up a two-way communication.
+     *
+     * @param srcFd   {@link FileDescriptor} to read packets from.
+     * @param mtu     MTU of the test network.
+     * @param dstFd   {@link FileDescriptor} to write packets to.
+     * @param portRemapRules    port remap rules
+     */
+    public PacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
+                           @NonNull FileDescriptor dstFd,
+                           @NonNull Map<Integer, Integer> portRemapRules) {
+        super(TAG);
+        mSrcFd = Objects.requireNonNull(srcFd);
+        mBuf = new byte[mtu];
+        mDstFd = Objects.requireNonNull(dstFd);
+        mPortRemapRules = Objects.requireNonNull(portRemapRules);
+    }
+
+    /**
+     * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
+     * which includes ports mapping.
+     * Subclasses should override this method to implement the needed port remapping.
+     * For internal forwarder will remapped destination port,
+     * external forwarder will remapped source port.
+     * Example:
+     * An outgoing packet from the internal interface with
+     * source 1.2.3.4:1234 and destination 8.8.8.8:80
+     * might be translated to 8.8.8.8:1234 -> 1.2.3.4:8080 before forwarding.
+     * An outgoing packet from the external interface with
+     * source 1.2.3.4:8080 and destination 8.8.8.8:1234
+     * might be translated to 8.8.8.8:80 -> 1.2.3.4:1234 before forwarding.
+     */
+    abstract void remapPort(@NonNull byte[] buf, int version);
+
+    /**
+     * Retrieves a potentially remapped port number from a packet.
+     *
+     * @param buf            The packet data as a byte array.
+     * @param transportOffset The offset within the packet where the transport layer port begins.
+     * @return The remapped port if a mapping exists in the internal forwarding map,
+     *         otherwise returns 0 (indicating no remapping).
+     */
+    int getRemappedPort(@NonNull byte[] buf, int transportOffset) {
+        int port = PacketReflectorUtil.getPortAt(buf, transportOffset);
+        return mPortRemapRules.getOrDefault(port, 0);
+    }
+
+    int getTransportOffset(int version) {
+        return version == 4 ? IPV4_HEADER_LENGTH : IPV6_HEADER_LENGTH;
+    }
+
+    private void forwardPacket(@NonNull byte[] buf, int len) {
+        try {
+            Os.write(mDstFd, buf, 0, len);
+        } catch (ErrnoException | IOException e) {
+            Log.e(TAG, "Error writing packet: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Reads one packet from mSrcFd, and writes the packet to the mDestFd for supported protocols.
+     * This includes:
+     * 1.Address Swapping: Swaps source and destination IP addresses.
+     * 2.Port Remapping: Remap port if necessary.
+     * 3.Checksum Recalculation: Updates IP and transport layer checksums to reflect changes.
+     */
+    private void processPacket() {
+        final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
+        if (len < 1) {
+            // Usually happens when socket read is being interrupted, e.g. stopping PacketForwarder.
+            return;
+        }
+
+        final int version = mBuf[0] >>> 4;
+        final int protoPos, ipHdrLen;
+        switch (version) {
+            case 4:
+                ipHdrLen = IPV4_HEADER_LENGTH;
+                protoPos = PacketReflector.IPV4_PROTO_OFFSET;
+                break;
+            case 6:
+                ipHdrLen = IPV6_HEADER_LENGTH;
+                protoPos = IPV6_PROTO_OFFSET;
+                break;
+            default:
+                throw new IllegalStateException("Unexpected version: " + version);
+        }
+        if (len < ipHdrLen) {
+            throw new IllegalStateException("Unexpected buffer length: " + len);
+        }
+
+        final byte proto = mBuf[protoPos];
+        final int transportHdrLen;
+        switch (proto) {
+            case IPPROTO_TCP:
+                transportHdrLen = TCP_HEADER_LENGTH;
+                break;
+            case IPPROTO_UDP:
+                transportHdrLen = UDP_HEADER_LENGTH;
+                break;
+            // TODO: Support ICMP.
+            default:
+                return; // Unknown protocol, ignored.
+        }
+
+        if (len < ipHdrLen + transportHdrLen) {
+            throw new IllegalStateException("Unexpected buffer length: " + len);
+        }
+
+        // Swap source and destination address.
+        PacketReflectorUtil.swapAddresses(mBuf, version);
+
+        // Remapping the port.
+        remapPort(mBuf, version);
+
+        // Fix IP and Transport layer checksum.
+        PacketReflectorUtil.fixPacketChecksum(mBuf, len, version, proto);
+
+        // Send the packet to the destination fd.
+        forwardPacket(mBuf, len);
+    }
+    @Override
+    public void run() {
+        Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+        while (!interrupted() && mSrcFd.valid()) {
+            processPacket();
+        }
+        Log.i(TAG, "exiting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
new file mode 100644
index 0000000..ce20d67
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import static android.system.OsConstants.ICMP6_ECHO_REPLY;
+import static android.system.OsConstants.ICMP6_ECHO_REQUEST;
+
+import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * A class that echoes packets received on a {@link TestNetworkInterface} back to itself.
+ *
+ * For testing purposes, sometimes a mocked environment to simulate a simple echo from the
+ * server side is needed. This is particularly useful if the test, e.g. VpnTest, is
+ * heavily relying on the outside world.
+ *
+ * This class reads packets from the {@link FileDescriptor} of a {@link TestNetworkInterface}, and:
+ *   1. For TCP and UDP packets, simply swaps the source address and the destination
+ *      address, then send it back to the {@link FileDescriptor}.
+ *   2. For ICMP ping packets, composes a ping reply and sends it back to the sender.
+ *   3. Ignore all other packets.
+ */
+public class PacketReflector extends Thread {
+
+    static final int IPV4_HEADER_LENGTH = 20;
+    static final int IPV6_HEADER_LENGTH = 40;
+
+    static final int IPV4_ADDR_OFFSET = 12;
+    static final int IPV6_ADDR_OFFSET = 8;
+    static final int IPV4_ADDR_LENGTH = 4;
+    static final int IPV6_ADDR_LENGTH = 16;
+
+    static final int IPV4_PROTO_OFFSET = 9;
+    static final int IPV6_PROTO_OFFSET = 6;
+
+    static final byte IPPROTO_ICMP = 1;
+    static final byte IPPROTO_TCP = 6;
+    static final byte IPPROTO_UDP = 17;
+    private static final byte IPPROTO_ICMPV6 = 58;
+
+    private static final int ICMP_HEADER_LENGTH = 8;
+    static final int TCP_HEADER_LENGTH = 20;
+    static final int UDP_HEADER_LENGTH = 8;
+
+    private static final byte ICMP_ECHO = 8;
+    private static final byte ICMP_ECHOREPLY = 0;
+
+    private static String TAG = "PacketReflector";
+
+    @NonNull
+    private final FileDescriptor mFd;
+    @NonNull
+    private final byte[] mBuf;
+
+    /**
+     * Construct a {@link PacketReflector} from the given {@code fd} of
+     * a {@link TestNetworkInterface}.
+     *
+     * @param fd {@link FileDescriptor} to read/write packets.
+     * @param mtu MTU of the test network.
+     */
+    public PacketReflector(@NonNull FileDescriptor fd, int mtu) {
+        super("PacketReflector");
+        mFd = Objects.requireNonNull(fd);
+        mBuf = new byte[mtu];
+    }
+
+    // Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
+    // This is used by the test to "connect to itself" through the VPN.
+    private void processTcpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+        if (len < hdrLen + TCP_HEADER_LENGTH) {
+            return;
+        }
+
+        // Swap src and dst IP addresses.
+        PacketReflectorUtil.swapAddresses(buf, version);
+
+        // Send the packet back.
+        writePacket(buf, len);
+    }
+
+    // Echo UDP packets: swap source and destination addresses, and source and destination ports.
+    // This is used by the test to check that the bytes it sends are echoed back.
+    private void processUdpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+        if (len < hdrLen + UDP_HEADER_LENGTH) {
+            return;
+        }
+
+        // Swap src and dst IP addresses.
+        PacketReflectorUtil.swapAddresses(buf, version);
+
+        // Swap dst and src ports.
+        int portOffset = hdrLen;
+        PacketReflectorUtil.swapBytes(buf, portOffset, portOffset + 2, 2);
+
+        // Send the packet back.
+        writePacket(buf, len);
+    }
+
+    private void processIcmpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+        if (len < hdrLen + ICMP_HEADER_LENGTH) {
+            return;
+        }
+
+        byte type = buf[hdrLen];
+        if (!(version == 4 && type == ICMP_ECHO) &&
+                !(version == 6 && type == (byte) ICMP6_ECHO_REQUEST)) {
+            return;
+        }
+
+        // Save the ping packet we received.
+        byte[] request = buf.clone();
+
+        // Swap src and dst IP addresses, and send the packet back.
+        // This effectively pings the device to see if it replies.
+        PacketReflectorUtil.swapAddresses(buf, version);
+        writePacket(buf, len);
+
+        // The device should have replied, and buf should now contain a ping response.
+        int received = PacketReflectorUtil.readPacket(mFd, buf);
+        if (received != len) {
+            Log.i(TAG, "Reflecting ping did not result in ping response: " +
+                    "read=" + received + " expected=" + len);
+            return;
+        }
+
+        byte replyType = buf[hdrLen];
+        if ((type == ICMP_ECHO && replyType != ICMP_ECHOREPLY)
+                || (type == (byte) ICMP6_ECHO_REQUEST && replyType != (byte) ICMP6_ECHO_REPLY)) {
+            Log.i(TAG, "Received unexpected ICMP reply: original " + type
+                    + ", reply " + replyType);
+            return;
+        }
+
+        // Compare the response we got with the original packet.
+        // The only thing that should have changed are addresses, type and checksum.
+        // Overwrite them with the received bytes and see if the packet is otherwise identical.
+        request[hdrLen] = buf[hdrLen];          // Type
+        request[hdrLen + 2] = buf[hdrLen + 2];  // Checksum byte 1.
+        request[hdrLen + 3] = buf[hdrLen + 3];  // Checksum byte 2.
+
+        // Since Linux kernel 4.2, net.ipv6.auto_flowlabels is set by default, and therefore
+        // the request and reply may have different IPv6 flow label: ignore that as well.
+        if (version == 6) {
+            request[1] = (byte) (request[1] & 0xf0 | buf[1] & 0x0f);
+            request[2] = buf[2];
+            request[3] = buf[3];
+        }
+
+        for (int i = 0; i < len; i++) {
+            if (buf[i] != request[i]) {
+                Log.i(TAG, "Received non-matching packet when expecting ping response.");
+                return;
+            }
+        }
+
+        // Now swap the addresses again and reflect the packet. This sends a ping reply.
+        PacketReflectorUtil.swapAddresses(buf, version);
+        writePacket(buf, len);
+    }
+
+    private void writePacket(@NonNull byte[] buf, int len) {
+        try {
+            Os.write(mFd, buf, 0, len);
+        } catch (ErrnoException | IOException e) {
+            Log.e(TAG, "Error writing packet: " + e.getMessage());
+        }
+    }
+
+    // Reads one packet from our mFd, and possibly writes the packet back.
+    private void processPacket() {
+        int len = PacketReflectorUtil.readPacket(mFd, mBuf);
+        if (len < 1) {
+            // Usually happens when socket read is being interrupted, e.g. stopping PacketReflector.
+            return;
+        }
+
+        int version = mBuf[0] >> 4;
+        int protoPos, hdrLen;
+        if (version == 4) {
+            hdrLen = IPV4_HEADER_LENGTH;
+            protoPos = IPV4_PROTO_OFFSET;
+        } else if (version == 6) {
+            hdrLen = IPV6_HEADER_LENGTH;
+            protoPos = IPV6_PROTO_OFFSET;
+        } else {
+            throw new IllegalStateException("Unexpected version: " + version);
+        }
+
+        if (len < hdrLen) {
+            throw new IllegalStateException("Unexpected buffer length: " + len);
+        }
+
+        byte proto = mBuf[protoPos];
+        switch (proto) {
+            case IPPROTO_ICMP:
+                // fall through
+            case IPPROTO_ICMPV6:
+                processIcmpPacket(mBuf, version, len, hdrLen);
+                break;
+            case IPPROTO_TCP:
+                processTcpPacket(mBuf, version, len, hdrLen);
+                break;
+            case IPPROTO_UDP:
+                processUdpPacket(mBuf, version, len, hdrLen);
+                break;
+        }
+    }
+
+    public void run() {
+        Log.i(TAG, "starting fd=" + mFd + " valid=" + mFd.valid());
+        while (!interrupted() && mFd.valid()) {
+            processPacket();
+        }
+        Log.i(TAG, "exiting fd=" + mFd + " valid=" + mFd.valid());
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
new file mode 100644
index 0000000..ad259c5
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("PacketReflectorUtil")
+
+package com.android.testutils
+
+import android.system.ErrnoException
+import android.system.Os
+import android.system.OsConstants
+import com.android.net.module.util.IpUtils
+import com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH
+import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
+import java.io.FileDescriptor
+import java.io.InterruptedIOException
+import java.net.InetAddress
+import java.nio.ByteBuffer
+
+fun readPacket(fd: FileDescriptor, buf: ByteArray): Int {
+    return try {
+        Os.read(fd, buf, 0, buf.size)
+    } catch (e: ErrnoException) {
+        // Ignore normal use cases such as the EAGAIN error indicates that the read operation
+        // cannot be completed immediately, or the EINTR error indicates that the read
+        // operation was interrupted by a signal.
+        if (e.errno == OsConstants.EAGAIN || e.errno == OsConstants.EINTR) {
+            -1
+        } else {
+            throw e
+        }
+    } catch (e: InterruptedIOException) {
+        -1
+    }
+}
+
+fun getInetAddressAt(buf: ByteArray, pos: Int, len: Int): InetAddress =
+    InetAddress.getByAddress(buf.copyOfRange(pos, pos + len))
+
+/**
+ * Reads a 16-bit unsigned int at pos in big endian, with no alignment requirements.
+ */
+fun getPortAt(buf: ByteArray, pos: Int): Int {
+    return (buf[pos].toInt() and 0xff shl 8) + (buf[pos + 1].toInt() and 0xff)
+}
+
+fun setPortAt(port: Int, buf: ByteArray, pos: Int) {
+    buf[pos] = (port ushr 8).toByte()
+    buf[pos + 1] = (port and 0xff).toByte()
+}
+
+fun getAddressPositionAndLength(version: Int) = when (version) {
+    4 -> PacketReflector.IPV4_ADDR_OFFSET to PacketReflector.IPV4_ADDR_LENGTH
+    6 -> PacketReflector.IPV6_ADDR_OFFSET to PacketReflector.IPV6_ADDR_LENGTH
+    else -> throw IllegalArgumentException("Unknown IP version $version")
+}
+
+private const val IPV4_CHKSUM_OFFSET = 10
+private const val UDP_CHECKSUM_OFFSET = 6
+private const val TCP_CHECKSUM_OFFSET = 16
+
+fun fixPacketChecksum(buf: ByteArray, len: Int, version: Int, protocol: Byte) {
+    // Fill Ip checksum for IPv4. IPv6 header doesn't have a checksum field.
+    if (version == 4) {
+        val checksum = IpUtils.ipChecksum(ByteBuffer.wrap(buf), 0)
+        // Place checksum in Big-endian order.
+        buf[IPV4_CHKSUM_OFFSET] = (checksum.toInt() ushr 8).toByte()
+        buf[IPV4_CHKSUM_OFFSET + 1] = (checksum.toInt() and 0xff).toByte()
+    }
+
+    // Fill transport layer checksum.
+    val transportOffset = if (version == 4) IPV4_HEADER_LENGTH else IPV6_HEADER_LENGTH
+    when (protocol) {
+        PacketReflector.IPPROTO_UDP -> {
+            val checksumPos = transportOffset + UDP_CHECKSUM_OFFSET
+            // Clear before calculate.
+            buf[checksumPos + 1] = 0x00
+            buf[checksumPos] = buf[checksumPos + 1]
+            val checksum = IpUtils.udpChecksum(
+                ByteBuffer.wrap(buf), 0,
+                transportOffset
+            )
+            buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+            buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+        }
+        PacketReflector.IPPROTO_TCP -> {
+            val checksumPos = transportOffset + TCP_CHECKSUM_OFFSET
+            // Clear before calculate.
+            buf[checksumPos + 1] = 0x00
+            buf[checksumPos] = buf[checksumPos + 1]
+            val transportLen: Int = len - transportOffset
+            val checksum = IpUtils.tcpChecksum(
+                ByteBuffer.wrap(buf), 0, transportOffset,
+                transportLen
+            )
+            buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+            buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+        }
+        // TODO: Support ICMP.
+        else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
+    }
+}
+
+fun swapBytes(buf: ByteArray, pos1: Int, pos2: Int, len: Int) {
+    for (i in 0 until len) {
+        val b = buf[pos1 + i]
+        buf[pos1 + i] = buf[pos2 + i]
+        buf[pos2 + i] = b
+    }
+}
+
+fun swapAddresses(buf: ByteArray, version: Int) {
+    val addrPos: Int
+    val addrLen: Int
+    when (version) {
+        4 -> {
+            addrPos = PacketReflector.IPV4_ADDR_OFFSET
+            addrLen = PacketReflector.IPV4_ADDR_LENGTH
+        }
+        6 -> {
+            addrPos = PacketReflector.IPV6_ADDR_OFFSET
+            addrLen = PacketReflector.IPV6_ADDR_LENGTH
+        }
+        else -> throw java.lang.IllegalArgumentException()
+    }
+    swapBytes(buf, addrPos, addrPos + addrLen, addrLen)
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
new file mode 100644
index 0000000..964c6c6
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import java.util.function.Predicate
+
+private const val POLL_FREQUENCY_MS = 1000L
+
+/**
+ * A class that can be used to reply to packets from a [TapPacketReader].
+ *
+ * A reply thread will be created to reply to incoming packets asynchronously.
+ * The receiver creates a new read head on the [TapPacketReader], to read packets, so it does not
+ * affect packets obtained through [TapPacketReader.popPacket].
+ *
+ * @param reader a [TapPacketReader] to obtain incoming packets and reply to them.
+ * @param packetFilter A filter to apply to incoming packets.
+ * @param name Name to use for the internal responder thread.
+ */
+abstract class PacketResponder(
+    private val reader: TapPacketReader,
+    private val packetFilter: Predicate<ByteArray>,
+    name: String
+) {
+    private val replyThread = ReplyThread(name)
+
+    protected abstract fun replyToPacket(packet: ByteArray, reader: TapPacketReader)
+
+    /**
+     * Start the [PacketResponder].
+     */
+    fun start() {
+        replyThread.start()
+    }
+
+    /**
+     * Stop the [PacketResponder].
+     *
+     * The responder cannot be used anymore after being stopped.
+     */
+    fun stop() {
+        replyThread.interrupt()
+        replyThread.join()
+    }
+
+    private inner class ReplyThread(name: String) : Thread(name) {
+        override fun run() {
+            try {
+                // Create a new ReadHead so other packets polled on the reader are not affected
+                val recvPackets = reader.receivedPackets.newReadHead()
+                while (!isInterrupted) {
+                    recvPackets.poll(POLL_FREQUENCY_MS, packetFilter::test)?.let {
+                        replyToPacket(it, reader)
+                    }
+                }
+            } catch (e: InterruptedException) {
+                // Exit gracefully
+            }
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ParcelUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/ParcelUtils.kt
new file mode 100644
index 0000000..14ed8e9
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ParcelUtils.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("ParcelUtils")
+
+package com.android.testutils
+
+import android.os.Parcel
+import android.os.Parcelable
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+/**
+ * Return a new instance of `T` after being parceled then unparceled.
+ */
+fun <T : Parcelable> parcelingRoundTrip(source: T): T {
+    val creator: Parcelable.Creator<T>
+    try {
+        creator = source.javaClass.getField("CREATOR").get(null) as Parcelable.Creator<T>
+    } catch (e: IllegalAccessException) {
+        fail("Missing CREATOR field: " + e.message)
+    } catch (e: NoSuchFieldException) {
+        fail("Missing CREATOR field: " + e.message)
+    }
+
+    var p = Parcel.obtain()
+    source.writeToParcel(p, /* flags */ 0)
+    p.setDataPosition(0)
+    val marshalled = p.marshall()
+    p = Parcel.obtain()
+    p.unmarshall(marshalled, 0, marshalled.size)
+    p.setDataPosition(0)
+    return creator.createFromParcel(p)
+}
+
+/**
+ * Assert that after being parceled then unparceled, `source` is equal to the original
+ * object. If a customized equals function is provided, uses the provided one.
+ */
+@JvmOverloads
+fun <T : Parcelable> assertParcelingIsLossless(
+    source: T,
+    equals: (T, T) -> Boolean = { a, b -> a == b }
+) {
+    val actual = parcelingRoundTrip(source)
+    assertTrue(equals(source, actual), "Expected $source, but was $actual")
+}
+
+@JvmOverloads
+fun <T : Parcelable> assertParcelSane(
+    obj: T,
+    fieldCount: Int,
+    equals: (T, T) -> Boolean = { a, b -> a == b }
+) {
+    assertFieldCountEquals(fieldCount, obj::class.java)
+    assertParcelingIsLossless(obj, equals)
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java b/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java
new file mode 100644
index 0000000..51d57bc
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.NsHeader;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RdnssOption;
+
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+
+/**
+ * ND (RA & NA) responder class useful for tests that require a provisioned IPv6 interface.
+ * TODO: rename to NdResponder
+ */
+public class RouterAdvertisementResponder extends PacketResponder {
+    private static final String TAG = "RouterAdvertisementResponder";
+    private static final Inet6Address DNS_SERVER =
+            (Inet6Address) InetAddresses.parseNumericAddress("2001:4860:4860::64");
+    private final TapPacketReader mPacketReader;
+    // Maps IPv6 address to MacAddress and isRouter boolean.
+    private final Map<Inet6Address, Pair<MacAddress, Boolean>> mNeighborMap = new ArrayMap<>();
+    private final IpPrefix mPrefix;
+
+    public RouterAdvertisementResponder(TapPacketReader packetReader, IpPrefix prefix) {
+        super(packetReader, RouterAdvertisementResponder::isRsOrNs, TAG);
+        mPacketReader = packetReader;
+        mPrefix = Objects.requireNonNull(prefix);
+    }
+
+    public RouterAdvertisementResponder(TapPacketReader packetReader) {
+        this(packetReader, makeRandomPrefix());
+    }
+
+    private static IpPrefix makeRandomPrefix() {
+        final byte[] prefixBytes = new IpPrefix("2001:db8::/64").getAddress().getAddress();
+        final Random r = new Random();
+        for (int i = 4; i < 8; i++) {
+            prefixBytes[i] = (byte) r.nextInt();
+        }
+        return new IpPrefix(prefixBytes, 64);
+    }
+
+    /** Returns true if the packet is a router solicitation or neighbor solicitation message. */
+    private static boolean isRsOrNs(byte[] packet) {
+        final ByteBuffer buffer = ByteBuffer.wrap(packet);
+        final EthernetHeader ethHeader = Struct.parse(EthernetHeader.class, buffer);
+        if (ethHeader.etherType != ETHER_TYPE_IPV6) {
+            return false;
+        }
+        final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buffer);
+        if (ipv6Header.nextHeader != IPPROTO_ICMPV6) {
+            return false;
+        }
+        final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buffer);
+        return icmpv6Header.type == ICMPV6_ROUTER_SOLICITATION
+            || icmpv6Header.type == ICMPV6_NEIGHBOR_SOLICITATION;
+    }
+
+    /**
+     * Adds a new router to be advertised.
+     * @param mac the mac address of the router.
+     * @param ip the link-local address of the router.
+     */
+    public void addRouterEntry(MacAddress mac, Inet6Address ip) {
+        mNeighborMap.put(ip, new Pair<>(mac, true));
+    }
+
+    /**
+     * Adds a new neighbor to be advertised.
+     * @param mac the mac address of the neighbor.
+     * @param ip the link-local address of the neighbor.
+     */
+    public void addNeighborEntry(MacAddress mac, Inet6Address ip) {
+        mNeighborMap.put(ip, new Pair<>(mac, false));
+    }
+
+    /**
+     * @return the prefix that is announced in the Router Advertisements sent by this object.
+     */
+    public IpPrefix getPrefix() {
+        return mPrefix;
+    }
+
+    private ByteBuffer buildPrefixOption() {
+        return PrefixInformationOption.build(
+                mPrefix, (byte) (PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS),
+                3600 /* valid lifetime */, 3600 /* preferred lifetime */);
+    }
+
+    private ByteBuffer buildRdnssOption() {
+        return RdnssOption.build(3600/*lifetime, must be at least 120*/, DNS_SERVER);
+    }
+
+    private ByteBuffer buildSllaOption(MacAddress srcMac) {
+        return LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
+    }
+
+    private ByteBuffer buildRaPacket(MacAddress srcMac, MacAddress dstMac, Inet6Address srcIp) {
+        return Ipv6Utils.buildRaPacket(srcMac, dstMac, srcIp, IPV6_ADDR_ALL_NODES_MULTICAST,
+                (byte) 0 /*M=0, O=0*/, 3600 /*lifetime*/, 0 /*reachableTime, unspecified*/,
+                0/*retransTimer, unspecified*/, buildPrefixOption(), buildRdnssOption(),
+                buildSllaOption(srcMac));
+    }
+
+    private static void sendResponse(TapPacketReader reader, ByteBuffer buffer) {
+        try {
+            reader.sendResponse(buffer);
+        } catch (IOException e) {
+            // Throwing an exception here will crash the test process. Let's stick to logging, as
+            // the test will fail either way.
+            Log.e(TAG, "Failed to send buffer", e);
+        }
+    }
+
+    private void replyToRouterSolicitation(TapPacketReader reader, MacAddress dstMac) {
+        for (Map.Entry<Inet6Address, Pair<MacAddress, Boolean>> it : mNeighborMap.entrySet()) {
+            final boolean isRouter = it.getValue().second;
+            if (!isRouter) {
+                continue;
+            }
+            final ByteBuffer raResponse = buildRaPacket(it.getValue().first, dstMac, it.getKey());
+            sendResponse(reader, raResponse);
+        }
+    }
+
+    private void replyToNeighborSolicitation(TapPacketReader reader, MacAddress dstMac,
+            Inet6Address dstIp, Inet6Address targetIp) {
+        final Pair<MacAddress, Boolean> neighbor = mNeighborMap.get(targetIp);
+        if (neighbor == null) {
+            return;
+        }
+
+        final MacAddress srcMac = neighbor.first;
+        final boolean isRouter = neighbor.second;
+        int flags = NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED | NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+        if (isRouter) {
+            flags |= NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER;
+        }
+
+        final ByteBuffer tlla = LlaOption.build((byte) ICMPV6_ND_OPTION_TLLA, srcMac);
+        final ByteBuffer naResponse = Ipv6Utils.buildNaPacket(srcMac, dstMac, targetIp, dstIp,
+                flags, targetIp, tlla);
+        sendResponse(reader, naResponse);
+    }
+
+    @Override
+    protected void replyToPacket(byte[] packet, TapPacketReader reader) {
+        final ByteBuffer buf = ByteBuffer.wrap(packet);
+        // Messages are filtered by parent class, so it is safe to assume that packet is either an
+        // RS or NS.
+        final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+        final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+        final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
+
+        if (icmpv6Header.type == ICMPV6_ROUTER_SOLICITATION) {
+            replyToRouterSolicitation(reader, ethHdr.srcMac);
+        } else if (icmpv6Header.type == ICMPV6_NEIGHBOR_SOLICITATION) {
+            final NsHeader nsHeader = Struct.parse(NsHeader.class, buf);
+            replyToNeighborSolicitation(reader, ethHdr.srcMac, ipv6Hdr.srcIp, nsHeader.target);
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/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/TapPacketReader.java b/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReader.java
new file mode 100644
index 0000000..b25b9f2
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReader.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.ArrayTrackRecord;
+import com.android.net.module.util.PacketReader;
+
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.function.Predicate;
+
+import kotlin.Lazy;
+import kotlin.LazyKt;
+
+/**
+ * A packet reader that runs on a TAP interface.
+ *
+ * It also implements facilities to reply to received packets.
+ */
+public class TapPacketReader extends PacketReader {
+    private final FileDescriptor mTapFd;
+    private final ArrayTrackRecord<byte[]> mReceivedPackets = new ArrayTrackRecord<>();
+    private final Lazy<ArrayTrackRecord<byte[]>.ReadHead> mReadHead =
+            LazyKt.lazy(mReceivedPackets::newReadHead);
+
+    public TapPacketReader(Handler h, FileDescriptor tapFd, int maxPacketSize) {
+        super(h, maxPacketSize);
+        mTapFd = tapFd;
+    }
+
+
+    /**
+     * Attempt to start the FdEventsReader on its handler thread.
+     *
+     * As opposed to {@link android.net.util.FdEventsReader#start()}, this method will not report
+     * failure to start, so it is only appropriate in tests that will fail later if that happens.
+     */
+    public void startAsyncForTest() {
+        getHandler().post(this::start);
+    }
+
+    @Override
+    protected FileDescriptor createFd() {
+        return mTapFd;
+    }
+
+    @Override
+    protected void handlePacket(byte[] recvbuf, int length) {
+        final byte[] newPacket = Arrays.copyOf(recvbuf, length);
+        if (!mReceivedPackets.add(newPacket)) {
+            throw new AssertionError("More than " + Integer.MAX_VALUE + " packets outstanding!");
+        }
+    }
+
+    /**
+     * @deprecated This method does not actually "pop" (which generally means the last packet).
+     * Use {@link #poll(long)}, which has the same behavior, instead.
+     */
+    @Nullable
+    @Deprecated
+    public byte[] popPacket(long timeoutMs) {
+        return poll(timeoutMs);
+    }
+
+    /**
+     * @deprecated This method does not actually "pop" (which generally means the last packet).
+     * Use {@link #poll(long, Predicate)}, which has the same behavior, instead.
+     */
+    @Nullable
+    @Deprecated
+    public byte[] popPacket(long timeoutMs, @NonNull Predicate<byte[]> filter) {
+        return poll(timeoutMs, filter);
+    }
+
+    /**
+     * Get the next packet that was received on the interface.
+     */
+    @Nullable
+    public byte[] poll(long timeoutMs) {
+        return mReadHead.getValue().poll(timeoutMs, packet -> true);
+    }
+
+    /**
+     * Get the next packet that was received on the interface and matches the specified filter.
+     */
+    @Nullable
+    public byte[] poll(long timeoutMs, @NonNull Predicate<byte[]> filter) {
+        return mReadHead.getValue().poll(timeoutMs, filter::test);
+    }
+
+    /**
+     * Get the {@link ArrayTrackRecord} that records all packets received by the reader since its
+     * creation.
+     */
+    public ArrayTrackRecord<byte[]> getReceivedPackets() {
+        return mReceivedPackets;
+    }
+
+    /*
+     * Send a response on the TAP interface.
+     *
+     * The passed ByteBuffer is flipped after use.
+     *
+     * @param packet The packet to send.
+     * @throws IOException if the interface can't be written to.
+     */
+    public void sendResponse(final ByteBuffer packet) throws IOException {
+        try (FileOutputStream out = new FileOutputStream(mTapFd)) {
+            byte[] packetBytes = new byte[packet.limit()];
+            packet.get(packetBytes);
+            packet.flip();  // So we can reuse it in the future.
+            out.write(packetBytes);
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt
new file mode 100644
index 0000000..701666c
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import kotlin.test.assertFalse
+import kotlin.test.fail
+
+private const val HANDLER_TIMEOUT_MS = 10_000L
+
+/**
+ * A [TestRule] that sets up a [TapPacketReader] on a [TestNetworkInterface] for use in the test.
+ *
+ * @param maxPacketSize Maximum size of packets read in the [TapPacketReader] buffer.
+ * @param autoStart Whether to initialize the interface and start the reader automatically for every
+ *                  test. If false, each test must either call start() and stop(), or be annotated
+ *                  with TapPacketReaderTest before using the reader or interface.
+ */
+class TapPacketReaderRule @JvmOverloads constructor(
+    private val maxPacketSize: Int = 1500,
+    private val autoStart: Boolean = true
+) : TestRule {
+    // Use lateinit as the below members can't be initialized in the rule constructor (the
+    // InstrumentationRegistry may not be ready), but from the point of view of test cases using
+    // this rule with autoStart = true, the members are always initialized (in setup/test/teardown):
+    // tests cases should be able use them directly.
+    // lateinit also allows getting good exceptions detailing what went wrong if the members are
+    // referenced before they could be initialized (typically if autoStart is false and the test
+    // does not call start or use @TapPacketReaderTest).
+    lateinit var iface: TestNetworkInterface
+    lateinit var reader: TapPacketReader
+
+    @Volatile
+    private var readerRunning = false
+
+    /**
+     * Indicates that the [TapPacketReaderRule] should initialize its [TestNetworkInterface] and
+     * start the [TapPacketReader] before the test, and tear them down afterwards.
+     *
+     * For use when [TapPacketReaderRule] is created with autoStart = false.
+     */
+    annotation class TapPacketReaderTest
+
+    /**
+     * Initialize the tap interface and start the [TapPacketReader].
+     *
+     * Tests using this method must also call [stop] before exiting.
+     * @param handler Handler to run the reader on. Callers are responsible for safely terminating
+     *                the handler when the test ends. If null, a handler thread managed by the
+     *                rule will be used.
+     */
+    @JvmOverloads
+    fun start(handler: Handler? = null) {
+        if (this::iface.isInitialized) {
+            fail("${TapPacketReaderRule::class.java.simpleName} was already started")
+        }
+
+        val ctx = InstrumentationRegistry.getInstrumentation().context
+        iface = runAsShell(MANAGE_TEST_NETWORKS) {
+            val tnm = ctx.getSystemService(TestNetworkManager::class.java)
+                    ?: fail("Could not obtain the TestNetworkManager")
+            tnm.createTapInterface()
+        }
+        val usedHandler = handler ?: HandlerThread(
+                TapPacketReaderRule::class.java.simpleName).apply { start() }.threadHandler
+        reader = TapPacketReader(usedHandler, iface.fileDescriptor.fileDescriptor, maxPacketSize)
+        reader.startAsyncForTest()
+        readerRunning = true
+    }
+
+    /**
+     * Stop the [TapPacketReader].
+     *
+     * Tests calling [start] must call this method before exiting. If a handler was specified in
+     * [start], all messages on that handler must also be processed after calling this method and
+     * before exiting.
+     *
+     * If [start] was not called, calling this method is a no-op.
+     */
+    fun stop() {
+        // The reader may not be initialized if the test case did not use the rule, even though
+        // other test cases in the same class may be using it (so test classes may call stop in
+        // tearDown even if start is not called for all test cases).
+        if (!this::reader.isInitialized) return
+        reader.handler.post {
+            reader.stop()
+            readerRunning = false
+        }
+    }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return TapReaderStatement(base, description)
+    }
+
+    private inner class TapReaderStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            val shouldStart = autoStart ||
+                    description.getAnnotation(TapPacketReaderTest::class.java) != null
+            if (shouldStart) {
+                start()
+            }
+
+            try {
+                base.evaluate()
+            } finally {
+                if (shouldStart) {
+                    stop()
+                    reader.handler.looper.apply {
+                        quitSafely()
+                        thread.join(HANDLER_TIMEOUT_MS)
+                        assertFalse(thread.isAlive,
+                                "HandlerThread did not exit within $HANDLER_TIMEOUT_MS ms")
+                    }
+                }
+
+                if (this@TapPacketReaderRule::iface.isInitialized) {
+                    iface.fileDescriptor.close()
+                }
+            }
+
+            assertFalse(readerRunning,
+                    "stop() was not called, or the provided handler did not process the stop " +
+                    "message before the test ended. If not using autostart, make sure to call " +
+                    "stop() after the test. If a handler is specified in start(), make sure all " +
+                    "messages are processed after calling stop(), before quitting (for example " +
+                    "by using HandlerThread#quitSafely and HandlerThread#join).")
+        }
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
new file mode 100644
index 0000000..70f20d6
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ *
+ * Fake BPF map class for tests that have no privilege to access real BPF maps. TestBpfMap does not
+ * load JNI and all member functions do not access real BPF maps.
+ *
+ * Implements IBpfMap so that any class using IBpfMap can use this class in its tests.
+ *
+ * @param <K> the key type
+ * @param <V> the value type
+ */
+public class TestBpfMap<K extends Struct, V extends Struct> implements IBpfMap<K, V> {
+    private final ConcurrentHashMap<K, V> mMap = new ConcurrentHashMap<>();
+
+    public TestBpfMap() {}
+
+    // TODO: Remove this constructor
+    public TestBpfMap(final Class<K> key, final Class<V> value) {
+    }
+
+    @Override
+    public void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException {
+        // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to
+        // implement the entry deletion in the iteration if required.
+        for (Map.Entry<K, V> entry : mMap.entrySet()) {
+            action.accept(entry.getKey(), entry.getValue());
+        }
+    }
+
+    @Override
+    public void updateEntry(K key, V value) throws ErrnoException {
+        mMap.put(key, value);
+    }
+
+    @Override
+    public void insertEntry(K key, V value) throws ErrnoException,
+            IllegalArgumentException {
+        // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry.
+        if (mMap.get(key) != null) {
+            throw new IllegalArgumentException(key + " already exist");
+        }
+        mMap.put(key, value);
+    }
+
+    @Override
+    public void replaceEntry(K key, V value) throws ErrnoException, NoSuchElementException {
+        if (!mMap.containsKey(key)) throw new NoSuchElementException();
+        mMap.put(key, value);
+    }
+
+    @Override
+    public boolean insertOrReplaceEntry(K key, V value) throws ErrnoException {
+        // Returns true if inserted, false if replaced.
+        boolean ret = !mMap.containsKey(key);
+        mMap.put(key, value);
+        return ret;
+    }
+
+    @Override
+    public boolean deleteEntry(Struct key) throws ErrnoException {
+        return mMap.remove(key) != null;
+    }
+
+    @Override
+    public boolean isEmpty() throws ErrnoException {
+        return mMap.isEmpty();
+    }
+
+    @Override
+    public K getNextKey(@NonNull K key) {
+        // Expensive, but since this is only for tests...
+        Iterator<K> it = mMap.keySet().iterator();
+        while (it.hasNext()) {
+            if (Objects.equals(it.next(), key)) {
+                return it.hasNext() ? it.next() : null;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public K getFirstKey() {
+        for (K key : mMap.keySet()) {
+            return key;
+        }
+        return null;
+    }
+
+    @Override
+    public boolean containsKey(@NonNull K key) throws ErrnoException {
+        return mMap.containsKey(key);
+    }
+
+    @Override
+    public V getValue(@NonNull K key) throws ErrnoException {
+        // Return value for a given key. Otherwise, return null without an error ENOENT.
+        // BpfMap#getValue treats that the entry is not found as no error.
+        return mMap.get(key);
+    }
+
+    @Override
+    public void clear() throws ErrnoException {
+        // TODO: consider using mocked #getFirstKey and #deleteEntry to implement.
+        mMap.clear();
+    }
+
+    @Override
+    public void close() throws IOException {
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestDnsServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestDnsServer.kt
new file mode 100644
index 0000000..e1b771b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestDnsServer.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.Network
+import android.util.Log
+import com.android.internal.annotations.GuardedBy
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE
+import com.android.net.module.util.DnsPacket
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.SocketAddress
+import java.net.SocketException
+import java.util.ArrayList
+
+private const val TAG = "TestDnsServer"
+private const val VDBG = true
+@VisibleForTesting(visibility = PRIVATE)
+const val MAX_BUF_SIZE = 8192
+
+/**
+ * A simple implementation of Dns Server that can be bound on specific address and Network.
+ *
+ * The caller should use start() to make the server start a new thread to receive DNS queries
+ * on the bound address, [isAlive] to check status, and stop() for stopping.
+ * The server allows user to manipulate the records to be answered through
+ * [setAnswer] at runtime.
+ *
+ * This server runs on its own thread. Please make sure writing the query to the socket
+ * happens-after using [setAnswer] to guarantee the correct answer is returned. If possible,
+ * use [setAnswer] before calling [start] for simplicity.
+ */
+class TestDnsServer(network: Network, addr: InetSocketAddress) {
+    enum class Status {
+        NOT_STARTED, STARTED, STOPPED
+    }
+    @GuardedBy("thread")
+    private var status: Status = Status.NOT_STARTED
+    private val thread = ReceivingThread()
+    private val socket = DatagramSocket(addr).also { network.bindSocket(it) }
+    private val ansProvider = DnsAnswerProvider()
+
+    // The buffer to store the received packet. They are being reused for
+    // efficiency and it's fine because they are only ever accessed
+    // on the server thread in a sequential manner.
+    private val buffer = ByteArray(MAX_BUF_SIZE)
+    private val packet = DatagramPacket(buffer, buffer.size)
+
+    fun setAnswer(hostname: String, answer: List<InetAddress>) =
+        ansProvider.setAnswer(hostname, answer)
+
+    private fun processPacket() {
+        // Blocking read and try construct a DnsQueryPacket object.
+        socket.receive(packet)
+        val q = DnsQueryPacket(packet.data)
+        handleDnsQuery(q, packet.socketAddress)
+    }
+
+    // TODO: Add support to reply some error with a DNS reply packet with failure RCODE.
+    private fun handleDnsQuery(q: DnsQueryPacket, src: SocketAddress) {
+        val queryRecords = q.queryRecords
+        if (queryRecords.size != 1) {
+            throw IllegalArgumentException(
+                "Expected one dns query record but got ${queryRecords.size}"
+            )
+        }
+        val answerRecords = queryRecords[0].let { ansProvider.getAnswer(it.dName, it.nsType) }
+
+        if (VDBG) {
+            Log.v(TAG, "handleDnsPacket: " +
+                        queryRecords.map { "${it.dName},${it.nsType}" }.joinToString() +
+                        " ansCount=${answerRecords.size} socketAddress=$src")
+        }
+
+        val bytes = q.getAnswerPacket(answerRecords).bytes
+        val reply = DatagramPacket(bytes, bytes.size, src)
+        socket.send(reply)
+    }
+
+    fun start() {
+        synchronized(thread) {
+            if (status != Status.NOT_STARTED) {
+                throw IllegalStateException("unexpected status: $status")
+            }
+            thread.start()
+            status = Status.STARTED
+        }
+    }
+    fun stop() {
+        synchronized(thread) {
+            if (status != Status.STARTED) {
+                throw IllegalStateException("unexpected status: $status")
+            }
+            // The thread needs to be interrupted before closing the socket to prevent a data
+            // race where the thread tries to read from the socket while it's being closed.
+            // DatagramSocket is not thread-safe and running both concurrently can end up in
+            // getPort() returning -1 after it's been checked not to, resulting in a crash by
+            // IllegalArgumentException inside the DatagramSocket implementation.
+            thread.interrupt()
+            socket.close()
+            thread.join()
+            status = Status.STOPPED
+        }
+    }
+    val isAlive get() = thread.isAlive
+    val port get() = socket.localPort
+
+    inner class ReceivingThread : Thread() {
+        override fun run() {
+            while (!interrupted() && !socket.isClosed) {
+                try {
+                    processPacket()
+                } catch (e: InterruptedException) {
+                    // The caller terminated the server, exit.
+                    break
+                } catch (e: SocketException) {
+                    // The caller terminated the server, exit.
+                    break
+                }
+            }
+            Log.i(TAG, "exiting socket={$socket}")
+        }
+    }
+
+    @VisibleForTesting(visibility = PRIVATE)
+    class DnsQueryPacket : DnsPacket {
+        constructor(data: ByteArray) : super(data)
+        constructor(header: DnsHeader, qd: List<DnsRecord>, an: List<DnsRecord>) :
+                super(header, qd, an)
+
+        init {
+            if (mHeader.isResponse) {
+                throw ParseException("Not a query packet")
+            }
+        }
+
+        val queryRecords: List<DnsRecord>
+            get() = mRecords[QDSECTION]
+
+        fun getAnswerPacket(ar: List<DnsRecord>): DnsAnswerPacket {
+            // Set QR bit of flag to 1 for response packet according to RFC 1035 section 4.1.1.
+            val flags = 1 shl 15
+            val qr = ArrayList(mRecords[QDSECTION])
+            // Copy the query packet header id to the answer packet as RFC 1035 section 4.1.1.
+            val header = DnsHeader(mHeader.id, flags, qr.size, ar.size)
+            return DnsAnswerPacket(header, qr, ar)
+        }
+    }
+
+    class DnsAnswerPacket : DnsPacket {
+        constructor(header: DnsHeader, qr: List<DnsRecord>, ar: List<DnsRecord>) :
+                super(header, qr, ar)
+        @VisibleForTesting(visibility = PRIVATE)
+        constructor(bytes: ByteArray) : super(bytes)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
new file mode 100644
index 0000000..f1f0c1c
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.Uri
+import com.android.net.module.util.ArrayTrackRecord
+import fi.iki.elonen.NanoHTTPD
+import java.io.IOException
+
+/**
+ * A minimal HTTP server running on a random available port.
+ *
+ * @param host The host to listen to, or null to listen on all hosts
+ * @param port The port to listen to, or 0 to auto select
+ */
+class TestHttpServer
+    @JvmOverloads constructor(host: String? = null, port: Int = 0) : NanoHTTPD(host, port) {
+    // Map of URL path -> HTTP response code
+    private val responses = HashMap<Request, Response>()
+
+    /**
+     * A record of all requests received by the server since it was started.
+     */
+    val requestsRecord = ArrayTrackRecord<Request>()
+
+    /**
+     * A request received by the test server.
+     */
+    data class Request(
+        val path: String,
+        val method: Method = Method.GET,
+        val queryParameters: String = ""
+    ) {
+        /**
+         * Returns whether the specified [Uri] matches parameters of this request.
+         */
+        fun matches(uri: Uri) = (uri.path ?: "") == path && (uri.query ?: "") == queryParameters
+    }
+
+    /**
+     * Add a response for GET requests with the path and query parameters of the specified [Uri].
+     */
+    fun addResponse(
+        uri: Uri,
+        statusCode: Response.IStatus,
+        headers: Map<String, String>? = null,
+        content: String = ""
+    ) {
+        addResponse(Request(uri.path
+                ?: "", Method.GET, uri.query ?: ""),
+                statusCode, headers, content)
+    }
+
+    /**
+     * Add a response for the given request.
+     */
+    fun addResponse(
+        request: Request,
+        statusCode: Response.IStatus,
+        headers: Map<String, String>? = null,
+        content: String = ""
+    ) {
+        val response = newFixedLengthResponse(statusCode, "text/plain", content)
+        headers?.forEach {
+            (key, value) -> response.addHeader(key, value)
+        }
+        responses[request] = response
+    }
+
+    override fun serve(session: IHTTPSession): Response {
+        val request = Request(session.uri
+                ?: "", session.method, session.queryParameterString ?: "")
+        requestsRecord.add(request)
+
+        // For PUT and POST, call parseBody to read InputStream before responding.
+        if (Method.PUT == session.method || Method.POST == session.method) {
+            try {
+                session.parseBody(HashMap())
+            } catch (e: Exception) {
+                when (e) {
+                    is IOException, is ResponseException -> e.toResponse()
+                    else -> throw e
+                }
+            }
+        }
+
+        // Default response is a 404
+        return responses[request] ?: super.serve(session)
+    }
+
+    fun Exception.toResponse() =
+        newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", this.toString())
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
new file mode 100644
index 0000000..84fb47b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.LinkAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.LinkProperties
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.os.Binder
+import android.os.Build
+import androidx.annotation.RequiresApi
+import com.android.modules.utils.build.SdkLevel.isAtLeastR
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertTrue
+
+/**
+ * Create a test network based on a TUN interface with a LinkAddress.
+ *
+ * TODO: remove this function after fixing all the callers to use a list of LinkAddresses.
+ * This method will block until the test network is available. Requires
+ * [android.Manifest.permission.CHANGE_NETWORK_STATE] and
+ * [android.Manifest.permission.MANAGE_TEST_NETWORKS].
+ */
+fun initTestNetwork(
+    context: Context,
+    interfaceAddr: LinkAddress,
+    setupTimeoutMs: Long = 10_000L
+): TestNetworkTracker {
+    return initTestNetwork(context, listOf(interfaceAddr), setupTimeoutMs)
+}
+
+/**
+ * Create a test network based on a TUN interface with a LinkAddress list.
+ *
+ * This method will block until the test network is available. Requires
+ * [android.Manifest.permission.CHANGE_NETWORK_STATE] and
+ * [android.Manifest.permission.MANAGE_TEST_NETWORKS].
+ */
+fun initTestNetwork(
+    context: Context,
+    linkAddrs: List<LinkAddress>,
+    setupTimeoutMs: Long = 10_000L
+): TestNetworkTracker {
+    return initTestNetwork(context, linkAddrs, lp = null, setupTimeoutMs = setupTimeoutMs)
+}
+
+/**
+ * Create a test network based on a TUN interface
+ *
+ * This method will block until the test network is available. Requires
+ * [android.Manifest.permission.CHANGE_NETWORK_STATE] and
+ * [android.Manifest.permission.MANAGE_TEST_NETWORKS].
+ *
+ * This is only usable starting from R as [TestNetworkManager] has no support for specifying
+ * LinkProperties on Q.
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+fun initTestNetwork(
+    context: Context,
+    lp: LinkProperties,
+    setupTimeoutMs: Long = 10_000L
+): TestNetworkTracker {
+    return initTestNetwork(context, lp.linkAddresses, lp, setupTimeoutMs)
+}
+
+private fun initTestNetwork(
+    context: Context,
+    linkAddrs: List<LinkAddress>,
+    lp: LinkProperties?,
+    setupTimeoutMs: Long = 10_000L
+): TestNetworkTracker {
+    val tnm = context.getSystemService(TestNetworkManager::class.java)!!
+    val iface = if (isAtLeastS()) tnm.createTunInterface(linkAddrs)
+    else tnm.createTunInterface(linkAddrs.toTypedArray())
+    val lpWithIface = if (lp == null) null else LinkProperties(lp).apply {
+        interfaceName = iface.interfaceName
+    }
+    return TestNetworkTracker(context, iface, tnm, lpWithIface, setupTimeoutMs)
+}
+
+/**
+ * Utility class to create and track test networks.
+ *
+ * This class is not thread-safe.
+ */
+class TestNetworkTracker internal constructor(
+    val context: Context,
+    val iface: TestNetworkInterface,
+    val tnm: TestNetworkManager,
+    val lp: LinkProperties?,
+    setupTimeoutMs: Long
+) : TestableNetworkCallback.HasNetwork {
+    private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+    private val binder = Binder()
+
+    private val networkCallback: NetworkCallback
+    override val network: Network
+    val testIface: TestNetworkInterface
+
+    init {
+        val networkFuture = CompletableFuture<Network>()
+        val networkRequest = NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                // Test networks do not have NOT_VPN or TRUSTED capabilities by default
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(iface.interfaceName))
+                .build()
+        networkCallback = object : NetworkCallback() {
+            override fun onAvailable(network: Network) {
+                networkFuture.complete(network)
+            }
+        }
+        cm.requestNetwork(networkRequest, networkCallback)
+
+        network = try {
+            if (lp != null) {
+                assertTrue(isAtLeastR(), "Cannot specify TestNetwork LinkProperties before R")
+                tnm.setupTestNetwork(lp, true /* isMetered */, binder)
+            } else {
+                tnm.setupTestNetwork(iface.interfaceName, binder)
+            }
+            networkFuture.get(setupTimeoutMs, TimeUnit.MILLISECONDS)
+        } catch (e: Throwable) {
+            cm.unregisterNetworkCallback(networkCallback)
+            throw e
+        }
+
+        testIface = iface
+    }
+
+    fun teardown() {
+        cm.unregisterNetworkCallback(networkCallback)
+        tnm.teardownTestNetwork(network)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
new file mode 100644
index 0000000..f571f64
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("TestPermissionUtil")
+
+package com.android.testutils
+
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.FunctionalUtils.ThrowingRunnable
+import com.android.testutils.FunctionalUtils.ThrowingSupplier
+
+/**
+ * Run the specified [task] with the specified [permissions] obtained through shell
+ * permission identity.
+ *
+ * Passing in an empty list of permissions can grant all shell permissions, but this is
+ * discouraged as it also causes the process to temporarily lose non-shell permissions.
+ */
+fun <T> runAsShell(vararg permissions: String, task: () -> T): T {
+    val autom = InstrumentationRegistry.getInstrumentation().uiAutomation
+
+    // Calls to adoptShellPermissionIdentity do not nest, and dropShellPermissionIdentity drops all
+    // permissions. Thus, nesting calls will almost certainly cause test bugs, On S+, where we can
+    // detect this, refuse to do it.
+    //
+    // TODO: when R is deprecated, we could try to make this work instead.
+    // - Get the list of previously-adopted permissions.
+    // - Adopt the union of the previously-adopted and newly-requested permissions.
+    // - Run the task.
+    // - Adopt the previously-adopted permissions, dropping the ones just adopted.
+    //
+    // This would allow tests (and utility classes, such as the TestCarrierConfigReceiver attempted
+    // in aosp/2106007) to call runAsShell even within a test that has already adopted permissions.
+    if (SdkLevel.isAtLeastS() && !autom.getAdoptedShellPermissions().isNullOrEmpty()) {
+        throw IllegalStateException("adoptShellPermissionIdentity calls must not be nested")
+    }
+
+    autom.adoptShellPermissionIdentity(*permissions)
+    try {
+        return task()
+    } finally {
+        autom.dropShellPermissionIdentity()
+    }
+}
+
+/**
+ * Convenience overload of [runAsShell] that uses a [ThrowingSupplier] for Java callers, when
+ * only one/two/three permissions are needed.
+ */
+@JvmOverloads
+fun <T> runAsShell(
+    perm1: String,
+    perm2: String = "",
+    perm3: String = "",
+    supplier: ThrowingSupplier<T>
+): T = runAsShell(*getNonEmptyVarargs(perm1, perm2, perm3)) { supplier.get() }
+
+/**
+ * Convenience overload of [runAsShell] that uses a [ThrowingRunnable] for Java callers, when
+ * only one/two/three permissions are needed.
+ */
+@JvmOverloads
+fun runAsShell(
+    perm1: String,
+    perm2: String = "",
+    perm3: String = "",
+    runnable: ThrowingRunnable
+): Unit = runAsShell(*getNonEmptyVarargs(perm1, perm2, perm3)) { runnable.run() }
+
+/**
+ * Get an array containing the first consecutive non-empty arguments out of three arguments.
+ *
+ * The first argument is assumed to be non-empty.
+ */
+private fun getNonEmptyVarargs(arg1: String, arg2: String, arg3: String): Array<String> {
+    return when {
+        arg2 == "" -> arrayOf(arg1)
+        arg3 == "" -> arrayOf(arg1, arg2)
+        else -> arrayOf(arg1, arg2, arg3)
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
new file mode 100644
index 0000000..8dc1bc4
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import android.content.Context
+import android.net.KeepalivePacketData
+import android.net.LinkProperties
+import android.net.NetworkAgent
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.QosFilter
+import android.net.Uri
+import android.os.Looper
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnDscpPolicyStatusUpdated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkDestroyed
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkUnwanted
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnRegisterQosCallback
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnRemoveKeepalivePacketFilter
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnSaveAcceptUnvalidated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnSignalStrengthThresholdsUpdated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStartSocketKeepalive
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import java.time.Duration
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import org.junit.Assert.assertArrayEquals
+
+// Any legal score (0~99) for the test network would do, as it is going to be kept up by the
+// requests filed by the test and should never match normal internet requests. 70 is the default
+// score of Ethernet networks, it's as good a value as any other.
+private const val TEST_NETWORK_SCORE = 70
+
+private class Provider(context: Context, looper: Looper) :
+            NetworkProvider(context, looper, "NetworkAgentTest NetworkProvider")
+
+public open class TestableNetworkAgent(
+    context: Context,
+    looper: Looper,
+    val nc: NetworkCapabilities,
+    val lp: LinkProperties,
+    conf: NetworkAgentConfig
+) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
+        nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+
+    val DEFAULT_TIMEOUT_MS = 5000L
+
+    val history = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+    sealed class CallbackEntry {
+        object OnBandwidthUpdateRequested : CallbackEntry()
+        object OnNetworkUnwanted : CallbackEntry()
+        data class OnAddKeepalivePacketFilter(
+            val slot: Int,
+            val packet: KeepalivePacketData
+        ) : CallbackEntry()
+        data class OnRemoveKeepalivePacketFilter(val slot: Int) : CallbackEntry()
+        data class OnStartSocketKeepalive(
+            val slot: Int,
+            val interval: Int,
+            val packet: KeepalivePacketData
+        ) : CallbackEntry()
+        data class OnStopSocketKeepalive(val slot: Int) : CallbackEntry()
+        data class OnSaveAcceptUnvalidated(val accept: Boolean) : CallbackEntry()
+        object OnAutomaticReconnectDisabled : CallbackEntry()
+        data class OnValidationStatus(val status: Int, val uri: Uri?) : CallbackEntry()
+        data class OnSignalStrengthThresholdsUpdated(val thresholds: IntArray) : CallbackEntry()
+        object OnNetworkCreated : CallbackEntry()
+        object OnNetworkDestroyed : CallbackEntry()
+        data class OnDscpPolicyStatusUpdated(val policyId: Int, val status: Int) : CallbackEntry()
+        data class OnRegisterQosCallback(
+            val callbackId: Int,
+            val filter: QosFilter
+        ) : CallbackEntry()
+        data class OnUnregisterQosCallback(val callbackId: Int) : CallbackEntry()
+    }
+
+    override fun onBandwidthUpdateRequested() {
+        history.add(OnBandwidthUpdateRequested)
+    }
+
+    override fun onNetworkUnwanted() {
+        history.add(OnNetworkUnwanted)
+    }
+
+    override fun onAddKeepalivePacketFilter(slot: Int, packet: KeepalivePacketData) {
+        history.add(OnAddKeepalivePacketFilter(slot, packet))
+    }
+
+    override fun onRemoveKeepalivePacketFilter(slot: Int) {
+        history.add(OnRemoveKeepalivePacketFilter(slot))
+    }
+
+    override fun onStartSocketKeepalive(
+        slot: Int,
+        interval: Duration,
+        packet: KeepalivePacketData
+    ) {
+        history.add(OnStartSocketKeepalive(slot, interval.seconds.toInt(), packet))
+    }
+
+    override fun onStopSocketKeepalive(slot: Int) {
+        history.add(OnStopSocketKeepalive(slot))
+    }
+
+    override fun onSaveAcceptUnvalidated(accept: Boolean) {
+        history.add(OnSaveAcceptUnvalidated(accept))
+    }
+
+    override fun onAutomaticReconnectDisabled() {
+        history.add(OnAutomaticReconnectDisabled)
+    }
+
+    override fun onSignalStrengthThresholdsUpdated(thresholds: IntArray) {
+        history.add(OnSignalStrengthThresholdsUpdated(thresholds))
+    }
+
+    fun expectSignalStrengths(thresholds: IntArray? = intArrayOf()) {
+        expectCallback<OnSignalStrengthThresholdsUpdated>().let {
+            assertArrayEquals(thresholds, it.thresholds)
+        }
+    }
+
+    override fun onQosCallbackRegistered(qosCallbackId: Int, filter: QosFilter) {
+        history.add(OnRegisterQosCallback(qosCallbackId, filter))
+    }
+
+    override fun onQosCallbackUnregistered(qosCallbackId: Int) {
+        history.add(OnUnregisterQosCallback(qosCallbackId))
+    }
+
+    override fun onValidationStatus(status: Int, uri: Uri?) {
+        history.add(OnValidationStatus(status, uri))
+    }
+
+    override fun onNetworkCreated() {
+        history.add(OnNetworkCreated)
+    }
+
+    override fun onNetworkDestroyed() {
+        history.add(OnNetworkDestroyed)
+    }
+
+    override fun onDscpPolicyStatusUpdated(policyId: Int, status: Int) {
+        history.add(OnDscpPolicyStatusUpdated(policyId, status))
+    }
+
+    // Expects the initial validation event that always occurs immediately after registering
+    // a NetworkAgent whose network does not require validation (which test networks do
+    // not, since they lack the INTERNET capability). It always contains the default argument
+    // for the URI.
+    fun expectValidationBypassedStatus() = expectCallback<OnValidationStatus>().let {
+        assertEquals(it.status, VALID_NETWORK)
+        // The returned Uri is parsed from the empty string, which means it's an
+        // instance of the (private) Uri.StringUri. There are no real good ways
+        // to check this, the least bad is to just convert it to a string and
+        // make sure it's empty.
+        assertEquals("", it.uri.toString())
+    }
+
+    inline fun <reified T : CallbackEntry> expectCallback(): T {
+        val foundCallback = history.poll(DEFAULT_TIMEOUT_MS)
+        assertTrue(foundCallback is T, "Expected ${T::class} but found $foundCallback")
+        return foundCallback
+    }
+
+    inline fun <reified T : CallbackEntry> expectCallback(valid: (T) -> Boolean) {
+        val foundCallback = history.poll(DEFAULT_TIMEOUT_MS)
+        assertTrue(foundCallback is T, "Expected ${T::class} but found $foundCallback")
+        assertTrue(valid(foundCallback), "Unexpected callback : $foundCallback")
+    }
+
+    inline fun <reified T : CallbackEntry> eventuallyExpect() =
+            history.poll(DEFAULT_TIMEOUT_MS) { it is T }.also {
+                assertNotNull(it, "Callback ${T::class} not received")
+    } as T
+
+    fun assertNoCallback() {
+        assertTrue(waitForIdle(DEFAULT_TIMEOUT_MS),
+                "Handler didn't became idle after ${DEFAULT_TIMEOUT_MS}ms")
+        assertNull(history.peek())
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
new file mode 100644
index 0000000..ae43c15
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -0,0 +1,649 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.LinkProperties
+import android.net.LocalNetworkInfo
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatusInt
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Losing
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
+import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import kotlin.reflect.KClass
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.fail
+
+object NULL_NETWORK : Network(-1)
+object ANY_NETWORK : Network(-2)
+fun anyNetwork() = ANY_NETWORK
+
+private val DEFAULT_TAG = RecorderCallback::class.simpleName
+    ?: fail("Could not determine class name")
+
+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
+        // this class are data classes. But while data classes can inherit from other classes,
+        // they may only have visible members in the constructors, so they couldn't declare
+        // a constructor with a non-val arg to pass to CallbackEntry. Instead, force all
+        // subclasses to implement a `network' property, which can be done in a data class
+        // constructor by specifying override.
+        abstract val network: Network
+
+        data class Available(override val network: Network) : CallbackEntry()
+        data class CapabilitiesChanged(
+            override val network: Network,
+            val caps: NetworkCapabilities
+        ) : CallbackEntry()
+        data class LinkPropertiesChanged(
+            override val network: Network,
+            val lp: LinkProperties
+        ) : CallbackEntry()
+        data class LocalInfoChanged(
+            override val network: Network,
+            val info: LocalNetworkInfo
+        ) : CallbackEntry()
+        data class Suspended(override val network: Network) : CallbackEntry()
+        data class Resumed(override val network: Network) : CallbackEntry()
+        data class Losing(override val network: Network, val maxMsToLive: Int) : CallbackEntry()
+        data class Lost(override val network: Network) : CallbackEntry()
+        data class Unavailable private constructor(
+            override val network: Network
+        ) : CallbackEntry() {
+            constructor() : this(NULL_NETWORK)
+        }
+        data class BlockedStatus(
+            override val network: Network,
+            val blocked: Boolean
+        ) : CallbackEntry()
+        data class BlockedStatusInt(
+            override val network: Network,
+            val reason: Int
+        ) : CallbackEntry()
+
+        // Convenience constants for expecting a type
+        companion object {
+            @JvmField
+            val AVAILABLE = Available::class
+            @JvmField
+            val NETWORK_CAPS_UPDATED = CapabilitiesChanged::class
+            @JvmField
+            val LINK_PROPERTIES_CHANGED = LinkPropertiesChanged::class
+            @JvmField
+            val LOCAL_INFO_CHANGED = LocalInfoChanged::class
+            @JvmField
+            val SUSPENDED = Suspended::class
+            @JvmField
+            val RESUMED = Resumed::class
+            @JvmField
+            val LOSING = Losing::class
+            @JvmField
+            val LOST = Lost::class
+            @JvmField
+            val UNAVAILABLE = Unavailable::class
+            @JvmField
+            val BLOCKED_STATUS = BlockedStatus::class
+            @JvmField
+            val BLOCKED_STATUS_INT = BlockedStatusInt::class
+        }
+    }
+
+    val history = backingRecord.newReadHead()
+    val mark get() = history.mark
+
+    override fun onAvailable(network: Network) {
+        Log.d(logTag, "onAvailable $network")
+        history.add(Available(network))
+    }
+
+    // PreCheck is not used in the tests today. For backward compatibility with existing tests that
+    // expect the callbacks not to record this, do not listen to PreCheck here.
+
+    override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
+        Log.d(logTag, "onCapabilitiesChanged $network $caps")
+        history.add(CapabilitiesChanged(network, caps))
+    }
+
+    override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) {
+        Log.d(logTag, "onLinkPropertiesChanged $network $lp")
+        history.add(LinkPropertiesChanged(network, lp))
+    }
+
+    override fun onLocalNetworkInfoChanged(network: Network, info: LocalNetworkInfo) {
+        Log.d(logTag, "onLocalNetworkInfoChanged $network $info")
+        history.add(LocalInfoChanged(network, info))
+    }
+
+    override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
+        Log.d(logTag, "onBlockedStatusChanged $network $blocked")
+        history.add(BlockedStatus(network, blocked))
+    }
+
+    // Cannot do:
+    // fun onBlockedStatusChanged(network: Network, blocked: Int) {
+    // because on S, that needs to be "override fun", and on R, that cannot be "override fun".
+    override fun onNetworkSuspended(network: Network) {
+        Log.d(logTag, "onNetworkSuspended $network $network")
+        history.add(Suspended(network))
+    }
+
+    override fun onNetworkResumed(network: Network) {
+        Log.d(logTag, "$network onNetworkResumed $network")
+        history.add(Resumed(network))
+    }
+
+    override fun onLosing(network: Network, maxMsToLive: Int) {
+        Log.d(logTag, "onLosing $network $maxMsToLive")
+        history.add(Losing(network, maxMsToLive))
+    }
+
+    override fun onLost(network: Network) {
+        Log.d(logTag, "onLost $network")
+        history.add(Lost(network))
+    }
+
+    override fun onUnavailable() {
+        Log.d(logTag, "onUnavailable")
+        history.add(Unavailable())
+    }
+}
+
+private const val DEFAULT_TIMEOUT = 30_000L // ms
+private const val DEFAULT_NO_CALLBACK_TIMEOUT = 200L // ms
+private val NOOP = Runnable {}
+
+/**
+ * See comments on the public constructor below for a description of the arguments.
+ */
+open class TestableNetworkCallback private constructor(
+    src: TestableNetworkCallback?,
+    val defaultTimeoutMs: Long,
+    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
+     *                  should be long in most cases, because the success case doesn't incur
+     *                  the wait.
+     * @param noCallbackTimeoutMs the timeout for expecting that no callback is received. Default
+     *                            200ms. Because the success case does incur the timeout, this
+     *                            should be short in most cases, but not so short as to frequently
+     *                            time out before an incorrect callback is received.
+     * @param waiterFunc a function to use before asserting no callback. For some specific tests,
+     *                   it is useful to run test-specific code before asserting no callback to
+     *                   increase the likelihood that a spurious callback is correctly detected.
+     *                   As an example, a unit test using mock loopers may want to use this to
+     *                   make sure the loopers are drained before asserting no callback, since
+     *                   one of them may cause a callback to be called. @see ConnectivityServiceTest
+     *                   for such an example.
+     */
+    @JvmOverloads
+    constructor(
+        timeoutMs: Long = DEFAULT_TIMEOUT,
+        noCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
+        waiterFunc: Runnable = NOOP, // "() -> 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,
+        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
+    val lastAvailableNetwork: Network?
+        get() = when (val it = history.lastOrNull { it is Available || it is Lost }) {
+            is Available -> it.network
+            else -> null
+        }
+
+    /**
+     * Get the next callback or null if timeout.
+     *
+     * With no argument, this method waits out the default timeout. To wait forever, pass
+     * Long.MAX_VALUE.
+     */
+    @JvmOverloads
+    fun poll(timeoutMs: Long = defaultTimeoutMs, predicate: (CallbackEntry) -> Boolean = { true }) =
+            history.poll(timeoutMs, predicate)
+
+    /*****
+     * expect family of methods.
+     * These methods fetch the next callback and assert it matches the conditions : type,
+     * passed predicate. If no callback is received within the timeout, these methods fail.
+     */
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        network: Network = ANY_NETWORK,
+        timeoutMs: Long = defaultTimeoutMs,
+        errorMsg: String? = null,
+        test: (T) -> Boolean = { true }
+    ) = expect<CallbackEntry>(network, timeoutMs, errorMsg) {
+        if (type.isInstance(it)) {
+            test(it as T) // Cast can't fail since type.isInstance(it) and type: KClass<T>
+        } else {
+            fail("Expected callback ${type.simpleName}, got $it")
+        }
+    } as T
+
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        network: HasNetwork,
+        timeoutMs: Long = defaultTimeoutMs,
+        errorMsg: String? = null,
+        test: (T) -> Boolean = { true }
+    ) = expect(type, network.network, timeoutMs, errorMsg, test)
+
+    // Java needs an explicit overload to let it omit arguments in the middle, so define these
+    // here. Note that @JvmOverloads give us the versions without the last arguments too, so
+    // there is no need to explicitly define versions without the test predicate.
+    // Without |network|
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        timeoutMs: Long,
+        errorMsg: String?,
+        test: (T) -> Boolean = { true }
+    ) = expect(type, ANY_NETWORK, timeoutMs, errorMsg, test)
+
+    // Without |timeout|, in Network and HasNetwork versions
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        network: Network,
+        errorMsg: String?,
+        test: (T) -> Boolean = { true }
+    ) = expect(type, network, defaultTimeoutMs, errorMsg, test)
+
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        network: HasNetwork,
+        errorMsg: String?,
+        test: (T) -> Boolean = { true }
+    ) = expect(type, network.network, defaultTimeoutMs, errorMsg, test)
+
+    // Without |errorMsg|, in Network and HasNetwork versions
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        network: Network,
+        timeoutMs: Long,
+        test: (T) -> Boolean
+    ) = expect(type, network, timeoutMs, null, test)
+
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        network: HasNetwork,
+        timeoutMs: Long,
+        test: (T) -> Boolean
+    ) = expect(type, network.network, timeoutMs, null, test)
+
+    // Without |network| or |timeout|
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        errorMsg: String?,
+        test: (T) -> Boolean = { true }
+    ) = expect(type, ANY_NETWORK, defaultTimeoutMs, errorMsg, test)
+
+    // Without |network| or |errorMsg|
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        timeoutMs: Long,
+        test: (T) -> Boolean = { true }
+    ) = expect(type, ANY_NETWORK, timeoutMs, null, test)
+
+    // Without |timeout| or |errorMsg|, in Network and HasNetwork versions
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        network: Network,
+        test: (T) -> Boolean
+    ) = expect(type, network, defaultTimeoutMs, null, test)
+
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        network: HasNetwork,
+        test: (T) -> Boolean
+    ) = expect(type, network.network, defaultTimeoutMs, null, test)
+
+    // Without |network| or |timeout| or |errorMsg|
+    @JvmOverloads
+    fun <T : CallbackEntry> expect(
+        type: KClass<T>,
+        test: (T) -> Boolean
+    ) = expect(type, ANY_NETWORK, defaultTimeoutMs, null, test)
+
+    // Kotlin reified versions. Don't call methods above, or the predicate would need to be noinline
+    inline fun <reified T : CallbackEntry> expect(
+        network: Network = ANY_NETWORK,
+        timeoutMs: Long = defaultTimeoutMs,
+        errorMsg: String? = null,
+        test: (T) -> Boolean = { true }
+    ) = (poll(timeoutMs) ?: failWithErrorReason(errorMsg,
+        "Did not receive ${T::class.simpleName} after ${timeoutMs}ms"))
+            .also {
+                if (it !is T) {
+                    failWithErrorReason(
+                        errorMsg,
+                        "Expected callback ${T::class.simpleName}, got $it"
+                    )
+                }
+                if (ANY_NETWORK !== network && it.network != network) {
+                    failWithErrorReason(errorMsg, "Expected network $network for callback : $it")
+                }
+                if (!test(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,
+        errorMsg: String? = null,
+        test: (T) -> Boolean = { true }
+    ) = expect(network.network, timeoutMs, errorMsg, test)
+
+    /*****
+     * assertNoCallback family of methods.
+     * These methods make sure that no callback that matches the predicate was received.
+     * If no predicate is given, they make sure that no callback at all was received.
+     * These methods run the waiter func given in the constructor if any.
+     */
+    @JvmOverloads
+    fun assertNoCallback(
+        timeoutMs: Long = defaultNoCallbackTimeoutMs,
+        valid: (CallbackEntry) -> Boolean = { true }
+    ) {
+        waiterFunc.run()
+        history.poll(timeoutMs) { valid(it) }?.let { fail("Expected no callback but got $it") }
+    }
+
+    fun assertNoCallback(valid: (CallbackEntry) -> Boolean) =
+            assertNoCallback(defaultNoCallbackTimeoutMs, valid)
+
+    /*****
+     * eventuallyExpect family of methods.
+     * These methods make sure a callback that matches the type/predicate is received eventually.
+     * Any callback of the wrong type, or doesn't match the optional predicate, is ignored.
+     * They fail if no callback matching the predicate is received within the timeout.
+     */
+    inline fun <reified T : CallbackEntry> eventuallyExpect(
+        timeoutMs: Long = defaultTimeoutMs,
+        from: Int = mark,
+        crossinline predicate: (T) -> Boolean = { true }
+    ): T = history.poll(timeoutMs, from) { it is T && predicate(it) }.also {
+        assertNotNull(
+            it,
+            "Callback ${T::class} not received within ${timeoutMs}ms. " +
+                "Got ${history.backtrace()}"
+        )
+    } as T
+
+    @JvmOverloads
+    fun <T : CallbackEntry> eventuallyExpect(
+        type: KClass<T>,
+        timeoutMs: Long = defaultTimeoutMs,
+        predicate: (cb: T) -> Boolean = { true }
+    ) = history.poll(timeoutMs) { type.java.isInstance(it) && predicate(it as T) }.also {
+        assertNotNull(
+            it,
+            "Callback ${type.java} not received within ${timeoutMs}ms. " +
+                "Got ${history.backtrace()}"
+        )
+    } as T
+
+    fun <T : CallbackEntry> eventuallyExpect(
+        type: KClass<T>,
+        timeoutMs: Long = defaultTimeoutMs,
+        from: Int = mark,
+        predicate: (cb: T) -> Boolean = { true }
+    ) = history.poll(timeoutMs, from) { type.java.isInstance(it) && predicate(it as T) }.also {
+        assertNotNull(
+            it,
+            "Callback ${type.java} not received within ${timeoutMs}ms. " +
+                "Got ${history.backtrace()}"
+        )
+    } as T
+
+    // Expects onAvailable and the callbacks that follow it. These are:
+    // - onSuspended, iff the network was suspended when the callbacks fire.
+    // - onCapabilitiesChanged.
+    // - onLinkPropertiesChanged.
+    // - onBlockedStatusChanged.
+    //
+    // @param network the network to expect the callbacks on.
+    // @param suspended whether to expect a SUSPENDED callback.
+    // @param validated the expected value of the VALIDATED capability in the
+    //        onCapabilitiesChanged callback.
+    // @param tmt how long to wait for the callbacks.
+    @JvmOverloads
+    fun expectAvailableCallbacks(
+        net: Network,
+        suspended: Boolean = false,
+        validated: Boolean? = true,
+        blocked: Boolean = false,
+        upstream: Network? = null,
+        tmt: Long = defaultTimeoutMs
+    ) {
+        expectAvailableCallbacksCommon(net, suspended, validated, upstream, tmt)
+        expect<BlockedStatus>(net, tmt) { it.blocked == blocked }
+    }
+
+    // For backward compatibility, add a method that allows callers to specify a timeout but
+    // no upstream.
+    fun expectAvailableCallbacks(
+        net: Network,
+        suspended: Boolean = false,
+        validated: Boolean? = true,
+        blocked: Boolean = false,
+        tmt: Long = defaultTimeoutMs
+    ) = expectAvailableCallbacks(net, suspended, validated, blocked, upstream = null, tmt = tmt)
+
+    fun expectAvailableCallbacks(
+        net: Network,
+        suspended: Boolean,
+        validated: Boolean,
+        blockedReason: Int,
+        upstream: Network? = null,
+        tmt: Long
+    ) {
+        expectAvailableCallbacksCommon(net, suspended, validated, upstream, tmt)
+        expect<BlockedStatusInt>(net) { it.reason == blockedReason }
+    }
+
+    // For backward compatibility, add a method that allows callers to specify a timeout but
+    // no upstream.
+    fun expectAvailableCallbacks(
+            net: Network,
+            suspended: Boolean = false,
+            validated: Boolean = true,
+            blockedReason: Int,
+            tmt: Long = defaultTimeoutMs
+    ) = expectAvailableCallbacks(net, suspended, validated, blockedReason, upstream = null, tmt)
+
+    private fun expectAvailableCallbacksCommon(
+        net: Network,
+        suspended: Boolean,
+        validated: Boolean?,
+        upstream: Network?,
+        tmt: Long
+    ) {
+        expect<Available>(net, tmt)
+        if (suspended) {
+            expect<Suspended>(net, tmt)
+        }
+        val caps = expect<CapabilitiesChanged>(net, tmt) {
+            validated == null || validated == it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+        }.caps
+        expect<LinkPropertiesChanged>(net, tmt)
+        if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)) {
+            expect<LocalInfoChanged>(net, tmt) { it.info.upstreamNetwork == upstream }
+        }
+    }
+
+    // Backward compatibility for existing Java code. Use named arguments instead and remove all
+    // these when there is no user left.
+    fun expectAvailableAndSuspendedCallbacks(
+        net: Network,
+        validated: Boolean,
+        tmt: Long = defaultTimeoutMs
+    ) = expectAvailableCallbacks(net, suspended = true, validated = validated, tmt = tmt)
+
+    // Expects the available callbacks (where the onCapabilitiesChanged must contain the
+    // VALIDATED capability), plus another onCapabilitiesChanged which is identical to the
+    // one we just sent.
+    // TODO: this is likely a bug. Fix it and remove this method.
+    fun expectAvailableDoubleValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
+        val mark = history.mark
+        expectAvailableCallbacks(net, tmt = tmt)
+        val firstCaps = history.poll(tmt, mark) { it is CapabilitiesChanged }
+        assertEquals(firstCaps, expect<CapabilitiesChanged>(net, tmt))
+    }
+
+    // Expects the available callbacks where the onCapabilitiesChanged must not have validated,
+    // then expects another onCapabilitiesChanged that has the validated bit set. This is used
+    // when a network connects and satisfies a callback, and then immediately validates.
+    fun expectAvailableThenValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
+        expectAvailableCallbacks(net, validated = false, tmt = tmt)
+        expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+    }
+
+    fun expectAvailableThenValidatedCallbacks(
+        net: Network,
+        blockedReason: Int,
+        tmt: Long = defaultTimeoutMs
+    ) {
+        expectAvailableCallbacks(
+            net,
+            validated = false,
+            suspended = false,
+            blockedReason = blockedReason,
+            tmt = tmt
+        )
+        expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+    }
+
+    // Temporary Java compat measure : have MockNetworkAgent implement this so that all existing
+    // calls with networkAgent can be routed through here without moving MockNetworkAgent.
+    // TODO: clean this up, remove this method.
+    interface HasNetwork {
+        val network: Network
+    }
+
+    @JvmOverloads
+    fun expectAvailableCallbacks(
+        n: HasNetwork,
+        suspended: Boolean,
+        validated: Boolean,
+        blocked: Boolean,
+        upstream: Network? = null,
+        timeoutMs: Long
+    ) = expectAvailableCallbacks(n.network, suspended, validated, blocked, upstream, timeoutMs)
+
+    fun expectAvailableAndSuspendedCallbacks(n: HasNetwork, expectValidated: Boolean) {
+        expectAvailableAndSuspendedCallbacks(n.network, expectValidated)
+    }
+
+    fun expectAvailableCallbacksValidated(n: HasNetwork) {
+        expectAvailableCallbacks(n.network)
+    }
+
+    fun expectAvailableCallbacksValidatedAndBlocked(n: HasNetwork) {
+        expectAvailableCallbacks(n.network, blocked = true)
+    }
+
+    fun expectAvailableCallbacksUnvalidated(n: HasNetwork) {
+        expectAvailableCallbacks(n.network, validated = false)
+    }
+
+    fun expectAvailableCallbacksUnvalidatedAndBlocked(n: HasNetwork) {
+        expectAvailableCallbacks(n.network, validated = false, blocked = true)
+    }
+
+    fun expectAvailableDoubleValidatedCallbacks(n: HasNetwork) {
+        expectAvailableDoubleValidatedCallbacks(n.network, defaultTimeoutMs)
+    }
+
+    fun expectAvailableThenValidatedCallbacks(n: HasNetwork) {
+        expectAvailableThenValidatedCallbacks(n.network, defaultTimeoutMs)
+    }
+
+    @JvmOverloads
+    fun expectCaps(
+        n: HasNetwork,
+        tmt: Long = defaultTimeoutMs,
+        valid: (NetworkCapabilities) -> Boolean = { true }
+    ) = expect<CapabilitiesChanged>(n.network, tmt) { valid(it.caps) }.caps
+
+    @JvmOverloads
+    fun expectCaps(
+        n: Network,
+        tmt: Long = defaultTimeoutMs,
+        valid: (NetworkCapabilities) -> Boolean
+    ) = expect<CapabilitiesChanged>(n, tmt) { valid(it.caps) }.caps
+
+    fun expectCaps(
+        n: HasNetwork,
+        valid: (NetworkCapabilities) -> Boolean
+    ) = expect<CapabilitiesChanged>(n.network) { valid(it.caps) }.caps
+
+    fun expectCaps(
+        tmt: Long,
+        valid: (NetworkCapabilities) -> Boolean
+    ) = expect<CapabilitiesChanged>(ANY_NETWORK, tmt) { valid(it.caps) }.caps
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
new file mode 100644
index 0000000..21bd60c
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import kotlin.test.fail
+
+class TestableNetworkOfferCallback(val timeoutMs: Long, private val noCallbackTimeoutMs: Long)
+            : NetworkProvider.NetworkOfferCallback {
+    private val TAG = this::class.simpleName
+    val history = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+    sealed class CallbackEntry {
+        data class OnNetworkNeeded(val request: NetworkRequest) : CallbackEntry()
+        data class OnNetworkUnneeded(val request: NetworkRequest) : CallbackEntry()
+    }
+
+    /**
+     * Called by the system when a network for this offer is needed to satisfy some
+     * networking request.
+     */
+    override fun onNetworkNeeded(request: NetworkRequest) {
+        Log.d(TAG, "onNetworkNeeded $request")
+        history.add(CallbackEntry.OnNetworkNeeded(request))
+    }
+
+    /**
+     * Called by the system when this offer is no longer valuable for this request.
+     */
+    override fun onNetworkUnneeded(request: NetworkRequest) {
+        Log.d(TAG, "onNetworkUnneeded $request")
+        history.add(CallbackEntry.OnNetworkUnneeded(request))
+    }
+
+    inline fun <reified T : CallbackEntry> expectCallbackThat(
+        crossinline predicate: (T) -> Boolean
+    ) {
+        val event = history.poll(timeoutMs)
+                ?: fail("Did not receive callback after ${timeoutMs}ms")
+        if (event !is T || !predicate(event)) fail("Received unexpected callback $event")
+    }
+
+    fun expectOnNetworkNeeded(capabilities: NetworkCapabilities) =
+            expectCallbackThat<CallbackEntry.OnNetworkNeeded> {
+                it.request.canBeSatisfiedBy(capabilities)
+            }
+
+    fun expectOnNetworkUnneeded(capabilities: NetworkCapabilities) =
+            expectCallbackThat<CallbackEntry.OnNetworkUnneeded> {
+                it.request.canBeSatisfiedBy(capabilities)
+            }
+
+    fun assertNoCallback() {
+        val cb = history.poll(noCallbackTimeoutMs)
+        if (null != cb) fail("Expected no callback but got $cb")
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
new file mode 100644
index 0000000..4a7b351
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.netstats.provider.NetworkStatsProvider
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val DEFAULT_TIMEOUT_MS = 200L
+const val TOKEN_ANY = -1
+
+open class TestableNetworkStatsProvider(
+    val defaultTimeoutMs: Long = DEFAULT_TIMEOUT_MS
+) : NetworkStatsProvider() {
+    sealed class CallbackType {
+        data class OnRequestStatsUpdate(val token: Int) : CallbackType()
+        data class OnSetWarningAndLimit(
+            val iface: String,
+            val warningBytes: Long,
+            val limitBytes: Long
+        ) : CallbackType()
+        data class OnSetLimit(val iface: String, val limitBytes: Long) : CallbackType() {
+            // Add getter for backward compatibility since old tests do not recognize limitBytes.
+            val quotaBytes: Long
+                get() = limitBytes
+        }
+        data class OnSetAlert(val quotaBytes: Long) : CallbackType()
+    }
+
+    private val TAG = this::class.simpleName
+    val history = ArrayTrackRecord<CallbackType>().newReadHead()
+    // See ReadHead#mark
+    val mark get() = history.mark
+
+    override fun onRequestStatsUpdate(token: Int) {
+        Log.d(TAG, "onRequestStatsUpdate $token")
+        history.add(CallbackType.OnRequestStatsUpdate(token))
+    }
+
+    override fun onSetWarningAndLimit(iface: String, warningBytes: Long, limitBytes: Long) {
+        Log.d(TAG, "onSetWarningAndLimit $iface $warningBytes $limitBytes")
+        history.add(CallbackType.OnSetWarningAndLimit(iface, warningBytes, limitBytes))
+    }
+
+    override fun onSetLimit(iface: String, quotaBytes: Long) {
+        Log.d(TAG, "onSetLimit $iface $quotaBytes")
+        history.add(CallbackType.OnSetLimit(iface, quotaBytes))
+    }
+
+    override fun onSetAlert(quotaBytes: Long) {
+        Log.d(TAG, "onSetAlert $quotaBytes")
+        history.add(CallbackType.OnSetAlert(quotaBytes))
+    }
+
+    fun expectOnRequestStatsUpdate(token: Int, timeout: Long = defaultTimeoutMs): Int {
+        val event = history.poll(timeout)
+        assertTrue(event is CallbackType.OnRequestStatsUpdate)
+        if (token != TOKEN_ANY) {
+            assertEquals(token, event.token)
+        }
+        return event.token
+    }
+
+    fun expectOnSetLimit(iface: String, quotaBytes: Long, timeout: Long = defaultTimeoutMs) {
+        assertEquals(CallbackType.OnSetLimit(iface, quotaBytes), history.poll(timeout))
+    }
+
+    fun expectOnSetAlert(quotaBytes: Long, timeout: Long = defaultTimeoutMs) {
+        assertEquals(CallbackType.OnSetAlert(quotaBytes), history.poll(timeout))
+    }
+
+    fun pollForNextCallback(timeout: Long = defaultTimeoutMs) =
+        history.poll(timeout) ?: fail("Did not receive callback after ${timeout}ms")
+
+    inline fun <reified T : CallbackType> expectCallback(
+        timeout: Long = defaultTimeoutMs,
+        predicate: (T) -> Boolean = { true }
+    ): T {
+        return pollForNextCallback(timeout).also { assertTrue(it is T && predicate(it)) } as T
+    }
+
+    // Expects a callback of the specified type matching the predicate within the timeout.
+    // Any callback that doesn't match the predicate will be skipped. Fails only if
+    // no matching callback is received within the timeout.
+    // TODO : factorize the code for this with the identical call in TestableNetworkCallback.
+    // There should be a common superclass doing this generically.
+    // TODO : have a better error message to have this fail. Right now the failure when no
+    // matching callback arrives comes from the casting to a non-nullable T.
+    // TODO : in fact, completely removing this method and have clients use
+    // history.poll(timeout, index, predicate) directly might be simpler.
+    inline fun <reified T : CallbackType> eventuallyExpect(
+        timeoutMs: Long = defaultTimeoutMs,
+        from: Int = mark,
+        crossinline predicate: (T) -> Boolean = { true }
+    ) = history.poll(timeoutMs, from) { it is T && predicate(it) } as T
+
+    fun drainCallbacks() {
+        history.mark = history.size
+    }
+
+    @JvmOverloads
+    fun assertNoCallback(timeout: Long = defaultTimeoutMs) {
+        val cb = history.poll(timeout)
+        cb?.let { fail("Expected no callback but got $cb") }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt
new file mode 100644
index 0000000..643346b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import com.android.net.module.util.ArrayTrackRecord
+import kotlin.test.assertEquals
+import kotlin.test.fail
+
+private const val DEFAULT_TIMEOUT_MS = 200L
+
+open class TestableNetworkStatsProviderBinder : NetworkStatsProviderStubCompat() {
+    sealed class CallbackType {
+        data class OnRequestStatsUpdate(val token: Int) : CallbackType()
+        data class OnSetAlert(val quotaBytes: Long) : CallbackType()
+        data class OnSetWarningAndLimit(
+            val iface: String,
+            val warningBytes: Long,
+            val limitBytes: Long
+        ) : CallbackType()
+    }
+
+    private val history = ArrayTrackRecord<CallbackType>().ReadHead()
+
+    override fun onRequestStatsUpdate(token: Int) {
+        history.add(CallbackType.OnRequestStatsUpdate(token))
+    }
+
+    override fun onSetAlert(quotaBytes: Long) {
+        history.add(CallbackType.OnSetAlert(quotaBytes))
+    }
+
+    override fun onSetWarningAndLimit(iface: String, warningBytes: Long, limitBytes: Long) {
+        history.add(CallbackType.OnSetWarningAndLimit(iface, warningBytes, limitBytes))
+    }
+
+    fun expectOnRequestStatsUpdate(token: Int) {
+        assertEquals(CallbackType.OnRequestStatsUpdate(token), history.poll(DEFAULT_TIMEOUT_MS))
+    }
+
+    fun expectOnSetWarningAndLimit(iface: String, warningBytes: Long, limitBytes: Long) {
+        assertEquals(CallbackType.OnSetWarningAndLimit(iface, warningBytes, limitBytes),
+                history.poll(DEFAULT_TIMEOUT_MS))
+    }
+
+    fun expectOnSetAlert(quotaBytes: Long) {
+        assertEquals(CallbackType.OnSetAlert(quotaBytes), history.poll(DEFAULT_TIMEOUT_MS))
+    }
+
+    @JvmOverloads
+    fun assertNoCallback(timeout: Long = DEFAULT_TIMEOUT_MS) {
+        val cb = history.poll(timeout)
+        cb?.let { fail("Expected no callback but got $cb") }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
new file mode 100644
index 0000000..5547c90
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.NetworkStats
+import com.android.net.module.util.ArrayTrackRecord
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val DEFAULT_TIMEOUT_MS = 3000L
+
+open class TestableNetworkStatsProviderCbBinder : NetworkStatsProviderCbStubCompat() {
+    sealed class CallbackType {
+        data class NotifyStatsUpdated(
+            val token: Int,
+            val ifaceStats: NetworkStats,
+            val uidStats: NetworkStats
+        ) : CallbackType()
+        object NotifyWarningReached : CallbackType()
+        object NotifyLimitReached : CallbackType()
+        object NotifyWarningOrLimitReached : CallbackType()
+        object NotifyAlertReached : CallbackType()
+        object Unregister : CallbackType()
+    }
+
+    private val history = ArrayTrackRecord<CallbackType>().ReadHead()
+
+    override fun notifyStatsUpdated(token: Int, ifaceStats: NetworkStats, uidStats: NetworkStats) {
+        history.add(CallbackType.NotifyStatsUpdated(token, ifaceStats, uidStats))
+    }
+
+    override fun notifyWarningReached() {
+        history.add(CallbackType.NotifyWarningReached)
+    }
+
+    override fun notifyLimitReached() {
+        history.add(CallbackType.NotifyLimitReached)
+    }
+
+    override fun notifyWarningOrLimitReached() {
+        // Older callback is split into notifyLimitReached and notifyWarningReached in T.
+        history.add(CallbackType.NotifyWarningOrLimitReached)
+    }
+
+    override fun notifyAlertReached() {
+        history.add(CallbackType.NotifyAlertReached)
+    }
+
+    override fun unregister() {
+        history.add(CallbackType.Unregister)
+    }
+
+    fun expectNotifyStatsUpdated() {
+        val event = history.poll(DEFAULT_TIMEOUT_MS)
+        assertTrue(event is CallbackType.NotifyStatsUpdated)
+    }
+
+    fun expectNotifyStatsUpdated(ifaceStats: NetworkStats, uidStats: NetworkStats) {
+        val event = history.poll(DEFAULT_TIMEOUT_MS)!!
+        if (event !is CallbackType.NotifyStatsUpdated) {
+            throw Exception("Expected NotifyStatsUpdated callback, but got ${event::class}")
+        }
+        // TODO: verify token.
+        assertNetworkStatsEquals(ifaceStats, event.ifaceStats)
+        assertNetworkStatsEquals(uidStats, event.uidStats)
+    }
+
+    fun expectNotifyWarningReached() =
+            assertEquals(CallbackType.NotifyWarningReached, history.poll(DEFAULT_TIMEOUT_MS))
+
+    fun expectNotifyLimitReached() =
+            assertEquals(CallbackType.NotifyLimitReached, history.poll(DEFAULT_TIMEOUT_MS))
+
+    fun expectNotifyWarningOrLimitReached() =
+            assertEquals(CallbackType.NotifyWarningOrLimitReached, history.poll(DEFAULT_TIMEOUT_MS))
+
+    fun expectNotifyAlertReached() =
+            assertEquals(CallbackType.NotifyAlertReached, history.poll(DEFAULT_TIMEOUT_MS))
+
+    // Assert there is no callback in current queue.
+    fun assertNoCallback() {
+        val cb = history.poll(0)
+        cb?.let { fail("Expected no callback but got $cb") }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java b/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
new file mode 100644
index 0000000..48b57d7
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.async;
+
+import android.os.ParcelFileDescriptor;
+import android.system.StructPollfd;
+import android.util.Log;
+
+import com.android.net.module.util.async.CircularByteBuffer;
+import com.android.net.module.util.async.OsAccess;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+public class FakeOsAccess extends OsAccess {
+    public static final boolean ENABLE_FINE_DEBUG = true;
+
+    public static final int DEFAULT_FILE_DATA_QUEUE_SIZE = 8 * 1024;
+
+    private enum FileType { PAIR, PIPE }
+
+    // Common poll() constants:
+    private static final short POLLIN  = 0x0001;
+    private static final short POLLOUT = 0x0004;
+    private static final short POLLERR = 0x0008;
+    private static final short POLLHUP = 0x0010;
+
+    private static final Constructor<FileDescriptor> FD_CONSTRUCTOR;
+    private static final Field FD_FIELD_DESCRIPTOR;
+    private static final Field PFD_FIELD_DESCRIPTOR;
+    private static final Field PFD_FIELD_GUARD;
+    private static final Method CLOSE_GUARD_METHOD_CLOSE;
+
+    private final int mReadQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+    private final int mWriteQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+    private final HashMap<Integer, File> mFiles = new HashMap<>();
+    private final byte[] mTmpBuffer = new byte[1024];
+    private final long mStartTime;
+    private final String mLogTag;
+    private int mFileNumberGen = 3;
+    private boolean mHasRateLimitedData;
+
+    public FakeOsAccess(String logTag) {
+        mLogTag = logTag;
+        mStartTime = monotonicTimeMillis();
+    }
+
+    @Override
+    public long monotonicTimeMillis() {
+        return System.nanoTime() / 1000000;
+    }
+
+    @Override
+    public FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd) {
+        try {
+            return (FileDescriptor) PFD_FIELD_DESCRIPTOR.get(fd);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void close(ParcelFileDescriptor fd) {
+        if (fd != null) {
+            close(getInnerFileDescriptor(fd));
+
+            try {
+                // Reduce CloseGuard warnings.
+                Object guard = PFD_FIELD_GUARD.get(fd);
+                CLOSE_GUARD_METHOD_CLOSE.invoke(guard);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    public synchronized void close(FileDescriptor fd) {
+        if (fd != null) {
+            File file = getFileOrNull(fd);
+            if (file != null) {
+                file.decreaseRefCount();
+                mFiles.remove(getFileDescriptorNumber(fd));
+                setFileDescriptorNumber(fd, -1);
+                notifyAll();
+            }
+        }
+    }
+
+    private File getFile(String func, FileDescriptor fd) throws IOException {
+        File file = getFileOrNull(fd);
+        if (file == null) {
+            throw newIOException(func, "Unknown file descriptor: " + getFileDebugName(fd));
+        }
+        return file;
+    }
+
+    private File getFileOrNull(FileDescriptor fd) {
+        return mFiles.get(getFileDescriptorNumber(fd));
+    }
+
+    @Override
+    public String getFileDebugName(ParcelFileDescriptor fd) {
+        return (fd != null ? getFileDebugName(getInnerFileDescriptor(fd)) : "null");
+    }
+
+    public String getFileDebugName(FileDescriptor fd) {
+        if (fd == null) {
+            return "null";
+        }
+
+        final int fdNumber = getFileDescriptorNumber(fd);
+        File file = mFiles.get(fdNumber);
+
+        StringBuilder sb = new StringBuilder();
+        if (file != null) {
+            if (file.name != null) {
+                sb.append(file.name);
+                sb.append("/");
+            }
+            sb.append(file.type);
+            sb.append("/");
+        } else {
+            sb.append("BADFD/");
+        }
+        sb.append(fdNumber);
+        return sb.toString();
+    }
+
+    public synchronized void setFileName(FileDescriptor fd, String name) {
+        File file = getFileOrNull(fd);
+        if (file != null) {
+            file.name = name;
+        }
+    }
+
+    @Override
+    public synchronized void setNonBlocking(FileDescriptor fd) throws IOException {
+        File file = getFile("fcntl", fd);
+        file.isBlocking = false;
+    }
+
+    @Override
+    public synchronized int read(FileDescriptor fd, byte[] buffer, int pos, int len)
+            throws IOException {
+        checkBoundaries("read", buffer, pos, len);
+
+        File file = getFile("read", fd);
+        if (file.readQueue == null) {
+            throw newIOException("read", "File not readable");
+        }
+        file.checkNonBlocking("read");
+
+        if (len == 0) {
+            return 0;
+        }
+
+        final int availSize = file.readQueue.size();
+        if (availSize == 0) {
+            if (file.isEndOfStream) {
+                // Java convention uses -1 to indicate end of stream.
+                return -1;
+            }
+            return 0;  // EAGAIN
+        }
+
+        final int readCount = Math.min(len, availSize);
+        file.readQueue.readBytes(buffer, pos, readCount);
+        maybeTransferData(file);
+        return readCount;
+    }
+
+    @Override
+    public synchronized int write(FileDescriptor fd, byte[] buffer, int pos, int len)
+            throws IOException {
+        checkBoundaries("write", buffer, pos, len);
+
+        File file = getFile("write", fd);
+        if (file.writeQueue == null) {
+            throw newIOException("read", "File not writable");
+        }
+        if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+            throw newIOException("write", "The other end of pipe is closed");
+        }
+        file.checkNonBlocking("write");
+
+        if (len == 0) {
+            return 0;
+        }
+
+        final int originalFreeSize = file.writeQueue.freeSize();
+        if (originalFreeSize == 0) {
+            return 0;  // EAGAIN
+        }
+
+        final int writeCount = Math.min(len, originalFreeSize);
+        file.writeQueue.writeBytes(buffer, pos, writeCount);
+        maybeTransferData(file);
+
+        if (file.writeQueue.freeSize() < originalFreeSize) {
+            final int additionalQueuedCount = originalFreeSize - file.writeQueue.freeSize();
+            Log.i(mLogTag, logStr("Delaying transfer of " + additionalQueuedCount
+                + " bytes, queued=" + file.writeQueue.size() + ", type=" + file.type
+                + ", src_red=" + file.outboundLimiter + ", dst_red=" + file.sink.inboundLimiter));
+        }
+
+        return writeCount;
+    }
+
+    private void maybeTransferData(File file) {
+        boolean hasChanges = copyFileBuffers(file, file.sink);
+        hasChanges = copyFileBuffers(file.source, file) || hasChanges;
+
+        if (hasChanges) {
+            // TODO(b/245971639): Avoid notifying if no-one is polling.
+            notifyAll();
+        }
+    }
+
+    private boolean copyFileBuffers(File src, File dst) {
+        if (src.writeQueue == null || dst.readQueue == null) {
+            return false;
+        }
+
+        final int originalCopyCount = Math.min(mTmpBuffer.length,
+            Math.min(src.writeQueue.size(), dst.readQueue.freeSize()));
+
+        final int allowedCopyCount = RateLimiter.limit(
+            src.outboundLimiter, dst.inboundLimiter, originalCopyCount);
+
+        if (allowedCopyCount < originalCopyCount) {
+            if (ENABLE_FINE_DEBUG) {
+                Log.i(mLogTag, logStr("Delaying transfer of "
+                    + (originalCopyCount - allowedCopyCount) + " bytes, original="
+                    + originalCopyCount + ", allowed=" + allowedCopyCount
+                    + ", type=" + src.type));
+            }
+            if (originalCopyCount > 0) {
+                mHasRateLimitedData = true;
+            }
+            if (allowedCopyCount == 0) {
+                return false;
+            }
+        }
+
+        boolean hasChanges = false;
+        if (allowedCopyCount > 0) {
+            if (dst.readQueue.size() == 0 || src.writeQueue.freeSize() == 0) {
+                hasChanges = true;  // Read queue had no data, or write queue was full.
+            }
+            src.writeQueue.readBytes(mTmpBuffer, 0, allowedCopyCount);
+            dst.readQueue.writeBytes(mTmpBuffer, 0, allowedCopyCount);
+        }
+
+        if (!dst.isEndOfStream && src.openCount == 0
+                && src.writeQueue.size() == 0 && dst.readQueue.size() == 0) {
+            dst.isEndOfStream = true;
+            hasChanges = true;
+        }
+
+        return hasChanges;
+    }
+
+    public void clearInboundRateLimit(FileDescriptor fd) {
+        setInboundRateLimit(fd, Integer.MAX_VALUE);
+    }
+
+    public void clearOutboundRateLimit(FileDescriptor fd) {
+        setOutboundRateLimit(fd, Integer.MAX_VALUE);
+    }
+
+    public synchronized void setInboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+        File file = getFileOrNull(fd);
+        if (file != null) {
+            file.inboundLimiter.setBytesPerSecond(bytesPerSecond);
+            maybeTransferData(file);
+        }
+    }
+
+    public synchronized void setOutboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+        File file = getFileOrNull(fd);
+        if (file != null) {
+            file.outboundLimiter.setBytesPerSecond(bytesPerSecond);
+            maybeTransferData(file);
+        }
+    }
+
+    public synchronized ParcelFileDescriptor[] socketpair() throws IOException {
+        int fdNumber1 = getNextFd("socketpair");
+        int fdNumber2 = getNextFd("socketpair");
+
+        File file1 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+        File file2 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+
+        return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+    }
+
+    @Override
+    public synchronized ParcelFileDescriptor[] pipe() throws IOException {
+        int fdNumber1 = getNextFd("pipe");
+        int fdNumber2 = getNextFd("pipe");
+
+        File file1 = new File(FileType.PIPE, mReadQueueSize, 0);
+        File file2 = new File(FileType.PIPE, 0, mWriteQueueSize);
+
+        return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+    }
+
+    private ParcelFileDescriptor[] registerFilePair(
+            int fdNumber1, File file1, int fdNumber2, File file2) {
+        file1.sink = file2;
+        file1.source = file2;
+        file2.sink = file1;
+        file2.source = file1;
+
+        mFiles.put(fdNumber1, file1);
+        mFiles.put(fdNumber2, file2);
+        return new ParcelFileDescriptor[] {
+            newParcelFileDescriptor(fdNumber1), newParcelFileDescriptor(fdNumber2)};
+    }
+
+    @Override
+    public short getPollInMask() {
+        return POLLIN;
+    }
+
+    @Override
+    public short getPollOutMask() {
+        return POLLOUT;
+    }
+
+    @Override
+    public synchronized int poll(StructPollfd[] fds, int timeoutMs) throws IOException {
+        if (timeoutMs < 0) {
+            timeoutMs = (int) TimeUnit.HOURS.toMillis(1);  // Make "infinite" equal to 1 hour.
+        }
+
+        if (fds == null || fds.length > 1000) {
+            throw newIOException("poll", "Invalid fds param");
+        }
+        for (StructPollfd pollFd : fds) {
+            getFile("poll", pollFd.fd);
+        }
+
+        int waitCallCount = 0;
+        final long deadline = monotonicTimeMillis() + timeoutMs;
+        while (true) {
+            if (mHasRateLimitedData) {
+                mHasRateLimitedData = false;
+                for (File file : mFiles.values()) {
+                    if (file.inboundLimiter.getLastRequestReduction() != 0) {
+                        copyFileBuffers(file.source, file);
+                    }
+                    if (file.outboundLimiter.getLastRequestReduction() != 0) {
+                        copyFileBuffers(file, file.sink);
+                    }
+                }
+            }
+
+            final int readyCount = calculateReadyCount(fds);
+            if (readyCount > 0) {
+                if (ENABLE_FINE_DEBUG) {
+                    Log.v(mLogTag, logStr("Poll returns " + readyCount
+                            + " after " + waitCallCount + " wait calls"));
+                }
+                return readyCount;
+            }
+
+            long remainingTimeoutMs = deadline - monotonicTimeMillis();
+            if (remainingTimeoutMs <= 0) {
+                if (ENABLE_FINE_DEBUG) {
+                    Log.v(mLogTag, logStr("Poll timeout " + timeoutMs
+                            + "ms after " + waitCallCount + " wait calls"));
+                }
+                return 0;
+            }
+
+            if (mHasRateLimitedData) {
+                remainingTimeoutMs = Math.min(RateLimiter.BUCKET_DURATION_MS, remainingTimeoutMs);
+            }
+
+            try {
+                wait(remainingTimeoutMs);
+            } catch (InterruptedException e) {
+                // Ignore and retry
+            }
+            waitCallCount++;
+        }
+    }
+
+    private int calculateReadyCount(StructPollfd[] fds) {
+        int fdCount = 0;
+        for (StructPollfd pollFd : fds) {
+            pollFd.revents = 0;
+
+            File file = getFileOrNull(pollFd.fd);
+            if (file == null) {
+                Log.w(mLogTag, logStr("Ignoring FD concurrently closed by a buggy app: "
+                        + getFileDebugName(pollFd.fd)));
+                continue;
+            }
+
+            if (ENABLE_FINE_DEBUG) {
+                Log.v(mLogTag, logStr("calculateReadyCount fd=" + getFileDebugName(pollFd.fd)
+                        + ", events=" + pollFd.events + ", eof=" + file.isEndOfStream
+                        + ", r=" + (file.readQueue != null ? file.readQueue.size() : -1)
+                        + ", w=" + (file.writeQueue != null ? file.writeQueue.freeSize() : -1)));
+            }
+
+            if ((pollFd.events & POLLIN) != 0) {
+                if (file.readQueue != null && file.readQueue.size() != 0) {
+                    pollFd.revents |= POLLIN;
+                }
+                if (file.isEndOfStream) {
+                    pollFd.revents |= POLLHUP;
+                }
+            }
+
+            if ((pollFd.events & POLLOUT) != 0) {
+                if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+                    pollFd.revents |= POLLERR;
+                }
+                if (file.writeQueue != null && file.writeQueue.freeSize() != 0) {
+                    pollFd.revents |= POLLOUT;
+                }
+            }
+
+            if (pollFd.revents != 0) {
+                fdCount++;
+            }
+        }
+        return fdCount;
+    }
+
+    private int getNextFd(String func) throws IOException {
+        if (mFileNumberGen > 100000) {
+            throw newIOException(func, "Too many files open");
+        }
+
+        return mFileNumberGen++;
+    }
+
+    private static IOException newIOException(String func, String message) {
+        return new IOException(message + ", func=" + func);
+    }
+
+    public static void checkBoundaries(String func, byte[] buffer, int pos, int len)
+            throws IOException {
+        if (((buffer.length | pos | len) < 0 || pos > buffer.length - len)) {
+            throw newIOException(func, "Invalid array bounds");
+        }
+    }
+
+    private ParcelFileDescriptor newParcelFileDescriptor(int fdNumber) {
+        try {
+            return new ParcelFileDescriptor(newFileDescriptor(fdNumber));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private FileDescriptor newFileDescriptor(int fdNumber) {
+        try {
+            return FD_CONSTRUCTOR.newInstance(Integer.valueOf(fdNumber));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public int getFileDescriptorNumber(FileDescriptor fd) {
+        try {
+            return (Integer) FD_FIELD_DESCRIPTOR.get(fd);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void setFileDescriptorNumber(FileDescriptor fd, int fdNumber) {
+        try {
+            FD_FIELD_DESCRIPTOR.set(fd, Integer.valueOf(fdNumber));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private String logStr(String message) {
+        return "[FakeOs " + (monotonicTimeMillis() - mStartTime) + "] " + message;
+    }
+
+    private class File {
+        final FileType type;
+        final CircularByteBuffer readQueue;
+        final CircularByteBuffer writeQueue;
+        final RateLimiter inboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+        final RateLimiter outboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+        String name;
+        int openCount = 1;
+        boolean isBlocking = true;
+        File sink;
+        File source;
+        boolean isEndOfStream;
+
+        File(FileType type, int readQueueSize, int writeQueueSize) {
+            this.type = type;
+            readQueue = (readQueueSize > 0 ? new CircularByteBuffer(readQueueSize) : null);
+            writeQueue = (writeQueueSize > 0 ? new CircularByteBuffer(writeQueueSize) : null);
+        }
+
+        void decreaseRefCount() {
+            if (openCount <= 0) {
+                throw new IllegalStateException();
+            }
+            openCount--;
+        }
+
+        void checkNonBlocking(String func) throws IOException {
+            if (isBlocking) {
+                throw newIOException(func, "File in blocking mode");
+            }
+        }
+    }
+
+    static {
+        try {
+            FD_CONSTRUCTOR = FileDescriptor.class.getDeclaredConstructor(int.class);
+            FD_CONSTRUCTOR.setAccessible(true);
+
+            Field descriptorIntField;
+            try {
+                descriptorIntField = FileDescriptor.class.getDeclaredField("descriptor");
+            } catch (NoSuchFieldException e) {
+                descriptorIntField = FileDescriptor.class.getDeclaredField("fd");
+            }
+            FD_FIELD_DESCRIPTOR = descriptorIntField;
+            FD_FIELD_DESCRIPTOR.setAccessible(true);
+
+            PFD_FIELD_DESCRIPTOR = ParcelFileDescriptor.class.getDeclaredField("mFd");
+            PFD_FIELD_DESCRIPTOR.setAccessible(true);
+
+            PFD_FIELD_GUARD = ParcelFileDescriptor.class.getDeclaredField("mGuard");
+            PFD_FIELD_GUARD.setAccessible(true);
+
+            CLOSE_GUARD_METHOD_CLOSE = Class.forName("dalvik.system.CloseGuard")
+                .getDeclaredMethod("close");
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java b/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java
new file mode 100644
index 0000000..d5cca0a
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.async;
+
+import com.android.net.module.util.async.OsAccess;
+
+import java.util.Arrays;
+
+/**
+ * Limits the number of bytes processed to the given maximum of bytes per second.
+ *
+ * The limiter tracks the total for the past second, along with sums for each 10ms
+ * in the past second, allowing the total to be adjusted as the time passes.
+ */
+public final class RateLimiter {
+    private static final int PERIOD_DURATION_MS = 1000;
+    private static final int BUCKET_COUNT = 100;
+
+    public static final int BUCKET_DURATION_MS = PERIOD_DURATION_MS / BUCKET_COUNT;
+
+    private final OsAccess mOsAccess;
+    private final int[] mStatBuckets = new int[BUCKET_COUNT];
+    private int mMaxPerPeriodBytes;
+    private int mMaxPerBucketBytes;
+    private int mRecordedPeriodBytes;
+    private long mLastLimitTimestamp;
+    private int mLastRequestReduction;
+
+    public RateLimiter(OsAccess osAccess, int bytesPerSecond) {
+        mOsAccess = osAccess;
+        setBytesPerSecond(bytesPerSecond);
+        clear();
+    }
+
+    public int getBytesPerSecond() {
+        return mMaxPerPeriodBytes;
+    }
+
+    public void setBytesPerSecond(int bytesPerSecond) {
+        mMaxPerPeriodBytes = bytesPerSecond;
+        mMaxPerBucketBytes = Math.max(1, (mMaxPerPeriodBytes / BUCKET_COUNT) * 2);
+    }
+
+    public void clear() {
+        mLastLimitTimestamp = mOsAccess.monotonicTimeMillis();
+        mRecordedPeriodBytes = 0;
+        Arrays.fill(mStatBuckets, 0);
+    }
+
+    public static int limit(RateLimiter limiter1, RateLimiter limiter2, int requestedBytes) {
+        final long now = limiter1.mOsAccess.monotonicTimeMillis();
+        final int allowedCount = Math.min(limiter1.calculateLimit(now, requestedBytes),
+            limiter2.calculateLimit(now, requestedBytes));
+        limiter1.recordBytes(now, requestedBytes, allowedCount);
+        limiter2.recordBytes(now, requestedBytes, allowedCount);
+        return allowedCount;
+    }
+
+    public int limit(int requestedBytes) {
+        final long now = mOsAccess.monotonicTimeMillis();
+        final int allowedCount = calculateLimit(now, requestedBytes);
+        recordBytes(now, requestedBytes, allowedCount);
+        return allowedCount;
+    }
+
+    public int getLastRequestReduction() {
+        return mLastRequestReduction;
+    }
+
+    public boolean acceptAllOrNone(int requestedBytes) {
+        final long now = mOsAccess.monotonicTimeMillis();
+        final int allowedCount = calculateLimit(now, requestedBytes);
+        if (allowedCount < requestedBytes) {
+            return false;
+        }
+        recordBytes(now, requestedBytes, allowedCount);
+        return true;
+    }
+
+    private int calculateLimit(long now, int requestedBytes) {
+        // First remove all stale bucket data and adjust the total.
+        final long currentBucketAbsIdx = now / BUCKET_DURATION_MS;
+        final long staleCutoffIdx = currentBucketAbsIdx - BUCKET_COUNT;
+        for (long i = mLastLimitTimestamp / BUCKET_DURATION_MS; i < staleCutoffIdx; i++) {
+            final int idx = (int) (i % BUCKET_COUNT);
+            mRecordedPeriodBytes -= mStatBuckets[idx];
+            mStatBuckets[idx] = 0;
+        }
+
+        final int bucketIdx = (int) (currentBucketAbsIdx % BUCKET_COUNT);
+        final int maxAllowed = Math.min(mMaxPerPeriodBytes - mRecordedPeriodBytes,
+            Math.min(mMaxPerBucketBytes - mStatBuckets[bucketIdx], requestedBytes));
+        return Math.max(0, maxAllowed);
+    }
+
+    private void recordBytes(long now, int requestedBytes, int actualBytes) {
+        mStatBuckets[(int) ((now / BUCKET_DURATION_MS) % BUCKET_COUNT)] += actualBytes;
+        mRecordedPeriodBytes += actualBytes;
+        mLastRequestReduction = requestedBytes - actualBytes;
+        mLastLimitTimestamp = now;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("{max=");
+        sb.append(mMaxPerPeriodBytes);
+        sb.append(",max_bucket=");
+        sb.append(mMaxPerBucketBytes);
+        sb.append(",total=");
+        sb.append(mRecordedPeriodBytes);
+        sb.append(",last_red=");
+        sb.append(mLastRequestReduction);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java b/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java
new file mode 100644
index 0000000..4bf5527
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.async;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+
+public class ReadableDataAnswer implements Answer {
+    private final ArrayList<byte[]> mBuffers = new ArrayList<>();
+    private int mBufferPos;
+
+    public ReadableDataAnswer(byte[] ... buffers) {
+        for (byte[] buffer : buffers) {
+            addBuffer(buffer);
+        }
+    }
+
+    public void addBuffer(byte[] buffer) {
+        if (buffer.length != 0) {
+            mBuffers.add(buffer);
+        }
+    }
+
+    public int getRemainingSize() {
+        int totalSize = 0;
+        for (byte[] buffer : mBuffers) {
+            totalSize += buffer.length;
+        }
+        return totalSize - mBufferPos;
+    }
+
+    private void cleanupBuffers() {
+        if (!mBuffers.isEmpty() && mBufferPos == mBuffers.get(0).length) {
+            mBuffers.remove(0);
+            mBufferPos = 0;
+        }
+    }
+
+    @Override
+    public Object answer(InvocationOnMock invocation) throws Throwable {
+        cleanupBuffers();
+
+        if (mBuffers.isEmpty()) {
+            return Integer.valueOf(0);
+        }
+
+        byte[] src = mBuffers.get(0);
+
+        byte[] dst = invocation.<byte[]>getArgument(0);
+        int dstPos = invocation.<Integer>getArgument(1);
+        int dstLen = invocation.<Integer>getArgument(2);
+
+        int copyLen = Math.min(dstLen, src.length - mBufferPos);
+        System.arraycopy(src, mBufferPos, dst, dstPos, copyLen);
+        mBufferPos += copyLen;
+
+        cleanupBuffers();
+        return Integer.valueOf(copyLen);
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk30.kt b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk30.kt
new file mode 100644
index 0000000..843c41e
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk30.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.filters
+
+/**
+ * Only run this test in the CtsNetTestCasesMaxTargetSdk30 suite.
+ */
+annotation class CtsNetTestCasesMaxTargetSdk30(val reason: String)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk31.kt b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk31.kt
new file mode 100644
index 0000000..be0103d
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk31.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.filters
+
+/**
+ * Only run this test in the CtsNetTestCasesMaxTargetSdk31 suite.
+ */
+annotation class CtsNetTestCasesMaxTargetSdk31(val reason: String)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt
new file mode 100644
index 0000000..5af890f
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.filters
+
+/**
+ * Only run this test in the CtsNetTestCasesMaxTargetSdk33 suite.
+ */
+annotation class CtsNetTestCasesMaxTargetSdk33(val reason: String)
diff --git a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
new file mode 100644
index 0000000..435fdd8
--- /dev/null
+++ b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import com.android.ddmlib.testrunner.TestResult
+import com.android.tradefed.config.Option
+import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.result.CollectingTestListener
+import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner
+import com.android.tradefed.targetprep.BaseTargetPreparer
+import com.android.tradefed.targetprep.TargetSetupError
+import com.android.tradefed.targetprep.suite.SuiteApkInstaller
+
+private const val CONNECTIVITY_CHECKER_APK = "ConnectivityTestPreparer.apk"
+private const val CONNECTIVITY_PKG_NAME = "com.android.testutils.connectivitypreparer"
+private const val CONNECTIVITY_CHECK_CLASS = "$CONNECTIVITY_PKG_NAME.ConnectivityCheckTest"
+
+// As per the <instrumentation> defined in the checker manifest
+private const val CONNECTIVITY_CHECK_RUNNER_NAME = "androidx.test.runner.AndroidJUnitRunner"
+private const val IGNORE_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
+private val UPDATER_PKGS = arrayOf("com.google.android.gms", "com.android.vending")
+
+/**
+ * A target preparer that sets up and verifies a device for connectivity tests.
+ *
+ * For quick and dirty local testing, the connectivity check can be disabled by running tests with
+ * "atest -- \
+ * --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-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_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>()
+
+    override fun setUp(testInfo: TestInformation) {
+        if (isDisabled) return
+        disableGmsUpdate(testInfo)
+        originalTestChainEnabled = getTestChainEnabled(testInfo)
+        originalUpdaterPkgsStatus.putAll(getUpdaterPkgsStatus(testInfo))
+        setUpdaterNetworkingEnabled(
+            testInfo,
+            enableChain = true,
+            enablePkgs = UPDATER_PKGS.associateWith { false }
+        )
+        runConnectivityCheckApk(testInfo)
+        refreshTime(testInfo)
+    }
+
+    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
+        )
+        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
+            )
+        }
+
+        val runResult = receiver.currentRunResults
+        if (runResult.isRunFailure) {
+            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) {
+                null
+            } else {
+                "$testDescription: ${testResult.stackTrace}"
+            }
+        }.joinToString("\n")
+        if (errorMsg.isBlank()) return
+
+        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 " +
+                "-a com.google.gservices.intent.action.GSERVICES_OVERRIDE " +
+                "-e finsky.play_services_auto_update_enabled false"
+        )
+    }
+
+    private fun clearGmsUpdateOverride(testInfo: TestInformation) {
+        testInfo.exec(
+            "su 0 am broadcast " +
+                "-a com.google.gservices.intent.action.GSERVICES_OVERRIDE " +
+                "--esn finsky.play_services_auto_update_enabled"
+        )
+    }
+
+    private fun setUpdaterNetworkingEnabled(
+            testInfo: TestInformation,
+            enableChain: Boolean,
+            enablePkgs: Map<String, Boolean>
+    ) {
+        // Build.VERSION_CODES.S = 31 where this is not available, then do nothing.
+        if (testInfo.device.getApiLevel() < 31) return
+        testInfo.exec("cmd connectivity set-chain3-enabled $enableChain")
+        enablePkgs.forEach { (pkg, allow) ->
+            testInfo.exec("cmd connectivity set-package-networking-enabled $allow $pkg")
+        }
+    }
+
+    private fun getTestChainEnabled(testInfo: TestInformation) =
+            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")
+        }
+
+    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
+        )
+        clearGmsUpdateOverride(testInfo)
+    }
+}
diff --git a/staticlibs/testutils/host/java/com/android/testutils/DisableConfigSyncTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/DisableConfigSyncTargetPreparer.kt
new file mode 100644
index 0000000..bc00f3c
--- /dev/null
+++ b/staticlibs/testutils/host/java/com/android/testutils/DisableConfigSyncTargetPreparer.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.targetprep.BaseTargetPreparer
+
+/**
+ * A target preparer that disables DeviceConfig sync while running a test.
+ *
+ * Without this preparer, tests that rely on stable values of DeviceConfig flags, for example to
+ * test behavior when setting the flag and resetting it afterwards, may flake as the flags may
+ * be synced with remote servers during the test.
+ */
+class DisableConfigSyncTargetPreparer : BaseTargetPreparer() {
+    private var syncDisabledOriginalValue = "none"
+
+    override fun setUp(testInfo: TestInformation) {
+        if (isDisabled) return
+        syncDisabledOriginalValue = readSyncDisabledOriginalValue(testInfo)
+
+        // The setter is the same in current and legacy S versions
+        testInfo.exec("cmd device_config set_sync_disabled_for_tests until_reboot")
+    }
+
+    override fun tearDown(testInfo: TestInformation, e: Throwable?) {
+        if (isTearDownDisabled) return
+        // May fail harmlessly if called before S
+        testInfo.exec("cmd device_config set_sync_disabled_for_tests $syncDisabledOriginalValue")
+    }
+
+    private fun readSyncDisabledOriginalValue(testInfo: TestInformation): String {
+        return when (val reply = testInfo.exec("cmd device_config get_sync_disabled_for_tests")) {
+            "until_reboot", "persistent", "none" -> reply
+            // Reply does not match known modes, try legacy commands used on S and some T builds
+            else -> when (testInfo.exec("cmd device_config is_sync_disabled_for_tests")) {
+                // The legacy command just said "true" for "until_reboot" or "persistent". There is
+                // no way to know which one was used, so just reset to "until_reboot" to be
+                // conservative.
+                "true" -> "until_reboot"
+                else -> "none"
+            }
+        }
+    }
+}
+
+fun TestInformation.exec(cmd: String) = this.device.executeShellCommand(cmd)
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_utils.py b/staticlibs/testutils/host/python/apf_utils.py
new file mode 100644
index 0000000..415799c
--- /dev/null
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -0,0 +1,256 @@
+#  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
+
+
+# Constants.
+ETHER_BROADCAST = "FFFFFFFFFFFF"
+ETH_P_ETHERCAT = "88A4"
+
+
+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 send_broadcast_empty_ethercat_packet(
+    ad: android_device.AndroidDevice, iface_name: str
+) -> None:
+  """Transmits a broadcast empty EtherCat packet on the specified interface."""
+
+  # Get the interface's MAC address.
+  mac_address = get_hardware_address(ad, iface_name)
+
+  # TODO: Build packet by using scapy library.
+  # Ethernet header (14 bytes).
+  packet = ETHER_BROADCAST  # Destination MAC (broadcast)
+  packet += mac_address.replace(":", "")  # Source MAC
+  packet += ETH_P_ETHERCAT  # EtherType (EtherCAT)
+
+  # EtherCAT header (2 bytes) + 44 bytes of zero padding.
+  packet += "00" * 46
+
+  # Send the packet using a raw socket.
+  send_raw_packet_downstream(ad, iface_name, packet)
+
+
+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/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/net/module/util/TrackRecord.kt b/staticlibs/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt
new file mode 100644
index 0000000..f24e4f1
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.locks.Condition
+import java.util.concurrent.locks.ReentrantLock
+import java.util.concurrent.locks.StampedLock
+import kotlin.concurrent.withLock
+
+/**
+ * A List that additionally offers the ability to append via the add() method, and to retrieve
+ * an element by its index optionally waiting for it to become available.
+ */
+interface TrackRecord<E> : List<E> {
+    /**
+     * Adds an element to this queue, waking up threads waiting for one. Returns true, as
+     * per the contract for List.
+     */
+    fun add(e: E): Boolean
+
+    /**
+     * Returns the first element after {@param pos}, possibly blocking until one is available, or
+     * null if no such element can be found within the timeout.
+     * If a predicate is given, only elements matching the predicate are returned.
+     *
+     * @param timeoutMs how long, in milliseconds, to wait at most (best effort approximation).
+     * @param pos the position at which to start polling.
+     * @param predicate an optional predicate to filter elements to be returned.
+     * @return an element matching the predicate, or null if timeout.
+     */
+    fun poll(timeoutMs: Long, pos: Int, predicate: (E) -> Boolean = { true }): E?
+}
+
+/**
+ * A thread-safe implementation of TrackRecord that is backed by an ArrayList.
+ *
+ * This class also supports the creation of a read-head for easier single-thread access.
+ * Refer to the documentation of {@link ArrayTrackRecord.ReadHead}.
+ */
+class ArrayTrackRecord<E> : TrackRecord<E> {
+    private val lock = ReentrantLock()
+    private val condition = lock.newCondition()
+    // Backing store. This stores the elements in this ArrayTrackRecord.
+    private val elements = ArrayList<E>()
+
+    // The list iterator for RecordingQueue iterates over a snapshot of the collection at the
+    // time the operator is created. Because TrackRecord is only ever mutated by appending,
+    // that makes this iterator thread-safe as it sees an effectively immutable List.
+    class ArrayTrackRecordIterator<E>(
+        private val list: ArrayList<E>,
+        start: Int,
+        private val end: Int
+    ) : ListIterator<E> {
+        var index = start
+        override fun hasNext() = index < end
+        override fun next() = list[index++]
+        override fun hasPrevious() = index > 0
+        override fun nextIndex() = index + 1
+        override fun previous() = list[--index]
+        override fun previousIndex() = index - 1
+    }
+
+    // List<E> implementation
+    override val size get() = lock.withLock { elements.size }
+    override fun contains(element: E) = lock.withLock { elements.contains(element) }
+    override fun containsAll(elements: Collection<E>) = lock.withLock {
+        this.elements.containsAll(elements)
+    }
+    override operator fun get(index: Int) = lock.withLock { elements[index] }
+    override fun indexOf(element: E): Int = lock.withLock { elements.indexOf(element) }
+    override fun lastIndexOf(element: E): Int = lock.withLock { elements.lastIndexOf(element) }
+    override fun isEmpty() = lock.withLock { elements.isEmpty() }
+    override fun listIterator(index: Int) = ArrayTrackRecordIterator(elements, index, size)
+    override fun listIterator() = listIterator(0)
+    override fun iterator() = listIterator()
+    override fun subList(fromIndex: Int, toIndex: Int): List<E> = lock.withLock {
+        elements.subList(fromIndex, toIndex)
+    }
+
+    // TrackRecord<E> implementation
+    override fun add(e: E): Boolean {
+        lock.withLock {
+            elements.add(e)
+            condition.signalAll()
+        }
+        return true
+    }
+    override fun poll(timeoutMs: Long, pos: Int, predicate: (E) -> Boolean) = lock.withLock {
+        elements.getOrNull(pollForIndexReadLocked(timeoutMs, pos, predicate))
+    }
+
+    // For convenience
+    fun getOrNull(pos: Int, predicate: (E) -> Boolean) = lock.withLock {
+        if (pos < 0 || pos > size) null else elements.subList(pos, size).find(predicate)
+    }
+
+    // Returns the index of the next element whose position is >= pos matching the predicate, if
+    // necessary waiting until such a time that such an element is available, with a timeout.
+    // If no such element is found within the timeout -1 is returned.
+    private fun pollForIndexReadLocked(timeoutMs: Long, pos: Int, predicate: (E) -> Boolean): Int {
+        val deadline = System.currentTimeMillis() + timeoutMs
+        var index = pos
+        do {
+            while (index < elements.size) {
+                if (predicate(elements[index])) return index
+                ++index
+            }
+        } while (condition.await(deadline - System.currentTimeMillis()))
+        return -1
+    }
+
+    /**
+     * Returns a ReadHead over this ArrayTrackRecord. The returned ReadHead is tied to the
+     * current thread.
+     */
+    fun newReadHead() = ReadHead()
+
+    /**
+     * ReadHead is an object that helps users of ArrayTrackRecord keep track of how far
+     * it has read this far in the ArrayTrackRecord. A ReadHead is always associated with
+     * a single instance of ArrayTrackRecord. Multiple ReadHeads can be created and used
+     * on the same instance of ArrayTrackRecord concurrently, and the ArrayTrackRecord
+     * instance can also be used concurrently. ReadHead maintains the current index that is
+     * the next to be read, and calls this the "mark".
+     *
+     * In a ReadHead, {@link poll(Long, (E) -> Boolean)} works similarly to a LinkedBlockingQueue.
+     * It can be called repeatedly and will return the elements as they arrive.
+     *
+     * Intended usage looks something like this :
+     * val TrackRecord<MyObject> record = ArrayTrackRecord().newReadHead()
+     * Thread().start {
+     *   // do stuff
+     *   record.add(something)
+     *   // do stuff
+     * }
+     *
+     * val obj1 = record.poll(timeout)
+     * // do something with obj1
+     * val obj2 = record.poll(timeout)
+     * // do something with obj2
+     *
+     * The point is that the caller does not have to track the mark like it would have to if
+     * it was using ArrayTrackRecord directly.
+     *
+     * Thread safety :
+     * A ReadHead delegates all TrackRecord methods to its associated ArrayTrackRecord, and
+     * inherits its thread-safe properties for all the TrackRecord methods.
+     *
+     * Poll() operates under its own set of rules that only allow execution on multiple threads
+     * within constrained boundaries, and never concurrently or pseudo-concurrently. This is
+     * because concurrent calls to poll() fundamentally do not make sense. poll() will move
+     * the mark according to what events remained to be read by this read head, and therefore
+     * if multiple threads were calling poll() concurrently on the same ReadHead, what
+     * happens to the mark and the return values could not be useful because there is no way to
+     * provide either a guarantee not to skip objects nor a guarantee about the mark position at
+     * the exit of poll(). This is even more true in the presence of a predicate to filter
+     * returned elements, because one thread might be filtering out the events the other is
+     * interested in. For this reason, this class will fail-fast if any concurrent access is
+     * detected with ConcurrentAccessException.
+     * It is possible to use poll() on different threads as long as the following can be
+     * guaranteed : one thread must call poll() for the last time, then execute a write barrier,
+     * then the other thread must execute a read barrier before calling poll() for the first time.
+     * This allows in particular to call poll in @Before and @After methods in JUnit unit tests,
+     * because JUnit will enforce those barriers by creating the testing thread after executing
+     * @Before and joining the thread after executing @After.
+     *
+     * peek() can be used by multiple threads concurrently, but only if no thread is calling
+     * poll() outside of the boundaries above. For simplicity, it can be considered that peek()
+     * is safe to call only when poll() is safe to call.
+     *
+     * Polling concurrently from the same ArrayTrackRecord is supported by creating multiple
+     * ReadHeads on the same instance of ArrayTrackRecord (or of course by using ArrayTrackRecord
+     * directly). Each ReadHead is then guaranteed to see all events always and
+     * guarantees are made on the value of the mark upon return. {@see poll(Long, (E) -> Boolean)}
+     * for details. Be careful to create each ReadHead on the thread it is meant to be used on, or
+     * to have a clear synchronization point between creation and use.
+     *
+     * Users of a ReadHead can ask for the current position of the mark at any time, on a thread
+     * where it's safe to call peek(). This mark can be used later to replay the history of events
+     * either on this ReadHead, on the associated ArrayTrackRecord or on another ReadHead
+     * associated with the same ArrayTrackRecord. It might look like this in the reader thread :
+     *
+     * val markAtStart = record.mark
+     * // Start processing interesting events
+     * while (val element = record.poll(timeout) { it.isInteresting() }) {
+     *   // Do something with element
+     * }
+     * // Look for stuff that happened while searching for interesting events
+     * val firstElementReceived = record.getOrNull(markAtStart)
+     * val firstSpecialElement = record.getOrNull(markAtStart) { it.isSpecial() }
+     * // Get the first special element since markAtStart, possibly blocking until one is available
+     * val specialElement = record.poll(timeout, markAtStart) { it.isSpecial() }
+     */
+    inner class ReadHead : TrackRecord<E> by this@ArrayTrackRecord {
+        // This lock only controls access to the readHead member below. The ArrayTrackRecord
+        // object has its own synchronization following different (and more usual) semantics.
+        // See the comment on the ReadHead class for details.
+        private val slock = StampedLock()
+        private var readHead = 0
+
+        // A special mark used to track the start of the last poll() operation.
+        private var pollMark = 0
+
+        /**
+         * @return the current value of the mark.
+         */
+        var mark
+            get() = checkThread { readHead }
+            set(v: Int) = rewind(v)
+        fun rewind(v: Int) {
+            val stamp = slock.tryWriteLock()
+            if (0L == stamp) concurrentAccessDetected()
+            readHead = v
+            pollMark = v
+            slock.unlockWrite(stamp)
+        }
+
+        private fun <T> checkThread(r: (Long) -> T): T {
+            // tryOptimisticRead is a read barrier, guarantees writes from other threads are visible
+            // after it
+            val stamp = slock.tryOptimisticRead()
+            val result = r(stamp)
+            // validate also performs a read barrier, guaranteeing that if validate returns true,
+            // then any change either happens-before tryOptimisticRead, or happens-after validate.
+            if (!slock.validate(stamp)) concurrentAccessDetected()
+            return result
+        }
+
+        private fun concurrentAccessDetected(): Nothing {
+            throw ConcurrentModificationException(
+                    "ReadHeads can't be used concurrently. Check your threading model.")
+        }
+
+        /**
+         * Returns the first element after the mark, optionally blocking until one is available, or
+         * null if no such element can be found within the timeout.
+         * If a predicate is given, only elements matching the predicate are returned.
+         *
+         * Upon return the mark will be set to immediately after the returned element, or after
+         * the last element in the queue if null is returned. This means this method will always
+         * skip elements that do not match the predicate, even if it returns null.
+         *
+         * This method can only be used by the thread that created this ManagedRecordingQueue.
+         * If used on another thread, this throws IllegalStateException.
+         *
+         * @param timeoutMs how long, in milliseconds, to wait at most (best effort approximation).
+         * @param predicate an optional predicate to filter elements to be returned.
+         * @return an element matching the predicate, or null if timeout.
+         */
+        fun poll(timeoutMs: Long, predicate: (E) -> Boolean = { true }): E? {
+            val stamp = slock.tryWriteLock()
+            if (0L == stamp) concurrentAccessDetected()
+            pollMark = readHead
+            try {
+                lock.withLock {
+                    val index = pollForIndexReadLocked(timeoutMs, readHead, predicate)
+                    readHead = if (index < 0) size else index + 1
+                    return getOrNull(index)
+                }
+            } finally {
+                slock.unlockWrite(stamp)
+            }
+        }
+
+        /**
+         * Returns a list of events that were observed since the last time poll() was called on this
+         * ReadHead.
+         *
+         * @return list of events since poll() was called.
+         */
+        fun backtrace(): List<E> {
+            val stamp = slock.tryReadLock()
+            if (0L == stamp) concurrentAccessDetected()
+
+            try {
+                lock.withLock {
+                    return ArrayList(subList(pollMark, mark))
+                }
+            } finally {
+                slock.unlockRead(stamp)
+            }
+        }
+
+        /**
+         * Returns the first element after the mark or null. This never blocks.
+         *
+         * This method is subject to threading restrictions. It can be used concurrently on
+         * multiple threads but not if any other thread might be executing poll() at the same
+         * time. See the class comment for details.
+         */
+        fun peek(): E? = checkThread { getOrNull(readHead) }
+    }
+}
+
+// Private helper
+private fun Condition.await(timeoutMs: Long) = this.await(timeoutMs, TimeUnit.MILLISECONDS)
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
new file mode 100644
index 0000000..9f28234
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("Cleanup")
+
+package com.android.testutils
+
+import com.android.testutils.FunctionalUtils.ThrowingRunnable
+import com.android.testutils.FunctionalUtils.ThrowingSupplier
+import javax.annotation.CheckReturnValue
+
+/**
+ * Utility to do cleanup in tests without replacing exceptions with those from a finally block.
+ *
+ * This utility is meant for tests that want to do cleanup after they execute their test
+ * logic, whether the test fails (and throws) or not.
+ *
+ * The usual way of doing this is to have a try{}finally{} block and put cleanup in finally{}.
+ * However, if any code in finally{} throws, the exception thrown in finally{} is thrown before
+ * any thrown in try{} ; that means errors reported from tests are from finally{} even if they
+ * have been caused by errors in try{}. This is unhelpful in tests, because it results in a
+ * stacktrace for a symptom rather than a stacktrace for a cause.
+ *
+ * To alleviate this, tests are encouraged to make sure the code in finally{} can't throw, or
+ * that the code in try{} can't cause it to fail. This is not always realistic ; not only does
+ * it require the developer thinks about complex interactions of code, test code often relies
+ * on bricks provided by other teams, not controlled by the team writing the test, which may
+ * start throwing with an update (see b/198998862 for an example).
+ *
+ * This utility allows a different approach : it offers a new construct, tryTest{}cleanup{} similar
+ * to try{}finally{}, but that will always throw the first exception that happens. In other words,
+ * if only tryTest{} throws or only cleanup{} throws, that exception will be thrown, but contrary
+ * to the standard try{}finally{}, if both throws, the construct throws the exception that happened
+ * in tryTest{} rather than the one that happened in cleanup{}.
+ *
+ * Kotlin usage is as try{}finally{}, but with multiple finally{} blocks :
+ * tryTest {
+ *   testing code
+ * } cleanupStep {
+ *   cleanup code 1
+ * } cleanupStep {
+ *   cleanup code 2
+ * } cleanup {
+ *   cleanup code 3
+ * }
+ * Catch blocks can be added with the following syntax :
+ * tryTest {
+ *   testing code
+ * }.catch<ExceptionType> { it ->
+ *   do something to it
+ * }
+ *
+ * Java doesn't allow this kind of syntax, so instead a function taking lambdas is provided.
+ * testAndCleanup(() -> {
+ *   testing code
+ * }, () -> {
+ *   cleanup code 1
+ * }, () -> {
+ *   cleanup code 2
+ * });
+ */
+
+@CheckReturnValue
+fun <T> tryTest(block: () -> T) = TryExpr(
+        try {
+            Result.success(block())
+        } catch (e: Throwable) {
+            Result.failure(e)
+        })
+
+// Some downstream branches have an older kotlin that doesn't know about value classes.
+// TODO : Change this to "value class" when aosp no longer merges into such branches.
+@Suppress("INLINE_CLASS_DEPRECATED")
+inline class TryExpr<T>(val result: Result<T>) {
+    inline infix fun <reified E : Throwable> catch(block: (E) -> T): TryExpr<T> {
+        val originalException = result.exceptionOrNull()
+        if (originalException !is E) return this
+        return TryExpr(try {
+            Result.success(block(originalException))
+        } catch (e: Throwable) {
+            Result.failure(e)
+        })
+    }
+
+    @CheckReturnValue
+    inline infix fun cleanupStep(block: () -> Unit): TryExpr<T> {
+        try {
+            block()
+        } catch (e: Throwable) {
+            val originalException = result.exceptionOrNull()
+            return TryExpr(if (null == originalException) {
+                Result.failure(e)
+            } else {
+                originalException.addSuppressed(e)
+                Result.failure(originalException)
+            })
+        }
+        return this
+    }
+
+    inline infix fun cleanup(block: () -> Unit): T = cleanupStep(block).result.getOrThrow()
+}
+
+// Java support
+fun <T> testAndCleanup(tryBlock: ThrowingSupplier<T>, vararg cleanupBlock: ThrowingRunnable): T {
+    return cleanupBlock.fold(tryTest { tryBlock.get() }) { previousExpr, nextCleanup ->
+        previousExpr.cleanupStep { nextCleanup.run() }
+    }.cleanup {}
+}
+fun testAndCleanup(tryBlock: ThrowingRunnable, vararg cleanupBlock: ThrowingRunnable) {
+    return testAndCleanup(ThrowingSupplier { tryBlock.run() }, *cleanupBlock)
+}
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt b/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
new file mode 100644
index 0000000..c6e5f25
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("ConcurrentUtils")
+
+package com.android.testutils
+
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.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/staticlibs/testutils/hostdevice/com/android/testutils/ConnectivityModuleTest.kt b/staticlibs/testutils/hostdevice/com/android/testutils/ConnectivityModuleTest.kt
new file mode 100644
index 0000000..ec485fe
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/ConnectivityModuleTest.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+/**
+ * Indicates that the test covers functionality that was rolled out in a connectivity module update.
+ *
+ * Annotated MTS tests will typically only be run in Connectivity/Tethering module MTS, and not when
+ * only other modules (such as NetworkStack) have been updated.
+ * Annotated CTS tests will always be run, as the Connectivity module should be at least newer than
+ * the CTS suite.
+ */
+annotation class ConnectivityModuleTest
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/DnsResolverModuleTest.kt b/staticlibs/testutils/hostdevice/com/android/testutils/DnsResolverModuleTest.kt
new file mode 100644
index 0000000..9e97d51
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/DnsResolverModuleTest.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+/**
+ * Indicates that the test covers functionality that was rolled out in a resolv module update.
+ */
+annotation class DnsResolverModuleTest
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/FileUtils.kt b/staticlibs/testutils/hostdevice/com/android/testutils/FileUtils.kt
new file mode 100644
index 0000000..678f977
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/FileUtils.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+// This function is private because the 2 is hardcoded here, and is not correct if not called
+// directly from __LINE__ or __FILE__.
+private fun callerStackTrace(): StackTraceElement = try {
+    throw RuntimeException()
+} catch (e: RuntimeException) {
+    e.stackTrace[2] // 0 is here, 1 is get() in __FILE__ or __LINE__
+}
+val __FILE__: String get() = callerStackTrace().fileName
+val __LINE__: Int get() = callerStackTrace().lineNumber
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/FunctionalUtils.java b/staticlibs/testutils/hostdevice/com/android/testutils/FunctionalUtils.java
new file mode 100644
index 0000000..da36e4d
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/FunctionalUtils.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import java.util.function.Supplier;
+
+/**
+ * A class grouping some utilities to deal with exceptions.
+ */
+public class FunctionalUtils {
+    /**
+     * Like a Consumer, but declared to throw an exception.
+     * @param <T>
+     */
+    @FunctionalInterface
+    public interface ThrowingConsumer<T> {
+        /** @see java.util.function.Consumer */
+        void accept(T t) throws Exception;
+    }
+
+    /**
+     * Like a Supplier, but declared to throw an exception.
+     * @param <T>
+     */
+    @FunctionalInterface
+    public interface ThrowingSupplier<T> {
+        /** @see java.util.function.Supplier */
+        T get() throws Exception;
+    }
+
+    /**
+     * Like a Runnable, but declared to throw an exception.
+     */
+    @FunctionalInterface
+    public interface ThrowingRunnable {
+        /** @see java.lang.Runnable */
+        void run() throws Exception;
+    }
+
+    /**
+     * Convert a supplier that throws into one that doesn't.
+     *
+     * The returned supplier returns null in cases where the source throws.
+     */
+    public static <T> Supplier<T> ignoreExceptions(ThrowingSupplier<T> func) {
+        return () -> {
+            try {
+                return func.get();
+            } catch (Exception e) {
+                return null;
+            }
+        };
+    }
+
+    /**
+     * Convert a runnable that throws into one that doesn't.
+     *
+     * All exceptions are ignored by the returned Runnable.
+     */
+    public static Runnable ignoreExceptions(ThrowingRunnable r) {
+        return () -> {
+            try {
+                r.run();
+            } catch (Exception e) {
+            }
+        };
+    }
+
+    // Java has Function<T, R> and BiFunction<T, U, V> but nothing for higher-arity functions.
+    // Function3 is what Kotlin and Scala use (they also have higher-arity variants, with
+    // FunctionN taking N arguments, as the JVM does not have variadic formal parameters)
+    /**
+     * A function with three arguments.
+     * @param <TArg1> Type of the first argument
+     * @param <TArg2> Type of the second argument
+     * @param <TArg3> Type of the third argument
+     * @param <TResult> Type of the return value
+     */
+    public interface Function3<TArg1, TArg2, TArg3, TResult> {
+        /**
+         * Apply the function to the arguments
+         */
+        TResult apply(TArg1 a1, TArg2 a2, TArg3 a3);
+    }
+}
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
new file mode 100644
index 0000000..1883387
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("MiscAsserts")
+
+package com.android.testutils
+
+import com.android.testutils.FunctionalUtils.ThrowingRunnable
+import java.lang.reflect.Modifier
+import kotlin.system.measureTimeMillis
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+private const val TAG = "Connectivity unit test"
+
+fun <T> assertEmpty(ts: Array<T>) = ts.size.let { len ->
+    assertEquals(0, len, "Expected empty array, but length was $len")
+}
+
+fun <T> assertEmpty(ts: Collection<T>) = ts.size.let { len ->
+    assertEquals(0, len, "Expected empty collection, but length was $len")
+}
+
+fun <T> assertLength(expected: Int, got: Array<T>) = got.size.let { len ->
+    assertEquals(expected, len, "Expected array of length $expected, but was $len for $got")
+}
+
+fun <T> assertLength(expected: Int, got: List<T>) = got.size.let { len ->
+    assertEquals(expected, len, "Expected list of length $expected, but was $len for $got")
+}
+
+// Bridge method to help write this in Java. If you're writing Kotlin, consider using
+// kotlin.test.assertFailsWith instead, as that method is reified and inlined.
+fun <T : Exception> assertThrows(expected: Class<T>, block: ThrowingRunnable): T {
+    return assertFailsWith(expected.kotlin) { block.run() }
+}
+
+fun <T : Exception> assertThrows(msg: String, expected: Class<T>, block: ThrowingRunnable): T {
+    return assertFailsWith(expected.kotlin, msg) { block.run() }
+}
+
+fun <T> assertEqualBothWays(o1: T, o2: T) {
+    assertTrue(o1 == o2)
+    assertTrue(o2 == o1)
+}
+
+fun <T> assertNotEqualEitherWay(o1: T, o2: T) {
+    assertFalse(o1 == o2)
+    assertFalse(o2 == o1)
+}
+
+fun assertStringContains(got: String, want: String) {
+    assertTrue(got.contains(want), "$got did not contain \"${want}\"")
+}
+
+fun assertContainsExactly(actual: IntArray, vararg expected: Int) {
+    // IntArray#sorted() returns a list, so it's fine to test with equals()
+    assertEquals(actual.sorted(), expected.sorted(),
+            "$actual does not contain exactly $expected")
+}
+
+fun assertContainsStringsExactly(actual: Array<String>, vararg expected: String) {
+    assertEquals(actual.sorted(), expected.sorted(),
+            "$actual does not contain exactly $expected")
+}
+
+fun <T> assertContainsAll(list: Collection<T>, vararg elems: T) {
+    assertContainsAll(list, elems.asList())
+}
+
+fun <T> assertContainsAll(list: Collection<T>, elems: Collection<T>) {
+    elems.forEach { assertTrue(list.contains(it), "$it not in list") }
+}
+
+fun assertRunsInAtMost(descr: String, timeLimit: Long, fn: Runnable) {
+    assertRunsInAtMost(descr, timeLimit) { fn.run() }
+}
+
+fun assertRunsInAtMost(descr: String, timeLimit: Long, fn: () -> Unit) {
+    val timeTaken = measureTimeMillis(fn)
+    val msg = String.format("%s: took %dms, limit was %dms", descr, timeTaken, timeLimit)
+    assertTrue(timeTaken <= timeLimit, msg)
+}
+
+/**
+ * Verifies that the number of nonstatic fields in a java class equals a given count.
+ * Note: this is essentially not useful for Kotlin code where fields are not really a thing.
+ *
+ * This assertion serves as a reminder to update test code around it if fields are added
+ * after the test is written.
+ * @param count Expected number of nonstatic fields in the class.
+ * @param clazz Class to test.
+ */
+fun <T> assertFieldCountEquals(count: Int, clazz: Class<T>) {
+    assertEquals(count, clazz.declaredFields.filter {
+        !Modifier.isStatic(it.modifiers) && !Modifier.isTransient(it.modifiers)
+    }.size)
+}
+
+fun <T> assertSameElements(expected: List<T>, actual: List<T>) {
+    val expectedSet: HashSet<T> = HashSet(expected)
+    assertEquals(expectedSet.size, expected.size, "expected list contains duplicates")
+    val actualSet: HashSet<T> = HashSet(actual)
+    assertEquals(actualSet.size, actual.size, "actual list contains duplicates")
+    assertEquals(expectedSet, actualSet)
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/NetworkStackModuleTest.kt b/staticlibs/testutils/hostdevice/com/android/testutils/NetworkStackModuleTest.kt
new file mode 100644
index 0000000..fe312a0
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/NetworkStackModuleTest.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+/**
+ * Indicates that the test covers functionality that was rolled out in a NetworkStack module update.
+ */
+annotation class NetworkStackModuleTest
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
new file mode 100644
index 0000000..a73a58a
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import java.net.Inet4Address
+import java.util.function.Predicate
+
+// Some of the below constants are duplicated with NetworkStackConstants, but this is a hostdevice
+// library usable for host-side tests, so device-side utils are not usable, and there is no
+// host-side non-test library to host common constants.
+private const val ETHER_TYPE_OFFSET = 12
+private const val ETHER_HEADER_LENGTH = 14
+private const val IPV4_PROTOCOL_OFFSET = ETHER_HEADER_LENGTH + 9
+private const val IPV6_PROTOCOL_OFFSET = ETHER_HEADER_LENGTH + 6
+private const val IPV4_CHECKSUM_OFFSET = ETHER_HEADER_LENGTH + 10
+private const val IPV4_DST_OFFSET = ETHER_HEADER_LENGTH + 16
+private const val IPV4_HEADER_LENGTH = 20
+private const val IPV6_HEADER_LENGTH = 40
+private const val IPV4_PAYLOAD_OFFSET = ETHER_HEADER_LENGTH + IPV4_HEADER_LENGTH
+private const val IPV6_PAYLOAD_OFFSET = ETHER_HEADER_LENGTH + IPV6_HEADER_LENGTH
+private const val UDP_HEADER_LENGTH = 8
+private const val BOOTP_OFFSET = IPV4_PAYLOAD_OFFSET + UDP_HEADER_LENGTH
+private const val BOOTP_TID_OFFSET = BOOTP_OFFSET + 4
+private const val BOOTP_CLIENT_MAC_OFFSET = BOOTP_OFFSET + 28
+private const val DHCP_OPTIONS_OFFSET = BOOTP_OFFSET + 240
+private const val ARP_OPCODE_OFFSET = ETHER_HEADER_LENGTH + 6
+
+/**
+ * A [Predicate] that matches a [ByteArray] if it contains the specified [bytes] at the specified
+ * [offset].
+ */
+class OffsetFilter(val offset: Int, vararg val bytes: Byte) : Predicate<ByteArray> {
+    override fun test(packet: ByteArray) =
+            bytes.withIndex().all { it.value == packet[offset + it.index] }
+}
+
+private class UdpPortFilter(
+    private val udpOffset: Int,
+    private val src: Short?,
+    private val dst: Short?
+) : Predicate<ByteArray> {
+    override fun test(t: ByteArray): Boolean {
+        if (src != null && !OffsetFilter(udpOffset,
+                        src.toInt().ushr(8).toByte(), src.toByte()).test(t)) {
+            return false
+        }
+
+        if (dst != null && !OffsetFilter(udpOffset + 2,
+                        dst.toInt().ushr(8).toByte(), dst.toByte()).test(t)) {
+            return false
+        }
+        return true
+    }
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped packets that contain an UDP over IPv4 datagram.
+ */
+class IPv4UdpFilter @JvmOverloads constructor(
+    srcPort: Short? = null,
+    dstPort: Short? = null
+) : Predicate<ByteArray> {
+    private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x08, 0x00 /* IPv4 */).and(
+            OffsetFilter(IPV4_PROTOCOL_OFFSET, 17 /* UDP */)).and(
+            UdpPortFilter(IPV4_PAYLOAD_OFFSET, srcPort, dstPort))
+    override fun test(t: ByteArray) = impl.test(t)
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped packets that contain an UDP over IPv6 datagram.
+ */
+class IPv6UdpFilter @JvmOverloads constructor(
+    srcPort: Short? = null,
+    dstPort: Short? = null
+) : Predicate<ByteArray> {
+    private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x86.toByte(), 0xdd.toByte() /* IPv6 */).and(
+            OffsetFilter(IPV6_PROTOCOL_OFFSET, 17 /* UDP */)).and(
+            UdpPortFilter(IPV6_PAYLOAD_OFFSET, srcPort, dstPort))
+    override fun test(t: ByteArray) = impl.test(t)
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped packets sent to the specified IPv4 destination.
+ */
+class IPv4DstFilter(dst: Inet4Address) : Predicate<ByteArray> {
+    private val impl = OffsetFilter(IPV4_DST_OFFSET, *dst.address)
+    override fun test(t: ByteArray) = impl.test(t)
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped ARP requests.
+ */
+class ArpRequestFilter : Predicate<ByteArray> {
+    private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x08, 0x06 /* ARP */)
+            .and(OffsetFilter(ARP_OPCODE_OFFSET, 0x00, 0x01 /* request */))
+    override fun test(t: ByteArray) = impl.test(t)
+}
+
+class Icmpv6Filter : Predicate<ByteArray> {
+    private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x86.toByte(), 0xdd.toByte() /* IPv6 */).and(
+        OffsetFilter(IPV6_PROTOCOL_OFFSET, 58 /* ICMPv6 */))
+    override fun test(t: ByteArray) = impl.test(t)
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped DHCP packets sent from a DHCP client.
+ */
+class DhcpClientPacketFilter : Predicate<ByteArray> {
+    private val impl = IPv4UdpFilter(srcPort = 68, dstPort = 67)
+    override fun test(t: ByteArray) = impl.test(t)
+}
+
+/**
+ * A [Predicate] that matches a [ByteArray] if it contains a ethernet-encapped DHCP packet that
+ * contains the specified option with the specified [bytes] as value.
+ */
+class DhcpOptionFilter(val option: Byte, vararg val bytes: Byte) : Predicate<ByteArray> {
+    override fun test(packet: ByteArray): Boolean {
+        val option = findDhcpOption(packet, option) ?: return false
+        return option.contentEquals(bytes)
+    }
+}
+
+/**
+ * Find a DHCP option in a packet and return its value, if found.
+ */
+fun findDhcpOption(packet: ByteArray, option: Byte): ByteArray? =
+        findOptionOffset(packet, option, DHCP_OPTIONS_OFFSET)?.let {
+            val optionLen = packet[it + 1]
+            return packet.copyOfRange(it + 2 /* type, length bytes */, it + 2 + optionLen)
+        }
+
+private tailrec fun findOptionOffset(packet: ByteArray, option: Byte, searchOffset: Int): Int? {
+    if (packet.size <= searchOffset + 2 /* type, length bytes */) return null
+
+    return if (packet[searchOffset] == option) searchOffset else {
+        val optionLen = packet[searchOffset + 1]
+        findOptionOffset(packet, option, searchOffset + 2 + optionLen)
+    }
+}
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/SkipMainlinePresubmit.kt b/staticlibs/testutils/hostdevice/com/android/testutils/SkipMainlinePresubmit.kt
new file mode 100644
index 0000000..5952365
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/SkipMainlinePresubmit.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+/**
+ * Skip the test in presubmit runs for the reason specified in [reason].
+ *
+ * This annotation is typically used to document limitations that prevent a test from being
+ * executed in presubmit on older builds.
+ */
+annotation class SkipMainlinePresubmit(val reason: String)
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/SkipPresubmit.kt b/staticlibs/testutils/hostdevice/com/android/testutils/SkipPresubmit.kt
new file mode 100644
index 0000000..69ed048
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/SkipPresubmit.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+/**
+ * Skip the test in presubmit runs for the reason specified in [reason].
+ *
+ * This annotation is typically used to document hardware or test bench limitations.
+ */
+annotation class SkipPresubmit(val reason: String)
\ No newline at end of file
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
new file mode 100644
index 0000000..7854bb5
--- /dev/null
+++ b/tests/benchmark/Android.bp
@@ -0,0 +1,43 @@
+//
+// 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"],
+}
+
+android_test {
+    name: "ConnectivityBenchmarkTests",
+    defaults: [
+        "framework-connectivity-internal-test-defaults",
+    ],
+    platform_apis: true,
+    srcs: [
+        "src/**/*.kt",
+        "src/**/*.aidl",
+    ],
+    asset_dirs: ["assets"],
+    static_libs: [
+        "androidx.test.rules",
+        "mockito-target-minus-junit4",
+        "net-tests-utils",
+        "service-connectivity-pre-jarjar",
+        "service-connectivity-tiramisu-pre-jarjar",
+    ],
+    test_suites: ["device-tests"],
+    jarjar_rules: ":connectivity-jarjar-rules",
+}
diff --git a/tests/benchmark/AndroidManifest.xml b/tests/benchmark/AndroidManifest.xml
new file mode 100644
index 0000000..bd2fce5
--- /dev/null
+++ b/tests/benchmark/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?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="com.android.server.connectivity.benchmarktests">
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.server.connectivity.benchmarktests"
+         android:label="Connectivity Benchmark Tests" />
+</manifest>
diff --git a/tests/benchmark/OWNERS b/tests/benchmark/OWNERS
new file mode 100644
index 0000000..3101da5
--- /dev/null
+++ b/tests/benchmark/OWNERS
@@ -0,0 +1,2 @@
+# Bug template url: http://b/new?component=31808
+# TODO: move bug template config to common owners file once b/226427845 is resolved
\ No newline at end of file
diff --git a/tests/benchmark/assets/dataset/A052701.zip b/tests/benchmark/assets/dataset/A052701.zip
new file mode 100644
index 0000000..fdde1ad
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052701.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052801.zip b/tests/benchmark/assets/dataset/A052801.zip
new file mode 100644
index 0000000..7f908b7
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052801.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052802.zip b/tests/benchmark/assets/dataset/A052802.zip
new file mode 100644
index 0000000..180ad3e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052802.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052803.zip b/tests/benchmark/assets/dataset/A052803.zip
new file mode 100644
index 0000000..321a79b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052803.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052804.zip b/tests/benchmark/assets/dataset/A052804.zip
new file mode 100644
index 0000000..298ec04
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052804.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052901.zip b/tests/benchmark/assets/dataset/A052901.zip
new file mode 100644
index 0000000..0f49543
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052901.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052902.zip b/tests/benchmark/assets/dataset/A052902.zip
new file mode 100644
index 0000000..ec22456
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052902.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053001.zip b/tests/benchmark/assets/dataset/A053001.zip
new file mode 100644
index 0000000..ad5d82e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053001.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053002.zip b/tests/benchmark/assets/dataset/A053002.zip
new file mode 100644
index 0000000..8a4bb0c
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053002.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053003.zip b/tests/benchmark/assets/dataset/A053003.zip
new file mode 100644
index 0000000..24d2057
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053003.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053004.zip b/tests/benchmark/assets/dataset/A053004.zip
new file mode 100644
index 0000000..352f93f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053004.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053005.zip b/tests/benchmark/assets/dataset/A053005.zip
new file mode 100644
index 0000000..2b49a1b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053005.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053006.zip b/tests/benchmark/assets/dataset/A053006.zip
new file mode 100644
index 0000000..a59f2ec
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053006.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053007.zip b/tests/benchmark/assets/dataset/A053007.zip
new file mode 100644
index 0000000..df7ae74
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053007.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053101.zip b/tests/benchmark/assets/dataset/A053101.zip
new file mode 100644
index 0000000..c10ed64
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053102.zip b/tests/benchmark/assets/dataset/A053102.zip
new file mode 100644
index 0000000..8c9f9cf
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053103.zip b/tests/benchmark/assets/dataset/A053103.zip
new file mode 100644
index 0000000..9202c50
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053103.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053104.zip b/tests/benchmark/assets/dataset/A053104.zip
new file mode 100644
index 0000000..3c77724
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053104.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060101.zip b/tests/benchmark/assets/dataset/A060101.zip
new file mode 100644
index 0000000..86443a7
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060102.zip b/tests/benchmark/assets/dataset/A060102.zip
new file mode 100644
index 0000000..4f2cf49
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060201.zip b/tests/benchmark/assets/dataset/A060201.zip
new file mode 100644
index 0000000..3c28bec
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060201.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060202.zip b/tests/benchmark/assets/dataset/A060202.zip
new file mode 100644
index 0000000..e39e493
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060202.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B053001.zip b/tests/benchmark/assets/dataset/B053001.zip
new file mode 100644
index 0000000..8408744
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B053001.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B053002.zip b/tests/benchmark/assets/dataset/B053002.zip
new file mode 100644
index 0000000..5245f70
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B053002.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060101.zip b/tests/benchmark/assets/dataset/B060101.zip
new file mode 100644
index 0000000..242c0d1
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060201.zip b/tests/benchmark/assets/dataset/B060201.zip
new file mode 100644
index 0000000..29df25a
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060201.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060202.zip b/tests/benchmark/assets/dataset/B060202.zip
new file mode 100644
index 0000000..bda9edd
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060202.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060203.zip b/tests/benchmark/assets/dataset/B060203.zip
new file mode 100644
index 0000000..b9fccfe
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060203.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060204.zip b/tests/benchmark/assets/dataset/B060204.zip
new file mode 100644
index 0000000..66227d2
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060204.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060205.zip b/tests/benchmark/assets/dataset/B060205.zip
new file mode 100644
index 0000000..6aaa06b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060205.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060206.zip b/tests/benchmark/assets/dataset/B060206.zip
new file mode 100644
index 0000000..18445b0
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060206.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060207.zip b/tests/benchmark/assets/dataset/B060207.zip
new file mode 100644
index 0000000..20f7c5b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060207.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060101.zip b/tests/benchmark/assets/dataset/C060101.zip
new file mode 100644
index 0000000..0b1c29f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060102.zip b/tests/benchmark/assets/dataset/C060102.zip
new file mode 100644
index 0000000..8064905
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060103.zip b/tests/benchmark/assets/dataset/C060103.zip
new file mode 100644
index 0000000..d0e819f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060103.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060104.zip b/tests/benchmark/assets/dataset/C060104.zip
new file mode 100644
index 0000000..f87ca8d
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060104.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060105.zip b/tests/benchmark/assets/dataset/C060105.zip
new file mode 100644
index 0000000..e869895
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060105.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060106.zip b/tests/benchmark/assets/dataset/C060106.zip
new file mode 100644
index 0000000..6d25a98
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060106.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060107.zip b/tests/benchmark/assets/dataset/C060107.zip
new file mode 100644
index 0000000..a7cb31c
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060107.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060108.zip b/tests/benchmark/assets/dataset/C060108.zip
new file mode 100644
index 0000000..c1a5898
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060108.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060109.zip b/tests/benchmark/assets/dataset/C060109.zip
new file mode 100644
index 0000000..bb9116e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060109.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060110.zip b/tests/benchmark/assets/dataset/C060110.zip
new file mode 100644
index 0000000..5ca0f96
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060110.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060111.zip b/tests/benchmark/assets/dataset/C060111.zip
new file mode 100644
index 0000000..6a12d7e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060111.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060112.zip b/tests/benchmark/assets/dataset/C060112.zip
new file mode 100644
index 0000000..fa2c30b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060112.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060113.zip b/tests/benchmark/assets/dataset/C060113.zip
new file mode 100644
index 0000000..63a34ba
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060113.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060114.zip b/tests/benchmark/assets/dataset/C060114.zip
new file mode 100644
index 0000000..bd60927
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060114.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/netstats-many-uids.zip b/tests/benchmark/assets/dataset/netstats-many-uids.zip
new file mode 100644
index 0000000..9554aaa
--- /dev/null
+++ b/tests/benchmark/assets/dataset/netstats-many-uids.zip
Binary files differ
diff --git a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
new file mode 100644
index 0000000..57602f1
--- /dev/null
+++ b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.net.benchmarktests
+
+import android.net.NetworkStats.NonMonotonicObserver
+import android.net.NetworkStatsCollection
+import android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID
+import android.os.DropBoxManager
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.util.FileRotator
+import com.android.internal.util.FileRotator.Reader
+import com.android.server.net.NetworkStatsRecorder
+import java.io.BufferedInputStream
+import java.io.DataInputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.nio.file.Files
+import java.util.concurrent.TimeUnit
+import java.util.zip.ZipInputStream
+import kotlin.test.assertTrue
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+
+@RunWith(JUnit4::class)
+class NetworkStatsTest {
+    companion object {
+        private val DEFAULT_BUFFER_SIZE = 8192
+        private val FILE_CACHE_WARM_UP_REPEAT_COUNT = 10
+        private val UID_COLLECTION_BUCKET_DURATION_MS = TimeUnit.HOURS.toMillis(2)
+        private val UID_RECORDER_ROTATE_AGE_MS = TimeUnit.DAYS.toMillis(15)
+        private val UID_RECORDER_DELETE_AGE_MS = TimeUnit.DAYS.toMillis(90)
+        private val TEST_DATASET_SUBFOLDER = "dataset/"
+
+        // These files are generated by using real user dataset which has many uid records
+        // and agreed to share the dataset for testing purpose. These dataset can be
+        // extracted from rooted devices by using
+        // "adb pull /data/misc/apexdata/com.android.tethering/netstats" command.
+        private val testFilesAssets by lazy {
+            val zipFiles = context.assets.list(TEST_DATASET_SUBFOLDER)!!.asList()
+            zipFiles.map {
+                val zipInputStream =
+                    ZipInputStream((TEST_DATASET_SUBFOLDER + it).toAssetInputStream())
+                File(unzipToTempDir(zipInputStream), "netstats")
+            }
+        }
+
+        // Test results shows the test cases who read the file first will take longer time to
+        // execute, and reading time getting shorter each time due to file caching mechanism.
+        // Read files several times prior to tests to minimize the impact.
+        // This cannot live in setUp() since the time spent on the file reading will be
+        // attributed to the time spent on the individual test case.
+        @JvmStatic
+        @BeforeClass
+        fun setUpOnce() {
+            repeat(FILE_CACHE_WARM_UP_REPEAT_COUNT) {
+                testFilesAssets.forEach {
+                    val uidTestFiles = getSortedListForPrefix(it, "uid")
+                    val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
+                    for (file in uidTestFiles) {
+                        readFile(file, collection)
+                    }
+                }
+            }
+        }
+
+        val context get() = InstrumentationRegistry.getInstrumentation().getContext()
+        private fun String.toAssetInputStream() = DataInputStream(context.assets.open(this))
+
+        private fun unzipToTempDir(zis: ZipInputStream): File {
+            val statsDir =
+                Files.createTempDirectory(NetworkStatsTest::class.simpleName).toFile()
+            generateSequence { zis.nextEntry }.forEach { entry ->
+                val entryFile = File(statsDir, entry.name)
+                if (entry.isDirectory) {
+                    entryFile.mkdirs()
+                    return@forEach
+                }
+
+                // Make sure all folders exists. There is no guarantee anywhere.
+                entryFile.parentFile!!.mkdirs()
+
+                // If the entry is a file extract it.
+                FileOutputStream(entryFile).use {
+                    zis.copyTo(it, DEFAULT_BUFFER_SIZE)
+                }
+            }
+            return statsDir
+        }
+
+        // List [xt|uid|uid_tag].<start>-<end> files under the given directory.
+        private fun getSortedListForPrefix(statsDir: File, prefix: String): List<File> {
+            assertTrue(statsDir.exists())
+            return statsDir.list { _, name -> name.startsWith("$prefix.") }
+                .orEmpty()
+                .map { it -> File(statsDir, it) }
+                .sorted()
+        }
+
+        private fun readFile(file: File, reader: Reader) =
+            BufferedInputStream(file.inputStream()).use {
+                reader.read(it)
+            }
+    }
+
+    @Test
+    fun testReadCollection_manyUids() {
+        // The file cache is warmed up by the @BeforeClass method, so now the test can repeat
+        // this a number of time to have a stable number.
+        testFilesAssets.forEach {
+            val uidTestFiles = getSortedListForPrefix(it, "uid")
+            val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
+            for (file in uidTestFiles) {
+                readFile(file, collection)
+            }
+        }
+    }
+
+    @Test
+    fun testReadFromRecorder_manyUids_useDataInput() {
+        doTestReadFromRecorder_manyUids(useFastDataInput = false)
+    }
+
+    @Test
+    fun testReadFromRecorder_manyUids_useFastDataInput() {
+        doTestReadFromRecorder_manyUids(useFastDataInput = true)
+    }
+
+    fun doTestReadFromRecorder_manyUids(useFastDataInput: Boolean) {
+        val mockObserver = mock<NonMonotonicObserver<String>>()
+        val mockDropBox = mock<DropBoxManager>()
+        testFilesAssets.forEach {
+            val recorder = NetworkStatsRecorder(
+                FileRotator(
+                    it, PREFIX_UID, UID_RECORDER_ROTATE_AGE_MS, UID_RECORDER_DELETE_AGE_MS
+                ),
+                mockObserver,
+                mockDropBox,
+                PREFIX_UID,
+                UID_COLLECTION_BUCKET_DURATION_MS,
+                false /* includeTags */,
+                false /* wipeOnError */,
+                useFastDataInput /* useFastDataInput */,
+                it
+            )
+            recorder.orLoadCompleteLocked
+        }
+    }
+
+    inline fun <reified T> mock(): T = mock(T::class.java)
+}
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 8e47235..e95a81a 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -17,6 +17,7 @@
 // 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"],
 }
@@ -31,7 +32,7 @@
 //   (currently, CTS 10, 11, and 12).
 java_defaults {
     name: "ConnectivityTestsLatestSdkDefaults",
-    target_sdk_version: "33",
+    target_sdk_version: "34",
 }
 
 java_library {
@@ -93,7 +94,10 @@
     name: "ConnectivityCoverageTests",
     // Tethering started on SDK 30
     min_sdk_version: "30",
-    test_suites: ["general-tests", "mts-tethering"],
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
     defaults: [
         "ConnectivityTestsLatestSdkDefaults",
         "framework-connectivity-internal-test-defaults",
@@ -185,7 +189,7 @@
 // See SuiteModuleLoader.java.
 // TODO: why are the modules separated by + instead of being separate entries in the array?
 mainline_presubmit_modules = [
-        "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex",
+    "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex",
 ]
 
 cc_defaults {
@@ -197,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/OWNERS b/tests/common/OWNERS
new file mode 100644
index 0000000..3101da5
--- /dev/null
+++ b/tests/common/OWNERS
@@ -0,0 +1,2 @@
+# Bug template url: http://b/new?component=31808
+# TODO: move bug template config to common owners file once b/226427845 is resolved
\ No newline at end of file
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/CaptivePortalDataTest.kt b/tests/common/java/android/net/CaptivePortalDataTest.kt
index f927380..67a523c 100644
--- a/tests/common/java/android/net/CaptivePortalDataTest.kt
+++ b/tests/common/java/android/net/CaptivePortalDataTest.kt
@@ -19,21 +19,20 @@
 import android.os.Build
 import androidx.test.filters.SmallTest
 import com.android.modules.utils.build.SdkLevel
-import com.android.testutils.assertParcelingIsLossless
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import kotlin.test.assertEquals
-import kotlin.test.assertNotEquals
 
 @SmallTest
 @RunWith(DevSdkIgnoreRunner::class)
-@IgnoreUpTo(Build.VERSION_CODES.Q)
 class CaptivePortalDataTest {
     @Rule @JvmField
     val ignoreRule = DevSdkIgnoreRule()
diff --git a/tests/common/java/android/net/KeepalivePacketDataTest.kt b/tests/common/java/android/net/KeepalivePacketDataTest.kt
index f464ec6..97a45fc 100644
--- a/tests/common/java/android/net/KeepalivePacketDataTest.kt
+++ b/tests/common/java/android/net/KeepalivePacketDataTest.kt
@@ -17,26 +17,20 @@
 
 import android.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS
 import android.net.InvalidPacketException.ERROR_INVALID_PORT
-import android.os.Build
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.NonNullTestUtils
 import java.net.InetAddress
 import java.util.Arrays
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class KeepalivePacketDataTest {
-    @Rule @JvmField
-    val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule()
-
     private val INVALID_PORT = 65537
     private val TEST_DST_PORT = 4244
     private val TEST_SRC_PORT = 4243
@@ -55,43 +49,41 @@
         dstAddress: InetAddress? = TEST_DST_ADDRV4,
         dstPort: Int = TEST_DST_PORT,
         data: ByteArray = TESTBYTES
-    ) : KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, data)
+    ) : KeepalivePacketData(NonNullTestUtils.nullUnsafe(srcAddress), srcPort,
+            NonNullTestUtils.nullUnsafe(dstAddress), dstPort, data)
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testConstructor() {
-        var data: TestKeepalivePacketData
-
         try {
-            data = TestKeepalivePacketData(srcAddress = null)
+            TestKeepalivePacketData(srcAddress = null)
             fail("Null src address should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
         }
 
         try {
-            data = TestKeepalivePacketData(dstAddress = null)
+            TestKeepalivePacketData(dstAddress = null)
             fail("Null dst address should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
         }
 
         try {
-            data = TestKeepalivePacketData(dstAddress = TEST_ADDRV6)
+            TestKeepalivePacketData(dstAddress = TEST_ADDRV6)
             fail("Ip family mismatched should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
         }
 
         try {
-            data = TestKeepalivePacketData(srcPort = INVALID_PORT)
+            TestKeepalivePacketData(srcPort = INVALID_PORT)
             fail("Invalid srcPort should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_PORT)
         }
 
         try {
-            data = TestKeepalivePacketData(dstPort = INVALID_PORT)
+            TestKeepalivePacketData(dstPort = INVALID_PORT)
             fail("Invalid dstPort should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_PORT)
@@ -99,22 +91,17 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testSrcAddress() = assertEquals(TEST_SRC_ADDRV4, TestKeepalivePacketData().srcAddress)
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testDstAddress() = assertEquals(TEST_DST_ADDRV4, TestKeepalivePacketData().dstAddress)
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testSrcPort() = assertEquals(TEST_SRC_PORT, TestKeepalivePacketData().srcPort)
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testDstPort() = assertEquals(TEST_DST_PORT, TestKeepalivePacketData().dstPort)
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testPacket() = assertTrue(Arrays.equals(TESTBYTES, TestKeepalivePacketData().packet))
-}
\ No newline at end of file
+}
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 09f5d6e..8f14572 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -134,13 +134,10 @@
         assertFalse(lp.isIpv4Provisioned());
         assertFalse(lp.isIpv6Provisioned());
         assertFalse(lp.isPrivateDnsActive());
-
-        if (SdkLevel.isAtLeastR()) {
-            assertNull(lp.getDhcpServerAddress());
-            assertFalse(lp.isWakeOnLanSupported());
-            assertNull(lp.getCaptivePortalApiUrl());
-            assertNull(lp.getCaptivePortalData());
-        }
+        assertNull(lp.getDhcpServerAddress());
+        assertFalse(lp.isWakeOnLanSupported());
+        assertNull(lp.getCaptivePortalApiUrl());
+        assertNull(lp.getCaptivePortalData());
     }
 
     private LinkProperties makeTestObject() {
@@ -162,12 +159,10 @@
         lp.setMtu(MTU);
         lp.setTcpBufferSizes(TCP_BUFFER_SIZES);
         lp.setNat64Prefix(new IpPrefix("2001:db8:0:64::/96"));
-        if (SdkLevel.isAtLeastR()) {
-            lp.setDhcpServerAddress(DHCPSERVER);
-            lp.setWakeOnLanSupported(true);
-            lp.setCaptivePortalApiUrl(CAPPORT_API_URL);
-            lp.setCaptivePortalData((CaptivePortalData) getCaptivePortalData());
-        }
+        lp.setDhcpServerAddress(DHCPSERVER);
+        lp.setWakeOnLanSupported(true);
+        lp.setCaptivePortalApiUrl(CAPPORT_API_URL);
+        lp.setCaptivePortalData((CaptivePortalData) getCaptivePortalData());
         return lp;
     }
 
@@ -206,19 +201,17 @@
         assertTrue(source.isIdenticalTcpBufferSizes(target));
         assertTrue(target.isIdenticalTcpBufferSizes(source));
 
-        if (SdkLevel.isAtLeastR()) {
-            assertTrue(source.isIdenticalDhcpServerAddress(target));
-            assertTrue(source.isIdenticalDhcpServerAddress(source));
+        assertTrue(source.isIdenticalDhcpServerAddress(target));
+        assertTrue(source.isIdenticalDhcpServerAddress(source));
 
-            assertTrue(source.isIdenticalWakeOnLan(target));
-            assertTrue(target.isIdenticalWakeOnLan(source));
+        assertTrue(source.isIdenticalWakeOnLan(target));
+        assertTrue(target.isIdenticalWakeOnLan(source));
 
-            assertTrue(source.isIdenticalCaptivePortalApiUrl(target));
-            assertTrue(target.isIdenticalCaptivePortalApiUrl(source));
+        assertTrue(source.isIdenticalCaptivePortalApiUrl(target));
+        assertTrue(target.isIdenticalCaptivePortalApiUrl(source));
 
-            assertTrue(source.isIdenticalCaptivePortalData(target));
-            assertTrue(target.isIdenticalCaptivePortalData(source));
-        }
+        assertTrue(source.isIdenticalCaptivePortalData(target));
+        assertTrue(target.isIdenticalCaptivePortalData(source));
 
         // Check result of equals().
         assertTrue(source.equals(target));
@@ -1017,7 +1010,7 @@
         assertParcelingIsLossless(source);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testLinkPropertiesParcelable() throws Exception {
         final LinkProperties source = makeLinkPropertiesForParceling();
 
@@ -1035,7 +1028,7 @@
     }
 
     // Parceling of the scope was broken until Q-QPR2
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testLinkLocalDnsServerParceling() throws Exception {
         final String strAddress = "fe80::1%lo";
         final LinkProperties lp = new LinkProperties();
@@ -1158,7 +1151,7 @@
         assertFalse(lp.isPrivateDnsActive());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testDhcpServerAddress() {
         final LinkProperties lp = makeTestObject();
         assertEquals(DHCPSERVER, lp.getDhcpServerAddress());
@@ -1167,7 +1160,7 @@
         assertNull(lp.getDhcpServerAddress());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testWakeOnLanSupported() {
         final LinkProperties lp = makeTestObject();
         assertTrue(lp.isWakeOnLanSupported());
@@ -1176,7 +1169,7 @@
         assertFalse(lp.isWakeOnLanSupported());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testCaptivePortalApiUrl() {
         final LinkProperties lp = makeTestObject();
         assertEquals(CAPPORT_API_URL, lp.getCaptivePortalApiUrl());
@@ -1185,7 +1178,7 @@
         assertNull(lp.getCaptivePortalApiUrl());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testCaptivePortalData() {
         final LinkProperties lp = makeTestObject();
         assertEquals(getCaptivePortalData(), lp.getCaptivePortalData());
@@ -1238,7 +1231,7 @@
         assertTrue(Ipv6.hasIpv6DnsServer());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testHasIpv4UnreachableDefaultRoute() {
         final LinkProperties lp = makeTestObject();
         assertFalse(lp.hasIpv4UnreachableDefaultRoute());
@@ -1249,7 +1242,7 @@
         assertFalse(lp.hasIpv6UnreachableDefaultRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testHasIpv6UnreachableDefaultRoute() {
         final LinkProperties lp = makeTestObject();
         assertFalse(lp.hasIpv6UnreachableDefaultRoute());
@@ -1260,7 +1253,7 @@
         assertFalse(lp.hasIpv4UnreachableDefaultRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({ConnectivityCompatChanges.EXCLUDED_ROUTES})
     public void testHasExcludeRoute() {
@@ -1273,7 +1266,7 @@
         assertTrue(lp.hasExcludeRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({ConnectivityCompatChanges.EXCLUDED_ROUTES})
     public void testRouteAddWithSameKey() throws Exception {
@@ -1347,14 +1340,14 @@
         assertExcludeRoutesVisible();
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({ConnectivityCompatChanges.EXCLUDED_ROUTES})
     public void testExcludedRoutesEnabledByCompatChange() {
         assertExcludeRoutesVisible();
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @DisableCompatChanges({ConnectivityCompatChanges.EXCLUDED_ROUTES})
     public void testExcludedRoutesDisabledByCompatChange() {
diff --git a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
index 4a4859d..70adbd7 100644
--- a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
+++ b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
@@ -52,7 +52,6 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     @IgnoreAfter(Build.VERSION_CODES.R)
     // Only run this test on Android R.
     // The method - satisfiedBy() has changed to canBeSatisfiedBy() starting from Android R, so the
diff --git a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
index ad7a526..1148eff 100644
--- a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
+++ b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
@@ -22,15 +22,17 @@
 import android.os.Build
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.assertEqualBothWays
+import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.assertEqualBothWays
 import com.android.testutils.assertParcelingIsLossless
 import com.android.testutils.parcelingRoundTrip
+import java.net.Inet6Address
 import java.net.InetAddress
+import kotlin.test.assertFailsWith
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotEquals
-import org.junit.Assert.fail
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -41,15 +43,36 @@
     @Rule @JvmField
     val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule()
 
-    /* Refer to the definition in {@code NattKeepalivePacketData} */
-    private val IPV4_HEADER_LENGTH = 20
-    private val UDP_HEADER_LENGTH = 8
-
     private val TEST_PORT = 4243
     private val TEST_PORT2 = 4244
+    // ::FFFF:1.2.3.4
+    private val SRC_V4_MAPPED_V6_ADDRESS_BYTES = byteArrayOf(
+        0x00.toByte(),
+        0x00.toByte(),
+        0x00.toByte(),
+        0x00.toByte(),
+        0x00.toByte(),
+        0x00.toByte(),
+        0x00.toByte(),
+        0x00.toByte(),
+        0x00.toByte(),
+        0x00.toByte(),
+        0xff.toByte(),
+        0xff.toByte(),
+        0x01.toByte(),
+        0x02.toByte(),
+        0x03.toByte(),
+        0x04.toByte()
+    )
     private val TEST_SRC_ADDRV4 = "198.168.0.2".address()
     private val TEST_DST_ADDRV4 = "198.168.0.1".address()
     private val TEST_ADDRV6 = "2001:db8::1".address()
+    // This constant requires to be an Inet6Address, but InetAddresses.parseNumericAddress() will
+    // convert v4 mapped v6 address into an Inet4Address. So use Inet6Address.getByAddress() to
+    // create the address.
+    private val TEST_ADDRV4MAPPEDV6 = Inet6Address.getByAddress(null /* host */,
+        SRC_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */)
+    private val TEST_ADDRV4 = "1.2.3.4".address()
 
     private fun String.address() = InetAddresses.parseNumericAddress(this)
     private fun nattKeepalivePacket(
@@ -59,43 +82,62 @@
         dstPort: Int = NATT_PORT
     ) = NattKeepalivePacketData.nattKeepalivePacket(srcAddress, srcPort, dstAddress, dstPort)
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testConstructor() {
-        try {
+        assertFailsWith<InvalidPacketException>(
+            "Dst port is not NATT port should cause exception") {
             nattKeepalivePacket(dstPort = TEST_PORT)
-            fail("Dst port is not NATT port should cause exception")
-        } catch (e: InvalidPacketException) {
-            assertEquals(e.error, ERROR_INVALID_PORT)
+        }.let {
+            assertEquals(it.error, ERROR_INVALID_PORT)
         }
 
-        try {
+        assertFailsWith<InvalidPacketException>("A v6 srcAddress should cause exception") {
             nattKeepalivePacket(srcAddress = TEST_ADDRV6)
-            fail("A v6 srcAddress should cause exception")
-        } catch (e: InvalidPacketException) {
-            assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
+        }.let {
+            assertEquals(it.error, ERROR_INVALID_IP_ADDRESS)
         }
 
-        try {
+        assertFailsWith<InvalidPacketException>("A v6 dstAddress should cause exception") {
             nattKeepalivePacket(dstAddress = TEST_ADDRV6)
-            fail("A v6 dstAddress should cause exception")
-        } catch (e: InvalidPacketException) {
-            assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
+        }.let {
+            assertEquals(it.error, ERROR_INVALID_IP_ADDRESS)
         }
 
-        try {
+        assertFailsWith<IllegalArgumentException>("Invalid data should cause exception") {
             parcelingRoundTrip(
-                    NattKeepalivePacketData(TEST_SRC_ADDRV4, TEST_PORT, TEST_DST_ADDRV4, TEST_PORT,
+                NattKeepalivePacketData(TEST_SRC_ADDRV4, TEST_PORT, TEST_DST_ADDRV4, TEST_PORT,
                     byteArrayOf(12, 31, 22, 44)))
-            fail("Invalid data should cause exception")
-        } catch (e: IllegalArgumentException) { }
+        }
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R) @ConnectivityModuleTest
+    fun testConstructor_afterR() {
+        // v4 mapped v6 will be translated to a v4 address.
+        assertFailsWith<InvalidPacketException> {
+            nattKeepalivePacket(srcAddress = TEST_ADDRV6, dstAddress = TEST_ADDRV4MAPPEDV6)
+        }
+        assertFailsWith<InvalidPacketException> {
+            nattKeepalivePacket(srcAddress = TEST_ADDRV4MAPPEDV6, dstAddress = TEST_ADDRV6)
+        }
+
+        // Both src and dst address will be v4 after translation, so it won't cause exception.
+        val packet1 = nattKeepalivePacket(
+            dstAddress = TEST_ADDRV4MAPPEDV6, srcAddress = TEST_ADDRV4MAPPEDV6)
+        assertEquals(TEST_ADDRV4, packet1.srcAddress)
+        assertEquals(TEST_ADDRV4, packet1.dstAddress)
+
+        // Packet with v6 src and v6 dst address is valid.
+        val packet2 = nattKeepalivePacket(srcAddress = TEST_ADDRV6, dstAddress = TEST_ADDRV6)
+        assertEquals(TEST_ADDRV6, packet2.srcAddress)
+        assertEquals(TEST_ADDRV6, packet2.dstAddress)
+    }
+
+    @Test
     fun testParcel() {
         assertParcelingIsLossless(nattKeepalivePacket())
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testEquals() {
         assertEqualBothWays(nattKeepalivePacket(), nattKeepalivePacket())
         assertNotEquals(nattKeepalivePacket(dstAddress = TEST_SRC_ADDRV4), nattKeepalivePacket())
@@ -104,8 +146,8 @@
         assertNotEquals(nattKeepalivePacket(srcPort = TEST_PORT2), nattKeepalivePacket())
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testHashCode() {
         assertEquals(nattKeepalivePacket().hashCode(), nattKeepalivePacket().hashCode())
     }
-}
\ No newline at end of file
+}
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt
index c05cdbd..d640a73 100644
--- a/tests/common/java/android/net/NetworkAgentConfigTest.kt
+++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -16,19 +16,15 @@
 
 package android.net
 
-import android.os.Build
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
 import com.android.modules.utils.build.SdkLevel.isAtLeastS
 import com.android.modules.utils.build.SdkLevel.isAtLeastT
 import com.android.testutils.ConnectivityModuleTest
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.assertParcelingIsLossless
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -36,10 +32,7 @@
 @SmallTest
 @ConnectivityModuleTest
 class NetworkAgentConfigTest {
-    @Rule @JvmField
-    val ignoreRule = DevSdkIgnoreRule()
-
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testParcelNetworkAgentConfig() {
         val config = NetworkAgentConfig.Builder().apply {
             setExplicitlySelected(true)
@@ -58,7 +51,7 @@
         assertParcelingIsLossless(config)
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testBuilder() {
         val testExtraInfo = "mylegacyExtraInfo"
         val config = NetworkAgentConfig.Builder().apply {
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index aae3425..0f0e2f1 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -26,6 +26,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
 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_MMS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -53,6 +54,7 @@
 import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
 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_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_USB;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -60,9 +62,9 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 import static android.os.Process.INVALID_UID;
 
-import static com.android.modules.utils.build.SdkLevel.isAtLeastR;
 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;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.MiscAsserts.assertEmpty;
 import static com.android.testutils.MiscAsserts.assertThrows;
@@ -81,10 +83,11 @@
 import android.net.wifi.aware.PeerHandle;
 import android.net.wifi.aware.WifiAwareNetworkSpecifier;
 import android.os.Build;
-import android.test.suitebuilder.annotation.SmallTest;
 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;
@@ -369,6 +372,9 @@
             .addCapability(NET_CAPABILITY_INTERNET)
             .addCapability(NET_CAPABILITY_EIMS)
             .addCapability(NET_CAPABILITY_NOT_METERED);
+        if (isAtLeastV()) {
+            netCap.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
+        }
         if (isAtLeastS()) {
             final ArraySet<Integer> allowedUids = new ArraySet<>();
             allowedUids.add(4);
@@ -377,10 +383,9 @@
             netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2));
             netCap.setUids(uids);
         }
-        if (isAtLeastR()) {
-            netCap.setOwnerUid(123);
-            netCap.setAdministratorUids(new int[] {5, 11});
-        }
+
+        netCap.setOwnerUid(123);
+        netCap.setAdministratorUids(new int[] {5, 11});
         assertParcelingIsLossless(netCap);
         netCap.setSSID(TEST_SSID);
         testParcelSane(netCap);
@@ -392,10 +397,8 @@
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addCapability(NET_CAPABILITY_EIMS)
                 .addCapability(NET_CAPABILITY_NOT_METERED);
-        if (isAtLeastR()) {
-            netCap.setRequestorPackageName("com.android.test");
-            netCap.setRequestorUid(9304);
-        }
+        netCap.setRequestorPackageName("com.android.test");
+        netCap.setRequestorUid(9304);
         assertParcelingIsLossless(netCap);
         netCap.setSSID(TEST_SSID);
         testParcelSane(netCap);
@@ -760,6 +763,47 @@
     }
 
     @Test
+    public void testSetNetworkSpecifierWithCellularAndSatelliteMultiTransportNc() {
+        final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+        NetworkCapabilities nc = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addTransportType(TRANSPORT_SATELLITE)
+                .setNetworkSpecifier(specifier)
+                .build();
+        // Adding a specifier did not crash with 2 transports if it is cellular + satellite
+        assertEquals(specifier, nc.getNetworkSpecifier());
+    }
+
+    @Test
+    public void testSetNetworkSpecifierWithWifiAndSatelliteMultiTransportNc() {
+        final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+        NetworkCapabilities.Builder nc1 = new NetworkCapabilities.Builder();
+        nc1.addTransportType(TRANSPORT_SATELLITE).addTransportType(TRANSPORT_WIFI);
+        // Adding multiple transports specifier to crash, apart from cellular + satellite
+        // combination
+        assertThrows("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!",
+                IllegalStateException.class,
+                () -> nc1.build().setNetworkSpecifier(specifier));
+        assertThrows("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!",
+                IllegalStateException.class,
+                () -> nc1.setNetworkSpecifier(specifier));
+    }
+
+    @Test
+    public void testSetNetworkSpecifierOnTestWithCellularAndSatelliteMultiTransportNc() {
+        final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+        NetworkCapabilities nc = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_TEST)
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addTransportType(TRANSPORT_SATELLITE)
+                .setNetworkSpecifier(specifier)
+                .build();
+        // Adding a specifier did not crash with 3 transports , TEST + CELLULAR + SATELLITE and if
+        // one is test
+        assertEquals(specifier, nc.getNetworkSpecifier());
+    }
+
+    @Test
     public void testSetNetworkSpecifierOnTestMultiTransportNc() {
         final NetworkSpecifier specifier = CompatUtil.makeEthernetNetworkSpecifier("eth0");
         NetworkCapabilities nc = new NetworkCapabilities.Builder()
@@ -815,16 +859,12 @@
             assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_NOT_ROAMING));
         }
 
-        if (isAtLeastR()) {
-            assertTrue(TEST_SSID.equals(nc2.getSsid()));
-        }
-
+        assertTrue(TEST_SSID.equals(nc2.getSsid()));
         nc1.setSSID(DIFFERENT_TEST_SSID);
         nc2.set(nc1);
         assertEquals(nc1, nc2);
-        if (isAtLeastR()) {
-            assertTrue(DIFFERENT_TEST_SSID.equals(nc2.getSsid()));
-        }
+        assertTrue(DIFFERENT_TEST_SSID.equals(nc2.getSsid()));
+
         if (isAtLeastS()) {
             nc1.setUids(uidRanges(10, 13));
         } else {
@@ -1431,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/NetworkProviderTest.kt b/tests/common/java/android/net/NetworkProviderTest.kt
index fcbb0dd..0d35960 100644
--- a/tests/common/java/android/net/NetworkProviderTest.kt
+++ b/tests/common/java/android/net/NetworkProviderTest.kt
@@ -39,6 +39,12 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.TestableNetworkOfferCallback
+import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.RejectedExecutionException
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.fail
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -47,12 +53,6 @@
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verifyNoMoreInteractions
-import java.util.UUID
-import java.util.concurrent.Executor
-import java.util.concurrent.RejectedExecutionException
-import kotlin.test.assertEquals
-import kotlin.test.assertNotEquals
-import kotlin.test.fail
 
 private const val DEFAULT_TIMEOUT_MS = 5000L
 private const val DEFAULT_NO_CALLBACK_TIMEOUT_MS = 200L
@@ -62,12 +62,11 @@
 private val PROVIDER_NAME = "NetworkProviderTest"
 
 @RunWith(DevSdkIgnoreRunner::class)
-@IgnoreUpTo(Build.VERSION_CODES.Q)
 @ConnectivityModuleTest
 class NetworkProviderTest {
     @Rule @JvmField
     val mIgnoreRule = DevSdkIgnoreRule()
-    private val mCm = context.getSystemService(ConnectivityManager::class.java)
+    private val mCm = context.getSystemService(ConnectivityManager::class.java)!!
     private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread")
 
     @Before
diff --git a/tests/common/java/android/net/NetworkSpecifierTest.kt b/tests/common/java/android/net/NetworkSpecifierTest.kt
index b960417..7edb474 100644
--- a/tests/common/java/android/net/NetworkSpecifierTest.kt
+++ b/tests/common/java/android/net/NetworkSpecifierTest.kt
@@ -15,21 +15,18 @@
  */
 package android.net
 
-import android.os.Build
 import androidx.test.filters.SmallTest
 import com.android.testutils.ConnectivityModuleTest
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.Test
-import org.junit.runner.RunWith
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(DevSdkIgnoreRunner::class)
-@IgnoreUpTo(Build.VERSION_CODES.Q)
 @ConnectivityModuleTest
 class NetworkSpecifierTest {
     private class TestNetworkSpecifier(
diff --git a/tests/common/java/android/net/NetworkStackTest.java b/tests/common/java/android/net/NetworkStackTest.java
index f8f9c72..13550f9 100644
--- a/tests/common/java/android/net/NetworkStackTest.java
+++ b/tests/common/java/android/net/NetworkStackTest.java
@@ -17,16 +17,11 @@
 
 import static org.junit.Assert.assertEquals;
 
-import android.os.Build;
 import android.os.IBinder;
 
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -34,16 +29,13 @@
 
 @RunWith(AndroidJUnit4.class)
 public class NetworkStackTest {
-    @Rule
-    public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule();
-
     @Mock private IBinder mConnectorBinder;
 
     @Before public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testGetService() {
         NetworkStack.setServiceForTest(mConnectorBinder);
         assertEquals(NetworkStack.getService(), mConnectorBinder);
diff --git a/tests/common/java/android/net/NetworkTest.java b/tests/common/java/android/net/NetworkTest.java
index c102cb3..86d2463 100644
--- a/tests/common/java/android/net/NetworkTest.java
+++ b/tests/common/java/android/net/NetworkTest.java
@@ -161,8 +161,7 @@
         assertEquals(16290598925L, three.getNetworkHandle());
     }
 
-    // getNetId() did not exist in Q
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testGetNetId() {
         assertEquals(1234, new Network(1234).getNetId());
         assertEquals(2345, new Network(2345, true).getNetId());
diff --git a/tests/common/java/android/net/RouteInfoTest.java b/tests/common/java/android/net/RouteInfoTest.java
index 5b28b84..154dc4c 100644
--- a/tests/common/java/android/net/RouteInfoTest.java
+++ b/tests/common/java/android/net/RouteInfoTest.java
@@ -31,17 +31,11 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import android.os.Build;
-
-import androidx.core.os.BuildCompat;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.testutils.ConnectivityModuleTest;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -53,9 +47,6 @@
 @SmallTest
 @ConnectivityModuleTest
 public class RouteInfoTest {
-    @Rule
-    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
     private static final int INVALID_ROUTE_TYPE = -1;
 
     private InetAddress Address(String addr) {
@@ -66,11 +57,6 @@
         return new IpPrefix(prefix);
     }
 
-    private static boolean isAtLeastR() {
-        // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R)
-        return Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR();
-    }
-
     @Test
     public void testConstructor() {
         RouteInfo r;
@@ -204,130 +190,108 @@
         assertTrue(r.isDefaultRoute());
         assertTrue(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
+
 
         r = new RouteInfo(Prefix("::/0"), Address("::"), "wlan0");
         assertFalse(r.isHostRoute());
         assertTrue(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertTrue(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0");
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("2001:db8::/48"), null, "wlan0");
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("192.0.2.0/32"), Address("0.0.0.0"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("2001:db8::/128"), Address("::"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("192.0.2.0/32"), null, "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("2001:db8::/128"), null, "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("::/128"), Address("fe80::"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE);
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertTrue(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertTrue(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE);
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertTrue(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertTrue(r.isIPv6UnreachableDefault());
     }
 
     @Test
@@ -376,14 +340,14 @@
         assertParcelingIsLossless(r);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testMtuParceling() {
         final RouteInfo r = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::"), "testiface",
                 RTN_UNREACHABLE, 1450 /* mtu */);
         assertParcelingIsLossless(r);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testMtu() {
         RouteInfo r;
         r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0",
@@ -394,7 +358,7 @@
         assertEquals(0, r.getMtu());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testRouteKey() {
         RouteInfo.RouteKey k1, k2;
         // Only prefix, null gateway and null interface
diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
index c90b1aa..8cef6aa 100644
--- a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
@@ -28,25 +28,18 @@
 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.DevSdkIgnoreRule
 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
-import kotlin.test.assertEquals
 
 @RunWith(JUnit4::class)
 @SmallTest
 class NetworkStatsApiTest {
-    @Rule
-    @JvmField
-    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
-
     private val testStatsEmpty = NetworkStats(0L, 0)
 
     // Note that these variables need to be initialized outside of constructor, initialize
diff --git a/tests/common/java/android/net/netstats/NetworkTemplateTest.kt b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
index 9469243..1b55be9 100644
--- a/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
@@ -165,9 +165,9 @@
                     assertEquals(expectedTemplate, it)
                 }
 
-        // Verify template which matches ethernet and bluetooth networks.
+        // Verify template which matches ethernet, bluetooth and proxy networks.
         // See buildTemplateEthernet and buildTemplateBluetooth.
-        listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
+        listOf(MATCH_ETHERNET, MATCH_BLUETOOTH, MATCH_PROXY).forEach { matchRule ->
             NetworkTemplate.Builder(matchRule).build().let {
                 val expectedTemplate = NetworkTemplate(matchRule,
                         emptyArray<String>() /*subscriberIds*/, emptyArray<String>(),
diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
new file mode 100644
index 0000000..21e34ab
--- /dev/null
+++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package 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;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.InetAddresses;
+import android.net.Network;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.ConnectivityModuleTest;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@ConnectivityModuleTest
+public class NsdServiceInfoTest {
+
+    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 {
+        NsdServiceInfo info = new NsdServiceInfo();
+
+        // Non-ASCII keys.
+        boolean exceptionThrown = false;
+        try {
+            info.setAttribute("猫", "meow");
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertEmptyServiceInfo(info);
+
+        // ASCII keys with '=' character.
+        exceptionThrown = false;
+        try {
+            info.setAttribute("kitten=", "meow");
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertEmptyServiceInfo(info);
+
+        // Single key + value length too long.
+        exceptionThrown = false;
+        try {
+            String longValue = "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo"
+                    + "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo"
+                    + "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo"
+                    + "ooooooooooooooooooooooooooooong";  // 248 characters.
+            info.setAttribute("longcat", longValue);  // Key + value == 255 characters.
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertEmptyServiceInfo(info);
+
+        // Total TXT record length too long.
+        exceptionThrown = false;
+        int recordsAdded = 0;
+        try {
+            for (int i = 100; i < 300; ++i) {
+                // 6 char key + 5 char value + 2 bytes overhead = 13 byte record length.
+                String key = String.format("key%d", i);
+                info.setAttribute(key, "12345");
+                recordsAdded++;
+            }
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertTrue(100 == recordsAdded);
+        assertTrue(info.getTxtRecord().length == 1300);
+    }
+
+    @Test
+    public void testParcel() throws Exception {
+        NsdServiceInfo emptyInfo = new NsdServiceInfo();
+        checkParcelable(emptyInfo);
+
+        NsdServiceInfo fullInfo = new NsdServiceInfo();
+        fullInfo.setServiceName("kitten");
+        fullInfo.setServiceType("_kitten._tcp");
+        fullInfo.setSubtypes(Set.of("_thread", "_matter"));
+        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);
+
+        NsdServiceInfo noHostInfo = new NsdServiceInfo();
+        noHostInfo.setServiceName("kitten");
+        noHostInfo.setServiceType("_kitten._tcp");
+        noHostInfo.setPort(4242);
+        checkParcelable(noHostInfo);
+
+        NsdServiceInfo attributedInfo = new NsdServiceInfo();
+        attributedInfo.setServiceName("kitten");
+        attributedInfo.setServiceType("_kitten._tcp");
+        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);
+        attributedInfo.setAttribute("sticky", "yes");
+        attributedInfo.setAttribute("siblings", new byte[] {});
+        attributedInfo.setAttribute("edge cases", new byte[] {0, -1, 127, -128});
+        attributedInfo.removeAttribute("sticky");
+        checkParcelable(attributedInfo);
+
+        // Sanity check that we actually wrote attributes to attributedInfo.
+        assertTrue(attributedInfo.getAttributes().keySet().contains("adorable"));
+        String sound = new String(attributedInfo.getAttributes().get("sound"), "UTF-8");
+        assertTrue(sound.equals("にゃあ"));
+        byte[] edgeCases = attributedInfo.getAttributes().get("edge cases");
+        assertTrue(Arrays.equals(edgeCases, new byte[] {0, -1, 127, -128}));
+        assertFalse(attributedInfo.getAttributes().keySet().contains("sticky"));
+    }
+
+    private static void checkParcelable(NsdServiceInfo original) {
+        // Write to parcel.
+        Parcel p = Parcel.obtain();
+        Bundle writer = new Bundle();
+        writer.putParcelable("test_info", original);
+        writer.writeToParcel(p, 0);
+
+        // Extract from parcel.
+        p.setDataPosition(0);
+        Bundle reader = p.readBundle();
+        reader.setClassLoader(NsdServiceInfo.class.getClassLoader());
+        NsdServiceInfo result = reader.getParcelable("test_info");
+
+        // Assert equality of base fields.
+        assertEquals(original.getServiceName(), result.getServiceName());
+        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());
+
+        // Assert equality of attribute map.
+        Map<String, byte[]> originalMap = original.getAttributes();
+        Map<String, byte[]> resultMap = result.getAttributes();
+        assertEquals(originalMap.keySet(), resultMap.keySet());
+        for (String key : originalMap.keySet()) {
+            assertTrue(Arrays.equals(originalMap.get(key), resultMap.get(key)));
+        }
+    }
+
+    private static void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
+        byte[] txtRecord = shouldBeEmpty.getTxtRecord();
+        if (txtRecord == null || txtRecord.length == 0) {
+            return;
+        }
+        fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord));
+    }
+
+    @Test
+    public void testSubtypesValidSubtypesSuccess() {
+        NsdServiceInfo info = new NsdServiceInfo();
+
+        info.setSubtypes(Set.of("_thread", "_matter"));
+
+        assertEquals(Set.of("_thread", "_matter"), info.getSubtypes());
+    }
+}
diff --git a/tests/common/java/android/net/util/SocketUtilsTest.kt b/tests/common/java/android/net/util/SocketUtilsTest.kt
index aaf97f3..520cf07 100644
--- a/tests/common/java/android/net/util/SocketUtilsTest.kt
+++ b/tests/common/java/android/net/util/SocketUtilsTest.kt
@@ -16,7 +16,6 @@
 
 package android.net.util
 
-import android.os.Build
 import android.system.NetlinkSocketAddress
 import android.system.Os
 import android.system.OsConstants.AF_INET
@@ -27,13 +26,10 @@
 import android.system.PacketSocketAddress
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -44,9 +40,6 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class SocketUtilsTest {
-    @Rule @JvmField
-    val ignoreRule = DevSdkIgnoreRule()
-
     @Test
     fun testMakeNetlinkSocketAddress() {
         val nlAddress = SocketUtils.makeNetlinkSocketAddress(TEST_PORT, RTMGRP_NEIGH)
@@ -67,7 +60,7 @@
         assertTrue("Not PacketSocketAddress object", pkAddress2 is PacketSocketAddress)
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testMakePacketSocketAddress() {
         val pkAddress = SocketUtils.makePacketSocketAddress(
                 ETH_P_ALL, TEST_INDEX, ByteArray(6) { FF_BYTE })
diff --git a/tests/cts/OWNERS b/tests/cts/OWNERS
index 8388cb7..acf506d 100644
--- a/tests/cts/OWNERS
+++ b/tests/cts/OWNERS
@@ -1,6 +1,14 @@
 # Bug template url: http://b/new?component=31808
+# TODO: move bug template config to common owners file once b/226427845 is resolved
 set noparent
-file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking_xts
+file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking_xts
 
 # IPsec
 per-file **IpSec* = benedictwong@google.com, nharold@google.com
+
+# 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-network-policy/Android.bp b/tests/cts/hostside-network-policy/Android.bp
new file mode 100644
index 0000000..c3ce0b9
--- /dev/null
+++ b/tests/cts/hostside-network-policy/Android.bp
@@ -0,0 +1,46 @@
+// 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_framework_backstage_power",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsHostsideNetworkPolicyTests",
+    defaults: ["cts_defaults"],
+    // Only compile source java files in this apk.
+    srcs: [
+        "src/**/*.java",
+        ":ArgumentConstants",
+    ],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+    ],
+    static_libs: [
+        "modules-utils-build-testing",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "sts",
+    ],
+    data: [
+        ":CtsHostsideNetworkPolicyTestsApp",
+        ":CtsHostsideNetworkPolicyTestsApp2",
+    ],
+    per_testcase_directory: true,
+}
diff --git a/tests/cts/hostside-network-policy/AndroidTest.xml b/tests/cts/hostside-network-policy/AndroidTest.xml
new file mode 100644
index 0000000..44f77f8
--- /dev/null
+++ b/tests/cts/hostside-network-policy/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 CTS network policy host test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
+    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <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.netpolicy.NetworkPolicyTestsPreparer" />
+
+    <!-- Enabling change id ALLOW_TEST_API_ACCESS allows that package to access @TestApi methods -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="am compat enable ALLOW_TEST_API_ACCESS com.android.cts.netpolicy.hostside.app2" />
+        <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS com.android.cts.netpolicy.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">
+        <option name="force-skip-system-props" value="true" />
+        <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
+        <option name="set-global-setting" key="low_power_standby_enabled" value="0" />
+    </target_preparer>
+
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsHostsideNetworkPolicyTests.jar" />
+        <option name="runtime-hint" value="3m56s" />
+    </test>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/sdcard/CtsHostsideNetworkPolicyTests" />
+        <option name="collect-on-run-ended-only" value="true" />
+    </metrics_collector>
+</configuration>
diff --git a/tests/cts/hostside-network-policy/OWNERS b/tests/cts/hostside-network-policy/OWNERS
new file mode 100644
index 0000000..ea83e61
--- /dev/null
+++ b/tests/cts/hostside-network-policy/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 61373
+# Inherits parent owners
+include platform/frameworks/base:/services/core/java/com/android/server/net/OWNERS
diff --git a/tests/cts/hostside-network-policy/TEST_MAPPING b/tests/cts/hostside-network-policy/TEST_MAPPING
new file mode 100644
index 0000000..57ac4f7
--- /dev/null
+++ b/tests/cts/hostside-network-policy/TEST_MAPPING
@@ -0,0 +1,27 @@
+{
+  "presubmit-large": [
+    {
+      "name": "CtsHostsideNetworkPolicyTests",
+      "options": [
+        {
+          "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+        },
+        {
+          "exclude-annotation": "android.platform.test.annotations.RequiresDevice"
+        }
+      ]
+    }
+  ],
+  "postsubmit": [
+    {
+      // Postsubmit on virtual devices to monitor flakiness of all tests that don't require a
+      // physical device
+      "name": "CtsHostsideNetworkPolicyTests",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/cts/hostside-network-policy/aidl/Android.bp b/tests/cts/hostside-network-policy/aidl/Android.bp
new file mode 100644
index 0000000..b182090
--- /dev/null
+++ b/tests/cts/hostside-network-policy/aidl/Android.bp
@@ -0,0 +1,26 @@
+// 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_framework_backstage_power",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_helper_library {
+    name: "CtsHostsideNetworkPolicyTestsAidl",
+    sdk_version: "current",
+    srcs: [
+        "com/android/cts/netpolicy/hostside/*.aidl",
+    ],
+}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl
new file mode 100644
index 0000000..068d9d8
--- /dev/null
+++ b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl
@@ -0,0 +1,33 @@
+/*
+ * 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.netpolicy.hostside;
+
+import android.app.job.JobInfo;
+
+import com.android.cts.netpolicy.hostside.INetworkCallback;
+import com.android.cts.netpolicy.hostside.NetworkCheckResult;
+
+interface IMyService {
+    void registerBroadcastReceiver();
+    int getCounters(String receiverName, String action);
+    NetworkCheckResult checkNetworkStatus(String customUrl);
+    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-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl
new file mode 100644
index 0000000..38efc7b
--- /dev/null
+++ b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.netpolicy.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-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl
new file mode 100644
index 0000000..c6b7a1c
--- /dev/null
+++ b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl
@@ -0,0 +1,30 @@
+/*
+ * 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.netpolicy.hostside;
+
+import android.net.NetworkInfo;
+
+import com.android.cts.netpolicy.hostside.NetworkCheckResult;
+
+interface INetworkStateObserver {
+    void onNetworkStateChecked(int resultCode, in NetworkCheckResult networkCheckResult);
+
+    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-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
new file mode 100644
index 0000000..7aac2ab
--- /dev/null
+++ b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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.netpolicy.hostside;
+
+import android.net.NetworkInfo;
+
+@JavaDerive(toString=true)
+parcelable NetworkCheckResult {
+   boolean connected;
+   String details;
+   NetworkInfo networkInfo;
+}
\ No newline at end of file
diff --git a/tests/cts/hostside-network-policy/app/Android.bp b/tests/cts/hostside-network-policy/app/Android.bp
new file mode 100644
index 0000000..a31c843
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/Android.bp
@@ -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.
+//
+
+package {
+    default_team: "trendy_team_framework_backstage_power",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "CtsHostsideNetworkPolicyTestsAppDefaults",
+    platform_apis: true,
+    static_libs: [
+        "CtsHostsideNetworkPolicyTestsAidl",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "androidx.test.uiautomator_uiautomator",
+        "compatibility-device-util-axt",
+        "cts-net-utils",
+        "ctstestrunner-axt",
+        "modules-utils-build",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    srcs: [
+        "src/**/*.java",
+        ":ArgumentConstants",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "general-tests",
+        "sts",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkPolicyTestsApp",
+    defaults: [
+        "cts_support_defaults",
+        "framework-connectivity-test-defaults",
+        "CtsHostsideNetworkPolicyTestsAppDefaults",
+    ],
+}
diff --git a/tests/cts/hostside-network-policy/app/AndroidManifest.xml b/tests/cts/hostside-network-policy/app/AndroidManifest.xml
new file mode 100644
index 0000000..f19e35f
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/AndroidManifest.xml
@@ -0,0 +1,50 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.netpolicy.hostside">
+
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+    <application android:requestLegacyExternalStorage="true">
+        <uses-library android:name="android.test.runner"/>
+        <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"
+         android:targetPackage="com.android.cts.netpolicy.hostside"/>
+
+</manifest>
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java
new file mode 100644
index 0000000..19e4364
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java
@@ -0,0 +1,201 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java
new file mode 100644
index 0000000..ae226e2
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java
@@ -0,0 +1,111 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
new file mode 100644
index 0000000..00f67f4
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
+
+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();
+
+        stopApp();
+        removePowerSaveModeWhitelist(TEST_APP2_PKG);
+        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+    }
+
+    @Test
+    public void testFgsNetworkAccess() throws Exception {
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
+        assertNetworkAccess(false, null);
+
+        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+    }
+
+    @Test
+    public void testActivityNetworkAccess() throws Exception {
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
+        assertNetworkAccess(false, null);
+
+        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+    }
+
+    @Test
+    public void testBackgroundNetworkAccess_inFullAllowlist() throws Exception {
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
+        assertNetworkAccess(false, null);
+
+        addPowerSaveModeWhitelist(TEST_APP2_PKG);
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+        assertNetworkAccess(true, null);
+    }
+
+    @Test
+    public void testBackgroundNetworkAccess_inExceptIdleAllowlist() throws Exception {
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
+        assertNetworkAccess(false, null);
+
+        addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+        assertNetworkAccess(true, null);
+    }
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java
new file mode 100644
index 0000000..0c8cb70
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java
@@ -0,0 +1,143 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+
+import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
+import static com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java
new file mode 100644
index 0000000..5435920
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
+import static com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
new file mode 100644
index 0000000..0f5f58c
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -0,0 +1,1165 @@
+/*
+ * 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.netpolicy.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.netpolicy.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
+import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.executeShellCommand;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.forceRunJob;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getConnectivityManager;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getContext;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getInstrumentation;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString;
+import static com.android.cts.netpolicy.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;
+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.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+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.netpolicy.hostside";
+    protected static final String TEST_APP2_PKG = "com.android.cts.netpolicy.hostside.app2";
+    // TODO(b/321797685): Configure it via device-config once it is available.
+    protected final long mProcessStateTransitionLongDelayMs =
+            useDifferentDelaysForBackgroundChain() ? TimeUnit.SECONDS.toMillis(20)
+                    : TimeUnit.SECONDS.toMillis(5);
+    protected final long mProcessStateTransitionShortDelayMs =
+            useDifferentDelaysForBackgroundChain() ? TimeUnit.SECONDS.toMillis(2)
+                    : 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.netpolicy.hostside.app2.action.FINISH_ACTIVITY";
+    private static final String ACTION_FINISH_JOB =
+            "com.android.cts.netpolicy.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.netpolicy.hostside.app2.action.RECEIVER_READY";
+    static final String ACTION_SHOW_TOAST =
+            "com.android.cts.netpolicy.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 KEY_CUSTOM_URL = TEST_PKG + ".custom_url";
+
+    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 @Nullable String mCustomUrl;
+    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();
+        mCustomUrl = args.getString(ARG_CONNECTION_CHECK_CUSTOM_URL);
+        if (mCustomUrl != null) {
+            Log.d(TAG, "Using custom URL " + mCustomUrl + " for network checks");
+        }
+
+        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);
+    }
+
+    /**
+     * Check if the flag to use different delays for sensitive 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_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN once the tests are moved to cts.
+     */
+    private boolean useDifferentDelaysForBackgroundChain() {
+        if (!SdkLevel.isAtLeastV()) {
+            return false;
+        }
+        final String output = executeShellCommand("device_config get backstage_power"
+                + " com.android.server.net.use_different_delays_for_background_chain");
+        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 NetworkCheckResult checkResult = mServiceClient.checkNetworkStatus(mCustomUrl);
+        return checkForAvailabilityInNetworkCheckResult(checkResult, expectAvailable,
+                expectedUnavailableError);
+    }
+
+    private String checkForAvailabilityInNetworkCheckResult(NetworkCheckResult networkCheckResult,
+            boolean expectAvailable, @Nullable final String expectedUnavailableError) {
+        assertNotNull("NetworkCheckResult from app2 is null", networkCheckResult);
+
+        final NetworkInfo networkInfo = networkCheckResult.networkInfo;
+        assertNotNull("NetworkInfo from app2 is null", networkInfo);
+
+        final State state = networkInfo.getState();
+        final DetailedState detailedState = networkInfo.getDetailedState();
+
+        final boolean connected = networkCheckResult.connected;
+        final String connectionCheckDetails = networkCheckResult.details;
+
+        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 stopApp() {
+        executeSilentShellCommand("am stop-app " + TEST_APP2_PKG);
+    }
+
+    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();
+        } else if (type == TYPE_COMPONENT_ACTIVTIY) {
+            turnScreenOn();
+            final CountDownLatch latch = new CountDownLatch(1);
+            final Intent launchIntent = getIntentForComponent(type);
+            final Bundle extras = new Bundle();
+            final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
+                    new AtomicReference<>();
+            extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
+            extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
+            extras.putString(KEY_CUSTOM_URL, mCustomUrl);
+            launchIntent.putExtras(extras);
+            mContext.startActivity(launchIntent);
+            if (latch.await(ACTIVITY_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                final int resultCode = result.get().first;
+                final NetworkCheckResult networkCheckResult = result.get().second;
+                if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
+                    final String error = checkForAvailabilityInNetworkCheckResult(
+                            networkCheckResult, 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, networkCheckResult.details);
+                    // App didn't come to foreground when the activity is started, so try again.
+                    assertTopNetworkAccess(true);
+                } else {
+                    fail("Unexpected resultCode=" + resultCode
+                            + "; networkCheckResult=[" + networkCheckResult + "]");
+                }
+            } 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 AtomicReference<Pair<Integer, NetworkCheckResult>> result =
+                    new AtomicReference<>();
+            final CountDownLatch latch = new CountDownLatch(1);
+            extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
+            extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
+            extras.putString(KEY_CUSTOM_URL, mCustomUrl);
+            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().first;
+                final NetworkCheckResult networkCheckResult = result.get().second;
+                if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
+                    final String error = checkForAvailabilityInNetworkCheckResult(
+                            networkCheckResult, 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
+                            + "; networkCheckResult=[" + networkCheckResult + "]");
+                }
+            } 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 AtomicReference<Pair<Integer, NetworkCheckResult>> result) {
+        return new INetworkStateObserver.Stub() {
+            @Override
+            public void onNetworkStateChecked(int resultCode,
+                    NetworkCheckResult networkCheckResult) {
+                result.set(Pair.create(resultCode, networkCheckResult));
+                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-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java
new file mode 100644
index 0000000..6b802f6
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class AppIdleMeteredTest extends AbstractAppIdleTestCase {
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java
new file mode 100644
index 0000000..2e725ae
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class AppIdleNonMeteredTest extends AbstractAppIdleTestCase {
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java
new file mode 100644
index 0000000..2e421f6
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class BatterySaverModeMeteredTest extends AbstractBatterySaverModeTestCase {
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java
new file mode 100644
index 0000000..0be5644
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java
@@ -0,0 +1,24 @@
+/*
+ * 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.netpolicy.hostside;
+
+
+import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class BatterySaverModeNonMeteredTest extends AbstractBatterySaverModeTestCase {
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
new file mode 100644
index 0000000..bfccce9
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.netpolicy.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.netpolicy.hostside.NetworkPolicyTestUtils.getUiDevice;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
+import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
+import static com.android.cts.netpolicy.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();
+        stopApp();
+        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);
+        assertNetworkAccess(false, null);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver", null);
+    }
+
+    @Test
+    @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
+    public void testStartActivity_dataSaver() throws Exception {
+        setRestrictBackground(true);
+        assertNetworkAccess(false, null);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver", null);
+    }
+
+    @Test
+    @RequiredProperties({DOZE_MODE})
+    public void testStartActivity_doze() throws Exception {
+        setDozeMode(true);
+        assertNetworkAccess(false, null);
+        // 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);
+        assertNetworkAccess(false, null);
+        // 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(mProcessStateTransitionLongDelayMs);
+            assertNetworkAccess(false, null);
+        });
+    }
+
+    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-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java
new file mode 100644
index 0000000..66e0d00
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java
@@ -0,0 +1,213 @@
+/*
+ * 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.netpolicy.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.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
+import static com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java
new file mode 100644
index 0000000..69ca206
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java
new file mode 100644
index 0000000..810fd19
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class DefaultRestrictionsMeteredTest extends AbstractDefaultRestrictionsTest {
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java
new file mode 100644
index 0000000..fef546c
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class DefaultRestrictionsNonMeteredTest extends AbstractDefaultRestrictionsTest {
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java
new file mode 100644
index 0000000..741dd7e
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class DozeModeMeteredTest extends AbstractDozeModeTestCase {
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java
new file mode 100644
index 0000000..f343df5
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class DozeModeNonMeteredTest extends AbstractDozeModeTestCase {
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java
new file mode 100644
index 0000000..2dc6cc4
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java
@@ -0,0 +1,118 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
+import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_APP2_PKG;
+import static com.android.cts.netpolicy.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(),
+            "CtsHostsideNetworkPolicyTests");
+
+    @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-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java
new file mode 100644
index 0000000..d56a50b
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class ExpeditedJobMeteredTest extends AbstractExpeditedJobTest {
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java
new file mode 100644
index 0000000..0a776ee
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class ExpeditedJobNonMeteredTest extends AbstractExpeditedJobTest {
+}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java
new file mode 100644
index 0000000..4f4e68e
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java
@@ -0,0 +1,59 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setupActiveNetworkMeteredness;
+import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
+import static com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java
new file mode 100644
index 0000000..b0fa106
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java
@@ -0,0 +1,370 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
+import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
+import static com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java
new file mode 100644
index 0000000..6dc9921
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java
@@ -0,0 +1,123 @@
+/*
+ * 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.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java
new file mode 100644
index 0000000..71b28f6
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java
@@ -0,0 +1,133 @@
+/*
+ * 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.netpolicy.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);
+    }
+
+    /** Retrieves the network state as observed from the bound test app */
+    public NetworkCheckResult checkNetworkStatus(String address) throws RemoteException {
+        ensureServiceConnection();
+        return mService.checkNetworkStatus(address);
+    }
+
+    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-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
new file mode 100644
index 0000000..3934cfa
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
@@ -0,0 +1,428 @@
+/*
+ * 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.netpolicy.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.netpolicy.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getActiveNetworkCapabilities;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.netpolicy.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();
+        stopApp();
+
+        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(mProcessStateTransitionLongDelayMs);
+            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(mProcessStateTransitionLongDelayMs);
+            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-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
new file mode 100644
index 0000000..6c5f2ff
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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.netpolicy.hostside;
+
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
+import static android.os.Process.SYSTEM_UID;
+
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.assertNetworkingBlockedStatusForUid;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isUidNetworkingBlocked;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.netpolicy.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();
+        stopApp();
+    }
+
+    @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_LAST_ACTIVITY);
+            SystemClock.sleep(mProcessStateTransitionShortDelayMs);
+            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(mProcessStateTransitionLongDelayMs);
+            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-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java
new file mode 100644
index 0000000..0207b00
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java
@@ -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.
+ */
+
+package com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java
new file mode 100644
index 0000000..26a88f2
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java
@@ -0,0 +1,486 @@
+/*
+ * 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.netpolicy.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.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java
new file mode 100644
index 0000000..a03833f
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDataSaverSupported;
+import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
+import static com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java
new file mode 100644
index 0000000..799a513
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java
@@ -0,0 +1,31 @@
+/*
+ * 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.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java
new file mode 100644
index 0000000..5dea67c
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.netpolicy.hostside;
+
+import static com.android.cts.netpolicy.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-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java
new file mode 100644
index 0000000..f183f4e
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.netpolicy.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-network-policy/app2/Android.bp b/tests/cts/hostside-network-policy/app2/Android.bp
new file mode 100644
index 0000000..6ef0b06
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app2/Android.bp
@@ -0,0 +1,39 @@
+//
+// 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_framework_backstage_power",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkPolicyTestsApp2",
+    defaults: ["cts_support_defaults"],
+    platform_apis: true,
+    static_libs: [
+        "androidx.annotation_annotation",
+        "CtsHostsideNetworkPolicyTestsAidl",
+        "modules-utils-build",
+    ],
+    srcs: ["src/**/*.java"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "sts",
+    ],
+    certificate: ":cts-netpolicy-app",
+}
diff --git a/tests/cts/hostside-network-policy/app2/AndroidManifest.xml b/tests/cts/hostside-network-policy/app2/AndroidManifest.xml
new file mode 100644
index 0000000..668f2da
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app2/AndroidManifest.xml
@@ -0,0 +1,77 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.netpolicy.hostside.app2">
+
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
+    <!--
+     This application is used to listen to RESTRICT_BACKGROUND_CHANGED intents and store
+     them in a shared preferences which is then read by the test app. These broadcasts are
+     handled by 2 listeners, one defined the manifest and another dynamically registered by
+     a service.
+
+     The manifest-defined listener also handles ordered broadcasts used to share data with the
+     test app.
+
+     This application also provides a service, RemoteSocketFactoryService, that the test app can
+     use to open sockets to remote hosts as a different user ID.
+    -->
+    <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>
+        <receiver android:name=".MyBroadcastReceiver"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.net.conn.RESTRICT_BACKGROUND_CHANGED"/>
+                <action android:name="com.android.cts.netpolicy.hostside.app2.action.GET_COUNTERS"/>
+                <action android:name="com.android.cts.netpolicy.hostside.app2.action.GET_RESTRICT_BACKGROUND_STATUS"/>
+                <action android:name="com.android.cts.netpolicy.hostside.app2.action.CHECK_NETWORK"/>
+                <action android:name="com.android.cts.netpolicy.hostside.app2.action.SEND_NOTIFICATION"/>
+                <action android:name="com.android.cts.netpolicy.hostside.app2.action.SHOW_TOAST"/>
+                </intent-filter>
+        </receiver>
+        <service android:name=".MyJobService"
+            android:permission="android.permission.BIND_JOB_SERVICE" />
+    </application>
+
+    <!--
+      Adding this to make sure that receiving the broadcast is not restricted by
+      package visibility restrictions.
+    -->
+    <queries>
+        <package android:name="android" />
+    </queries>
+
+</manifest>
diff --git a/tests/cts/hostside/app2/res/drawable/ic_notification.png b/tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png
similarity index 100%
rename from tests/cts/hostside/app2/res/drawable/ic_notification.png
rename to tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png
Binary files differ
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java
new file mode 100644
index 0000000..1719f9b
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java
@@ -0,0 +1,234 @@
+/*
+ * 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.netpolicy.hostside.app2;
+
+import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_OTHER;
+import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES;
+import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.cts.netpolicy.hostside.INetworkStateObserver;
+import com.android.cts.netpolicy.hostside.NetworkCheckResult;
+
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+
+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.netpolicy.hostside.app2.action.RECEIVER_READY";
+    static final String ACTION_FINISH_ACTIVITY =
+            "com.android.cts.netpolicy.hostside.app2.action.FINISH_ACTIVITY";
+    static final String ACTION_FINISH_JOB =
+            "com.android.cts.netpolicy.hostside.app2.action.FINISH_JOB";
+    static final String ACTION_SHOW_TOAST =
+            "com.android.cts.netpolicy.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";
+
+    private static final String DEFAULT_TEST_URL =
+            "https://connectivitycheck.android.com/generate_204";
+
+    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.netpolicy.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 String KEY_CUSTOM_URL =  TEST_PKG + ".custom_url";
+
+    static final int TYPE_COMPONENT_ACTIVTY = 0;
+    static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
+    static final int TYPE_COMPONENT_EXPEDITED_JOB = 2;
+    private static final int NETWORK_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10);
+
+    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 NetworkCheckResult createNetworkCheckResult(boolean connected, String details,
+            NetworkInfo networkInfo) {
+        final NetworkCheckResult checkResult = new NetworkCheckResult();
+        checkResult.connected = connected;
+        checkResult.details = details;
+        checkResult.networkInfo = networkInfo;
+        return checkResult;
+    }
+
+    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(RESULT_ERROR_UNEXPECTED_PROC_STATE,
+                            createNetworkCheckResult(false, "Unexpected procstate: " + procState,
+                                    null));
+                    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(RESULT_ERROR_UNEXPECTED_PROC_STATE,
+                            createNetworkCheckResult(false, "Unexpected procstate: " + procState,
+                                    null));
+                    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(RESULT_ERROR_UNEXPECTED_CAPABILITIES,
+                            createNetworkCheckResult(false,
+                                    "Unexpected capabilities: " + capabilities, null));
+                    return false;
+                }
+                return true;
+            }
+            default: {
+                observer.onNetworkStateChecked(RESULT_ERROR_OTHER,
+                        createNetworkCheckResult(false, "Unknown component type: " + componentType,
+                                null));
+                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) {
+            final String customUrl = extras.getString(KEY_CUSTOM_URL);
+            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,
+                            checkNetworkStatus(context, customUrl));
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error occurred while notifying the observer: " + e);
+                }
+            });
+        }
+    }
+
+    /**
+     * Checks whether the network is available by attempting a connection to the given address
+     * and returns a {@link NetworkCheckResult} object containing all the relevant details for
+     * debugging. Uses a default address if the given address is {@code null}.
+     *
+     * <p>
+     * The returned object has the following fields:
+     *
+     * <ul>
+     * <li>{@code connected}: whether or not the connection was successful.
+     * <li>{@code networkInfo}: the {@link NetworkInfo} describing the current active network as
+     * visible to this app.
+     * <li>{@code details}: A human readable string giving useful information about the success or
+     * failure.
+     * </ul>
+     */
+    static NetworkCheckResult checkNetworkStatus(Context context, String customUrl) {
+        final String address = (customUrl == null) ? DEFAULT_TEST_URL : customUrl;
+
+        // The current Android DNS resolver returns an UnknownHostException whenever network access
+        // is blocked. This can get cached in the current process-local InetAddress cache. Clearing
+        // the cache before attempting a connection ensures we never report a failure due to a
+        // negative cache entry.
+        InetAddress.clearDnsCache();
+
+        final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
+
+        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.connect();
+            final int response = conn.getResponseCode();
+            checkStatus = true;
+            checkDetails = "HTTP response for " + address + ": " + response;
+        } catch (Exception e) {
+            checkStatus = false;
+            checkDetails = "Exception getting " + address + ": " + e;
+        }
+        final NetworkCheckResult result = createNetworkCheckResult(checkStatus, checkDetails,
+                networkInfo);
+        Log.d(TAG, "Offering: " + result);
+        return result;
+    }
+}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java
new file mode 100644
index 0000000..d274c50
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java
@@ -0,0 +1,102 @@
+/*
+ * 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.netpolicy.hostside.app2;
+
+import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_FINISH_ACTIVITY;
+import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
+import static com.android.cts.netpolicy.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-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java
new file mode 100644
index 0000000..27aec8c
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java
@@ -0,0 +1,193 @@
+/*
+ * 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.netpolicy.hostside.app2;
+
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+
+import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_RECEIVER_READY;
+import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SHOW_TOAST;
+import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SNOOZE_WARNING;
+import static com.android.cts.netpolicy.hostside.app2.Common.MANIFEST_RECEIVER;
+import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION;
+import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_BUNDLE;
+import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_REMOTE_INPUT;
+import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_BUNDLE;
+import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_CONTENT;
+import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_DELETE;
+import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_FULL_SCREEN;
+import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
+
+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.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+/**
+ * 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 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);
+    }
+
+    /**
+     * 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-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java
new file mode 100644
index 0000000..54cee3c
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java
@@ -0,0 +1,73 @@
+/*
+ * 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.netpolicy.hostside.app2;
+
+import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
+import static com.android.cts.netpolicy.hostside.app2.Common.TEST_PKG;
+import static com.android.cts.netpolicy.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.netpolicy.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-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java
new file mode 100644
index 0000000..eba55ed
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java
@@ -0,0 +1,82 @@
+/*
+ * 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.netpolicy.hostside.app2;
+
+import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_FINISH_JOB;
+import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
+import static com.android.cts.netpolicy.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-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java
new file mode 100644
index 0000000..71bcead
--- /dev/null
+++ b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java
@@ -0,0 +1,200 @@
+/*
+ * 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.netpolicy.hostside.app2;
+
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+
+import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_RECEIVER_READY;
+import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SNOOZE_WARNING;
+import static com.android.cts.netpolicy.hostside.app2.Common.DYNAMIC_RECEIVER;
+import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
+
+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.netpolicy.hostside.IMyService;
+import com.android.cts.netpolicy.hostside.INetworkCallback;
+import com.android.cts.netpolicy.hostside.NetworkCheckResult;
+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 NetworkCheckResult checkNetworkStatus(String customUrl) {
+            return Common.checkNetworkStatus(getApplicationContext(), customUrl);
+        }
+
+        @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-network-policy/certs/Android.bp b/tests/cts/hostside-network-policy/certs/Android.bp
new file mode 100644
index 0000000..bfbc341
--- /dev/null
+++ b/tests/cts/hostside-network-policy/certs/Android.bp
@@ -0,0 +1,9 @@
+package {
+    default_team: "trendy_team_framework_backstage_power",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app_certificate {
+    name: "cts-netpolicy-app",
+    certificate: "cts-net-app",
+}
diff --git a/tests/cts/hostside/certs/README b/tests/cts/hostside-network-policy/certs/README
similarity index 100%
rename from tests/cts/hostside/certs/README
rename to tests/cts/hostside-network-policy/certs/README
diff --git a/tests/cts/hostside/certs/cts-net-app.pk8 b/tests/cts/hostside-network-policy/certs/cts-net-app.pk8
similarity index 100%
rename from tests/cts/hostside/certs/cts-net-app.pk8
rename to tests/cts/hostside-network-policy/certs/cts-net-app.pk8
Binary files differ
diff --git a/tests/cts/hostside/certs/cts-net-app.x509.pem b/tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem
similarity index 100%
rename from tests/cts/hostside/certs/cts-net-app.x509.pem
rename to tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem
diff --git a/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp b/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp
new file mode 100644
index 0000000..cdede36
--- /dev/null
+++ b/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp
@@ -0,0 +1,22 @@
+// 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_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "ArgumentConstants",
+    srcs: ["src/**/*.java"],
+}
diff --git a/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java b/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java
new file mode 100644
index 0000000..0fe98e9
--- /dev/null
+++ b/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java
@@ -0,0 +1,22 @@
+/*
+ * 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.netpolicy.arguments;
+
+public interface InstrumentationArguments {
+    String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
+    String ARG_CONNECTION_CHECK_CUSTOM_URL = "connection_check_custom_url";
+}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java
new file mode 100644
index 0000000..422231d
--- /dev/null
+++ b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.netpolicy;
+
+import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
+import android.platform.test.annotations.FlakyTest;
+
+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;
+
+@FlakyTest(bugId = 288324467)
+public class HostsideConnOnActivityStartTest extends HostsideNetworkPolicyTestCase {
+    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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
+    }
+
+    @Test
+    public void testStartActivity_dataSaver() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
+    }
+
+    @FlakyTest(bugId = 231440256)
+    @Test
+    public void testStartActivity_doze() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
+    }
+
+    @Test
+    public void testStartActivity_appStandby() throws Exception {
+        runDeviceTestsWithCustomOptions(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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_default",
+                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+    }
+}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java
new file mode 100644
index 0000000..62952bb
--- /dev/null
+++ b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java
@@ -0,0 +1,104 @@
+/*
+ * 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.netpolicy;
+
+import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
+import android.platform.test.annotations.FlakyTest;
+
+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.
+@FlakyTest(bugId = 288324467)
+public class HostsideDefaultNetworkRestrictionsTests extends HostsideNetworkPolicyTestCase {
+    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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG, METERED_TEST_CLASS, methodName,
+                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+    }
+
+    private void runNonMeteredTest(String methodName) throws DeviceNotAvailableException {
+        runDeviceTestsWithCustomOptions(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-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java
new file mode 100644
index 0000000..2c2b118
--- /dev/null
+++ b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java
@@ -0,0 +1,61 @@
+/*
+ * 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.netpolicy;
+
+import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
+import android.platform.test.annotations.FlakyTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+@FlakyTest(bugId = 288324467)
+public class HostsideNetworkCallbackTests extends HostsideNetworkPolicyTestCase {
+
+    @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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG,
+                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
+    }
+
+    @Test
+    public void testOnBlockedStatusChanged_powerSaver() throws Exception {
+        runDeviceTestsWithCustomOptions(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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkCallbackTest",
+                "testOnBlockedStatusChanged_default", Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+    }
+}
+
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java
new file mode 100644
index 0000000..8ffe360
--- /dev/null
+++ b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java
@@ -0,0 +1,86 @@
+/*
+ * 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.netpolicy;
+
+import static com.android.cts.netpolicy.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 HostsideNetworkPolicyTestCase {
+    @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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest",
+                "testIsUidNetworkingBlocked_withUidNotBlocked");
+    }
+
+    @Test
+    public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidNetworkingBlocked_withSystemUid");
+    }
+
+    @Test
+    public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest",
+                "testIsUidNetworkingBlocked_withDataSaverMode");
+    }
+
+    @Test
+    public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest",
+                "testIsUidNetworkingBlocked_withRestrictedNetworkingMode");
+    }
+
+    @Test
+    public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest",
+                "testIsUidNetworkingBlocked_withPowerSaverMode");
+    }
+
+    @Test
+    public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
+        runDeviceTestsWithCustomOptions(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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkPolicyManagerTest",
+                "testIsUidNetworkingBlocked_whenInBackground",
+                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+    }
+}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java
new file mode 100644
index 0000000..6de6b17
--- /dev/null
+++ b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java
@@ -0,0 +1,187 @@
+/*
+ * 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.netpolicy;
+
+import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
+
+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.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+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 HostsideNetworkPolicyTestCase extends BaseHostJUnit4Test {
+    protected static final boolean DEBUG = false;
+    protected static final String TAG = "HostsideNetworkPolicyTests";
+    protected static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
+    protected static final String TEST_APK = "CtsHostsideNetworkPolicyTestsApp.apk";
+    protected static final String TEST_APP2_PKG = "com.android.cts.netpolicy.hostside.app2";
+    protected static final String TEST_APP2_APK = "CtsHostsideNetworkPolicyTestsApp2.apk";
+
+    @Option(name = "custom-url", importance = Option.Importance.IF_UNSET,
+            description = "A custom url to use for testing network connections")
+    protected String mCustomUrl;
+
+    @BeforeClassWithInfo
+    public static void setUpOnceBase(TestInformation testInfo) throws Exception {
+        uninstallPackage(testInfo, TEST_PKG, false);
+        installPackage(testInfo, TEST_APK);
+    }
+
+    @AfterClassWithInfo
+    public static void tearDownOnceBase(TestInformation testInfo)
+            throws DeviceNotAvailableException {
+        uninstallPackage(testInfo, TEST_PKG, true);
+    }
+
+    // Custom static method to install the specified package, this is used to bypass auto-cleanup
+    // per test in BaseHostJUnit4.
+    protected static void installPackage(TestInformation testInfo, String apk)
+            throws DeviceNotAvailableException, TargetSetupError {
+        assertNotNull(testInfo);
+        final int userId = testInfo.getDevice().getCurrentUser();
+        final SuiteApkInstaller installer = new SuiteApkInstaller();
+        // Force the apk clean up
+        installer.setCleanApk(true);
+        installer.addTestFileName(apk);
+        installer.setUserId(userId);
+        installer.setShouldGrantPermission(true);
+        installer.addInstallArg("-t");
+        try {
+            installer.setUp(testInfo);
+        } catch (BuildError e) {
+            throw new TargetSetupError(
+                    e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor(), e.getErrorId());
+        }
+    }
+
+    protected void installPackage(String apk) throws DeviceNotAvailableException, TargetSetupError {
+        installPackage(getTestInformation(), apk);
+    }
+
+    protected static void uninstallPackage(TestInformation testInfo, String packageName,
+            boolean shouldSucceed)
+            throws DeviceNotAvailableException {
+        assertNotNull(testInfo);
+        final String result = testInfo.getDevice().uninstallPackage(packageName);
+        if (shouldSucceed) {
+            assertNull("uninstallPackage(" + packageName + ") failed: " + result, result);
+        }
+    }
+
+    protected void uninstallPackage(String packageName,
+            boolean shouldSucceed)
+            throws DeviceNotAvailableException {
+        uninstallPackage(getTestInformation(), packageName, shouldSucceed);
+    }
+
+    protected void assertPackageUninstalled(String packageName) throws DeviceNotAvailableException {
+        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.netpolicy.hostside' could return
+            // 'com.android.cts.netpolicy.hostside.app2'
+            boolean found = false;
+            for (String line : result.split("[\\r\\n]+")) {
+                if (line.endsWith(packageName)) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                return;
+            }
+            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 runDeviceTestsWithCustomOptions(String packageName, String className)
+            throws DeviceNotAvailableException {
+        return runDeviceTestsWithCustomOptions(packageName, className, null);
+    }
+
+    protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
+            String methodName) throws DeviceNotAvailableException {
+        return runDeviceTestsWithCustomOptions(packageName, className, methodName, null);
+    }
+
+    protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
+            String methodName, Map<String, String> testArgs) throws DeviceNotAvailableException {
+        final DeviceTestRunOptions deviceTestRunOptions = new DeviceTestRunOptions(packageName)
+                .setTestClassName(className)
+                .setTestMethodName(methodName);
+
+        // Currently there is only one custom option that the test exposes.
+        if (mCustomUrl != null) {
+            deviceTestRunOptions.addInstrumentationArg(ARG_CONNECTION_CHECK_CUSTOM_URL, mCustomUrl);
+        }
+        // Pass over any test specific arguments.
+        if (testArgs != null) {
+            for (Map.Entry<String, String> arg : testArgs.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-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java
new file mode 100644
index 0000000..0261c7d
--- /dev/null
+++ b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java
@@ -0,0 +1,466 @@
+/*
+ * 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.netpolicy;
+
+import static org.junit.Assert.fail;
+
+import android.platform.test.annotations.FlakyTest;
+import android.platform.test.annotations.SecurityTest;
+
+import com.android.ddmlib.Log;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.util.RunUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@FlakyTest(bugId = 288324467)
+public class HostsideRestrictBackgroundNetworkTests extends HostsideNetworkPolicyTestCase {
+
+    @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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataWarningReceiverTest",
+                "testSnoozeWarningNotReceived");
+    }
+
+    /**************************
+     * Data Saver Mode tests. *
+     **************************/
+
+    @Test
+    public void testDataSaverMode_disabled() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+                "testGetRestrictBackgroundStatus_disabled");
+    }
+
+    @Test
+    public void testDataSaverMode_whitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+                "testGetRestrictBackgroundStatus_whitelisted");
+    }
+
+    @Test
+    public void testDataSaverMode_enabled() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+                "testGetRestrictBackgroundStatus_enabled");
+    }
+
+    @Test
+    public void testDataSaverMode_blacklisted() throws Exception {
+        runDeviceTestsWithCustomOptions(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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+                "testGetRestrictBackgroundStatus_requiredWhitelistedPackages");
+    }
+
+    @Test
+    public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+                "testBroadcastNotSentOnUnsupportedDevices");
+    }
+
+    /*****************************
+     * Battery Saver Mode tests. *
+     *****************************/
+
+    @Test
+    public void testBatterySaverModeMetered_disabled() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+                "testBackgroundNetworkAccess_disabled");
+    }
+
+    @Test
+    public void testBatterySaverModeMetered_whitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+                "testBackgroundNetworkAccess_whitelisted");
+    }
+
+    @Test
+    public void testBatterySaverModeMetered_enabled() throws Exception {
+        runDeviceTestsWithCustomOptions(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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+                "testBackgroundNetworkAccess_disabled");
+    }
+
+    @Test
+    public void testBatterySaverModeNonMetered_whitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+                "testBackgroundNetworkAccess_whitelisted");
+    }
+
+    @Test
+    public void testBatterySaverModeNonMetered_enabled() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+                "testBackgroundNetworkAccess_enabled");
+    }
+
+    /*******************
+     * App idle tests. *
+     *******************/
+
+    @Test
+    public void testAppIdleMetered_disabled() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+                "testBackgroundNetworkAccess_disabled");
+    }
+
+    @Test
+    public void testAppIdleMetered_whitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+                "testBackgroundNetworkAccess_whitelisted");
+    }
+
+    @Test
+    public void testAppIdleMetered_tempWhitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+                "testBackgroundNetworkAccess_tempWhitelisted");
+    }
+
+    @Test
+    public void testAppIdleMetered_enabled() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+                "testBackgroundNetworkAccess_enabled");
+    }
+
+    @Test
+    public void testAppIdleMetered_idleWhitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+                "testBackgroundNetworkAccess_disabled");
+    }
+
+
+    @Test
+    public void testAppIdleNonMetered_whitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+                "testBackgroundNetworkAccess_whitelisted");
+    }
+
+    @Test
+    public void testAppIdleNonMetered_tempWhitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+                "testBackgroundNetworkAccess_tempWhitelisted");
+    }
+
+    @Test
+    public void testAppIdleNonMetered_enabled() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+                "testBackgroundNetworkAccess_enabled");
+    }
+
+    @Test
+    public void testAppIdleNonMetered_idleWhitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+                "testAppIdleNetworkAccess_idleWhitelisted");
+    }
+
+    @Test
+    public void testAppIdleNonMetered_whenCharging() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+                "testAppIdleNetworkAccess_whenCharging");
+    }
+
+    @Test
+    public void testAppIdleMetered_whenCharging() throws Exception {
+        runDeviceTestsWithCustomOptions(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
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+                "testAppIdle_toast");
+    }
+
+    /********************
+     * Doze Mode tests. *
+     ********************/
+
+    @Test
+    public void testDozeModeMetered_disabled() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+                "testBackgroundNetworkAccess_disabled");
+    }
+
+    @Test
+    public void testDozeModeMetered_whitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+                "testBackgroundNetworkAccess_whitelisted");
+    }
+
+    @Test
+    public void testDozeModeMetered_enabled() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+                "testBackgroundNetworkAccess_enabled");
+    }
+
+    @Test
+    public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception {
+        runDeviceTestsWithCustomOptions(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 {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+                "testBackgroundNetworkAccess_disabled");
+    }
+
+    @Test
+    public void testDozeModeNonMetered_whitelisted() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+                "testBackgroundNetworkAccess_whitelisted");
+    }
+
+    @Test
+    public void testDozeModeNonMetered_enabled() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+                "testBackgroundNetworkAccess_enabled");
+    }
+
+    @Test
+    public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
+            throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+                "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
+    }
+
+    /**********************
+     * Mixed modes tests. *
+     **********************/
+
+    @Test
+    public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
+                "testDataAndBatterySaverModes_meteredNetwork");
+    }
+
+    @Test
+    public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
+                "testDataAndBatterySaverModes_nonMeteredNetwork");
+    }
+
+    @Test
+    public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
+                "testDozeAndBatterySaverMode_powerSaveWhitelists");
+    }
+
+    @Test
+    public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
+                "testDozeAndAppIdle_powerSaveWhitelists");
+    }
+
+    @Test
+    public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
+                "testAppIdleAndDoze_tempPowerSaveWhitelists");
+    }
+
+    @Test
+    public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
+                "testAppIdleAndBatterySaver_tempPowerSaveWhitelists");
+    }
+
+    @Test
+    public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
+                "testDozeAndAppIdle_appIdleWhitelist");
+    }
+
+    @Test
+    public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
+                "testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists");
+    }
+
+    @Test
+    public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
+                "testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
+    }
+
+    /**************************
+     * Restricted mode tests. *
+     **************************/
+
+    @Test
+    public void testNetworkAccess_restrictedMode() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
+                "testNetworkAccess");
+    }
+
+    @Test
+    public void testNetworkAccess_restrictedMode_withBatterySaver() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
+                "testNetworkAccess_withBatterySaver");
+    }
+
+    /************************
+     * Expedited job tests. *
+     ************************/
+
+    @Test
+    public void testMeteredNetworkAccess_expeditedJob() throws Exception {
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
+    }
+
+    @Test
+    public void testNonMeteredNetworkAccess_expeditedJob() throws Exception {
+        runDeviceTestsWithCustomOptions(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-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java
new file mode 100644
index 0000000..cbf2f4d
--- /dev/null
+++ b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java
@@ -0,0 +1,92 @@
+/*
+ * 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.netpolicy;
+
+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/hostside/Android.bp b/tests/cts/hostside/Android.bp
index 891c2dd..14d5d54 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -12,17 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-next_app_data = [ ":CtsHostsideNetworkTestsAppNext" ]
+next_app_data = [":CtsHostsideNetworkTestsAppNext"]
 
 // The above line is put in place to prevent any future automerger merge conflict between aosp,
 // downstream branches. The CtsHostsideNetworkTestsAppNext target will not exist in
 // some downstream branches, but it should exist in aosp and some downstream branches.
 
-
-
-
-
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -30,8 +27,11 @@
     name: "CtsHostsideNetworkTests",
     defaults: ["cts_defaults"],
     // Only compile source java files in this apk.
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+    ],
     libs: [
+        "net-tests-utils-host-device-common",
         "cts-tradefed",
         "tradefed",
     ],
@@ -42,7 +42,9 @@
     test_suites: [
         "cts",
         "general-tests",
-        "sts"
+        "mcts-tethering",
+        "mts-tethering",
+        "sts",
     ],
     data: [
         ":CtsHostsideNetworkTestsApp",
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/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING
index 2cfd7af..dc86fb1 100644
--- a/tests/cts/hostside/TEST_MAPPING
+++ b/tests/cts/hostside/TEST_MAPPING
@@ -11,6 +11,20 @@
         },
         {
           "exclude-annotation": "android.platform.test.annotations.RequiresDevice"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        }
+      ]
+    }
+  ],
+  "postsubmit": [
+    {
+      // Postsubmit on virtual devices to monitor flakiness of @SkipPresubmit methods
+      "name": "CtsHostsideNetworkTests",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
     }
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 2751f6f..33761dc 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -20,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 1d6828f..919e025 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -30,13 +31,14 @@
         "cts-net-utils",
         "ctstestrunner-axt",
         "modules-utils-build",
-        "ub-uiautomator",
     ],
     libs: [
         "android.test.runner",
         "android.test.base",
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "general-tests",
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 04d054d..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);
-        assertForegroundNetworkAccess();
-
-        // Although it should not have access while the screen is off.
-        turnScreenOff();
-        assertBackgroundNetworkAccess(false);
-        turnScreenOn();
-        assertForegroundNetworkAccess();
-
-        // 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);
-        assertForegroundNetworkAccess();
-        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/AbstractDozeModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
deleted file mode 100644
index e0ce4ea..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
+++ /dev/null
@@ -1,141 +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.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);
-        assertForegroundNetworkAccess();
-        stopForegroundService();
-        assertBackgroundState();
-        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();
-        assertBackgroundState();
-    }
-}
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 a850e3b..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java
+++ /dev/null
@@ -1,134 +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();
-        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 9acfc19..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ /dev/null
@@ -1,1068 +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.job.JobScheduler.RESULT_SUCCESS;
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-import static android.os.BatteryManager.BATTERY_PLUGGED_AC;
-import static android.os.BatteryManager.BATTERY_PLUGGED_USB;
-import static android.os.BatteryManager.BATTERY_PLUGGED_WIRELESS;
-
-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.app.ActivityManager;
-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 com.android.compatibility.common.util.AmUtils;
-import com.android.compatibility.common.util.BatteryUtils;
-import com.android.compatibility.common.util.DeviceConfigStateHelper;
-
-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;
-
-/**
- * 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";
-
-    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";
-
-    // TODO: Update BatteryManager.BATTERY_PLUGGED_ANY as @TestApi
-    public static final int BATTERY_PLUGGED_ANY =
-            BATTERY_PLUGGED_AC | BATTERY_PLUGGED_USB | BATTERY_PLUGGED_WIRELESS;
-
-    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 int PROCESS_STATE_FOREGROUND_SERVICE;
-
-    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 = 6_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);
-
-    private static final String APP_NOT_FOREGROUND_ERROR = "app_not_fg";
-
-    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 {
-        // TODO: Annotate these constants with @TestApi instead of obtaining them using reflection
-        PROCESS_STATE_FOREGROUND_SERVICE = (Integer) ActivityManager.class
-                .getDeclaredField("PROCESS_STATE_FOREGROUND_SERVICE").get(null);
-        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);
-        mServiceClient.bind();
-        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();
-        if (mLock.isHeld()) mLock.release();
-    }
-
-    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)));
-    }
-
-    protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
-        assertBackgroundState();
-        assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */);
-    }
-
-    protected void assertForegroundNetworkAccess() throws Exception {
-        assertForegroundNetworkAccess(true);
-    }
-
-    protected void assertForegroundNetworkAccess(boolean expectAllowed) throws Exception {
-        assertForegroundState();
-        // We verified that app is in foreground state but if the screen turns-off while
-        // verifying for network access, the app will go into background state (in case app's
-        // foreground status was due to top activity). So, turn the screen on when verifying
-        // network connectivity.
-        assertNetworkAccess(expectAllowed /* expectAvailable */, 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();
-    }
-
-    protected final void assertBackgroundState() throws Exception {
-        final int maxTries = 30;
-        ProcessState state = null;
-        for (int i = 1; i <= maxTries; i++) {
-            state = getProcessStateByUid(mUid);
-            Log.v(TAG, "assertBackgroundState(): status for app2 (" + mUid + ") on attempt #" + i
-                    + ": " + state);
-            if (isBackground(state.state)) {
-                return;
-            }
-            Log.d(TAG, "App not on background state (" + state + ") on attempt #" + i
-                    + "; sleeping 1s before trying again");
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(SECOND_IN_MS);
-            }
-        }
-        fail("App2 (" + mUid + ") is not on background state after "
-                + maxTries + " attempts: " + state);
-    }
-
-    protected final void assertForegroundState() throws Exception {
-        final int maxTries = 30;
-        ProcessState state = null;
-        for (int i = 1; i <= maxTries; i++) {
-            state = getProcessStateByUid(mUid);
-            Log.v(TAG, "assertForegroundState(): status for app2 (" + mUid + ") on attempt #" + i
-                    + ": " + state);
-            if (!isBackground(state.state)) {
-                return;
-            }
-            Log.d(TAG, "App not on foreground state on attempt #" + i
-                    + "; sleeping 1s before trying again");
-            turnScreenOn();
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(SECOND_IN_MS);
-            }
-        }
-        fail("App2 (" + mUid + ") is not on foreground state after "
-                + maxTries + " attempts: " + state);
-    }
-
-    protected final void assertForegroundServiceState() throws Exception {
-        final int maxTries = 30;
-        ProcessState state = null;
-        for (int i = 1; i <= maxTries; i++) {
-            state = getProcessStateByUid(mUid);
-            Log.v(TAG, "assertForegroundServiceState(): status for app2 (" + mUid + ") on attempt #"
-                    + i + ": " + state);
-            if (state.state == PROCESS_STATE_FOREGROUND_SERVICE) {
-                return;
-            }
-            Log.d(TAG, "App not on foreground service state on attempt #" + i
-                    + "; sleeping 1s before trying again");
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(SECOND_IN_MS);
-            }
-        }
-        fail("App2 (" + mUid + ") is not on foreground service state after "
-                + maxTries + " attempts: " + state);
-    }
-
-    /**
-     * Returns whether an app state should be considered "background" for restriction purposes.
-     */
-    protected boolean isBackground(int state) {
-        return state > PROCESS_STATE_FOREGROUND_SERVICE;
-    }
-
-    /**
-     * Asserts whether the active network is available or not.
-     */
-    private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
-            throws Exception {
-        final int maxTries = 5;
-        String error = null;
-        int timeoutMs = 500;
-
-        for (int i = 1; i <= maxTries; i++) {
-            error = checkNetworkAccess(expectAvailable);
-
-            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);
-    }
-
-    /**
-     * Checks whether the network is available as expected.
-     *
-     * @return error message with the mismatch (or empty if assertion passed).
-     */
-    private String checkNetworkAccess(boolean expectAvailable) throws Exception {
-        final String resultData = mServiceClient.checkNetworkStatus();
-        return checkForAvailabilityInResultData(resultData, expectAvailable);
-    }
-
-    private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable) {
-        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));
-        }
-
-        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 enabled) throws Exception {
-        if (!isAppStandbySupported()) {
-            return;
-        }
-        Log.i(TAG, "Setting app idle to " + enabled);
-        executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
-        assertAppIdle(enabled);
-    }
-
-    protected void setAppIdleNoAssert(boolean enabled) throws Exception {
-        if (!isAppStandbySupported()) {
-            return;
-        }
-        Log.i(TAG, "Setting app idle to " + enabled);
-        executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
-    }
-
-    protected void assertAppIdle(boolean enabled) throws Exception {
-        try {
-            assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
-                    30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + enabled);
-        } 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);
-                    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.
-                    assertForegroundNetworkAccess();
-                } 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);
-                    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 10775d0..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
+++ /dev/null
@@ -1,102 +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 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 android.util.Log;
-
-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");
-    }
-
-    @Test
-    @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
-    public void testStartActivity_dataSaver() throws Exception {
-        setRestrictBackground(true);
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver");
-    }
-
-    @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");
-    }
-
-    @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");
-    }
-
-    private void assertLaunchedActivityHasNetworkAccess(String testName) throws Exception {
-        for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
-            Log.i(TAG, testName + " start #" + i);
-            launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-            getUiDevice().pressHome();
-            assertBackgroundState();
-            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 2f30536..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);
-        assertForegroundNetworkAccess();
-
-        // 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();
-        }
-        assertForegroundNetworkAccess();
-
-        // 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);
-        assertForegroundNetworkAccess();
-        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 b2e81ff..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.support.test.uiautomator.By;
-import android.support.test.uiautomator.Direction;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
-import android.telephony.SubscriptionManager;
-import android.telephony.SubscriptionPlan;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.UiDevice;
-
-import com.android.compatibility.common.util.SystemUtil;
-import com.android.compatibility.common.util.UiAutomatorUtils;
-
-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 = UiAutomatorUtils.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/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 a558010..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
+++ /dev/null
@@ -1,117 +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"
-            }) {
-                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/ExpeditedJobNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java
deleted file mode 100644
index 6596269..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.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.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class ExpeditedJobNonMeteredTest 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 0610774..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ /dev/null
@@ -1,113 +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 = 5000;
-    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 IMyService mService;
-
-    public MyServiceClient(Context context) {
-        mContext = context;
-    }
-
-    public void bind() {
-        if (mService != null) {
-            throw new IllegalStateException("Already bound");
-        }
-
-        final ConditionVariable cv = new ConditionVariable();
-        mServiceConnection = new ServiceConnection() {
-            @Override
-            public void onServiceConnected(ComponentName name, IBinder service) {
-                mService = IMyService.Stub.asInterface(service);
-                cv.open();
-            }
-            @Override
-            public void onServiceDisconnected(ComponentName name) {
-                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
-                | Context.BIND_NOT_FOREGROUND);
-        cv.block(TIMEOUT_MS);
-        if (mService == null) {
-            throw new IllegalStateException(
-                    "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
-        }
-    }
-
-    public void unbind() {
-        if (mService != null) {
-            mContext.unbindService(mServiceConnection);
-        }
-    }
-
-    public void registerBroadcastReceiver() throws RemoteException {
-        mService.registerBroadcastReceiver();
-    }
-
-    public int getCounters(String receiverName, String action) throws RemoteException {
-        return mService.getCounters(receiverName, action);
-    }
-
-    public String checkNetworkStatus() throws RemoteException {
-        return mService.checkNetworkStatus();
-    }
-
-    public String getRestrictBackgroundStatus() throws RemoteException {
-        return mService.getRestrictBackgroundStatus();
-    }
-
-    public void sendNotification(int notificationId, String notificationType)
-            throws RemoteException {
-        mService.sendNotification(notificationId, notificationType);
-    }
-
-    public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
-            throws RemoteException {
-        mService.registerNetworkCallback(request, cb);
-    }
-
-    public void unregisterNetworkCallback() throws RemoteException {
-        mService.unregisterNetworkCallback();
-    }
-
-    public int scheduleJob(JobInfo jobInfo) throws RemoteException {
-        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 0715e32..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ /dev/null
@@ -1,308 +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.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.util.Log;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-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();
-    @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 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);
-
-        // 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.expectBlockedStatusCallback(mNetwork, false);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-
-        setRestrictBackground(false);
-        setBatterySaverMode(false);
-        unregisterNetworkCallback();
-    }
-
-    @RequiredProperties({DATA_SAVER_MODE})
-    @Test
-    public void testOnBlockedStatusChanged_dataSaver() throws Exception {
-        try {
-            // Enable restrict background
-            setRestrictBackground(true);
-            assertBackgroundNetworkAccess(false);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-
-            // Add to whitelist
-            addRestrictBackgroundWhitelist(mUid);
-            assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-
-            // Remove from whitelist
-            removeRestrictBackgroundWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-            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);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-
-            // Disable restrict background, should not trigger callback
-            setRestrictBackground(false);
-            assertBackgroundNetworkAccess(true);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    @Test
-    public void testOnBlockedStatusChanged_powerSaver() throws Exception {
-        try {
-            // Enable Power Saver
-            setBatterySaverMode(true);
-            assertBackgroundNetworkAccess(false);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-
-            // Disable Power Saver
-            setBatterySaverMode(false);
-            assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-        } 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);
-            assertBackgroundNetworkAccess(false);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-
-            // Disable Power Saver
-            setBatterySaverMode(false);
-            assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-        } 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 a0d88c9..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
+++ /dev/null
@@ -1,240 +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.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 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();
-            assertForegroundState();
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_TMP_ALLOWLIST
-
-            // Back to background.
-            finishActivity();
-            assertBackgroundState();
-            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();
-            assertForegroundState();
-            assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
-            // Back to background.
-            finishActivity();
-            assertBackgroundState();
-
-            // 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 */);
-        }
-    }
-}
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 4266aad..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
+++ /dev/null
@@ -1,70 +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);
-        assertForegroundNetworkAccess(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);
-        assertForegroundNetworkAccess(true);
-
-        // go to background state
-        finishActivity();
-        assertBackgroundNetworkAccess(true);
-    }
-
-    @Test
-    public void testNetworkAccess_withBatterySaver() throws Exception {
-        setBatterySaverMode(true);
-        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);
-    }
-}
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 c28ee64..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;
@@ -100,9 +105,6 @@
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject;
-import android.support.test.uiautomator.UiSelector;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
@@ -114,12 +116,17 @@
 import android.util.Range;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject;
+import androidx.test.uiautomator.UiSelector;
 
 import com.android.compatibility.common.util.BlockingBroadcastReceiver;
 import com.android.modules.utils.build.SdkLevel;
 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;
@@ -154,7 +161,6 @@
 import java.util.Random;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
@@ -218,6 +224,7 @@
     private WifiManager mWifiManager;
     private RemoteSocketFactoryClient mRemoteSocketFactoryClient;
     private CtsNetUtils mCtsNetUtils;
+    private ConnectUtil mConnectUtil;
     private PackageManager mPackageManager;
     private Context mTestContext;
     private Context mTargetContext;
@@ -231,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");
@@ -263,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();
@@ -809,26 +821,12 @@
                 mOldPrivateDnsSpecifier);
     }
 
-    // TODO: replace with CtsNetUtils.awaitPrivateDnsSetting in Q or above.
     private void expectPrivateDnsHostname(final String hostname) throws Exception {
-        final NetworkRequest request = new NetworkRequest.Builder()
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
-                .build();
-        final CountDownLatch latch = new CountDownLatch(1);
-        final NetworkCallback callback = new NetworkCallback() {
-            @Override
-            public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
-                if (network.equals(mNetwork) &&
-                        Objects.equals(lp.getPrivateDnsServerName(), hostname)) {
-                    latch.countDown();
-                }
-            }
-        };
-
-        registerNetworkCallback(request, callback);
-
-        assertTrue("Private DNS hostname was not " + hostname + " after " + TIMEOUT_MS + "ms",
-                latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        for (Network network : mCtsNetUtils.getTestableNetworks()) {
+            // Wait for private DNS setting to propagate.
+            mCtsNetUtils.awaitPrivateDnsSetting("Test wait private DNS setting timeout",
+                    network, hostname, false);
+        }
     }
 
     private void setAndVerifyPrivateDns(boolean strictMode) throws Exception {
@@ -894,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.
@@ -953,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)) {
@@ -977,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);
@@ -1032,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();
@@ -1041,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
@@ -1152,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();
@@ -1190,8 +1196,8 @@
 
         final String origMode = runWithShellPermissionIdentity(() -> {
             final String mode = DeviceConfig.getProperty(
-                    DeviceConfig.NAMESPACE_CONNECTIVITY, AUTOMATIC_ON_OFF_KEEPALIVE_VERSION);
-            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CONNECTIVITY,
+                    DeviceConfig.NAMESPACE_TETHERING, AUTOMATIC_ON_OFF_KEEPALIVE_VERSION);
+            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_TETHERING,
                     AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
                     AUTOMATIC_ON_OFF_KEEPALIVE_ENABLED, false /* makeDefault */);
             return mode;
@@ -1231,7 +1237,7 @@
 
             runWithShellPermissionIdentity(() -> {
                 DeviceConfig.setProperty(
-                                DeviceConfig.NAMESPACE_CONNECTIVITY,
+                                DeviceConfig.NAMESPACE_TETHERING,
                                 AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
                                 origMode, false);
                 mCM.setTestLowTcpPollingTimerForKeepalive(0);
@@ -1241,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);
 
@@ -1275,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<>();
 
@@ -1300,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
@@ -1321,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 */,
@@ -1340,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
@@ -1368,8 +1366,6 @@
 
     @Test
     public void testGetConnectionOwnerUidSecurity() throws Exception {
-        assumeTrue(supportedHardware());
-
         DatagramSocket s;
         InetAddress address = InetAddress.getByName("localhost");
         s = new DatagramSocket();
@@ -1390,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();
@@ -1430,7 +1425,6 @@
 
     @Test
     public void testSetProxyDisallowedApps() throws Exception {
-        assumeTrue(supportedHardware());
         ProxyInfo initialProxy = mCM.getDefaultProxy();
 
         String disallowedApps = mPackageName;
@@ -1456,7 +1450,6 @@
 
     @Test
     public void testNoProxy() throws Exception {
-        assumeTrue(supportedHardware());
         ProxyInfo initialProxy = mCM.getDefaultProxy();
         BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
         proxyBroadcastReceiver.register();
@@ -1491,7 +1484,6 @@
 
     @Test
     public void testBindToNetworkWithProxy() throws Exception {
-        assumeTrue(supportedHardware());
         String allowedApps = mPackageName;
         Network initialNetwork = mCM.getActiveNetwork();
         ProxyInfo initialProxy = mCM.getDefaultProxy();
@@ -1516,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;
@@ -1548,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"
@@ -1577,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"
@@ -1619,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"
@@ -1646,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"
@@ -1686,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;
@@ -1741,10 +1715,21 @@
         assertEquals(VpnManager.TYPE_VPN_SERVICE, ((VpnTransportInfo) ti).getType());
     }
 
-    private void assertDefaultProxy(ProxyInfo expected) {
+    private void assertDefaultProxy(ProxyInfo expected) throws Exception {
         assertEquals("Incorrect proxy config.", expected, mCM.getDefaultProxy());
         String expectedHost = expected == null ? null : expected.getHost();
         String expectedPort = expected == null ? null : String.valueOf(expected.getPort());
+
+        // ActivityThread may not have time to set it in the properties yet which will cause flakes.
+        // Wait for some time to deflake the test.
+        int attempt = 0;
+        while (!(Objects.equals(expectedHost, System.getProperty("http.proxyHost"))
+                && Objects.equals(expectedPort, System.getProperty("http.proxyPort")))
+                && attempt < 300) {
+            attempt++;
+            Log.d(TAG, "Wait for proxy being updated, attempt=" + attempt);
+            Thread.sleep(100);
+        }
         assertEquals("Incorrect proxy host system property.", expectedHost,
             System.getProperty("http.proxyHost"));
         assertEquals("Incorrect proxy port system property.", expectedPort,
@@ -1785,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"},
@@ -1842,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);
 
@@ -1911,7 +1893,6 @@
 
     @Test
     public void testSetVpnDefaultForUids() throws Exception {
-        assumeTrue(supportedHardware());
         assumeTrue(SdkLevel.isAtLeastU());
 
         final Network defaultNetwork = mCM.getActiveNetwork();
@@ -1957,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 {
 
@@ -2000,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];
@@ -2008,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);
         }
 
@@ -2042,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 db92f5c..12ea23b 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -34,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/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 60b5476..0000000
--- a/tests/cts/hostside/certs/Android.bp
+++ /dev/null
@@ -1,8 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_app_certificate {
-    name: "cts-net-app",
-    certificate: "cts-net-app",
-}
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index 2aa3f69..c220000 100644
--- a/tests/cts/hostside/networkslicingtestapp/Android.bp
+++ b/tests/cts/hostside/networkslicingtestapp/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -39,27 +40,30 @@
 android_test_helper_app {
     name: "CtsHostsideNetworkCapTestsAppWithoutProperty",
     defaults: [
-           "cts_support_defaults",
-           "CtsHostsideNetworkCapTestsAppDefaults"
+        "cts_support_defaults",
+        "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     manifest: "AndroidManifestWithoutProperty.xml",
+    sdk_version: "test_current",
 }
 
 android_test_helper_app {
     name: "CtsHostsideNetworkCapTestsAppWithProperty",
     defaults: [
-           "cts_support_defaults",
-           "CtsHostsideNetworkCapTestsAppDefaults"
+        "cts_support_defaults",
+        "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     manifest: "AndroidManifestWithProperty.xml",
+    sdk_version: "test_current",
 }
 
 android_test_helper_app {
     name: "CtsHostsideNetworkCapTestsAppSdk33",
     defaults: [
-           "cts_support_defaults",
-           "CtsHostsideNetworkCapTestsAppDefaults"
+        "cts_support_defaults",
+        "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     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 cfd3130..0000000
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
+++ /dev/null
@@ -1,54 +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 android.platform.test.annotations.FlakyTest;
-
-public class HostsideConnOnActivityStartTest extends HostsideNetworkTestCase {
-    private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    public void testStartActivity_batterySaver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
-    }
-
-    public void testStartActivity_dataSaver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
-    }
-
-    @FlakyTest(bugId = 231440256)
-    public void testStartActivity_doze() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
-    }
-
-    public void testStartActivity_appStandby() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
-    }
-}
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 1312085..0000000
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
+++ /dev/null
@@ -1,42 +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;
-public class HostsideNetworkCallbackTests extends HostsideNetworkTestCase {
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    public void testOnBlockedStatusChanged_dataSaver() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
-    }
-
-    public void testOnBlockedStatusChanged_powerSaver() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
-    }
-}
-
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 fdb8876..0000000
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
+++ /dev/null
@@ -1,66 +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;
-
-public class HostsideNetworkPolicyManagerTests extends HostsideNetworkTestCase {
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withUidNotBlocked");
-    }
-
-    public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidNetworkingBlocked_withSystemUid");
-    }
-
-    public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withDataSaverMode");
-    }
-
-    public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withRestrictedNetworkingMode");
-    }
-
-    public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withPowerSaverMode");
-    }
-
-    public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidRestrictedOnMeteredNetworks");
-    }
-}
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 2aa1032..69d61b3 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -16,179 +16,83 @@
 
 package com.android.cts.net;
 
-import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
-import com.android.ddmlib.Log;
-import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
-import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
 import com.android.modules.utils.build.testing.DeviceSdkLevel;
-import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.result.CollectingTestListener;
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestRunResult;
-import com.android.tradefed.testtype.DeviceTestCase;
-import com.android.tradefed.testtype.IAbi;
-import com.android.tradefed.testtype.IAbiReceiver;
-import com.android.tradefed.testtype.IBuildReceiver;
-import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
 
-import java.io.FileNotFoundException;
-import java.util.Map;
+import org.junit.runner.RunWith;
 
-abstract class HostsideNetworkTestCase extends DeviceTestCase implements IAbiReceiver,
-        IBuildReceiver {
-    protected static final boolean DEBUG = false;
-    protected static final String TAG = "HostsideNetworkTests";
+@RunWith(DeviceJUnit4ClassRunner.class)
+abstract class HostsideNetworkTestCase extends BaseHostJUnit4Test {
     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";
     protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
     protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
 
-    private IAbi mAbi;
-    private IBuildInfo mCtsBuild;
+    @BeforeClassWithInfo
+    public static void setUpOnceBase(TestInformation testInfo) throws Exception {
+        DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(testInfo.getDevice());
+        String testApk = deviceSdkLevel.isDeviceAtLeastV() ? TEST_APK_NEXT : TEST_APK;
 
-    @Override
-    public void setAbi(IAbi abi) {
-        mAbi = abi;
+        uninstallPackage(testInfo, TEST_PKG, false);
+        installPackage(testInfo, testApk);
     }
 
-    @Override
-    public void setBuild(IBuildInfo buildInfo) {
-        mCtsBuild = buildInfo;
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        assertNotNull(mAbi);
-        assertNotNull(mCtsBuild);
-
-        DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(getDevice());
-        String testApk = deviceSdkLevel.isDeviceAtLeastT() ? TEST_APK_NEXT
-                : TEST_APK;
-
-        uninstallPackage(TEST_PKG, false);
-        installPackage(testApk);
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-
-        uninstallPackage(TEST_PKG, true);
-    }
-
-    protected void installPackage(String apk) throws FileNotFoundException,
-            DeviceNotAvailableException {
-        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
-        assertNull(getDevice().installPackage(buildHelper.getTestFile(apk),
-                false /* reinstall */, true /* grantPermissions */, "-t"));
-    }
-
-    protected void uninstallPackage(String packageName, boolean shouldSucceed)
+    @AfterClassWithInfo
+    public static void tearDownOnceBase(TestInformation testInfo)
             throws DeviceNotAvailableException {
-        final String result = getDevice().uninstallPackage(packageName);
+        uninstallPackage(testInfo, TEST_PKG, true);
+    }
+
+    // Custom static method to install the specified package, this is used to bypass auto-cleanup
+    // per test in BaseHostJUnit4.
+    protected static void installPackage(TestInformation testInfo, String apk)
+            throws DeviceNotAvailableException, TargetSetupError {
+        assertNotNull(testInfo);
+        final int userId = testInfo.getDevice().getCurrentUser();
+        final SuiteApkInstaller installer = new SuiteApkInstaller();
+        // Force the apk clean up
+        installer.setCleanApk(true);
+        installer.addTestFileName(apk);
+        installer.setUserId(userId);
+        installer.setShouldGrantPermission(true);
+        installer.addInstallArg("-t");
+        try {
+            installer.setUp(testInfo);
+        } catch (BuildError e) {
+            throw new TargetSetupError(
+                    e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor(), e.getErrorId());
+        }
+    }
+
+    protected void installPackage(String apk) throws DeviceNotAvailableException, TargetSetupError {
+        installPackage(getTestInformation(), apk);
+    }
+
+    protected static void uninstallPackage(TestInformation testInfo, String packageName,
+            boolean shouldSucceed)
+            throws DeviceNotAvailableException {
+        assertNotNull(testInfo);
+        final String result = testInfo.getDevice().uninstallPackage(packageName);
         if (shouldSucceed) {
             assertNull("uninstallPackage(" + packageName + ") failed: " + result, result);
         }
     }
 
-    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 void runDeviceTests(String packageName, String testClassName)
+    protected void uninstallPackage(String packageName,
+            boolean shouldSucceed)
             throws DeviceNotAvailableException {
-        runDeviceTests(packageName, testClassName, null);
-    }
-
-    protected void runDeviceTests(String packageName, String testClassName, String methodName)
-            throws DeviceNotAvailableException {
-        RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(packageName,
-                "androidx.test.runner.AndroidJUnitRunner", getDevice().getIDevice());
-
-        if (testClassName != null) {
-            if (methodName != null) {
-                testRunner.setMethodName(testClassName, methodName);
-            } else {
-                testRunner.setClassName(testClassName);
-            }
-        }
-
-        final CollectingTestListener listener = new CollectingTestListener();
-        getDevice().runInstrumentationTests(testRunner, listener);
-
-        final TestRunResult result = listener.getCurrentRunResults();
-        if (result.isRunFailure()) {
-            throw new AssertionError("Failed to successfully run device tests for "
-                    + result.getName() + ": " + result.getRunFailureMessage());
-        }
-
-        if (result.hasFailedTests()) {
-            // build a meaningful error message
-            StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n");
-            for (Map.Entry<TestDescription, TestResult> resultEntry :
-                    result.getTestResults().entrySet()) {
-                final TestStatus testStatus = resultEntry.getValue().getStatus();
-                if (!TestStatus.PASSED.equals(testStatus)
-                        && !TestStatus.ASSUMPTION_FAILURE.equals(testStatus)) {
-                    errorBuilder.append(resultEntry.getKey().toString());
-                    errorBuilder.append(":\n");
-                    errorBuilder.append(resultEntry.getValue().getStackTrace());
-                }
-            }
-            throw new AssertionError(errorBuilder.toString());
-        }
-    }
-
-    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 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;
+        uninstallPackage(getTestInformation(), packageName, shouldSucceed);
     }
 }
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 21c78b7..0000000
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
+++ /dev/null
@@ -1,411 +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 android.platform.test.annotations.SecurityTest;
-
-import com.android.ddmlib.Log;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.util.RunUtil;
-
-public class HostsideRestrictBackgroundNetworkTests extends HostsideNetworkTestCase {
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    @SecurityTest
-    public void testDataWarningReceiver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataWarningReceiverTest",
-                "testSnoozeWarningNotReceived");
-    }
-
-    /**************************
-     * Data Saver Mode tests. *
-     **************************/
-
-    public void testDataSaverMode_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_disabled");
-    }
-
-    public void testDataSaverMode_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_whitelisted");
-    }
-
-    public void testDataSaverMode_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_enabled");
-    }
-
-    public void testDataSaverMode_blacklisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_blacklisted");
-    }
-
-    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);
-    }
-
-    public void testDataSaverMode_requiredWhitelistedPackages() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_requiredWhitelistedPackages");
-    }
-
-    public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testBroadcastNotSentOnUnsupportedDevices");
-    }
-
-    /*****************************
-     * Battery Saver Mode tests. *
-     *****************************/
-
-    public void testBatterySaverModeMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    public void testBatterySaverModeMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    public void testBatterySaverModeMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    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);
-    }
-
-    public void testBatterySaverModeNonMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    public void testBatterySaverModeNonMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    public void testBatterySaverModeNonMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    /*******************
-     * App idle tests. *
-     *******************/
-
-    public void testAppIdleMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    public void testAppIdleMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    public void testAppIdleMetered_tempWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_tempWhitelisted");
-    }
-
-    public void testAppIdleMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    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 {
-    //    }
-
-    public void testAppIdleNonMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    public void testAppIdleNonMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    public void testAppIdleNonMetered_tempWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_tempWhitelisted");
-    }
-
-    public void testAppIdleNonMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    public void testAppIdleNonMetered_idleWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testAppIdleNetworkAccess_idleWhitelisted");
-    }
-
-    public void testAppIdleNonMetered_whenCharging() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testAppIdleNetworkAccess_whenCharging");
-    }
-
-    public void testAppIdleMetered_whenCharging() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testAppIdleNetworkAccess_whenCharging");
-    }
-
-    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. *
-     ********************/
-
-    public void testDozeModeMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    public void testDozeModeMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    public void testDozeModeMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    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 {
-    //    }
-
-    public void testDozeModeNonMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    public void testDozeModeNonMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    public void testDozeModeNonMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
-            throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
-    }
-
-    /**********************
-     * Mixed modes tests. *
-     **********************/
-
-    public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDataAndBatterySaverModes_meteredNetwork");
-    }
-
-    public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDataAndBatterySaverModes_nonMeteredNetwork");
-    }
-
-    public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDozeAndBatterySaverMode_powerSaveWhitelists");
-    }
-
-    public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDozeAndAppIdle_powerSaveWhitelists");
-    }
-
-    public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndDoze_tempPowerSaveWhitelists");
-    }
-
-    public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndBatterySaver_tempPowerSaveWhitelists");
-    }
-
-    public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDozeAndAppIdle_appIdleWhitelist");
-    }
-
-    public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists");
-    }
-
-    public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
-    }
-
-    /**************************
-     * Restricted mode tests. *
-     **************************/
-    public void testNetworkAccess_restrictedMode() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
-                "testNetworkAccess");
-    }
-
-    public void testNetworkAccess_restrictedMode_withBatterySaver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
-                "testNetworkAccess_withBatterySaver");
-    }
-
-    /************************
-     * Expedited job tests. *
-     ************************/
-
-    public void testMeteredNetworkAccess_expeditedJob() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
-    }
-
-    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/HostsideSelfDeclaredNetworkCapabilitiesCheckTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideSelfDeclaredNetworkCapabilitiesCheckTest.java
index 4c2985d..c3bdb6d 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideSelfDeclaredNetworkCapabilitiesCheckTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideSelfDeclaredNetworkCapabilitiesCheckTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.cts.net;
 
+import org.junit.Test;
+
 public class HostsideSelfDeclaredNetworkCapabilitiesCheckTest extends HostsideNetworkTestCase {
 
     private static final String TEST_WITH_PROPERTY_IN_CURRENT_SDK_APK =
@@ -34,6 +36,7 @@
             "requestNetwork_withoutRequestCapabilities";
 
 
+    @Test
     public void testRequestNetworkInCurrentSdkWithProperty() throws Exception {
         uninstallPackage(TEST_APP_PKG, false);
         installPackage(TEST_WITH_PROPERTY_IN_CURRENT_SDK_APK);
@@ -48,6 +51,7 @@
         uninstallPackage(TEST_APP_PKG, true);
     }
 
+    @Test
     public void testRequestNetworkInCurrentSdkWithoutProperty() throws Exception {
         uninstallPackage(TEST_APP_PKG, false);
         installPackage(TEST_WITHOUT_PROPERTY_IN_CURRENT_SDK_APK);
@@ -62,6 +66,7 @@
         uninstallPackage(TEST_APP_PKG, true);
     }
 
+    @Test
     public void testRequestNetworkInSdk33() throws Exception {
         uninstallPackage(TEST_APP_PKG, false);
         installPackage(TEST_IN_SDK_33_APK);
@@ -75,6 +80,7 @@
         uninstallPackage(TEST_APP_PKG, true);
     }
 
+    @Test
     public void testReinstallPackageWillUpdateProperty() throws Exception {
         uninstallPackage(TEST_APP_PKG, false);
         installPackage(TEST_WITHOUT_PROPERTY_IN_CURRENT_SDK_APK);
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 3ca4775..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,95 +18,112 @@
 
 import android.platform.test.annotations.RequiresDevice;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
 public class HostsideVpnTests extends HostsideNetworkTestCase {
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
+    @Before
+    public void setUp() throws Exception {
         uninstallPackage(TEST_APP2_PKG, false);
         installPackage(TEST_APP2_APK);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-
+    @After
+    public void tearDown() throws Exception {
         uninstallPackage(TEST_APP2_PKG, true);
     }
 
+    @Test
     public void testChangeUnderlyingNetworks() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testChangeUnderlyingNetworks");
     }
 
+    @Test
     public void testDefault() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testDefault");
     }
 
+    @Test
     public void testAppAllowed() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testAppAllowed");
     }
 
+    @Test
     public void testAppDisallowed() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testAppDisallowed");
     }
 
+    @Test
     public void testSocketClosed() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSocketClosed");
     }
 
+    @Test
     public void testGetConnectionOwnerUidSecurity() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testGetConnectionOwnerUidSecurity");
     }
 
+    @Test
     public void testSetProxy() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetProxy");
     }
 
+    @Test
     public void testSetProxyDisallowedApps() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetProxyDisallowedApps");
     }
 
+    @Test
     public void testNoProxy() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testNoProxy");
     }
 
+    @Test
     public void testBindToNetworkWithProxy() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBindToNetworkWithProxy");
     }
 
+    @Test
     public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception {
         runDeviceTests(
                 TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNoUnderlyingNetwork");
     }
 
+    @Test
     public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
         runDeviceTests(
                 TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNullUnderlyingNetwork");
     }
 
+    @Test
     public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
         runDeviceTests(
                 TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNonNullUnderlyingNetwork");
     }
 
+    @Test
     public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
         runDeviceTests(
                 TEST_PKG, TEST_PKG + ".VpnTest", "testAlwaysMeteredVpnWithNullUnderlyingNetwork");
     }
 
     @RequiresDevice // Keepalive is not supported on virtual hardware
+    @Test
     public void testAutomaticOnOffKeepaliveModeClose() throws Exception {
         runDeviceTests(
                 TEST_PKG, TEST_PKG + ".VpnTest", "testAutomaticOnOffKeepaliveModeClose");
     }
 
     @RequiresDevice // Keepalive is not supported on virtual hardware
+    @Test
     public void testAutomaticOnOffKeepaliveModeNoClose() throws Exception {
         runDeviceTests(
                 TEST_PKG, TEST_PKG + ".VpnTest", "testAutomaticOnOffKeepaliveModeNoClose");
     }
 
+    @Test
     public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
         runDeviceTests(
                 TEST_PKG,
@@ -114,32 +131,51 @@
                 "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork");
     }
 
+    @Test
     public void testB141603906() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testB141603906");
     }
 
+    @Test
     public void testDownloadWithDownloadManagerDisallowed() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
                 "testDownloadWithDownloadManagerDisallowed");
     }
 
+    @Test
     public void testExcludedRoutes() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testExcludedRoutes");
     }
 
+    @Test
     public void testIncludedRoutes() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testIncludedRoutes");
     }
 
+    @Test
     public void testInterleavedRoutes() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testInterleavedRoutes");
     }
 
+    @Test
     public void testBlockIncomingPackets() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBlockIncomingPackets");
     }
 
+    @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/hostside/src/com/android/cts/net/ProcNetTest.java b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
index adaac6d..fa68e3e 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -169,13 +169,13 @@
         for (String interfaceDir : mSysctlDirs) {
             String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitations";
             int value = readIntFromPath(path);
-            assertEquals(IPV6_WIFI_ROUTER_SOLICITATIONS, value);
+            assertEquals(path, IPV6_WIFI_ROUTER_SOLICITATIONS, value);
             path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitation_max_interval";
             int interval = readIntFromPath(path);
             final int lowerBoundSec = 15 * 60;
             final int upperBoundSec = 60 * 60;
-            assertTrue(lowerBoundSec <= interval);
-            assertTrue(interval <= upperBoundSec);
+            assertTrue(path, lowerBoundSec <= interval);
+            assertTrue(path, interval <= upperBoundSec);
         }
     }
 
@@ -191,6 +191,6 @@
 
         String path = "/proc/sys/net/ipv4/tcp_congestion_control";
         String value = mDevice.executeAdbCommand("shell", "cat", path).trim();
-        assertEquals(value, "cubic");
+        assertEquals("cubic", value);
     }
 }
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
new file mode 100644
index 0000000..dc90adb
--- /dev/null
+++ b/tests/cts/multidevices/Android.bp
@@ -0,0 +1,46 @@
+// 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_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",
+    ],
+    libs: [
+        "mobly",
+        "net-tests-utils-host-python-common",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    test_options: {
+        unit_test: false,
+    },
+    data: [
+        // Package the snippet with the mobly test
+        ":connectivity_multi_devices_snippet",
+    ],
+    version: {
+        py3: {
+            embedded_launcher: true,
+        },
+    },
+}
diff --git a/tests/cts/multidevices/AndroidTest.xml b/tests/cts/multidevices/AndroidTest.xml
new file mode 100644
index 0000000..5312b4d
--- /dev/null
+++ b/tests/cts/multidevices/AndroidTest.xml
@@ -0,0 +1,43 @@
+<?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 CTS Connectivity multi devices test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
+    <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" />
+
+    <device name="device1">
+        <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+            <option name="test-file-name" value="connectivity_multi_devices_snippet.apk" />
+        </target_preparer>
+        <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        </target_preparer>
+    </device>
+    <device name="device2">
+        <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+            <option name="test-file-name" value="connectivity_multi_devices_snippet.apk" />
+        </target_preparer>
+        <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        </target_preparer>
+    </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="CtsConnectivityMultiDevicesTestCases" />
+      <!-- Timeout limit in milliseconds for all test cases of the python binary -->
+      <option name="mobly-test-timeout" value="180000" />
+    </test>
+</configuration>
+
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
new file mode 100644
index 0000000..cdb684a
--- /dev/null
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -0,0 +1,70 @@
+# Lint as: python3
+"""Connectivity multi devices tests."""
+import sys
+
+from mobly import base_test
+from mobly import test_runner
+from mobly import utils
+from mobly.controllers import android_device
+from net_tests_utils.host.python import adb_utils, apf_utils, assert_utils, tether_utils
+from net_tests_utils.host.python.tether_utils import UpstreamType
+
+CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+
+
+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,
+    )
+
+  def test_hotspot_upstream_wifi(self):
+    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):
+    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()
diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp
new file mode 100644
index 0000000..c94087e
--- /dev/null
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -0,0 +1,39 @@
+// 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_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "connectivity_multi_devices_snippet",
+    defaults: [
+        "ConnectivityTestsLatestSdkDefaults",
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
+    srcs: [
+        "ConnectivityMultiDevicesSnippet.kt",
+        "MdnsMultiDevicesSnippet.kt",
+        "Wifip2pMultiDevicesSnippet.kt",
+    ],
+    manifest: "AndroidManifest.xml",
+    static_libs: [
+        "androidx.test.runner",
+        "mobly-snippet-lib",
+        "cts-net-utils",
+    ],
+    platform_apis: true,
+    min_sdk_version: "30", // R
+}
diff --git a/tests/cts/multidevices/snippet/AndroidManifest.xml b/tests/cts/multidevices/snippet/AndroidManifest.xml
new file mode 100644
index 0000000..4637497
--- /dev/null
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?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.
+-->
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.snippet.connectivity">
+  <!-- Declare the minimum Android SDK version and internet permission,
+       which are required by Mobly Snippet Lib since it uses network socket. -->
+  <uses-sdk android:minSdkVersion="30" />
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+  <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,
+                       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`
+       is derived from `AndroidJUnitRunner`, and is required to use the
+       Mobly Snippet Lib. -->
+  <instrumentation
+      android:name="com.google.android.mobly.snippet.SnippetRunner"
+      android:targetPackage="com.google.snippet.connectivity" />
+</manifest>
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
new file mode 100644
index 0000000..7368669
--- /dev/null
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.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
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.cts.util.CtsNetUtils
+import android.net.cts.util.CtsTetheringUtils
+import android.net.wifi.ScanResult
+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.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)
+
+    @Rpc(description = "Check whether the device has telephony feature.")
+    fun hasTelephonyFeature() = pm.hasSystemFeature(FEATURE_TELEPHONY)
+
+    @Rpc(description = "Check whether the device supporters AP + STA concurrency.")
+    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 = cbHelper.requestCell()
+        ctsNetUtils.expectNetworkIsSystemDefault(network)
+    }
+
+    @Rpc(description = "Unregister all connections.")
+    fun unregisterAll() {
+        cbHelper.unregisterAll()
+    }
+
+    @Rpc(description = "Ensure any wifi is connected and is the default network.")
+    fun ensureWifiIsDefault() {
+        val network = ctsNetUtils.ensureWifiConnected()
+        ctsNetUtils.expectNetworkIsSystemDefault(network)
+    }
+
+    @Rpc(description = "Connect to specified wifi network.")
+    // 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): Long {
+        val specifier = WifiNetworkSpecifier.Builder()
+            .setBand(ScanResult.WIFI_BAND_24_GHZ)
+            .build()
+        val wifiConfig = WifiConfiguration()
+        wifiConfig.SSID = "\"" + ssid + "\""
+        wifiConfig.preSharedKey = "\"" + passphrase + "\""
+        wifiConfig.hiddenSSID = true
+        wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK)
+        wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
+        wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
+
+        // 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).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()
+        try {
+            return tetheringCallback.isWifiTetheringSupported(context)
+        } finally {
+            ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
+        }
+    }
+
+    @Rpc(description = "Start a hotspot with given SSID and passphrase.")
+    fun startHotspot(ssid: String, passphrase: String): String {
+        // Store old config.
+        runAsShell(OVERRIDE_WIFI_CONFIG) {
+            oldSoftApConfig = wifiManager.getSoftApConfiguration()
+        }
+
+        val softApConfig = SoftApConfiguration.Builder()
+            .setWifiSsid(WifiSsid.fromBytes(ssid.toByteArray()))
+            .setPassphrase(passphrase, SECURITY_TYPE_WPA2_PSK)
+            .setBand(SoftApConfiguration.BAND_2GHZ)
+            .build()
+        runAsShell(OVERRIDE_WIFI_CONFIG) {
+            wifiManager.setSoftApConfiguration(softApConfig)
+        }
+        val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
+        try {
+            tetheringCallback.expectNoTetheringActive()
+            return ctsTetheringUtils.startWifiTethering(tetheringCallback).getInterface()
+        } finally {
+            ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
+        }
+    }
+
+    @Rpc(description = "Stop all tethering.")
+    fun stopAllTethering() {
+        ctsTetheringUtils.stopAllTethering()
+
+        // Restore old config.
+        oldSoftApConfig?.let {
+            runAsShell(OVERRIDE_WIFI_CONFIG) {
+                wifiManager.setSoftApConfiguration(it)
+            }
+        }
+    }
+}
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 ce01d71..6e2f849 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -45,6 +46,7 @@
     ],
     jarjar_rules: "jarjar-rules-shared.txt",
     static_libs: [
+        "ApfGeneratorLib",
         "bouncycastle-unbundled",
         "FrameworksNetCommonTests",
         "core-tests-support",
@@ -54,32 +56,40 @@
         "junit",
         "junit-params",
         "modules-utils-build",
+        "net-tests-utils",
         "net-utils-framework-common",
-        "truth-prebuilt",
+        "truth",
+        "TetheringIntegrationTestsBaseLib",
     ],
 
-    // uncomment when b/13249961 is fixed
-    // sdk_version: "current",
-    platform_apis: true,
-    data: [":ConnectivityChecker"],
+    min_sdk_version: "30",
     per_testcase_directory: true,
     host_required: ["net-tests-utils-host-common"],
     test_config_template: "AndroidTestTemplate.xml",
+    data: [
+        ":ConnectivityTestPreparer",
+        ":CtsCarrierServicePackage",
+    ],
 }
 
 // Networking 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: "CtsNetTestCases",
-    defaults: ["CtsNetTestCasesDefaults", "ConnectivityNextEnableDefaults"],
+    defaults: [
+        "CtsNetTestCasesDefaults",
+        "ConnectivityNextEnableDefaults",
+    ],
     static_libs: [
         "DhcpPacketLib",
         "NetworkStackApiCurrentShims",
     ],
     test_suites: [
         "cts",
+        "mts-tethering",
+        "mcts-tethering",
         "general-tests",
     ],
 }
@@ -92,26 +102,7 @@
         "NetworkStackApiStableShims",
     ],
     jni_uses_sdk_apis: true,
-    min_sdk_version: "29",
-}
-
-// 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",
-    ],
+    min_sdk_version: "30",
 }
 
 java_defaults {
@@ -129,7 +120,7 @@
 }
 
 android_test {
-    name: "CtsNetTestCasesMaxTargetSdk33",  // Must match CtsNetTestCasesMaxTargetSdk33 annotation.
+    name: "CtsNetTestCasesMaxTargetSdk33", // Must match CtsNetTestCasesMaxTargetSdk33 annotation.
     defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
     target_sdk_version: "33",
     package_name: "android.net.cts.maxtargetsdk33",
@@ -137,17 +128,32 @@
 }
 
 android_test {
-    name: "CtsNetTestCasesMaxTargetSdk31",  // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
+    name: "CtsNetTestCasesMaxTargetSdk31", // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
     defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
     target_sdk_version: "31",
-    package_name: "android.net.cts.maxtargetsdk31",  // CTS package names must be unique.
+    package_name: "android.net.cts.maxtargetsdk31", // CTS package names must be unique.
     instrumentation_target_package: "android.net.cts.maxtargetsdk31",
 }
 
 android_test {
-    name: "CtsNetTestCasesMaxTargetSdk30",  // Must match CtsNetTestCasesMaxTargetSdk30 annotation.
+    name: "CtsNetTestCasesMaxTargetSdk30", // Must match CtsNetTestCasesMaxTargetSdk30 annotation.
     defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
     target_sdk_version: "30",
-    package_name: "android.net.cts.maxtargetsdk30",  // CTS package names must be unique.
+    package_name: "android.net.cts.maxtargetsdk30", // CTS package names must be unique.
     instrumentation_target_package: "android.net.cts.maxtargetsdk30",
 }
+
+android_test_helper_app {
+    name: "CtsCarrierServicePackage",
+    defaults: ["cts_defaults"],
+    package_name: "android.net.cts.carrierservicepackage",
+    manifest: "carrierservicepackage/AndroidManifest.xml",
+    srcs: ["carrierservicepackage/src/**/*.java"],
+    min_sdk_version: "30",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 68e36ff..098cc0a 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -36,6 +36,7 @@
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <!-- This test also uses signature permissions through adopting the shell identity.
          The permissions acquired that way include (probably not exhaustive) :
@@ -46,6 +47,7 @@
                  android:usesCleartextTraffic="true">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="org.apache.http.legacy" android:required="false" />
+
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
@@ -54,4 +56,3 @@
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 9ce986c..ba65038 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -19,6 +19,7 @@
     <option name="config-descriptor:metadata" key="parameter" value="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="parameter" value="secondary_user_on_secondary_display" />
 
     <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
     <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk" />
@@ -32,13 +33,15 @@
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="{MODULE}.apk" />
+        <option name="test-file-name" value="CtsCarrierServicePackage.apk" />
     </target_preparer>
-    <target_preparer class="com.android.testutils.ConnectivityCheckTargetPreparer">
+    <target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
     </target_preparer>
     <target_preparer class="com.android.testutils.DisableConfigSyncTargetPreparer">
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="{PACKAGE}" />
+        <option name="shell-timeout" value="1500s"/>
         <option name="runtime-hint" value="9m4s" />
         <option name="hidden-api-checks" value="false" />
         <option name="isolated-storage" value="false" />
@@ -47,8 +50,8 @@
              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/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 9b81a56..587d5a5 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -41,7 +42,7 @@
         "mockwebserver",
         "junit",
         "junit-params",
-        "truth-prebuilt",
+        "truth",
     ],
 
     platform_apis: true,
@@ -55,4 +56,5 @@
         ":CtsNetTestAppForApi23",
     ],
     per_testcase_directory: true,
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/net/api23Test/AndroidTest.xml b/tests/cts/net/api23Test/AndroidTest.xml
index 8042d50..fcc73f3 100644
--- a/tests/cts/net/api23Test/AndroidTest.xml
+++ b/tests/cts/net/api23Test/AndroidTest.xml
@@ -18,6 +18,7 @@
     <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="parameter" value="secondary_user_on_secondary_display" />
     <option name="not-shardable" value="true" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
diff --git a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
index 8d68c5f..af1af43 100644
--- a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
+++ b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
@@ -65,7 +65,7 @@
         }
         ConnectivityReceiver.prepare();
 
-        mCtsNetUtils.toggleWifi();
+        mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
 
         // The connectivity broadcast has been sent; push through a terminal broadcast
         // to wait for in the receive to confirm it didn't see the connectivity change.
@@ -88,7 +88,7 @@
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
         Thread.sleep(200);
 
-        mCtsNetUtils.toggleWifi();
+        mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
 
         Intent getConnectivityCount = new Intent(GET_WIFI_CONNECTIVITY_ACTION_COUNT);
         assertEquals(2, sendOrderedBroadcastAndReturnResultCode(
@@ -106,7 +106,7 @@
         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
         mContext.registerReceiver(receiver, filter);
 
-        mCtsNetUtils.toggleWifi();
+        mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
         Intent finalIntent = new Intent(ConnectivityReceiver.FINAL_ACTION);
         finalIntent.setClass(mContext, ConnectivityReceiver.class);
         mContext.sendBroadcast(finalIntent);
diff --git a/tests/cts/net/appForApi23/Android.bp b/tests/cts/net/appForApi23/Android.bp
index b39690f..d300743 100644
--- a/tests/cts/net/appForApi23/Android.bp
+++ b/tests/cts/net/appForApi23/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/tests/cts/net/carrierservicepackage/AndroidManifest.xml b/tests/cts/net/carrierservicepackage/AndroidManifest.xml
new file mode 100644
index 0000000..c2a45eb
--- /dev/null
+++ b/tests/cts/net/carrierservicepackage/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?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"
+     android:versionCode="1"
+     android:versionName="1.0.0"
+     package="android.net.cts.carrierservicepackage">
+    <uses-sdk android:minSdkVersion="30"
+              android:targetSdkVersion="33" />
+    <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
+    <application android:allowBackup="false"
+         android:directBootAware="true">
+        <service android:name=".DummyCarrierConfigService"
+                 android:permission="android.permission.BIND_CARRIER_SERVICES"
+                 android:exported="true">
+            <intent-filter>
+              <action android:name="android.service.carrier.CarrierService"/>
+            </intent-filter>
+        </service>
+    </application>
+
+</manifest>
diff --git a/tests/cts/net/carrierservicepackage/src/android/net/cts/carrierservicepackage/DummyCarrierConfigService.java b/tests/cts/net/carrierservicepackage/src/android/net/cts/carrierservicepackage/DummyCarrierConfigService.java
new file mode 100644
index 0000000..ca2015b
--- /dev/null
+++ b/tests/cts/net/carrierservicepackage/src/android/net/cts/carrierservicepackage/DummyCarrierConfigService.java
@@ -0,0 +1,45 @@
+/*
+ * 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.cts.carrierservicepackage;
+
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.service.carrier.CarrierIdentifier;
+import android.service.carrier.CarrierService;
+
+public class DummyCarrierConfigService extends CarrierService {
+    private static final String TAG = "DummyCarrierConfigService";
+
+    public DummyCarrierConfigService() {}
+
+    @Override
+    public PersistableBundle onLoadConfig(CarrierIdentifier id) {
+        return new PersistableBundle(); // Do nothing
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return super.onBind(intent);
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        return super.onUnbind(intent);
+    }
+}
+
diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp
index 8f0d78f..fbf4f29 100644
--- a/tests/cts/net/jni/Android.bp
+++ b/tests/cts/net/jni/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -31,9 +32,9 @@
         "liblog",
     ],
     stl: "libc++_static",
-    // To be compatible with Q devices, the min_sdk_version must be 29.
+    // To be compatible with R devices, the min_sdk_version must be 30.
     sdk_version: "current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
 }
 
 cc_library_shared {
diff --git a/tests/cts/net/jni/NativeMultinetworkJni.cpp b/tests/cts/net/jni/NativeMultinetworkJni.cpp
index 6610d10..f2214a3 100644
--- a/tests/cts/net/jni/NativeMultinetworkJni.cpp
+++ b/tests/cts/net/jni/NativeMultinetworkJni.cpp
@@ -42,11 +42,14 @@
 
 // Since the tests in this file commonly pass expression statements as parameters to these macros,
 // get the returned value of the statements to avoid statement double-called.
+// By checking ExceptionCheck(), these macros don't throw another exception if an exception has
+// been thrown, because ART's JNI disallows to throw another exception while an exception is
+// pending (See CheckThread in check_jni.cc).
 #define EXPECT_GE(env, actual_stmt, expected_stmt, msg)              \
     do {                                                             \
         const auto expected = (expected_stmt);                       \
         const auto actual = (actual_stmt);                           \
-        if (actual < expected) {                                     \
+        if (actual < expected && !env->ExceptionCheck()) {           \
             jniThrowExceptionFmt(env, "java/lang/AssertionError",    \
                     "%s:%d: %s EXPECT_GE: expected %d, got %d",      \
                     __FILE__, __LINE__, msg, expected, actual);      \
@@ -57,7 +60,7 @@
     do {                                                             \
         const auto expected = (expected_stmt);                       \
         const auto actual = (actual_stmt);                           \
-        if (actual <= expected) {                                    \
+        if (actual <= expected && !env->ExceptionCheck()) {          \
             jniThrowExceptionFmt(env, "java/lang/AssertionError",    \
                     "%s:%d: %s EXPECT_GT: expected %d, got %d",      \
                     __FILE__, __LINE__, msg, expected, actual);      \
@@ -68,7 +71,7 @@
     do {                                                             \
         const auto expected = (expected_stmt);                       \
         const auto actual = (actual_stmt);                           \
-        if (actual != expected) {                                    \
+        if (actual != expected && !env->ExceptionCheck()) {          \
             jniThrowExceptionFmt(env, "java/lang/AssertionError",    \
                     "%s:%d: %s EXPECT_EQ: expected %d, got %d",      \
                     __FILE__, __LINE__, msg, expected, actual);      \
diff --git a/tests/cts/net/native/Android.bp b/tests/cts/net/native/Android.bp
index 153ff51..3f24592 100644
--- a/tests/cts/net/native/Android.bp
+++ b/tests/cts/net/native/Android.bp
@@ -15,6 +15,7 @@
 // Build the unit tests.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index 55e2642..8138598 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -1,7 +1,13 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     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",
 
@@ -19,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",
@@ -28,8 +42,9 @@
         "libbase",
         "libnetdutils",
     ],
-    // To be compatible with Q devices, the min_sdk_version must be 29.
-    min_sdk_version: "29",
+    // To be compatible with R devices, the min_sdk_version must be 30.
+    min_sdk_version: "30",
+    host_required: ["net-tests-utils-host-common"],
 }
 
 cc_test {
@@ -47,8 +62,6 @@
         "cts",
         "general-tests",
         "mts-dnsresolver",
-        "mts-networking",
         "mcts-dnsresolver",
-        "mcts-networking",
     ],
 }
diff --git a/tests/cts/net/native/dns/AndroidTest.xml b/tests/cts/net/native/dns/AndroidTest.xml
index 6d03c23..5d68ac7 100644
--- a/tests/cts/net/native/dns/AndroidTest.xml
+++ b/tests/cts/net/native/dns/AndroidTest.xml
@@ -19,11 +19,14 @@
     <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
         <option name="cleanup" value="true" />
         <option name="push" value="CtsNativeNetDnsTestCases->/data/local/tmp/CtsNativeNetDnsTestCases" />
         <option name="append-bitness" value="true" />
     </target_preparer>
+    <target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
+    </target_preparer>
     <test class="com.android.tradefed.testtype.GTest" >
         <option name="native-test-device-path" value="/data/local/tmp" />
         <option name="module-name" value="CtsNativeNetDnsTestCases" />
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..8630377
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -0,0 +1,611 @@
+/*
+ * 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
+import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
+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.os.UserManager
+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")
+            waitForInteractiveState(false)
+        }
+
+        fun turnScreenOn() {
+            if (wakeLock.isHeld()) wakeLock.release()
+            runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
+            waitForInteractiveState(true)
+        }
+
+        private fun waitForInteractiveState(interactive: Boolean) {
+            // TODO(b/366037029): This test condition should be removed once
+            // PowerManager#isInteractive is fully implemented on automotive
+            // form factor with visible background user.
+            if (isAutomotiveWithVisibleBackgroundUser()) {
+                // Wait for 2 seconds to ensure the interactive state is updated.
+                // This is a workaround for b/366037029.
+                Thread.sleep(2000L)
+            } else {
+                val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
+                assertThat(result).isEqualTo(interactive)
+            }
+        }
+
+        private fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
+            val packageManager = context.getPackageManager()
+            val userManager = context.getSystemService(UserManager::class.java)!!
+            return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE)
+                    && userManager.isVisibleBackgroundUsersSupported)
+        }
+
+        @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 exception 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)
+        }
+    }
+
+    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()
+        installProgram(program)
+        readProgram() // wait for install completion
+
+        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)
+        installProgram(program)
+        readProgram() // wait for install completion
+
+        // 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)
+
+        installProgram(gen.generate())
+        readProgram() // wait for install completion
+
+        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)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
index 3c71c90..16a7b73 100644
--- a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
 
 import static androidx.test.InstrumentationRegistry.getContext;
 
@@ -47,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;
@@ -66,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";
@@ -118,8 +123,10 @@
             // side effect is the point of using --write here.
             executeShellCommand("dumpsys batterystats --write");
 
-            // Make sure wifi is disabled.
-            mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
+            if (mPm.hasSystemFeature(FEATURE_WIFI)) {
+                // Make sure wifi is disabled.
+                mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
+            }
 
             verifyGetCellBatteryStats();
             verifyGetWifiBatteryStats();
@@ -128,6 +135,9 @@
             // Reset battery settings.
             executeShellCommand("dumpsys batterystats disable no-auto-reset");
             executeShellCommand("cmd battery reset");
+            if (mPm.hasSystemFeature(FEATURE_WIFI)) {
+                mCtsNetUtils.ensureWifiConnected();
+            }
         }
     }
 
@@ -139,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
@@ -153,23 +163,31 @@
         // The mobile battery stats are updated when a network stops being the default network.
         // ConnectivityService will call BatteryStatsManager.reportMobileRadioPowerState when
         // removing data activity tracking.
-        mCtsNetUtils.ensureWifiConnected();
+        try {
+            mCtsNetUtils.setMobileDataEnabled(false);
 
-        // There's rate limit to update mobile battery so if ConnectivityService calls
-        // BatteryStatsManager.reportMobileRadioPowerState when default network changed,
-        // the mobile stats might not be updated. But if the mobile update due to other
-        // reasons (plug/unplug, battery level change, etc) will be unaffected. Thus here
-        // dumps the battery stats to trigger a full sync of data.
-        executeShellCommand("dumpsys batterystats");
+            // There's rate limit to update mobile battery so if ConnectivityService calls
+            // BatteryStatsManager.reportMobileRadioPowerState when default network changed,
+            // the mobile stats might not be updated. But if the mobile update due to other
+            // reasons (plug/unplug, battery level change, etc) will be unaffected. Thus here
+            // dumps the battery stats to trigger a full sync of data.
+            executeShellCommand("dumpsys batterystats");
 
-        // Check cellular battery stats are updated.
-        runAsShell(UPDATE_DEVICE_STATS,
-                () -> assertStatsEventually(mBsm::getCellularBatteryStats,
-                    cellularStatsAfter -> cellularBatteryStatsIncreased(
-                    cellularStatsBefore, cellularStatsAfter)));
+            // Check cellular battery stats are updated.
+            runAsShell(UPDATE_DEVICE_STATS,
+                    () -> assertStatsEventually(mBsm::getCellularBatteryStats,
+                        cellularStatsAfter -> cellularBatteryStatsIncreased(
+                        cellularStatsBefore, cellularStatsAfter)));
+        } finally {
+            mCtsNetUtils.setMobileDataEnabled(true);
+        }
     }
 
     private void verifyGetWifiBatteryStats() throws Exception {
+        if (!mPm.hasSystemFeature(FEATURE_WIFI)) {
+            return;
+        }
+
         final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
         final URL url = new URL(TEST_URL);
 
@@ -185,7 +203,8 @@
         Log.d(TAG, "Generate traffic on wifi network.");
         generateNetworkTraffic(wifiNetwork, url);
         // Wifi battery stats are updated when wifi on.
-        mCtsNetUtils.toggleWifi();
+        mCtsNetUtils.disableWifi();
+        mCtsNetUtils.ensureWifiConnected();
 
         // Check wifi battery stats are updated.
         runAsShell(UPDATE_DEVICE_STATS,
@@ -199,9 +218,9 @@
     @Test
     public void testReportNetworkInterfaceForTransports_throwsSecurityException()
             throws Exception {
-        Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
-        final String iface = mCm.getLinkProperties(wifiNetwork).getInterfaceName();
-        final int[] transportType = mCm.getNetworkCapabilities(wifiNetwork).getTransportTypes();
+        final Network network = mCm.getActiveNetwork();
+        final String iface = mCm.getLinkProperties(network).getInterfaceName();
+        final int[] transportType = mCm.getNetworkCapabilities(network).getTransportTypes();
         assertThrows(SecurityException.class,
                 () -> mBsm.reportNetworkInterfaceForTransports(iface, transportType));
     }
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index dc22369..07e2024 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -47,8 +47,10 @@
 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
 import com.android.testutils.TestHttpServer
 import com.android.testutils.TestHttpServer.Request
 import com.android.testutils.TestableNetworkCallback
@@ -94,15 +96,18 @@
 @RunWith(AndroidJUnit4::class)
 class CaptivePortalTest {
     private val context: android.content.Context by lazy { getInstrumentation().context }
-    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
     private val pm by lazy { context.packageManager }
     private val utils by lazy { CtsNetUtils(context) }
 
     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() {
@@ -137,20 +142,21 @@
     }
 
     @Test
+    @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
     fun testCaptivePortalIsNotDefaultNetwork() {
         assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
         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)
         }
@@ -211,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/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index 7662ba3..ceb48d4 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -38,9 +38,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 
-import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 import static com.android.testutils.Cleanup.testAndCleanup;
 
 import static org.junit.Assert.assertEquals;
@@ -82,6 +80,8 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.ThrowingRunnable;
 import com.android.internal.telephony.uicc.IccUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.modules.utils.build.SdkLevel;
@@ -99,6 +99,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
@@ -117,7 +118,7 @@
     private static final int FAIL_RATE_PERCENTAGE = 100;
     private static final int UNKNOWN_DETECTION_METHOD = 4;
     private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0;
-    private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 5000;
+    private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 10000;
     private static final int DELAY_FOR_BROADCAST_IDLE = 30_000;
 
     private static final Executor INLINE_EXECUTOR = x -> x.run();
@@ -143,7 +144,7 @@
     // runWithShellPermissionIdentity, and callWithShellPermissionIdentity ensures Shell Permission
     // is not interrupted by another operation (which would drop all previously adopted
     // permissions).
-    private Object mShellPermissionsIdentityLock = new Object();
+    private final Object mShellPermissionsIdentityLock = new Object();
 
     private Context mContext;
     private ConnectivityManager mConnectivityManager;
@@ -177,6 +178,20 @@
         Log.i(TAG, "Waited for broadcast idle for " + (SystemClock.elapsedRealtime() - st) + "ms");
     }
 
+    private void runWithShellPermissionIdentity(ThrowingRunnable runnable,
+            String... permissions) {
+        synchronized (mShellPermissionsIdentityLock) {
+            SystemUtil.runWithShellPermissionIdentity(runnable, permissions);
+        }
+    }
+
+    private <T> T callWithShellPermissionIdentity(Callable<T> callable, String... permissions)
+            throws Exception {
+        synchronized (mShellPermissionsIdentityLock) {
+            return SystemUtil.callWithShellPermissionIdentity(callable, permissions);
+        }
+    }
+
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getContext();
@@ -199,7 +214,7 @@
             runWithShellPermissionIdentity(() -> {
                 final TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class);
                 tnm.teardownTestNetwork(mTestNetwork);
-            });
+            }, android.Manifest.permission.MANAGE_TEST_NETWORKS);
             mTestNetwork = null;
         }
 
@@ -249,7 +264,7 @@
             doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable(
                     subId, carrierConfigReceiver, testNetworkCallback);
         }, () -> {
-            runWithShellPermissionIdentity(
+                runWithShellPermissionIdentity(
                     () -> mCarrierConfigManager.overrideConfig(subId, null),
                     android.Manifest.permission.MODIFY_PHONE_STATE);
             mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
@@ -276,27 +291,12 @@
                 CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
                 new String[] {getCertHashForThisPackage()});
 
-        synchronized (mShellPermissionsIdentityLock) {
-            runWithShellPermissionIdentity(
-                    () -> {
-                        mCarrierConfigManager.overrideConfig(subId, carrierConfigs);
-                        mCarrierConfigManager.notifyConfigChangedForSubId(subId);
-                    },
-                    android.Manifest.permission.MODIFY_PHONE_STATE);
-        }
-
-        // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
-        // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
-        // permissions are updated.
-        synchronized (mShellPermissionsIdentityLock) {
-            runWithShellPermissionIdentity(
-                    () -> mConnectivityManager.requestNetwork(
-                            CELLULAR_NETWORK_REQUEST, testNetworkCallback),
-                    android.Manifest.permission.CONNECTIVITY_INTERNAL);
-        }
-
-        final Network network = testNetworkCallback.waitForAvailable();
-        assertNotNull(network);
+        runWithShellPermissionIdentity(
+                () -> {
+                    mCarrierConfigManager.overrideConfig(subId, carrierConfigs);
+                    mCarrierConfigManager.notifyConfigChangedForSubId(subId);
+                },
+                android.Manifest.permission.MODIFY_PHONE_STATE);
 
         assertTrue("Didn't receive broadcast for ACTION_CARRIER_CONFIG_CHANGED for subId=" + subId,
                 carrierConfigReceiver.waitForCarrierConfigChanged());
@@ -313,6 +313,17 @@
 
         Thread.sleep(5_000);
 
+        // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
+        // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
+        // permissions are updated.
+        runWithShellPermissionIdentity(
+                () -> mConnectivityManager.requestNetwork(
+                        CELLULAR_NETWORK_REQUEST, testNetworkCallback),
+                android.Manifest.permission.CONNECTIVITY_INTERNAL);
+
+        final Network network = testNetworkCallback.waitForAvailable();
+        assertNotNull(network);
+
         // TODO(b/217559768): Receiving carrier config change and immediately checking carrier
         //  privileges is racy, as the CP status is updated after receiving the same signal. Move
         //  the CP check after sleep to temporarily reduce the flakiness. This will soon be fixed
@@ -494,7 +505,7 @@
                     final TestNetworkInterface tni = tnm.createTunInterface(new LinkAddress[0]);
                     tnm.setupTestNetwork(tni.getInterfaceName(), administratorUids, BINDER);
                     return tni;
-                });
+                }, android.Manifest.permission.MANAGE_TEST_NETWORKS);
     }
 
     private static class TestConnectivityDiagnosticsCallback
@@ -648,11 +659,9 @@
 
             final PersistableBundle carrierConfigs;
             try {
-                synchronized (mShellPermissionsIdentityLock) {
-                    carrierConfigs = callWithShellPermissionIdentity(
-                            () -> mCarrierConfigManager.getConfigForSubId(subId),
-                            android.Manifest.permission.READ_PHONE_STATE);
-                }
+                carrierConfigs = callWithShellPermissionIdentity(
+                        () -> mCarrierConfigManager.getConfigForSubId(subId),
+                        android.Manifest.permission.READ_PHONE_STATE);
             } catch (Exception exception) {
                 // callWithShellPermissionIdentity() threw an Exception - cache it and allow
                 // waitForCarrierConfigChanged() to throw it
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index ff7cb48..46e4f35 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -36,10 +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;
@@ -47,6 +59,7 @@
 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_DEFAULT;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
 import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
@@ -69,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;
@@ -79,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;
@@ -191,14 +205,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.DeviceConfigRule;
 import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
+import com.android.testutils.SkipPresubmit;
 import com.android.testutils.TestHttpServer;
 import com.android.testutils.TestNetworkTracker;
 import com.android.testutils.TestableNetworkCallback;
@@ -209,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;
@@ -258,10 +277,14 @@
 
 @RunWith(AndroidJUnit4.class)
 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 */);
 
@@ -277,9 +300,7 @@
     private static final int MIN_KEEPALIVE_INTERVAL = 10;
 
     private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
-    // Timeout for waiting network to be validated. Set the timeout to 30s, which is more than
-    // DNS timeout.
-    // TODO(b/252972908): reset the original timer when aosp/2188755 is ramped up.
+    // Timeout for waiting network to be validated.
     private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 30_000;
     private static final int NO_CALLBACK_TIMEOUT_MS = 100;
     private static final int NETWORK_REQUEST_TIMEOUT_MS = 3000;
@@ -291,6 +312,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;
@@ -334,8 +356,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<>();
 
@@ -417,11 +437,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),
@@ -431,15 +446,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));
         });
     }
 
@@ -561,7 +573,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));
 
@@ -732,6 +744,7 @@
         return mCm.getRedactedNetworkCapabilitiesForPackage(nc, uid, packageName);
     }
 
+    @ConnectivityModuleTest
     @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
     @AppModeFull(reason = "Cannot get installed packages in instant app mode")
     @Test
@@ -800,7 +813,7 @@
             // Make sure that the NC is null if the package doesn't hold ACCESS_NETWORK_STATE.
             assertNull(redactNc(nc, groundedUid, groundedPkg));
 
-            // Uids, ssid, underlying networks & subscriptionIds will be redacted if the given uid
+            // Uids, ssid & underlying networks will be redacted if the given uid
             // doesn't hold the associated permissions. The wifi transport info is also suitably
             // redacted.
             final NetworkCapabilities redactedNormal = redactNc(nc, normalUid, normalPkg);
@@ -857,8 +870,8 @@
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
 
-        Network wifiNetwork = mCtsNetUtils.connectToWifi();
-        Network cellNetwork = mCtsNetUtils.connectToCell();
+        Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
+        Network cellNetwork = networkCallbackRule.requestCell();
         // This server returns the requestor's IP address as the response body.
         String ipAddressEchoUrl = getIpAddressEchoUrlFromConfig();
         URL url = new URL(ipAddressEchoUrl);
@@ -1012,10 +1025,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();
     }
 
@@ -1045,12 +1058,14 @@
         final String invalidPrivateDnsServer = "invalidhostname.example.com";
         final String goodPrivateDnsServer = "dns.google";
         mCtsNetUtils.storePrivateDnsSetting();
-        final TestableNetworkCallback cb = new TestableNetworkCallback();
-        registerNetworkCallback(makeWifiNetworkRequest(), cb);
+        final NetworkRequest networkRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET).build();
+        final TestableNetworkCallback cb =
+                networkCallbackRule.registerNetworkCallback(networkRequest);
+        final Network networkForPrivateDns = mCm.getActiveNetwork();
         try {
             // Verifying the good private DNS sever
             mCtsNetUtils.setPrivateDnsStrictMode(goodPrivateDnsServer);
-            final Network networkForPrivateDns =  mCtsNetUtils.ensureWifiConnected();
             cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
                     entry -> hasPrivateDnsValidated(entry, networkForPrivateDns));
 
@@ -1061,8 +1076,11 @@
                     .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
         } finally {
             mCtsNetUtils.restorePrivateDnsSetting();
-            // Toggle wifi to make sure it is re-validated
-            reconnectWifi();
+            // Toggle network to make sure it is re-validated
+            mCm.reportNetworkConnectivity(networkForPrivateDns, true);
+            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> !(((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                    .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
         }
     }
 
@@ -1081,24 +1099,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;
@@ -1107,24 +1128,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);
         }
     }
@@ -1136,8 +1155,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);
     }
 
@@ -1239,13 +1258,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));
                 }
@@ -1260,6 +1280,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
@@ -1307,15 +1330,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();
     }
 
     /**
@@ -1325,21 +1347,21 @@
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
     public void testRequestNetworkCallback_onUnavailable() {
-        final boolean previousWifiEnabledState = mWifiManager.isWifiEnabled();
-        if (previousWifiEnabledState) {
-            mCtsNetUtils.ensureWifiDisconnected(null);
+        boolean previousWifiEnabledState = false;
+        if (mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+            previousWifiEnabledState = mWifiManager.isWifiEnabled();
+            if (previousWifiEnabledState) {
+                mCtsNetUtils.ensureWifiDisconnected(null);
+            }
         }
 
-        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();
@@ -1363,9 +1385,9 @@
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
     public void testToggleWifiConnectivityAction() throws Exception {
-        // toggleWifi calls connectToWifi and disconnectFromWifi, which both wait for
-        // CONNECTIVITY_ACTION broadcasts.
-        mCtsNetUtils.toggleWifi();
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
+
+        mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
     }
 
     /** Verify restricted networks cannot be requested. */
@@ -1426,40 +1448,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,
@@ -1523,7 +1553,7 @@
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
         final ContentResolver resolver = mContext.getContentResolver();
         mCtsNetUtils.ensureWifiConnected();
-        final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+        final String ssid = unquoteSSID(getSSID());
         final String oldMeteredSetting = getWifiMeteredStatus(ssid);
         final String oldMeteredMultipathPreference = Settings.Global.getString(
                 resolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
@@ -1536,7 +1566,7 @@
             // since R.
             final Network network = setWifiMeteredStatusAndWait(ssid, true /* isMetered */,
                     false /* waitForValidation */);
-            assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID()));
+            assertEquals(ssid, unquoteSSID(getSSID()));
             assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
                     NET_CAPABILITY_NOT_METERED), false);
             assertMultipathPreferenceIsEventually(network, initialMeteredPreference,
@@ -1564,6 +1594,40 @@
         }
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
+    public void testSetBackgroundNetworkingShellCommand() {
+        final int testUid = 54352;
+        runShellCommand("cmd connectivity set-background-networking-enabled-for-uid " + testUid
+                + " true");
+        int rule = runAsShell(NETWORK_SETTINGS,
+                () -> mCm.getUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid));
+        assertEquals(rule, FIREWALL_RULE_ALLOW);
+
+        runShellCommand("cmd connectivity set-background-networking-enabled-for-uid " + testUid
+                + " false");
+        rule = runAsShell(NETWORK_SETTINGS,
+                () -> mCm.getUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid));
+        assertEquals(rule, FIREWALL_RULE_DENY);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
+    public void testGetBackgroundNetworkingShellCommand() {
+        final int testUid = 54312;
+        runAsShell(NETWORK_SETTINGS,
+                () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid,
+                        FIREWALL_RULE_ALLOW));
+        String output = runShellCommand(
+                "cmd connectivity get-background-networking-enabled-for-uid " + testUid);
+        assertTrue(output.contains("allow"));
+
+        runAsShell(NETWORK_SETTINGS,
+                () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid,
+                        FIREWALL_RULE_DEFAULT));
+        output = runShellCommand(
+                "cmd connectivity get-background-networking-enabled-for-uid " + testUid);
+        assertTrue(output.contains("deny"));
+    }
+
     // TODO: move the following socket keep alive test to dedicated test class.
     /**
      * Callback used in tcp keepalive offload that allows caller to wait callback fires.
@@ -2001,7 +2065,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);
@@ -2067,15 +2131,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.
@@ -2091,7 +2155,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));
@@ -2153,6 +2217,7 @@
      */
     @AppModeFull(reason = "NETWORK_AIRPLANE_MODE permission can't be granted to instant apps")
     @Test
+    @SkipPresubmit(reason = "Out of SLO flakiness")
     public void testSetAirplaneMode() throws Exception{
         // Starting from T, wifi supports airplane mode enhancement which may not disconnect wifi
         // when airplane mode is on. The actual behavior that the device will have could only be
@@ -2175,8 +2240,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);
         }
 
@@ -2206,7 +2270,10 @@
                 mCtsNetUtils.ensureWifiConnected();
                 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 "
@@ -2216,7 +2283,7 @@
 
     private void registerCallbackAndWaitForAvailable(@NonNull final NetworkRequest request,
             @NonNull final TestableNetworkCallback cb) {
-        registerNetworkCallback(request, cb);
+        networkCallbackRule.registerNetworkCallback(request, cb);
         waitForAvailable(cb);
     }
 
@@ -2301,7 +2368,7 @@
                 mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         final Network network = mCtsNetUtils.ensureWifiConnected();
-        final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+        final String ssid = unquoteSSID(getSSID());
         assertNotNull("Ssid getting from WifiManager is null", ssid);
         // This package should have no NETWORK_SETTINGS permission. Verify that no ssid is contained
         // in the NetworkCapabilities.
@@ -2324,21 +2391,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());
     }
 
@@ -2367,8 +2426,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();
@@ -2377,14 +2436,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.
@@ -2455,8 +2515,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.
@@ -2477,9 +2540,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 =
@@ -2535,14 +2599,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
@@ -2735,7 +2799,8 @@
             // the network with the TEST transport. Also wait for validation here, in case there
             // is a bug that's only visible when the network is validated.
             setWifiMeteredStatusAndWait(ssid, true /* isMetered */, true /* waitForValidation */);
-            defaultCallback.expect(CallbackEntry.LOST, wifiNetwork, NETWORK_CALLBACK_TIMEOUT_MS);
+            defaultCallback.eventuallyExpect(CallbackEntry.LOST, NETWORK_CALLBACK_TIMEOUT_MS,
+                    l -> l.getNetwork().equals(wifiNetwork));
             waitForAvailable(defaultCallback, tnt.getNetwork());
             // Depending on if this device has cellular connectivity or not, multiple available
             // callbacks may be received. Eventually, metered Wi-Fi should be the final available
@@ -2776,17 +2841,19 @@
         final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback();
 
         final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
+        final Network testNetwork = tnt.getNetwork();
 
         testAndCleanup(() -> {
             setOemNetworkPreferenceForMyPackage(
                     OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY);
             registerTestOemNetworkPreferenceCallbacks(defaultCallback, systemDefaultCallback);
-            waitForAvailable(defaultCallback, tnt.getNetwork());
+            waitForAvailable(defaultCallback, testNetwork);
             systemDefaultCallback.eventuallyExpect(CallbackEntry.AVAILABLE,
                     NETWORK_CALLBACK_TIMEOUT_MS, cb -> wifiNetwork.equals(cb.getNetwork()));
         }, /* cleanup */ () -> {
                 runWithShellPermissionIdentity(tnt::teardown);
-                defaultCallback.expect(CallbackEntry.LOST, tnt, NETWORK_CALLBACK_TIMEOUT_MS);
+                defaultCallback.eventuallyExpect(CallbackEntry.LOST, NETWORK_CALLBACK_TIMEOUT_MS,
+                        cb -> testNetwork.equals(cb.getNetwork()));
 
                 // This network preference should only ever use the test network therefore available
                 // should not trigger when the test network goes down (e.g. switch to cellular).
@@ -2806,12 +2873,21 @@
     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);
     }
 
+    /**
+     * It needs android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
+     * to use WifiManager.getConnectionInfo() on the visible background user.
+     */
+    private String getSSID() {
+        return runWithShellPermissionIdentity(() ->
+                mWifiManager.getConnectionInfo().getSSID());
+    }
+
     private static final class OnCompleteListenerCallback {
         final CompletableFuture<Object> mDone = new CompletableFuture<>();
 
@@ -2877,20 +2953,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 {
@@ -2924,18 +2990,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
@@ -2963,17 +3029,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 */);
             });
@@ -3000,20 +3066,20 @@
         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();
 
-        final Network cellNetwork = mCtsNetUtils.connectToCell();
-        final Network wifiNetwork = prepareValidatedNetwork();
-
-        registerDefaultNetworkCallback(defaultCb);
-        registerNetworkCallback(makeWifiNetworkRequest(), wifiCb);
-
         try {
+            final Network cellNetwork = networkCallbackRule.requestCell();
+            final Network wifiNetwork = prepareValidatedNetwork();
+
+            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,
                     entry -> wifiNetwork.equals(entry.getNetwork()));
@@ -3075,20 +3141,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 {
@@ -3178,7 +3235,8 @@
     }
 
     @AppModeFull(reason = "Need WiFi support to test the default active network")
-    @Test
+    // NetworkActivityTracker is not mainlined before S.
+    @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
     public void testDefaultNetworkActiveListener() throws Exception {
         final boolean supportWifi = mPackageManager.hasSystemFeature(FEATURE_WIFI);
         final boolean supportTelephony = mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
@@ -3187,8 +3245,6 @@
 
         if (supportWifi) {
             mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
-        } else {
-            mCtsNetUtils.disconnectFromCell();
         }
 
         final CompletableFuture<Boolean> future = new CompletableFuture<>();
@@ -3199,7 +3255,7 @@
             if (supportWifi) {
                 mCtsNetUtils.ensureWifiConnected();
             } else {
-                mCtsNetUtils.connectToCell();
+                networkCallbackRule.requestCell();
             }
             assertTrue(future.get(LISTEN_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }, () -> {
@@ -3240,13 +3296,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
@@ -3262,7 +3319,8 @@
             newMobileDataPreferredUids.add(uid);
             ConnectivitySettingsManager.setMobileDataPreferredUids(
                     mContext, newMobileDataPreferredUids);
-            waitForAvailable(defaultTrackingCb, cellNetwork);
+            defaultTrackingCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> cellNetwork.equals(entry.getNetwork()));
             // No change for system default network. Expect no callback except CapabilitiesChanged
             // or LinkPropertiesChanged which may be triggered randomly from wifi network.
             assertNoCallbackExceptCapOrLpChange(systemDefaultCb);
@@ -3274,7 +3332,8 @@
             newMobileDataPreferredUids.remove(uid);
             ConnectivitySettingsManager.setMobileDataPreferredUids(
                     mContext, newMobileDataPreferredUids);
-            waitForAvailable(defaultTrackingCb, wifiNetwork);
+            defaultTrackingCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> wifiNetwork.equals(entry.getNetwork()));
             // No change for system default network. Expect no callback except CapabilitiesChanged
             // or LinkPropertiesChanged which may be triggered randomly from wifi network.
             assertNoCallbackExceptCapOrLpChange(systemDefaultCb);
@@ -3313,7 +3372,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();
@@ -3347,23 +3406,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();
@@ -3380,7 +3439,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));
@@ -3398,13 +3457,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();
@@ -3437,6 +3496,7 @@
 
     private void checkFirewallBlocking(final DatagramSocket srcSock, final DatagramSocket dstSock,
             final boolean expectBlock, final int chain) throws Exception {
+        final int uid = Process.myUid();
         final Random random = new Random();
         final byte[] sendData = new byte[100];
         random.nextBytes(sendData);
@@ -3452,7 +3512,8 @@
             fail("Expect not to be blocked by firewall but sending packet was blocked:"
                     + " chain=" + chain
                     + " chainEnabled=" + mCm.getFirewallChainEnabled(chain)
-                    + " uidFirewallRule=" + mCm.getUidFirewallRule(chain, Process.myUid()));
+                    + " uid=" + uid
+                    + " uidFirewallRule=" + mCm.getUidFirewallRule(chain, uid));
         }
 
         dstSock.receive(pkt);
@@ -3462,7 +3523,8 @@
             fail("Expect to be blocked by firewall but sending packet was not blocked:"
                     + " chain=" + chain
                     + " chainEnabled=" + mCm.getFirewallChainEnabled(chain)
-                    + " uidFirewallRule=" + mCm.getUidFirewallRule(chain, Process.myUid()));
+                    + " uid=" + uid
+                    + " uidFirewallRule=" + mCm.getUidFirewallRule(chain, uid));
         }
     }
 
@@ -3540,6 +3602,14 @@
         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() {
+        doTestFirewallBlocking(FIREWALL_CHAIN_BACKGROUND, ALLOWLIST);
+    }
+
     @Test @IgnoreUpTo(SC_V2) @ConnectivityModuleTest
     @AppModeFull(reason = "Socket cannot bind in instant app mode")
     public void testFirewallBlockingPowersave() {
@@ -3594,6 +3664,15 @@
         }
     }
 
+    private void setUidFirewallRule(final int chain, final int uid, final int rule) {
+        try {
+            mCm.setUidFirewallRule(chain, uid, rule);
+        } 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.
+        }
+    }
+
     private static final boolean EXPECT_OPEN = false;
     private static final boolean EXPECT_CLOSE = true;
 
@@ -3602,6 +3681,8 @@
         runWithShellPermissionIdentity(() -> {
             // Firewall chain status will be restored after the test.
             final boolean wasChainEnabled = mCm.getFirewallChainEnabled(chain);
+            final int myUid = Process.myUid();
+            final int previousMyUidFirewallRule = mCm.getUidFirewallRule(chain, myUid);
             final int previousUidFirewallRule = mCm.getUidFirewallRule(chain, targetUid);
             final Socket socket = new Socket(TEST_HOST, HTTP_PORT);
             socket.setSoTimeout(NETWORK_REQUEST_TIMEOUT_MS);
@@ -3609,12 +3690,12 @@
                 mCm.setFirewallChainEnabled(chain, false /* enable */);
                 assertSocketOpen(socket);
 
-                try {
-                    mCm.setUidFirewallRule(chain, targetUid, rule);
-                } 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.
+                setUidFirewallRule(chain, targetUid, rule);
+                if (targetUid != myUid) {
+                    // If this test does not set rule on myUid, remove existing rule on myUid
+                    setUidFirewallRule(chain, myUid, FIREWALL_RULE_DEFAULT);
                 }
+
                 mCm.setFirewallChainEnabled(chain, true /* enable */);
 
                 if (expectClose) {
@@ -3627,11 +3708,9 @@
                     mCm.setFirewallChainEnabled(chain, wasChainEnabled);
                 }, /* cleanup */ () -> {
                     // Restore the uid firewall rule status
-                    try {
-                        mCm.setUidFirewallRule(chain, targetUid, previousUidFirewallRule);
-                    } 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.
+                    setUidFirewallRule(chain, targetUid, previousUidFirewallRule);
+                    if (targetUid != myUid) {
+                        setUidFirewallRule(chain, myUid, previousMyUidFirewallRule);
                     }
                 }, /* cleanup */ () -> {
                     socket.close();
@@ -3679,63 +3758,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/DiscoveryRequestTest.kt b/tests/cts/net/src/android/net/cts/DiscoveryRequestTest.kt
new file mode 100644
index 0000000..909a5bc
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DiscoveryRequestTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.cts
+
+import android.net.Network
+import android.net.nsd.DiscoveryRequest
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import com.android.testutils.assertThrows
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** CTS tests for {@link DiscoveryRequest}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class DiscoveryRequestTest {
+    @Test
+    fun testParcelingIsLossLess() {
+        val requestWithNullFields =
+                DiscoveryRequest.Builder("_ipps._tcp").build()
+        val requestWithAllFields =
+                DiscoveryRequest.Builder("_ipps._tcp")
+                                .setSubtype("_xyz")
+                                .setNetwork(Network(1))
+                                .build()
+
+        assertParcelingIsLossless(requestWithNullFields)
+        assertParcelingIsLossless(requestWithAllFields)
+    }
+
+    @Test
+    fun testBuilder_success() {
+        val request = DiscoveryRequest.Builder("_ipps._tcp")
+                                      .setSubtype("_xyz")
+                                      .setNetwork(Network(1))
+                                      .build()
+
+        assertEquals("_ipps._tcp", request.serviceType)
+        assertEquals("_xyz", request.subtype)
+        assertEquals(Network(1), request.network)
+    }
+
+    @Test
+    fun testBuilderConstructor_emptyServiceType_throwsIllegalArgument() {
+        assertThrows(IllegalArgumentException::class.java) {
+            DiscoveryRequest.Builder("")
+        }
+    }
+
+    @Test
+    fun testEquality() {
+        val request1 = DiscoveryRequest.Builder("_ipps._tcp").build()
+        val request2 = DiscoveryRequest.Builder("_ipps._tcp").build()
+        val request3 = DiscoveryRequest.Builder("_ipps._tcp")
+                .setSubtype("_xyz")
+                .setNetwork(Network(1))
+                .build()
+        val request4 = DiscoveryRequest.Builder("_ipps._tcp")
+                .setSubtype("_xyz")
+                .setNetwork(Network(1))
+                .build()
+
+        assertEquals(request1, request2)
+        assertEquals(request3, request4)
+        assertNotEquals(request1, request3)
+        assertNotEquals(request2, request4)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 3821cea..fa44ae9 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -23,6 +23,7 @@
 import static android.net.DnsResolver.TYPE_AAAA;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.system.OsConstants.ETIMEDOUT;
 
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -59,10 +60,14 @@
 import com.android.net.module.util.DnsPacket;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceConfigRule;
+import com.android.testutils.DnsResolverModuleTest;
 import com.android.testutils.SkipPresubmit;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -79,6 +84,8 @@
 @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
 @RunWith(AndroidJUnit4.class)
 public class DnsResolverTest {
+    @ClassRule
+    public static final DeviceConfigRule DEVICE_CONFIG_CLASS_RULE = new DeviceConfigRule();
     @Rule
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
 
@@ -122,6 +129,20 @@
 
     private TestNetworkCallback mWifiRequestCallback = null;
 
+    /**
+     * @see BeforeClass
+     */
+    @BeforeClass
+    public static void beforeClass() throws Exception {
+        // Use async private DNS resolution to avoid flakes due to races applying the setting
+        DEVICE_CONFIG_CLASS_RULE.setConfig(NAMESPACE_CONNECTIVITY,
+                "networkmonitor_async_privdns_resolution", "1");
+        // Make sure NetworkMonitor is restarted before and after the test so the flag is applied
+        // and cleaned up.
+        maybeToggleWifiAndCell();
+        DEVICE_CONFIG_CLASS_RULE.runAfterNextCleanup(DnsResolverTest::maybeToggleWifiAndCell);
+    }
+
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getContext();
@@ -143,6 +164,12 @@
         }
     }
 
+    private static void maybeToggleWifiAndCell() throws Exception {
+        final CtsNetUtils utils = new CtsNetUtils(InstrumentationRegistry.getContext());
+        utils.reconnectWifiIfSupported();
+        utils.reconnectCellIfSupported();
+    }
+
     private static String byteArrayToHexString(byte[] bytes) {
         char[] hexChars = new char[bytes.length * 2];
         for (int i = 0; i < bytes.length; ++i) {
@@ -317,51 +344,61 @@
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testRawQuery() throws Exception {
         doTestRawQuery(mExecutor);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testRawQueryInline() throws Exception {
         doTestRawQuery(mExecutorInline);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testRawQueryBlob() throws Exception {
         doTestRawQueryBlob(mExecutor);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testRawQueryBlobInline() throws Exception {
         doTestRawQueryBlob(mExecutorInline);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testRawQueryRoot() throws Exception {
         doTestRawQueryRoot(mExecutor);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testRawQueryRootInline() throws Exception {
         doTestRawQueryRoot(mExecutorInline);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testRawQueryNXDomain() throws Exception {
         doTestRawQueryNXDomain(mExecutor);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testRawQueryNXDomainInline() throws Exception {
         doTestRawQueryNXDomain(mExecutorInline);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testRawQueryNXDomainWithPrivateDns() throws Exception {
         doTestRawQueryNXDomainWithPrivateDns(mExecutor);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testRawQueryNXDomainInlineWithPrivateDns() throws Exception {
         doTestRawQueryNXDomainWithPrivateDns(mExecutorInline);
     }
@@ -610,41 +647,49 @@
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testQueryForInetAddress() throws Exception {
         doTestQueryForInetAddress(mExecutor);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testQueryForInetAddressInline() throws Exception {
         doTestQueryForInetAddress(mExecutorInline);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testQueryForInetAddressIpv4() throws Exception {
         doTestQueryForInetAddressIpv4(mExecutor);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testQueryForInetAddressIpv4Inline() throws Exception {
         doTestQueryForInetAddressIpv4(mExecutorInline);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testQueryForInetAddressIpv6() throws Exception {
         doTestQueryForInetAddressIpv6(mExecutor);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testQueryForInetAddressIpv6Inline() throws Exception {
         doTestQueryForInetAddressIpv6(mExecutorInline);
     }
 
     @Test
+    @DnsResolverModuleTest
     public void testContinuousQueries() throws Exception {
         doTestContinuousQueries(mExecutor);
     }
 
     @Test
+    @DnsResolverModuleTest
     @SkipPresubmit(reason = "Flaky: b/159762682; add to presubmit after fixing")
     public void testContinuousQueriesInline() throws Exception {
         doTestContinuousQueries(mExecutorInline);
@@ -807,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);
 
@@ -841,4 +887,9 @@
             assertEquals(DnsResolver.ERROR_SYSTEM, e.code);
         }
     }
+
+    @Test
+    public void testNoRawBinderAccess() {
+        assertNull(mContext.getSystemService("dnsresolver"));
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index db13c49..f73134a 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -125,7 +125,7 @@
     private val TEST_TARGET_MAC_ADDR = MacAddress.fromString("12:34:56:78:9a:bc")
 
     private val realContext = InstrumentationRegistry.getContext()
-    private val cm = realContext.getSystemService(ConnectivityManager::class.java)
+    private val cm = realContext.getSystemService(ConnectivityManager::class.java)!!
 
     private val agentsToCleanUp = mutableListOf<NetworkAgent>()
     private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
@@ -160,7 +160,7 @@
         assumeTrue(kernelIsAtLeast(5, 15))
 
         runAsShell(MANAGE_TEST_NETWORKS) {
-            val tnm = realContext.getSystemService(TestNetworkManager::class.java)
+            val tnm = realContext.getSystemService(TestNetworkManager::class.java)!!
 
             // Only statically configure the IPv4 address; for IPv6, use the SLAAC generated
             // address.
@@ -306,7 +306,7 @@
 
         val socket = Os.socket(if (sendV6) AF_INET6 else AF_INET, SOCK_DGRAM or SOCK_NONBLOCK,
                 IPPROTO_UDP)
-        agent.network.bindSocket(socket)
+        checkNotNull(agent.network).bindSocket(socket)
 
         val originalPacket = testPacket.readAsArray()
         Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 732a42b..61ebd8f 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -92,7 +92,6 @@
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.assertTrue
-import kotlin.test.fail
 import org.junit.After
 import org.junit.Assume.assumeFalse
 import org.junit.Assume.assumeTrue
@@ -135,8 +134,8 @@
 class EthernetManagerTest {
 
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
-    private val em by lazy { context.getSystemService(EthernetManager::class.java) }
-    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val em by lazy { context.getSystemService(EthernetManager::class.java)!! }
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
     private val handler by lazy { Handler(Looper.getMainLooper()) }
 
     private val ifaceListener = EthernetStateListener()
@@ -160,7 +159,7 @@
 
         init {
             tnm = runAsShell(MANAGE_TEST_NETWORKS) {
-                context.getSystemService(TestNetworkManager::class.java)
+                context.getSystemService(TestNetworkManager::class.java)!!
             }
             tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
                 // Configuring a tun/tap interface always enables the carrier. If hasCarrier is
@@ -254,7 +253,7 @@
         }
 
         fun <T : CallbackEntry> expectCallback(expected: T): T {
-            val event = pollOrThrow()
+            val event = events.poll(TIMEOUT_MS)
             assertEquals(expected, event)
             return event as T
         }
@@ -267,24 +266,21 @@
             expectCallback(EthernetStateChanged(state))
         }
 
-        fun createChangeEvent(iface: String, state: Int, role: Int) =
+        private fun createChangeEvent(iface: String, state: Int, role: Int) =
                 InterfaceStateChanged(iface, state, role,
                         if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null)
 
-        fun pollOrThrow(): CallbackEntry {
-            return events.poll(TIMEOUT_MS) ?: fail("Did not receive callback after ${TIMEOUT_MS}ms")
+        fun eventuallyExpect(expected: CallbackEntry) {
+            val cb = events.poll(TIMEOUT_MS) { it == expected }
+            assertNotNull(cb, "Never received expected $expected. Received: ${events.backtrace()}")
         }
 
-        fun eventuallyExpect(expected: CallbackEntry) = events.poll(TIMEOUT_MS) { it == expected }
-
         fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
-            val event = createChangeEvent(iface.name, state, role)
-            assertNotNull(eventuallyExpect(event), "Never received expected $event")
+            eventuallyExpect(createChangeEvent(iface.name, state, role))
         }
 
         fun eventuallyExpect(state: Int) {
-            val event = EthernetStateChanged(state)
-            assertNotNull(eventuallyExpect(event), "Never received expected $event")
+            eventuallyExpect(EthernetStateChanged(state))
         }
 
         fun assertNoCallback() {
@@ -307,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 :
@@ -352,7 +336,9 @@
         }
     }
 
-    private fun isEthernetSupported() = em != null
+    private fun isEthernetSupported() : Boolean {
+        return context.getSystemService(EthernetManager::class.java) != null
+    }
 
     @Before
     fun setUp() {
@@ -389,6 +375,9 @@
         }
         registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
         releaseTetheredInterface()
+        // Force releaseTetheredInterface() to be processed before starting the next test by calling
+        // setEthernetEnabled(true) which always waits on a callback.
+        setEthernetEnabled(true)
     }
 
     // Setting the carrier up / down relies on TUNSETCARRIER which was added in kernel version 5.0.
@@ -639,6 +628,9 @@
             // do nothing -- the TimeoutException indicates that no interface is available for
             // tethering.
             releaseTetheredInterface()
+            // Force releaseTetheredInterface() to be processed before proceeding by calling
+            // setEthernetEnabled(true) which always waits on a callback.
+            setEthernetEnabled(true)
         }
     }
 
@@ -652,9 +644,8 @@
 
         val listener = EthernetStateListener()
         addInterfaceStateListener(listener)
-        // Note: using eventuallyExpect as there may be other interfaces present.
-        listener.eventuallyExpect(InterfaceStateChanged(iface.name,
-                STATE_LINK_UP, ROLE_SERVER, /* IpConfiguration */ null))
+        // TODO(b/295146844): do not report IpConfiguration for server mode interfaces.
+        listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
 
         releaseTetheredInterface()
         listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
@@ -668,6 +659,20 @@
     }
 
     @Test
+    fun testCallbacks_afterRemovingServerModeInterface() {
+        // do not run this test if an interface that can be used for tethering already exists.
+        assumeNoInterfaceForTetheringAvailable()
+
+        val iface = createInterface()
+        requestTetheredInterface().expectOnAvailable()
+        removeInterface(iface)
+
+        val listener = EthernetStateListener()
+        addInterfaceStateListener(listener)
+        listener.assertNoCallback()
+    }
+
+    @Test
     fun testGetInterfaceList() {
         // Create two test interfaces and check the return list contains the interface names.
         val iface1 = createInterface()
@@ -884,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/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 805dd65..cb55bd5 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -22,6 +22,7 @@
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
@@ -34,12 +35,14 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
+import static org.junit.Assume.assumeFalse;
 
 import android.Manifest;
 import android.annotation.NonNull;
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.net.Ikev2VpnProfile;
 import android.net.IpSecAlgorithm;
@@ -60,11 +63,7 @@
 
 import com.android.internal.util.HexDump;
 import com.android.networkstack.apishim.ConstantsShim;
-import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
-import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
 import com.android.networkstack.apishim.VpnManagerShimImpl;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
 import com.android.networkstack.apishim.common.VpnManagerShim;
 import com.android.networkstack.apishim.common.VpnProfileStateShim;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -75,6 +74,7 @@
 
 import org.bouncycastle.x509.X509V1CertificateGenerator;
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -184,6 +184,8 @@
 
     // Static state to reduce setup/teardown
     private static final Context sContext = InstrumentationRegistry.getContext();
+    private static boolean sIsWatch =
+                sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
     private static final ConnectivityManager sCM =
             (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
     private static final VpnManager sVpnMgr =
@@ -203,13 +205,31 @@
         mUserCertKey = generateRandomCertAndKeyPair();
     }
 
+    @Before
+    public void setUp() {
+        assumeFalse("Skipping test because watches don't support VPN", sIsWatch);
+    }
+
     @After
     public void tearDown() {
+        if (sIsWatch) {
+            return; // Tests are skipped for watches.
+        }
+
         for (TestableNetworkCallback callback : mCallbacksToUnregister) {
             sCM.unregisterNetworkCallback(callback);
         }
         setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
         setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
+
+        // Make sure the VpnProfile is not provisioned already.
+        sVpnMgr.stopProvisionedVpnProfile();
+
+        try {
+            sVpnMgr.startProvisionedVpnProfile();
+            fail("Expected SecurityException for missing consent");
+        } catch (SecurityException expected) {
+        }
     }
 
     /**
@@ -227,28 +247,25 @@
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileCommon(
-            @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks,
+            @NonNull Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks,
             boolean requiresValidation, boolean automaticIpVersionSelectionEnabled,
             boolean automaticNattKeepaliveTimerEnabled) throws Exception {
 
-        builderShim.setBypassable(true)
+        builder.setBypassable(true)
                 .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
                 .setProxy(TEST_PROXY_INFO)
                 .setMaxMtu(TEST_MTU)
                 .setMetered(false);
-        if (TestUtils.shouldTestTApis()) {
-            builderShim.setRequiresInternetValidation(requiresValidation);
+        if (isAtLeastT()) {
+            builder.setRequiresInternetValidation(requiresValidation);
         }
 
-        if (TestUtils.shouldTestUApis()) {
-            builderShim.setAutomaticIpVersionSelectionEnabled(automaticIpVersionSelectionEnabled);
-            builderShim.setAutomaticNattKeepaliveTimerEnabled(automaticNattKeepaliveTimerEnabled);
+        if (isAtLeastU()) {
+            builder.setAutomaticIpVersionSelectionEnabled(automaticIpVersionSelectionEnabled);
+            builder.setAutomaticNattKeepaliveTimerEnabled(automaticNattKeepaliveTimerEnabled);
         }
 
-        // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
-        // method and is not defined in shims.
         // TODO: replace it in alternative way to remove the hidden method usage
-        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
@@ -264,16 +281,14 @@
                         ? IkeSessionTestUtils.IKE_PARAMS_V6 : IkeSessionTestUtils.IKE_PARAMS_V4,
                         IkeSessionTestUtils.CHILD_PARAMS);
 
-        final Ikev2VpnProfileBuilderShim builderShim =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(params)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(params)
                         .setRequiresInternetValidation(requiresValidation)
                         .setProxy(TEST_PROXY_INFO)
                         .setMaxMtu(TEST_MTU)
                         .setMetered(false);
-        // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
-        // method and is not defined in shims.
+
         // TODO: replace it in alternative way to remove the hidden method usage
-        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
@@ -283,8 +298,8 @@
     private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
             boolean isRestrictedToTestNetworks, boolean requiresValidation)
             throws Exception {
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY)
                         .setAuthPsk(TEST_PSK);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 requiresValidation, false /* automaticIpVersionSelectionEnabled */,
@@ -293,8 +308,8 @@
 
     private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 false /* requiresValidation */, false /* automaticIpVersionSelectionEnabled */,
@@ -303,8 +318,8 @@
 
     private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthDigitalSignature(
                                 mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
@@ -347,7 +362,6 @@
     @Test
     public void testBuildIkev2VpnProfileWithIkeTunnelConnectionParams() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
-        assumeTrue(TestUtils.shouldTestTApis());
 
         final IkeTunnelConnectionParams expectedParams = new IkeTunnelConnectionParams(
                 IkeSessionTestUtils.IKE_PARAMS_V6, IkeSessionTestUtils.CHILD_PARAMS);
@@ -567,7 +581,7 @@
         // regardless of its value. However, there is a race in Vpn(see b/228574221) that VPN may
         // misuse VPN network itself as the underlying network. The fix is not available without
         // SDK > T platform. Thus, verify this only on T+ platform.
-        if (!requiresValidation && TestUtils.shouldTestTApis()) {
+        if (!requiresValidation && isAtLeastT()) {
             cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, TIMEOUT_MS,
                     entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
                             .hasCapability(NET_CAPABILITY_VALIDATED));
@@ -639,7 +653,7 @@
                         testIpv6Only, requiresValidation, testSessionKey , testIkeTunConnParams)));
     }
 
-    @Test
+    @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileV4() throws Exception {
         doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
@@ -647,12 +661,11 @@
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileV4WithValidation() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
-    @Test
+    @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileV6() throws Exception {
         doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
@@ -660,35 +673,30 @@
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileV6WithValidation() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileIkeTunConnParamsV4() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
                 false /* testSessionKey */, true /* testIkeTunConnParams */);
     }
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileIkeTunConnParamsV4WithValidation() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
                 false /* testSessionKey */, true /* testIkeTunConnParams */);
     }
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileIkeTunConnParamsV6() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
                 false /* testSessionKey */, true /* testIkeTunConnParams */);
     }
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileIkeTunConnParamsV6WithValidation() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
                 false /* testSessionKey */, true /* testIkeTunConnParams */);
     }
@@ -696,7 +704,6 @@
     @IgnoreUpTo(SC_V2)
     @Test
     public void testStartProvisionedVpnV4ProfileSession() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
                 true /* testSessionKey */, false /* testIkeTunConnParams */);
     }
@@ -704,59 +711,44 @@
     @IgnoreUpTo(SC_V2)
     @Test
     public void testStartProvisionedVpnV6ProfileSession() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
                 true /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     @Test
     public void testBuildIkev2VpnProfileWithAutomaticNattKeepaliveTimerEnabled() throws Exception {
-        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) because this test also requires API
-        // 34 shims, and @IgnoreUpTo does not check that.
-        assumeTrue(TestUtils.shouldTestUApis());
-
         final Ikev2VpnProfile profileWithDefaultValue = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
                 false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shimWithDefaultValue =
-                Ikev2VpnProfileShimImpl.newInstance(profileWithDefaultValue);
-        assertFalse(shimWithDefaultValue.isAutomaticNattKeepaliveTimerEnabled());
+        assertFalse(profileWithDefaultValue.isAutomaticNattKeepaliveTimerEnabled());
 
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthPsk(TEST_PSK);
         final Ikev2VpnProfile profile = buildIkev2VpnProfileCommon(builder,
                 false /* isRestrictedToTestNetworks */,
                 false /* requiresValidation */,
                 false /* automaticIpVersionSelectionEnabled */,
                 true /* automaticNattKeepaliveTimerEnabled */);
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim =
-                Ikev2VpnProfileShimImpl.newInstance(profile);
-        assertTrue(shim.isAutomaticNattKeepaliveTimerEnabled());
+        assertTrue(profile.isAutomaticNattKeepaliveTimerEnabled());
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     @Test
     public void testBuildIkev2VpnProfileWithAutomaticIpVersionSelectionEnabled() throws Exception {
-        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) because this test also requires API
-        // 34 shims, and @IgnoreUpTo does not check that.
-        assumeTrue(TestUtils.shouldTestUApis());
-
         final Ikev2VpnProfile profileWithDefaultValue = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
                 false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shimWithDefaultValue =
-                Ikev2VpnProfileShimImpl.newInstance(profileWithDefaultValue);
-        assertFalse(shimWithDefaultValue.isAutomaticIpVersionSelectionEnabled());
+        assertFalse(profileWithDefaultValue.isAutomaticIpVersionSelectionEnabled());
 
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthPsk(TEST_PSK);
         final Ikev2VpnProfile profile = buildIkev2VpnProfileCommon(builder,
                 false /* isRestrictedToTestNetworks */,
                 false /* requiresValidation */,
                 true /* automaticIpVersionSelectionEnabled */,
                 false /* automaticNattKeepaliveTimerEnabled */);
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim =
-                Ikev2VpnProfileShimImpl.newInstance(profile);
-        assertTrue(shim.isAutomaticIpVersionSelectionEnabled());
+        assertTrue(profile.isAutomaticIpVersionSelectionEnabled());
     }
 
     private static class CertificateAndKey {
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
index 7f710d7..c480135 100644
--- a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -23,15 +23,21 @@
 import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384;
 import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512;
 import static android.net.IpSecAlgorithm.CRYPT_AES_CBC;
+import static android.net.cts.PacketUtils.ICMP_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
 import static android.system.OsConstants.FIONREAD;
 
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 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 +71,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 +93,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 +133,54 @@
                                 .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);
+
+        // There might be ICMPv6(Router Solicitation) packets. Thus we can only check the lower
+        // bound of the outgoing traffic.
+        final long icmpV6RsCnt = transformState.getTxHighestSequenceNumber() - txHighestSeqNum;
+        assertTrue(icmpV6RsCnt >= 0);
+
+        final long adjustedPacketCnt = packetCnt + icmpV6RsCnt;
+        final long adjustedByteCnt = byteCnt + icmpV6RsCnt * (IP6_HDRLEN + ICMP_HDRLEN);
+
+        assertEquals(adjustedPacketCnt, transformState.getPacketCount());
+        assertEquals(adjustedByteCnt, transformState.getByteCount());
+        assertEquals(rxHighestSeqNum, transformState.getRxHighestSequenceNumber());
+        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 f935cef..b5f43d3 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,8 +83,10 @@
 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;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -97,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 {
@@ -380,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);
@@ -425,6 +451,11 @@
             long uidTxDelta = 0;
             long uidRxDelta = 0;
             for (int i = 0; i < 100; i++) {
+                // 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());
+                }
                 uidTxDelta = TrafficStats.getUidTxPackets(Os.getuid()) - uidTxPackets;
                 uidRxDelta = TrafficStats.getUidRxPackets(Os.getuid()) - uidRxPackets;
 
@@ -457,9 +488,8 @@
             long newUidRxPackets = TrafficStats.getUidRxPackets(Os.getuid());
 
             assertEquals(expectedTxByteDelta, newUidTxBytes - uidTxBytes);
-            assertTrue(
-                    newUidRxBytes - uidRxBytes >= minRxByteDelta
-                            && newUidRxBytes - uidRxBytes <= maxRxByteDelta);
+            assertTrue("Not enough bytes", newUidRxBytes - uidRxBytes >= minRxByteDelta);
+            assertTrue("Too many bytes", newUidRxBytes - uidRxBytes <= maxRxByteDelta);
             assertEquals(expectedTxPacketDelta, newUidTxPackets - uidTxPackets);
             assertEquals(expectedRxPacketDelta, newUidRxPackets - uidRxPackets);
         }
@@ -500,6 +530,11 @@
         }
 
         private static void initStatsChecker() throws Exception {
+            // 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());
+            }
             uidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
             uidRxBytes = TrafficStats.getUidRxBytes(Os.getuid());
             uidTxPackets = TrafficStats.getUidTxPackets(Os.getuid());
@@ -717,6 +752,7 @@
     }
 
     @Test
+    @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
     public void testIkeOverUdpEncapSocket() throws Exception {
         // IPv6 not supported for UDP-encap-ESP
         InetAddress local = InetAddress.getByName(IPV4_LOOPBACK);
@@ -1595,4 +1631,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..890c071 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;
@@ -63,6 +65,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.CollectionUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
@@ -117,6 +120,8 @@
 
     private static final int TIMEOUT_MS = 500;
 
+    private static final int PACKET_COUNT = 100;
+
     // Static state to reduce setup/teardown
     private static ConnectivityManager sCM;
     private static TestNetworkManager sTNM;
@@ -256,7 +261,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 +287,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 +303,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 +412,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 +549,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;
                 }
             };
@@ -1048,6 +1089,27 @@
             UdpEncapsulationSocket encapSocket,
             IpSecTunnelTestRunnable test)
             throws Exception {
+        return buildTunnelNetworkAndRunTests(
+                localInner,
+                remoteInner,
+                localOuter,
+                remoteOuter,
+                spi,
+                encapSocket,
+                test,
+                true /* enableEncrypt */);
+    }
+
+    private int buildTunnelNetworkAndRunTests(
+            InetAddress localInner,
+            InetAddress remoteInner,
+            InetAddress localOuter,
+            InetAddress remoteOuter,
+            int spi,
+            UdpEncapsulationSocket encapSocket,
+            IpSecTunnelTestRunnable test,
+            boolean enableEncrypt)
+            throws Exception {
         int innerPrefixLen = localInner instanceof Inet6Address ? IP6_PREFIX_LEN : IP4_PREFIX_LEN;
         TestNetworkCallback testNetworkCb = null;
         int innerSocketPort;
@@ -1075,8 +1137,12 @@
 
             // Configure Transform parameters
             IpSecTransform.Builder transformBuilder = new IpSecTransform.Builder(sContext);
-            transformBuilder.setEncryption(
-                    new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+
+            if (enableEncrypt) {
+                transformBuilder.setEncryption(
+                        new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+            }
+
             transformBuilder.setAuthentication(
                     new IpSecAlgorithm(
                             IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4));
@@ -1127,6 +1193,19 @@
         return innerSocketPort;
     }
 
+    private int buildTunnelNetworkAndRunTestsSimple(
+            int spi, IpSecTunnelTestRunnable test, boolean enableEncrypt) throws Exception {
+        return buildTunnelNetworkAndRunTests(
+                LOCAL_INNER_6,
+                REMOTE_INNER_6,
+                LOCAL_OUTER_6,
+                REMOTE_OUTER_6,
+                spi,
+                null /* encapSocket */,
+                test,
+                enableEncrypt);
+    }
+
     private static void receiveAndValidatePacket(JavaUdpSocket socket) throws Exception {
         byte[] socketResponseBytes = socket.receive();
         assertArrayEquals(TEST_DATA, socketResponseBytes);
@@ -1691,4 +1770,108 @@
         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(PACKET_COUNT));
+
+                    return innerSocketPort;
+                },
+                true /* enableEncrypt */);
+    }
+
+    @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 outSeqNum = 1;
+                    int receivedTestDataEspCnt = 0;
+
+                    while (receivedTestDataEspCnt < PACKET_COUNT) {
+                        outSocket.sendTo(TEST_DATA, REMOTE_INNER_6, innerSocketPort);
+
+                        byte[] pkt = null;
+
+                        // If it is an ESP that contains the TEST_DATA, move to the next
+                        // loop. Otherwise, the ESP may contain an ICMPv6(Router Solicitation).
+                        // In this case, just increase the expected sequence number and continue
+                        // waiting for the ESP with TEST_DATA
+                        do {
+                            pkt = tunUtils.awaitEspPacket(spi, false /* useEncap */, outSeqNum++);
+                        } while (CollectionUtils.indexOfSubArray(pkt, TEST_DATA) == -1);
+                        receivedTestDataEspCnt++;
+                    }
+
+                    final int innerPacketSize =
+                            getInnerPacketSize(AF_INET6, false /* transportInTunnelMode */);
+                    checkTransformState(
+                            outTunnelTransform,
+                            PACKET_COUNT,
+                            0L,
+                            PACKET_COUNT,
+                            PACKET_COUNT * (long) innerPacketSize,
+                            newReplayBitmap(0));
+
+                    return innerSocketPort;
+                },
+                false /* enableEncrypt */);
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java b/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java
new file mode 100644
index 0000000..7b42306
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.net.IpSecTransformState;
+import android.os.Build;
+import android.os.SystemClock;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RunWith(DevSdkIgnoreRunner.class)
+public class IpSecTransformStateTest {
+    @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+    private static final long TIMESTAMP_MILLIS = 1000L;
+    private static final long HIGHEST_SEQ_NUMBER_TX = 10000L;
+    private static final long HIGHEST_SEQ_NUMBER_RX = 20000L;
+    private static final long PACKET_COUNT = 9000L;
+    private static final long BYTE_COUNT = 900000L;
+
+    private static final int REPLAY_BITMAP_LEN_BYTE = 512;
+    private static final byte[] REPLAY_BITMAP_NO_PACKETS = new byte[REPLAY_BITMAP_LEN_BYTE];
+    private static final byte[] REPLAY_BITMAP_ALL_RECEIVED = new byte[REPLAY_BITMAP_LEN_BYTE];
+
+    static {
+        for (int i = 0; i < REPLAY_BITMAP_ALL_RECEIVED.length; i++) {
+            REPLAY_BITMAP_ALL_RECEIVED[i] = (byte) 0xff;
+        }
+    }
+
+    @Test
+    public void testBuildAndGet() {
+        final IpSecTransformState state =
+                new IpSecTransformState.Builder()
+                        .setTimestampMillis(TIMESTAMP_MILLIS)
+                        .setTxHighestSequenceNumber(HIGHEST_SEQ_NUMBER_TX)
+                        .setRxHighestSequenceNumber(HIGHEST_SEQ_NUMBER_RX)
+                        .setPacketCount(PACKET_COUNT)
+                        .setByteCount(BYTE_COUNT)
+                        .setReplayBitmap(REPLAY_BITMAP_ALL_RECEIVED)
+                        .build();
+
+        assertEquals(TIMESTAMP_MILLIS, state.getTimestampMillis());
+        assertEquals(HIGHEST_SEQ_NUMBER_TX, state.getTxHighestSequenceNumber());
+        assertEquals(HIGHEST_SEQ_NUMBER_RX, state.getRxHighestSequenceNumber());
+        assertEquals(PACKET_COUNT, state.getPacketCount());
+        assertEquals(BYTE_COUNT, state.getByteCount());
+        assertArrayEquals(REPLAY_BITMAP_ALL_RECEIVED, state.getReplayBitmap());
+    }
+
+    @Test
+    public void testSelfGeneratedTimestampMillis() {
+        final long elapsedRealtimeBefore = SystemClock.elapsedRealtime();
+
+        final IpSecTransformState state =
+                new IpSecTransformState.Builder().setReplayBitmap(REPLAY_BITMAP_NO_PACKETS).build();
+
+        final long elapsedRealtimeAfter = SystemClock.elapsedRealtime();
+
+        // Verify  elapsedRealtimeBefore <= state.getTimestampMillis() <= elapsedRealtimeAfter
+        assertFalse(elapsedRealtimeBefore > state.getTimestampMillis());
+        assertFalse(elapsedRealtimeAfter < state.getTimestampMillis());
+    }
+
+    @Test
+    public void testBuildWithoutReplayBitmap() throws Exception {
+        try {
+            new IpSecTransformState.Builder().build();
+            fail("Expected expcetion if replay bitmap is not set");
+        } catch (NullPointerException expected) {
+        }
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 691ab99..2c7d5c6 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -16,24 +16,52 @@
 
 package android.net.cts;
 
+import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 
-import android.content.Context;
+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.fail;
+
 import android.content.ContentResolver;
+import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkUtils;
 import android.net.cts.util.CtsNetUtils;
 import android.platform.test.annotations.AppModeFull;
-import android.provider.Settings;
 import android.system.ErrnoException;
 import android.system.OsConstants;
-import android.test.AndroidTestCase;
+import android.util.ArraySet;
 
-import java.util.ArrayList;
+import androidx.test.platform.app.InstrumentationRegistry;
 
-public class MultinetworkApiTest extends AndroidTestCase {
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.DeviceConfigRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+@RunWith(DevSdkIgnoreRunner.class)
+public class MultinetworkApiTest {
+    @Rule(order = 1)
+    public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule();
+
+    @Rule(order = 2)
+    public final AutoReleaseNetworkCallbackRule
+            mNetworkCallbackRule = new AutoReleaseNetworkCallbackRule();
 
     static {
         System.loadLibrary("nativemultinetwork_jni");
@@ -59,41 +87,19 @@
     private ContentResolver mCR;
     private ConnectivityManager mCM;
     private CtsNetUtils mCtsNetUtils;
-    private String mOldMode;
-    private String mOldDnsSpecifier;
+    private Context mContext;
+    private Network mRequestedCellNetwork;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-        mCR = getContext().getContentResolver();
-        mCtsNetUtils = new CtsNetUtils(getContext());
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mCM = mContext.getSystemService(ConnectivityManager.class);
+        mCR = mContext.getContentResolver();
+        mCtsNetUtils = new CtsNetUtils(mContext);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-    }
-
-    private Network[] getTestableNetworks() {
-        final ArrayList<Network> testableNetworks = new ArrayList<Network>();
-        for (Network network : mCM.getAllNetworks()) {
-            final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
-            if (nc != null
-                    && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
-                    && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
-                testableNetworks.add(network);
-            }
-        }
-
-        assertTrue(
-                "This test requires that at least one network be connected. " +
-                "Please ensure that the device is connected to a network.",
-                testableNetworks.size() >= 1);
-        return testableNetworks.toArray(new Network[0]);
-    }
-
-    public void testGetaddrinfo() throws ErrnoException {
+    @Test
+    public void testGetaddrinfo() throws Exception {
         for (Network network : getTestableNetworks()) {
             int errno = runGetaddrinfoCheck(network.getNetworkHandle());
             if (errno != 0) {
@@ -103,8 +109,9 @@
         }
     }
 
+    @Test
     @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
-    public void testSetprocnetwork() throws ErrnoException {
+    public void testSetprocnetwork() throws Exception {
         // Hopefully no prior test in this process space has set a default network.
         assertNull(mCM.getProcessDefaultNetwork());
         assertEquals(0, NetworkUtils.getBoundNetworkForProcess());
@@ -146,8 +153,9 @@
         }
     }
 
+    @Test
     @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
-    public void testSetsocknetwork() throws ErrnoException {
+    public void testSetsocknetwork() throws Exception {
         for (Network network : getTestableNetworks()) {
             int errno = runSetsocknetwork(network.getNetworkHandle());
             if (errno != 0) {
@@ -157,7 +165,8 @@
         }
     }
 
-    public void testNativeDatagramTransmission() throws ErrnoException {
+    @Test
+    public void testNativeDatagramTransmission() throws Exception {
         for (Network network : getTestableNetworks()) {
             int errno = runDatagramCheck(network.getNetworkHandle());
             if (errno != 0) {
@@ -167,7 +176,8 @@
         }
     }
 
-    public void testNoSuchNetwork() {
+    @Test
+    public void testNoSuchNetwork() throws Exception {
         final Network eNoNet = new Network(54321);
         assertNull(mCM.getNetworkInfo(eNoNet));
 
@@ -179,7 +189,8 @@
         // assertEquals(-OsConstants.ENONET, runGetaddrinfoCheck(eNoNetHandle));
     }
 
-    public void testNetworkHandle() {
+    @Test
+    public void testNetworkHandle() throws Exception {
         // Test Network -> NetworkHandle -> Network results in the same Network.
         for (Network network : getTestableNetworks()) {
             long networkHandle = network.getNetworkHandle();
@@ -202,10 +213,9 @@
         } catch (IllegalArgumentException e) {}
     }
 
+    @Test
     public void testResNApi() throws Exception {
-        final Network[] testNetworks = getTestableNetworks();
-
-        for (Network network : testNetworks) {
+        for (Network network : getTestableNetworks()) {
             // Throws AssertionError directly in jni function if test fail.
             runResNqueryCheck(network.getNetworkHandle());
             runResNsendCheck(network.getNetworkHandle());
@@ -222,9 +232,21 @@
         }
     }
 
+    @Test
     @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
-    public void testResNApiNXDomainPrivateDns() throws InterruptedException {
+    public void testResNApiNXDomainPrivateDns() throws Exception {
+        // Use async private DNS resolution to avoid flakes due to races applying the setting
+        mDeviceConfigRule.setConfig(NAMESPACE_CONNECTIVITY,
+                "networkmonitor_async_privdns_resolution", "1");
+        mCtsNetUtils.reconnectWifiIfSupported();
+        mCtsNetUtils.reconnectCellIfSupported();
+
         mCtsNetUtils.storePrivateDnsSetting();
+
+        mDeviceConfigRule.runAfterNextCleanup(() -> {
+            mCtsNetUtils.reconnectWifiIfSupported();
+            mCtsNetUtils.reconnectCellIfSupported();
+        });
         // Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
         // b/144521720
         try {
@@ -239,4 +261,52 @@
             mCtsNetUtils.restorePrivateDnsSetting();
         }
     }
+
+    /**
+     * 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 (mRequestedCellNetwork == null) {
+                mRequestedCellNetwork = mNetworkCallbackRule.requestCell();
+            }
+            assertNotNull("Cell network requested but not obtained", mRequestedCellNetwork);
+            testableNetworks.add(mRequestedCellNetwork);
+        }
+
+        if (mContext.getPackageManager().hasSystemFeature(FEATURE_WIFI)) {
+            testableNetworks.add(mCtsNetUtils.ensureWifiConnected());
+        }
+
+        // Obtain other networks through the synchronous API, if any.
+        for (Network network : mCtsNetUtils.getTestableNetworks()) {
+            final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+            if (nc != null
+                    && !nc.hasTransport(TRANSPORT_WIFI)
+                    && !nc.hasTransport(TRANSPORT_CELLULAR)) {
+                testableNetworks.add(network);
+            }
+        }
+
+        // In practice this should not happen as getTestableNetworks throws if there is no network
+        // at all.
+        assertFalse("This device does not support WiFi nor cell data, and does not have any other "
+                        + "network connected. This test requires at least one internet-providing "
+                        + "network.",
+                testableNetworks.isEmpty());
+        return testableNetworks;
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 98ea224..60081d4 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -15,9 +15,13 @@
  */
 package android.net.cts
 
+import android.Manifest.permission.MODIFY_PHONE_STATE
 import android.Manifest.permission.NETWORK_SETTINGS
+import android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
 import android.app.Instrumentation
 import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION
 import android.net.ConnectivityManager
 import android.net.EthernetNetworkSpecifier
 import android.net.INetworkAgent
@@ -44,7 +48,9 @@
 import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
 import android.net.NetworkCapabilities.TRANSPORT_TEST
 import android.net.NetworkCapabilities.TRANSPORT_VPN
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
@@ -53,6 +59,7 @@
 import android.net.NetworkReleasedException
 import android.net.NetworkRequest
 import android.net.NetworkScore
+import android.net.NetworkSpecifier
 import android.net.QosCallback
 import android.net.QosCallback.QosCallbackRegistrationException
 import android.net.QosCallbackException
@@ -61,29 +68,41 @@
 import android.net.QosSocketInfo
 import android.net.RouteInfo
 import android.net.SocketKeepalive
+import android.net.TelephonyNetworkSpecifier
 import android.net.TestNetworkInterface
 import android.net.TestNetworkManager
+import android.net.TransportInfo
 import android.net.Uri
 import android.net.VpnManager
 import android.net.VpnTransportInfo
 import android.net.cts.NetworkAgentTest.TestableQosCallback.CallbackEntry.OnError
 import android.net.cts.NetworkAgentTest.TestableQosCallback.CallbackEntry.OnQosSessionAvailable
 import android.net.cts.NetworkAgentTest.TestableQosCallback.CallbackEntry.OnQosSessionLost
+import android.net.wifi.WifiInfo
 import android.os.Build
+import android.os.ConditionVariable
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.Message
+import android.os.PersistableBundle
 import android.os.Process
 import android.os.SystemClock
 import android.platform.test.annotations.AppModeFull
 import android.system.OsConstants.IPPROTO_TCP
 import android.system.OsConstants.IPPROTO_UDP
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionManager
 import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.CarrierPrivilegesCallback
 import android.telephony.data.EpsBearerQosSessionAttributes
+import android.util.ArraySet
 import android.util.DebugUtils.valueToString
+import android.util.Log
 import androidx.test.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
 import com.android.compatibility.common.util.ThrowingSupplier
+import com.android.compatibility.common.util.UiccUtil
 import com.android.modules.utils.build.SdkLevel
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.testutils.CompatUtil
@@ -112,12 +131,15 @@
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.assertThrows
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
 import java.io.Closeable
 import java.io.IOException
 import java.net.DatagramSocket
 import java.net.InetAddress
 import java.net.InetSocketAddress
 import java.net.Socket
+import java.security.MessageDigest
 import java.time.Duration
 import java.util.Arrays
 import java.util.UUID
@@ -130,6 +152,7 @@
 import kotlin.test.assertTrue
 import kotlin.test.fail
 import org.junit.After
+import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -141,6 +164,7 @@
 import org.mockito.Mockito.timeout
 import org.mockito.Mockito.verify
 
+private const val TAG = "NetworkAgentTest"
 // This test doesn't really have a constraint on how fast the methods should return. If it's
 // going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
 // without affecting the run time of successful runs. Thus, set a very high timeout.
@@ -170,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")
@@ -257,21 +282,22 @@
         callback: TestableNetworkCallback,
         handler: Handler
     ) {
-        mCM!!.registerBestMatchingNetworkCallback(request, callback, handler)
+        mCM.registerBestMatchingNetworkCallback(request, callback, handler)
         callbacksToCleanUp.add(callback)
     }
 
-    private fun makeTestNetworkRequest(specifier: String? = null): NetworkRequest {
-        return NetworkRequest.Builder()
-                .clearCapabilities()
-                .addTransportType(TRANSPORT_TEST)
-                .also {
-                    if (specifier != null) {
-                        it.setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifier))
-                    }
-                }
-                .build()
-    }
+    private fun String?.asEthSpecifier(): NetworkSpecifier? =
+            if (null == this) null else CompatUtil.makeEthernetNetworkSpecifier(this)
+    private fun makeTestNetworkRequest(specifier: NetworkSpecifier? = null) =
+            NetworkRequest.Builder().run {
+                clearCapabilities()
+                addTransportType(TRANSPORT_TEST)
+                if (specifier != null) setNetworkSpecifier(specifier)
+                build()
+            }
+
+    private fun makeTestNetworkRequest(specifier: String?) =
+            makeTestNetworkRequest(specifier.asEthSpecifier())
 
     private fun makeTestNetworkCapabilities(
         specifier: String? = null,
@@ -322,7 +348,7 @@
     ): Pair<TestableNetworkAgent, TestableNetworkCallback> {
         val callback = TestableNetworkCallback()
         // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
-        requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
+        requestNetwork(makeTestNetworkRequest(specifier), callback)
         val nc = makeTestNetworkCapabilities(specifier, transports)
         val agent = createNetworkAgent(context, initialConfig = initialConfig, initialNc = nc)
         agent.setTeardownDelayMillis(0)
@@ -394,8 +420,8 @@
                 .setLegacyExtraInfo(legacyExtraInfo).build()
         val (agent, callback) = createConnectedNetworkAgent(initialConfig = config)
         val networkInfo = mCM.getNetworkInfo(agent.network)
-        assertEquals(subtypeLTE, networkInfo.getSubtype())
-        assertEquals(subtypeNameLTE, networkInfo.getSubtypeName())
+        assertEquals(subtypeLTE, networkInfo?.getSubtype())
+        assertEquals(subtypeNameLTE, networkInfo?.getSubtypeName())
         assertEquals(legacyExtraInfo, config.getLegacyExtraInfo())
     }
 
@@ -417,8 +443,8 @@
             val nc = NetworkCapabilities(agent.nc)
             nc.addCapability(NET_CAPABILITY_NOT_METERED)
             agent.sendNetworkCapabilities(nc)
-            callback.expectCaps(agent.network) { it.hasCapability(NET_CAPABILITY_NOT_METERED) }
-            val networkInfo = mCM.getNetworkInfo(agent.network)
+            callback.expectCaps(agent.network!!) { it.hasCapability(NET_CAPABILITY_NOT_METERED) }
+            val networkInfo = mCM.getNetworkInfo(agent.network!!)!!
             assertEquals(subtypeUMTS, networkInfo.getSubtype())
             assertEquals(subtypeNameUMTS, networkInfo.getSubtypeName())
     }
@@ -543,6 +569,275 @@
                 .addTransportType(TRANSPORT_TEST)
                 .setAllowedUids(uids.toSet()).build()
 
+    /**
+     * Get the single element from this ArraySet, or fail() if doesn't contain exactly 1 element.
+     */
+    fun <T> ArraySet<T>.getSingleElement(): T {
+        if (size != 1) fail("Expected exactly one element, contained $size")
+        return iterator().next()
+    }
+
+    private fun doTestAllowedUids(
+            transports: IntArray,
+            uid: Int,
+            expectUidsPresent: Boolean,
+            specifier: NetworkSpecifier?,
+            transportInfo: TransportInfo?
+    ) {
+        val callback = TestableNetworkCallback(DEFAULT_TIMEOUT_MS)
+        val agent = createNetworkAgent(initialNc = NetworkCapabilities.Builder().run {
+            addTransportType(TRANSPORT_TEST)
+            transports.forEach { addTransportType(it) }
+            addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+            addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+            removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+            setNetworkSpecifier(specifier)
+            setTransportInfo(transportInfo)
+            setAllowedUids(setOf(uid))
+            setOwnerUid(Process.myUid())
+            setAdministratorUids(intArrayOf(Process.myUid()))
+            build()
+        })
+        runWithShellPermissionIdentity {
+            agent.register()
+        }
+        agent.markConnected()
+
+        registerNetworkCallback(makeTestNetworkRequest(specifier), callback)
+        callback.expect<Available>(agent.network!!)
+        callback.expect<CapabilitiesChanged>(agent.network!!) {
+            if (expectUidsPresent) {
+                it.caps.allowedUidsNoCopy.getSingleElement() == uid
+            } else {
+                it.caps.allowedUidsNoCopy.isEmpty()
+            }
+        }
+        agent.unregister()
+        callback.eventuallyExpect<Lost> { it.network == agent.network }
+        // callback will be unregistered in tearDown()
+    }
+
+    private fun doTestAllowedUids(
+            transport: Int,
+            uid: Int,
+            expectUidsPresent: Boolean
+    ) {
+        doTestAllowedUids(intArrayOf(transport), uid, expectUidsPresent,
+                specifier = null, transportInfo = null)
+    }
+
+    private fun doTestAllowedUidsWithSubId(
+            subId: Int,
+            transport: Int,
+            uid: Int,
+            expectUidsPresent: Boolean
+    ) {
+        doTestAllowedUidsWithSubId(subId, intArrayOf(transport), uid, expectUidsPresent)
+    }
+
+    private fun doTestAllowedUidsWithSubId(
+            subId: Int,
+            transports: IntArray,
+            uid: Int,
+            expectUidsPresent: Boolean
+    ) {
+        val specifier = when {
+            transports.size != 1 -> null
+            TRANSPORT_ETHERNET in transports -> EthernetNetworkSpecifier("testInterface")
+            TRANSPORT_CELLULAR in transports -> TelephonyNetworkSpecifier(subId)
+            else -> null
+        }
+        val transportInfo = if (TRANSPORT_WIFI in transports && SdkLevel.isAtLeastV()) {
+            // setSubscriptionId only exists in V+
+            WifiInfo.Builder().setSubscriptionId(subId).build()
+        } else {
+            null
+        }
+        doTestAllowedUids(transports, uid, expectUidsPresent, specifier, transportInfo)
+    }
+
+    private fun setHoldCarrierPrivilege(hold: Boolean, subId: Int) {
+        fun getCertHash(): String {
+            val pkgInfo = realContext.packageManager.getPackageInfo(realContext.opPackageName,
+                    PackageManager.GET_SIGNATURES)
+            val digest = MessageDigest.getInstance("SHA-256")
+            val certHash = digest.digest(pkgInfo.signatures!![0]!!.toByteArray())
+            return UiccUtil.bytesToHexString(certHash)!!
+        }
+
+        val tm = realContext.getSystemService(TelephonyManager::class.java)!!
+        val ccm = realContext.getSystemService(CarrierConfigManager::class.java)!!
+
+        val cv = ConditionVariable()
+        val cpb = PrivilegeWaiterCallback(cv)
+        tryTest {
+            val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+                tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
+            }
+            // Wait for the callback to be registered
+            assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't register CarrierPrivilegesCallback")
+            if (cpb.hasPrivilege == hold) {
+                if (hold) {
+                    Log.w(TAG, "Package ${realContext.opPackageName} already is privileged")
+                } else {
+                    Log.w(TAG, "Package ${realContext.opPackageName} already isn't privileged")
+                }
+                return@tryTest
+            }
+            cv.close()
+            runAsShell(MODIFY_PHONE_STATE) {
+                val carrierConfigs = if (hold) {
+                    PersistableBundle().also {
+                        it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
+                                arrayOf(getCertHash()))
+                    }
+                } else {
+                    null
+                }
+                ccm.overrideConfig(subId, carrierConfigs)
+            }
+            assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't change carrier privilege")
+        } cleanup {
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+                tm.unregisterCarrierPrivilegesCallback(cpb)
+            }
+        }
+    }
+
+    private fun acquireCarrierPrivilege(subId: Int) = setHoldCarrierPrivilege(true, subId)
+    private fun dropCarrierPrivilege(subId: Int) = setHoldCarrierPrivilege(false, subId)
+
+    private fun setCarrierServicePackageOverride(subId: Int, pkg: String?) {
+        val tm = realContext.getSystemService(TelephonyManager::class.java)!!
+
+        val cv = ConditionVariable()
+        val cpb = CarrierServiceChangedWaiterCallback(cv)
+        tryTest {
+            val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+                tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
+            }
+            // Wait for the callback to be registered
+            assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't register CarrierPrivilegesCallback")
+            if (cpb.pkgName == pkg) {
+                Log.w(TAG, "Carrier service package was already $pkg")
+                return@tryTest
+            }
+            cv.close()
+            runAsShell(MODIFY_PHONE_STATE) {
+                if (null == pkg) {
+                    // There is a bug is clear-carrier-service-package-override where not adding
+                    // the -s argument will use the wrong slot index : b/299604822
+                    runShellCommand("cmd phone clear-carrier-service-package-override" +
+                            " -s $subId")
+                } else {
+                    // -s could set the subId, but this test works with the default subId.
+                    runShellCommand("cmd phone set-carrier-service-package-override $pkg")
+                }
+            }
+            assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't modify carrier service package")
+        } cleanup {
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+                tm.unregisterCarrierPrivilegesCallback(cpb)
+            }
+        }
+    }
+
+    private fun String.execute() = runShellCommand(this).trim()
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S)
+    fun testAllowedUids() {
+        doTestAllowedUids(TRANSPORT_CELLULAR, Process.myUid(), expectUidsPresent = false)
+        doTestAllowedUids(TRANSPORT_WIFI, Process.myUid(), expectUidsPresent = false)
+        doTestAllowedUids(TRANSPORT_BLUETOOTH, Process.myUid(), expectUidsPresent = false)
+
+        // TODO(b/315136340): Allow ownerUid to see allowedUids and add cases that expect uids
+        // present
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S)
+    fun testAllowedUids_WithCarrierServicePackage() {
+        assumeTrue(realContext.packageManager.hasSystemFeature(FEATURE_TELEPHONY_SUBSCRIPTION))
+
+        // Use a different package than this one to make sure that a package that doesn't hold
+        // carrier service permission can be set as an allowed UID.
+        val servicePackage = "android.net.cts.carrierservicepackage"
+        val uid = try {
+            realContext.packageManager.getApplicationInfo(servicePackage, 0).uid
+        } catch (e: PackageManager.NameNotFoundException) {
+            fail("$servicePackage could not be installed, please check the SuiteApkInstaller" +
+                    " installed CtsCarrierServicePackage.apk", e)
+        }
+
+        val tm = realContext.getSystemService(TelephonyManager::class.java)!!
+        val defaultSubId = SubscriptionManager.getDefaultSubscriptionId()
+        assertTrue(defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+                "getDefaultSubscriptionId returns INVALID_SUBSCRIPTION_ID")
+        tryTest {
+            // This process is not the carrier service UID, so allowedUids should be ignored in all
+            // the following cases.
+            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_CELLULAR, uid,
+                    expectUidsPresent = false)
+            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_WIFI, uid,
+                    expectUidsPresent = false)
+            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_BLUETOOTH, uid,
+                    expectUidsPresent = false)
+
+            // The tools to set the carrier service package override do not exist before U,
+            // so there is no way to test the rest of this test on < U.
+            if (!SdkLevel.isAtLeastU()) return@tryTest
+            // Acquiring carrier privilege is necessary to override the carrier service package.
+            val defaultSlotIndex = SubscriptionManager.getSlotIndex(defaultSubId)
+            acquireCarrierPrivilege(defaultSubId)
+            setCarrierServicePackageOverride(defaultSubId, servicePackage)
+            val actualServicePackage: String? = runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+                tm.getCarrierServicePackageNameForLogicalSlot(defaultSlotIndex)
+            }
+            assertEquals(servicePackage, actualServicePackage)
+
+            // Wait for CarrierServiceAuthenticator to have seen the update of the service package
+            val timeout = SystemClock.elapsedRealtime() + DEFAULT_TIMEOUT_MS
+            while (true) {
+                if (SystemClock.elapsedRealtime() > timeout) {
+                    fail("Couldn't make $servicePackage the service package for $defaultSubId: " +
+                            "dumpsys connectivity".execute().split("\n")
+                                    .filter { it.contains("Logical slot = $defaultSlotIndex.*") })
+                }
+                if ("dumpsys connectivity"
+                        .execute()
+                        .split("\n")
+                        .filter { it.contains("Logical slot = $defaultSlotIndex : uid = $uid") }
+                        .isNotEmpty()) {
+                    // Found the configuration
+                    break
+                }
+                Thread.sleep(500)
+            }
+
+            // Cell and WiFi are allowed to set UIDs, but not Bluetooth or agents with multiple
+            // transports.
+            // TODO(b/315136340): Allow ownerUid to see allowedUids and enable below test case
+            // doTestAllowedUids(defaultSubId, TRANSPORT_CELLULAR, uid, expectUidsPresent = true)
+            if (SdkLevel.isAtLeastV()) {
+                // Cannot be tested before V because WifiInfo.Builder#setSubscriptionId doesn't
+                // exist
+                // TODO(b/315136340): Allow ownerUid to see allowedUids and enable below test case
+                // doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = true)
+            }
+            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_BLUETOOTH, uid,
+                    expectUidsPresent = false)
+            doTestAllowedUidsWithSubId(defaultSubId, intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI),
+                    uid, expectUidsPresent = false)
+        } cleanupStep {
+            if (SdkLevel.isAtLeastU()) setCarrierServicePackageOverride(defaultSubId, null)
+        } cleanup {
+            if (SdkLevel.isAtLeastU()) dropCarrierPrivilege(defaultSubId)
+        }
+    }
+
     @Test
     fun testRejectedUpdates() {
         val callback = TestableNetworkCallback(DEFAULT_TIMEOUT_MS)
@@ -631,6 +926,7 @@
         val defaultNetwork = mCM.activeNetwork
         assertNotNull(defaultNetwork)
         val defaultNetworkCapabilities = mCM.getNetworkCapabilities(defaultNetwork)
+        assertNotNull(defaultNetworkCapabilities)
         val defaultNetworkTransports = defaultNetworkCapabilities.transportTypes
 
         val agent = createNetworkAgent(initialNc = nc)
@@ -671,7 +967,7 @@
         // This is not very accurate because the test does not control the capabilities of the
         // underlying networks, and because not congested, not roaming, and not suspended are the
         // default anyway. It's still useful as an extra check though.
-        vpnNc = mCM.getNetworkCapabilities(agent.network!!)
+        vpnNc = mCM.getNetworkCapabilities(agent.network!!)!!
         for (cap in listOf(NET_CAPABILITY_NOT_CONGESTED,
                 NET_CAPABILITY_NOT_ROAMING,
                 NET_CAPABILITY_NOT_SUSPENDED)) {
@@ -1041,8 +1337,8 @@
     }
 
     fun QosSocketInfo(agent: NetworkAgent, socket: Closeable) = when (socket) {
-        is Socket -> QosSocketInfo(agent.network, socket)
-        is DatagramSocket -> QosSocketInfo(agent.network, socket)
+        is Socket -> QosSocketInfo(checkNotNull(agent.network), socket)
+        is DatagramSocket -> QosSocketInfo(checkNotNull(agent.network), socket)
         else -> fail("unexpected socket type")
     }
 
@@ -1405,8 +1701,8 @@
         val nc = makeTestNetworkCapabilities(ifName, transports).also {
             if (transports.contains(TRANSPORT_VPN)) {
                 val sessionId = "NetworkAgentTest-${Process.myPid()}"
-                it.transportInfo = VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, sessionId,
-                    /*bypassable=*/ false, /*longLivedTcpConnectionsExpensive=*/ false)
+                it.setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, sessionId,
+                    /*bypassable=*/ false, /*longLivedTcpConnectionsExpensive=*/ false))
                 it.underlyingNetworks = listOf()
             }
         }
@@ -1495,3 +1791,25 @@
         doTestNativeNetworkCreation(expectCreatedImmediately = true, intArrayOf(TRANSPORT_VPN))
     }
 }
+
+// Subclasses of CarrierPrivilegesCallback can't be inline, or they'll be compiled as
+// inner classes of the test class and will fail resolution on R as the test harness
+// uses reflection to list all methods and classes
+class PrivilegeWaiterCallback(private val cv: ConditionVariable) :
+        CarrierPrivilegesCallback {
+    var hasPrivilege = false
+    override fun onCarrierPrivilegesChanged(p: MutableSet<String>, uids: MutableSet<Int>) {
+        hasPrivilege = uids.contains(Process.myUid())
+        cv.open()
+    }
+}
+
+class CarrierServiceChangedWaiterCallback(private val cv: ConditionVariable) :
+        CarrierPrivilegesCallback {
+    var pkgName: String? = null
+    override fun onCarrierPrivilegesChanged(p: MutableSet<String>, u: MutableSet<Int>) {}
+    override fun onCarrierServiceChanged(pkgName: String?, uid: Int) {
+        this.pkgName = pkgName
+        cv.open()
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
index d6120f8..499d97f 100644
--- a/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
@@ -16,12 +16,12 @@
 
 package android.net.cts
 
-import android.os.Build
 import android.content.Context
 import android.net.ConnectivityManager
 import android.net.NetworkInfo
 import android.net.NetworkInfo.DetailedState
 import android.net.NetworkInfo.State
+import android.os.Build
 import android.telephony.TelephonyManager
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -29,16 +29,17 @@
 import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.NonNullTestUtils
+import kotlin.reflect.jvm.isAccessible
+import kotlin.test.assertFails
+import kotlin.test.assertFailsWith
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Rule
-import org.junit.runner.RunWith
 import org.junit.Test
-import kotlin.reflect.jvm.isAccessible
-import kotlin.test.assertFails
-import kotlin.test.assertFailsWith
+import org.junit.runner.RunWith
 
 const val TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE
 const val TYPE_WIFI = ConnectivityManager.TYPE_WIFI
@@ -106,10 +107,12 @@
         }
 
         if (SdkLevel.isAtLeastT()) {
-            assertFailsWith<NullPointerException> { NetworkInfo(null) }
+            assertFailsWith<NullPointerException> {
+                NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
+            }
         } else {
             // Doesn't immediately crash on S-
-            NetworkInfo(null)
+            NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
         }
     }
 
@@ -134,10 +137,11 @@
         val incorrectDetailedState = constructor.newInstance("any", 200) as DetailedState
         if (SdkLevel.isAtLeastT()) {
             assertFailsWith<NullPointerException> {
-                NetworkInfo(null)
+                NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
             }
             assertFailsWith<NullPointerException> {
-                networkInfo.setDetailedState(null, "reason", "extraInfo")
+                networkInfo.setDetailedState(NonNullTestUtils.nullUnsafe<DetailedState>(null),
+                        "reason", "extraInfo")
             }
             // This actually throws ArrayOutOfBoundsException because of the implementation of
             // EnumMap, but that's an implementation detail so accept any crash.
@@ -146,8 +150,9 @@
             }
         } else {
             // Doesn't immediately crash on S-
-            NetworkInfo(null)
-            networkInfo.setDetailedState(null, "reason", "extraInfo")
+            NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
+            networkInfo.setDetailedState(NonNullTestUtils.nullUnsafe<DetailedState>(null),
+                    "reason", "extraInfo")
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 637ed26..ff10e1a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -20,14 +20,20 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 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.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.fail;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -58,7 +64,9 @@
 import com.android.networkstack.apishim.NetworkRequestShimImpl;
 import com.android.networkstack.apishim.common.NetworkRequestShim;
 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;
@@ -68,6 +76,7 @@
 import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
+@ConnectivityModuleTest
 public class NetworkRequestTest {
     @Rule
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
@@ -104,6 +113,23 @@
         verifyNoCapabilities(nr);
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testForbiddenCapabilities() {
+        final NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        builder.addForbiddenCapability(NET_CAPABILITY_MMS);
+        assertTrue(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+        builder.removeForbiddenCapability(NET_CAPABILITY_MMS);
+        assertFalse(builder.build().hasCapability(NET_CAPABILITY_MMS));
+        builder.addCapability(NET_CAPABILITY_MMS);
+        assertFalse(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+        assertTrue(builder.build().hasCapability(NET_CAPABILITY_MMS));
+        builder.addForbiddenCapability(NET_CAPABILITY_MMS);
+        assertTrue(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+        assertFalse(builder.build().hasCapability(NET_CAPABILITY_MMS));
+        builder.clearCapabilities();
+        verifyNoCapabilities(builder.build());
+    }
+
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testTemporarilyNotMeteredCapability() {
         assertTrue(new NetworkRequest.Builder()
@@ -152,6 +178,20 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S)
+    public void testSubscriptionIds() {
+        int[] subIds = {1, 2};
+        assertTrue(
+                new NetworkRequest.Builder().build()
+                        .getSubscriptionIds().isEmpty());
+        assertThat(new NetworkRequest.Builder()
+                .setSubscriptionIds(Set.of(subIds[0], subIds[1]))
+                .build()
+                .getSubscriptionIds())
+                .containsExactly(subIds[0], subIds[1]);
+    }
+
+    @Test
     @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testRequestorPackageName() {
         assertNull(new NetworkRequest.Builder().build().getRequestorPackageName());
@@ -472,6 +512,22 @@
         assertArrayEquals(netCapabilities, nr.getCapabilities());
     }
 
+    // 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();
+
+        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));
+
+        assertEquals(0, defaultNR.getForbiddenCapabilities().length);
+    }
+
     @Test
     public void testBuildRequestFromExistingRequestWithBuilder() {
         assumeTrue(TestUtils.shouldTestSApis());
diff --git a/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt b/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
index fcfecad..e660b1e 100644
--- a/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
@@ -30,6 +30,7 @@
 import android.os.Build
 import android.os.Handler
 import android.os.HandlerThread
+import android.util.Log
 import androidx.test.InstrumentationRegistry
 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
@@ -41,6 +42,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import java.util.Collections
 
 // This test doesn't really have a constraint on how fast the methods should return. If it's
 // going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
@@ -64,10 +66,11 @@
 @IgnoreUpTo(Build.VERSION_CODES.R)
 @RunWith(DevSdkIgnoreRunner::class)
 class NetworkScoreTest {
-    private val mCm = testContext.getSystemService(ConnectivityManager::class.java)
-    private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread")
+    private val TAG = javaClass.simpleName
+    private val mCm = testContext.getSystemService(ConnectivityManager::class.java)!!
+    private val mHandlerThread = HandlerThread("$TAG handler thread")
     private val mHandler by lazy { Handler(mHandlerThread.looper) }
-    private val agentsToCleanUp = mutableListOf<NetworkAgent>()
+    private val agentsToCleanUp = Collections.synchronizedList(mutableListOf<NetworkAgent>())
     private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
 
     @Before
@@ -83,15 +86,18 @@
                     .addTransportType(NetworkCapabilities.TRANSPORT_TEST).build(), cb, mHandler
             )
         }
+        Log.i(TAG, "Teardown on thread ${System.identityHashCode(Thread.currentThread())} " +
+                "cleaning up ${agentsToCleanUp.size} agents")
         agentsToCleanUp.forEach {
+            Log.i(TAG, "Unregister agent for net ${it.network}")
             it.unregister()
             agentCleanUpCb.eventuallyExpect<CallbackEntry.Lost> { cb -> cb.network == it.network }
         }
         mCm.unregisterNetworkCallback(agentCleanUpCb)
 
+        callbacksToCleanUp.forEach { mCm.unregisterNetworkCallback(it) }
         mHandlerThread.quitSafely()
         mHandlerThread.join()
-        callbacksToCleanUp.forEach { mCm.unregisterNetworkCallback(it) }
     }
 
     // Returns a networkCallback that sends onAvailable on the best network with TRANSPORT_TEST.
@@ -105,7 +111,7 @@
     // made for ConnectivityServiceTest.
     // TODO : have TestNetworkCallback work for NetworkAgent too and remove this class.
     private class AgentWrapper(val agent: NetworkAgent) : HasNetwork {
-        override val network = agent.network
+        override val network = checkNotNull(agent.network)
         fun sendNetworkScore(s: NetworkScore) = agent.sendNetworkScore(s)
     }
 
@@ -145,6 +151,8 @@
         val agent = object : NetworkAgent(context, looper, "NetworkScore test agent", nc,
                 LinkProperties(), score, config, NetworkProvider(context, looper,
                 "NetworkScore test provider")) {}.also {
+            Log.i(TAG, "Add on thread ${System.identityHashCode(Thread.currentThread())} " +
+                    "agent to clean up $it")
             agentsToCleanUp.add(it)
         }
         runWithShellPermissionIdentity({ agent.register() }, MANAGE_TEST_NETWORKS)
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index f86c5cd..bc1a154 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -62,6 +62,7 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.platform.test.annotations.AppModeFull;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -82,9 +83,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.UnknownHostException;
@@ -103,8 +104,8 @@
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(Build.VERSION_CODES.Q);
 
     private static final String LOG_TAG = "NetworkStatsManagerTest";
-    private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} {1} {2}";
-    private static final String APPOPS_GET_SHELL_COMMAND = "appops get {0} {1}";
+    private static final String APPOPS_SET_SHELL_COMMAND = "appops set --user {0} {1} {2} {3}";
+    private static final String APPOPS_GET_SHELL_COMMAND = "appops get --user {0} {1} {2}";
 
     private static final long MINUTE = 1000 * 60;
     private static final int TIMEOUT_MILLIS = 15000;
@@ -210,7 +211,6 @@
     private long mStartTime;
     private long mEndTime;
 
-    private long mBytesRead;
     private String mWriteSettingsMode;
     private String mUsageStatsMode;
 
@@ -221,7 +221,7 @@
         } else {
             Log.w(LOG_TAG, "Network: " + networkInfo.toString());
         }
-        InputStreamReader in = null;
+        BufferedInputStream in = null;
         HttpURLConnection urlc = null;
         String originalKeepAlive = System.getProperty("http.keepAlive");
         System.setProperty("http.keepAlive", "false");
@@ -229,6 +229,7 @@
             TrafficStats.setThreadStatsTag(NETWORK_TAG);
             urlc = (HttpURLConnection) network.openConnection(url);
             urlc.setConnectTimeout(TIMEOUT_MILLIS);
+            urlc.setReadTimeout(TIMEOUT_MILLIS);
             urlc.setUseCaches(false);
             // Disable compression so we generate enough traffic that assertWithinPercentage will
             // not be affected by the small amount of traffic (5-10kB) sent by the test harness.
@@ -236,11 +237,10 @@
             urlc.connect();
             boolean ping = urlc.getResponseCode() == 200;
             if (ping) {
-                in = new InputStreamReader(
-                        (InputStream) urlc.getContent());
-
-                mBytesRead = 0;
-                while (in.read() != -1) ++mBytesRead;
+                in = new BufferedInputStream((InputStream) urlc.getContent());
+                while (in.read() != -1) {
+                    // Comments to suppress lint error.
+                }
             }
         } catch (Exception e) {
             Log.i(LOG_TAG, "Badness during exercising remote server: " + e);
@@ -291,12 +291,14 @@
     }
 
     private void setAppOpsMode(String appop, String mode) throws Exception {
-        final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND, mPkg, appop, mode);
+        final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND,
+                UserHandle.myUserId(), mPkg, appop, mode);
         SystemUtil.runShellCommand(mInstrumentation, command);
     }
 
     private String getAppOpsMode(String appop) throws Exception {
-        final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND, mPkg, appop);
+        final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND,
+                UserHandle.myUserId(), mPkg, appop);
         String result = SystemUtil.runShellCommand(mInstrumentation, command);
         if (result == null) {
             Log.w(LOG_TAG, "App op " + appop + " could not be read.");
@@ -378,11 +380,17 @@
                 .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                 .build(), callback);
         synchronized (this) {
-            try {
-                wait((int) (TIMEOUT_MILLIS * 1.2));
-            } catch (InterruptedException e) {
+            long now = System.currentTimeMillis();
+            final long deadline = (long) (now + TIMEOUT_MILLIS * 2.4);
+            while (!callback.success && now < deadline) {
+                try {
+                    wait(deadline - now);
+                } catch (InterruptedException e) {
+                }
+                now = System.currentTimeMillis();
             }
         }
+        mCm.unregisterNetworkCallback(callback);
         if (callback.success) {
             mNetworkInterfacesToTest[networkTypeIndex].setMetered(callback.metered);
             mNetworkInterfacesToTest[networkTypeIndex].setRoaming(callback.roaming);
@@ -394,7 +402,7 @@
         assertFalse(mNetworkInterfacesToTest[networkTypeIndex].getSystemFeature()
                 + " is a reported system feature, "
                 + "however no corresponding connected network interface was found or the attempt "
-                + "to connect has timed out (timeout = " + TIMEOUT_MILLIS + "ms)."
+                + "to connect and read has timed out (timeout = " + (TIMEOUT_MILLIS * 2) + "ms)."
                 + mNetworkInterfacesToTest[networkTypeIndex].getErrorMessage(), hasFeature);
         return false;
     }
@@ -800,7 +808,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, 10);
+                    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/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
new file mode 100644
index 0000000..f45f881
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.cts
+
+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.nsd.NsdManager
+import android.os.Build
+import android.platform.test.annotations.AppModeFull
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.TapPacketReader
+import com.android.testutils.pollForQuery
+import com.android.testutils.tryTest
+import java.util.Random
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+@AppModeFull(reason = "WifiManager cannot be obtained in instant mode")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class NsdManagerDownstreamTetheringTest : EthernetTetheringTestBase() {
+    private val nsdManager by lazy { context.getSystemService(NsdManager::class.java)!! }
+    private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
+
+    @Before
+    override fun setUp() {
+        super.setUp()
+        setIncludeTestInterfaces(true)
+    }
+
+    @After
+    override fun tearDown() {
+        super.tearDown()
+    }
+
+    @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 = mTetheredInterfaceRequester.getInterface()
+            assertEquals(iface, downstreamIface?.interfaceName)
+            val request = TetheringRequest.Builder(TETHERING_ETHERNET)
+                .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
+            tetheringEventCallback = enableEthernetTethering(
+                iface, request,
+                null /* any upstream */
+            ).apply {
+                awaitInterfaceLocalOnly()
+            }
+            // This shouldn't be flaky because the TAP interface will buffer all packets even
+            // before the reader is started.
+            downstreamReader = makePacketReader(downstreamIface)
+            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 */))
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
+        } cleanupStep {
+            maybeStopTapPacketReader(downstreamReader)
+        } cleanupStep {
+            maybeCloseTestInterface(downstreamIface)
+        } cleanup {
+            maybeUnregisterTetheringEventCallback(tetheringEventCallback)
+        }
+    }
+
+    @Test
+    fun testMdnsDiscoveryWorkOnTetheringInterface() {
+        assumeFalse(isInterfaceForTetheringAvailable())
+
+        var downstreamIface: TestNetworkInterface? = null
+        var tetheringEventCallback: MyTetheringEventCallback? = null
+        var downstreamReader: TapPacketReader? = null
+
+        val discoveryRecord = NsdDiscoveryRecord()
+
+        tryTest {
+            downstreamIface = createTestInterface()
+            val iface = mTetheredInterfaceRequester.getInterface()
+            assertEquals(iface, downstreamIface?.interfaceName)
+
+            val localAddr = LinkAddress("192.0.2.3/28")
+            val clientAddr = LinkAddress("192.0.2.2/28")
+            val request = TetheringRequest.Builder(TETHERING_ETHERNET)
+                .setStaticIpv4Addresses(localAddr, clientAddr)
+                .setShouldShowEntitlementUi(false).build()
+            tetheringEventCallback = enableEthernetTethering(
+                iface, request,
+                null /* any upstream */
+            ).apply {
+                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 */))
+            // 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 9808137..be80787 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -16,14 +16,17 @@
 package android.net.cts
 
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.NETWORK_SETTINGS
 import android.app.compat.CompatChanges
 import android.net.ConnectivityManager
 import android.net.ConnectivityManager.NetworkCallback
+import android.net.DnsResolver
 import android.net.InetAddresses.parseNumericAddress
 import android.net.LinkAddress
 import android.net.LinkProperties
 import android.net.LocalSocket
 import android.net.LocalSocketAddress
+import android.net.MacAddress
 import android.net.Network
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
@@ -36,63 +39,81 @@
 import android.net.TestNetworkManager
 import android.net.TestNetworkSpecifier
 import android.net.connectivity.ConnectivityCompatChanges
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.StartDiscoveryFailed
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.StopDiscoveryFailed
-import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
-import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
-import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
-import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.UnregistrationFailed
-import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolutionStopped
-import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolveFailed
-import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ServiceResolved
-import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.StopResolutionFailed
-import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.RegisterCallbackFailed
-import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
-import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
-import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
 import android.net.cts.util.CtsNetUtils
+import android.net.nsd.DiscoveryRequest
 import android.net.nsd.NsdManager
-import android.net.nsd.NsdManager.DiscoveryListener
-import android.net.nsd.NsdManager.RegistrationListener
-import android.net.nsd.NsdManager.ResolveListener
 import android.net.nsd.NsdServiceInfo
+import android.net.nsd.OffloadEngine
+import android.net.nsd.OffloadServiceInfo
 import android.os.Build
 import android.os.Handler
 import android.os.HandlerThread
-import android.os.Process.myTid
 import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig.NAMESPACE_TETHERING
 import android.system.ErrnoException
 import android.system.Os
 import android.system.OsConstants.AF_INET6
 import android.system.OsConstants.EADDRNOTAVAIL
 import android.system.OsConstants.ENETUNREACH
+import android.system.OsConstants.ETH_P_IPV6
+import android.system.OsConstants.IPPROTO_IPV6
 import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.RT_SCOPE_LINK
 import android.system.OsConstants.SOCK_DGRAM
 import android.util.Log
 import androidx.test.filters.SmallTest
 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.ArrayTrackRecord
-import com.android.net.module.util.TrackRecord
-import com.android.networkstack.apishim.NsdShimImpl
-import com.android.networkstack.apishim.common.NsdShim
+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
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 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.assertContainsExactly
+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
@@ -102,6 +123,7 @@
 import java.net.InetAddress
 import java.net.NetworkInterface
 import java.net.ServerSocket
+import java.nio.ByteBuffer
 import java.nio.charset.StandardCharsets
 import java.util.Random
 import java.util.concurrent.Executor
@@ -110,7 +132,6 @@
 import kotlin.test.assertFailsWith
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
-import kotlin.test.assertTrue
 import kotlin.test.fail
 import org.junit.After
 import org.junit.Assert.assertArrayEquals
@@ -121,10 +142,10 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import kotlin.test.assertNotEquals
 
 private const val TAG = "NsdManagerTest"
 private const val TIMEOUT_MS = 2000L
-private const val NO_CALLBACK_TIMEOUT_MS = 200L
 // 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
@@ -132,8 +153,12 @@
 private const val REGISTRATION_TIMEOUT_MS = 10_000L
 private const val DBG = false
 private const val TEST_PORT = 12345
-
-private val nsdShim = NsdShimImpl.newInstance()
+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
 
 @AppModeFull(reason = "Socket cannot bind in instant app mode")
 @RunWith(DevSdkIgnoreRunner::class)
@@ -145,12 +170,28 @@
     @get:Rule
     val ignoreRule = DevSdkIgnoreRule()
 
-    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
-    private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
+    @get:Rule
+    val deviceConfigRule = DeviceConfigRule()
 
-    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+    private val nsdManager by lazy {
+        context.getSystemService(NsdManager::class.java) ?: fail("Could not get NsdManager service")
+    }
+
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
     private val serviceName = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
+    private val serviceName2 = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
+    private val serviceName3 = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
     private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
+    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) }
 
@@ -171,189 +212,19 @@
         }
     }
 
-    private interface NsdEvent
-    private open class NsdRecord<T : NsdEvent> private constructor(
-        private val history: ArrayTrackRecord<T>,
-        private val expectedThreadId: Int? = null
-    ) : TrackRecord<T> by history {
-        constructor(expectedThreadId: Int? = null) : this(ArrayTrackRecord(), expectedThreadId)
-
-        val nextEvents = history.newReadHead()
-
-        override fun add(e: T): Boolean {
-            if (expectedThreadId != null) {
-                assertEquals(expectedThreadId, myTid(), "Callback is running on the wrong thread")
-            }
-            return history.add(e)
+    private class TestNsdOffloadEngine : OffloadEngine,
+        NsdRecord<TestNsdOffloadEngine.OffloadEvent>() {
+        sealed class OffloadEvent : NsdEvent {
+            data class AddOrUpdateEvent(val info: OffloadServiceInfo) : OffloadEvent()
+            data class RemoveEvent(val info: OffloadServiceInfo) : OffloadEvent()
         }
 
-        inline fun <reified V : NsdEvent> expectCallbackEventually(
-            timeoutMs: Long = TIMEOUT_MS,
-            crossinline predicate: (V) -> Boolean = { true }
-        ): V = nextEvents.poll(timeoutMs) { e -> e is V && predicate(e) } as V?
-                ?: fail("Callback for ${V::class.java.simpleName} not seen after $timeoutMs ms")
-
-        inline fun <reified V : NsdEvent> expectCallback(timeoutMs: Long = TIMEOUT_MS): V {
-            val nextEvent = nextEvents.poll(timeoutMs)
-            assertNotNull(nextEvent, "No callback received after $timeoutMs ms, expected " +
-                    "${V::class.java.simpleName}")
-            assertTrue(nextEvent is V, "Expected ${V::class.java.simpleName} but got " +
-                    nextEvent.javaClass.simpleName)
-            return nextEvent
+        override fun onOffloadServiceUpdated(info: OffloadServiceInfo) {
+            add(OffloadEvent.AddOrUpdateEvent(info))
         }
 
-        inline fun assertNoCallback(timeoutMs: Long = NO_CALLBACK_TIMEOUT_MS) {
-            val cb = nextEvents.poll(timeoutMs)
-            assertNull(cb, "Expected no callback but got $cb")
-        }
-    }
-
-    private class NsdRegistrationRecord(expectedThreadId: Int? = null) : RegistrationListener,
-            NsdRecord<NsdRegistrationRecord.RegistrationEvent>(expectedThreadId) {
-        sealed class RegistrationEvent : NsdEvent {
-            abstract val serviceInfo: NsdServiceInfo
-
-            data class RegistrationFailed(
-                override val serviceInfo: NsdServiceInfo,
-                val errorCode: Int
-            ) : RegistrationEvent()
-
-            data class UnregistrationFailed(
-                override val serviceInfo: NsdServiceInfo,
-                val errorCode: Int
-            ) : RegistrationEvent()
-
-            data class ServiceRegistered(override val serviceInfo: NsdServiceInfo) :
-                    RegistrationEvent()
-            data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo) :
-                    RegistrationEvent()
-        }
-
-        override fun onRegistrationFailed(si: NsdServiceInfo, err: Int) {
-            add(RegistrationFailed(si, err))
-        }
-
-        override fun onUnregistrationFailed(si: NsdServiceInfo, err: Int) {
-            add(UnregistrationFailed(si, err))
-        }
-
-        override fun onServiceRegistered(si: NsdServiceInfo) {
-            add(ServiceRegistered(si))
-        }
-
-        override fun onServiceUnregistered(si: NsdServiceInfo) {
-            add(ServiceUnregistered(si))
-        }
-    }
-
-    private class NsdDiscoveryRecord(expectedThreadId: Int? = null) :
-            DiscoveryListener, NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>(expectedThreadId) {
-        sealed class DiscoveryEvent : NsdEvent {
-            data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int) :
-                    DiscoveryEvent()
-
-            data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int) :
-                    DiscoveryEvent()
-
-            data class DiscoveryStarted(val serviceType: String) : DiscoveryEvent()
-            data class DiscoveryStopped(val serviceType: String) : DiscoveryEvent()
-            data class ServiceFound(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
-            data class ServiceLost(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
-        }
-
-        override fun onStartDiscoveryFailed(serviceType: String, err: Int) {
-            add(StartDiscoveryFailed(serviceType, err))
-        }
-
-        override fun onStopDiscoveryFailed(serviceType: String, err: Int) {
-            add(StopDiscoveryFailed(serviceType, err))
-        }
-
-        override fun onDiscoveryStarted(serviceType: String) {
-            add(DiscoveryStarted(serviceType))
-        }
-
-        override fun onDiscoveryStopped(serviceType: String) {
-            add(DiscoveryStopped(serviceType))
-        }
-
-        override fun onServiceFound(si: NsdServiceInfo) {
-            add(ServiceFound(si))
-        }
-
-        override fun onServiceLost(si: NsdServiceInfo) {
-            add(ServiceLost(si))
-        }
-
-        fun waitForServiceDiscovered(
-            serviceName: String,
-            serviceType: String,
-            expectedNetwork: Network? = null
-        ): NsdServiceInfo {
-            val serviceFound = expectCallbackEventually<ServiceFound> {
-                it.serviceInfo.serviceName == serviceName &&
-                        (expectedNetwork == null ||
-                                expectedNetwork == nsdShim.getNetwork(it.serviceInfo))
-            }.serviceInfo
-            // Discovered service types have a dot at the end
-            assertEquals("$serviceType.", serviceFound.serviceType)
-            return serviceFound
-        }
-    }
-
-    private class NsdResolveRecord : ResolveListener,
-            NsdRecord<NsdResolveRecord.ResolveEvent>() {
-        sealed class ResolveEvent : NsdEvent {
-            data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
-                    ResolveEvent()
-
-            data class ServiceResolved(val serviceInfo: NsdServiceInfo) : ResolveEvent()
-            data class ResolutionStopped(val serviceInfo: NsdServiceInfo) : ResolveEvent()
-            data class StopResolutionFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
-                    ResolveEvent()
-        }
-
-        override fun onResolveFailed(si: NsdServiceInfo, err: Int) {
-            add(ResolveFailed(si, err))
-        }
-
-        override fun onServiceResolved(si: NsdServiceInfo) {
-            add(ServiceResolved(si))
-        }
-
-        override fun onResolutionStopped(si: NsdServiceInfo) {
-            add(ResolutionStopped(si))
-        }
-
-        override fun onStopResolutionFailed(si: NsdServiceInfo, err: Int) {
-            super.onStopResolutionFailed(si, err)
-            add(StopResolutionFailed(si, err))
-        }
-    }
-
-    private class NsdServiceInfoCallbackRecord : NsdShim.ServiceInfoCallbackShim,
-            NsdRecord<NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent>() {
-        sealed class ServiceInfoCallbackEvent : NsdEvent {
-            data class RegisterCallbackFailed(val errorCode: Int) : ServiceInfoCallbackEvent()
-            data class ServiceUpdated(val serviceInfo: NsdServiceInfo) : ServiceInfoCallbackEvent()
-            object ServiceUpdatedLost : ServiceInfoCallbackEvent()
-            object UnregisterCallbackSucceeded : ServiceInfoCallbackEvent()
-        }
-
-        override fun onServiceInfoCallbackRegistrationFailed(err: Int) {
-            add(RegisterCallbackFailed(err))
-        }
-
-        override fun onServiceUpdated(si: NsdServiceInfo) {
-            add(ServiceUpdated(si))
-        }
-
-        override fun onServiceLost() {
-            add(ServiceUpdatedLost)
-        }
-
-        override fun onServiceInfoCallbackUnregistered() {
-            add(UnregisterCallbackSucceeded)
+        override fun onOffloadServiceRemoved(info: OffloadServiceInfo) {
+            add(OffloadEvent.RemoveEvent(info))
         }
     }
 
@@ -361,16 +232,14 @@
     fun setUp() {
         handlerThread.start()
 
-        if (TestUtils.shouldTestTApis()) {
-            runAsShell(MANAGE_TEST_NETWORKS) {
-                testNetwork1 = createTestNetwork()
-                testNetwork2 = createTestNetwork()
-            }
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            testNetwork1 = createTestNetwork()
+            testNetwork2 = createTestNetwork()
         }
     }
 
     private fun createTestNetwork(): TestTapNetwork {
-        val tnm = context.getSystemService(TestNetworkManager::class.java)
+        val tnm = context.getSystemService(TestNetworkManager::class.java)!!
         val iface = tnm.createTapInterface()
         val cb = TestableNetworkCallback()
         val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
@@ -398,7 +267,6 @@
         val lp = LinkProperties().apply {
             interfaceName = ifaceName
         }
-
         val agent = TestableNetworkAgent(context, handlerThread.looper,
                 NetworkCapabilities().apply {
                     removeCapability(NET_CAPABILITY_TRUSTED)
@@ -450,12 +318,10 @@
 
     @After
     fun tearDown() {
-        if (TestUtils.shouldTestTApis()) {
-            runAsShell(MANAGE_TEST_NETWORKS) {
-                // Avoid throwing here if initializing failed in setUp
-                if (this::testNetwork1.isInitialized) testNetwork1.close(cm)
-                if (this::testNetwork2.isInitialized) testNetwork2.close(cm)
-            }
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            // Avoid throwing here if initializing failed in setUp
+            if (this::testNetwork1.isInitialized) testNetwork1.close(cm)
+            if (this::testNetwork2.isInitialized) testNetwork2.close(cm)
         }
         handlerThread.waitForIdle(TIMEOUT_MS)
         handlerThread.quitSafely()
@@ -548,8 +414,9 @@
         assertTrue(resolvedService.attributes.containsKey("nullBinaryDataAttr"))
         assertNull(resolvedService.attributes["nullBinaryDataAttr"])
         assertTrue(resolvedService.attributes.containsKey("emptyBinaryDataAttr"))
-        // TODO: change the check to target SDK U when this is what the code implements
-        if (isAtLeastU()) {
+        if (isAtLeastU() || CompatChanges.isChangeEnabled(
+                ConnectivityCompatChanges.ENABLE_PLATFORM_MDNS_BACKEND
+            )) {
             assertArrayEquals(byteArrayOf(), resolvedService.attributes["emptyBinaryDataAttr"])
         } else {
             assertNull(resolvedService.attributes["emptyBinaryDataAttr"])
@@ -600,32 +467,25 @@
 
     @Test
     fun testNsdManager_DiscoverOnNetwork() {
-        // This test requires shims supporting T+ APIs (discovering on specific network)
-        assumeTrue(TestUtils.shouldTestTApis())
-
-        val si = NsdServiceInfo()
-        si.serviceType = serviceType
-        si.serviceName = this.serviceName
-        si.port = 12345 // Test won't try to connect so port does not matter
-
+        val si = makeTestServiceInfo()
         val registrationRecord = NsdRegistrationRecord()
         val registeredInfo = registerService(registrationRecord, si)
 
         tryTest {
             val discoveryRecord = NsdDiscoveryRecord()
-            nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
                     testNetwork1.network, Executor { it.run() }, discoveryRecord)
 
             val foundInfo = discoveryRecord.waitForServiceDiscovered(
                     serviceName, serviceType, testNetwork1.network)
-            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
+            assertEquals(testNetwork1.network, foundInfo.network)
 
             // Rewind to ensure the service is not found on the other interface
             discoveryRecord.nextEvents.rewind(0)
             assertNull(discoveryRecord.nextEvents.poll(timeoutMs = 100L) {
                 it is ServiceFound &&
                         it.serviceInfo.serviceName == registeredInfo.serviceName &&
-                        nsdShim.getNetwork(it.serviceInfo) != testNetwork1.network
+                        it.serviceInfo.network != testNetwork1.network
             }, "The service should not be found on this network")
         } cleanup {
             nsdManager.unregisterService(registrationRecord)
@@ -634,14 +494,7 @@
 
     @Test
     fun testNsdManager_DiscoverWithNetworkRequest() {
-        // This test requires shims supporting T+ APIs (discovering on network request)
-        assumeTrue(TestUtils.shouldTestTApis())
-
-        val si = NsdServiceInfo()
-        si.serviceType = serviceType
-        si.serviceName = this.serviceName
-        si.port = 12345 // Test won't try to connect so port does not matter
-
+        val si = makeTestServiceInfo()
         val handler = Handler(handlerThread.looper)
         val executor = Executor { handler.post(it) }
 
@@ -651,7 +504,7 @@
 
         tryTest {
             val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName)
-            nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
                     NetworkRequest.Builder()
                             .removeCapability(NET_CAPABILITY_TRUSTED)
                             .addTransportType(TRANSPORT_TEST)
@@ -666,27 +519,27 @@
             assertEquals(registeredInfo1.serviceName, serviceDiscovered.serviceInfo.serviceName)
             // Discovered service types have a dot at the end
             assertEquals("$serviceType.", serviceDiscovered.serviceInfo.serviceType)
-            assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered.serviceInfo))
+            assertEquals(testNetwork1.network, serviceDiscovered.serviceInfo.network)
 
             // Unregister, then register the service back: it should be lost and found again
             nsdManager.unregisterService(registrationRecord)
             val serviceLost1 = discoveryRecord.expectCallback<ServiceLost>()
             assertEquals(registeredInfo1.serviceName, serviceLost1.serviceInfo.serviceName)
-            assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceLost1.serviceInfo))
+            assertEquals(testNetwork1.network, serviceLost1.serviceInfo.network)
 
             registrationRecord.expectCallback<ServiceUnregistered>()
             val registeredInfo2 = registerService(registrationRecord, si, executor)
             val serviceDiscovered2 = discoveryRecord.expectCallback<ServiceFound>()
             assertEquals(registeredInfo2.serviceName, serviceDiscovered2.serviceInfo.serviceName)
             assertEquals("$serviceType.", serviceDiscovered2.serviceInfo.serviceType)
-            assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered2.serviceInfo))
+            assertEquals(testNetwork1.network, serviceDiscovered2.serviceInfo.network)
 
             // Teardown, then bring back up a network on the test interface: the service should
             // go away, then come back
             testNetwork1.agent.unregister()
             val serviceLost = discoveryRecord.expectCallback<ServiceLost>()
             assertEquals(registeredInfo2.serviceName, serviceLost.serviceInfo.serviceName)
-            assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceLost.serviceInfo))
+            assertEquals(testNetwork1.network, serviceLost.serviceInfo.network)
 
             val newAgent = runAsShell(MANAGE_TEST_NETWORKS) {
                 registerTestNetworkAgent(testNetwork1.iface.interfaceName)
@@ -695,7 +548,7 @@
             val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>()
             assertEquals(registeredInfo2.serviceName, serviceDiscovered3.serviceInfo.serviceName)
             assertEquals("$serviceType.", serviceDiscovered3.serviceInfo.serviceType)
-            assertEquals(newNetwork, nsdShim.getNetwork(serviceDiscovered3.serviceInfo))
+            assertEquals(newNetwork, serviceDiscovered3.serviceInfo.network)
         } cleanupStep {
             nsdManager.stopServiceDiscovery(discoveryRecord)
             discoveryRecord.expectCallback<DiscoveryStopped>()
@@ -706,14 +559,6 @@
 
     @Test
     fun testNsdManager_DiscoverWithNetworkRequest_NoMatchingNetwork() {
-        // This test requires shims supporting T+ APIs (discovering on network request)
-        assumeTrue(TestUtils.shouldTestTApis())
-
-        val si = NsdServiceInfo()
-        si.serviceType = serviceType
-        si.serviceName = this.serviceName
-        si.port = 12345 // Test won't try to connect so port does not matter
-
         val handler = Handler(handlerThread.looper)
         val executor = Executor { handler.post(it) }
 
@@ -721,7 +566,7 @@
         val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName)
 
         tryTest {
-            nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
                     NetworkRequest.Builder()
                             .removeCapability(NET_CAPABILITY_TRUSTED)
                             .addTransportType(TRANSPORT_TEST)
@@ -753,14 +598,7 @@
 
     @Test
     fun testNsdManager_ResolveOnNetwork() {
-        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
-        assumeTrue(TestUtils.shouldTestTApis())
-
-        val si = NsdServiceInfo()
-        si.serviceType = serviceType
-        si.serviceName = this.serviceName
-        si.port = 12345 // Test won't try to connect so port does not matter
-
+        val si = makeTestServiceInfo()
         val registrationRecord = NsdRegistrationRecord()
         val registeredInfo = registerService(registrationRecord, si)
         tryTest {
@@ -771,21 +609,21 @@
 
             val foundInfo1 = discoveryRecord.waitForServiceDiscovered(
                     serviceName, serviceType, testNetwork1.network)
-            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo1))
+            assertEquals(testNetwork1.network, foundInfo1.network)
             // Rewind as the service could be found on each interface in any order
             discoveryRecord.nextEvents.rewind(0)
             val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
                     serviceName, serviceType, testNetwork2.network)
-            assertEquals(testNetwork2.network, nsdShim.getNetwork(foundInfo2))
+            assertEquals(testNetwork2.network, foundInfo2.network)
 
-            nsdShim.resolveService(nsdManager, foundInfo1, Executor { it.run() }, resolveRecord)
+            nsdManager.resolveService(foundInfo1, Executor { it.run() }, resolveRecord)
             val cb = resolveRecord.expectCallback<ServiceResolved>()
             cb.serviceInfo.let {
                 // Resolved service type has leading dot
                 assertEquals(".$serviceType", it.serviceType)
                 assertEquals(registeredInfo.serviceName, it.serviceName)
                 assertEquals(si.port, it.port)
-                assertEquals(testNetwork1.network, nsdShim.getNetwork(it))
+                assertEquals(testNetwork1.network, it.network)
                 checkAddressScopeId(testNetwork1.iface, it.hostAddresses)
             }
             // TODO: check that MDNS packets are sent only on testNetwork1.
@@ -798,15 +636,7 @@
 
     @Test
     fun testNsdManager_RegisterOnNetwork() {
-        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
-        assumeTrue(TestUtils.shouldTestTApis())
-
-        val si = NsdServiceInfo()
-        si.serviceType = serviceType
-        si.serviceName = this.serviceName
-        si.network = testNetwork1.network
-        si.port = 12345 // Test won't try to connect so port does not matter
-
+        val si = makeTestServiceInfo(testNetwork1.network)
         // Register service on testNetwork1
         val registrationRecord = NsdRegistrationRecord()
         registerService(registrationRecord, si)
@@ -816,27 +646,27 @@
 
         tryTest {
             // Discover service on testNetwork1.
-            nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
                 testNetwork1.network, Executor { it.run() }, discoveryRecord)
             // Expect that service is found on testNetwork1
             val foundInfo = discoveryRecord.waitForServiceDiscovered(
                 serviceName, serviceType, testNetwork1.network)
-            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
+            assertEquals(testNetwork1.network, foundInfo.network)
 
             // Discover service on testNetwork2.
-            nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
                 testNetwork2.network, Executor { it.run() }, discoveryRecord2)
             // Expect that discovery is started then no other callbacks.
             discoveryRecord2.expectCallback<DiscoveryStarted>()
             discoveryRecord2.assertNoCallback()
 
             // Discover service on all networks (not specify any network).
-            nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
                 null as Network? /* network */, Executor { it.run() }, discoveryRecord3)
             // Expect that service is found on testNetwork1
             val foundInfo3 = discoveryRecord3.waitForServiceDiscovered(
                     serviceName, serviceType, testNetwork1.network)
-            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo3))
+            assertEquals(testNetwork1.network, foundInfo3.network)
         } cleanupStep {
             nsdManager.stopServiceDiscovery(discoveryRecord2)
             discoveryRecord2.expectCallback<DiscoveryStopped>()
@@ -880,6 +710,160 @@
         }
     }
 
+    @Test
+    fun testRegisterService_twoServicesWithSameNameButDifferentTypes_registeredAndDiscoverable() {
+        val si1 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceName = serviceName
+            it.serviceType = serviceType
+            it.port = TEST_PORT
+        }
+        val si2 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceName = serviceName
+            it.serviceType = serviceType2
+            it.port = TEST_PORT + 1
+        }
+        val registrationRecord1 = NsdRegistrationRecord()
+        val registrationRecord2 = NsdRegistrationRecord()
+        val discoveryRecord1 = NsdDiscoveryRecord()
+        val discoveryRecord2 = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord1, si1)
+            registerService(registrationRecord2, si2)
+
+            nsdManager.discoverServices(serviceType,
+                    NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, discoveryRecord1)
+            nsdManager.discoverServices(serviceType2,
+                    NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, discoveryRecord2)
+
+            discoveryRecord1.waitForServiceDiscovered(serviceName, serviceType,
+                    testNetwork1.network)
+            discoveryRecord2.waitForServiceDiscovered(serviceName, serviceType2,
+                    testNetwork1.network)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord1)
+            nsdManager.stopServiceDiscovery(discoveryRecord2)
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord1)
+            nsdManager.unregisterService(registrationRecord2)
+        }
+    }
+
+    fun checkOffloadServiceInfo(serviceInfo: OffloadServiceInfo, si: NsdServiceInfo) {
+        val expectedServiceType = si.serviceType.split(",")[0]
+        assertEquals(si.serviceName, serviceInfo.key.serviceName)
+        assertEquals(expectedServiceType, serviceInfo.key.serviceType)
+        assertEquals(listOf("_subtype"), serviceInfo.subtypes)
+        assertTrue(serviceInfo.hostname.startsWith("Android_"))
+        assertTrue(serviceInfo.hostname.endsWith("local"))
+        // Test service types should not be in the priority list
+        assertEquals(Integer.MAX_VALUE, serviceInfo.priority)
+        assertEquals(OffloadEngine.OFFLOAD_TYPE_REPLY.toLong(), serviceInfo.offloadType)
+        val offloadPayload = serviceInfo.offloadPayload
+        assertNotNull(offloadPayload)
+        val dnsPacket = TestDnsPacket(offloadPayload, dstAddr = multicastIpv6Addr)
+        assertEquals(0x8400, dnsPacket.header.flags)
+        assertEquals(0, dnsPacket.records[DnsPacket.QDSECTION].size)
+        assertTrue(dnsPacket.records[DnsPacket.ANSECTION].size >= 5)
+        assertEquals(0, dnsPacket.records[DnsPacket.NSSECTION].size)
+        assertEquals(0, dnsPacket.records[DnsPacket.ARSECTION].size)
+
+        val ptrRecord = dnsPacket.records[DnsPacket.ANSECTION][0]
+        assertEquals("$expectedServiceType.local", ptrRecord.dName)
+        assertEquals(0x0C /* PTR */, ptrRecord.nsType)
+        val ptrSubRecord = dnsPacket.records[DnsPacket.ANSECTION][1]
+        assertEquals("_subtype._sub.$expectedServiceType.local", ptrSubRecord.dName)
+        assertEquals(0x0C /* PTR */, ptrSubRecord.nsType)
+        val srvRecord = dnsPacket.records[DnsPacket.ANSECTION][2]
+        assertEquals("${si.serviceName}.$expectedServiceType.local", srvRecord.dName)
+        assertEquals(0x21 /* SRV */, srvRecord.nsType)
+        val txtRecord = dnsPacket.records[DnsPacket.ANSECTION][3]
+        assertEquals("${si.serviceName}.$expectedServiceType.local", txtRecord.dName)
+        assertEquals(0x10 /* TXT */, txtRecord.nsType)
+        val iface = NetworkInterface.getByName(testNetwork1.iface.interfaceName)
+        val allAddress = iface.inetAddresses.toList()
+        for (i in 4 until dnsPacket.records[DnsPacket.ANSECTION].size) {
+            val addressRecord = dnsPacket.records[DnsPacket.ANSECTION][i]
+            assertTrue(addressRecord.dName.startsWith("Android_"))
+            assertTrue(addressRecord.dName.endsWith("local"))
+            assertTrue(addressRecord.nsType in arrayOf(0x1C /* AAAA */, 0x01 /* A */))
+            val rData = addressRecord.rr
+            assertNotNull(rData)
+            val addr = InetAddress.getByAddress(rData)
+            assertTrue(addr in allAddress)
+        }
+    }
+
+    @Test
+    fun testNsdManager_registerOffloadEngine() {
+        val targetSdkVersion = context.packageManager
+            .getTargetSdkVersion(context.applicationInfo.packageName)
+        // The offload callbacks are only supported with the new backend,
+        // enabled with target SDK U+.
+        assumeTrue(isAtLeastU() || targetSdkVersion > Build.VERSION_CODES.TIRAMISU)
+
+        // TODO: also have a test that use an executor that runs in a different thread, and pass
+        // in the thread ID NsdServiceInfo to check it
+        val si1 = NsdServiceInfo()
+        si1.serviceType = "$serviceType,_subtype"
+        si1.serviceName = serviceName
+        si1.network = testNetwork1.network
+        si1.port = 23456
+        val record1 = NsdRegistrationRecord()
+
+        val si2 = NsdServiceInfo()
+        si2.serviceType = "$serviceType,_subtype"
+        si2.serviceName = serviceName2
+        si2.network = testNetwork1.network
+        si2.port = 12345
+        val record2 = NsdRegistrationRecord()
+        val offloadEngine = TestNsdOffloadEngine()
+
+        tryTest {
+            // Register service before the OffloadEngine is registered.
+            nsdManager.registerService(si1, NsdManager.PROTOCOL_DNS_SD, record1)
+            record1.expectCallback<ServiceRegistered>()
+            runAsShell(NETWORK_SETTINGS) {
+                nsdManager.registerOffloadEngine(testNetwork1.iface.interfaceName,
+                    OffloadEngine.OFFLOAD_TYPE_REPLY.toLong(),
+                    OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK.toLong(),
+                    { it.run() }, offloadEngine)
+            }
+            val addOrUpdateEvent1 = offloadEngine
+                .expectCallbackEventually<TestNsdOffloadEngine.OffloadEvent.AddOrUpdateEvent> {
+                    it.info.key.serviceName == si1.serviceName
+                }
+            checkOffloadServiceInfo(addOrUpdateEvent1.info, si1)
+
+            // Register service after OffloadEngine is registered.
+            nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, record2)
+            record2.expectCallback<ServiceRegistered>()
+            val addOrUpdateEvent2 = offloadEngine
+                .expectCallbackEventually<TestNsdOffloadEngine.OffloadEvent.AddOrUpdateEvent> {
+                    it.info.key.serviceName == si2.serviceName
+                }
+            checkOffloadServiceInfo(addOrUpdateEvent2.info, si2)
+
+            nsdManager.unregisterService(record2)
+            record2.expectCallback<ServiceUnregistered>()
+            val unregisterEvent = offloadEngine
+                .expectCallbackEventually<TestNsdOffloadEngine.OffloadEvent.RemoveEvent> {
+                    it.info.key.serviceName == si2.serviceName
+                }
+            checkOffloadServiceInfo(unregisterEvent.info, si2)
+        } cleanupStep {
+            runAsShell(NETWORK_SETTINGS) {
+                nsdManager.unregisterOffloadEngine(offloadEngine)
+            }
+        } cleanup {
+            nsdManager.unregisterService(record1)
+            record1.expectCallback<ServiceUnregistered>()
+        }
+    }
+
     private fun checkConnectSocketToMdnsd(shouldFail: Boolean) {
         val discoveryRecord = NsdDiscoveryRecord()
         val localSocket = LocalSocket()
@@ -943,6 +927,8 @@
         checkConnectSocketToMdnsd(shouldFail = false)
     }
 
+    // Native mdns powered by Netd is removed after U.
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test @CtsNetTestCasesMaxTargetSdk30("Socket is started with the service up to target SDK 30")
     fun testManagerCreatesLegacySocket() {
         nsdManager // Ensure the lazy-init member is initialized, so NsdManager is created
@@ -969,19 +955,12 @@
 
     @Test
     fun testStopServiceResolution() {
-        // This test requires shims supporting U+ APIs (NsdManager.stopServiceResolution)
-        assumeTrue(TestUtils.shouldTestUApis())
-
-        val si = NsdServiceInfo()
-        si.serviceType = this@NsdManagerTest.serviceType
-        si.serviceName = this@NsdManagerTest.serviceName
-        si.port = 12345 // Test won't try to connect so port does not matter
-
+        val si = makeTestServiceInfo()
         val resolveRecord = NsdResolveRecord()
         // Try to resolve an unknown service then stop it immediately.
         // Expected ResolutionStopped callback.
-        nsdShim.resolveService(nsdManager, si, { it.run() }, resolveRecord)
-        nsdShim.stopServiceResolution(nsdManager, resolveRecord)
+        nsdManager.resolveService(si, { it.run() }, resolveRecord)
+        nsdManager.stopServiceResolution(resolveRecord)
         val stoppedCb = resolveRecord.expectCallback<ResolutionStopped>()
         assertEquals(si.serviceName, stoppedCb.serviceInfo.serviceName)
         assertEquals(si.serviceType, stoppedCb.serviceInfo.serviceType)
@@ -989,20 +968,12 @@
 
     @Test
     fun testRegisterServiceInfoCallback() {
-        // This test requires shims supporting U+ APIs (NsdManager.registerServiceInfoCallback)
-        assumeTrue(TestUtils.shouldTestUApis())
-
         val lp = cm.getLinkProperties(testNetwork1.network)
         assertNotNull(lp)
         val addresses = lp.addresses
         assertFalse(addresses.isEmpty())
 
-        val si = NsdServiceInfo().apply {
-            serviceType = this@NsdManagerTest.serviceType
-            serviceName = this@NsdManagerTest.serviceName
-            network = testNetwork1.network
-            port = 12345 // Test won't try to connect so port does not matter
-        }
+        val si = makeTestServiceInfo(testNetwork1.network)
 
         // Register service on the network
         val registrationRecord = NsdRegistrationRecord()
@@ -1012,13 +983,13 @@
         val cbRecord = NsdServiceInfoCallbackRecord()
         tryTest {
             // Discover service on the network.
-            nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
                     testNetwork1.network, Executor { it.run() }, discoveryRecord)
             val foundInfo = discoveryRecord.waitForServiceDiscovered(
                     serviceName, serviceType, testNetwork1.network)
 
             // Register service callback and check the addresses are the same as network addresses
-            nsdShim.registerServiceInfoCallback(nsdManager, foundInfo, { it.run() }, cbRecord)
+            nsdManager.registerServiceInfoCallback(foundInfo, { it.run() }, cbRecord)
             val serviceInfoCb = cbRecord.expectCallback<ServiceUpdated>()
             assertEquals(foundInfo.serviceName, serviceInfoCb.serviceInfo.serviceName)
             val hostAddresses = serviceInfoCb.serviceInfo.hostAddresses
@@ -1034,7 +1005,7 @@
             cbRecord.expectCallback<ServiceUpdatedLost>()
         } cleanupStep {
             // Cancel subscription and check stop callback received.
-            nsdShim.unregisterServiceInfoCallback(nsdManager, cbRecord)
+            nsdManager.unregisterServiceInfoCallback(cbRecord)
             cbRecord.expectCallback<UnregisterCallbackSucceeded>()
         } cleanup {
             nsdManager.stopServiceDiscovery(discoveryRecord)
@@ -1044,9 +1015,6 @@
 
     @Test
     fun testStopServiceResolutionFailedCallback() {
-        // This test requires shims supporting U+ APIs (NsdManager.stopServiceResolution)
-        assumeTrue(TestUtils.shouldTestUApis())
-
         // It's not possible to make ResolutionListener#onStopResolutionFailed callback sending
         // because it is only sent in very edge-case scenarios when the legacy implementation is
         // used, and the legacy implementation is never used in the current AOSP builds. Considering
@@ -1106,6 +1074,1528 @@
         }
     }
 
+    @Test
+    fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApi() {
+        runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = false)
+    }
+
+    @Test
+    fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApiAndLegacySpecifier() {
+        runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = true)
+    }
+
+    private fun runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier: Boolean) {
+        val si = makeTestServiceInfo(network = testNetwork1.network)
+        if (useLegacySpecifier) {
+            si.subtypes = setOf("_subtype1")
+
+            // Test "_type._tcp.local,_subtype" syntax with the registration
+            si.serviceType = si.serviceType + ",_subtype2"
+        } else {
+            si.subtypes = setOf("_subtype1", "_subtype2")
+        }
+
+        val registrationRecord = NsdRegistrationRecord()
+
+        val baseTypeDiscoveryRecord = NsdDiscoveryRecord()
+        val subtype1DiscoveryRecord = NsdDiscoveryRecord()
+        val subtype2DiscoveryRecord = NsdDiscoveryRecord()
+        val otherSubtypeDiscoveryRecord = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord, si)
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, baseTypeDiscoveryRecord)
+
+            // Test "<subtype>._type._tcp.local" syntax with discovery. Note this is not
+            // "<subtype>._sub._type._tcp.local".
+            nsdManager.discoverServices("_othersubtype.$serviceType",
+                    NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, otherSubtypeDiscoveryRecord)
+            nsdManager.discoverServices("_subtype1.$serviceType",
+                    NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, subtype1DiscoveryRecord)
+
+            nsdManager.discoverServices(
+                    DiscoveryRequest.Builder(serviceType).setSubtype("_subtype2")
+                            .setNetwork(testNetwork1.network).build(),
+                    Executor { it.run() }, subtype2DiscoveryRecord)
+
+            val info1 = subtype1DiscoveryRecord.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            assertTrue(info1.subtypes.contains("_subtype1"))
+            val info2 = subtype2DiscoveryRecord.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            assertTrue(info2.subtypes.contains("_subtype2"))
+            baseTypeDiscoveryRecord.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStarted>()
+            // The subtype callback was registered later but called, no need for an extra delay
+            otherSubtypeDiscoveryRecord.assertNoCallback(timeoutMs = 0)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(baseTypeDiscoveryRecord)
+            nsdManager.stopServiceDiscovery(subtype1DiscoveryRecord)
+            nsdManager.stopServiceDiscovery(subtype2DiscoveryRecord)
+            nsdManager.stopServiceDiscovery(otherSubtypeDiscoveryRecord)
+
+            baseTypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+            subtype1DiscoveryRecord.expectCallback<DiscoveryStopped>()
+            subtype2DiscoveryRecord.expectCallback<DiscoveryStopped>()
+            otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+        }
+    }
+
+    @Test
+    fun testMultipleSubTypeAdvertisingAndDiscovery_withUpdate() {
+        val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+            serviceType += ",_subtype1"
+        }
+        val si2 = makeTestServiceInfo(network = testNetwork1.network).apply {
+            serviceType += ",_subtype2"
+        }
+        val registrationRecord = NsdRegistrationRecord()
+        val subtype3DiscoveryRecord = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord, si1)
+            updateService(registrationRecord, si2)
+            nsdManager.discoverServices("_subtype2.$serviceType",
+                    NsdManager.PROTOCOL_DNS_SD, testNetwork1.network,
+                    { it.run() }, subtype3DiscoveryRecord)
+            subtype3DiscoveryRecord.waitForServiceDiscovered(serviceName,
+                    serviceType, testNetwork1.network)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(subtype3DiscoveryRecord)
+            subtype3DiscoveryRecord.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+        }
+    }
+
+    @Test
+    fun testSubtypeAdvertisingAndDiscovery_nonAlphanumericalSubtypes() {
+        // All non-alphanumerical characters between 0x20 and 0x7e, with a leading underscore
+        val nonAlphanumSubtype = "_ !\"#\$%&'()*+-/:;<=>?@[\\]^_`{|}"
+        // Test both legacy syntax and the subtypes setter, on different networks
+        val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+            serviceType = "$serviceType,_test1,$nonAlphanumSubtype"
+        }
+        val si2 = makeTestServiceInfo(network = testNetwork2.network).apply {
+            subtypes = setOf("_test2", nonAlphanumSubtype)
+        }
+
+        val registrationRecord1 = NsdRegistrationRecord()
+        val registrationRecord2 = NsdRegistrationRecord()
+        val subtypeDiscoveryRecord1 = NsdDiscoveryRecord()
+        val subtypeDiscoveryRecord2 = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord1, si1)
+            registerService(registrationRecord2, si2)
+            nsdManager.discoverServices(DiscoveryRequest.Builder(serviceType)
+                .setSubtype(nonAlphanumSubtype)
+                .setNetwork(testNetwork1.network)
+                .build(), { it.run() }, subtypeDiscoveryRecord1)
+            nsdManager.discoverServices("$nonAlphanumSubtype.$serviceType",
+                NsdManager.PROTOCOL_DNS_SD, testNetwork2.network, { it.run() },
+                subtypeDiscoveryRecord2)
+
+            val discoveredInfo1 = subtypeDiscoveryRecord1.waitForServiceDiscovered(serviceName,
+                serviceType, testNetwork1.network)
+            val discoveredInfo2 = subtypeDiscoveryRecord2.waitForServiceDiscovered(serviceName,
+                serviceType, testNetwork2.network)
+            assertTrue(discoveredInfo1.subtypes.contains(nonAlphanumSubtype))
+            assertTrue(discoveredInfo2.subtypes.contains(nonAlphanumSubtype))
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(subtypeDiscoveryRecord1)
+            subtypeDiscoveryRecord1.expectCallback<DiscoveryStopped>()
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(subtypeDiscoveryRecord2)
+            subtypeDiscoveryRecord2.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord1)
+            nsdManager.unregisterService(registrationRecord2)
+        }
+    }
+
+    @Test
+    fun testSubtypeDiscovery_typeMatchButSubtypeNotMatch_notDiscovered() {
+        val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+            serviceType += ",_subtype1"
+        }
+        val registrationRecord = NsdRegistrationRecord()
+        val subtype2DiscoveryRecord = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord, si1)
+            val request = DiscoveryRequest.Builder(serviceType)
+                    .setSubtype("_subtype2").setNetwork(testNetwork1.network).build()
+            nsdManager.discoverServices(request, { it.run() }, subtype2DiscoveryRecord)
+            subtype2DiscoveryRecord.expectCallback<DiscoveryStarted>()
+            subtype2DiscoveryRecord.assertNoCallback(timeoutMs = 2000)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(subtype2DiscoveryRecord)
+            subtype2DiscoveryRecord.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+        }
+    }
+
+    @Test
+    fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() {
+        val si = makeTestServiceInfo(network = testNetwork1.network)
+        // Sets 101 subtypes in total
+        val seq = generateSequence(1) { it + 1}
+        si.subtypes = seq.take(100).toList().map {it -> "_subtype" + it}.toSet()
+        si.serviceType = si.serviceType + ",_subtype"
+
+        val record = NsdRegistrationRecord()
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
+
+        val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
+        assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
+    }
+
+    @Test
+    fun testSubtypeAdvertising_emptySubtypeLabel_returnsFailureBadParameters() {
+        val si = makeTestServiceInfo(network = testNetwork1.network)
+        si.subtypes = setOf("")
+
+        val record = NsdRegistrationRecord()
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
+
+        val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
+        assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
+    }
+
+    @Test
+    fun testRegisterWithConflictDuringProbing() {
+        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
+        assumeTrue(TestUtils.shouldTestTApis())
+
+        val si = makeTestServiceInfo(testNetwork1.network)
+
+        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)
+
+        tryTest {
+            assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+                    "Did not find a probe for the service")
+            packetReader.sendResponse(buildConflictingAnnouncement())
+
+            // Registration must use an updated name to avoid the conflict
+            val cb = registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+            cb.serviceInfo.serviceName.let {
+                assertTrue("Unexpected registered name: $it",
+                        it.startsWith(serviceName) && it != serviceName)
+            }
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        } cleanup {
+            packetReader.handler.post { packetReader.stop() }
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
+    @Test
+    fun testRegisterServiceWithCustomHostAndAddresses_conflictDuringProbing_hostRenamed() {
+        val si = makeTestServiceInfo(testNetwork1.network).apply {
+            hostname = customHostname
+            hostAddresses = listOf(
+                    parseNumericAddress("192.0.2.24"),
+                    parseNumericAddress("2001:db8::3"))
+        }
+
+        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)
+
+        tryTest {
+            assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+                    "Did not find a probe for the service")
+            packetReader.sendResponse(buildConflictingAnnouncementForCustomHost())
+
+            // Registration must use an updated hostname to avoid the conflict
+            val cb = registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+            // Service name is not renamed because there's no conflict on the service name.
+            assertEquals(serviceName, cb.serviceInfo.serviceName)
+            val hostname = cb.serviceInfo.hostname ?: fail("Missing hostname")
+            hostname.let {
+                assertTrue("Unexpected registered hostname: $it",
+                        it.startsWith(customHostname) && it != customHostname)
+            }
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        } cleanup {
+            packetReader.handler.post { packetReader.stop() }
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
+    @Test
+    fun testRegisterServiceWithCustomHostNoAddresses_noConflictDuringProbing_notRenamed() {
+        val si = makeTestServiceInfo(testNetwork1.network).apply {
+            hostname = customHostname
+        }
+
+        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)
+
+        tryTest {
+            assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+                    "Did not find a probe for the service")
+            // Not a conflict because no record is registered for the hostname
+            packetReader.sendResponse(buildConflictingAnnouncementForCustomHost())
+
+            // Registration is not renamed because there's no conflict
+            val cb = registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+            assertEquals(serviceName, cb.serviceInfo.serviceName)
+            assertEquals(customHostname, cb.serviceInfo.hostname)
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        } cleanup {
+            packetReader.handler.post { packetReader.stop() }
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
+    @Test
+    fun testRegisterWithConflictAfterProbing() {
+        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
+        assumeTrue(TestUtils.shouldTestTApis())
+
+        val si = makeTestServiceInfo(testNetwork1.network)
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        val discoveryRecord = NsdDiscoveryRecord()
+        val registeredService = registerService(registrationRecord, si)
+        val packetReader = TapPacketReader(Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        tryTest {
+            assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
+                    "No announcements sent after initial probing")
+
+            assertEquals(si.serviceName, registeredService.serviceName)
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network, { it.run() }, discoveryRecord)
+            discoveryRecord.waitForServiceDiscovered(si.serviceName, serviceType)
+
+            // Send a conflicting announcement
+            val conflictingAnnouncement = buildConflictingAnnouncement()
+            packetReader.sendResponse(conflictingAnnouncement)
+
+            // Expect to see probes (RFC6762 9., service is reset to probing state)
+            assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+                    "Probe not received within timeout after conflict")
+
+            // Send the conflicting packet again to reply to the probe
+            packetReader.sendResponse(conflictingAnnouncement)
+
+            // Note the legacy mdnsresponder would send an exit announcement here (a 0-lifetime
+            // advertisement just for the PTR record), but not the new advertiser. This probably
+            // follows RFC 6762 8.4, saying that when a record rdata changed, "In the case of shared
+            // records, a host MUST send a "goodbye" announcement with RR TTL zero [...] for the old
+            // rdata, to cause it to be deleted from peer caches, before announcing the new rdata".
+            //
+            // This should be implemented by the new advertiser, but in the case of conflicts it is
+            // not very valuable since an identical PTR record would be used by the conflicting
+            // service (except for subtypes). In that case the exit announcement may be
+            // counter-productive as it conflicts with announcements done by the conflicting
+            // service.
+
+            // Note that before sending the following ServiceRegistered callback for the renamed
+            // service, the legacy mdnsresponder-based implementation would first send a
+            // Service*Registered* callback for the original service name being *unregistered*; it
+            // should have been a ServiceUnregistered callback instead (bug in NsdService
+            // interpretation of the callback).
+            val newRegistration = registrationRecord.expectCallbackEventually<ServiceRegistered>(
+                    REGISTRATION_TIMEOUT_MS) {
+                it.serviceInfo.serviceName.startsWith(serviceName) &&
+                        it.serviceInfo.serviceName != serviceName
+            }
+
+            discoveryRecord.expectCallbackEventually<ServiceFound> {
+                it.serviceInfo.serviceName == newRegistration.serviceInfo.serviceName
+            }
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        } cleanup {
+            packetReader.handler.post { packetReader.stop() }
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
+    @Test
+    fun testRegisterServiceWithCustomHostAndAddresses_conflictAfterProbing_hostRenamed() {
+        val si = makeTestServiceInfo(testNetwork1.network).apply {
+            hostname = customHostname
+            hostAddresses = listOf(
+                    parseNumericAddress("192.0.2.24"),
+                    parseNumericAddress("2001:db8::3"))
+        }
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        val discoveryRecord = NsdDiscoveryRecord()
+        val registeredService = registerService(registrationRecord, si)
+        val packetReader = TapPacketReader(
+                Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        tryTest {
+            assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
+                "No announcements sent after initial probing")
+
+            assertEquals(si.serviceName, registeredService.serviceName)
+            assertEquals(si.hostname, registeredService.hostname)
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, { it.run() }, discoveryRecord)
+            val discoveredInfo = discoveryRecord.waitForServiceDiscovered(
+                    si.serviceName, serviceType)
+
+            // Send a conflicting announcement
+            val conflictingAnnouncement = buildConflictingAnnouncementForCustomHost()
+            packetReader.sendResponse(conflictingAnnouncement)
+
+            // Expect to see probes (RFC6762 9., service is reset to probing state)
+            assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+                    "Probe not received within timeout after conflict")
+
+            // Send the conflicting packet again to reply to the probe
+            packetReader.sendResponse(conflictingAnnouncement)
+
+            val newRegistration =
+                    registrationRecord
+                            .expectCallbackEventually<ServiceRegistered>(REGISTRATION_TIMEOUT_MS) {
+                                it.serviceInfo.serviceName == serviceName
+                                        && it.serviceInfo.hostname.let { hostname ->
+                                    hostname != null
+                                            && hostname.startsWith(customHostname)
+                                            && hostname != customHostname
+                                }
+                            }
+
+            val resolvedInfo = resolveService(discoveredInfo)
+            assertEquals(newRegistration.serviceInfo.serviceName, resolvedInfo.serviceName)
+            assertEquals(newRegistration.serviceInfo.hostname, resolvedInfo.hostname)
+
+            discoveryRecord.assertNoCallback()
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        } cleanup {
+            packetReader.handler.post { packetReader.stop() }
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
+    @Test
+    fun testRegisterServiceWithCustomHostNoAddresses_noConflictAfterProbing_notRenamed() {
+        val si = makeTestServiceInfo(testNetwork1.network).apply {
+            hostname = customHostname
+        }
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        val discoveryRecord = NsdDiscoveryRecord()
+        val registeredService = registerService(registrationRecord, si)
+        val packetReader = TapPacketReader(Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        tryTest {
+            assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
+                    "No announcements sent after initial probing")
+
+            assertEquals(si.serviceName, registeredService.serviceName)
+            assertEquals(si.hostname, registeredService.hostname)
+
+            // Send a conflicting announcement
+            val conflictingAnnouncement = buildConflictingAnnouncementForCustomHost()
+            packetReader.sendResponse(conflictingAnnouncement)
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, { it.run() }, discoveryRecord)
+
+            // The service is not renamed
+            discoveryRecord.waitForServiceDiscovered(si.serviceName, serviceType)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        } cleanup {
+            packetReader.handler.post { packetReader.stop() }
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
+    // Test that even if only a PTR record is received as a reply when discovering, without the
+    // SRV, TXT, address records as recommended (but not mandated) by RFC 6763 12, the service can
+    // still be discovered.
+    @Test
+    fun testDiscoveryWithPtrOnlyResponse_ServiceIsFound() {
+        // 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)
+            }
+        } cleanup {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        }
+    }
+
+    // Test RFC 6763 12. "Clients MUST be capable of functioning correctly with DNS servers [...]
+    // that fail to generate these additional records automatically, by issuing subsequent queries
+    // for any further record(s) they require"
+    @Test
+    fun testResolveWhenServerSendsNoAdditionalRecord() {
+        // Resolve service on testNetwork1
+        val resolveRecord = NsdResolveRecord()
+        val packetReader = TapPacketReader(Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */
+        )
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        val si = makeTestServiceInfo(testNetwork1.network)
+        nsdManager.resolveService(si, { it.run() }, resolveRecord)
+
+        val serviceFullName = "$serviceName.$serviceType.local"
+        // The query should ask for ANY, since both SRV and TXT are requested. Note legacy
+        // mdnsresponder will ask for SRV and TXT separately, and will not proceed to asking for
+        // address records without an answer for both.
+        val srvTxtQuery = packetReader.pollForQuery(serviceFullName, DnsResolver.TYPE_ANY)
+        assertNotNull(srvTxtQuery)
+
+        /*
+        Generated with:
+        scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+            scapy.DNSRRSRV(rrname='NsdTest123456789._nmt123456789._tcp.local',
+                rclass=0x8001, port=31234, target='testhost.local', ttl=120) /
+            scapy.DNSRR(rrname='NsdTest123456789._nmt123456789._tcp.local', type='TXT', ttl=120,
+                rdata='testkey=testvalue')
+        ))).hex()
+         */
+        val srvTxtResponsePayload = HexDump.hexStringToByteArray("000084000000000200000000104" +
+                "e7364546573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f6" +
+                "3616c0000218001000000780011000000007a020874657374686f7374c030c00c00100001000" +
+                "00078001211746573746b65793d7465737476616c7565")
+        replaceServiceNameAndTypeWithTestSuffix(srvTxtResponsePayload)
+        packetReader.sendResponse(buildMdnsPacket(srvTxtResponsePayload))
+
+        val testHostname = "testhost.local"
+        val addressQuery = packetReader.pollForQuery(testHostname,
+            DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA)
+        assertNotNull(addressQuery)
+
+        /*
+        Generated with:
+        scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+            scapy.DNSRR(rrname='testhost.local', type='A', ttl=120,
+                rdata='192.0.2.123') /
+            scapy.DNSRR(rrname='testhost.local', type='AAAA', ttl=120,
+                rdata='2001:db8::123')
+        ))).hex()
+         */
+        val addressPayload = HexDump.hexStringToByteArray("0000840000000002000000000874657374" +
+                "686f7374056c6f63616c0000010001000000780004c000027bc00c001c000100000078001020" +
+                "010db8000000000000000000000123")
+        packetReader.sendResponse(buildMdnsPacket(addressPayload))
+
+        val serviceResolved = resolveRecord.expectCallback<ServiceResolved>()
+        serviceResolved.serviceInfo.let {
+            assertEquals(serviceName, it.serviceName)
+            assertEquals(".$serviceType", it.serviceType)
+            assertEquals(testNetwork1.network, it.network)
+            assertEquals(31234, it.port)
+            assertEquals(1, it.attributes.size)
+            assertArrayEquals("testvalue".encodeToByteArray(), it.attributes["testkey"])
+        }
+        assertEquals(
+                setOf(parseNumericAddress("192.0.2.123"), parseNumericAddress("2001:db8::123")),
+                serviceResolved.serviceInfo.hostAddresses.toSet())
+    }
+
+    @Test
+    fun testUnicastReplyUsedWhenQueryUnicastFlagSet() {
+        // The flag may be removed in the future but unicast replies should be enabled by default
+        // in that case. The rule will reset flags automatically on teardown.
+        deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+        val si = makeTestServiceInfo(testNetwork1.network)
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        var nsResponder: NSResponder? = null
+        tryTest {
+            registerService(registrationRecord, si)
+            val packetReader = TapPacketReader(Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+            packetReader.startAsyncForTest()
+
+            handlerThread.waitForIdle(TIMEOUT_MS)
+            /*
+            Send a "query unicast" query.
+            Generated with:
+            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)
+
+            val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+            nsResponder = NSResponder(packetReader, mapOf(
+                testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+            )).apply { start() }
+
+            packetReader.sendResponse(buildMdnsPacket(mdnsPayload, testSrcAddr))
+            // The reply is sent unicast to the source address. There may be announcements sent
+            // multicast around this time, so filter by destination address.
+            val reply = packetReader.pollForMdnsPacket { pkt ->
+                pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+                        pkt.dstAddr == testSrcAddr
+            }
+            assertNotNull(reply)
+        } cleanup {
+            nsResponder?.stop()
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
+    @Test
+    fun testReplyWhenKnownAnswerSuppressionFlagSet() {
+        // 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_known_answer_suppression", "1")
+        deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+        val si = makeTestServiceInfo(testNetwork1.network)
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        var nsResponder: NSResponder? = null
+        tryTest {
+            registerService(registrationRecord, si)
+            val packetReader = TapPacketReader(Handler(handlerThread.looper),
+                    testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+            packetReader.startAsyncForTest()
+
+            handlerThread.waitForIdle(TIMEOUT_MS)
+            /*
+            Send a query with a known answer. Expect to receive a response containing TXT record
+            only.
+            Generated with:
+            scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+                    scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+                            qclass=0x8001) /
+                    scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+                            qclass=0x8001),
+                    an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
+                            rdata='NsdTest123456789._nmt123456789._tcp.local')
+            )).hex()
+            */
+            val query = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d74313233343" +
+                    "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
+                    "d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d743132333" +
+                    "43536373839045f746370056c6f63616c00000c000100001194002b104e73645465737431323" +
+                    "33435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+            replaceServiceNameAndTypeWithTestSuffix(query)
+
+            val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+            nsResponder = NSResponder(packetReader, mapOf(
+                    testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+            )).apply { start() }
+
+            packetReader.sendResponse(buildMdnsPacket(query, testSrcAddr))
+            // The reply is sent unicast to the source address. There may be announcements sent
+            // multicast around this time, so filter by destination address.
+            val reply = packetReader.pollForMdnsPacket { pkt ->
+                pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+                        !pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+                        pkt.dstAddr == testSrcAddr
+            }
+            assertNotNull(reply)
+
+            /*
+            Send a query with a known answer (TTL is less than half). Expect to receive a response
+            containing both PTR and TXT records.
+            Generated with:
+            scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+                    scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+                            qclass=0x8001) /
+                    scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+                            qclass=0x8001),
+                    an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=2150,
+                            rdata='NsdTest123456789._nmt123456789._tcp.local')
+            )).hex()
+            */
+            val query2 = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d7431323334" +
+                    "3536373839045f746370056c6f63616c00000c8001104e736454657374313233343536373839" +
+                    "0d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d74313233" +
+                    "343536373839045f746370056c6f63616c00000c000100000866002b104e7364546573743132" +
+                    "333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+            replaceServiceNameAndTypeWithTestSuffix(query2)
+
+            packetReader.sendResponse(buildMdnsPacket(query2, testSrcAddr))
+            // The reply is sent unicast to the source address. There may be announcements sent
+            // multicast around this time, so filter by destination address.
+            val reply2 = packetReader.pollForMdnsPacket { pkt ->
+                pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+                        pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+                        pkt.dstAddr == testSrcAddr
+            }
+            assertNotNull(reply2)
+        } cleanup {
+            nsResponder?.stop()
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
+    @Test
+    fun testReplyWithMultipacketWhenKnownAnswerSuppressionFlagSet() {
+        // 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_known_answer_suppression", "1")
+        deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+        val si = makeTestServiceInfo(testNetwork1.network)
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        var nsResponder: NSResponder? = null
+        tryTest {
+            registerService(registrationRecord, si)
+            val packetReader = TapPacketReader(Handler(handlerThread.looper),
+                    testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+            packetReader.startAsyncForTest()
+
+            handlerThread.waitForIdle(TIMEOUT_MS)
+            /*
+            Send a query with truncated bit set.
+            Generated with:
+            scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, tc=1, qd=
+                    scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+                            qclass=0x8001) /
+                    scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+                            qclass=0x8001)
+            )).hex()
+            */
+            val query = HexDump.hexStringToByteArray("0000020000020000000000000d5f6e6d74313233343" +
+                    "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
+                    "d5f6e6d74313233343536373839045f746370056c6f63616c0000108001")
+            replaceServiceNameAndTypeWithTestSuffix(query)
+            /*
+            Send a known answer packet (other service) with truncated bit set.
+            Generated with:
+            scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, tc=1, qd=None,
+                    an = scapy.DNSRR(rrname='_test._tcp.local', type='PTR', ttl=4500,
+                            rdata='NsdTest._test._tcp.local')
+            )).hex()
+            */
+            val knownAnswer1 = HexDump.hexStringToByteArray("000002000000000100000000055f74657374" +
+                    "045f746370056c6f63616c00000c000100001194001a074e736454657374055f74657374045f" +
+                    "746370056c6f63616c00")
+            replaceServiceNameAndTypeWithTestSuffix(knownAnswer1)
+            /*
+            Send a known answer packet.
+            Generated with:
+            scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd=None,
+                    an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
+                            rdata='NsdTest123456789._nmt123456789._tcp.local')
+            )).hex()
+            */
+            val knownAnswer2 = HexDump.hexStringToByteArray("0000000000000001000000000d5f6e6d7431" +
+                    "3233343536373839045f746370056c6f63616c00000c000100001194002b104e736454657374" +
+                    "3132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+            replaceServiceNameAndTypeWithTestSuffix(knownAnswer2)
+
+            val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+            nsResponder = NSResponder(packetReader, mapOf(
+                    testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+            )).apply { start() }
+
+            packetReader.sendResponse(buildMdnsPacket(query, testSrcAddr))
+            packetReader.sendResponse(buildMdnsPacket(knownAnswer1, testSrcAddr))
+            packetReader.sendResponse(buildMdnsPacket(knownAnswer2, testSrcAddr))
+            // The reply is sent unicast to the source address. There may be announcements sent
+            // multicast around this time, so filter by destination address.
+            val reply = packetReader.pollForMdnsPacket { pkt ->
+                pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+                        !pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+                        pkt.dstAddr == testSrcAddr
+            }
+            assertNotNull(reply)
+        } cleanup {
+            nsResponder?.stop()
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
+    @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
+        val linkAddr = lp.linkAddresses.firstOrNull {
+            it.isIPv6 && it.scope == RT_SCOPE_LINK && it.prefixLength == 64
+        } ?: fail("No /64 link-local address found in ${lp.linkAddresses} for net $network")
+
+        // Add one to the device address to simulate the address of another device on the prefix
+        val addrBytes = linkAddr.address.address
+        addrBytes[IPV6_ADDR_LEN - 1]++
+        return Inet6Address.getByAddress(addrBytes) as Inet6Address
+    }
+
+    @Test
+    fun testAdvertisingAndDiscovery_servicesWithCustomHost_customHostAddressesFound() {
+        val hostAddresses1 = listOf(
+                parseNumericAddress("192.0.2.23"),
+                parseNumericAddress("2001:db8::1"),
+                parseNumericAddress("2001:db8::2"))
+        val hostAddresses2 = listOf(
+                parseNumericAddress("192.0.2.24"),
+                parseNumericAddress("2001:db8::3"))
+        val si1 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceName = serviceName
+            it.serviceType = serviceType
+            it.port = TEST_PORT
+            it.hostname = customHostname
+            it.hostAddresses = hostAddresses1
+        }
+        val si2 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceName = serviceName2
+            it.serviceType = serviceType
+            it.port = TEST_PORT + 1
+            it.hostname = customHostname2
+            it.hostAddresses = hostAddresses2
+        }
+        val registrationRecord1 = NsdRegistrationRecord()
+        val registrationRecord2 = NsdRegistrationRecord()
+
+        val discoveryRecord1 = NsdDiscoveryRecord()
+        val discoveryRecord2 = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord1, si1)
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, discoveryRecord1)
+
+            val discoveredInfo = discoveryRecord1.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            val resolvedInfo = resolveService(discoveredInfo)
+
+            assertEquals(TEST_PORT, resolvedInfo.port)
+            assertEquals(si1.hostname, resolvedInfo.hostname)
+            assertAddressEquals(hostAddresses1, resolvedInfo.hostAddresses)
+
+            registerService(registrationRecord2, si2)
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, discoveryRecord2)
+
+            val discoveredInfo2 = discoveryRecord2.waitForServiceDiscovered(
+                    serviceName2, serviceType, testNetwork1.network)
+            val resolvedInfo2 = resolveService(discoveredInfo2)
+
+            assertEquals(TEST_PORT + 1, resolvedInfo2.port)
+            assertEquals(si2.hostname, resolvedInfo2.hostname)
+            assertAddressEquals(hostAddresses2, resolvedInfo2.hostAddresses)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord1)
+            nsdManager.stopServiceDiscovery(discoveryRecord2)
+
+            discoveryRecord1.expectCallbackEventually<DiscoveryStopped>()
+            discoveryRecord2.expectCallbackEventually<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord1)
+            nsdManager.unregisterService(registrationRecord2)
+        }
+    }
+
+    @Test
+    fun testAdvertisingAndDiscovery_multipleRegistrationsForSameCustomHost_hostRenamed() {
+        val hostAddresses1 = listOf(
+                parseNumericAddress("192.0.2.23"),
+                parseNumericAddress("2001:db8::1"),
+                parseNumericAddress("2001:db8::2"))
+        val hostAddresses2 = listOf(
+                parseNumericAddress("192.0.2.24"),
+                parseNumericAddress("2001:db8::3"))
+        val si1 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.hostname = customHostname
+            it.hostAddresses = hostAddresses1
+        }
+        val si2 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceName = serviceName
+            it.serviceType = serviceType
+            it.port = TEST_PORT
+            it.hostname = customHostname
+            it.hostAddresses = hostAddresses2
+        }
+
+        val registrationRecord1 = NsdRegistrationRecord()
+        val registrationRecord2 = NsdRegistrationRecord()
+
+        val discoveryRecord = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord1, si1)
+            registerService(registrationRecord2, si2)
+
+            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(TEST_PORT, resolvedInfo.port)
+            assertNotEquals(si1.hostname, resolvedInfo.hostname)
+            assertAddressEquals(hostAddresses2, resolvedInfo.hostAddresses)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+
+            discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord1)
+            nsdManager.unregisterService(registrationRecord2)
+        }
+    }
+
+    @Test
+    fun testAdvertisingAndDiscovery_servicesWithTheSameCustomHostAddressOmitted_addressesFound() {
+        val hostAddresses = listOf(
+                parseNumericAddress("192.0.2.23"),
+                parseNumericAddress("2001:db8::1"),
+                parseNumericAddress("2001:db8::2"))
+        val si1 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceType = serviceType
+            it.serviceName = serviceName
+            it.port = TEST_PORT
+            it.hostname = customHostname
+            it.hostAddresses = hostAddresses
+        }
+        val si2 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceType = serviceType
+            it.serviceName = serviceName2
+            it.port = TEST_PORT + 1
+            it.hostname = customHostname
+        }
+
+        val registrationRecord1 = NsdRegistrationRecord()
+        val registrationRecord2 = NsdRegistrationRecord()
+
+        val discoveryRecord = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord1, si1)
+
+            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)
+            assertEquals(si1.hostname, resolvedInfo1.hostname)
+            assertAddressEquals(hostAddresses, resolvedInfo1.hostAddresses)
+
+            registerService(registrationRecord2, si2)
+
+            val discoveredInfo2 = discoveryRecord.waitForServiceDiscovered(
+                    serviceName2, serviceType, testNetwork1.network)
+            val resolvedInfo2 = resolveService(discoveredInfo2)
+
+            assertEquals(serviceName2, discoveredInfo2.serviceName)
+            assertEquals(TEST_PORT + 1, resolvedInfo2.port)
+            assertEquals(si2.hostname, resolvedInfo2.hostname)
+            assertAddressEquals(hostAddresses, resolvedInfo2.hostAddresses)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord1)
+            nsdManager.unregisterService(registrationRecord2)
+        }
+    }
+
+    @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>()
+        }
+    }
+
+    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:
+        scapy.raw(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+                scapy.DNSRRSRV(rrname='NsdTest123456789._nmt123456789._tcp.local',
+                    rclass=0x8001, port=31234, target='conflict.local', ttl=120)
+        )).hex()
+         */
+        val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000104e736454657" +
+                "3743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00002" +
+                "18001000000780016000000007a0208636f6e666c696374056c6f63616c00")
+        replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
+
+        return buildMdnsPacket(mdnsPayload)
+    }
+
+    private fun buildConflictingAnnouncementForCustomHost(): ByteBuffer {
+        /*
+        Generated with scapy:
+        raw(DNS(rd=0, qr=1, aa=1, qd = None, an =
+            DNSRR(rrname='NsdTestHost123456789.local', type=28, rclass=1, ttl=120,
+                    rdata='2001:db8::321')
+        )).hex()
+         */
+        val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000144e7364" +
+                "54657374486f7374313233343536373839056c6f63616c00001c000100000078001020010db80000" +
+                "00000000000000000321")
+        replaceCustomHostnameWithTestSuffix(mdnsPayload)
+
+        return buildMdnsPacket(mdnsPayload)
+    }
+
+    /**
+     * Replaces occurrences of "NsdTest123456789" and "_nmt123456789" in mDNS payload with the
+     * actual random name and type that are used by the test.
+     */
+    private fun replaceServiceNameAndTypeWithTestSuffix(mdnsPayload: ByteArray) {
+        // Test service name and types have consistent length and are always ASCII
+        val testPacketName = "NsdTest123456789".encodeToByteArray()
+        val testPacketTypePrefix = "_nmt123456789".encodeToByteArray()
+        val encodedServiceName = serviceName.encodeToByteArray()
+        val encodedTypePrefix = serviceType.split('.')[0].encodeToByteArray()
+
+        val packetBuffer = ByteBuffer.wrap(mdnsPayload)
+        replaceAll(packetBuffer, testPacketName, encodedServiceName)
+        replaceAll(packetBuffer, testPacketTypePrefix, encodedTypePrefix)
+    }
+
+    /**
+     * Replaces occurrences of "NsdTestHost123456789" in mDNS payload with the
+     * actual random host name that are used by the test.
+     */
+    private fun replaceCustomHostnameWithTestSuffix(mdnsPayload: ByteArray) {
+        // Test custom hostnames have consistent length and are always ASCII
+        val testPacketName = "NsdTestHost123456789".encodeToByteArray()
+        val encodedHostname = customHostname.encodeToByteArray()
+
+        val packetBuffer = ByteBuffer.wrap(mdnsPayload)
+        replaceAll(packetBuffer, testPacketName, encodedHostname)
+    }
+
+    private tailrec fun replaceAll(buffer: ByteBuffer, source: ByteArray, replacement: ByteArray) {
+        assertEquals(source.size, replacement.size)
+        val index = buffer.array().indexOf(source)
+        if (index < 0) return
+
+        val origPosition = buffer.position()
+        buffer.position(index)
+        buffer.put(replacement)
+        buffer.position(origPosition)
+        replaceAll(buffer, source, replacement)
+    }
+
+    private fun buildMdnsPacket(
+        mdnsPayload: ByteArray,
+        srcAddr: Inet6Address = testSrcAddr
+    ): ByteBuffer {
+        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()))
+        packetBuilder.writeL2Header(
+                MacAddress.fromBytes(byteArrayOf(1, 2, 3, 4, 5, 6)) /* srcMac */,
+                multicastEthAddr,
+                ETH_P_IPV6.toShort())
+        packetBuilder.writeIpv6Header(
+                0x60000000, // version=6, traffic class=0x0, flowlabel=0x0
+                IPPROTO_UDP.toByte(),
+                64 /* hop limit */,
+                srcAddr,
+                multicastIpv6Addr /* dstIp */)
+        packetBuilder.writeUdpHeader(MDNS_PORT /* srcPort */, MDNS_PORT /* dstPort */)
+        packetBuffer.put(mdnsPayload)
+        return packetBuilder.finalizePacket()
+    }
+
     /**
      * Register a service and return its registration record.
      */
@@ -1114,16 +2604,28 @@
         si: NsdServiceInfo,
         executor: Executor = Executor { it.run() }
     ): NsdServiceInfo {
-        nsdShim.registerService(nsdManager, si, NsdManager.PROTOCOL_DNS_SD, executor, record)
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, executor, record)
         // We may not always get the name that we tried to register;
         // This events tells us the name that was registered.
         val cb = record.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
         return cb.serviceInfo
     }
 
+    /**
+     * Update a service.
+     */
+    private fun updateService(
+            record: NsdRegistrationRecord,
+            si: NsdServiceInfo,
+            executor: Executor = Executor { it.run() }
+    ) {
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, executor, record)
+        // TODO: add the callback check for the update.
+    }
+
     private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
         val record = NsdResolveRecord()
-        nsdShim.resolveService(nsdManager, discoveredInfo, Executor { it.run() }, record)
+        nsdManager.resolveService(discoveredInfo, Executor { it.run() }, record)
         val resolvedCb = record.expectCallback<ServiceResolved>()
         assertEquals(discoveredInfo.serviceName, resolvedCb.serviceInfo.serviceName)
 
@@ -1131,7 +2633,33 @@
     }
 }
 
+private fun ByteArray.indexOf(sub: ByteArray): Int {
+    var subIndex = 0
+    forEachIndexed { i, b ->
+        when (b) {
+            // Still matching: continue comparing with next byte
+            sub[subIndex] -> {
+                subIndex++
+                if (subIndex == sub.size) {
+                    return i - sub.size + 1
+                }
+            }
+            // Not matching next byte but matches first byte: continue comparing with 2nd byte
+            sub[0] -> subIndex = 1
+            // No matches: continue comparing from first byte
+            else -> subIndex = 0
+        }
+    }
+    return -1
+}
+
 private fun ByteArray?.utf8ToString(): String {
     if (this == null) return ""
     return String(this, StandardCharsets.UTF_8)
 }
+
+private fun assertAddressEquals(expected: List<InetAddress>, actual: List<InetAddress>) {
+    // No duplicate addresses in the actual address list
+    assertEquals(actual.toSet().size, actual.size)
+    assertEquals(expected.toSet(), actual.toSet())
+}
diff --git a/tests/cts/net/src/android/net/cts/OffloadServiceInfoTest.kt b/tests/cts/net/src/android/net/cts/OffloadServiceInfoTest.kt
new file mode 100644
index 0000000..36de4f2
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/OffloadServiceInfoTest.kt
@@ -0,0 +1,55 @@
+/*
+ * 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
+
+import android.net.nsd.OffloadEngine.OFFLOAD_TYPE_FILTER_QUERIES
+import android.net.nsd.OffloadServiceInfo
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** CTS tests for {@link OffloadServiceInfo}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class OffloadServiceInfoTest {
+    @Test
+    fun testCreateOffloadServiceInfo() {
+        val offloadServiceInfo = OffloadServiceInfo(
+            OffloadServiceInfo.Key("_testService", "_testType"),
+            listOf("_sub1", "_sub2"),
+            "Android.local",
+            byteArrayOf(0x1, 0x2, 0x3),
+            1 /* priority */,
+            OFFLOAD_TYPE_FILTER_QUERIES.toLong()
+        )
+
+        assertEquals(OffloadServiceInfo.Key("_testService", "_testType"), offloadServiceInfo.key)
+        assertEquals(listOf("_sub1", "_sub2"), offloadServiceInfo.subtypes)
+        assertEquals("Android.local", offloadServiceInfo.hostname)
+        assertContentEquals(byteArrayOf(0x1, 0x2, 0x3), offloadServiceInfo.offloadPayload)
+        assertEquals(1, offloadServiceInfo.priority)
+        assertEquals(OFFLOAD_TYPE_FILTER_QUERIES.toLong(), offloadServiceInfo.offloadType)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/PacProxyManagerTest.java b/tests/cts/net/src/android/net/cts/PacProxyManagerTest.java
index 4854901..b462f71 100644
--- a/tests/cts/net/src/android/net/cts/PacProxyManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/PacProxyManagerTest.java
@@ -44,12 +44,15 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.RequiredFeatureRule;
+
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.TestHttpServer;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -76,6 +79,11 @@
     private ServerSocket mServerSocket;
     private Instrumentation mInstrumentation;
 
+    // Devices without WebView/JavaScript cannot support PAC proxies.
+    @Rule
+    public RequiredFeatureRule mRequiredWebviewFeatureRule =
+        new RequiredFeatureRule(PackageManager.FEATURE_WEBVIEW);
+
     private static final String PAC_FILE = "function FindProxyForURL(url, host)"
             + "{"
             + "  return \"PROXY 192.168.0.1:9091\";"
@@ -152,9 +160,6 @@
     @AppModeFull(reason = "Instant apps can't bind sockets to localhost for a test proxy server")
     @Test
     public void testSetCurrentProxyScriptUrl() throws Exception {
-        // Devices without WebView/JavaScript cannot support PAC proxies
-        assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WEBVIEW));
-
         // Register a PacProxyInstalledListener
         final TestPacProxyInstalledListener listener = new TestPacProxyInstalledListener();
         final Executor executor = (Runnable r) -> r.run();
diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java
index 4d924d1..1588835 100644
--- a/tests/cts/net/src/android/net/cts/PacketUtils.java
+++ b/tests/cts/net/src/android/net/cts/PacketUtils.java
@@ -49,6 +49,7 @@
     static final int TCP_HDRLEN = 20;
     static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12;
     static final int ESP_HDRLEN = 8;
+    static final int ICMP_HDRLEN = 8;
     static final int ESP_BLK_SIZE = 4; // ESP has to be 4-byte aligned
     static final int ESP_TRAILER_LEN = 2;
 
diff --git a/tests/cts/net/src/android/net/cts/ProxyTest.kt b/tests/cts/net/src/android/net/cts/ProxyTest.kt
index a661b26..872dbb9 100644
--- a/tests/cts/net/src/android/net/cts/ProxyTest.kt
+++ b/tests/cts/net/src/android/net/cts/ProxyTest.kt
@@ -70,7 +70,7 @@
 
     private fun getDefaultProxy(): ProxyInfo? {
         return InstrumentationRegistry.getInstrumentation().context
-                .getSystemService(ConnectivityManager::class.java)
+                .getSystemService(ConnectivityManager::class.java)!!
                 .getDefaultProxy()
     }
 
@@ -100,4 +100,4 @@
             Proxy.setHttpProxyConfiguration(original)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java b/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java
index 0eb5644..1b22f42 100644
--- a/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java
+++ b/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java
@@ -95,14 +95,17 @@
                 testIface.getFileDescriptor().close();
             }
 
-            if (tunNetworkCallback != null) {
-                sCm.unregisterNetworkCallback(tunNetworkCallback);
-            }
 
             final Network testNetwork = tunNetworkCallback.currentNetwork;
             if (testNetwork != null) {
                 tnm.teardownTestNetwork(testNetwork);
             }
+            // Ensure test network being torn down.
+            tunNetworkCallback.waitForLost();
+
+            if (tunNetworkCallback != null) {
+                sCm.unregisterNetworkCallback(tunNetworkCallback);
+            }
         }
     }
 
diff --git a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
index 6c5c1c8..5c867b7 100755
--- a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
+++ b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
@@ -37,6 +37,9 @@
 
     /** Verify the given value is in range [lower, upper] */
     private void assertInRange(String tag, long value, long lower, long upper) {
+        if (lower > upper) {
+            fail("lower must be less than or equal to upper: [" + lower + "," + upper + "]");
+        }
         final Range range = new Range(lower, upper);
         assertTrue(tag + ": " + value + " is not within range [" + lower + ", " + upper + "]",
                 range.contains(value));
diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java
index 268d8d2..8bf4998 100644
--- a/tests/cts/net/src/android/net/cts/TunUtils.java
+++ b/tests/cts/net/src/android/net/cts/TunUtils.java
@@ -47,6 +47,8 @@
     protected static final int IP4_PROTO_OFFSET = 9;
     protected static final int IP6_PROTO_OFFSET = 6;
 
+    private static final int SEQ_NUM_MATCH_NOT_REQUIRED = -1;
+
     private static final int DATA_BUFFER_LEN = 4096;
     private static final int TIMEOUT = 2000;
 
@@ -146,16 +148,30 @@
         return espPkt; // We've found the packet we're looking for.
     }
 
+    /** Await the expected ESP packet */
     public byte[] awaitEspPacket(int spi, boolean useEncap) throws Exception {
-        return awaitPacket((pkt) -> isEsp(pkt, spi, useEncap));
+        return awaitEspPacket(spi, useEncap, SEQ_NUM_MATCH_NOT_REQUIRED);
     }
 
-    private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) {
+    /** Await the expected ESP packet with a matching sequence number */
+    public byte[] awaitEspPacket(int spi, boolean useEncap, int seqNum) throws Exception {
+        return awaitPacket((pkt) -> isEsp(pkt, spi, seqNum, useEncap));
+    }
+
+    private static boolean isMatchingEspPacket(byte[] pkt, int espOffset, int spi, int seqNum) {
         ByteBuffer buffer = ByteBuffer.wrap(pkt);
         buffer.get(new byte[espOffset]); // Skip IP, UDP header
         int actualSpi = buffer.getInt();
+        int actualSeqNum = buffer.getInt();
 
-        return actualSpi == spi;
+        if (actualSeqNum < 0) {
+            throw new UnsupportedOperationException(
+                    "actualSeqNum overflowed and needs to be converted to an unsigned integer");
+        }
+
+        boolean isSeqNumMatched = (seqNum == SEQ_NUM_MATCH_NOT_REQUIRED || seqNum == actualSeqNum);
+
+        return actualSpi == spi && isSeqNumMatched;
     }
 
     /**
@@ -173,29 +189,32 @@
             fail("Banned plaintext packet found");
         }
 
-        return isEsp(pkt, spi, encap);
+        return isEsp(pkt, spi, SEQ_NUM_MATCH_NOT_REQUIRED, encap);
     }
 
-    private static boolean isEsp(byte[] pkt, int spi, boolean encap) {
+    private static boolean isEsp(byte[] pkt, int spi, int seqNum, boolean encap) {
         if (isIpv6(pkt)) {
             if (encap) {
                 return pkt[IP6_PROTO_OFFSET] == IPPROTO_UDP
-                        && isSpiEqual(pkt, IP6_HDRLEN + UDP_HDRLEN, spi);
+                        && isMatchingEspPacket(pkt, IP6_HDRLEN + UDP_HDRLEN, spi, seqNum);
             } else {
-                return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP6_HDRLEN, spi);
+                return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP
+                        && isMatchingEspPacket(pkt, IP6_HDRLEN, spi, seqNum);
             }
 
         } else {
             // Use default IPv4 header length (assuming no options)
             if (encap) {
                 return pkt[IP4_PROTO_OFFSET] == IPPROTO_UDP
-                        && isSpiEqual(pkt, IP4_HDRLEN + UDP_HDRLEN, spi);
+                        && isMatchingEspPacket(pkt, IP4_HDRLEN + UDP_HDRLEN, spi, seqNum);
             } else {
-                return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP4_HDRLEN, spi);
+                return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP
+                        && isMatchingEspPacket(pkt, IP4_HDRLEN, spi, seqNum);
             }
         }
     }
 
+
     public static boolean isIpv6(byte[] pkt) {
         // First nibble shows IP version. 0x60 for IPv6
         return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
diff --git a/tests/cts/net/src/android/net/cts/VpnServiceTest.java b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
index 5c7b5ca..f343e83 100644
--- a/tests/cts/net/src/android/net/cts/VpnServiceTest.java
+++ b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
@@ -15,12 +15,28 @@
  */
 package android.net.cts;
 
+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.Assume.assumeFalse;
+
+import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.net.VpnService;
 import android.os.ParcelFileDescriptor;
 import android.platform.test.annotations.AppModeFull;
 import android.test.AndroidTestCase;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.File;
 import java.net.DatagramSocket;
 import java.net.Socket;
@@ -30,12 +46,21 @@
  * blocks us from writing tests for positive cases. For now we only test for
  * negative cases, and we will try to cover the rest in the future.
  */
-public class VpnServiceTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class VpnServiceTest {
 
     private static final String TAG = VpnServiceTest.class.getSimpleName();
 
+    private final Context mContext = InstrumentationRegistry.getContext();
     private VpnService mVpnService = new VpnService();
 
+    @Before
+    public void setUp() {
+        assumeFalse("Skipping test because watches don't support VPN",
+            mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH));
+    }
+
+    @Test
     @AppModeFull(reason = "PackageManager#queryIntentActivities cannot access in instant app mode")
     public void testPrepare() throws Exception {
         // Should never return null since we are not prepared.
@@ -47,6 +72,7 @@
         assertEquals(1, count);
     }
 
+    @Test
     @AppModeFull(reason = "establish() requires prepare(), which requires PackageManager access")
     public void testEstablish() throws Exception {
         ParcelFileDescriptor descriptor = null;
@@ -63,6 +89,7 @@
         }
     }
 
+    @Test
     @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
     public void testProtect_DatagramSocket() throws Exception {
         DatagramSocket socket = new DatagramSocket();
@@ -78,6 +105,7 @@
         }
     }
 
+    @Test
     @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
     public void testProtect_Socket() throws Exception {
         Socket socket = new Socket();
@@ -93,6 +121,7 @@
         }
     }
 
+    @Test
     @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
     public void testProtect_int() throws Exception {
         DatagramSocket socket = new DatagramSocket();
@@ -114,6 +143,7 @@
         }
     }
 
+    @Test
     public void testTunDevice() throws Exception {
         File file = new File("/dev/tun");
         assertTrue(file.exists());
diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp
index fffd30f..644634b 100644
--- a/tests/cts/net/util/Android.bp
+++ b/tests/cts/net/util/Android.bp
@@ -16,12 +16,16 @@
 
 // Common utilities for cts net tests.
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 java_library {
     name: "cts-net-utils",
-    srcs: ["java/**/*.java", "java/**/*.kt"],
+    srcs: [
+        "java/**/*.java",
+        "java/**/*.kt",
+    ],
     static_libs: [
         "compatibility-device-util-axt",
         "junit",
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 9d73946..173d13f 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
@@ -16,6 +16,7 @@
 
 package android.net.cts.util;
 
+import static android.Manifest.permission.MODIFY_PHONE_STATE;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -52,14 +53,20 @@
 import android.os.Build;
 import android.os.ConditionVariable;
 import android.os.IBinder;
+import android.os.UserHandle;
 import android.system.Os;
 import android.system.OsConstants;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.SystemUtil;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.ConnectivitySettingsUtils;
 import com.android.testutils.ConnectUtil;
 
@@ -68,6 +75,8 @@
 import java.io.OutputStream;
 import java.net.InetSocketAddress;
 import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -82,10 +91,10 @@
     private static final String FEATURE_IPSEC_TUNNEL_MIGRATION =
             "android.software.ipsec_tunnel_migration";
 
-    private static final int SOCKET_TIMEOUT_MS = 2000;
+    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";
@@ -137,7 +146,8 @@
         for (final String pkg : new String[] {"com.android.shell", mContext.getPackageName()}) {
             final String cmd =
                     String.format(
-                            "appops set %s %s %s",
+                            "appops set --user %d %s %s %s",
+                            UserHandle.myUserId(), // user id
                             pkg, // Package name
                             opName, // Appop
                             (allow ? "allow" : "deny")); // Action
@@ -165,21 +175,39 @@
         return cb;
     }
 
-    // Toggle WiFi twice, leaving it in the state it started in
-    public void toggleWifi() throws Exception {
-        if (mWifiManager.isWifiEnabled()) {
-            Network wifiNetwork = getWifiNetwork();
-            // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
-            expectNetworkIsSystemDefault(wifiNetwork);
-            disconnectFromWifi(wifiNetwork);
-            connectToWifi();
-        } else {
-            connectToWifi();
-            Network wifiNetwork = getWifiNetwork();
-            // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
-            expectNetworkIsSystemDefault(wifiNetwork);
-            disconnectFromWifi(wifiNetwork);
+    /**
+     * Toggle Wi-Fi off and on, waiting for the {@link ConnectivityManager#CONNECTIVITY_ACTION}
+     * broadcast in both cases.
+     */
+    public void reconnectWifiAndWaitForConnectivityAction() throws Exception {
+        assertTrue(mWifiManager.isWifiEnabled());
+        Network wifiNetwork = getWifiNetwork();
+        // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
+        expectNetworkIsSystemDefault(wifiNetwork);
+        disconnectFromWifi(wifiNetwork, true /* expectLegacyBroadcast */);
+        connectToWifi(true /* expectLegacyBroadcast */);
+    }
+
+    /**
+     * Turn Wi-Fi off, then back on and make sure it connects, if it is supported.
+     */
+    public void reconnectWifiIfSupported() throws Exception {
+        if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            return;
         }
+        disableWifi();
+        ensureWifiConnected();
+    }
+
+    /**
+     * Turn cell data off, then back on and make sure it connects, if it is supported.
+     */
+    public void reconnectCellIfSupported() throws Exception {
+        if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            return;
+        }
+        setMobileDataEnabled(false);
+        setMobileDataEnabled(true);
     }
 
     public Network expectNetworkIsSystemDefault(Network network)
@@ -382,40 +410,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)
@@ -508,17 +506,18 @@
      * @throws InterruptedException If the thread is interrupted.
      */
     public void awaitPrivateDnsSetting(@NonNull String msg, @NonNull Network network,
-            @NonNull String server, boolean requiresValidatedServer) throws InterruptedException {
+            @Nullable String server, boolean requiresValidatedServer) throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(1);
         final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
-        NetworkCallback callback = new NetworkCallback() {
+        final NetworkCallback callback = new NetworkCallback() {
             @Override
             public void onLinkPropertiesChanged(Network n, LinkProperties lp) {
                 Log.i(TAG, "Link properties of network " + n + " changed to " + lp);
                 if (requiresValidatedServer && lp.getValidatedPrivateDnsServers().isEmpty()) {
                     return;
                 }
-                if (network.equals(n) && server.equals(lp.getPrivateDnsServerName())) {
+                Log.i(TAG, "Set private DNS server to " + server);
+                if (network.equals(n) && Objects.equals(server, lp.getPrivateDnsServerName())) {
                     latch.countDown();
                 }
             }
@@ -541,6 +540,67 @@
     }
 
     /**
+     * Get all testable Networks with internet capability.
+     */
+    public Network[] getTestableNetworks() {
+        final ArrayList<Network> testableNetworks = new ArrayList<Network>();
+        for (Network network : mCm.getAllNetworks()) {
+            final NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
+            if (nc != null
+                    && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                    && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+                testableNetworks.add(network);
+            }
+        }
+
+        assertTrue("This test requires that at least one public Internet-providing"
+                        + " network be connected. Please ensure that the device is connected to"
+                        + " a network.",
+                testableNetworks.size() >= 1);
+        return testableNetworks.toArray(new Network[0]);
+    }
+
+    /**
+     * Enables or disables the mobile data and waits for the state to change.
+     *
+     * @param enabled - true to enable, false to disable the mobile data.
+     */
+    public void setMobileDataEnabled(boolean enabled) throws InterruptedException {
+        final TelephonyManager tm =  mContext.getSystemService(TelephonyManager.class)
+                .createForSubscriptionId(SubscriptionManager.getDefaultDataSubscriptionId());
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.requestNetwork(request, callback);
+
+        try {
+            if (!enabled) {
+                assertNotNull("Cannot disable mobile data unless mobile data is connected",
+                        callback.waitForAvailable());
+            }
+
+            if (SdkLevel.isAtLeastS()) {
+                runAsShell(MODIFY_PHONE_STATE, () -> tm.setDataEnabledForReason(
+                        TelephonyManager.DATA_ENABLED_REASON_USER, enabled));
+            } else {
+                runAsShell(MODIFY_PHONE_STATE, () -> tm.setDataEnabled(enabled));
+            }
+            if (enabled) {
+                assertNotNull("Enabling mobile data did not connect mobile data",
+                        callback.waitForAvailable());
+            } else {
+                assertNotNull("Disabling mobile data did not disconnect mobile data",
+                        callback.waitForLost());
+            }
+
+        } finally {
+            mCm.unregisterNetworkCallback(callback);
+        }
+    }
+
+    /**
      * Receiver that captures the last connectivity change's network type and state. Recognizes
      * both {@code CONNECTIVITY_ACTION} and {@code NETWORK_CALLBACK_ACTION} intents.
      */
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
index f506c23..dffd9d5 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
@@ -393,21 +393,28 @@
         }
 
         public void assumeTetheringSupported() {
+            assumeTrue(isTetheringSupported());
+        }
+
+        private boolean isTetheringSupported() {
             final ArrayTrackRecord<CallbackValue>.ReadHead history =
                     mHistory.newReadHead();
-            assertNotNull("No onSupported callback", history.poll(TIMEOUT_MS, (cv) -> {
-                if (cv.callbackType != CallbackType.ON_SUPPORTED) return false;
+            final CallbackValue result = history.poll(TIMEOUT_MS, (cv) -> {
+                return cv.callbackType == CallbackType.ON_SUPPORTED;
+            });
 
-                assumeTrue(cv.callbackParam2 == 1 /* supported */);
-                return true;
-            }));
+            assertNotNull("No onSupported callback", result);
+            return result.callbackParam2 == 1 /* supported */;
         }
 
         public void assumeWifiTetheringSupported(final Context ctx) throws Exception {
-            assumeTetheringSupported();
+            assumeTrue(isWifiTetheringSupported(ctx));
+        }
 
-            assumeTrue(!getTetheringInterfaceRegexps().getTetherableWifiRegexs().isEmpty());
-            assumeTrue(isPortableHotspotSupported(ctx));
+        public boolean isWifiTetheringSupported(final Context ctx) throws Exception {
+            return isTetheringSupported()
+                    && !getTetheringInterfaceRegexps().getTetherableWifiRegexs().isEmpty()
+                    && isPortableHotspotSupported(ctx);
         }
 
         public TetheringInterfaceRegexps getTetheringInterfaceRegexps() {
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
index 37ad7cb..71d2b6e 100644
--- a/tests/cts/netpermission/internetpermission/Android.bp
+++ b/tests/cts/netpermission/internetpermission/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -29,5 +30,9 @@
         "cts",
         "general-tests",
     ],
-
+    host_required: ["net-tests-utils-host-common"],
+    sdk_version: "test_current",
+    data: [
+        ":ConnectivityTestPreparer",
+    ],
 }
diff --git a/tests/cts/netpermission/internetpermission/AndroidTest.xml b/tests/cts/netpermission/internetpermission/AndroidTest.xml
index 3b23e72..13deb82 100644
--- a/tests/cts/netpermission/internetpermission/AndroidTest.xml
+++ b/tests/cts/netpermission/internetpermission/AndroidTest.xml
@@ -16,14 +16,18 @@
 <configuration description="Config for CTS internet permission test cases">
     <option name="test-suite-tag" value="cts" />
     <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
     <option name="config-descriptor:metadata" key="parameter" value="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="parameter" value="secondary_user_on_secondary_display" />
     <option name="not-shardable" value="true" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsNetTestCasesInternetPermission.apk" />
     </target_preparer>
+    <target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
+    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.networkpermission.internetpermission.cts" />
         <option name="runtime-hint" value="10s" />
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index 7a24886..b324dc8 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -13,21 +13,29 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 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: [
         "cts",
         "general-tests",
     ],
-
+    data: [":ConnectivityTestPreparer"],
+    host_required: ["net-tests-utils-host-common"],
 }
diff --git a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
index c47cad9..82994c4 100644
--- a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
+++ b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
@@ -16,14 +16,18 @@
 <configuration description="Config for CTS update stats permission test cases">
     <option name="test-suite-tag" value="cts" />
     <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
     <option name="config-descriptor:metadata" key="parameter" value="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="parameter" value="secondary_user_on_secondary_display" />
     <option name="not-shardable" value="true" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsNetTestCasesUpdateStatsPermission.apk" />
     </target_preparer>
+    <target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
+    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.networkpermission.updatestatspermission.cts" />
         <option name="runtime-hint" value="10s" />
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 4284f56..1023173 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -46,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"],
 }
 
@@ -79,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"],
@@ -92,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/AndroidTestTemplate.xml b/tests/cts/tethering/AndroidTestTemplate.xml
index c842c09..dd5b23e 100644
--- a/tests/cts/tethering/AndroidTestTemplate.xml
+++ b/tests/cts/tethering/AndroidTestTemplate.xml
@@ -20,12 +20,13 @@
     <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="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
     <option name="not-shardable" value="true" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="{MODULE}.apk" />
     </target_preparer>
-    <target_preparer class="com.android.testutils.ConnectivityCheckTargetPreparer">
+    <target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.tethering.cts" />
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 274596f..81608f7 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -71,6 +71,8 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.testutils.ParcelUtils;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -236,6 +238,26 @@
     }
 
     @Test
+    public void testTetheringRequestParcelable() {
+        final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
+        final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
+        final TetheringRequest unparceled = new TetheringRequest.Builder(TETHERING_USB)
+                .setStaticIpv4Addresses(localAddr, clientAddr)
+                .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());
+    }
+
+    @Test
     public void testRegisterTetheringEventCallback() throws Exception {
         final TestTetheringEventCallback tetherEventCallback =
                 mCtsTetheringUtils.registerTetheringEventCallback();
diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp
index 8205f1c..726e504 100644
--- a/tests/deflake/Android.bp
+++ b/tests/deflake/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 12919ae..349529dd 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
@@ -45,6 +46,7 @@
         // order-dependent setup.
         "NetworkStackApiStableLib",
         "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
         "frameworks-net-integration-testutils",
         "kotlin-reflect",
         "mockito-target-extended-minus-junit4",
@@ -73,7 +75,10 @@
 java_library {
     name: "frameworks-net-integration-testutils",
     defaults: ["framework-connectivity-test-defaults"],
-    srcs: ["util/**/*.java", "util/**/*.kt"],
+    srcs: [
+        "util/**/*.java",
+        "util/**/*.kt",
+    ],
     static_libs: [
         "androidx.annotation_annotation",
         "androidx.test.rules",
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
index 50f02d3..1821329 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -40,6 +40,11 @@
     <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
     <!-- Querying the resources package -->
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+    <!-- Register UidFrozenStateChangedCallback -->
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
+    <!-- Permission required for CTS test - NetworkStatsIntegrationTest -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
 
diff --git a/tests/integration/OWNERS b/tests/integration/OWNERS
new file mode 100644
index 0000000..3101da5
--- /dev/null
+++ b/tests/integration/OWNERS
@@ -0,0 +1,2 @@
+# Bug template url: http://b/new?component=31808
+# TODO: move bug template config to common owners file once b/226427845 is resolved
\ No newline at end of file
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 67e1296..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
@@ -37,28 +38,43 @@
 import android.net.Uri
 import android.net.metrics.IpConnectivityLog
 import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
 import android.os.IBinder
 import android.os.SystemConfigManager
 import android.os.UserHandle
+import android.os.VintfRuntimeInfo
+import android.telephony.TelephonyManager
 import android.testing.TestableContext
 import android.util.Log
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil
 import com.android.connectivity.resources.R
+import com.android.net.module.util.BpfUtils
+import com.android.networkstack.apishim.TelephonyManagerShimImpl
 import com.android.server.BpfNetMaps
 import com.android.server.ConnectivityService
 import com.android.server.NetworkAgentWrapper
 import com.android.server.TestNetIdManager
+import com.android.server.connectivity.CarrierPrivilegeAuthenticator
 import com.android.server.connectivity.ConnectivityResources
 import com.android.server.connectivity.MockableSystemProperties
 import com.android.server.connectivity.MultinetworkPolicyTracker
 import com.android.server.connectivity.ProxyTracker
+import com.android.server.connectivity.SatelliteAccessController
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.DeviceInfoUtils
 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
 import kotlin.test.fail
 import org.junit.After
+import org.junit.Assume
 import org.junit.Before
 import org.junit.BeforeClass
 import org.junit.Test
@@ -68,12 +84,10 @@
 import org.mockito.Mock
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doNothing
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
 import org.mockito.MockitoAnnotations
 import org.mockito.Spy
 
@@ -84,7 +98,8 @@
  * Test that exercises an instrumented version of ConnectivityService against an instrumented
  * NetworkStack in a different test process.
  */
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
 class ConnectivityServiceIntegrationTest {
     // lateinit used here for mocks as they need to be reinitialized between each test and the test
     // should crash if they are used before being initialized.
@@ -111,6 +126,8 @@
     private lateinit var service: ConnectivityService
     private lateinit var cm: ConnectivityManager
 
+    private val handlerThreads = mutableListOf<HandlerThread>()
+
     companion object {
         // lateinit for this binder token, as it must be initialized before any test code is run
         // and use of it before init should crash the test.
@@ -191,7 +208,7 @@
         networkStackClient = TestNetworkStackClient(realContext)
         networkStackClient.start()
 
-        service = TestConnectivityService(makeDependencies())
+        service = TestConnectivityService(TestDependencies())
         cm = ConnectivityManager(context, service)
         context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm)
         context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager)
@@ -202,31 +219,66 @@
     private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
             context, dnsResolver, log, netd, deps)
 
-    private fun makeDependencies(): ConnectivityService.Dependencies {
-        val deps = spy(ConnectivityService.Dependencies())
-        doReturn(networkStackClient).`when`(deps).networkStack
-        doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any())
-        doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties
-        doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager()
-        doReturn(mock(BpfNetMaps::class.java)).`when`(deps).getBpfNetMaps(any(), any())
-        doAnswer { inv ->
-            MultinetworkPolicyTracker(inv.getArgument(0),
-                    inv.getArgument(1),
-                    inv.getArgument(2),
-                    object : MultinetworkPolicyTracker.Dependencies() {
-                        override fun getResourcesForActiveSubId(
-                                connResources: ConnectivityResources,
-                                activeSubId: Int
-                        ) = resources
-                    })
-        }.`when`(deps).makeMultinetworkPolicyTracker(any(), any(), any())
-        return deps
+    private inner class TestDependencies : ConnectivityService.Dependencies() {
+        override fun getNetworkStack() = networkStackClient
+        override fun makeProxyTracker(context: Context, connServiceHandler: Handler) =
+            mock(ProxyTracker::class.java)
+        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,
+            h: Handler,
+            r: Runnable
+        ) = MultinetworkPolicyTracker(c, h, r,
+            object : MultinetworkPolicyTracker.Dependencies() {
+                override fun getResourcesForActiveSubId(
+                    connResources: ConnectivityResources,
+                    activeSubId: Int
+                ) = resources
+            })
+
+        override fun makeHandlerThread(tag: String): HandlerThread =
+            super.makeHandlerThread(tag).also { handlerThreads.add(it) }
+
+        override fun makeCarrierPrivilegeAuthenticator(
+                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, handler)
+        }
+
+        override fun makeSatelliteAccessController(
+            context: Context,
+            updateSatellitePreferredUid: Consumer<MutableSet<Int>>?,
+            connectivityServiceInternalHandler: Handler
+        ): SatelliteAccessController? = mock(
+            SatelliteAccessController::class.java)
     }
 
     @After
     fun tearDown() {
         nsInstrumentation.clearAllState()
         ConnectivityResources.setResourcesContextForTest(null)
+        handlerThreads.forEach {
+            it.quitSafely()
+            it.join()
+        }
+        handlerThreads.clear()
     }
 
     @Test
@@ -248,8 +300,18 @@
         na.addCapability(NET_CAPABILITY_INTERNET)
         na.connect()
 
-        testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
-        assertEquals(2, nsInstrumentation.getRequestUrls().size)
+        tryTest {
+            testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
+            val requestedSize = nsInstrumentation.getRequestUrls().size
+            if (requestedSize == 2 || (requestedSize == 1 &&
+                        nsInstrumentation.getRequestUrls()[0] == httpsProbeUrl)
+            ) {
+                return@tryTest
+            }
+            fail("Unexpected request urls: ${nsInstrumentation.getRequestUrls()}")
+        } cleanup {
+            na.destroy()
+        }
     }
 
     @Test
@@ -281,23 +343,53 @@
         val lp = LinkProperties()
         lp.captivePortalApiUrl = Uri.parse(apiUrl)
         val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, lp, null /* ncTemplate */, context)
-        networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
 
-        na.addCapability(NET_CAPABILITY_INTERNET)
-        na.connect()
+        tryTest {
+            networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
 
-        testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS)
+            na.addCapability(NET_CAPABILITY_INTERNET)
+            na.connect()
 
-        val capportData = testCb.expect<LinkPropertiesChanged>(na, TEST_TIMEOUT_MS) {
-            it.lp.captivePortalData != null
-        }.lp.captivePortalData
-        assertTrue(capportData.isCaptive)
-        assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
-        assertEquals(Uri.parse("https://venueinfo.capport.android.com"), capportData.venueInfoUrl)
+            testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS)
 
-        testCb.expectCaps(na, TEST_TIMEOUT_MS) {
-            it.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) &&
-                    !it.hasCapability(NET_CAPABILITY_VALIDATED)
+            val capportData = testCb.expect<LinkPropertiesChanged>(na, TEST_TIMEOUT_MS) {
+                it.lp.captivePortalData != null
+            }.lp.captivePortalData
+            assertNotNull(capportData)
+            assertTrue(capportData.isCaptive)
+            assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
+            assertEquals(
+                Uri.parse("https://venueinfo.capport.android.com"),
+                capportData.venueInfoUrl
+            )
+
+            testCb.expectCaps(na, TEST_TIMEOUT_MS) {
+                it.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) &&
+                        !it.hasCapability(NET_CAPABILITY_VALIDATED)
+            }
+        } cleanup {
+            na.destroy()
+        }
+    }
+
+    private fun isBpfGetCgroupProgramIdSupportedByKernel(): Boolean {
+        val kVersionString = VintfRuntimeInfo.getKernelVersion()
+        return DeviceInfoUtils.compareMajorMinorVersion(kVersionString, "4.19") >= 0
+    }
+
+    @Test
+    fun testBpfProgramAttachStatus() {
+        Assume.assumeTrue(isBpfGetCgroupProgramIdSupportedByKernel())
+
+        listOf(
+                BpfUtils.BPF_CGROUP_INET_INGRESS,
+                BpfUtils.BPF_CGROUP_INET_EGRESS,
+                BpfUtils.BPF_CGROUP_INET_SOCK_CREATE
+        ).forEach {
+            val ret = SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
+                    "cmd connectivity bpf-get-cgroup-program-id $it").trim()
+
+            assertTrue(Integer.parseInt(ret) > 0, "Unexpected output $ret for type $it")
         }
     }
 }
diff --git a/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt
index e206313..467708a 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt
@@ -20,9 +20,9 @@
 import android.os.Parcelable
 
 data class HttpResponse(
-    val requestUrl: String,
+    val requestUrl: String?,
     val responseCode: Int,
-    val content: String = "",
+    val content: String? = "",
     val redirectUrl: String? = null
 ) : Parcelable {
     constructor(p: Parcel): this(p.readString(), p.readInt(), p.readString(), p.readString())
@@ -46,4 +46,4 @@
         override fun createFromParcel(source: Parcel) = HttpResponse(source)
         override fun newArray(size: Int) = arrayOfNulls<HttpResponse?>(size)
     }
-}
\ No newline at end of file
+}
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 e807952..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()
         }
@@ -70,7 +116,7 @@
          * request is seen, the test will fail.
          */
         override fun addHttpResponse(response: HttpResponse) {
-            httpResponses.getValue(response.requestUrl).add(response)
+            httpResponses.getValue(checkNotNull(response.requestUrl)).add(response)
         }
 
         /**
@@ -81,4 +127,4 @@
             return ArrayList(httpRequestUrls)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
new file mode 100644
index 0000000..4780c5d
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
@@ -0,0 +1,626 @@
+/*
+ * 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.net.integrationtests
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.annotation.TargetApi
+import android.app.usage.NetworkStats
+import android.app.usage.NetworkStats.Bucket
+import android.app.usage.NetworkStats.Bucket.TAG_NONE
+import android.app.usage.NetworkStatsManager
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.TYPE_TEST
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_TEST
+import android.net.TestNetworkSpecifier
+import android.net.TrafficStats
+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
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.PacketBridge
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TestDnsServer
+import com.android.testutils.TestHttpServer
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import fi.iki.elonen.NanoHTTPD
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.net.HttpURLConnection
+import java.net.HttpURLConnection.HTTP_OK
+import java.net.InetSocketAddress
+import java.net.URL
+import java.nio.charset.Charset
+import kotlin.math.ceil
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_TAG = 0xF00D
+
+@RunWith(DevSdkIgnoreRunner::class)
+@TargetApi(Build.VERSION_CODES.S)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class NetworkStatsIntegrationTest {
+    private val TAG = NetworkStatsIntegrationTest::class.java.simpleName
+    private val LOCAL_V6ADDR =
+        LinkAddress(InetAddresses.parseNumericAddress("2001:db8::1234"), 64)
+
+    // Remote address, both the client and server will have a hallucination that
+    // they are talking to this address.
+    private val REMOTE_V6ADDR =
+        LinkAddress(InetAddresses.parseNumericAddress("dead:beef::808:808"), 64)
+    private val REMOTE_V4ADDR =
+        LinkAddress(InetAddresses.parseNumericAddress("8.8.8.8"), 32)
+    private val DEFAULT_MTU = 1500
+    private val DEFAULT_BUFFER_SIZE = 1500 // Any size greater than or equal to mtu
+    private val CONNECTION_TIMEOUT_MILLIS = 15000
+    private val TEST_DOWNLOAD_SIZE = 10000L
+    private val TEST_UPLOAD_SIZE = 20000L
+    private val HTTP_SERVER_NAME = "test.com"
+    private val HTTP_SERVER_PORT = 8080 // Use port > 1024 to avoid restrictions on system ports
+    private val DNS_INTERNAL_SERVER_PORT = 53
+    private val DNS_EXTERNAL_SERVER_PORT = 1053
+    private val TCP_ACK_SIZE = 72
+
+    // Packet overheads that are not part of the actual data transmission, these
+    // include DNS packets, TCP handshake/termination packets, and HTTP header
+    // packets. These overheads were gathered from real samples and may not
+    // be perfectly accurate because of DNS caches and TCP retransmissions, etc.
+    private val CONSTANT_PACKET_OVERHEAD = 8
+
+    // 130 is an observed average.
+    private val CONSTANT_BYTES_OVERHEAD = 130 * CONSTANT_PACKET_OVERHEAD
+    private val TOLERANCE = 1.3
+
+    // Set up the packet bridge with two IPv6 address only test networks.
+    private val inst = InstrumentationRegistry.getInstrumentation()
+    private val context = inst.getContext()
+    private val packetBridge = runAsShell(MANAGE_TEST_NETWORKS) {
+        PacketBridge(
+            context,
+            listOf(LOCAL_V6ADDR),
+            REMOTE_V6ADDR.address,
+            listOf(
+                Pair(DNS_INTERNAL_SERVER_PORT, DNS_EXTERNAL_SERVER_PORT)
+            )
+        )
+    }
+    private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+
+    // Set up DNS server for testing server and DNS64.
+    private val fakeDns = TestDnsServer(
+        packetBridge.externalNetwork,
+        InetSocketAddress(LOCAL_V6ADDR.address, DNS_EXTERNAL_SERVER_PORT)
+    ).apply {
+        start()
+        setAnswer(
+            "ipv4only.arpa",
+            listOf(IpPrefix(REMOTE_V6ADDR.address, REMOTE_V6ADDR.prefixLength).address)
+        )
+        setAnswer(HTTP_SERVER_NAME, listOf(REMOTE_V4ADDR.address))
+    }
+
+    // Start up test http server.
+    private val httpServer = TestHttpServer(
+        LOCAL_V6ADDR.address.hostAddress,
+        HTTP_SERVER_PORT
+    ).apply {
+        start()
+    }
+
+    @Before
+    fun setUp() {
+        assumeTrue(shouldRunTests())
+        packetBridge.start()
+    }
+
+    // For networkstack tests, it is not guaranteed that the tethering module will be
+    // updated at the same time. If the tethering module is not new enough, it may not contain
+    // the necessary abilities to run these tests. For example, The tests depends on test
+    // network stats being counted, which can only be achieved when they are marked as TYPE_TEST.
+    // If the tethering module does not support TYPE_TEST stats, then these tests will need
+    // to be skipped.
+    fun shouldRunTests() = cm.getNetworkInfo(packetBridge.internalNetwork)!!.type == TYPE_TEST
+
+    @After
+    fun tearDown() {
+        packetBridge.stop()
+        fakeDns.stop()
+        httpServer.stop()
+    }
+
+    private fun waitFor464XlatReady(network: Network): String {
+        val iface = cm.getLinkProperties(network)!!.interfaceName!!
+
+        // Make a network request to listen to the specific test network.
+        val nr = NetworkRequest.Builder()
+            .clearCapabilities()
+            .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+            .setNetworkSpecifier(TestNetworkSpecifier(iface))
+            .build()
+        val testCb = TestableNetworkCallback()
+        cm.registerNetworkCallback(nr, testCb)
+
+        // Wait for the stacked address to be available.
+        testCb.eventuallyExpect<LinkPropertiesChanged> {
+            it.lp.stackedLinks.getOrNull(0)?.linkAddresses?.getOrNull(0) != null
+        }
+
+        return iface
+    }
+
+    private val Network.mtu: Int get() {
+        val lp = cm.getLinkProperties(this)!!
+        val mtuStacked = if (lp.stackedLinks[0]?.mtu != 0) lp.stackedLinks[0].mtu else DEFAULT_MTU
+        val mtuInterface = if (lp.mtu != 0) lp.mtu else DEFAULT_MTU
+        return mtuInterface.coerceAtMost(mtuStacked)
+    }
+
+    /**
+     * Verify data usage download stats with test 464xlat networks.
+     *
+     * This test starts two test networks and binds them together, the internal one is for the
+     * client to make http traffic on the test network, and the external one is for the mocked
+     * http and dns server to bind to and provide responses.
+     *
+     * After Clat setup, the client will use clat v4 address to send packets to the mocked
+     * server v4 address, which will be translated into a v6 packet by the clat daemon with
+     * NAT64 prefix learned from the mocked DNS64 response. And send to the interface.
+     *
+     * While the packets are being forwarded to the external interface, the servers will see
+     * the packets originated from the mocked v6 address, and destined to a local v6 address.
+     */
+    @Test
+    fun test464XlatTcpStats() {
+        // Wait for 464Xlat to be ready.
+        val internalInterfaceName = waitFor464XlatReady(packetBridge.internalNetwork)
+        val mtu = packetBridge.internalNetwork.mtu
+
+        val snapshotBeforeTest = StatsSnapshot(context, internalInterfaceName)
+
+        // Generate the download traffic.
+        genHttpTraffic(packetBridge.internalNetwork, uploadSize = 0L, TEST_DOWNLOAD_SIZE)
+
+        // 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,
+            mtu,
+            DOWNLOAD
+        )
+        assertOnlyNonTaggedStatsIncreases(
+            snapshotBeforeTest,
+            snapshotAfterDownload,
+            expectedDownloadLower,
+            expectedDownloadUpper
+        )
+
+        // Generate upload traffic with tag to verify tagged data accounting as well.
+        genHttpTrafficWithTag(
+            packetBridge.internalNetwork,
+            TEST_UPLOAD_SIZE,
+            downloadSize = 0L,
+            TEST_TAG
+        )
+
+        // Verify upload data usage accounting.
+        if (SdkLevel.isAtLeastV()) {
+            TrafficStats.clearRateLimitCaches()
+        }
+        val snapshotAfterUpload = StatsSnapshot(context, internalInterfaceName)
+        val (expectedUploadLower, expectedUploadUpper) = getExpectedStatsBounds(
+            TEST_UPLOAD_SIZE,
+            mtu,
+            UPLOAD
+        )
+        assertAllStatsIncreases(
+            snapshotAfterDownload,
+            snapshotAfterUpload,
+            expectedUploadLower,
+            expectedUploadUpper
+        )
+    }
+
+    private enum class Direction {
+        DOWNLOAD,
+        UPLOAD
+    }
+
+    private fun getExpectedStatsBounds(
+        transmittedSize: Long,
+        mtu: Int,
+        direction: Direction
+    ): Pair<BareStats, BareStats> {
+        // This is already an underestimated value since the input doesn't include TCP/IP
+        // layer overhead.
+        val txBytesLower = transmittedSize
+        // Include TCP/IP header overheads and retransmissions in the upper bound.
+        val txBytesUpper = (transmittedSize * TOLERANCE).toLong()
+        val txPacketsLower = txBytesLower / mtu + (CONSTANT_PACKET_OVERHEAD / TOLERANCE).toLong()
+        val estTransmissionPacketsUpper = ceil(txBytesUpper / mtu.toDouble()).toLong()
+        val txPacketsUpper = estTransmissionPacketsUpper +
+                (CONSTANT_PACKET_OVERHEAD * TOLERANCE).toLong()
+        // Assume ACK only sent once for the entire transmission.
+        val rxPacketsLower = 1L + (CONSTANT_PACKET_OVERHEAD / TOLERANCE).toLong()
+        // Assume ACK sent for every RX packet.
+        val rxPacketsUpper = txPacketsUpper
+        val rxBytesLower = 1L * TCP_ACK_SIZE + (CONSTANT_BYTES_OVERHEAD / TOLERANCE).toLong()
+        val rxBytesUpper = estTransmissionPacketsUpper * TCP_ACK_SIZE +
+                (CONSTANT_BYTES_OVERHEAD * TOLERANCE).toLong()
+
+        return if (direction == UPLOAD) {
+            BareStats(rxBytesLower, rxPacketsLower, txBytesLower, txPacketsLower) to
+                    BareStats(rxBytesUpper, rxPacketsUpper, txBytesUpper, txPacketsUpper)
+        } else {
+            BareStats(txBytesLower, txPacketsLower, rxBytesLower, rxPacketsLower) to
+                    BareStats(txBytesUpper, txPacketsUpper, rxBytesUpper, rxPacketsUpper)
+        }
+    }
+
+    private fun genHttpTraffic(network: Network, uploadSize: Long, downloadSize: Long) =
+        genHttpTrafficWithTag(network, uploadSize, downloadSize, NetworkStats.Bucket.TAG_NONE)
+
+    private fun genHttpTrafficWithTag(
+        network: Network,
+        uploadSize: Long,
+        downloadSize: Long,
+        tag: Int
+    ) {
+        val path = "/test_upload_download"
+        val buf = ByteArray(DEFAULT_BUFFER_SIZE)
+
+        httpServer.addResponse(
+            TestHttpServer.Request(path, NanoHTTPD.Method.POST),
+            NanoHTTPD.Response.Status.OK,
+            content = getRandomString(downloadSize)
+        )
+        var httpConnection: HttpURLConnection? = null
+        try {
+            TrafficStats.setThreadStatsTag(tag)
+            val spec = "http://$HTTP_SERVER_NAME:${httpServer.listeningPort}$path"
+            val url = URL(spec)
+            httpConnection = network.openConnection(url) as HttpURLConnection
+            httpConnection.connectTimeout = CONNECTION_TIMEOUT_MILLIS
+            httpConnection.requestMethod = "POST"
+            httpConnection.doOutput = true
+            // Tell the server that the response should not be compressed. Otherwise, the data usage
+            // accounted will be less than expected.
+            httpConnection.setRequestProperty("Accept-Encoding", "identity")
+            // Tell the server that to close connection after this request, this is needed to
+            // prevent from reusing the same socket that has different tagging requirement.
+            httpConnection.setRequestProperty("Connection", "close")
+
+            // Send http body.
+            val outputStream = BufferedOutputStream(httpConnection.outputStream)
+            outputStream.write(getRandomString(uploadSize).toByteArray(Charset.forName("UTF-8")))
+            outputStream.close()
+            assertEquals(HTTP_OK, httpConnection.responseCode)
+
+            // Receive response from the server.
+            val inputStream = BufferedInputStream(httpConnection.getInputStream())
+            var total = 0L
+            while (true) {
+                val count = inputStream.read(buf)
+                if (count == -1) break // End-of-Stream
+                total += count
+            }
+            assertEquals(downloadSize, total)
+        } finally {
+            httpConnection?.inputStream?.close()
+            TrafficStats.clearThreadStatsTag()
+        }
+    }
+
+    // NetworkStats.Bucket cannot be written. So another class is needed to
+    // perform arithmetic operations.
+    data class BareStats(
+        val rxBytes: Long,
+        val rxPackets: Long,
+        val txBytes: Long,
+        val txPackets: Long
+    ) {
+        operator fun plus(other: BareStats): BareStats {
+            return BareStats(
+                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
+            )
+        }
+
+        fun reverse(): BareStats =
+            BareStats(
+                rxBytes = txBytes,
+                rxPackets = txPackets,
+                txBytes = rxBytes,
+                txPackets = rxPackets
+            )
+
+        override fun toString(): String {
+            return "BareStats{rx/txBytes=$rxBytes/$txBytes, rx/txPackets=$rxPackets/$txPackets}"
+        }
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is BareStats) return false
+
+            if (rxBytes != other.rxBytes) return false
+            if (rxPackets != other.rxPackets) return false
+            if (txBytes != other.txBytes) return false
+            if (txPackets != other.txPackets) return false
+
+            return true
+        }
+
+        override fun hashCode(): Int {
+            return (rxBytes * 11 + rxPackets * 13 + txBytes * 17 + txPackets * 19).toInt()
+        }
+
+        companion object {
+            val EMPTY = BareStats(0L, 0L, 0L, 0L)
+        }
+    }
+
+    data class StatsSnapshot(val context: Context, val iface: String) {
+        val statsSummary = getNetworkSummary(iface)
+        val statsUid = getUidDetail(iface, TAG_NONE)
+        val taggedSummary = getTaggedNetworkSummary(iface, TEST_TAG)
+        val taggedUid = getUidDetail(iface, TEST_TAG)
+        val trafficStatsIface = getTrafficStatsIface(iface)
+        val trafficStatsUid = getTrafficStatsUid(Process.myUid())
+
+        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
+                )
+            }
+        }
+
+        private fun getNetworkSummary(iface: String): BareStats {
+            return getNetworkStatsThat(iface, TAG_NONE) { nsm, template ->
+                nsm.querySummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+            }
+        }
+
+        private fun getTaggedNetworkSummary(iface: String, tag: Int): BareStats {
+            return getNetworkStatsThat(iface, tag) { nsm, template ->
+                nsm.queryTaggedSummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+            }
+        }
+
+        private fun getNetworkStatsThat(
+            iface: String,
+            tag: Int,
+            queryApi: (nsm: NetworkStatsManager, template: NetworkTemplate) -> NetworkStats
+        ): BareStats {
+            val nsm = context.getSystemService(NetworkStatsManager::class.java)!!
+            nsm.forceUpdate()
+            val testTemplate = NetworkTemplate.Builder(MATCH_TEST)
+                .setWifiNetworkKeys(setOf(iface)).build()
+            val stats = queryApi.invoke(nsm, testTemplate)
+            val filteredBuckets =
+                stats.buckets().filter { it.uid == Process.myUid() && it.tag == tag }
+            return filteredBuckets.fold(BareStats.EMPTY) { acc, it ->
+                acc + BareStats(
+                    it.rxBytes,
+                    it.rxPackets,
+                    it.txBytes,
+                    it.txPackets
+                )
+            }
+        }
+
+        // Helper function to iterate buckets in app.usage.NetworkStats.
+        private fun NetworkStats.buckets() = object : Iterable<NetworkStats.Bucket> {
+            override fun iterator() = object : Iterator<NetworkStats.Bucket> {
+                override operator fun hasNext() = hasNextBucket()
+                override operator fun next() =
+                    NetworkStats.Bucket().also { assertTrue(getNextBucket(it)) }
+            }
+        }
+
+        private fun getTrafficStatsIface(iface: String): BareStats = BareStats(
+            TrafficStats.getRxBytes(iface),
+            TrafficStats.getRxPackets(iface),
+            TrafficStats.getTxBytes(iface),
+            TrafficStats.getTxPackets(iface)
+        )
+
+        private fun getTrafficStatsUid(uid: Int): BareStats = BareStats(
+            TrafficStats.getUidRxBytes(uid),
+            TrafficStats.getUidRxPackets(uid),
+            TrafficStats.getUidTxBytes(uid),
+            TrafficStats.getUidTxPackets(uid)
+        )
+    }
+
+    private fun assertAllStatsIncreases(
+        before: StatsSnapshot,
+        after: StatsSnapshot,
+        lower: BareStats,
+        upper: BareStats
+    ) {
+        assertNonTaggedStatsIncreases(before, after, lower, upper)
+        assertTaggedStatsIncreases(before, after, lower, upper)
+    }
+
+    private fun assertOnlyNonTaggedStatsIncreases(
+        before: StatsSnapshot,
+        after: StatsSnapshot,
+        lower: BareStats,
+        upper: BareStats
+    ) {
+        assertNonTaggedStatsIncreases(before, after, lower, upper)
+        assertTaggedStatsEquals(before, after)
+    }
+
+    private fun assertNonTaggedStatsIncreases(
+        before: StatsSnapshot,
+        after: StatsSnapshot,
+        lower: BareStats,
+        upper: BareStats
+    ) {
+        assertInRange(
+            "Unexpected iface traffic stats",
+            after.iface,
+            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()
+        )
+        assertInRange(
+            "Unexpected non-tagged summary stats",
+            after.iface,
+            before.statsSummary,
+            after.statsSummary,
+            lower,
+            upper
+        )
+        assertInRange(
+            "Unexpected non-tagged uid stats",
+            after.iface,
+            before.statsUid,
+            after.statsUid,
+            lower,
+            upper
+        )
+    }
+
+    private fun assertTaggedStatsEquals(before: StatsSnapshot, after: StatsSnapshot) {
+        // Increment of tagged data should be zero since no tagged traffic was generated.
+        assertEquals(
+            before.taggedSummary,
+            after.taggedSummary,
+            "Unexpected tagged summary stats: ${after.iface}"
+        )
+        assertEquals(
+            before.taggedUid,
+            after.taggedUid,
+            "Unexpected tagged uid stats: ${Process.myUid()} on ${after.iface}"
+        )
+    }
+
+    private fun assertTaggedStatsIncreases(
+        before: StatsSnapshot,
+        after: StatsSnapshot,
+        lower: BareStats,
+        upper: BareStats
+    ) {
+        assertInRange(
+            "Unexpected tagged summary stats",
+            after.iface,
+            before.taggedSummary,
+            after.taggedSummary,
+            lower,
+            upper
+        )
+        assertInRange(
+            "Unexpected tagged uid stats: ${Process.myUid()}",
+            after.iface,
+            before.taggedUid,
+            after.taggedUid,
+            lower,
+            upper
+        )
+    }
+
+    /** Verify the given BareStats is in range [lower, upper] */
+    private fun assertInRange(
+        tag: String,
+        iface: String,
+        before: BareStats,
+        after: BareStats,
+        lower: BareStats,
+        upper: BareStats
+    ) {
+        // Passing the value after operation and the value before operation to dump the actual
+        // numbers if it fails.
+        assertTrue(
+            checkInRange(before, after, lower, upper),
+            "$tag on $iface: $after - $before is not within range [$lower, $upper]"
+        )
+    }
+
+    private fun checkInRange(
+            before: BareStats,
+            after: BareStats,
+            lower: BareStats,
+            upper: BareStats
+    ): Boolean {
+        val value = after - before
+        return value.rxBytes in lower.rxBytes..upper.rxBytes &&
+                value.rxPackets in lower.rxPackets..upper.rxPackets &&
+                value.txBytes in lower.txBytes..upper.txBytes &&
+                value.txPackets in lower.txPackets..upper.txPackets
+    }
+
+    fun getRandomString(length: Long): String {
+        val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
+        return (1..length)
+            .map { allowedChars.random() }
+            .joinToString("")
+    }
+}
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 361c968..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)
     }
 
     /**
@@ -69,7 +74,8 @@
         url: URL,
         private val response: HttpResponse
     ) : HttpURLConnection(url) {
-        private val responseBytes = response.content.toByteArray(StandardCharsets.UTF_8)
+        private val responseBytes = checkNotNull(response.content)
+            .toByteArray(StandardCharsets.UTF_8)
         override fun getResponseCode() = response.responseCode
         override fun getContentLengthLong() = responseBytes.size.toLong()
         override fun getHeaderField(field: String): String? {
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index 28edcb2..960c6ca 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+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_TEST;
@@ -35,6 +36,7 @@
 import static org.junit.Assert.fail;
 
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.LinkProperties;
@@ -50,6 +52,7 @@
 import android.os.ConditionVariable;
 import android.os.HandlerThread;
 import android.os.Message;
+import android.util.CloseGuard;
 import android.util.Log;
 import android.util.Range;
 
@@ -64,8 +67,14 @@
 import java.util.function.Consumer;
 
 public class NetworkAgentWrapper implements TestableNetworkCallback.HasNetwork {
+    private static final long DESTROY_TIMEOUT_MS = 10_000L;
+
+    // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+    // please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more
+    // tools in ConnectivityServiceTest.
     private final NetworkCapabilities mNetworkCapabilities;
     private final HandlerThread mHandlerThread;
+    private final CloseGuard mCloseGuard;
     private final Context mContext;
     private final String mLogTag;
     private final NetworkAgentConfig mNetworkAgentConfig;
@@ -120,6 +129,10 @@
         mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
         mNetworkCapabilities.addTransportType(transport);
         switch (transport) {
+            case TRANSPORT_BLUETOOTH:
+                // Score for Wear companion proxy network; not BLUETOOTH tethering.
+                mScore = new NetworkScore.Builder().setLegacyInt(100).build();
+                break;
             case TRANSPORT_ETHERNET:
                 mScore = new NetworkScore.Builder().setLegacyInt(70).build();
                 break;
@@ -149,6 +162,8 @@
         mLogTag = "Mock-" + typeName;
         mHandlerThread = new HandlerThread(mLogTag);
         mHandlerThread.start();
+        mCloseGuard = new CloseGuard();
+        mCloseGuard.open("destroy");
 
         // extraInfo is set to "" by default in NetworkAgentConfig.
         final String extraInfo = (transport == TRANSPORT_CELLULAR) ? "internet.apn" : "";
@@ -351,6 +366,35 @@
         mNetworkAgent.unregister();
     }
 
+    /**
+     * Destroy the network agent and stop its looper.
+     *
+     * <p>This must always be called.
+     */
+    public void destroy() {
+        mHandlerThread.quitSafely();
+        try {
+            mHandlerThread.join(DESTROY_TIMEOUT_MS);
+        } catch (InterruptedException e) {
+            Log.e(mLogTag, "Interrupted when waiting for handler thread on destroy", e);
+        }
+        mCloseGuard.close();
+    }
+
+    @SuppressLint("Finalize") // Follows the recommended pattern for CloseGuard
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            // Note that mCloseGuard could be null if the constructor threw.
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            destroy();
+        } finally {
+            super.finalize();
+        }
+    }
+
     @Override
     public Network getNetwork() {
         return mNetworkAgent.getNetwork();
@@ -468,4 +512,8 @@
     public boolean isBypassableVpn() {
         return mNetworkAgentConfig.isBypassableVpn();
     }
+
+    // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+    // please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more
+    // tools in ConnectivityServiceTest.
 }
diff --git a/tests/mts/Android.bp b/tests/mts/Android.bp
index 74fee3d..c118d0a 100644
--- a/tests/mts/Android.bp
+++ b/tests/mts/Android.bp
@@ -14,6 +14,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -30,6 +31,8 @@
     header_libs: [
         "bpf_headers",
     ],
+    version_script: ":connectivity_mainline_test_map",
+    stl: "libc++_static",
     static_libs: [
         "libbase",
         "libmodules-utils-build",
@@ -38,5 +41,5 @@
         "bpf_existence_test.cpp",
     ],
     compile_multilib: "first",
-    min_sdk_version: "29",  // Ensure test runs on Q and above.
+    min_sdk_version: "30", // Ensure test runs on R and above.
 }
diff --git a/tests/mts/OWNERS b/tests/mts/OWNERS
new file mode 100644
index 0000000..3101da5
--- /dev/null
+++ b/tests/mts/OWNERS
@@ -0,0 +1,2 @@
+# Bug template url: http://b/new?component=31808
+# TODO: move bug template config to common owners file once b/226427845 is resolved
\ No newline at end of file
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index c294e7b..29f5cd2 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -33,11 +33,14 @@
 using android::modules::sdklevel::IsAtLeastR;
 using android::modules::sdklevel::IsAtLeastS;
 using android::modules::sdklevel::IsAtLeastT;
+using android::modules::sdklevel::IsAtLeastU;
+using android::modules::sdklevel::IsAtLeastV;
 
 #define PLATFORM "/sys/fs/bpf/"
 #define TETHERING "/sys/fs/bpf/tethering/"
 #define PRIVATE "/sys/fs/bpf/net_private/"
 #define SHARED "/sys/fs/bpf/net_shared/"
+#define NETD_RO "/sys/fs/bpf/netd_readonly/"
 #define NETD "/sys/fs/bpf/netd_shared/"
 
 class BpfExistenceTest : public ::testing::Test {
@@ -65,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",
@@ -91,8 +96,10 @@
     NETD "map_netd_app_uid_stats_map",
     NETD "map_netd_configuration_map",
     NETD "map_netd_cookie_tag_map",
+    NETD "map_netd_data_saver_enabled_map",
     NETD "map_netd_iface_index_name_map",
     NETD "map_netd_iface_stats_map",
+    NETD "map_netd_ingress_discard_map",
     NETD "map_netd_stats_map_A",
     NETD "map_netd_stats_map_B",
     NETD "map_netd_uid_counterset_map",
@@ -116,9 +123,9 @@
 };
 
 // Provided by *current* mainline module for T+ devices with 5.4+ kernels
-static const set<string> MAINLINE_FOR_T_5_4_PLUS = {
-    SHARED "prog_block_bind4_block_port",
-    SHARED "prog_block_bind6_block_port",
+static const set<string> MAINLINE_FOR_T_4_19_PLUS = {
+    NETD_RO "prog_block_bind4_block_port",
+    NETD_RO "prog_block_bind6_block_port",
 };
 
 // Provided by *current* mainline module for T+ devices with 5.15+ kernels
@@ -126,6 +133,37 @@
     SHARED "prog_dscpPolicy_schedcls_set_dscp_ether",
 };
 
+// Provided by *current* mainline module for U+ devices
+static const set<string> MAINLINE_FOR_U_PLUS = {
+    NETD "map_netd_packet_trace_enabled_map",
+};
+
+// Provided by *current* mainline module for U+ devices with 5.10+ kernels
+static const set<string> MAINLINE_FOR_U_5_10_PLUS = {
+    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());
 }
@@ -147,10 +185,14 @@
     // so we should only test for the removal of stuff that was mainline'd,
     // and for the presence of mainline stuff.
 
+    // Note: Q is no longer supported by mainline
+    ASSERT_TRUE(IsAtLeastR());
+
     // R can potentially run on pre-4.9 kernel non-eBPF capable devices.
     DO_EXPECT(IsAtLeastR() && !IsAtLeastS() && isAtLeastKernelVersion(4, 9, 0), PLATFORM_ONLY_IN_R);
 
     // S requires Linux Kernel 4.9+ and thus requires eBPF support.
+    if (IsAtLeastS()) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
     DO_EXPECT(IsAtLeastS(), MAINLINE_FOR_S_PLUS);
     DO_EXPECT(IsAtLeastS() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_S_5_10_PLUS);
 
@@ -159,10 +201,19 @@
     // T still only requires Linux Kernel 4.9+.
     DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
     DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
-    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_T_5_4_PLUS);
+    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
     DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
 
     // U requires Linux Kernel 4.14+, but nothing (as yet) added or removed in U.
+    if (IsAtLeastU()) ASSERT_TRUE(isAtLeastKernelVersion(4, 14, 0));
+    DO_EXPECT(IsAtLeastU(), MAINLINE_FOR_U_PLUS);
+    DO_EXPECT(IsAtLeastU() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_U_5_10_PLUS);
+
+    // 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 8825aa4..c5088c6 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -16,8 +17,9 @@
         "connectivity_native_test.cpp",
     ],
     header_libs: ["bpf_connectivity_headers"],
+    version_script: ":connectivity_mainline_test_map",
+    stl: "libc++_static",
     shared_libs: [
-        "libbase",
         "libbinder_ndk",
         "liblog",
         "libnetutils",
@@ -25,6 +27,7 @@
     ],
     static_libs: [
         "connectivity_native_aidl_interface-lateststable-ndk",
+        "libbase",
         "libcutils",
         "libmodules-utils-build",
         "libutils",
diff --git a/tests/native/connectivity_native_test/OWNERS b/tests/native/connectivity_native_test/OWNERS
index 8dfa455..c9bfc40 100644
--- a/tests/native/connectivity_native_test/OWNERS
+++ b/tests/native/connectivity_native_test/OWNERS
@@ -1,3 +1,4 @@
-# Bug component: 31808
+# Bug template url: http://b/new?component=31808
+# TODO: move bug template config to common owners file once b/226427845 is resolved
 set noparent
-file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking_xts
+file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking_xts
diff --git a/tests/native/connectivity_native_test/connectivity_native_test.cpp b/tests/native/connectivity_native_test/connectivity_native_test.cpp
index 27a9d35..f62a30b 100644
--- a/tests/native/connectivity_native_test/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test/connectivity_native_test.cpp
@@ -41,13 +41,14 @@
 
     void SetUp() override {
         restoreBlockedPorts = false;
+
         // Skip test case if not on U.
-        if (!android::modules::sdklevel::IsAtLeastU()) GTEST_SKIP() <<
-                "Should be at least T device.";
+        if (!android::modules::sdklevel::IsAtLeastU())
+            GTEST_SKIP() << "Should be at least U device.";
 
         // Skip test case if not on 5.4 kernel which is required by bpf prog.
-        if (!android::bpf::isAtLeastKernelVersion(5, 4, 0)) GTEST_SKIP() <<
-                "Kernel should be at least 5.4.";
+        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.
diff --git a/tests/native/utilities/Android.bp b/tests/native/utilities/Android.bp
index 4706b3d..48a5414 100644
--- a/tests/native/utilities/Android.bp
+++ b/tests/native/utilities/Android.bp
@@ -14,14 +14,17 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+// TODO: delete this as it is a cross-module api boundary violation
 cc_test_library {
     name: "libconnectivity_native_test_utils",
+    visibility: ["//packages/modules/DnsResolver/tests:__subpackages__"],
     defaults: [
         "netd_defaults",
-        "resolv_test_defaults"
+        "resolv_test_defaults",
     ],
     srcs: [
         "firewall.cpp",
diff --git a/tests/native/utilities/firewall.cpp b/tests/native/utilities/firewall.cpp
index e4669cb..34b4f07 100644
--- a/tests/native/utilities/firewall.cpp
+++ b/tests/native/utilities/firewall.cpp
@@ -27,6 +27,12 @@
 
     result = mUidOwnerMap.init(UID_OWNER_MAP_PATH);
     EXPECT_RESULT_OK(result) << "init mUidOwnerMap failed";
+
+    // Do not check whether DATA_SAVER_ENABLED_MAP_PATH init succeeded or failed since the map is
+    // defined in tethering module, but the user of this class may be in other modules. For example,
+    // DNS resolver tests statically link to this class. But when running MTS, the test infra
+    // installs only DNS resolver module without installing tethering module together.
+    mDataSaverEnabledMap.init(DATA_SAVER_ENABLED_MAP_PATH);
 }
 
 Firewall* Firewall::getInstance() {
@@ -53,9 +59,11 @@
 Result<void> Firewall::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
     // iif should be non-zero if and only if match == MATCH_IIF
     if (match == IIF_MATCH && iif == 0) {
-        return Errorf("Interface match {} must have nonzero interface index", match);
+        return Errorf("Interface match {} must have nonzero interface index",
+                      static_cast<uint32_t>(match));
     } else if (match != IIF_MATCH && iif != 0) {
-        return Errorf("Non-interface match {} must have zero interface index", match);
+        return Errorf("Non-interface match {} must have zero interface index",
+                      static_cast<uint32_t>(match));
     }
 
     std::lock_guard guard(mMutex);
@@ -63,14 +71,14 @@
     if (oldMatch.ok()) {
         UidOwnerValue newMatch = {
                 .iif = iif ? iif : oldMatch.value().iif,
-                .rule = static_cast<uint8_t>(oldMatch.value().rule | match),
+                .rule = oldMatch.value().rule | match,
         };
         auto res = mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY);
         if (!res.ok()) return Errorf("Failed to update rule: {}", res.error().message());
     } else {
         UidOwnerValue newMatch = {
                 .iif = iif,
-                .rule = static_cast<uint8_t>(match),
+                .rule = match,
         };
         auto res = mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY);
         if (!res.ok()) return Errorf("Failed to add rule: {}", res.error().message());
@@ -85,7 +93,7 @@
 
     UidOwnerValue newMatch = {
             .iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
-            .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match),
+            .rule = oldMatch.value().rule & ~match,
     };
     if (newMatch.rule == 0) {
         auto res = mUidOwnerMap.deleteValue(uid);
@@ -116,3 +124,28 @@
     }
     return {};
 }
+
+Result<bool> Firewall::getDataSaverSetting() {
+    std::lock_guard guard(mMutex);
+    if (!mDataSaverEnabledMap.isValid()) {
+        return Errorf("init mDataSaverEnabledMap failed");
+    }
+
+    auto dataSaverSetting = mDataSaverEnabledMap.readValue(DATA_SAVER_ENABLED_KEY);
+    if (!dataSaverSetting.ok()) {
+        return Errorf("Cannot read the data saver setting: {}", dataSaverSetting.error().message());
+    }
+    return dataSaverSetting;
+}
+
+Result<void> Firewall::setDataSaver(bool enabled) {
+    std::lock_guard guard(mMutex);
+    if (!mDataSaverEnabledMap.isValid()) {
+        return Errorf("init mDataSaverEnabledMap failed");
+    }
+
+    auto res = mDataSaverEnabledMap.writeValue(DATA_SAVER_ENABLED_KEY, enabled, BPF_EXIST);
+    if (!res.ok()) return Errorf("Failed to set data saver: {}", res.error().message());
+
+    return {};
+}
diff --git a/tests/native/utilities/firewall.h b/tests/native/utilities/firewall.h
index 1e7e987..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"
 
@@ -33,9 +34,11 @@
     Result<void> removeRule(uint32_t uid, UidOwnerMatchType match) EXCLUDES(mMutex);
     Result<void> addUidInterfaceRules(const std::string& ifName, const std::vector<int32_t>& uids);
     Result<void> removeUidInterfaceRules(const std::vector<int32_t>& uids);
-
+    Result<bool> getDataSaverSetting();
+    Result<void> setDataSaver(bool enabled);
   private:
     BpfMap<uint32_t, uint32_t> mConfigurationMap GUARDED_BY(mMutex);
     BpfMap<uint32_t, UidOwnerValue> mUidOwnerMap GUARDED_BY(mMutex);
+    BpfMap<uint32_t, bool> mDataSaverEnabledMap GUARDED_BY(mMutex);
     std::mutex mMutex;
 };
diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp
index 4ab24fc..121efa1 100644
--- a/tests/smoketest/Android.bp
+++ b/tests/smoketest/Android.bp
@@ -10,6 +10,7 @@
 // TODO: remove this hack when there is a better solution for jni_libs that includes
 // dependent libraries.
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 8b286a0..ef3ebb0 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -2,6 +2,7 @@
 // Build FrameworksNetTests package
 //########################################################################
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
     // all of the 'license_kinds' from "Android-Apache-2.0"
@@ -57,34 +58,18 @@
 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/VpnManagerServiceTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
         "java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
         "java/com/android/server/connectivity/MetricsTestUtil.java",
         "java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
         "java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
-        "java/com/android/server/connectivity/VpnTest.java",
         "java/com/android/server/net/ipmemorystore/*.java",
-    ]
-}
-
-// 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 {
@@ -107,15 +92,15 @@
         "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"
+        "cts-net-utils",
     ],
     libs: [
         "android.net.ipsec.ike.stubs.module_lib",
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 5d4bdf7..2853f31 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -49,6 +49,9 @@
     <uses-permission android:name="android.permission.NETWORK_FACTORY" />
     <uses-permission android:name="android.permission.NETWORK_STATS_PROVIDER" />
     <uses-permission android:name="android.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE" />
+    <!-- Workaround for flakes where the launcher package is not found despite the <queries> tag
+         below (b/286550950). -->
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <!-- Declare the intent that the test intends to query. This is necessary for
          UiDevice.getLauncherPackageName which is used in NetworkNotificationManagerTest
diff --git a/tests/unit/OWNERS b/tests/unit/OWNERS
new file mode 100644
index 0000000..3101da5
--- /dev/null
+++ b/tests/unit/OWNERS
@@ -0,0 +1,2 @@
+# Bug template url: http://b/new?component=31808
+# TODO: move bug template config to common owners file once b/226427845 is resolved
\ No newline at end of file
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index 45a9dbc..9a77c89 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -51,6 +51,8 @@
 import static org.mockito.Mockito.after;
 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;
@@ -71,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;
@@ -78,9 +81,11 @@
 import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
+import org.junit.Rule;
 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;
 
@@ -90,11 +95,14 @@
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
 public class ConnectivityManagerTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
     private static final int TIMEOUT_MS = 30_000;
     private static final int SHORT_TIMEOUT_MS = 150;
 
     @Mock Context mCtx;
     @Mock IConnectivityManager mService;
+    @Mock NetworkPolicyManager mNpm;
 
     @Before
     public void setUp() {
@@ -235,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
@@ -264,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
@@ -282,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
@@ -306,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);
@@ -398,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());
@@ -414,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);
     }
 
@@ -510,4 +518,155 @@
         assertNull("ConnectivityManager weak reference still not null after " + attempts
                     + " attempts", ref.get());
     }
+
+    @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);
+
+        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/KeepalivePacketDataUtilTest.java b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
index 6afa4e9..7e245dc 100644
--- a/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
+++ b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
@@ -168,12 +168,6 @@
         assertEquals(resultData.rcvWndScale, wndScale);
         assertEquals(resultData.tos, tos);
         assertEquals(resultData.ttl, ttl);
-
-        final String expected = TcpKeepalivePacketDataParcelable.class.getName()
-                + "{srcAddress: [10, 0, 0, 1],"
-                + " srcPort: 1234, dstAddress: [10, 0, 0, 5], dstPort: 4321, seq: 286331153,"
-                + " ack: 572662306, rcvWnd: 48000, rcvWndScale: 2, tos: 4, ttl: 64}";
-        assertEquals(expected, resultData.toString());
     }
 
     @Test
diff --git a/tests/unit/java/android/net/MulticastRoutingConfigTest.kt b/tests/unit/java/android/net/MulticastRoutingConfigTest.kt
new file mode 100644
index 0000000..f01057b
--- /dev/null
+++ b/tests/unit/java/android/net/MulticastRoutingConfigTest.kt
@@ -0,0 +1,99 @@
+/*
+ * 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 android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.Inet6Address
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+import android.net.MulticastRoutingConfig.Builder
+import android.net.MulticastRoutingConfig.FORWARD_NONE
+import android.net.MulticastRoutingConfig.FORWARD_SELECTED
+import android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class MulticastRoutingConfigTest {
+
+    val address1 = Inet6Address.getByName("2000::8888") as Inet6Address
+    val address2 = Inet6Address.getByName("2000::9999") as Inet6Address
+
+    private fun configNone() = Builder(FORWARD_NONE).build()
+    private fun configMinScope(scope: Int) = Builder(FORWARD_WITH_MIN_SCOPE, scope).build()
+    private fun configSelected() = Builder(FORWARD_SELECTED).build()
+    private fun configSelectedWithAddress1AndAddress2() =
+            Builder(FORWARD_SELECTED).addListeningAddress(address1)
+            .addListeningAddress(address2).build()
+    private fun configSelectedWithAddress2AndAddress1() =
+            Builder(FORWARD_SELECTED).addListeningAddress(address2)
+            .addListeningAddress(address1).build()
+
+    @Test
+    fun equalityTests() {
+
+        assertTrue(configNone().equals(configNone()))
+
+        assertTrue(configSelected().equals(configSelected()))
+
+        assertTrue(configMinScope(4).equals(configMinScope(4)))
+
+        assertTrue(configSelectedWithAddress1AndAddress2()
+                .equals(configSelectedWithAddress2AndAddress1()))
+    }
+
+    @Test
+    fun inequalityTests() {
+
+        assertFalse(configNone().equals(configSelected()))
+
+        assertFalse(configNone().equals(configMinScope(4)))
+
+        assertFalse(configSelected().equals(configMinScope(4)))
+
+        assertFalse(configMinScope(4).equals(configMinScope(5)))
+
+        assertFalse(configSelected().equals(configSelectedWithAddress1AndAddress2()))
+    }
+
+    @Test
+    fun toString_equalObjects_returnsEqualStrings() {
+        val config1 = configSelectedWithAddress1AndAddress2()
+        val config2 = configSelectedWithAddress2AndAddress1()
+
+        val str1 = config1.toString()
+        val str2 = config2.toString()
+
+        assertTrue(str1.equals(str2))
+    }
+
+    @Test
+    fun toString_unequalObjects_returnsUnequalStrings() {
+        val config1 = configSelected()
+        val config2 = configSelectedWithAddress1AndAddress2()
+
+        val str1 = config1.toString()
+        val str2 = config2.toString()
+
+        assertFalse(str1.equals(str2))
+    }
+}
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/NetworkStackBpfNetMapsTest.kt b/tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
new file mode 100644
index 0000000..b5d78f3
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
@@ -0,0 +1,280 @@
+/*
+ * 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 android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED
+import android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED
+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_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
+import com.android.net.module.util.Struct.U8
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestBpfMap
+import java.lang.reflect.Modifier
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+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
+
+// NetworkStack can not use this before U due to b/326143935
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(VERSION_CODES.TIRAMISU)
+class NetworkStackBpfNetMapsTest {
+    @Rule
+    @JvmField
+    val ignoreRule = DevSdkIgnoreRule()
+
+    private val testConfigurationMap: IBpfMap<S32, U32> = TestBpfMap()
+    private val testUidOwnerMap: IBpfMap<S32, UidOwnerValue> = TestBpfMap()
+    private val testDataSaverEnabledMap: IBpfMap<S32, U8> = TestBpfMap()
+    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>
+    ) : NetworkStackBpfNetMaps.Dependencies() {
+        override fun getConfigurationMap() = configMap
+        override fun getUidOwnerMap() = uidOwnerMap
+        override fun getDataSaverEnabledMap() = dataSaverEnabledMap
+    }
+
+    private fun doTestIsChainEnabled(chain: Int) {
+        testConfigurationMap.updateEntry(
+            UID_RULES_CONFIGURATION_KEY,
+            U32(getMatchByFirewallChain(chain))
+        )
+        assertTrue(bpfNetMapsReader.isChainEnabled(chain))
+        testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
+        assertFalse(bpfNetMapsReader.isChainEnabled(chain))
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testIsChainEnabled() {
+        doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE)
+        doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_STANDBY)
+        doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_POWERSAVE)
+        doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_RESTRICTED)
+        doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY)
+    }
+
+    @Test
+    fun testFirewallChainList() {
+        // Verify that when a firewall chain constant is added, it should also be included in
+        // firewall chain list.
+        val declaredChains = ConnectivityManager::class.java.declaredFields.filter {
+            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 +
+                        BpfNetMapsConstants.METERED_ALLOW_CHAINS.size +
+                        BpfNetMapsConstants.METERED_DENY_CHAINS.size,
+            declaredChains.size
+        )
+        declaredChains.forEach {
+            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))
+            )
+        }
+    }
+
+    private fun mockChainEnabled(chain: Int, enabled: Boolean) {
+        val config = testConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).`val`
+        val newConfig = if (enabled) {
+            config or getMatchByFirewallChain(chain)
+        } else {
+            config and getMatchByFirewallChain(chain).inv()
+        }
+        testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(newConfig))
+    }
+
+    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))
+
+        // Enable dozable chain but does not provide allowed list. Verify the network is blocked
+        // for all uids.
+        mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, true)
+        assertTrue(isUidNetworkingBlocked(TEST_UID1))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2))
+
+        // Add uid1 to dozable allowed list. Verify the network is not blocked for uid1, while
+        // uid2 is blocked.
+        testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, DOZABLE_MATCH))
+        assertFalse(isUidNetworkingBlocked(TEST_UID1))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2))
+    }
+
+    @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))
+        mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_STANDBY, true)
+        assertFalse(isUidNetworkingBlocked(TEST_UID1))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2))
+
+        // Add uid1 to standby allowed list. Verify the network is blocked for uid1, while
+        // uid2 is not blocked.
+        testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, STANDBY_MATCH))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2))
+    }
+
+    @Test
+    fun testIsUidNetworkingBlockedByFirewallChains_blockedWithAllowed() {
+        // Uids blocked by powersave chain but allowed by standby chain, verify the blocking
+        // takes higher priority.
+        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_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))
+        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_USER_MATCH or HAPPY_BOX_MATCH)
+        )
+        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))
+        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))
+
+        // Make the network non-metered, nothing is blocked.
+        assertFalse(isUidNetworkingBlocked(TEST_UID1))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2))
+        assertFalse(isUidNetworkingBlocked(TEST_UID3))
+    }
+
+    @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)
+        testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_ENABLED))
+        assertTrue(bpfNetMapsReader.dataSaverEnabled)
+    }
+}
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index a6e9e95..81557f8 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -64,6 +64,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
@@ -90,7 +91,8 @@
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
 public class NetworkStatsCollectionTest {
-
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
     private static final String TEST_FILE = "test.bin";
     private static final String TEST_IMSI = "310260000000000";
     private static final int TEST_SUBID = 1;
@@ -199,6 +201,33 @@
                 77017831L, 100995L, 35436758L, 92344L);
     }
 
+    private InputStream getUidInputStreamFromRes(int uidRes) throws Exception {
+        final File testFile =
+                new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE);
+        stageFile(uidRes, testFile);
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyUid(testFile, true);
+
+        // now export into a unified format
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        collection.write(bos);
+        return new ByteArrayInputStream(bos.toByteArray());
+    }
+
+    @Test
+    public void testFastDataInputRead() throws Exception {
+        final NetworkStatsCollection legacyCollection =
+                new NetworkStatsCollection(30 * MINUTE_IN_MILLIS, false /* useFastDataInput */);
+        final NetworkStatsCollection fastReadCollection =
+                new NetworkStatsCollection(30 * MINUTE_IN_MILLIS, true /* useFastDataInput */);
+        final InputStream bis = getUidInputStreamFromRes(R.raw.netstats_uid_v4);
+        legacyCollection.read(bis);
+        bis.reset();
+        fastReadCollection.read(bis);
+        assertCollectionEntries(legacyCollection.getEntries(), fastReadCollection);
+    }
+
     @Test
     public void testStartEndAtomicBuckets() throws Exception {
         final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS);
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index 2170882..1e1fd35 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -54,6 +54,7 @@
 import com.android.frameworks.tests.net.R;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.SkipPresubmit;
 
 import org.junit.After;
 import org.junit.Test;
@@ -343,6 +344,7 @@
 
     }
 
+    @SkipPresubmit(reason = "Flaky: b/302325928; add to presubmit after fixing")
     @Test
     public void testFuzzing() throws Exception {
         try {
diff --git a/tests/unit/java/android/net/NetworkStatsRecorderTest.java b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
index fad11a3..7d039b6 100644
--- a/tests/unit/java/android/net/NetworkStatsRecorderTest.java
+++ b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
@@ -16,8 +16,17 @@
 
 package com.android.server.net;
 
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
+import static android.net.NetworkStats.TAG_NONE;
+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.text.format.DateUtils.HOUR_IN_MILLIS;
 
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.mockito.Mockito.any;
@@ -29,21 +38,31 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.annotation.NonNull;
+import android.net.NetworkIdentity;
+import android.net.NetworkIdentitySet;
 import android.net.NetworkStats;
+import android.net.NetworkStatsCollection;
 import android.os.DropBoxManager;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.util.FileRotator;
+import com.android.metrics.NetworkStatsMetricsLogger;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
+import libcore.testing.io.TestIoUtils;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 
 @RunWith(DevSdkIgnoreRunner.class)
@@ -53,6 +72,8 @@
     private static final String TAG = NetworkStatsRecorderTest.class.getSimpleName();
 
     private static final String TEST_PREFIX = "test";
+    private static final int TEST_UID1 = 1234;
+    private static final int TEST_UID2 = 1235;
 
     @Mock private DropBoxManager mDropBox;
     @Mock private NetworkStats.NonMonotonicObserver mObserver;
@@ -64,7 +85,8 @@
 
     private NetworkStatsRecorder buildRecorder(FileRotator rotator, boolean wipeOnError) {
         return new NetworkStatsRecorder(rotator, mObserver, mDropBox, TEST_PREFIX,
-                    HOUR_IN_MILLIS, false /* includeTags */, wipeOnError);
+                HOUR_IN_MILLIS, false /* includeTags */, wipeOnError,
+                false /* useFastDataInput */, null /* baseDir */);
     }
 
     @Test
@@ -85,4 +107,110 @@
         // Verify that the rotator won't delete files.
         verify(rotator, never()).deleteAll();
     }
+
+    @Test
+    public void testFileReadingMetrics_empty() {
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+        final NetworkStatsMetricsLogger.Dependencies deps =
+                mock(NetworkStatsMetricsLogger.Dependencies.class);
+        final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+        logger.logRecorderFileReading(PREFIX_XT, 888, null /* statsDir */, collection,
+                false /* useFastDataInput */);
+        verify(deps).writeRecorderFileReadingStats(
+                NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT,
+                1 /* readIndex */,
+                888 /* readLatencyMillis */,
+                0 /* fileCount */,
+                0 /* totalFileSize */,
+                0 /* keys */,
+                0 /* uids */,
+                0 /* totalHistorySize */,
+                false /* useFastDataInput */
+        );
+
+        // Write second time, verify the index increases.
+        logger.logRecorderFileReading(PREFIX_XT, 567, null /* statsDir */, collection,
+                true /* useFastDataInput */);
+        verify(deps).writeRecorderFileReadingStats(
+                NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT,
+                2 /* readIndex */,
+                567 /* readLatencyMillis */,
+                0 /* fileCount */,
+                0 /* totalFileSize */,
+                0 /* keys */,
+                0 /* uids */,
+                0 /* totalHistorySize */,
+                true /* useFastDataInput */
+        );
+    }
+
+    @Test
+    public void testFileReadingMetrics() {
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        final NetworkIdentitySet identSet = new NetworkIdentitySet();
+        identSet.add(new NetworkIdentity.Builder().build());
+        // Empty entries will be skipped, put some ints to make sure they can be recorded.
+        entry.rxBytes = 1;
+
+        collection.recordData(identSet, TEST_UID1, SET_DEFAULT, TAG_NONE, 0, 60, entry);
+        collection.recordData(identSet, TEST_UID2, SET_DEFAULT, TAG_NONE, 0, 60, entry);
+        collection.recordData(identSet, TEST_UID2, SET_FOREGROUND, TAG_NONE, 30, 60, entry);
+
+        final NetworkStatsMetricsLogger.Dependencies deps =
+                mock(NetworkStatsMetricsLogger.Dependencies.class);
+        final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+        logger.logRecorderFileReading(PREFIX_UID, 123, null /* statsDir */, collection,
+                false /* useFastDataInput */);
+        verify(deps).writeRecorderFileReadingStats(
+                NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID,
+                1 /* readIndex */,
+                123 /* readLatencyMillis */,
+                0 /* fileCount */,
+                0 /* totalFileSize */,
+                3 /* keys */,
+                2 /* uids */,
+                5 /* totalHistorySize */,
+                false /* useFastDataInput */
+        );
+    }
+
+    @Test
+    public void testFileReadingMetrics_fileAttributes() throws IOException {
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+
+        // Create files for testing. Only the first and the third files should be counted,
+        // with total 26 (each char takes 2 bytes) bytes in the content.
+        final File statsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+        write(statsDir, "uid_tag.1024-2048", "wanted");
+        write(statsDir, "uid_tag.1024-2048.backup", "");
+        write(statsDir, "uid_tag.2048-", "wanted2");
+        write(statsDir, "uid.2048-4096", "unwanted");
+        write(statsDir, "uid.2048-4096.backup", "unwanted2");
+
+        final NetworkStatsMetricsLogger.Dependencies deps =
+                mock(NetworkStatsMetricsLogger.Dependencies.class);
+        final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+        logger.logRecorderFileReading(PREFIX_UID_TAG, 678, statsDir, collection,
+                false /* useFastDataInput */);
+        verify(deps).writeRecorderFileReadingStats(
+                NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG,
+                1 /* readIndex */,
+                678 /* readLatencyMillis */,
+                2 /* fileCount */,
+                26 /* totalFileSize */,
+                0 /* keys */,
+                0 /* uids */,
+                0 /* totalHistorySize */,
+                false /* useFastDataInput */
+        );
+    }
+
+    private void write(@NonNull File baseDir, @NonNull String name,
+                       @NonNull String value) throws IOException {
+        final DataOutputStream out = new DataOutputStream(
+                new FileOutputStream(new File(baseDir, name)));
+        out.writeChars(value);
+        out.close();
+    }
 }
diff --git a/tests/unit/java/android/net/NetworkStatsTest.java b/tests/unit/java/android/net/NetworkStatsTest.java
index 126ad55..70ddc17 100644
--- a/tests/unit/java/android/net/NetworkStatsTest.java
+++ b/tests/unit/java/android/net/NetworkStatsTest.java
@@ -51,6 +51,7 @@
 
 import com.google.android.collect.Sets;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -63,7 +64,8 @@
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NetworkStatsTest {
-
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
     private static final String TEST_IFACE = "test0";
     private static final String TEST_IFACE2 = "test2";
     private static final int TEST_UID = 1001;
@@ -339,6 +341,7 @@
         assertEquals(96L, uidRoaming.getTotalBytes());
     }
 
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     public void testGroupedByIfaceEmpty() throws Exception {
         final NetworkStats uidStats = new NetworkStats(TEST_START, 3);
@@ -348,6 +351,7 @@
         assertEquals(0, grouped.size());
     }
 
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     public void testGroupedByIfaceAll() throws Exception {
         final NetworkStats uidStats = new NetworkStats(TEST_START, 3)
@@ -366,6 +370,7 @@
                 DEFAULT_NETWORK_ALL, 384L, 24L, 0L, 6L, 0L);
     }
 
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     public void testGroupedByIface() throws Exception {
         final NetworkStats uidStats = new NetworkStats(TEST_START, 7)
@@ -1068,35 +1073,40 @@
     }
 
     @Test
-    public void testClearInterfaces() {
+    public void testWithoutInterfaces() {
         final NetworkStats stats = new NetworkStats(TEST_START, 1);
         final NetworkStats.Entry entry1 = new NetworkStats.Entry(
                 "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                 DEFAULT_NETWORK_NO, 1024L, 50L, 100L, 20L, 0L);
-
         final NetworkStats.Entry entry2 = new NetworkStats.Entry(
                 "test2", 10101, SET_DEFAULT, 0xF0DD, METERED_NO, ROAMING_NO,
                 DEFAULT_NETWORK_NO, 51200, 25L, 101010L, 50L, 0L);
+        final NetworkStats.Entry entry3 = new NetworkStats.Entry(
+                "test3", 10101, SET_DEFAULT, 0xF0DD, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 1, 2L, 3L, 4L, 5L);
 
         stats.insertEntry(entry1);
         stats.insertEntry(entry2);
+        stats.insertEntry(entry3);
 
         // Verify that the interfaces have indeed been recorded.
-        assertEquals(2, stats.size());
+        assertEquals(3, stats.size());
         assertValues(stats, 0, "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO,
                 ROAMING_NO, DEFAULT_NETWORK_NO, 1024L, 50L, 100L, 20L, 0L);
         assertValues(stats, 1, "test2", 10101, SET_DEFAULT, 0xF0DD, METERED_NO,
                 ROAMING_NO, DEFAULT_NETWORK_NO, 51200, 25L, 101010L, 50L, 0L);
+        assertValues(stats, 2, "test3", 10101, SET_DEFAULT, 0xF0DD, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 1, 2L, 3L, 4L, 5L);
 
-        // Clear interfaces.
-        stats.clearInterfaces();
+        // Get stats without interfaces.
+        final NetworkStats ifaceClearedStats = stats.withoutInterfaces();
 
-        // Verify that the interfaces are cleared.
-        assertEquals(2, stats.size());
-        assertValues(stats, 0, null /* iface */, 10100, SET_DEFAULT, TAG_NONE, METERED_NO,
-                ROAMING_NO, DEFAULT_NETWORK_NO, 1024L, 50L, 100L, 20L, 0L);
-        assertValues(stats, 1, null /* iface */, 10101, SET_DEFAULT, 0xF0DD, METERED_NO,
-                ROAMING_NO, DEFAULT_NETWORK_NO, 51200, 25L, 101010L, 50L, 0L);
+        // Verify that the interfaces do not exist, and key-duplicated items are merged.
+        assertEquals(2, ifaceClearedStats.size());
+        assertValues(ifaceClearedStats, 0, null /* iface */, 10100, SET_DEFAULT, TAG_NONE,
+                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1024L, 50L, 100L, 20L, 0L);
+        assertValues(ifaceClearedStats, 1, null /* iface */, 10101, SET_DEFAULT, 0xF0DD,
+                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 51201, 27L, 101013L, 54L, 5L);
     }
 
     private static void assertContains(NetworkStats stats,  String iface, int uid, int set,
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 2f6c76b..a8414ca 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -49,6 +49,7 @@
 import android.telephony.TelephonyManager
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.NonNullTestUtils
 import com.android.testutils.assertParcelSane
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
@@ -221,12 +222,13 @@
     @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
     @Test
     fun testBuildTemplateMobileAll_nullSubscriberId() {
-        val templateMobileAllWithNullImsi = buildTemplateMobileAll(null)
+        val templateMobileAllWithNullImsi =
+                buildTemplateMobileAll(NonNullTestUtils.nullUnsafe<String>(null))
         val setWithNull = HashSet<String?>().apply {
             add(null)
         }
         val templateFromBuilder = NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES)
-            .setSubscriberIds(setWithNull).build()
+                .setSubscriberIds(setWithNull).build()
         assertEquals(templateFromBuilder, templateMobileAllWithNullImsi)
     }
 
diff --git a/tests/unit/java/android/net/NetworkUtilsTest.java b/tests/unit/java/android/net/NetworkUtilsTest.java
index a28245d..e453c02 100644
--- a/tests/unit/java/android/net/NetworkUtilsTest.java
+++ b/tests/unit/java/android/net/NetworkUtilsTest.java
@@ -16,19 +16,37 @@
 
 package android.net;
 
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVTIMEO;
+
+import static com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel;
+
 import static junit.framework.Assert.assertEquals;
 
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
 import android.os.Build;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.net.module.util.SocketUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.FileDescriptor;
 import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.util.TreeSet;
 
 @RunWith(DevSdkIgnoreRunner.class)
@@ -131,4 +149,42 @@
         assertEquals(BigInteger.valueOf(7l - 4 + 4 + 16 + 65536),
                 NetworkUtils.routedIPv6AddressCount(set));
     }
+
+    private byte[] getTimevalBytes(StructTimeval tv) {
+        byte[] timeval = new byte[16];
+        ByteBuffer buf = ByteBuffer.wrap(timeval);
+        buf.order(ByteOrder.nativeOrder());
+        buf.putLong(tv.tv_sec);
+        buf.putLong(tv.tv_usec);
+        return timeval;
+    }
+
+    private void testSetSockOptBytes(FileDescriptor sock, long timeValMillis)
+            throws ErrnoException {
+        final StructTimeval writeTimeval = StructTimeval.fromMillis(timeValMillis);
+        byte[] timeval = getTimevalBytes(writeTimeval);
+        final StructTimeval readTimeval;
+
+        NetworkUtils.setsockoptBytes(sock, SOL_SOCKET, SO_RCVTIMEO, timeval);
+        readTimeval = Os.getsockoptTimeval(sock, SOL_SOCKET, SO_RCVTIMEO);
+
+        assertEquals(writeTimeval, readTimeval);
+    }
+
+    @Test
+    public void testSetSockOptBytes() throws ErrnoException {
+        final FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
+
+        testSetSockOptBytes(sock, 3000);
+
+        testSetSockOptBytes(sock, 5000);
+
+        SocketUtils.closeSocketQuietly(sock);
+    }
+
+    @Test
+    public void testIsKernel64Bit() {
+        assumeTrue(getVsrApiLevel() > Build.VERSION_CODES.TIRAMISU);
+        assertTrue(NetworkUtils.isKernel64Bit());
+    }
 }
diff --git a/tests/unit/java/android/net/VpnManagerTest.java b/tests/unit/java/android/net/VpnManagerTest.java
deleted file mode 100644
index 532081a..0000000
--- a/tests/unit/java/android/net/VpnManagerTest.java
+++ /dev/null
@@ -1,141 +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.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.os.Build;
-import android.test.mock.MockContext;
-import android.util.SparseArray;
-
-import androidx.test.filters.SmallTest;
-
-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 {
-        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
new file mode 100644
index 0000000..c491f37
--- /dev/null
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.net.nsd.AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY
+import android.net.nsd.NsdManager.PROTOCOL_DNS_SD
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.parcelingRoundTrip
+import java.time.Duration
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO: move this class to CTS tests when AdvertisingRequest is made public
+/** Unit tests for {@link AdvertisingRequest}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class AdvertisingRequestTest {
+    @Test
+    fun testParcelingIsLossLess() {
+        val info = NsdServiceInfo().apply {
+            serviceType = "_ipp._tcp"
+        }
+        val beforeParcel = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setTtl(Duration.ofSeconds(30L))
+                .build()
+
+        val afterParcel = parcelingRoundTrip(beforeParcel)
+
+        assertEquals(beforeParcel.serviceInfo.serviceType, afterParcel.serviceInfo.serviceType)
+        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()
+
+        assertNull(request.ttl)
+    }
+
+    @Test
+    fun testBuilder_setPropertiesSuccess() {
+        val info = NsdServiceInfo().apply {
+            serviceType = "_ipp._tcp"
+        }
+        val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setTtl(Duration.ofSeconds(100L))
+                .build()
+
+        assertEquals("_ipp._tcp", request.serviceInfo.serviceType)
+        assertEquals(PROTOCOL_DNS_SD, request.protocolType)
+        assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.advertisingConfig)
+        assertEquals(Duration.ofSeconds(100L), request.ttl)
+    }
+
+    @Test
+    fun testEquality() {
+        val info = NsdServiceInfo().apply {
+            serviceType = "_ipp._tcp"
+        }
+        val request1 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
+        val request2 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
+        val request3 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setTtl(Duration.ofSeconds(120L))
+                .build()
+        val request4 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setTtl(Duration.ofSeconds(120L))
+                .build()
+
+        assertEquals(request1, request2)
+        assertEquals(request3, request4)
+        assertNotEquals(request1, request3)
+        assertNotEquals(request2, request4)
+    }
+}
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 0965193..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;
 
@@ -23,6 +28,7 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -38,6 +44,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.FunctionalUtils.ThrowingConsumer;
@@ -51,6 +58,12 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.List;
+import java.time.Duration;
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -85,73 +98,81 @@
     @Test
     @EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testResolveServiceS() throws Exception {
-        verify(mServiceConn, never()).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ false);
         doTestResolveService();
     }
 
     @Test
     @DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testResolveServicePreS() throws Exception {
-        verify(mServiceConn).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ true);
         doTestResolveService();
     }
 
     @Test
     @EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testDiscoverServiceS() throws Exception {
-        verify(mServiceConn, never()).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ false);
         doTestDiscoverService();
     }
 
     @Test
     @DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testDiscoverServicePreS() throws Exception {
-        verify(mServiceConn).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ true);
         doTestDiscoverService();
     }
 
     @Test
     @EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testParallelResolveServiceS() throws Exception {
-        verify(mServiceConn, never()).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ false);
         doTestParallelResolveService();
     }
 
     @Test
     @DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testParallelResolveServicePreS() throws Exception {
-        verify(mServiceConn).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ true);
         doTestParallelResolveService();
     }
 
     @Test
     @EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testInvalidCallsS() throws Exception {
-        verify(mServiceConn, never()).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ false);
         doTestInvalidCalls();
     }
 
     @Test
     @DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testInvalidCallsPreS() throws Exception {
-        verify(mServiceConn).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ true);
         doTestInvalidCalls();
     }
 
     @Test
     @EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testRegisterServiceS() throws Exception {
-        verify(mServiceConn, never()).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ false);
         doTestRegisterService();
     }
 
     @Test
     @DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     public void testRegisterServicePreS() throws Exception {
-        verify(mServiceConn).startDaemon();
+        verifyDaemonStarted(/* targetSdkPreS= */ true);
         doTestRegisterService();
     }
 
+    private void verifyDaemonStarted(boolean targetSdkPreS) throws Exception {
+        if (targetSdkPreS && !SdkLevel.isAtLeastV()) {
+            verify(mServiceConn).startDaemon();
+        } else {
+            verify(mServiceConn, never()).startDaemon();
+        }
+    }
+
     private void doTestResolveService() throws Exception {
         NsdManager manager = mManager;
 
@@ -195,6 +216,39 @@
         verify(listener2, timeout(mTimeoutMs).times(1)).onServiceResolved(reply);
     }
 
+    @Test
+    public void testRegisterServiceWithAdvertisingRequest() throws Exception {
+        final NsdManager manager = mManager;
+        final NsdServiceInfo request = new NsdServiceInfo("another_name2", "another_type2");
+        request.setPort(2203);
+        final AdvertisingRequest advertisingRequest = new AdvertisingRequest.Builder(request,
+                PROTOCOL).build();
+        final NsdManager.RegistrationListener listener = mock(
+                NsdManager.RegistrationListener.class);
+
+        manager.registerService(advertisingRequest, Runnable::run, listener);
+        int key4 = getRequestKey(req -> verify(mServiceConn).registerService(req.capture(), any()));
+        mCallback.onRegisterServiceSucceeded(key4, request);
+        verify(listener, timeout(mTimeoutMs).times(1)).onServiceRegistered(request);
+    }
+
+    @Test
+    public void testRegisterServiceWithCustomTtl() throws Exception {
+        final NsdManager manager = mManager;
+        final NsdServiceInfo info = new NsdServiceInfo("another_name2", "another_type2");
+        info.setPort(2203);
+        final AdvertisingRequest request = new AdvertisingRequest.Builder(info, PROTOCOL)
+                .setTtl(Duration.ofSeconds(30)).build();
+        final NsdManager.RegistrationListener listener = mock(
+                NsdManager.RegistrationListener.class);
+
+        manager.registerService(request, Runnable::run, listener);
+
+        AdvertisingRequest capturedRequest = getAdvertisingRequest(
+                req -> verify(mServiceConn).registerService(anyInt(), req.capture()));
+        assertEquals(request.getTtl(), capturedRequest.getTtl());
+    }
+
     private void doTestRegisterService() throws Exception {
         NsdManager manager = mManager;
 
@@ -259,6 +313,7 @@
     private void doTestDiscoverService() throws Exception {
         NsdManager manager = mManager;
 
+        DiscoveryRequest request1 = new DiscoveryRequest.Builder("a_type").build();
         NsdServiceInfo reply1 = new NsdServiceInfo("a_name", "a_type");
         NsdServiceInfo reply2 = new NsdServiceInfo("another_name", "a_type");
         NsdServiceInfo reply3 = new NsdServiceInfo("a_third_name", "a_type");
@@ -279,7 +334,7 @@
         int key2 = getRequestKey(req ->
                 verify(mServiceConn, times(2)).discoverServices(req.capture(), any()));
 
-        mCallback.onDiscoverServicesStarted(key2, reply1);
+        mCallback.onDiscoverServicesStarted(key2, request1);
         verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type");
 
 
@@ -319,7 +374,7 @@
         int key3 = getRequestKey(req ->
                 verify(mServiceConn, times(3)).discoverServices(req.capture(), any()));
 
-        mCallback.onDiscoverServicesStarted(key3, reply1);
+        mCallback.onDiscoverServicesStarted(key3, request1);
         verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type");
 
         // Client unregisters immediately, it fails
@@ -343,10 +398,66 @@
         NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
         NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
         NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
+        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");
+        NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
+        NsdServiceInfo otherServiceWithSubtype = new NsdServiceInfo("b_name", "_a_type._tcp,_sub1");
+        NsdServiceInfo validServiceDuplicate = new NsdServiceInfo("a_name", "_a_type._tcp");
+        NsdServiceInfo validServiceSubtypeUpdate = new NsdServiceInfo("a_name",
+                "_a_type._tcp,_sub1,_s2");
+        NsdServiceInfo otherSubtypeUpdate = new NsdServiceInfo("a_name", "_a_type._tcp,_sub1,_s3");
+        NsdServiceInfo dotSyntaxSubtypeUpdate = new NsdServiceInfo("a_name", "_sub1._a_type._tcp");
+
         validService.setPort(2222);
+        otherServiceWithSubtype.setPort(2222);
+        validServiceDuplicate.setPort(2222);
+        validServiceSubtypeUpdate.setPort(2222);
+        otherSubtypeUpdate.setPort(2222);
+        dotSyntaxSubtypeUpdate.setPort(2222);
+
+        NsdServiceInfo invalidMissingHostnameWithAddresses = new NsdServiceInfo(null, null);
+        invalidMissingHostnameWithAddresses.setHostAddresses(
+                List.of(
+                        InetAddress.parseNumericAddress("192.168.82.14"),
+                        InetAddress.parseNumericAddress("2001::1")));
+
+        NsdServiceInfo validCustomHostWithAddresses = new NsdServiceInfo(null, null);
+        validCustomHostWithAddresses.setHostname("a_host");
+        validCustomHostWithAddresses.setHostAddresses(
+                List.of(
+                        InetAddress.parseNumericAddress("192.168.82.14"),
+                        InetAddress.parseNumericAddress("2001::1")));
+
+        NsdServiceInfo validServiceWithCustomHostAndAddresses =
+                new NsdServiceInfo("a_name", "_a_type._tcp");
+        validServiceWithCustomHostAndAddresses.setPort(2222);
+        validServiceWithCustomHostAndAddresses.setHostname("a_host");
+        validServiceWithCustomHostAndAddresses.setHostAddresses(
+                List.of(
+                        InetAddress.parseNumericAddress("192.168.82.14"),
+                        InetAddress.parseNumericAddress("2001::1")));
+
+        NsdServiceInfo validServiceWithCustomHostNoAddresses =
+                new NsdServiceInfo("a_name", "_a_type._tcp");
+        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
@@ -356,13 +467,43 @@
         mustFail(() -> { manager.registerService(invalidService, PROTOCOL, listener1); });
         mustFail(() -> { manager.registerService(validService, -1, listener1); });
         mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
+        mustFail(() -> {
+            manager.registerService(invalidMissingHostnameWithAddresses, PROTOCOL, listener1); });
+        mustFail(() -> {
+            manager.registerService(invalidServiceWithTooShortPublicKey, PROTOCOL, listener1); });
         manager.registerService(validService, PROTOCOL, listener1);
-        //  - listener already registered
+        //  - update without subtype is not allowed
+        mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
+        //  - update with subtype is allowed
+        manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+        //  - re-updating to the same subtype is allowed
+        manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+        //  - updating to other subtypes is allowed
+        manager.registerService(otherSubtypeUpdate, PROTOCOL, listener1);
+        //  - update back to the service without subtype is allowed
+        manager.registerService(validService, PROTOCOL, listener1);
+        //  - updating to a subtype with _sub._type syntax is not allowed
+        mustFail(() -> { manager.registerService(dotSyntaxSubtypeUpdate, PROTOCOL, listener1); });
+        //  - updating to a different service name is not allowed
+        mustFail(() -> { manager.registerService(otherServiceWithSubtype, PROTOCOL, listener1); });
+        //  - listener already registered, and not using subtypes
         mustFail(() -> { manager.registerService(validService, PROTOCOL, listener1); });
         manager.unregisterService(listener1);
         // TODO: make listener immediately reusable
         //mustFail(() -> { manager.unregisterService(listener1); });
         //manager.registerService(validService, PROTOCOL, listener1);
+        //  - registering a custom host without a service is valid
+        manager.registerService(validCustomHostWithAddresses, PROTOCOL, listener4);
+        manager.unregisterService(listener4);
+        //  - registering a service with a custom host is valid
+        manager.registerService(validServiceWithCustomHostAndAddresses, PROTOCOL, listener5);
+        manager.unregisterService(listener5);
+        //  - 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
@@ -390,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();
@@ -404,4 +768,12 @@
         verifier.accept(captor);
         return captor.getValue();
     }
+
+    AdvertisingRequest getAdvertisingRequest(
+            ThrowingConsumer<ArgumentCaptor<AdvertisingRequest>> verifier) throws Exception {
+        final ArgumentCaptor<AdvertisingRequest> captor =
+                ArgumentCaptor.forClass(AdvertisingRequest.class);
+        verifier.accept(captor);
+        return captor.getValue();
+    }
 }
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
deleted file mode 100644
index 9ce0693..0000000
--- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.nsd;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.net.InetAddresses;
-import android.net.Network;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.StrictMode;
-
-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.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-
-@RunWith(DevSdkIgnoreRunner.class)
-@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
-public class NsdServiceInfoTest {
-
-    private static final InetAddress IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
-    private static final InetAddress IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::");
-    public final static InetAddress LOCALHOST;
-    static {
-        // Because test.
-        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
-        StrictMode.setThreadPolicy(policy);
-
-        InetAddress _host = null;
-        try {
-            _host = InetAddress.getLocalHost();
-        } catch (UnknownHostException e) { }
-        LOCALHOST = _host;
-    }
-
-    @Test
-    public void testLimits() throws Exception {
-        NsdServiceInfo info = new NsdServiceInfo();
-
-        // Non-ASCII keys.
-        boolean exceptionThrown = false;
-        try {
-            info.setAttribute("猫", "meow");
-        } catch (IllegalArgumentException e) {
-            exceptionThrown = true;
-        }
-        assertTrue(exceptionThrown);
-        assertEmptyServiceInfo(info);
-
-        // ASCII keys with '=' character.
-        exceptionThrown = false;
-        try {
-            info.setAttribute("kitten=", "meow");
-        } catch (IllegalArgumentException e) {
-            exceptionThrown = true;
-        }
-        assertTrue(exceptionThrown);
-        assertEmptyServiceInfo(info);
-
-        // Single key + value length too long.
-        exceptionThrown = false;
-        try {
-            String longValue = "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
-                    "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
-                    "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
-                    "ooooooooooooooooooooooooooooong";  // 248 characters.
-            info.setAttribute("longcat", longValue);  // Key + value == 255 characters.
-        } catch (IllegalArgumentException e) {
-            exceptionThrown = true;
-        }
-        assertTrue(exceptionThrown);
-        assertEmptyServiceInfo(info);
-
-        // Total TXT record length too long.
-        exceptionThrown = false;
-        int recordsAdded = 0;
-        try {
-            for (int i = 100; i < 300; ++i) {
-                // 6 char key + 5 char value + 2 bytes overhead = 13 byte record length.
-                String key = String.format("key%d", i);
-                info.setAttribute(key, "12345");
-                recordsAdded++;
-            }
-        } catch (IllegalArgumentException e) {
-            exceptionThrown = true;
-        }
-        assertTrue(exceptionThrown);
-        assertTrue(100 == recordsAdded);
-        assertTrue(info.getTxtRecord().length == 1300);
-    }
-
-    @Test
-    public void testParcel() throws Exception {
-        NsdServiceInfo emptyInfo = new NsdServiceInfo();
-        checkParcelable(emptyInfo);
-
-        NsdServiceInfo fullInfo = new NsdServiceInfo();
-        fullInfo.setServiceName("kitten");
-        fullInfo.setServiceType("_kitten._tcp");
-        fullInfo.setPort(4242);
-        fullInfo.setHost(LOCALHOST);
-        fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
-        fullInfo.setNetwork(new Network(123));
-        fullInfo.setInterfaceIndex(456);
-        checkParcelable(fullInfo);
-
-        NsdServiceInfo noHostInfo = new NsdServiceInfo();
-        noHostInfo.setServiceName("kitten");
-        noHostInfo.setServiceType("_kitten._tcp");
-        noHostInfo.setPort(4242);
-        checkParcelable(noHostInfo);
-
-        NsdServiceInfo attributedInfo = new NsdServiceInfo();
-        attributedInfo.setServiceName("kitten");
-        attributedInfo.setServiceType("_kitten._tcp");
-        attributedInfo.setPort(4242);
-        attributedInfo.setHost(LOCALHOST);
-        fullInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
-        attributedInfo.setAttribute("color", "pink");
-        attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
-        attributedInfo.setAttribute("adorable", (String) null);
-        attributedInfo.setAttribute("sticky", "yes");
-        attributedInfo.setAttribute("siblings", new byte[] {});
-        attributedInfo.setAttribute("edge cases", new byte[] {0, -1, 127, -128});
-        attributedInfo.removeAttribute("sticky");
-        checkParcelable(attributedInfo);
-
-        // Sanity check that we actually wrote attributes to attributedInfo.
-        assertTrue(attributedInfo.getAttributes().keySet().contains("adorable"));
-        String sound = new String(attributedInfo.getAttributes().get("sound"), "UTF-8");
-        assertTrue(sound.equals("にゃあ"));
-        byte[] edgeCases = attributedInfo.getAttributes().get("edge cases");
-        assertTrue(Arrays.equals(edgeCases, new byte[] {0, -1, 127, -128}));
-        assertFalse(attributedInfo.getAttributes().keySet().contains("sticky"));
-    }
-
-    public void checkParcelable(NsdServiceInfo original) {
-        // Write to parcel.
-        Parcel p = Parcel.obtain();
-        Bundle writer = new Bundle();
-        writer.putParcelable("test_info", original);
-        writer.writeToParcel(p, 0);
-
-        // Extract from parcel.
-        p.setDataPosition(0);
-        Bundle reader = p.readBundle();
-        reader.setClassLoader(NsdServiceInfo.class.getClassLoader());
-        NsdServiceInfo result = reader.getParcelable("test_info");
-
-        // Assert equality of base fields.
-        assertEquals(original.getServiceName(), result.getServiceName());
-        assertEquals(original.getServiceType(), result.getServiceType());
-        assertEquals(original.getHost(), result.getHost());
-        assertTrue(original.getPort() == result.getPort());
-        assertEquals(original.getNetwork(), result.getNetwork());
-        assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
-
-        // Assert equality of attribute map.
-        Map<String, byte[]> originalMap = original.getAttributes();
-        Map<String, byte[]> resultMap = result.getAttributes();
-        assertEquals(originalMap.keySet(), resultMap.keySet());
-        for (String key : originalMap.keySet()) {
-            assertTrue(Arrays.equals(originalMap.get(key), resultMap.get(key)));
-        }
-    }
-
-    public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
-        byte[] txtRecord = shouldBeEmpty.getTxtRecord();
-        if (txtRecord == null || txtRecord.length == 0) {
-            return;
-        }
-        fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord));
-    }
-}
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/android/net/util/KeepaliveUtilsTest.kt b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
index cb3a315..470274d 100644
--- a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
+++ b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
@@ -95,11 +95,11 @@
 
         // Check resource with invalid transport type.
         assertRunWithException(arrayOf("-1,3"))
-        assertRunWithException(arrayOf("10,3"))
+        assertRunWithException(arrayOf("11,3"))
 
         // Check valid customization generates expected array.
         val validRes = arrayOf("0,3", "1,0", "4,4")
-        val expectedValidRes = intArrayOf(3, 0, 0, 0, 4, 0, 0, 0, 0, 0)
+        val expectedValidRes = intArrayOf(3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0)
 
         val mockContext = getMockedContextWithStringArrayRes(
                 R.array.config_networkSupportedKeepaliveCount,
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 b2dff2e..0000000
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ /dev/null
@@ -1,314 +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.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);
-    }
-}
diff --git a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
new file mode 100644
index 0000000..8a9286f
--- /dev/null
+++ b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
@@ -0,0 +1,249 @@
+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
+import android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE
+import android.net.NetworkCapabilities.NET_CAPABILITY_IMS
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1
+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
+import com.android.server.CSTest
+import com.android.server.FromS
+import com.android.server.connectivity.FullScore
+import com.android.server.connectivity.FullScore.POLICY_IS_UNMETERED
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+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>()
+    post { future.complete(f()) }
+    return future.get()
+}
+
+private fun flags(vararg flags: Int) = flags.fold(0L) { acc, it -> acc or (1L shl it) }
+
+private fun Number.toTransportsString() = StringBuilder().also { sb ->
+    BitUtils.appendStringRepresentationOfBitMaskToStringBuilder(sb, this.toLong(),
+            { NetworkCapabilities.transportNameOf(it) }, "|") }.toString()
+
+private fun Number.toCapsString() = StringBuilder().also { sb ->
+    BitUtils.appendStringRepresentationOfBitMaskToStringBuilder(sb, this.toLong(),
+            { NetworkCapabilities.capabilityNameOf(it) }, "&") }.toString()
+
+private fun Number.toPolicyString() = StringBuilder().also {sb ->
+    BitUtils.appendStringRepresentationOfBitMaskToStringBuilder(sb, this.toLong(),
+            { FullScore.policyNameOf(it) }, "|") }.toString()
+
+private fun Number.exceptCSManaged() = this.toLong() and CONNECTIVITY_MANAGED_CAPABILITIES.inv()
+
+private val NetworkCapabilities.meteredState get() = when {
+    hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED) ->
+        MeteredState.METERED_TEMPORARILY_UNMETERED
+    hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ->
+        MeteredState.METERED_NO
+    else ->
+        MeteredState.METERED_YES
+}
+
+private val NetworkCapabilities.validatedState get() = when {
+    hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) -> ValidatedState.VS_PORTAL
+    hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY) -> ValidatedState.VS_PARTIAL
+    hasCapability(NET_CAPABILITY_VALIDATED) -> ValidatedState.VS_VALID
+    else -> ValidatedState.VS_INVALID
+}
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class ConnectivitySampleMetricsTest : CSTest() {
+    @Test
+    fun testSampleConnectivityState_Network() {
+        val wifi1Caps = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_NOT_METERED)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_ROAMING)
+                .build()
+        val wifi1Score = NetworkScore.Builder()
+                .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+                .setExiting(true)
+                .build()
+        val agentWifi1 = Agent(nc = wifi1Caps, score = FromS(wifi1Score)).also { it.connect() }
+
+        val wifi2Caps = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_ENTERPRISE)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_ROAMING)
+                .addEnterpriseId(NET_ENTERPRISE_ID_3)
+                .build()
+        val wifi2Score = NetworkScore.Builder()
+                .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+                .setTransportPrimary(true)
+                .build()
+        val agentWifi2 = Agent(nc = wifi2Caps, score = FromS(wifi2Score)).also { it.connect() }
+
+        val cellCaps = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_IMS)
+                .addCapability(NET_CAPABILITY_ENTERPRISE)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_ROAMING)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
+                .build()
+        val cellScore = NetworkScore.Builder()
+                .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+                .build()
+        val agentCell = Agent(nc = cellCaps, score = FromS(cellScore)).also { it.connect() }
+
+        val stats = csHandler.onHandler { service.sampleConnectivityState() }
+        assertEquals(3, stats.networks.networkDescriptionList.size)
+        val foundCell = stats.networks.networkDescriptionList.find {
+            it.transportTypes == (1 shl TRANSPORT_CELLULAR)
+        } ?: fail("Can't find cell network (searching by transport)")
+        val foundWifi1 = stats.networks.networkDescriptionList.find {
+            it.transportTypes == (1 shl TRANSPORT_WIFI) &&
+                    0L != (it.capabilities and (1L shl NET_CAPABILITY_NOT_METERED))
+        } ?: fail("Can't find wifi1 (searching by WIFI transport and the NOT_METERED capability)")
+        val foundWifi2 = stats.networks.networkDescriptionList.find {
+            it.transportTypes == (1 shl TRANSPORT_WIFI) &&
+                    0L != (it.capabilities and (1L shl NET_CAPABILITY_ENTERPRISE))
+        } ?: fail("Can't find wifi2 (searching by WIFI transport and the ENTERPRISE capability)")
+
+        fun checkNetworkDescription(
+                network: String,
+                found: NetworkDescription,
+                expected: NetworkCapabilities
+        ) {
+            assertEquals(expected.transportTypesInternal, found.transportTypes.toLong(),
+                    "Transports differ for network $network, " +
+                            "expected ${expected.transportTypesInternal.toTransportsString()}, " +
+                            "found ${found.transportTypes.toTransportsString()}")
+            val expectedCaps = expected.capabilitiesInternal.exceptCSManaged()
+            val foundCaps = found.capabilities.exceptCSManaged()
+            assertEquals(expectedCaps, foundCaps,
+                    "Capabilities differ for network $network, " +
+                            "expected ${expectedCaps.toCapsString()}, " +
+                            "found ${foundCaps.toCapsString()}")
+            assertEquals(expected.enterpriseIdsInternal, found.enterpriseId,
+                    "Enterprise IDs differ for network $network, " +
+                            "expected ${expected.enterpriseIdsInternal}," +
+                            " found ${found.enterpriseId}")
+            assertEquals(expected.meteredState, found.meteredState,
+                    "Metered states differ for network $network, " +
+                            "expected ${expected.meteredState}, " +
+                            "found ${found.meteredState}")
+            assertEquals(expected.validatedState, found.validatedState,
+                    "Validated states differ for network $network, " +
+                            "expected ${expected.validatedState}, " +
+                            "found ${found.validatedState}")
+        }
+
+        checkNetworkDescription("Cell network", foundCell, cellCaps)
+        checkNetworkDescription("Wifi1", foundWifi1, wifi1Caps)
+        checkNetworkDescription("Wifi2", foundWifi2, wifi2Caps)
+
+        assertEquals(0, foundCell.scorePolicies, "Cell score policies incorrect, expected 0, " +
+                        "found ${foundCell.scorePolicies.toPolicyString()}")
+        val expectedWifi1Policies = flags(POLICY_EXITING, POLICY_IS_UNMETERED)
+        assertEquals(expectedWifi1Policies, foundWifi1.scorePolicies,
+                "Wifi1 score policies incorrect, " +
+                        "expected ${expectedWifi1Policies.toPolicyString()}, " +
+                        "found ${foundWifi1.scorePolicies.toPolicyString()}")
+        val expectedWifi2Policies = flags(POLICY_TRANSPORT_PRIMARY)
+        assertEquals(expectedWifi2Policies, foundWifi2.scorePolicies,
+                "Wifi2 score policies incorrect, " +
+                        "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
new file mode 100644
index 0000000..10ba6a4
--- /dev/null
+++ b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
@@ -0,0 +1,319 @@
+/*
+ * 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.metrics
+
+import android.os.Build
+import android.stats.connectivity.MdnsQueryResult
+import android.stats.connectivity.NsdEventType
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.util.Random
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class NetworkNsdReportedMetricsTest {
+    private val deps = mock(NetworkNsdReportedMetrics.Dependencies::class.java)
+    private val random = mock(Random::class.java)
+
+    @Before
+    fun setUp() {
+        doReturn(random).`when`(deps).makeRandomGenerator()
+    }
+
+    @Test
+    fun testReportServiceRegistrationSucceeded() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceRegistrationSucceeded(true /* isLegacy */, transactionId, durationMs)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertTrue(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_REGISTER, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_REGISTERED, it.queryResult)
+            assertEquals(durationMs, it.eventDurationMillisec)
+        }
+    }
+
+    @Test
+    fun testReportServiceRegistrationFailed() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceRegistrationFailed(false /* isLegacy */, transactionId, durationMs)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertFalse(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_REGISTER, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_REGISTRATION_FAILED, it.queryResult)
+            assertEquals(durationMs, it.eventDurationMillisec)
+        }
+    }
+
+    @Test
+    fun testReportServiceUnregistration() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val repliedRequestsCount = 25
+        val sentPacketCount = 50
+        val conflictDuringProbingCount = 2
+        val conflictAfterProbingCount = 1
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceUnregistration(true /* isLegacy */, transactionId, durationMs,
+                repliedRequestsCount, sentPacketCount, conflictDuringProbingCount,
+                conflictAfterProbingCount)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertTrue(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_REGISTER, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_UNREGISTERED, it.queryResult)
+            assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(repliedRequestsCount, it.repliedRequestsCount)
+            assertEquals(sentPacketCount, it.sentPacketCount)
+            assertEquals(conflictDuringProbingCount, it.conflictDuringProbingCount)
+            assertEquals(conflictAfterProbingCount, it.conflictAfterProbingCount)
+        }
+    }
+
+    @Test
+    fun testReportServiceDiscoveryStarted() {
+        val clientId = 99
+        val transactionId = 100
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceDiscoveryStarted(true /* isLegacy */, transactionId)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertTrue(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_DISCOVER, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_DISCOVERY_STARTED, it.queryResult)
+        }
+    }
+
+    @Test
+    fun testReportServiceDiscoveryFailed() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceDiscoveryFailed(false /* isLegacy */, transactionId, durationMs)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertFalse(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_DISCOVER, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_DISCOVERY_FAILED, it.queryResult)
+            assertEquals(durationMs, it.eventDurationMillisec)
+        }
+    }
+
+    @Test
+    fun testReportServiceDiscoveryStop() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val foundCallbackCount = 100
+        val lostCallbackCount = 49
+        val servicesCount = 75
+        val sentQueryCount = 150
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceDiscoveryStop(true /* isLegacy */, transactionId, durationMs,
+                foundCallbackCount, lostCallbackCount, servicesCount, sentQueryCount,
+                true /* isServiceFromCache */)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertTrue(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_DISCOVER, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_DISCOVERY_STOP, it.queryResult)
+            assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(foundCallbackCount, it.foundCallbackCount)
+            assertEquals(lostCallbackCount, it.lostCallbackCount)
+            assertEquals(servicesCount, it.foundServiceCount)
+            assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(sentQueryCount, it.sentQueryCount)
+            assertTrue(it.isKnownService)
+        }
+    }
+
+    @Test
+    fun testReportServiceResolved() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val sentQueryCount = 0
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceResolved(true /* isLegacy */, transactionId, durationMs,
+                true /* isServiceFromCache */, sentQueryCount)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertTrue(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_RESOLVE, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_RESOLVED, it.queryResult)
+            assertTrue(it.isKnownService)
+            assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(sentQueryCount, it.sentQueryCount)
+        }
+    }
+
+    @Test
+    fun testReportServiceResolutionFailed() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceResolutionFailed(false /* isLegacy */, transactionId, durationMs)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertFalse(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_RESOLVE, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_RESOLUTION_FAILED, it.queryResult)
+            assertEquals(durationMs, it.eventDurationMillisec)
+        }
+    }
+
+    @Test
+    fun testReportServiceResolutionStop() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val sentQueryCount = 10
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceResolutionStop(
+                true /* isLegacy */, transactionId, durationMs, sentQueryCount)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertTrue(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_RESOLVE, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_RESOLUTION_STOP, it.queryResult)
+            assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(sentQueryCount, it.sentQueryCount)
+        }
+    }
+
+    @Test
+    fun testReportServiceInfoCallbackRegistered() {
+        val clientId = 99
+        val transactionId = 100
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceInfoCallbackRegistered(transactionId)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertFalse(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_SERVICE_INFO_CALLBACK, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_INFO_CALLBACK_REGISTERED, it.queryResult)
+        }
+    }
+
+    @Test
+    fun testReportServiceInfoCallbackRegistrationFailed() {
+        val clientId = 99
+        val transactionId = 100
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceInfoCallbackRegistrationFailed(transactionId)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertFalse(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_SERVICE_INFO_CALLBACK, it.type)
+            assertEquals(
+                    MdnsQueryResult.MQR_SERVICE_INFO_CALLBACK_REGISTRATION_FAILED, it.queryResult)
+        }
+    }
+
+    @Test
+    fun testReportServiceInfoCallbackUnregistered() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val updateCallbackCount = 100
+        val lostCallbackCount = 10
+        val sentQueryCount = 150
+        val metrics = NetworkNsdReportedMetrics(clientId, deps)
+        metrics.reportServiceInfoCallbackUnregistered(transactionId, durationMs,
+                updateCallbackCount, lostCallbackCount, false /* isServiceFromCache */,
+                sentQueryCount)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertFalse(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_SERVICE_INFO_CALLBACK, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_INFO_CALLBACK_UNREGISTERED, it.queryResult)
+            assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(updateCallbackCount, it.foundCallbackCount)
+            assertEquals(lostCallbackCount, it.lostCallbackCount)
+            assertFalse(it.isKnownService)
+            assertEquals(sentQueryCount, it.sentQueryCount)
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 19fa41d..859c54a 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -16,8 +16,41 @@
 
 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;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
+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.IIF_MATCH;
+import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
+import static android.net.BpfNetMapsConstants.LOW_POWER_STANDBY_MATCH;
+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_USER_RESTRICTED;
+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;
@@ -33,19 +66,6 @@
 import static android.system.OsConstants.EINVAL;
 import static android.system.OsConstants.EPERM;
 
-import static com.android.server.BpfNetMaps.DOZABLE_MATCH;
-import static com.android.server.BpfNetMaps.HAPPY_BOX_MATCH;
-import static com.android.server.BpfNetMaps.IIF_MATCH;
-import static com.android.server.BpfNetMaps.LOCKDOWN_VPN_MATCH;
-import static com.android.server.BpfNetMaps.LOW_POWER_STANDBY_MATCH;
-import static com.android.server.BpfNetMaps.NO_MATCH;
-import static com.android.server.BpfNetMaps.OEM_DENY_1_MATCH;
-import static com.android.server.BpfNetMaps.OEM_DENY_2_MATCH;
-import static com.android.server.BpfNetMaps.OEM_DENY_3_MATCH;
-import static com.android.server.BpfNetMaps.PENALTY_BOX_MATCH;
-import static com.android.server.BpfNetMaps.POWERSAVE_MATCH;
-import static com.android.server.BpfNetMaps.RESTRICTED_MATCH;
-import static com.android.server.BpfNetMaps.STANDBY_MATCH;
 import static com.android.server.ConnectivityStatsLog.NETWORK_BPF_MAP_INFO;
 
 import static org.junit.Assert.assertEquals;
@@ -62,9 +82,14 @@
 
 import android.app.StatsManager;
 import android.content.Context;
+import android.net.BpfNetMapsUtils;
 import android.net.INetd;
+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;
@@ -78,6 +103,8 @@
 import com.android.net.module.util.Struct.U8;
 import com.android.net.module.util.bpf.CookieTagMapKey;
 import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.net.module.util.bpf.IngressDiscardKey;
+import com.android.net.module.util.bpf.IngressDiscardValue;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -93,6 +120,8 @@
 
 import java.io.FileDescriptor;
 import java.io.StringWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -111,23 +140,21 @@
     private static final int TEST_IF_INDEX = 7;
     private static final int NO_IIF = 0;
     private static final int NULL_IIF = 0;
+    private static final Inet4Address TEST_V4_ADDRESS =
+            (Inet4Address) InetAddresses.parseNumericAddress("192.0.2.1");
+    private static final Inet6Address TEST_V6_ADDRESS =
+            (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
     private static final String CHAINNAME = "fw_dozable";
-    private static final S32 UID_RULES_CONFIGURATION_KEY = new S32(0);
-    private static final S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new S32(1);
-    private static final List<Integer> FIREWALL_CHAINS = List.of(
-            FIREWALL_CHAIN_DOZABLE,
-            FIREWALL_CHAIN_STANDBY,
-            FIREWALL_CHAIN_POWERSAVE,
-            FIREWALL_CHAIN_RESTRICTED,
-            FIREWALL_CHAIN_LOW_POWER_STANDBY,
-            FIREWALL_CHAIN_OEM_DENY_1,
-            FIREWALL_CHAIN_OEM_DENY_2,
-            FIREWALL_CHAIN_OEM_DENY_3
-    );
 
     private static final long STATS_SELECT_MAP_A = 0;
     private static final long STATS_SELECT_MAP_B = 1;
 
+    private static final List<Integer> FIREWALL_CHAINS = new ArrayList<>();
+    static {
+        FIREWALL_CHAINS.addAll(ALLOW_CHAINS);
+        FIREWALL_CHAINS.addAll(DENY_CHAINS);
+    }
+
     private BpfNetMaps mBpfNetMaps;
 
     @Mock INetd mNetd;
@@ -139,13 +166,16 @@
     private final IBpfMap<S32, U8> mUidPermissionMap = new TestBpfMap<>(S32.class, U8.class);
     private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap =
             spy(new TestBpfMap<>(CookieTagMapKey.class, CookieTagMapValue.class));
+    private final IBpfMap<S32, U8> mDataSaverEnabledMap = new TestBpfMap<>(S32.class, U8.class);
+    private final IBpfMap<IngressDiscardKey, IngressDiscardValue> mIngressDiscardMap =
+            new TestBpfMap<>(IngressDiscardKey.class, IngressDiscardValue.class);
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         doReturn(TEST_IF_INDEX).when(mDeps).getIfIndex(TEST_IF_NAME);
+        doReturn(TEST_IF_NAME).when(mDeps).getIfName(TEST_IF_INDEX);
         doReturn(0).when(mDeps).synchronizeKernelRCU();
-        BpfNetMaps.setEnableJavaBpfMapForTest(true /* enable */);
         BpfNetMaps.setConfigurationMapForTest(mConfigurationMap);
         mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
         mConfigurationMap.updateEntry(
@@ -153,6 +183,9 @@
         BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
         BpfNetMaps.setUidPermissionMapForTest(mUidPermissionMap);
         BpfNetMaps.setCookieTagMapForTest(mCookieTagMap);
+        BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
+        mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
+        BpfNetMaps.setIngressDiscardMapForTest(mIngressDiscardMap);
         mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps);
     }
 
@@ -170,7 +203,7 @@
     private long getMatch(final List<Integer> chains) {
         long match = 0;
         for (final int chain: chains) {
-            match |= mBpfNetMaps.getMatchByFirewallChain(chain);
+            match |= BpfNetMapsUtils.getMatchByFirewallChain(chain);
         }
         return match;
     }
@@ -239,7 +272,7 @@
     private void doTestSetChildChain(final List<Integer> testChains) throws Exception {
         long expectedMatch = 0;
         for (final int chain: testChains) {
-            expectedMatch |= mBpfNetMaps.getMatchByFirewallChain(chain);
+            expectedMatch |= BpfNetMapsUtils.getMatchByFirewallChain(chain);
         }
 
         assertEquals(0, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
@@ -316,146 +349,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) {
@@ -609,7 +502,7 @@
         mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(TEST_IF_INDEX, IIF_MATCH));
 
         for (final int chain: testChains) {
-            final int ruleToAddMatch = mBpfNetMaps.isFirewallAllowList(chain)
+            final int ruleToAddMatch = BpfNetMapsUtils.isFirewallAllowList(chain)
                     ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
             mBpfNetMaps.setUidRule(chain, TEST_UID, ruleToAddMatch);
         }
@@ -617,7 +510,7 @@
         checkUidOwnerValue(TEST_UID, TEST_IF_INDEX, IIF_MATCH | getMatch(testChains));
 
         for (final int chain: testChains) {
-            final int ruleToRemoveMatch = mBpfNetMaps.isFirewallAllowList(chain)
+            final int ruleToRemoveMatch = BpfNetMapsUtils.isFirewallAllowList(chain)
                     ? FIREWALL_RULE_DENY : FIREWALL_RULE_ALLOW;
             mBpfNetMaps.setUidRule(chain, TEST_UID, ruleToRemoveMatch);
         }
@@ -640,6 +533,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
@@ -697,11 +593,11 @@
         for (final int chain: FIREWALL_CHAINS) {
             final String testCase = "EnabledChains: " + enableChains + " CheckedChain: " + chain;
             if (enableChains.contains(chain)) {
-                final int expectedRule = mBpfNetMaps.isFirewallAllowList(chain)
+                final int expectedRule = BpfNetMapsUtils.isFirewallAllowList(chain)
                         ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
                 assertEquals(testCase, expectedRule, mBpfNetMaps.getUidRule(chain, TEST_UID));
             } else {
-                final int expectedRule = mBpfNetMaps.isFirewallAllowList(chain)
+                final int expectedRule = BpfNetMapsUtils.isFirewallAllowList(chain)
                         ? FIREWALL_RULE_DENY : FIREWALL_RULE_ALLOW;
                 assertEquals(testCase, expectedRule, mBpfNetMaps.getUidRule(chain, TEST_UID));
             }
@@ -744,7 +640,7 @@
     public void testGetUidRuleNoEntry() throws Exception {
         mUidOwnerMap.clear();
         for (final int chain: FIREWALL_CHAINS) {
-            final int expectedRule = mBpfNetMaps.isFirewallAllowList(chain)
+            final int expectedRule = BpfNetMapsUtils.isFirewallAllowList(chain)
                     ? FIREWALL_RULE_DENY : FIREWALL_RULE_ALLOW;
             assertEquals(expectedRule, mBpfNetMaps.getUidRule(chain, TEST_UID));
         }
@@ -954,6 +850,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(
@@ -1061,7 +972,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");
@@ -1071,6 +982,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");
@@ -1119,7 +1031,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");
@@ -1153,6 +1064,21 @@
         assertDumpContains(getDump(), "cookie=123 tag=0x789 uid=456");
     }
 
+    private void doTestDumpDataSaverConfig(final short value, final boolean expected)
+            throws Exception {
+        mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(value));
+        assertDumpContains(getDump(),
+                "sDataSaverEnabledMap: " + expected);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testDumpDataSaverConfig() throws Exception {
+        doTestDumpDataSaverConfig(DATA_SAVER_DISABLED, false);
+        doTestDumpDataSaverConfig(DATA_SAVER_ENABLED, true);
+        doTestDumpDataSaverConfig((short) 2, true);
+    }
+
     @Test
     public void testGetUids() throws ErrnoException {
         final int uid0 = TEST_UIDS[0];
@@ -1181,4 +1107,233 @@
         assertThrows(expected,
                 () -> mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(FIREWALL_CHAIN_OEM_DENY_1));
     }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testSetDataSaverEnabledBeforeT() {
+        for (boolean enable : new boolean[]{true, false}) {
+            assertThrows(UnsupportedOperationException.class,
+                    () -> mBpfNetMaps.setDataSaverEnabled(enable));
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetDataSaverEnabled() throws Exception {
+        for (boolean enable : new boolean[]{true, false}) {
+            mBpfNetMaps.setDataSaverEnabled(enable);
+            assertEquals(enable ? DATA_SAVER_ENABLED : DATA_SAVER_DISABLED,
+                    mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val);
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetIngressDiscardRule_V4address() throws Exception {
+        mBpfNetMaps.setIngressDiscardRule(TEST_V4_ADDRESS, TEST_IF_NAME);
+        final IngressDiscardValue val = mIngressDiscardMap.getValue(new IngressDiscardKey(
+                TEST_V4_ADDRESS));
+        assertEquals(TEST_IF_INDEX, val.iif1);
+        assertEquals(TEST_IF_INDEX, val.iif2);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetIngressDiscardRule_V6address() throws Exception {
+        mBpfNetMaps.setIngressDiscardRule(TEST_V6_ADDRESS, TEST_IF_NAME);
+        final IngressDiscardValue val =
+                mIngressDiscardMap.getValue(new IngressDiscardKey(TEST_V6_ADDRESS));
+        assertEquals(TEST_IF_INDEX, val.iif1);
+        assertEquals(TEST_IF_INDEX, val.iif2);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveIngressDiscardRule() throws Exception {
+        mBpfNetMaps.setIngressDiscardRule(TEST_V4_ADDRESS, TEST_IF_NAME);
+        mBpfNetMaps.setIngressDiscardRule(TEST_V6_ADDRESS, TEST_IF_NAME);
+        final IngressDiscardKey v4Key = new IngressDiscardKey(TEST_V4_ADDRESS);
+        final IngressDiscardKey v6Key = new IngressDiscardKey(TEST_V6_ADDRESS);
+        assertTrue(mIngressDiscardMap.containsKey(v4Key));
+        assertTrue(mIngressDiscardMap.containsKey(v6Key));
+
+        mBpfNetMaps.removeIngressDiscardRule(TEST_V4_ADDRESS);
+        assertFalse(mIngressDiscardMap.containsKey(v4Key));
+        assertTrue(mIngressDiscardMap.containsKey(v6Key));
+
+        mBpfNetMaps.removeIngressDiscardRule(TEST_V6_ADDRESS);
+        assertFalse(mIngressDiscardMap.containsKey(v4Key));
+        assertFalse(mIngressDiscardMap.containsKey(v6Key));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testDumpIngressDiscardRule() throws Exception {
+        mBpfNetMaps.setIngressDiscardRule(TEST_V4_ADDRESS, TEST_IF_NAME);
+        mBpfNetMaps.setIngressDiscardRule(TEST_V6_ADDRESS, TEST_IF_NAME);
+        final String dump = getDump();
+        assertDumpContains(dump, TEST_V4_ADDRESS.getHostAddress());
+        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));
+
+        final int[] coreAids = new int[] {
+                Process.ROOT_UID,
+                Process.SYSTEM_UID,
+                Process.FIRST_APPLICATION_UID - 10,
+                Process.FIRST_APPLICATION_UID - 1,
+        };
+        // 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 : coreAids) {
+                final int uid = UserHandle.getUid(userId, aid);
+                assertFalse(BpfNetMapsUtils.isUidNetworkingBlocked(uid, false, 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..a6dd5c3
--- /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.shrinkedBackingArray)
+        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 57c3acc..999d17d 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -30,6 +30,8 @@
 import static android.Manifest.permission.NETWORK_SETUP_WIZARD;
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.STATUS_BAR_SERVICE;
 import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
 import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -46,16 +48,25 @@
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN;
+import static android.net.ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE;
 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_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;
+import static android.net.ConnectivityManager.EXTRA_IS_ACTIVE;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
+import static android.net.ConnectivityManager.EXTRA_REALTIME_NS;
+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;
@@ -71,15 +82,13 @@
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
-import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
-import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
 import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
-import static android.net.ConnectivityManager.TYPE_PROXY;
 import static android.net.ConnectivityManager.TYPE_VPN;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 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;
@@ -127,6 +136,7 @@
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
 import static android.net.NetworkCapabilities.REDACT_NONE;
+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_TEST;
@@ -143,13 +153,19 @@
 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;
 import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
 import static android.os.Process.INVALID_UID;
 import static android.system.OsConstants.IPPROTO_TCP;
+import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
+import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
 
+import static com.android.server.ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK;
+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.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
@@ -161,6 +177,10 @@
 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;
 import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -209,6 +229,7 @@
 import static org.mockito.Matchers.anyInt;
 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.doReturn;
@@ -246,6 +267,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.ModuleInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -353,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;
@@ -374,11 +395,12 @@
 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;
 import com.android.net.module.util.LocationPermissionChecker;
 import com.android.net.module.util.NetworkMonitorUtils;
@@ -396,6 +418,7 @@
 import com.android.server.connectivity.ClatCoordinator;
 import com.android.server.connectivity.ConnectivityFlags;
 import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.MultinetworkPolicyTracker;
 import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies;
 import com.android.server.connectivity.Nat464Xlat;
@@ -404,10 +427,9 @@
 import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
 import com.android.server.connectivity.ProxyTracker;
 import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.SatelliteAccessController;
+import com.android.server.connectivity.TcpKeepaliveController;
 import com.android.server.connectivity.UidRangeUtils;
-import com.android.server.connectivity.Vpn;
-import com.android.server.connectivity.VpnProfileStore;
-import com.android.server.net.LockdownVpnTracker;
 import com.android.server.net.NetworkPinner;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -441,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;
@@ -459,6 +481,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
@@ -469,6 +492,7 @@
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
@@ -482,8 +506,11 @@
  * Build, install and run with:
  *  runtest frameworks-net -c com.android.server.ConnectivityServiceTest
  */
+// TODO : move methods from this test to smaller tests in the 'connectivityservice' directory
+// to enable faster testing of smaller groups of functionality.
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class ConnectivityServiceTest {
     private static final String TAG = "ConnectivityServiceTest";
@@ -504,7 +531,7 @@
     // between a LOST callback that arrives immediately and a LOST callback that arrives after
     // the linger/nascent timeout. For this, our assertions should run fast enough to leave
     // less than (mService.mLingerDelayMs - TEST_CALLBACK_TIMEOUT_MS) between the time callbacks are
-    // supposedly fired, and the time we call expectCallback.
+    // supposedly fired, and the time we call expectCapChanged.
     private static final int TEST_CALLBACK_TIMEOUT_MS = 250;
     // Chosen to be less than TEST_CALLBACK_TIMEOUT_MS. This ensures that requests have time to
     // complete before callbacks are verified.
@@ -537,10 +564,13 @@
     private static final String WIFI_IFNAME = "test_wlan0";
     private static final String WIFI_WOL_IFNAME = "test_wlan_wol";
     private static final String VPN_IFNAME = "tun10042";
+    private static final String ETHERNET_IFNAME = "eth0";
     private static final String TEST_PACKAGE_NAME = "com.android.test.package";
     private static final int TEST_PACKAGE_UID = 123;
     private static final int TEST_PACKAGE_UID2 = 321;
     private static final int TEST_PACKAGE_UID3 = 456;
+    private static final int NETWORK_ACTIVITY_NO_UID = -1;
+    private static final int TEST_SUBSCRIPTION_ID = 1;
 
     private static final int PACKET_WAKEUP_MARK_MASK = 0x80000000;
 
@@ -569,6 +599,7 @@
     private TestNetworkAgentWrapper mWiFiAgent;
     private TestNetworkAgentWrapper mCellAgent;
     private TestNetworkAgentWrapper mEthernetAgent;
+    private final List<TestNetworkAgentWrapper> mCreatedAgents = new ArrayList<>();
     private MockVpn mMockVpn;
     private Context mContext;
     private NetworkPolicyCallback mPolicyCallback;
@@ -605,7 +636,6 @@
     @Mock TelephonyManager mTelephonyManager;
     @Mock EthernetManager mEthernetManager;
     @Mock NetworkPolicyManager mNetworkPolicyManager;
-    @Mock VpnProfileStore mVpnProfileStore;
     @Mock SystemConfigManager mSystemConfigManager;
     @Mock DevicePolicyManager mDevicePolicyManager;
     @Mock Resources mResources;
@@ -618,11 +648,13 @@
     @Mock ActivityManager mActivityManager;
     @Mock DestroySocketsWrapper mDestroySocketsWrapper;
     @Mock SubscriptionManager mSubscriptionManager;
+    @Mock KeepaliveTracker.Dependencies mMockKeepaliveTrackerDependencies;
+    @Mock SatelliteAccessController mSatelliteAccessController;
 
     // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
     // underlying binder calls.
-    final BatteryStatsManager mBatteryStatsManager =
-            new BatteryStatsManager(mock(IBatteryStats.class));
+    final IBatteryStats mIBatteryStats = mock(IBatteryStats.class);
+    final BatteryStatsManager mBatteryStatsManager = new BatteryStatsManager(mIBatteryStats);
 
     private ArgumentCaptor<ResolverParamsParcel> mResolverParamsParcelCaptor =
             ArgumentCaptor.forClass(ResolverParamsParcel.class);
@@ -743,6 +775,9 @@
             if (Context.TETHERING_SERVICE.equals(name)) return mTetheringManager;
             if (Context.ACTIVITY_SERVICE.equals(name)) return mActivityManager;
             if (Context.TELEPHONY_SUBSCRIPTION_SERVICE.equals(name)) return mSubscriptionManager;
+            // StatsManager is final and can't be mocked, and uses static methods for mostly
+            // everything. The simplest fix is to return null and not have metrics in tests.
+            if (Context.STATS_MANAGER.equals(name)) return null;
             return super.getSystemService(name);
         }
 
@@ -771,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
@@ -888,6 +925,25 @@
             }
             super.sendStickyBroadcast(intent, options);
         }
+
+        private final ArrayTrackRecord<Intent>.ReadHead mOrderedBroadcastAsUserHistory =
+                new ArrayTrackRecord<Intent>().newReadHead();
+
+        public void expectDataActivityBroadcast(int deviceType, boolean isActive, long tsNanos) {
+            assertNotNull(mOrderedBroadcastAsUserHistory.poll(BROADCAST_TIMEOUT_MS,
+                    intent -> intent.getAction().equals(ACTION_DATA_ACTIVITY_CHANGE)
+                            && intent.getIntExtra(EXTRA_DEVICE_TYPE, -1) == deviceType
+                            && intent.getBooleanExtra(EXTRA_IS_ACTIVE, !isActive) == isActive
+                            && intent.getLongExtra(EXTRA_REALTIME_NS, -1) == tsNanos
+            ));
+        }
+
+        @Override
+        public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
+                String receiverPermission, BroadcastReceiver resultReceiver,
+                Handler scheduler, int initialCode, String initialData, Bundle initialExtras) {
+            mOrderedBroadcastAsUserHistory.add(intent);
+        }
     }
 
     // This was only added in the T SDK, but this test needs to build against the R+S SDKs, too.
@@ -896,24 +952,39 @@
         return appUid + (firstSdkSandboxUid - Process.FIRST_APPLICATION_UID);
     }
 
-    // This function assumes the UID range for user 0 ([1, 99999])
-    private static UidRangeParcel[] uidRangeParcelsExcludingUids(Integer... excludedUids) {
-        int start = 1;
-        Arrays.sort(excludedUids);
-        List<UidRangeParcel> parcels = new ArrayList<UidRangeParcel>();
+    // Create the list of ranges for the primary user (User 0), excluding excludedUids.
+    private static List<Range<Integer>> intRangesPrimaryExcludingUids(List<Integer> excludedUids) {
+        final List<Integer> excludedUidsList = new ArrayList<>(excludedUids);
+        // Uid 0 is always excluded
+        if (!excludedUidsList.contains(0)) {
+            excludedUidsList.add(0);
+        }
+        return intRangesExcludingUids(PRIMARY_USER, excludedUidsList);
+    }
+
+    private static List<Range<Integer>> intRangesExcludingUids(int userId,
+            List<Integer> excludedAppIds) {
+        final List<Integer> excludedUids = CollectionUtils.map(excludedAppIds,
+                appId -> UserHandle.getUid(userId, appId));
+        final int userBase = userId * UserHandle.PER_USER_RANGE;
+        final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
+
+        int start = userBase;
+        Collections.sort(excludedUids);
+        final List<Range<Integer>> ranges = new ArrayList<>();
         for (int excludedUid : excludedUids) {
             if (excludedUid == start) {
                 start++;
             } else {
-                parcels.add(new UidRangeParcel(start, excludedUid - 1));
+                ranges.add(new Range<>(start, excludedUid - 1));
                 start = excludedUid + 1;
             }
         }
-        if (start <= 99999) {
-            parcels.add(new UidRangeParcel(start, 99999));
+        if (start <= maxUid) {
+            ranges.add(new Range<>(start, maxUid));
         }
 
-        return parcels.toArray(new UidRangeParcel[0]);
+        return ranges;
     }
 
     private void waitForIdle() {
@@ -984,6 +1055,9 @@
     }
 
     private class TestNetworkAgentWrapper extends NetworkAgentWrapper {
+        // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+        // please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more
+        // tools in ConnectivityServiceTest.
         private static final int VALIDATION_RESULT_INVALID = 0;
 
         private static final long DATA_STALL_TIMESTAMP = 10L;
@@ -1031,6 +1105,7 @@
                 NetworkCapabilities ncTemplate, NetworkProvider provider,
                 NetworkAgentWrapper.Callbacks callbacks) throws Exception {
             super(transport, linkProperties, ncTemplate, provider, callbacks, mServiceContext);
+            mCreatedAgents.add(this);
 
             // Waits for the NetworkAgent to be registered, which includes the creation of the
             // NetworkMonitor.
@@ -1308,6 +1383,9 @@
      * operations have been processed and test for them.
      */
     private static class MockNetworkFactory extends NetworkFactory {
+        // 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.
         private final AtomicBoolean mNetworkStarted = new AtomicBoolean(false);
 
         static class RequestEntry {
@@ -1437,62 +1515,31 @@
         return uidRangesForUids(CollectionUtils.toIntArray(uids));
     }
 
-    private static Looper startHandlerThreadAndReturnLooper() {
-        final HandlerThread handlerThread = new HandlerThread("MockVpnThread");
-        handlerThread.start();
-        return handlerThread.getLooper();
-    }
+    // Helper class to mock vpn interaction.
+    private class MockVpn implements TestableNetworkCallback.HasNetwork {
+        // 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.
 
-    private class MockVpn extends Vpn implements TestableNetworkCallback.HasNetwork {
         // Careful ! This is different from mNetworkAgent, because MockNetworkAgent does
         // not inherit from NetworkAgent.
         private TestNetworkAgentWrapper mMockNetworkAgent;
+        // Initialize a stored NetworkCapabilities following the defaults of VPN. The TransportInfo
+        // should at least be updated to a valid VPN type before usage, see registerAgent(...).
+        private NetworkCapabilities mNetworkCapabilities = new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+                .setTransportInfo(new VpnTransportInfo(
+                        VpnManager.TYPE_VPN_NONE,
+                        null /* sessionId */,
+                        false /* bypassable */,
+                        false /* longLivedTcpConnectionsExpensive */))
+                .build();
         private boolean mAgentRegistered = false;
 
         private int mVpnType = VpnManager.TYPE_VPN_SERVICE;
-        private UnderlyingNetworkInfo mUnderlyingNetworkInfo;
-
-        // These ConditionVariables allow tests to wait for LegacyVpnRunner to be stopped/started.
-        // TODO: this scheme is ad-hoc and error-prone because it does not fail if, for example, the
-        // test expects two starts in a row, or even if the production code calls start twice in a
-        // row. find a better solution. Simply putting a method to create a LegacyVpnRunner into
-        // Vpn.Dependencies doesn't work because LegacyVpnRunner is not a static class and has
-        // extensive access into the internals of Vpn.
-        private ConditionVariable mStartLegacyVpnCv = new ConditionVariable();
-        private ConditionVariable mStopVpnRunnerCv = new ConditionVariable();
-
-        public MockVpn(int userId) {
-            super(startHandlerThreadAndReturnLooper(), mServiceContext,
-                    new Dependencies() {
-                        @Override
-                        public boolean isCallerSystem() {
-                            return true;
-                        }
-
-                        @Override
-                        public DeviceIdleInternal getDeviceIdleInternal() {
-                            return mDeviceIdleInternal;
-                        }
-                    },
-                    mNetworkManagementService, mMockNetd, userId, mVpnProfileStore,
-                    new SystemServices(mServiceContext) {
-                        @Override
-                        public String settingsSecureGetStringForUser(String key, int userId) {
-                            switch (key) {
-                                // Settings keys not marked as @Readable are not readable from
-                                // non-privileged apps, unless marked as testOnly=true
-                                // (atest refuses to install testOnly=true apps), even if mocked
-                                // in the content provider, because
-                                // Settings.Secure.NameValueCache#getStringForUser checks the key
-                                // before querying the mock settings provider.
-                                case Settings.Secure.ALWAYS_ON_VPN_APP:
-                                    return null;
-                                default:
-                                    return super.settingsSecureGetStringForUser(key, userId);
-                            }
-                        }
-                    }, new Ikev2SessionCreator());
-        }
+        private String mSessionKey;
 
         public void setUids(Set<UidRange> uids) {
             mNetworkCapabilities.setUids(UidRange.toIntRanges(uids));
@@ -1505,7 +1552,6 @@
             mVpnType = vpnType;
         }
 
-        @Override
         public Network getNetwork() {
             return (mMockNetworkAgent == null) ? null : mMockNetworkAgent.getNetwork();
         }
@@ -1514,7 +1560,6 @@
             return null == mMockNetworkAgent ? null : mMockNetworkAgent.getNetworkAgentConfig();
         }
 
-        @Override
         public int getActiveVpnType() {
             return mVpnType;
         }
@@ -1528,14 +1573,11 @@
         private void registerAgent(boolean isAlwaysMetered, Set<UidRange> uids, LinkProperties lp)
                 throws Exception {
             if (mAgentRegistered) throw new IllegalStateException("already registered");
-            updateState(NetworkInfo.DetailedState.CONNECTING, "registerAgent");
-            mConfig = new VpnConfig();
-            mConfig.session = "MySession12345";
+            final String session = "MySession12345";
             setUids(uids);
             if (!isAlwaysMetered) mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
-            mInterface = VPN_IFNAME;
             mNetworkCapabilities.setTransportInfo(new VpnTransportInfo(getActiveVpnType(),
-                    mConfig.session));
+                    session));
             mMockNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_VPN, lp,
                     mNetworkCapabilities);
             mMockNetworkAgent.waitForIdle(TIMEOUT_MS);
@@ -1549,9 +1591,7 @@
             mAgentRegistered = true;
             verify(mMockNetd).networkCreate(nativeNetworkConfigVpn(getNetwork().netId,
                     !mMockNetworkAgent.isBypassableVpn(), mVpnType));
-            updateState(NetworkInfo.DetailedState.CONNECTED, "registerAgent");
             mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities());
-            mNetworkAgent = mMockNetworkAgent.getNetworkAgent();
         }
 
         private void registerAgent(Set<UidRange> uids) throws Exception {
@@ -1611,57 +1651,50 @@
         public void disconnect() {
             if (mMockNetworkAgent != null) {
                 mMockNetworkAgent.disconnect();
-                updateState(NetworkInfo.DetailedState.DISCONNECTED, "disconnect");
             }
             mAgentRegistered = false;
             setUids(null);
             // Remove NET_CAPABILITY_INTERNET or MockNetworkAgent will refuse to connect later on.
             mNetworkCapabilities.removeCapability(NET_CAPABILITY_INTERNET);
-            mInterface = null;
         }
 
-        @Override
-        public void startLegacyVpnRunner() {
-            mStartLegacyVpnCv.open();
+        private void startLegacyVpn() {
+            // Do nothing.
         }
 
-        public void expectStartLegacyVpnRunner() {
-            assertTrue("startLegacyVpnRunner not called after " + TIMEOUT_MS + " ms",
-                    mStartLegacyVpnCv.block(TIMEOUT_MS));
-
-            // startLegacyVpn calls stopVpnRunnerPrivileged, which will open mStopVpnRunnerCv, just
-            // before calling startLegacyVpnRunner. Restore mStopVpnRunnerCv, so the test can expect
-            // that the VpnRunner is stopped and immediately restarted by calling
-            // expectStartLegacyVpnRunner() and expectStopVpnRunnerPrivileged() back-to-back.
-            mStopVpnRunnerCv = new ConditionVariable();
+        // Mock the interaction of IkeV2VpnRunner start. In the context of ConnectivityService,
+        // setVpnDefaultForUids() is the main interaction and a sessionKey is stored.
+        private void startPlatformVpn() {
+            mSessionKey = UUID.randomUUID().toString();
+            // Assuming no disallowed applications
+            final Set<Range<Integer>> ranges = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
+            mCm.setVpnDefaultForUids(mSessionKey, ranges);
+            // Wait for vpn network preference updates.
+            waitForIdle();
         }
 
-        @Override
-        public void stopVpnRunnerPrivileged() {
-            if (mVpnRunner != null) {
-                super.stopVpnRunnerPrivileged();
-                disconnect();
-                mStartLegacyVpnCv = new ConditionVariable();
+        public void startLegacyVpnPrivileged(boolean isIkev2Vpn) {
+            if (isIkev2Vpn) {
+                startPlatformVpn();
+            } else {
+                startLegacyVpn();
             }
-            mVpnRunner = null;
-            mStopVpnRunnerCv.open();
         }
 
-        public void expectStopVpnRunnerPrivileged() {
-            assertTrue("stopVpnRunnerPrivileged not called after " + TIMEOUT_MS + " ms",
-                    mStopVpnRunnerCv.block(TIMEOUT_MS));
+        public void stopVpnRunnerPrivileged() {
+            if (mSessionKey != null) {
+                // Clear vpn network preference.
+                mCm.setVpnDefaultForUids(mSessionKey, Collections.EMPTY_LIST);
+                mSessionKey = null;
+            }
+            disconnect();
         }
 
-        @Override
-        public synchronized UnderlyingNetworkInfo getUnderlyingNetworkInfo() {
-            if (mUnderlyingNetworkInfo != null) return mUnderlyingNetworkInfo;
-
-            return super.getUnderlyingNetworkInfo();
-        }
-
-        private synchronized void setUnderlyingNetworkInfo(
-                UnderlyingNetworkInfo underlyingNetworkInfo) {
-            mUnderlyingNetworkInfo = underlyingNetworkInfo;
+        public boolean setUnderlyingNetworks(@Nullable Network[] networks) {
+            if (!mAgentRegistered) return false;
+            mMockNetworkAgent.setUnderlyingNetworks(
+                    (networks == null) ? null : Arrays.asList(networks));
+            return true;
         }
     }
 
@@ -1674,6 +1707,12 @@
         return ranges.stream().map(r -> new UidRangeParcel(r, r)).toArray(UidRangeParcel[]::new);
     }
 
+    private static UidRangeParcel[] intToUidRangeStableParcels(
+            final @NonNull List<Range<Integer>> ranges) {
+        return ranges.stream().map(
+                r -> new UidRangeParcel(r.getLower(), r.getUpper())).toArray(UidRangeParcel[]::new);
+    }
+
     private void assertVpnTransportInfo(NetworkCapabilities nc, int type) {
         assertNotNull(nc);
         final TransportInfo ti = nc.getTransportInfo();
@@ -1688,14 +1727,11 @@
         waitForIdle();
     }
 
-    private void mockVpn(int uid) {
-        int userId = UserHandle.getUserId(uid);
-        mMockVpn = new MockVpn(userId);
-    }
-
     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) {
@@ -1711,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) {
@@ -1806,6 +1850,8 @@
     private static final UserHandle TERTIARY_USER_HANDLE = new UserHandle(TERTIARY_USER);
 
     private static final int RESTRICTED_USER = 1;
+    private static final UidRange RESTRICTED_USER_UIDRANGE =
+            UidRange.createForUser(UserHandle.of(RESTRICTED_USER));
     private static final UserInfo RESTRICTED_USER_INFO = new UserInfo(RESTRICTED_USER, "",
             UserInfo.FLAG_RESTRICTED);
     static {
@@ -1820,6 +1866,9 @@
 
         MockitoAnnotations.initMocks(this);
 
+        // 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.
         doReturn(asList(PRIMARY_USER_INFO)).when(mUserManager).getAliveUsers();
         doReturn(asList(PRIMARY_USER_HANDLE)).when(mUserManager).getUserHandles(anyBoolean());
         doReturn(PRIMARY_USER_INFO).when(mUserManager).getUserInfo(PRIMARY_USER);
@@ -1856,6 +1905,7 @@
         mServiceContext.setPermission(CONTROL_OEM_PAID_NETWORK_PREFERENCE, PERMISSION_GRANTED);
         mServiceContext.setPermission(PACKET_KEEPALIVE_OFFLOAD, PERMISSION_GRANTED);
         mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_GRANTED);
+        mServiceContext.setPermission(READ_DEVICE_CONFIG, PERMISSION_GRANTED);
 
         mAlarmManagerThread = new HandlerThread("TestAlarmManager");
         mAlarmManagerThread.start();
@@ -1870,6 +1920,12 @@
         doReturn(mResources).when(mockResContext).getResources();
         ConnectivityResources.setResourcesContextForTest(mockResContext);
         mDeps = new ConnectivityServiceDependencies(mockResContext);
+        doReturn(true).when(mMockKeepaliveTrackerDependencies)
+                .isAddressTranslationEnabled(mServiceContext);
+        doReturn(new ConnectivityResources(mockResContext)).when(mMockKeepaliveTrackerDependencies)
+                .createConnectivityResources(mServiceContext);
+        doReturn(new int[] {1, 3, 0, 0}).when(mMockKeepaliveTrackerDependencies)
+                .getSupportedKeepalives(mServiceContext);
         mAutoOnOffKeepaliveDependencies =
                 new AutomaticOnOffKeepaliveTrackerDependencies(mServiceContext);
         mService = new ConnectivityService(mServiceContext,
@@ -1880,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.
@@ -1892,7 +1953,7 @@
         mService.systemReadyInternal();
         verify(mMockDnsResolver).registerUnsolicitedEventListener(any());
 
-        mockVpn(Process.myUid());
+        mMockVpn = new MockVpn();
         mCm.bindProcessToNetwork(null);
         mQosCallbackTracker = mock(QosCallbackTracker.class);
 
@@ -1900,6 +1961,13 @@
         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.
     }
 
     private void initMockedResources() {
@@ -1936,12 +2004,15 @@
         final ConnectivityResources mConnRes;
         final ArraySet<Pair<Long, Integer>> mEnabledChangeIds = new ArraySet<>();
 
+        // 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.
         ConnectivityServiceDependencies(final Context mockResContext) {
             mConnRes = new ConnectivityResources(mockResContext);
         }
 
         @Override
-        public HandlerThread makeHandlerThread() {
+        public HandlerThread makeHandlerThread(@NonNull final String tag) {
             return mCsHandlerThread;
         }
 
@@ -1997,13 +2068,28 @@
             };
         }
 
+        private BiConsumer<Integer, Integer> mCarrierPrivilegesLostListener;
+
         @Override
         public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
-                @NonNull final Context context, @NonNull final TelephonyManager tm) {
+                @NonNull final Context context,
+                @NonNull final TelephonyManager tm,
+                final boolean requestRestrictedWifiEnabled,
+                BiConsumer<Integer, Integer> listener,
+                @NonNull final Handler handler) {
+            mCarrierPrivilegesLostListener = listener;
             return mDeps.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
         }
 
         @Override
+        public SatelliteAccessController makeSatelliteAccessController(
+                @NonNull final Context context,
+                Consumer<Set<Integer>> updateSatelliteNetworkFallbackUidCallback,
+                @NonNull final Handler connectivityServiceInternalHandler) {
+            return mSatelliteAccessController;
+        }
+
+        @Override
         public boolean intentFilterEquals(final PendingIntent a, final PendingIntent b) {
             return runAsShell(GET_INTENT_SENDER_INTENT, () -> a.intentFilterEquals(b));
         }
@@ -2091,7 +2177,10 @@
         public boolean isFeatureEnabled(Context context, String name) {
             switch (name) {
                 case ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER:
-                    return true;
+                case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
+                case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
+                case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
+                case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
                 case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
                     return true;
                 default:
@@ -2099,6 +2188,24 @@
             }
         }
 
+        @Override
+        public boolean isFeatureNotChickenedOut(Context context, String name) {
+            switch (name) {
+                case ALLOW_SYSUI_CONNECTIVITY_REPORTS:
+                    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);
+            }
+        }
+
         public void setChangeIdEnabled(final boolean enabled, final long changeId, final int uid) {
             final Pair<Long, Integer> data = new Pair<>(changeId, uid);
             // mEnabledChangeIds is read on the handler thread and maybe the test thread, so
@@ -2191,6 +2298,11 @@
         }
 
         @Override
+        public int getBpfProgramId(final int attachType) {
+            return 0;
+        }
+
+        @Override
         public BroadcastOptionsShim makeBroadcastOptionsShim(BroadcastOptions options) {
             reset(mBroadcastOptionsShim);
             return mBroadcastOptionsShim;
@@ -2244,15 +2356,17 @@
         @Override @SuppressWarnings("DirectInvocationOnMock")
         public void destroyLiveTcpSocketsByOwnerUids(final Set<Integer> ownerUids) {
             // Call mocked destroyLiveTcpSocketsByOwnerUids so that test can verify this method call
-            mDestroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(ownerUids);
+            // Create copy of ownerUids so that tests can verify the correct value even if the
+            // ConnectivityService update the ownerUids after this method call.
+            mDestroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(new ArraySet<>(ownerUids));
         }
 
-        final ArrayTrackRecord<Long>.ReadHead mScheduledEvaluationTimeouts =
-                new ArrayTrackRecord<Long>().newReadHead();
+        final ArrayTrackRecord<Pair<Integer, Long>>.ReadHead mScheduledEvaluationTimeouts =
+                new ArrayTrackRecord<Pair<Integer, Long>>().newReadHead();
         @Override
         public void scheduleEvaluationTimeout(@NonNull Handler handler,
                 @NonNull final Network network, final long delayMs) {
-            mScheduledEvaluationTimeouts.add(delayMs);
+            mScheduledEvaluationTimeouts.add(new Pair<>(network.netId, delayMs));
             super.scheduleEvaluationTimeout(handler, network, delayMs);
         }
     }
@@ -2264,11 +2378,17 @@
         }
 
         @Override
-        public boolean isFeatureEnabled(@NonNull final String name, final boolean defaultEnabled) {
+        public boolean isTetheringFeatureNotChickenedOut(@NonNull final String name) {
             // Tests for enabling the feature are verified in AutomaticOnOffKeepaliveTrackerTest.
             // Assuming enabled here to focus on ConnectivityService tests.
             return true;
         }
+        public KeepaliveTracker newKeepaliveTracker(@NonNull Context context,
+                @NonNull Handler connectivityserviceHander) {
+            return new KeepaliveTracker(context, connectivityserviceHander,
+                    new TcpKeepaliveController(connectivityserviceHander),
+                    mMockKeepaliveTrackerDependencies);
+        }
     }
 
     private static void initAlarmManager(final AlarmManager am, final Handler alarmHandler) {
@@ -2325,6 +2445,11 @@
         FakeSettingsProvider.clearSettingsProvider();
         ConnectivityResources.setResourcesContextForTest(null);
 
+        for (TestNetworkAgentWrapper agent : mCreatedAgents) {
+            agent.destroy();
+        }
+        mCreatedAgents.clear();
+
         mCsHandlerThread.quitSafely();
         mCsHandlerThread.join();
         mAlarmManagerThread.quitSafely();
@@ -2335,6 +2460,7 @@
         final String myPackageName = mContext.getPackageName();
         final PackageInfo myPackageInfo = mContext.getPackageManager().getPackageInfo(
                 myPackageName, PackageManager.GET_PERMISSIONS);
+        myPackageInfo.setLongVersionCode(9_999_999L);
         doReturn(new String[] {myPackageName}).when(mPackageManager)
                 .getPackagesForUid(Binder.getCallingUid());
         doReturn(myPackageInfo).when(mPackageManager).getPackageInfoAsUser(
@@ -2346,6 +2472,13 @@
                 buildPackageInfo(/* SYSTEM */ false, VPN_UID)
         })).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
 
+        final ModuleInfo moduleInfo = new ModuleInfo();
+        moduleInfo.setPackageName(TETHERING_MODULE_NAME);
+        doReturn(moduleInfo).when(mPackageManager)
+                .getModuleInfo(TETHERING_MODULE_NAME, PackageManager.MODULE_APEX_NAME);
+        doReturn(myPackageInfo).when(mPackageManager)
+                .getPackageInfo(TETHERING_MODULE_NAME, PackageManager.MATCH_APEX);
+
         // Create a fake always-on VPN package.
         final int userId = UserHandle.getCallingUserId();
         final ApplicationInfo applicationInfo = new ApplicationInfo();
@@ -2535,23 +2668,6 @@
     }
 
     @Test
-    public void testNetworkTypes() {
-        // Ensure that our mocks for the networkAttributes config variable work as expected. If they
-        // don't, then tests that depend on CONNECTIVITY_ACTION broadcasts for these network types
-        // will fail. Failing here is much easier to debug.
-        assertTrue(mCm.isNetworkSupported(TYPE_WIFI));
-        assertTrue(mCm.isNetworkSupported(TYPE_MOBILE));
-        assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_MMS));
-        assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_FOTA));
-        assertFalse(mCm.isNetworkSupported(TYPE_PROXY));
-
-        // Check that TYPE_ETHERNET is supported. Unlike the asserts above, which only validate our
-        // mocks, this assert exercises the ConnectivityService code path that ensures that
-        // TYPE_ETHERNET is supported if the ethernet service is running.
-        assertTrue(mCm.isNetworkSupported(TYPE_ETHERNET));
-    }
-
-    @Test
     public void testNetworkFeature() throws Exception {
         // Connect the cell agent and wait for the connected broadcast.
         mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
@@ -2756,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.
@@ -4234,7 +4350,9 @@
         testFactory.terminate();
         testFactory.assertNoRequestChanged();
         if (networkCallback != null) mCm.unregisterNetworkCallback(networkCallback);
-        handlerThread.quit();
+
+        handlerThread.quitSafely();
+        handlerThread.join();
     }
 
     @Test
@@ -4295,6 +4413,8 @@
         expectNoRequestChanged(testFactoryAll); // still seeing the request
 
         mWiFiAgent.disconnect();
+        handlerThread.quitSafely();
+        handlerThread.join();
     }
 
     @Test
@@ -4328,7 +4448,8 @@
                 }
             }
         }
-        handlerThread.quit();
+        handlerThread.quitSafely();
+        handlerThread.join();
     }
 
     @Test
@@ -4819,6 +4940,34 @@
     }
 
     @Test
+    public void testNoAvoidCaptivePortalOnWearProxy() throws Exception {
+        // Bring up a BLUETOOTH network which is companion proxy on wear
+        // then set captive portal.
+        mockHasSystemFeature(PackageManager.FEATURE_WATCH, true);
+        setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID);
+        TestNetworkAgentWrapper btAgent = new TestNetworkAgentWrapper(TRANSPORT_BLUETOOTH);
+        final String firstRedirectUrl = "http://example.com/firstPath";
+
+        btAgent.connectWithCaptivePortal(firstRedirectUrl, false /* privateDnsProbeSent */);
+        btAgent.assertNotDisconnected(TIMEOUT_MS);
+    }
+
+    @Test
+    public void testAvoidCaptivePortalOnBluetooth() throws Exception {
+        // When not on Wear, BLUETOOTH is just regular network,
+        // then set captive portal.
+        mockHasSystemFeature(PackageManager.FEATURE_WATCH, false);
+        setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID);
+        TestNetworkAgentWrapper btAgent = new TestNetworkAgentWrapper(TRANSPORT_BLUETOOTH);
+        final String firstRedirectUrl = "http://example.com/firstPath";
+
+        btAgent.connectWithCaptivePortal(firstRedirectUrl, false /* privateDnsProbeSent */);
+
+        btAgent.expectDisconnected();
+        btAgent.expectPreventReconnectReceived();
+    }
+
+    @Test
     public void testCaptivePortalApi() throws Exception {
         mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
 
@@ -5259,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 =
@@ -5921,7 +6070,8 @@
             testFactory.assertNoRequestChanged();
         } finally {
             mCm.unregisterNetworkCallback(cellNetworkCallback);
-            handlerThread.quit();
+            handlerThread.quitSafely();
+            handlerThread.join();
         }
     }
 
@@ -6124,7 +6274,7 @@
     }
 
     public void doTestPreferBadWifi(final boolean avoidBadWifi,
-            final boolean preferBadWifi,
+            final boolean preferBadWifi, final boolean explicitlySelected,
             @NonNull Predicate<Long> checkUnvalidationTimeout) throws Exception {
         // Pretend we're on a carrier that restricts switching away from bad wifi, and
         // depending on the parameter one that may indeed prefer bad wifi.
@@ -6148,10 +6298,13 @@
         mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellAgent);
 
         mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiAgent.explicitlySelected(explicitlySelected, false /* acceptUnvalidated */);
         mWiFiAgent.connect(false);
         wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
 
-        mDeps.mScheduledEvaluationTimeouts.poll(TIMEOUT_MS, t -> checkUnvalidationTimeout.test(t));
+        assertNotNull(mDeps.mScheduledEvaluationTimeouts.poll(TIMEOUT_MS,
+                t -> t.first == mWiFiAgent.getNetwork().netId
+                        && checkUnvalidationTimeout.test(t.second)));
 
         if (!avoidBadWifi && preferBadWifi) {
             expectUnvalidationCheckWillNotify(mWiFiAgent, NotificationType.LOST_INTERNET);
@@ -6167,27 +6320,33 @@
         // Starting with U this mode is no longer supported and can't actually be tested
         assumeFalse(mDeps.isAtLeastU());
         doTestPreferBadWifi(false /* avoidBadWifi */, false /* preferBadWifi */,
-                timeout -> timeout < 14_000);
+                false /* explicitlySelected */, timeout -> timeout < 14_000);
     }
 
     @Test
-    public void testPreferBadWifi_doNotAvoid_doPrefer() throws Exception {
+    public void testPreferBadWifi_doNotAvoid_doPrefer_notExplicit() throws Exception {
         doTestPreferBadWifi(false /* avoidBadWifi */, true /* preferBadWifi */,
-                timeout -> timeout > 14_000);
+                false /* explicitlySelected */, timeout -> timeout > 14_000);
+    }
+
+    @Test
+    public void testPreferBadWifi_doNotAvoid_doPrefer_explicitlySelected() throws Exception {
+        doTestPreferBadWifi(false /* avoidBadWifi */, true /* preferBadWifi */,
+                true /* explicitlySelected */, timeout -> timeout < 14_000);
     }
 
     @Test
     public void testPreferBadWifi_doAvoid_doNotPrefer() throws Exception {
         // If avoidBadWifi=true, then preferBadWifi should be irrelevant. Test anyway.
         doTestPreferBadWifi(true /* avoidBadWifi */, false /* preferBadWifi */,
-                timeout -> timeout < 14_000);
+                false /* explicitlySelected */, timeout -> timeout < 14_000);
     }
 
     @Test
     public void testPreferBadWifi_doAvoid_doPrefer() throws Exception {
         // If avoidBadWifi=true, then preferBadWifi should be irrelevant. Test anyway.
         doTestPreferBadWifi(true /* avoidBadWifi */, true /* preferBadWifi */,
-                timeout -> timeout < 14_000);
+                false /* explicitlySelected */, timeout -> timeout < 14_000);
     }
 
     @Test
@@ -6502,7 +6661,8 @@
             }
         } finally {
             testFactory.terminate();
-            handlerThread.quit();
+            handlerThread.quitSafely();
+            handlerThread.join();
         }
     }
 
@@ -6821,17 +6981,19 @@
 
     @Test
     public void testPacketKeepalives() throws Exception {
-        InetAddress myIPv4 = InetAddress.getByName("192.0.2.129");
+        final LinkAddress v4Addr = new LinkAddress("192.0.2.129/24");
+        final InetAddress myIPv4 = v4Addr.getAddress();
         InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35");
         InetAddress myIPv6 = InetAddress.getByName("2001:db8::1");
         InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8");
         InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888");
-
+        doReturn(getClatInterfaceConfigParcel(v4Addr)).when(mMockNetd)
+                .interfaceGetCfg(CLAT_MOBILE_IFNAME);
         final int validKaInterval = 15;
         final int invalidKaInterval = 9;
 
         LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName("wlan12");
+        lp.setInterfaceName(MOBILE_IFNAME);
         lp.addLinkAddress(new LinkAddress(myIPv6, 64));
         lp.addLinkAddress(new LinkAddress(myIPv4, 25));
         lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234")));
@@ -6856,10 +7018,6 @@
         ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv6, 1234, dstIPv4);
         callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
 
-        // NAT-T is only supported for IPv4.
-        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv6, 1234, dstIPv6);
-        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
-
         ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 123456, dstIPv4);
         callback.expectError(PacketKeepalive.ERROR_INVALID_PORT);
 
@@ -7010,13 +7168,6 @@
             callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
         }
 
-        // NAT-T is only supported for IPv4.
-        try (SocketKeepalive ka = mCm.createSocketKeepalive(
-                myNet, testSocket, myIPv6, dstIPv6, executor, callback)) {
-            ka.start(validKaInterval);
-            callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
-        }
-
         // Basic check before testing started keepalive.
         try (SocketKeepalive ka = mCm.createSocketKeepalive(
                 myNet, testSocket, myIPv4, dstIPv4, executor, callback)) {
@@ -7390,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<>();
@@ -7505,6 +7656,19 @@
             NetworkCallback networkCallback = new NetworkCallback();
             mCm.requestNetwork(networkRequest, networkCallback);
             mCm.unregisterNetworkCallback(networkCallback);
+            // While requestNetwork increases the count synchronously, unregister decreases it
+            // asynchronously on a handler, so unregistering doesn't immediately free up
+            // a slot : calling unregister-register when max requests are registered throws.
+            // Potential fix : ConnectivityService catches TooManyRequestsException once when
+            // creating NetworkRequestInfo and waits for handler thread (see
+            // https://r.android.com/2707373 for impl). However, this complexity is not equal to
+            // the issue ; the purpose of having "max requests" is only to help apps detect leaks.
+            // Apps relying on exact enforcement or rapid request registration should reconsider.
+            //
+            // In this test, test thread registering all before handler thread decrements can cause
+            // flakes. A single waitForIdle at (e.g.) MAX_REQUESTS / 2 processes decrements up to
+            // that point, fixing the flake.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
         waitForIdle();
 
@@ -7512,6 +7676,8 @@
             NetworkCallback networkCallback = new NetworkCallback();
             mCm.registerNetworkCallback(networkRequest, networkCallback);
             mCm.unregisterNetworkCallback(networkCallback);
+            // See comment above for the reasons for this wait.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
         waitForIdle();
 
@@ -7519,6 +7685,8 @@
             NetworkCallback networkCallback = new NetworkCallback();
             mCm.registerDefaultNetworkCallback(networkCallback);
             mCm.unregisterNetworkCallback(networkCallback);
+            // See comment above for the reasons for this wait.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
         waitForIdle();
 
@@ -7526,6 +7694,8 @@
             NetworkCallback networkCallback = new NetworkCallback();
             mCm.registerDefaultNetworkCallback(networkCallback);
             mCm.unregisterNetworkCallback(networkCallback);
+            // See comment above for the reasons for this wait.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
         waitForIdle();
 
@@ -7535,6 +7705,8 @@
                 mCm.registerDefaultNetworkCallbackForUid(1000000 + i, networkCallback,
                         new Handler(ConnectivityThread.getInstanceLooper()));
                 mCm.unregisterNetworkCallback(networkCallback);
+                // See comment above for the reasons for this wait.
+                if (MAX_REQUESTS / 2 == i) waitForIdle();
             }
         });
         waitForIdle();
@@ -7544,6 +7716,8 @@
                     mContext, 0 /* requestCode */, new Intent("e" + i), FLAG_IMMUTABLE);
             mCm.requestNetwork(networkRequest, pendingIntent);
             mCm.unregisterNetworkCallback(pendingIntent);
+            // See comment above for the reasons for this wait.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
         waitForIdle();
 
@@ -7552,6 +7726,8 @@
                     mContext, 0 /* requestCode */, new Intent("f" + i), FLAG_IMMUTABLE);
             mCm.registerNetworkCallback(networkRequest, pendingIntent);
             mCm.unregisterNetworkCallback(pendingIntent);
+            // See comment above for the reasons for this wait.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
     }
 
@@ -9009,6 +9185,18 @@
         mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback);
         vpnNetworkCallback.assertNoCallback();
 
+        // Lingering timer is short and cell might be disconnected if the device is particularly
+        // slow running the test, unless it's requested. Make sure the networks the test needs
+        // are all requested.
+        final NetworkCallback cellCallback = new NetworkCallback() {};
+        final NetworkCallback wifiCallback = new NetworkCallback() {};
+        mCm.requestNetwork(
+                new NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build(),
+                cellCallback);
+        mCm.requestNetwork(
+                new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(),
+                wifiCallback);
+
         mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */,
                 false /* privateDnsProbeSent */);
         assertUidRangesUpdatedForMyUid(true);
@@ -9165,6 +9353,8 @@
         assertDefaultNetworkCapabilities(userId /* no networks */);
 
         mMockVpn.disconnect();
+        mCm.unregisterNetworkCallback(cellCallback);
+        mCm.unregisterNetworkCallback(wifiCallback);
     }
 
     @Test
@@ -9270,11 +9460,11 @@
                         && c.hasTransport(TRANSPORT_WIFI));
         callback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
 
-        doReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID)).when(mPackageManager)
-                .getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER);
-
-        // New user added
-        mMockVpn.onUserAdded(RESTRICTED_USER);
+        // New user added, this updates the Vpn uids, coverage in VpnTest.
+        // This is equivalent to `mMockVpn.onUserAdded(RESTRICTED_USER);`
+        final Set<UidRange> ranges = uidRangesForUids(uid);
+        ranges.add(RESTRICTED_USER_UIDRANGE);
+        mMockVpn.setUids(ranges);
 
         // Expect that the VPN UID ranges contain both |uid| and the UID range for the newly-added
         // restricted user.
@@ -9299,7 +9489,9 @@
                 && !c.hasTransport(TRANSPORT_WIFI));
 
         // User removed and expect to lose the UID range for the restricted user.
-        mMockVpn.onUserRemoved(RESTRICTED_USER);
+        // This updates the Vpn uids, coverage in VpnTest.
+        // This is equivalent to `mMockVpn.onUserRemoved(RESTRICTED_USER);`
+        mMockVpn.setUids(uidRangesForUids(uid));
 
         // Expect that the VPN gains the UID range for the restricted user, and that the capability
         // change made just before that (i.e., loss of TRANSPORT_WIFI) is preserved.
@@ -9340,8 +9532,16 @@
         assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
 
         // Enable always-on VPN lockdown. The main user loses network access because no VPN is up.
-        final ArrayList<String> allowList = new ArrayList<>();
-        mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+        // Coverage in VpnTest.
+        final List<Integer> excludedUids = new ArrayList<>();
+        excludedUids.add(VPN_UID);
+        if (mDeps.isAtLeastT()) {
+            // On T onwards, the corresponding SDK sandbox UID should also be excluded
+            excludedUids.add(toSdkSandboxUid(VPN_UID));
+        }
+        final List<Range<Integer>> primaryRanges = intRangesPrimaryExcludingUids(excludedUids);
+        mCm.setRequireVpnForUids(true, primaryRanges);
+
         waitForIdle();
         assertNull(mCm.getActiveNetworkForUid(uid));
         // This is arguably overspecified: a UID that is not running doesn't have an active network.
@@ -9350,32 +9550,28 @@
         assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
 
         // Start the restricted profile, and check that the UID within it loses network access.
-        doReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID)).when(mPackageManager)
-                .getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER);
-        doReturn(asList(PRIMARY_USER_INFO, RESTRICTED_USER_INFO)).when(mUserManager)
-                .getAliveUsers();
         // TODO: check that VPN app within restricted profile still has access, etc.
-        mMockVpn.onUserAdded(RESTRICTED_USER);
-        final Intent addedIntent = new Intent(ACTION_USER_ADDED);
-        addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER));
-        addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER);
-        processBroadcast(addedIntent);
+        // Add a restricted user.
+        // This is equivalent to `mMockVpn.onUserAdded(RESTRICTED_USER);`, coverage in VpnTest.
+        final List<Range<Integer>> restrictedRanges =
+                intRangesExcludingUids(RESTRICTED_USER, excludedUids);
+        mCm.setRequireVpnForUids(true, restrictedRanges);
+        waitForIdle();
+
         assertNull(mCm.getActiveNetworkForUid(uid));
         assertNull(mCm.getActiveNetworkForUid(restrictedUid));
 
         // Stop the restricted profile, and check that the UID within it has network access again.
-        doReturn(asList(PRIMARY_USER_INFO)).when(mUserManager).getAliveUsers();
+        // Remove the restricted user.
+        // This is equivalent to `mMockVpn.onUserRemoved(RESTRICTED_USER);`, coverage in VpnTest.
+        mCm.setRequireVpnForUids(false, restrictedRanges);
+        waitForIdle();
 
-        // Send a USER_REMOVED broadcast and expect to lose the UID range for the restricted user.
-        mMockVpn.onUserRemoved(RESTRICTED_USER);
-        final Intent removedIntent = new Intent(ACTION_USER_REMOVED);
-        removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER));
-        removedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER);
-        processBroadcast(removedIntent);
         assertNull(mCm.getActiveNetworkForUid(uid));
         assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
 
-        mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+        mCm.setRequireVpnForUids(false, primaryRanges);
+
         waitForIdle();
     }
 
@@ -9685,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);
     }
 
@@ -9828,18 +10046,20 @@
                 new Handler(ConnectivityThread.getInstanceLooper()));
 
         final int uid = Process.myUid();
-        final ArrayList<String> allowList = new ArrayList<>();
-        mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
-        waitForIdle();
 
-        final Set<Integer> excludedUids = new ArraySet<Integer>();
+        // Enable always-on VPN lockdown, coverage in VpnTest.
+        final List<Integer> excludedUids = new ArrayList<Integer>();
         excludedUids.add(VPN_UID);
         if (mDeps.isAtLeastT()) {
             // On T onwards, the corresponding SDK sandbox UID should also be excluded
             excludedUids.add(toSdkSandboxUid(VPN_UID));
         }
-        final UidRangeParcel[] uidRangeParcels = uidRangeParcelsExcludingUids(
-                excludedUids.toArray(new Integer[0]));
+
+        final List<Range<Integer>> primaryRanges = intRangesPrimaryExcludingUids(excludedUids);
+        mCm.setRequireVpnForUids(true, primaryRanges);
+        waitForIdle();
+
+        final UidRangeParcel[] uidRangeParcels = intToUidRangeStableParcels(primaryRanges);
         InOrder inOrder = inOrder(mMockNetd);
         expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
 
@@ -9859,7 +10079,8 @@
         assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
 
         // Disable lockdown, expect to see the network unblocked.
-        mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+        mCm.setRequireVpnForUids(false, primaryRanges);
+        waitForIdle();
         callback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> !cb.getBlocked());
         defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> !cb.getBlocked());
         vpnUidCallback.assertNoCallback();
@@ -9872,22 +10093,25 @@
         assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
         assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
 
-        // Add our UID to the allowlist and re-enable lockdown, expect network is not blocked.
-        allowList.add(TEST_PACKAGE_NAME);
-        mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+        // Add our UID to the allowlist, expect network is not blocked. Coverage in VpnTest.
+        excludedUids.add(uid);
+        if (mDeps.isAtLeastT()) {
+            // On T onwards, the corresponding SDK sandbox UID should also be excluded
+            excludedUids.add(toSdkSandboxUid(uid));
+        }
+        final List<Range<Integer>> primaryRangesExcludingUid =
+                intRangesPrimaryExcludingUids(excludedUids);
+        mCm.setRequireVpnForUids(true, primaryRangesExcludingUid);
+        waitForIdle();
+
         callback.assertNoCallback();
         defaultCallback.assertNoCallback();
         vpnUidCallback.assertNoCallback();
         vpnUidDefaultCallback.assertNoCallback();
         vpnDefaultCallbackAsUid.assertNoCallback();
 
-        excludedUids.add(uid);
-        if (mDeps.isAtLeastT()) {
-            // On T onwards, the corresponding SDK sandbox UID should also be excluded
-            excludedUids.add(toSdkSandboxUid(uid));
-        }
-        final UidRangeParcel[] uidRangeParcelsAlsoExcludingUs = uidRangeParcelsExcludingUids(
-                excludedUids.toArray(new Integer[0]));
+        final UidRangeParcel[] uidRangeParcelsAlsoExcludingUs =
+                intToUidRangeStableParcels(primaryRangesExcludingUid);
         expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcelsAlsoExcludingUs);
         assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
         assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetwork());
@@ -9910,15 +10134,15 @@
         assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
         assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
 
-        // Disable lockdown, remove our UID from the allowlist, and re-enable lockdown.
-        // Everything should now be blocked.
-        mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+        // Disable lockdown
+        mCm.setRequireVpnForUids(false, primaryRangesExcludingUid);
         waitForIdle();
         expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcelsAlsoExcludingUs);
-        allowList.clear();
-        mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+        // Remove our UID from the allowlist, and re-enable lockdown.
+        mCm.setRequireVpnForUids(true, primaryRanges);
         waitForIdle();
         expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
+        // Everything should now be blocked.
         defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> cb.getBlocked());
         assertBlockedCallbackInAnyOrder(callback, true, mWiFiAgent, mCellAgent);
         vpnUidCallback.assertNoCallback();
@@ -9931,7 +10155,7 @@
         assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
 
         // Disable lockdown. Everything is unblocked.
-        mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+        mCm.setRequireVpnForUids(false, primaryRanges);
         defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> !cb.getBlocked());
         assertBlockedCallbackInAnyOrder(callback, false, mWiFiAgent, mCellAgent);
         vpnUidCallback.assertNoCallback();
@@ -9943,36 +10167,8 @@
         assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
         assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
 
-        // Enable and disable an always-on VPN package without lockdown. Expect no changes.
-        reset(mMockNetd);
-        mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, false /* lockdown */, allowList);
-        inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any());
-        callback.assertNoCallback();
-        defaultCallback.assertNoCallback();
-        vpnUidCallback.assertNoCallback();
-        vpnUidDefaultCallback.assertNoCallback();
-        vpnDefaultCallbackAsUid.assertNoCallback();
-        assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
-        assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetwork());
-        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
-        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
-        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
-
-        mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
-        inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any());
-        callback.assertNoCallback();
-        defaultCallback.assertNoCallback();
-        vpnUidCallback.assertNoCallback();
-        vpnUidDefaultCallback.assertNoCallback();
-        vpnDefaultCallbackAsUid.assertNoCallback();
-        assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
-        assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetwork());
-        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
-        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
-        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
-
         // Enable lockdown and connect a VPN. The VPN is not blocked.
-        mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+        mCm.setRequireVpnForUids(true, primaryRanges);
         defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> cb.getBlocked());
         assertBlockedCallbackInAnyOrder(callback, true, mWiFiAgent, mCellAgent);
         vpnUidCallback.assertNoCallback();
@@ -10058,22 +10254,6 @@
         doAsUid(Process.SYSTEM_UID, () -> mCm.unregisterNetworkCallback(perUidCb));
     }
 
-    private VpnProfile setupLegacyLockdownVpn() {
-        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.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK;
-        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);
@@ -10085,8 +10265,8 @@
         mMockVpn.connect(true);
     }
 
-    @Test
-    public void testLegacyLockdownVpn() throws Exception {
+    private void doTestLockdownVpn(boolean isIkev2Vpn)
+            throws Exception {
         mServiceContext.setPermission(
                 Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
 
@@ -10101,107 +10281,63 @@
         mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback,
                 new Handler(ConnectivityThread.getInstanceLooper()));
 
-        // Pretend lockdown VPN was configured.
-        final VpnProfile profile = setupLegacyLockdownVpn();
-
-        // LockdownVpnTracker disables the Vpn teardown code and enables lockdown.
-        // Check the VPN's state before it does so.
-        assertTrue(mMockVpn.getEnableTeardown());
-        assertFalse(mMockVpn.getLockdown());
-
-        // VMSHandlerThread was used inside VpnManagerService and taken into LockDownVpnTracker.
-        // VpnManagerService was decoupled from this test but this handlerThread is still required
-        // in LockDownVpnTracker. Keep it until LockDownVpnTracker related verification is moved to
-        // its own test.
-        final HandlerThread VMSHandlerThread = new HandlerThread("TestVpnManagerService");
-        VMSHandlerThread.start();
-
-        // LockdownVpnTracker is created from VpnManagerService but VpnManagerService is decoupled
-        // from ConnectivityServiceTest. Create it directly to simulate LockdownVpnTracker is
-        // created.
-        // TODO: move LockdownVpnTracker related tests to its own test.
-        // Lockdown VPN disables teardown and enables lockdown.
-        final LockdownVpnTracker lockdownVpnTracker = new LockdownVpnTracker(mServiceContext,
-                VMSHandlerThread.getThreadHandler(), mMockVpn, profile);
-        lockdownVpnTracker.init();
-        assertFalse(mMockVpn.getEnableTeardown());
-        assertTrue(mMockVpn.getLockdown());
+        // Init lockdown state to simulate LockdownVpnTracker behavior.
+        mCm.setLegacyLockdownVpnEnabled(true);
+        final List<Range<Integer>> ranges =
+                intRangesPrimaryExcludingUids(Collections.EMPTY_LIST /* excludedeUids */);
+        mCm.setRequireVpnForUids(true /* requireVpn */, ranges);
 
         // Bring up a network.
-        // Expect nothing to happen because the network does not have an IPv4 default route: legacy
-        // VPN only supports IPv4.
         final LinkProperties cellLp = new LinkProperties();
         cellLp.setInterfaceName("rmnet0");
-        cellLp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
-        cellLp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, "rmnet0"));
-        mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
-        mCellAgent.connect(false /* validated */);
-        callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
-        defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
-        systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
-        waitForIdle();
-        assertNull(mMockVpn.getAgent());
-
-        // Add an IPv4 address. Ideally the VPN should start, but it doesn't because nothing calls
-        // LockdownVpnTracker#handleStateChangedLocked. This is a bug.
-        // TODO: consider fixing this.
         cellLp.addLinkAddress(new LinkAddress("192.0.2.2/25"));
         cellLp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, "rmnet0"));
-        mCellAgent.sendLinkProperties(cellLp);
-        callback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
-        defaultCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
-        systemDefaultCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
-        waitForIdle();
-        assertNull(mMockVpn.getAgent());
-
-        // Disconnect, then try again with a network that supports IPv4 at connection time.
-        // Expect lockdown VPN to come up.
-        ExpectedBroadcast b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
-        mCellAgent.disconnect();
-        callback.expect(LOST, mCellAgent);
-        defaultCallback.expect(LOST, mCellAgent);
-        systemDefaultCallback.expect(LOST, mCellAgent);
-        b1.expectBroadcast();
-
         // When lockdown VPN is active, the NetworkInfo state in CONNECTIVITY_ACTION is overwritten
         // with the state of the VPN network. So expect a CONNECTING broadcast.
-        b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING);
+        final ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING);
         mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
         mCellAgent.connect(false /* validated */);
         callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
         defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
         systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
-        b1.expectBroadcast();
-        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        b.expectBroadcast();
+        // Simulate LockdownVpnTracker attempting to start the VPN since it received the
+        // systemDefault callback.
+        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);
+            // Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
+            // as the network satisfier.
+            assertNull(mCm.getActiveNetworkInfo());
+        } else {
+            assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        }
         assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
         assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
         assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED);
         assertExtraInfoFromCmBlocked(mCellAgent);
 
-        // TODO: it would be nice if we could simply rely on the production code here, and have
-        // LockdownVpnTracker start the VPN, have the VPN code register its NetworkAgent with
-        // ConnectivityService, etc. That would require duplicating a fair bit of code from the
-        // Vpn tests around how to mock out LegacyVpnRunner. But even if we did that, this does not
-        // work for at least two reasons:
-        // 1. In this test, calling registerNetworkAgent does not actually result in an agent being
-        //    registered. This is because nothing calls onNetworkMonitorCreated, which is what
-        //    actually ends up causing handleRegisterNetworkAgent to be called. Code in this test
-        //    that wants to register an agent must use TestNetworkAgentWrapper.
-        // 2. Even if we exposed Vpn#agentConnect to the test, and made MockVpn#agentConnect call
-        //    the TestNetworkAgentWrapper code, this would deadlock because the
-        //    TestNetworkAgentWrapper code cannot be called on the handler thread since it calls
-        //    waitForIdle().
-        mMockVpn.expectStartLegacyVpnRunner();
-        b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
-        ExpectedBroadcast b2 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
+        final ExpectedBroadcast b2 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
+        final ExpectedBroadcast b3 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
         establishLegacyLockdownVpn(mCellAgent.getNetwork());
         callback.expectAvailableThenValidatedCallbacks(mMockVpn);
         defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
         systemDefaultCallback.assertNoCallback();
-        NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
-        b1.expectBroadcast();
+        final NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
         b2.expectBroadcast();
-        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        b3.expectBroadcast();
+        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);
+        } else {
+            // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+            // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+            // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+            assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        }
         assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
         assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED);
         assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10222,53 +10358,79 @@
         wifiNc.addCapability(NET_CAPABILITY_NOT_METERED);
         mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp, wifiNc);
 
-        b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        final ExpectedBroadcast b4 =
+                expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
         // Wifi is CONNECTING because the VPN isn't up yet.
-        b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING);
-        ExpectedBroadcast b3 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+        final ExpectedBroadcast b5 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING);
         mWiFiAgent.connect(false /* validated */);
-        b1.expectBroadcast();
-        b2.expectBroadcast();
-        b3.expectBroadcast();
-        mMockVpn.expectStopVpnRunnerPrivileged();
-        mMockVpn.expectStartLegacyVpnRunner();
-
-        // TODO: why is wifi not blocked? Is it because when this callback is sent, the VPN is still
-        // connected, so the network is not considered blocked by the lockdown UID ranges? But the
-        // fact that a VPN is connected should only result in the VPN itself being unblocked, not
-        // any other network. Bug in isUidBlockedByVpn?
+        // Wifi is not blocked since VPN network is still connected.
         callback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+        defaultCallback.assertNoCallback();
+        systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+        b4.expectBroadcast();
+        b5.expectBroadcast();
+
+        // Simulate LockdownVpnTracker restarting the VPN since it received the systemDefault
+        // callback with different network.
+        final ExpectedBroadcast b6 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+        mMockVpn.stopVpnRunnerPrivileged();
+
+        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);
-        systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+        if (isIkev2Vpn) {
+            // setVpnDefaultForUids() releases the original network request and creates a VPN
+            // request so LOST callback is received.
+            defaultCallback.expect(LOST, mWiFiAgent);
+        }
+        systemDefaultCallback.assertNoCallback();
+        b6.expectBroadcast();
 
         // While the VPN is reconnecting on the new network, everything is blocked.
-        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+        if (isIkev2Vpn) {
+            // Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
+            // as the network satisfier.
+            assertNull(mCm.getActiveNetworkInfo());
+        } else {
+            assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+        }
         assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
         assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
         assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED);
         assertExtraInfoFromCmBlocked(mWiFiAgent);
 
         // The VPN comes up again on wifi.
-        b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
-        b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
+        final ExpectedBroadcast b7 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
+        final ExpectedBroadcast b8 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
         establishLegacyLockdownVpn(mWiFiAgent.getNetwork());
         callback.expectAvailableThenValidatedCallbacks(mMockVpn);
         defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
         systemDefaultCallback.assertNoCallback();
-        b1.expectBroadcast();
-        b2.expectBroadcast();
-        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        b7.expectBroadcast();
+        b8.expectBroadcast();
+        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);
+        } else {
+            // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+            // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+            // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+            assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        }
         assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
         assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
         assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
         assertExtraInfoFromCmPresent(mWiFiAgent);
-        vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
-        assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
-        assertTrue(vpnNc.hasTransport(TRANSPORT_WIFI));
-        assertFalse(vpnNc.hasTransport(TRANSPORT_CELLULAR));
-        assertTrue(vpnNc.hasCapability(NET_CAPABILITY_NOT_METERED));
+        final NetworkCapabilities vpnNc2 = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+        assertTrue(vpnNc2.hasTransport(TRANSPORT_VPN));
+        assertTrue(vpnNc2.hasTransport(TRANSPORT_WIFI));
+        assertFalse(vpnNc2.hasTransport(TRANSPORT_CELLULAR));
+        assertTrue(vpnNc2.hasCapability(NET_CAPABILITY_NOT_METERED));
 
         // Disconnect cell. Nothing much happens since it's not the default network.
         mCellAgent.disconnect();
@@ -10276,30 +10438,59 @@
         defaultCallback.assertNoCallback();
         systemDefaultCallback.assertNoCallback();
 
-        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        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);
+        } else {
+            // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+            // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+            // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+            assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        }
         assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
         assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
         assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
         assertExtraInfoFromCmPresent(mWiFiAgent);
 
-        b1 = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
-        b2 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+        final ExpectedBroadcast b9 =
+                expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
+        final ExpectedBroadcast b10 =
+                expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
         mWiFiAgent.disconnect();
         callback.expect(LOST, mWiFiAgent);
-        systemDefaultCallback.expect(LOST, mWiFiAgent);
-        b1.expectBroadcast();
         callback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
-        mMockVpn.expectStopVpnRunnerPrivileged();
+        defaultCallback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
+        systemDefaultCallback.expect(LOST, mWiFiAgent);
+        // TODO: There should only be one LOST callback. Since the WIFI network is underlying a VPN
+        // network, ConnectivityService#propagateUnderlyingNetworkCapabilities() causes a rematch to
+        // occur. Notably, this happens before setting the satisfiers of its network requests to
+        // null. Since the satisfiers are set to null in the rematch, an extra LOST callback is
+        // called.
+        systemDefaultCallback.expect(LOST, mWiFiAgent);
+        b9.expectBroadcast();
+        mMockVpn.stopVpnRunnerPrivileged();
         callback.expect(LOST, mMockVpn);
-        b2.expectBroadcast();
+        defaultCallback.expect(LOST, mMockVpn);
+        b10.expectBroadcast();
 
-        VMSHandlerThread.quitSafely();
-        VMSHandlerThread.join();
+        assertNoCallbacks(callback, defaultCallback, systemDefaultCallback);
+    }
+
+    @Test
+    public void testLockdownVpn_LegacyVpnRunner() throws Exception {
+        doTestLockdownVpn(false /* isIkev2Vpn */);
+    }
+
+    @Test
+    public void testLockdownVpn_Ikev2VpnRunner() throws Exception {
+        doTestLockdownVpn(true /* isIkev2Vpn */);
     }
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testLockdownSetFirewallUidRule() throws Exception {
-        final Set<Range<Integer>> lockdownRange = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
+        final List<Range<Integer>> lockdownRange =
+                intRangesPrimaryExcludingUids(Collections.EMPTY_LIST /* excludedeUids */);
         // Enable Lockdown
         mCm.setRequireVpnForUids(true /* requireVpn */, lockdownRange);
         waitForIdle();
@@ -10349,14 +10540,21 @@
         doTestSetUidFirewallRule(FIREWALL_CHAIN_POWERSAVE, FIREWALL_RULE_DENY);
         doTestSetUidFirewallRule(FIREWALL_CHAIN_RESTRICTED, FIREWALL_RULE_DENY);
         doTestSetUidFirewallRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, 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,
@@ -10364,7 +10562,11 @@
                 FIREWALL_CHAIN_LOW_POWER_STANDBY,
                 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 */);
@@ -10407,13 +10609,15 @@
         final boolean allowlist = true;
         final boolean denylist = false;
 
-        doReturn(true).when(mBpfNetMaps).isFirewallAllowList(anyInt());
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_DOZABLE, allowlist);
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_POWERSAVE, allowlist);
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_RESTRICTED, allowlist);
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_LOW_POWER_STANDBY, allowlist);
+        if (SdkLevel.isAtLeastV()) {
+            // FIREWALL_CHAIN_BACKGROUND is only available on V+.
+            doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_BACKGROUND, allowlist);
+        }
 
-        doReturn(false).when(mBpfNetMaps).isFirewallAllowList(anyInt());
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_STANDBY, denylist);
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_OEM_DENY_1, denylist);
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_OEM_DENY_2, denylist);
@@ -10434,6 +10638,10 @@
         doTestReplaceFirewallChain(FIREWALL_CHAIN_POWERSAVE);
         doTestReplaceFirewallChain(FIREWALL_CHAIN_RESTRICTED);
         doTestReplaceFirewallChain(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+        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);
@@ -10666,6 +10874,11 @@
         expectNativeNetworkCreated(netId, permission, iface, null /* inOrder */);
     }
 
+    private int getIdleTimerLabel(int netId, int transportType) {
+        return ConnectivityService.LegacyNetworkActivityTracker.getIdleTimerLabel(
+                mDeps.isAtLeastV(), netId, transportType);
+    }
+
     @Test
     public void testStackedLinkProperties() throws Exception {
         final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24");
@@ -10681,6 +10894,8 @@
         final RouteInfo ipv4Subnet = new RouteInfo(myIpv4, null, MOBILE_IFNAME);
         final RouteInfo stackedDefault =
                 new RouteInfo((IpPrefix) null, myIpv4.getAddress(), CLAT_MOBILE_IFNAME);
+        final BaseNetdUnsolicitedEventListener netdUnsolicitedListener =
+                getRegisteredNetdUnsolicitedEventListener();
 
         final NetworkRequest networkRequest = new NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
@@ -10748,7 +10963,6 @@
         assertRoutesRemoved(cellNetId, ipv4Subnet);
 
         // When NAT64 prefix discovery succeeds, LinkProperties are updated and clatd is started.
-        Nat464Xlat clat = getNat464Xlat(mCellAgent);
         assertNull(mCm.getLinkProperties(mCellAgent.getNetwork()).getNat64Prefix());
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
                 makeNat64PrefixEvent(cellNetId, PREFIX_OPERATION_ADDED, kNat64PrefixString, 96));
@@ -10759,7 +10973,8 @@
         verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
 
         // Clat iface comes up. Expect stacked link to be added.
-        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
+        netdUnsolicitedListener.onInterfaceLinkStateChanged(
+                CLAT_MOBILE_IFNAME, true);
         networkCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
         List<LinkProperties> stackedLps = mCm.getLinkProperties(mCellAgent.getNetwork())
                 .getStackedLinks();
@@ -10805,7 +11020,7 @@
                 kOtherNat64Prefix.toString());
         networkCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent,
                 cb -> cb.getLp().getNat64Prefix().equals(kOtherNat64Prefix));
-        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
+        netdUnsolicitedListener.onInterfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
         networkCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent,
                 cb -> cb.getLp().getStackedLinks().size() == 1);
         assertRoutesAdded(cellNetId, stackedDefault);
@@ -10833,7 +11048,7 @@
         assertRoutesRemoved(cellNetId, stackedDefault);
 
         // The interface removed callback happens but has no effect after stop is called.
-        clat.interfaceRemoved(CLAT_MOBILE_IFNAME);
+        netdUnsolicitedListener.onInterfaceRemoved(CLAT_MOBILE_IFNAME);
         networkCallback.assertNoCallback();
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
 
@@ -10870,7 +11085,7 @@
         verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
 
         // Clat iface comes up. Expect stacked link to be added.
-        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
+        netdUnsolicitedListener.onInterfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
         networkCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent,
                 cb -> cb.getLp().getStackedLinks().size() == 1
                         && cb.getLp().getNat64Prefix() != null);
@@ -10905,7 +11120,7 @@
         networkCallback.expect(LOST, mCellAgent);
         networkCallback.assertNoCallback();
         verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(Integer.toString(getIdleTimerLabel(cellNetId, TRANSPORT_CELLULAR))));
         verify(mMockNetd).networkDestroy(cellNetId);
         if (mDeps.isAtLeastU()) {
             verify(mMockNetd).setNetworkAllowlist(any());
@@ -10938,8 +11153,7 @@
 
         // Clatd is started and clat iface comes up. Expect stacked link to be added.
         verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
-        clat = getNat464Xlat(mCellAgent);
-        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true /* up */);
+        netdUnsolicitedListener.onInterfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true /* up */);
         networkCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent,
                 cb -> cb.getLp().getStackedLinks().size() == 1
                         && cb.getLp().getNat64Prefix().equals(kNat64Prefix));
@@ -10965,7 +11179,7 @@
         }
 
         verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(Integer.toString(getIdleTimerLabel(cellNetId, TRANSPORT_CELLULAR))));
         verify(mMockNetd).networkDestroy(cellNetId);
         if (mDeps.isAtLeastU()) {
             verify(mMockNetd).setNetworkAllowlist(any());
@@ -11195,6 +11409,179 @@
         assertTrue("Nat464Xlat was not IDLE", !clat.isStarted());
     }
 
+    final String transportToTestIfaceName(int transport) {
+        switch (transport) {
+            case TRANSPORT_WIFI:
+                return WIFI_IFNAME;
+            case TRANSPORT_CELLULAR:
+                return MOBILE_IFNAME;
+            case TRANSPORT_ETHERNET:
+                return ETHERNET_IFNAME;
+            default:
+                throw new AssertionError("Unsupported transport type");
+        }
+    }
+
+    private void doTestInterfaceClassActivityChanged(final int transportType) throws Exception {
+        final BaseNetdUnsolicitedEventListener netdUnsolicitedEventListener =
+                getRegisteredNetdUnsolicitedEventListener();
+
+        final int legacyType = transportToLegacyType(transportType);
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(transportToTestIfaceName(transportType));
+        final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(transportType, lp);
+
+        final ConditionVariable onNetworkActiveCv = new ConditionVariable();
+        final ConnectivityManager.OnNetworkActiveListener listener = onNetworkActiveCv::open;
+
+        TestNetworkCallback defaultCallback = new TestNetworkCallback();
+
+        testAndCleanup(() -> {
+            mCm.registerDefaultNetworkCallback(defaultCallback);
+            agent.connect(true);
+            defaultCallback.expectAvailableThenValidatedCallbacks(agent);
+            if (transportType == TRANSPORT_CELLULAR) {
+                verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                        anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+            } else if (transportType == TRANSPORT_WIFI) {
+                verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                        anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+            }
+            clearInvocations(mIBatteryStats);
+            final int idleTimerLabel = getIdleTimerLabel(agent.getNetwork().netId, transportType);
+
+            // Network is considered active when the network becomes the default network.
+            assertTrue(mCm.isDefaultNetworkActive());
+
+            mCm.addDefaultNetworkActiveListener(listener);
+
+            // Interface goes to inactive state
+            netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                    idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+            mServiceContext.expectDataActivityBroadcast(legacyType, false /* isActive */,
+                    TIMESTAMP);
+            assertFalse(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
+            assertFalse(mCm.isDefaultNetworkActive());
+            if (mDeps.isAtLeastV()) {
+                if (transportType == TRANSPORT_CELLULAR) {
+                    verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_LOW),
+                            anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+                } else if (transportType == TRANSPORT_WIFI) {
+                    verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_LOW),
+                            anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+                }
+            } else {
+                // If TrackMultiNetworks is disabled, LegacyNetworkActivityTracker does not call
+                // BatteryStats API by the netd activity change callback since BatteryStatsService
+                // listen to netd callback via NetworkManagementService and update battery stats by
+                // itself.
+                verify(mIBatteryStats, never())
+                        .noteMobileRadioPowerState(anyInt(), anyLong(), anyInt());
+                verify(mIBatteryStats, never())
+                        .noteWifiRadioPowerState(anyInt(), anyLong(), anyInt());
+            }
+
+            // Interface goes to active state
+            netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+                    idleTimerLabel, TIMESTAMP, TEST_PACKAGE_UID);
+            mServiceContext.expectDataActivityBroadcast(legacyType, true /* isActive */, TIMESTAMP);
+            assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
+            assertTrue(mCm.isDefaultNetworkActive());
+            if (mDeps.isAtLeastV()) {
+                if (transportType == TRANSPORT_CELLULAR) {
+                    verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                            anyLong() /* timestampNs */, eq(TEST_PACKAGE_UID));
+                } else if (transportType == TRANSPORT_WIFI) {
+                    verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                            anyLong() /* timestampNs */, eq(TEST_PACKAGE_UID));
+                }
+            } else {
+                // If TrackMultiNetworks is disabled, LegacyNetworkActivityTracker does not call
+                // BatteryStats API by the netd activity change callback since BatteryStatsService
+                // listen to netd callback via NetworkManagementService and update battery stats by
+                // itself.
+                verify(mIBatteryStats, never())
+                        .noteMobileRadioPowerState(anyInt(), anyLong(), anyInt());
+                verify(mIBatteryStats, never())
+                        .noteWifiRadioPowerState(anyInt(), anyLong(), anyInt());
+            }
+        }, () -> { // Cleanup
+                mCm.unregisterNetworkCallback(defaultCallback);
+            }, () -> { // Cleanup
+                mCm.removeDefaultNetworkActiveListener(listener);
+            }, () -> { // Cleanup
+                agent.disconnect();
+            });
+    }
+
+    @Test
+    public void testInterfaceClassActivityChangedWifi() throws Exception {
+        doTestInterfaceClassActivityChanged(TRANSPORT_WIFI);
+    }
+
+    @Test
+    public void testInterfaceClassActivityChangedCellular() throws Exception {
+        doTestInterfaceClassActivityChanged(TRANSPORT_CELLULAR);
+    }
+
+    private void doTestOnNetworkActive_NewNetworkConnects(int transportType, boolean expectCapChanged)
+            throws Exception {
+        final ConditionVariable onNetworkActiveCv = new ConditionVariable();
+        final ConnectivityManager.OnNetworkActiveListener listener = onNetworkActiveCv::open;
+
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(transportToTestIfaceName(transportType));
+        final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(transportType, lp);
+
+        testAndCleanup(() -> {
+            mCm.addDefaultNetworkActiveListener(listener);
+            agent.connect(true);
+            if (expectCapChanged) {
+                assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
+            } else {
+                assertFalse(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
+            }
+            assertTrue(mCm.isDefaultNetworkActive());
+        }, () -> { // Cleanup
+                mCm.removeDefaultNetworkActiveListener(listener);
+            }, () -> { // Cleanup
+                agent.disconnect();
+            });
+    }
+
+    @Test
+    public void testOnNetworkActive_NewCellConnects_CallbackCalled() throws Exception {
+        doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_CELLULAR, true /* expectCapChanged */);
+    }
+
+    @Test
+    public void testOnNetworkActive_NewEthernetConnects_Callback() throws Exception {
+        // On pre-V devices, LegacyNetworkActivityTracker calls onNetworkActive callback only for
+        // networks that tracker adds the idle timer to. And the tracker does not set the idle timer
+        // for the ethernet network.
+        // So onNetworkActive is not called when the ethernet becomes the default network
+        final boolean expectCapChanged = mDeps.isAtLeastV();
+        doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCapChanged);
+    }
+
+    @Test
+    public void testIsDefaultNetworkActiveNoDefaultNetwork() throws Exception {
+        // isDefaultNetworkActive returns true if there is no default network, which is known issue.
+        assertTrue(mCm.isDefaultNetworkActive());
+
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName(MOBILE_IFNAME);
+        mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+        mCellAgent.connect(true);
+        // Network is considered active when the network becomes the default network.
+        assertTrue(mCm.isDefaultNetworkActive());
+
+        mCellAgent.disconnect();
+        waitForIdle();
+
+        assertTrue(mCm.isDefaultNetworkActive());
+    }
+
     @Test
     public void testDataActivityTracking() throws Exception {
         final TestNetworkCallback networkCallback = new TestNetworkCallback();
@@ -11204,15 +11591,19 @@
         mCm.registerNetworkCallback(networkRequest, networkCallback);
 
         mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        final String cellIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+                mCellAgent.getNetwork().netId, TRANSPORT_CELLULAR));
         final LinkProperties cellLp = new LinkProperties();
         cellLp.setInterfaceName(MOBILE_IFNAME);
         mCellAgent.sendLinkProperties(cellLp);
         mCellAgent.connect(true);
         networkCallback.expectAvailableThenValidatedCallbacks(mCellAgent);
         verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(cellIdleTimerLabel));
 
         mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        String wifiIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+                mWiFiAgent.getNetwork().netId, TRANSPORT_WIFI));
         final LinkProperties wifiLp = new LinkProperties();
         wifiLp.setInterfaceName(WIFI_IFNAME);
         mWiFiAgent.sendLinkProperties(wifiLp);
@@ -11223,9 +11614,18 @@
         networkCallback.expectLosing(mCellAgent);
         networkCallback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
         verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_WIFI)));
-        verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(wifiIdleTimerLabel));
+        if (mDeps.isAtLeastV()) {
+            // V+ devices add idleTimer when the network is first connected and remove when the
+            // network is disconnected.
+            verify(mMockNetd, never()).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(mCellAgent.getNetwork().netId)));
+        } else {
+            // pre V devices add idleTimer when the network becomes the default network and remove
+            // when the network becomes no longer the default network.
+            verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(TRANSPORT_CELLULAR)));
+        }
 
         // Disconnect wifi and switch back to cell
         reset(mMockNetd);
@@ -11233,13 +11633,20 @@
         networkCallback.expect(LOST, mWiFiAgent);
         assertNoCallbacks(networkCallback);
         verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_WIFI)));
-        verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(wifiIdleTimerLabel));
+        if (mDeps.isAtLeastV()) {
+            verify(mMockNetd, never()).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(mCellAgent.getNetwork().netId)));
+        } else {
+            verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(TRANSPORT_CELLULAR)));
+        }
 
         // reconnect wifi
         reset(mMockNetd);
         mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        wifiIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+                mWiFiAgent.getNetwork().netId, TRANSPORT_WIFI));
         wifiLp.setInterfaceName(WIFI_IFNAME);
         mWiFiAgent.sendLinkProperties(wifiLp);
         mWiFiAgent.connect(true);
@@ -11247,20 +11654,30 @@
         networkCallback.expectLosing(mCellAgent);
         networkCallback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
         verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_WIFI)));
-        verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+                eq(wifiIdleTimerLabel));
+        if (mDeps.isAtLeastV()) {
+            verify(mMockNetd, never()).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(mCellAgent.getNetwork().netId)));
+        } else {
+            verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(TRANSPORT_CELLULAR)));
+        }
 
         // Disconnect cell
         reset(mMockNetd);
         mCellAgent.disconnect();
         networkCallback.expect(LOST, mCellAgent);
-        // LOST callback is triggered earlier than removing idle timer. Broadcast should also be
-        // sent as network being switched. Ensure rule removal for cell will not be triggered
-        // unexpectedly before network being removed.
         waitForIdle();
-        verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_CELLULAR)));
+        if (mDeps.isAtLeastV()) {
+            verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(mCellAgent.getNetwork().netId)));
+        }  else {
+            // LOST callback is triggered earlier than removing idle timer. Broadcast should also be
+            // sent as network being switched. Ensure rule removal for cell will not be triggered
+            // unexpectedly before network being removed.
+            verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                    eq(Integer.toString(TRANSPORT_CELLULAR)));
+        }
         verify(mMockNetd, times(1)).networkDestroy(eq(mCellAgent.getNetwork().netId));
         verify(mMockDnsResolver, times(1)).destroyNetworkCache(eq(mCellAgent.getNetwork().netId));
 
@@ -11269,12 +11686,27 @@
         mWiFiAgent.disconnect();
         b.expectBroadcast();
         verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
-                eq(Integer.toString(TRANSPORT_WIFI)));
+                eq(wifiIdleTimerLabel));
 
         // Clean up
         mCm.unregisterNetworkCallback(networkCallback);
     }
 
+    @Test
+    public void testDataActivityTracking_VpnNetwork() throws Exception {
+        mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiAgent.connect(true /* validated */);
+        mMockVpn.setUnderlyingNetworks(new Network[] { mWiFiAgent.getNetwork() });
+
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(VPN_IFNAME);
+        mMockVpn.establishForMyUid(lp);
+
+        // NetworkActivityTracker should not track the VPN network since VPN can change the
+        // underlying network without disconnect.
+        verify(mMockNetd, never()).idletimerAddInterface(eq(VPN_IFNAME), anyInt(), any());
+    }
+
     private void verifyTcpBufferSizeChange(String tcpBufferSizes) throws Exception {
         String[] values = tcpBufferSizes.split(",");
         String rmemValues = String.join(" ", values[0], values[1], values[2]);
@@ -12296,9 +12728,6 @@
         mMockVpn.establish(new LinkProperties(), vpnOwnerUid, vpnRange);
         assertVpnUidRangesUpdated(true, vpnRange, vpnOwnerUid);
 
-        final UnderlyingNetworkInfo underlyingNetworkInfo =
-                new UnderlyingNetworkInfo(vpnOwnerUid, VPN_IFNAME, new ArrayList<>());
-        mMockVpn.setUnderlyingNetworkInfo(underlyingNetworkInfo);
         mDeps.setConnectionOwnerUid(42);
     }
 
@@ -12542,7 +12971,8 @@
 
     private NetworkAgentInfo fakeNai(NetworkCapabilities nc, NetworkInfo networkInfo) {
         return new NetworkAgentInfo(null, new Network(NET_ID), networkInfo, new LinkProperties(),
-                nc, new NetworkScore.Builder().setLegacyInt(0).build(),
+                nc, null /* localNetworkConfig */,
+                new NetworkScore.Builder().setLegacyInt(0).build(),
                 mServiceContext, null, new NetworkAgentConfig(), mService, null, null, 0,
                 INVALID_UID, TEST_LINGER_DELAY_MS, mQosCallbackTracker,
                 new ConnectivityService.Dependencies());
@@ -12555,7 +12985,19 @@
         mServiceContext.setPermission(NETWORK_STACK, PERMISSION_GRANTED);
         assertTrue(
                 "NetworkStack permission not applied",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
+                        Process.myPid(), Process.myUid(), naiWithoutUid,
+                        mContext.getOpPackageName()));
+    }
+
+    @Test
+    public void testCheckConnectivityDiagnosticsPermissionsSysUi() throws Exception {
+        final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities());
+
+        mServiceContext.setPermission(STATUS_BAR_SERVICE, PERMISSION_GRANTED);
+        assertTrue(
+                "SysUi permission (STATUS_BAR_SERVICE) not applied",
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), naiWithoutUid,
                         mContext.getOpPackageName()));
     }
@@ -12572,7 +13014,7 @@
 
         assertFalse(
                 "Mismatched uid/package name should not pass the location permission check",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid() + 1, wrongUid, naiWithUid, mContext.getOpPackageName()));
     }
 
@@ -12583,7 +13025,7 @@
         assertEquals(
                 "Unexpected ConnDiags permission",
                 expectPermission,
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), info, mContext.getOpPackageName()));
     }
 
@@ -12625,7 +13067,7 @@
         waitForIdle();
         assertTrue(
                 "Active VPN permission not applied",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), naiWithoutUid,
                         mContext.getOpPackageName()));
 
@@ -12633,7 +13075,7 @@
         waitForIdle();
         assertFalse(
                 "VPN shouldn't receive callback on non-underlying network",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), naiWithoutUid,
                         mContext.getOpPackageName()));
     }
@@ -12650,7 +13092,7 @@
 
         assertTrue(
                 "NetworkCapabilities administrator uid permission not applied",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid(), Process.myUid(), naiWithUid, mContext.getOpPackageName()));
     }
 
@@ -12668,7 +13110,7 @@
         // Use wrong pid and uid
         assertFalse(
                 "Permissions allowed when they shouldn't be granted",
-                mService.checkConnectivityDiagnosticsPermissions(
+                mService.hasConnectivityDiagnosticsPermissions(
                         Process.myPid() + 1, Process.myUid() + 1, naiWithUid,
                         mContext.getOpPackageName()));
     }
@@ -12934,7 +13376,7 @@
     }
 
     @Test
-    public void testDumpDoesNotCrash() {
+    public void testDumpDoesNotCrash() throws Exception {
         mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
         // Filing a couple requests prior to testing the dump.
         final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
@@ -12946,6 +13388,44 @@
         mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
         mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
 
+        // NetworkProvider
+        final NetworkProvider wifiProvider = new NetworkProvider(mServiceContext,
+                mCsHandlerThread.getLooper(), "Wifi provider");
+        mCm.registerNetworkProvider(wifiProvider);
+
+        // NetworkAgent
+        final LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName(WIFI_IFNAME);
+        mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+        mWiFiAgent.connect(true);
+
+        // NetworkOffer
+        final NetworkScore wifiScore = new NetworkScore.Builder().build();
+        final NetworkCapabilities wifiCaps = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .build();
+        final TestableNetworkOfferCallback wifiCallback = new TestableNetworkOfferCallback(
+                TIMEOUT_MS /* timeout */, TEST_CALLBACK_TIMEOUT_MS /* noCallbackTimeout */);
+        wifiProvider.registerNetworkOffer(wifiScore, wifiCaps, r -> r.run(), wifiCallback);
+
+        // Profile preferences
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent();
+        workAgent.connect(true);
+        mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                null /* executor */, null /* listener */);
+
+        // OEM preferences
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID;
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        setOemNetworkPreference(networkPref, TEST_PACKAGE_NAME);
+
+        // Mobile data preferred UIDs
+        setAndUpdateMobileDataPreferredUids(Set.of(TEST_PACKAGE_UID));
+
         verifyDump(new String[0]);
 
         // Verify dump with arguments.
@@ -13176,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 */)
             );
         }
     }
@@ -15039,6 +15520,8 @@
         expectNoRequestChanged(oemPaidFactory);
         internetFactory.expectRequestAdd();
         mCm.unregisterNetworkCallback(wifiCallback);
+        handlerThread.quitSafely();
+        handlerThread.join();
     }
 
     /**
@@ -15403,6 +15886,8 @@
             assertTrue(testFactory.getMyStartRequested());
         } finally {
             testFactory.terminate();
+            handlerThread.quitSafely();
+            handlerThread.join();
         }
     }
 
@@ -16210,6 +16695,15 @@
         // Other callbacks will be unregistered by tearDown()
     }
 
+    private NetworkCallback requestForEnterpriseId(@NetworkCapabilities.EnterpriseId final int id) {
+        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_ENTERPRISE).addEnterpriseId(id).build();
+        final NetworkRequest req = new NetworkRequest.Builder().setCapabilities(nc).build();
+        final NetworkCallback cb = new TestableNetworkCallback();
+        mCm.requestNetwork(req, cb);
+        return cb;
+    }
+
     /**
      * Make sure per profile network preferences behave as expected when multiple slices with
      * multiple different apps within same user profile is configured.
@@ -16217,8 +16711,6 @@
     @Test
     public void testSetPreferenceWithMultiplePreferences()
             throws Exception {
-        final InOrder inOrder = inOrder(mMockNetd);
-
         final UserHandle testHandle = setupEnterpriseNetwork();
         mServiceContext.setWorkProfile(testHandle, true);
         registerDefaultNetworkCallbacks();
@@ -16256,6 +16748,12 @@
         final TestNetworkAgentWrapper workAgent4 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_4);
         final TestNetworkAgentWrapper workAgent5 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_5);
 
+        final NetworkCallback keepupCb1 = requestForEnterpriseId(NET_ENTERPRISE_ID_1);
+        final NetworkCallback keepupCb2 = requestForEnterpriseId(NET_ENTERPRISE_ID_2);
+        final NetworkCallback keepupCb3 = requestForEnterpriseId(NET_ENTERPRISE_ID_3);
+        final NetworkCallback keepupCb4 = requestForEnterpriseId(NET_ENTERPRISE_ID_4);
+        final NetworkCallback keepupCb5 = requestForEnterpriseId(NET_ENTERPRISE_ID_5);
+
         workAgent1.connect(true);
         workAgent2.connect(true);
         workAgent3.connect(true);
@@ -16414,6 +16912,12 @@
         appCb4.expectAvailableCallbacksValidated(mCellAgent);
         mCellAgent.disconnect();
 
+        mCm.unregisterNetworkCallback(keepupCb1);
+        mCm.unregisterNetworkCallback(keepupCb2);
+        mCm.unregisterNetworkCallback(keepupCb3);
+        mCm.unregisterNetworkCallback(keepupCb4);
+        mCm.unregisterNetworkCallback(keepupCb5);
+
         mCm.unregisterNetworkCallback(appCb1);
         mCm.unregisterNetworkCallback(appCb2);
         mCm.unregisterNetworkCallback(appCb3);
@@ -16875,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);
@@ -16906,9 +17396,16 @@
                 .build();
     }
 
+    private NetworkRequest getRestrictedRequestForWifiWithSubIds() {
+        return new NetworkRequest.Builder()
+            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+            .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+            .setSubscriptionIds(Collections.singleton(TEST_SUBSCRIPTION_ID))
+            .build();
+    }
+
     @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();
@@ -16924,18 +17421,183 @@
     }
 
     @Test
-    public void testNetworkRequestWithSubIdsWithoutNetworkFactoryPermission() throws Exception {
-        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testCarrierConfigAppSendNetworkRequestForRestrictedWifi() throws Exception {
+        mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
+        doReturn(true).when(mCarrierPrivilegeAuthenticator)
+                .isCarrierServiceUidForNetworkCapabilities(anyInt(), any());
         final PendingIntent pendingIntent = PendingIntent.getBroadcast(
                 mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
+        final NetworkCallback networkCallback1 = new NetworkCallback();
+        final NetworkCallback networkCallback2 = new NetworkCallback();
 
-        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()));
+        mCm.requestNetwork(
+                getRestrictedRequestForWifiWithSubIds(), networkCallback1);
+        mCm.requestNetwork(
+                getRestrictedRequestForWifiWithSubIds(), pendingIntent);
+        mCm.registerNetworkCallback(
+                getRestrictedRequestForWifiWithSubIds(), networkCallback2);
+
+        mCm.unregisterNetworkCallback(networkCallback1);
+        mCm.releaseNetworkRequest(pendingIntent);
+        mCm.unregisterNetworkCallback(networkCallback2);
+    }
+
+    private void doTestNetworkRequestWithCarrierPrivilegesLost(
+            boolean shouldGrantRestrictedNetworkPermission,
+            int lostPrivilegeUid,
+            int lostPrivilegeSubId,
+            boolean expectUnavailable,
+            boolean expectCapChanged) throws Exception {
+        if (shouldGrantRestrictedNetworkPermission) {
+            mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_GRANTED);
+        } else {
+            mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
+        }
+
+        NetworkCapabilities filter =
+                getRestrictedRequestForWifiWithSubIds().networkCapabilities;
+        final HandlerThread handlerThread = new HandlerThread("testRestrictedFactoryRequests");
+        handlerThread.start();
+
+        final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactory", filter, mCsHandlerThread);
+        testFactory.register();
+        testFactory.assertRequestCountEquals(0);
+
+        doReturn(true).when(mCarrierPrivilegeAuthenticator)
+                .isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        final NetworkRequest networkrequest =
+                getRestrictedRequestForWifiWithSubIds();
+        mCm.requestNetwork(networkrequest, networkCallback);
+        testFactory.expectRequestAdd();
+        testFactory.assertRequestCountEquals(1);
+
+        NetworkCapabilities nc = new NetworkCapabilities.Builder(filter)
+                .setAllowedUids(Set.of(Process.myUid()))
+                .build();
+        mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), nc);
+        mWiFiAgent.connect(false);
+        networkCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+        final NetworkAgentInfo nai = mService.getNetworkAgentInfoForNetwork(
+                mWiFiAgent.getNetwork());
+
+        doReturn(false).when(mCarrierPrivilegeAuthenticator)
+                .isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
+        doReturn(TEST_SUBSCRIPTION_ID).when(mCarrierPrivilegeAuthenticator)
+                .getSubIdFromNetworkCapabilities(any());
+
+        visibleOnHandlerThread(mCsHandlerThread.getThreadHandler(), () -> {
+            mDeps.mCarrierPrivilegesLostListener.accept(lostPrivilegeUid, lostPrivilegeSubId);
+        });
+        waitForIdle();
+
+        if (expectCapChanged) {
+            networkCallback.expect(NETWORK_CAPS_UPDATED);
+        }
+        if (expectUnavailable) {
+            networkCallback.expect(UNAVAILABLE);
+        }
+        if (!expectCapChanged && !expectUnavailable) {
+            networkCallback.assertNoCallback();
+        }
+
+        mWiFiAgent.disconnect();
+
+        if (expectUnavailable) {
+            testFactory.expectRequestRemove();
+            testFactory.assertRequestCountEquals(0);
+        } else {
+            testFactory.expectRequestAdd();
+            testFactory.assertRequestCountEquals(1);
+        }
+
+        handlerThread.quitSafely();
+        handlerThread.join();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testRestrictedRequestRemovedDueToCarrierPrivilegesLost() throws Exception {
+        doTestNetworkRequestWithCarrierPrivilegesLost(
+                false /* shouldGrantRestrictedNetworkPermission */,
+                Process.myUid(),
+                TEST_SUBSCRIPTION_ID,
+                true /* expectUnavailable */,
+                true /* expectCapChanged */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testRequestNotRemoved_MismatchSubId() throws Exception {
+        doTestNetworkRequestWithCarrierPrivilegesLost(
+                false /* shouldGrantRestrictedNetworkPermission */,
+                Process.myUid(),
+                TEST_SUBSCRIPTION_ID + 1,
+                false /* expectUnavailable */,
+                false /* expectCapChanged */);
+    }
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testRequestNotRemoved_MismatchUid() throws Exception {
+        doTestNetworkRequestWithCarrierPrivilegesLost(
+                false /* shouldGrantRestrictedNetworkPermission */,
+                Process.myUid() + 1,
+                TEST_SUBSCRIPTION_ID,
+                false /* expectUnavailable */,
+                false /* expectCapChanged */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testRequestNotRemoved_HasRestrictedNetworkPermission() throws Exception {
+        doTestNetworkRequestWithCarrierPrivilegesLost(
+                true /* shouldGrantRestrictedNetworkPermission */,
+                Process.myUid(),
+                TEST_SUBSCRIPTION_ID,
+                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
@@ -16948,6 +17610,7 @@
         mCm.requestNetwork(new NetworkRequest.Builder()
                         .clearCapabilities()
                         .addTransportType(TRANSPORT_TEST)
+                        .addTransportType(TRANSPORT_CELLULAR)
                         .build(),
                 cb);
 
@@ -17093,7 +17756,7 @@
 
         // In this test TEST_PACKAGE_UID will be the UID of the carrier service UID.
         doReturn(true).when(mCarrierPrivilegeAuthenticator)
-                .hasCarrierPrivilegeForNetworkCapabilities(eq(TEST_PACKAGE_UID), any());
+                .isCarrierServiceUidForNetworkCapabilities(eq(TEST_PACKAGE_UID), any());
 
         // Simulate a restricted telephony network. The telephony factory is entitled to set
         // the access UID to the service package on any of its restricted networks.
@@ -17158,17 +17821,18 @@
         // TODO : fix the builder
         ncb.setNetworkSpecifier(null);
         ncb.removeTransportType(TRANSPORT_CELLULAR);
-        ncb.addTransportType(TRANSPORT_WIFI);
+        ncb.addTransportType(TRANSPORT_BLUETOOTH);
         // Wifi does not get to set access UID, even to the correct UID
         mCm.requestNetwork(new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_WIFI)
+                .addTransportType(TRANSPORT_BLUETOOTH)
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .build(), cb);
-        mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), ncb.build());
-        mWiFiAgent.connect(true);
-        cb.expectAvailableThenValidatedCallbacks(mWiFiAgent);
+        final TestNetworkAgentWrapper bluetoothAgent = new TestNetworkAgentWrapper(
+                TRANSPORT_BLUETOOTH, new LinkProperties(), ncb.build());
+        bluetoothAgent.connect(true);
+        cb.expectAvailableThenValidatedCallbacks(bluetoothAgent);
         ncb.setAllowedUids(serviceUidSet);
-        mWiFiAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        bluetoothAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
         mCm.unregisterNetworkCallback(cb);
     }
@@ -18344,6 +19008,27 @@
                 anyInt());
     }
 
+    // UidFrozenStateChangedCallback is added in U API.
+    // Returning UidFrozenStateChangedCallback directly makes the test fail on T- devices since
+    // AndroidJUnit4ClassRunner iterates all declared methods and tries to resolve the return type.
+    // Solve this by wrapping it in an AtomicReference. Because of erasure, this removes the
+    // resolving problem as the type isn't seen dynamically.
+    private AtomicReference<UidFrozenStateChangedCallback> getUidFrozenStateChangedCallback() {
+        ArgumentCaptor<UidFrozenStateChangedCallback> activityManagerCallbackCaptor =
+                ArgumentCaptor.forClass(UidFrozenStateChangedCallback.class);
+        verify(mActivityManager).registerUidFrozenStateChangedCallback(any(),
+                activityManagerCallbackCaptor.capture());
+        return new AtomicReference<>(activityManagerCallbackCaptor.getValue());
+    }
+
+    private BaseNetdUnsolicitedEventListener getRegisteredNetdUnsolicitedEventListener()
+            throws RemoteException {
+        ArgumentCaptor<BaseNetdUnsolicitedEventListener> netdCallbackCaptor =
+                ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener.class);
+        verify(mMockNetd).registerUnsolicitedEventListener(netdCallbackCaptor.capture());
+        return netdCallbackCaptor.getValue();
+    }
+
     private static final int TEST_FROZEN_UID = 1000;
     private static final int TEST_UNFROZEN_UID = 2000;
 
@@ -18354,25 +19039,178 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testFrozenUidSocketDestroy() throws Exception {
-        ArgumentCaptor<UidFrozenStateChangedCallback> callbackArg =
-                ArgumentCaptor.forClass(UidFrozenStateChangedCallback.class);
-
-        verify(mActivityManager).registerUidFrozenStateChangedCallback(any(),
-                callbackArg.capture());
+        final UidFrozenStateChangedCallback callback =
+                getUidFrozenStateChangedCallback().get();
 
         final int[] uids = {TEST_FROZEN_UID, TEST_UNFROZEN_UID};
         final int[] frozenStates = {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_UNFROZEN};
 
-        callbackArg.getValue().onUidFrozenStateChanged(uids, frozenStates);
+        callback.onUidFrozenStateChanged(uids, frozenStates);
 
         waitForIdle();
 
-        final Set<Integer> exemptUids = new ArraySet();
-        final UidRange frozenUidRange = new UidRange(TEST_FROZEN_UID, TEST_FROZEN_UID);
-        final Set<UidRange> ranges = Collections.singleton(frozenUidRange);
+        verify(mDestroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(Set.of(TEST_FROZEN_UID));
+    }
 
-        verify(mDestroySocketsWrapper).destroyLiveTcpSockets(eq(UidRange.toIntRanges(ranges)),
-                eq(exemptUids));
+    private void doTestDelayFrozenUidSocketDestroy(int transportType,
+            boolean freezeWithNetworkInactive, boolean expectDelay) throws Exception {
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(transportToTestIfaceName(transportType));
+        final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(transportType, lp);
+        final int idleTimerLabel = getIdleTimerLabel(agent.getNetwork().netId, transportType);
+        testAndCleanup(() -> {
+            final UidFrozenStateChangedCallback uidFrozenStateChangedCallback =
+                    getUidFrozenStateChangedCallback().get();
+            final BaseNetdUnsolicitedEventListener netdUnsolicitedEventListener =
+                    getRegisteredNetdUnsolicitedEventListener();
+
+            mCm.registerDefaultNetworkCallback(defaultCallback);
+            agent.connect(true);
+            defaultCallback.expectAvailableThenValidatedCallbacks(agent);
+            if (freezeWithNetworkInactive) {
+                // Make network inactive
+                netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                        idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+            }
+
+            // Freeze TEST_FROZEN_UID and TEST_UNFROZEN_UID
+            final int[] uids1 = {TEST_FROZEN_UID, TEST_UNFROZEN_UID};
+            final int[] frozenStates1 = {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_FROZEN};
+            uidFrozenStateChangedCallback.onUidFrozenStateChanged(uids1, frozenStates1);
+            waitForIdle();
+
+            if (expectDelay) {
+                verify(mDestroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(any());
+            } else {
+                verify(mDestroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(
+                        Set.of(TEST_FROZEN_UID, TEST_UNFROZEN_UID));
+                clearInvocations(mDestroySocketsWrapper);
+            }
+
+            // Unfreeze TEST_UNFROZEN_UID
+            final int[] uids2 = {TEST_UNFROZEN_UID};
+            final int[] frozenStates2 = {UID_FROZEN_STATE_UNFROZEN};
+            uidFrozenStateChangedCallback.onUidFrozenStateChanged(uids2, frozenStates2);
+
+            // Make network active
+            netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+                    idleTimerLabel, TIMESTAMP, TEST_PACKAGE_UID);
+            waitForIdle();
+
+            if (expectDelay) {
+                verify(mDestroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(
+                        Set.of(TEST_FROZEN_UID));
+            } else {
+                verify(mDestroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(any());
+            }
+        }, () -> { // Cleanup
+                agent.disconnect();
+            }, () -> {
+                mCm.unregisterNetworkCallback(defaultCallback);
+            });
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testDelayFrozenUidSocketDestroy_ActiveCellular() throws Exception {
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR, false /* freezeWithNetworkInactive */,
+                false /* expectDelay */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testDelayFrozenUidSocketDestroy_InactiveCellular() throws Exception {
+        // When the default network is cellular and cellular network is inactive, closing socket
+        // is delayed.
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR, true /* freezeWithNetworkInactive */,
+                true /* expectDelay */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testDelayFrozenUidSocketDestroy_ActiveWifi() throws Exception {
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI, false /* freezeWithNetworkInactive */,
+                false /* expectDelay */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testDelayFrozenUidSocketDestroy_InactiveWifi() throws Exception {
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI, true /* freezeWithNetworkInactive */,
+                false /* expectDelay */);
+    }
+
+    /**
+     * @param switchToWifi if true, simulate a migration of the default network to wifi
+     *                     if false, simulate a cell disconnection
+     */
+    private void doTestLoseCellDefaultNetwork_ClosePendingFrozenSockets(final boolean switchToWifi)
+            throws Exception {
+        final UidFrozenStateChangedCallback uidFrozenStateChangedCallback =
+                getUidFrozenStateChangedCallback().get();
+        final BaseNetdUnsolicitedEventListener netdUnsolicitedEventListener =
+                getRegisteredNetdUnsolicitedEventListener();
+
+        final LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName(WIFI_IFNAME);
+        mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName(MOBILE_IFNAME);
+        mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+        final int idleTimerLabel =
+                getIdleTimerLabel(mCellAgent.getNetwork().netId, TRANSPORT_CELLULAR);
+
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+        try {
+            mCellAgent.connect(true);
+            defaultCallback.expectAvailableThenValidatedCallbacks(mCellAgent);
+
+            // Make cell network inactive
+            netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                    idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+
+            // Freeze TEST_FROZEN_UID
+            final int[] uids = {TEST_FROZEN_UID};
+            final int[] frozenStates = {UID_FROZEN_STATE_FROZEN};
+            uidFrozenStateChangedCallback.onUidFrozenStateChanged(uids, frozenStates);
+            waitForIdle();
+
+            // Closing frozen sockets should be delayed since the default network is cellular
+            // and cellular network is inactive.
+            verify(mDestroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(any());
+
+            if (switchToWifi) {
+                mWiFiAgent.connect(true);
+                defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiAgent);
+            } else {
+                mCellAgent.disconnect();
+                waitForIdle();
+            }
+
+            // Pending frozen sockets should be closed since the cellular network is no longer the
+            // default network.
+            verify(mDestroySocketsWrapper)
+                    .destroyLiveTcpSocketsByOwnerUids(Set.of(TEST_FROZEN_UID));
+        } finally {
+            mCm.unregisterNetworkCallback(defaultCallback);
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testLoseCellDefaultNetwork_SwitchToWifi_ClosePendingFrozenSockets()
+            throws Exception {
+        doTestLoseCellDefaultNetwork_ClosePendingFrozenSockets(true /* switchToWifi */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testLoseCellDefaultNetwork_NoDefaultNetwork_ClosePendingFrozenSockets()
+            throws Exception {
+        doTestLoseCellDefaultNetwork_ClosePendingFrozenSockets(false /* switchToWifi */);
     }
 
     @Test
@@ -18403,4 +19241,26 @@
 
         verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
     }
+
+    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/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
index 1618a62..8037542 100644
--- a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
@@ -33,6 +33,7 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
@@ -74,6 +75,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
 import com.android.server.IpSecService.TunnelInterfaceRecord;
 import com.android.testutils.DevSdkIgnoreRule;
 
@@ -85,6 +87,7 @@
 import org.junit.runners.Parameterized;
 
 import java.net.Inet4Address;
+import java.net.InetAddress;
 import java.net.Socket;
 import java.util.Arrays;
 import java.util.Collection;
@@ -149,6 +152,7 @@
         private Set<String> mAllowedPermissions = new ArraySet<>(Arrays.asList(
                 android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
                 android.Manifest.permission.NETWORK_STACK,
+                android.Manifest.permission.ACCESS_NETWORK_STATE,
                 PERMISSION_MAINLINE_NETWORK_STACK));
 
         private void setAllowedPermissions(String... permissions) {
@@ -202,11 +206,13 @@
     private IpSecService.Dependencies makeDependencies() throws RemoteException {
         final IpSecService.Dependencies deps = mock(IpSecService.Dependencies.class);
         when(deps.getNetdInstance(mTestContext)).thenReturn(mMockNetd);
+        when(deps.getIpSecXfrmController()).thenReturn(mMockXfrmCtrl);
         return deps;
     }
 
     INetd mMockNetd;
     PackageManager mMockPkgMgr;
+    IpSecXfrmController mMockXfrmCtrl;
     IpSecService.Dependencies mDeps;
     IpSecService mIpSecService;
     Network fakeNetwork = new Network(0xAB);
@@ -235,6 +241,7 @@
     @Before
     public void setUp() throws Exception {
         mMockNetd = mock(INetd.class);
+        mMockXfrmCtrl = mock(IpSecXfrmController.class);
         mMockPkgMgr = mock(PackageManager.class);
         mDeps = makeDependencies();
         mIpSecService = new IpSecService(mTestContext, mDeps);
@@ -506,6 +513,32 @@
     }
 
     @Test
+    public void getTransformState() throws Exception {
+        XfrmNetlinkNewSaMessage mockXfrmNewSaMsg = mock(XfrmNetlinkNewSaMessage.class);
+        when(mockXfrmNewSaMsg.getBitmap()).thenReturn(new byte[512]);
+        when(mMockXfrmCtrl.ipSecGetSa(any(InetAddress.class), anyLong()))
+                .thenReturn(mockXfrmNewSaMsg);
+
+        // Create transform
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+        assertEquals(IpSecManager.Status.OK, createTransformResp.status);
+
+        // Get transform state
+        mIpSecService.getTransformState(createTransformResp.resourceId);
+
+        // Verifications
+        verify(mMockXfrmCtrl)
+                .ipSecGetSa(
+                        eq(InetAddresses.parseNumericAddress(mDestinationAddr)),
+                        eq(Integer.toUnsignedLong(TEST_SPI)));
+    }
+
+    @Test
     public void testReleaseOwnedSpi() throws Exception {
         IpSecConfig ipSecConfig = new IpSecConfig();
         addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
diff --git a/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java b/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java
new file mode 100644
index 0000000..8c1f47f
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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 static com.android.server.IpSecXfrmControllerTestHex.XFRM_ESRCH_HEX;
+import static com.android.server.IpSecXfrmControllerTestHex.XFRM_NEW_SA_HEX;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.InetAddresses;
+import android.system.ErrnoException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpSecXfrmControllerTest {
+    private static final InetAddress DEST_ADDRESS =
+            InetAddresses.parseNumericAddress("2001:db8::111");
+    private static final long SPI = 0xaabbccddL;
+    private static final int ESRCH = -3;
+
+    private IpSecXfrmController mXfrmController;
+    private FileDescriptor mDummyNetlinkSocket;
+
+    @Mock private IpSecXfrmController.Dependencies mMockDeps;
+
+    @Captor private ArgumentCaptor<byte[]> mRequestByteArrayCaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mDummyNetlinkSocket = new FileDescriptor();
+
+        when(mMockDeps.newNetlinkSocket()).thenReturn(mDummyNetlinkSocket);
+        mXfrmController = new IpSecXfrmController(mMockDeps);
+    }
+
+    @Test
+    public void testStartStop() throws Exception {
+        mXfrmController.openNetlinkSocketIfNeeded();
+
+        verify(mMockDeps).newNetlinkSocket();
+        assertNotNull(mXfrmController.getNetlinkSocket());
+
+        mXfrmController.closeNetlinkSocketIfNeeded();
+        verify(mMockDeps).releaseNetlinkSocket(eq(mDummyNetlinkSocket));
+        assertNull(mXfrmController.getNetlinkSocket());
+    }
+
+    private static void injectRxMessage(IpSecXfrmController.Dependencies mockDeps, byte[] bytes)
+            throws Exception {
+        final ByteBuffer buff = ByteBuffer.wrap(bytes);
+        buff.order(ByteOrder.nativeOrder());
+
+        when(mockDeps.recvMessage(any(FileDescriptor.class))).thenReturn(buff);
+    }
+
+    @Test
+    public void testIpSecGetSa() throws Exception {
+        final int expectedReqLen = 40;
+        injectRxMessage(mMockDeps, XFRM_NEW_SA_HEX);
+
+        final NetlinkMessage netlinkMessage = mXfrmController.ipSecGetSa(DEST_ADDRESS, SPI);
+        final XfrmNetlinkNewSaMessage message = (XfrmNetlinkNewSaMessage) netlinkMessage;
+
+        // Verifications
+        assertEquals(SPI, message.getXfrmUsersaInfo().getSpi());
+        assertEquals(DEST_ADDRESS, message.getXfrmUsersaInfo().getDestAddress());
+
+        verify(mMockDeps).sendMessage(eq(mDummyNetlinkSocket), mRequestByteArrayCaptor.capture());
+        final byte[] request = mRequestByteArrayCaptor.getValue();
+        assertEquals(expectedReqLen, request.length);
+
+        verify(mMockDeps).recvMessage(eq(mDummyNetlinkSocket));
+    }
+
+    @Test
+    public void testIpSecGetSa_NlErrorMsg() throws Exception {
+        injectRxMessage(mMockDeps, XFRM_ESRCH_HEX);
+
+        try {
+            mXfrmController.ipSecGetSa(DEST_ADDRESS, SPI);
+            fail("Expected to fail with ESRCH ");
+        } catch (ErrnoException e) {
+            assertEquals(ESRCH, e.errno);
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java b/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java
new file mode 100644
index 0000000..a2082c4
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java
@@ -0,0 +1,83 @@
+/*
+ * 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 com.android.net.module.util.HexDump;
+
+public class IpSecXfrmControllerTestHex {
+    private static final String XFRM_NEW_SA_HEX_STRING =
+            "2003000010000000000000003FE1D4B6"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000A00000000000000"
+                    + "000000000000000020010DB800000000"
+                    + "0000000000000111AABBCCDD32000000"
+                    + "20010DB8000000000000000000000222"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "FD464C65000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "024000000A0000000000000000000000"
+                    + "5C000100686D61632873686131290000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000A000000055F01AC07E15E437"
+                    + "115DDE0AEDD18A822BA9F81E60001400"
+                    + "686D6163287368613129000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "A00000006000000055F01AC07E15E437"
+                    + "115DDE0AEDD18A822BA9F81E58000200"
+                    + "63626328616573290000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "800000006AED4975ADF006D65C76F639"
+                    + "23A6265B1C0117004000000000000000"
+                    + "00000000000000000000000000080000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000"
+                    + "00000000000000000000000000000000";
+    public static final byte[] XFRM_NEW_SA_HEX =
+            HexDump.hexStringToByteArray(XFRM_NEW_SA_HEX_STRING);
+
+    private static final String XFRM_ESRCH_HEX_STRING =
+            "3C0000000200000000000000A5060000"
+                    + "FDFFFFFF280000001200010000000000"
+                    + "0000000020010DB80000000000000000"
+                    + "00000111AABBCCDD0A003200";
+    public static final byte[] XFRM_ESRCH_HEX = HexDump.hexStringToByteArray(XFRM_ESRCH_HEX_STRING);
+}
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 1997215..979e0a1 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -16,13 +16,32 @@
 
 package com.android.server;
 
+import static android.Manifest.permission.DEVICE_POWER;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_STACK;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_PLATFORM_MDNS_BACKEND;
 import static android.net.connectivity.ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER;
 import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
 import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
+import static android.net.nsd.NsdManager.FAILURE_MAX_LIMIT;
 import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
 
+import static com.android.networkstack.apishim.api33.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
+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;
 
@@ -35,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;
@@ -42,6 +62,7 @@
 import static org.mockito.Mockito.doCallRealMethod;
 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.reset;
@@ -51,9 +72,12 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
+import android.app.ActivityManager.OnUidImportanceListener;
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.net.INetd;
 import android.net.Network;
 import android.net.mdns.aidl.DiscoveryInfo;
@@ -61,6 +85,7 @@
 import android.net.mdns.aidl.IMDnsEventListener;
 import android.net.mdns.aidl.RegistrationInfo;
 import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
 import android.net.nsd.MDnsManager;
@@ -68,7 +93,10 @@
 import android.net.nsd.NsdManager.DiscoveryListener;
 import android.net.nsd.NsdManager.RegistrationListener;
 import android.net.nsd.NsdManager.ResolveListener;
+import android.net.nsd.NsdManager.ServiceInfoCallback;
 import android.net.nsd.NsdServiceInfo;
+import android.net.nsd.OffloadEngine;
+import android.net.wifi.WifiManager;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Handler;
@@ -76,19 +104,25 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
+import com.android.metrics.NetworkNsdReportedMetrics;
 import com.android.server.NsdService.Dependencies;
 import com.android.server.connectivity.mdns.MdnsAdvertiser;
+import com.android.server.connectivity.mdns.MdnsAdvertisingOptions;
 import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
+import com.android.server.connectivity.mdns.MdnsInterfaceSocket;
 import com.android.server.connectivity.mdns.MdnsSearchOptions;
 import com.android.server.connectivity.mdns.MdnsServiceBrowserListener;
 import com.android.server.connectivity.mdns.MdnsServiceInfo;
 import com.android.server.connectivity.mdns.MdnsSocketProvider;
+import com.android.server.connectivity.mdns.MdnsSocketProvider.SocketRequestMonitor;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
@@ -101,27 +135,38 @@
 import org.junit.runner.RunWith;
 import org.mockito.AdditionalAnswers;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Queue;
+import java.util.Set;
 
 // TODOs:
 //  - test client can send requests and receive replies
 //  - test NSD_ON ENABLE/DISABLED listening
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NsdServiceTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
     private static final long CLEANUP_DELAY_MS = 500;
     private static final long TIMEOUT_MS = 500;
+    private static final long TEST_TIME_MS = 123L;
     private static final String SERVICE_NAME = "a_name";
     private static final String SERVICE_TYPE = "_test._tcp";
     private static final String SERVICE_FULL_NAME = SERVICE_NAME + "." + SERVICE_TYPE;
@@ -138,13 +183,23 @@
 
     @Rule
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
+    @Rule
+    public TestRule ignoreRule = new DevSdkIgnoreRule();
     @Mock Context mContext;
+    @Mock PackageManager mPackageManager;
     @Mock ContentResolver mResolver;
     @Mock MDnsManager mMockMDnsM;
     @Mock Dependencies mDeps;
     @Mock MdnsDiscoveryManager mDiscoveryManager;
     @Mock MdnsAdvertiser mAdvertiser;
     @Mock MdnsSocketProvider mSocketProvider;
+    @Mock WifiManager mWifiManager;
+    @Mock WifiManager.MulticastLock mMulticastLock;
+    @Mock ActivityManager mActivityManager;
+    @Mock NetworkNsdReportedMetrics mMetrics;
+    @Mock MdnsUtils.Clock mClock;
+    SocketRequestMonitor mSocketRequestMonitor;
+    OnUidImportanceListener mUidImportanceListener;
     HandlerThread mThread;
     TestHandler mHandler;
     NsdService mService;
@@ -167,9 +222,14 @@
         mHandler = new TestHandler(mThread.getLooper());
         when(mContext.getContentResolver()).thenReturn(mResolver);
         mockService(mContext, MDnsManager.class, MDnsManager.MDNS_SERVICE, mMockMDnsM);
+        mockService(mContext, WifiManager.class, Context.WIFI_SERVICE, mWifiManager);
+        mockService(mContext, ActivityManager.class, Context.ACTIVITY_SERVICE, mActivityManager);
+        doReturn(mPackageManager).when(mContext).getPackageManager();
         if (mContext.getSystemService(MDnsManager.class) == null) {
             // Test is using mockito-extended
             doCallRealMethod().when(mContext).getSystemService(MDnsManager.class);
+            doCallRealMethod().when(mContext).getSystemService(WifiManager.class);
+            doCallRealMethod().when(mContext).getSystemService(ActivityManager.class);
         }
         doReturn(true).when(mMockMDnsM).registerService(
                 anyInt(), anyString(), anyString(), anyInt(), any(), anyInt());
@@ -179,20 +239,42 @@
                 anyInt(), anyString(), anyString(), anyString(), anyInt());
         doReturn(false).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
         doReturn(mDiscoveryManager).when(mDeps)
-                .makeMdnsDiscoveryManager(any(), any(), any());
-        doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any(), any());
-        doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any(), any());
+                .makeMdnsDiscoveryManager(any(), any(), any(), any());
+        doReturn(mMulticastLock).when(mWifiManager).createMulticastLock(any());
+        doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any(), any(), any());
+        doReturn(DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF).when(mDeps).getDeviceConfigInt(
+                eq(NsdService.MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF), anyInt());
+        doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any(), any(), any(),
+                any());
+        doReturn(mMetrics).when(mDeps).makeNetworkNsdReportedMetrics(anyInt());
+        doReturn(mClock).when(mDeps).makeClock();
+        doReturn(TEST_TIME_MS).when(mClock).elapsedRealtime();
         mService = makeService();
+        final ArgumentCaptor<SocketRequestMonitor> cbMonitorCaptor =
+                ArgumentCaptor.forClass(SocketRequestMonitor.class);
+        verify(mDeps).makeMdnsSocketProvider(any(), any(), any(), cbMonitorCaptor.capture());
+        mSocketRequestMonitor = cbMonitorCaptor.getValue();
+
+        final ArgumentCaptor<OnUidImportanceListener> uidListenerCaptor =
+                ArgumentCaptor.forClass(OnUidImportanceListener.class);
+        verify(mActivityManager).addOnUidImportanceListener(uidListenerCaptor.capture(), anyInt());
+        mUidImportanceListener = uidListenerCaptor.getValue();
     }
 
     @After
     public void tearDown() throws Exception {
         if (mThread != null) {
-            mThread.quit();
-            mThread = null;
+            mThread.quitSafely();
+            mThread.join();
         }
+
+        // Clear inline mocks as there are possible memory leaks if not done (see mockito
+        // doc for clearInlineMocks), and some tests create many of them.
+        Mockito.framework().clearInlineMocks();
     }
 
+    // Native mdns provided by Netd is removed after U.
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     @DisableCompatChanges({
             RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER,
@@ -225,6 +307,7 @@
     @Test
     @EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testNoDaemonStartedWhenClientsConnect() throws Exception {
         // Creating an NsdManager will not cause daemon startup.
         connectClient(mService);
@@ -260,6 +343,7 @@
     @Test
     @EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testClientRequestsAreGCedAtDisconnection() throws Exception {
         final NsdManager client = connectClient(mService);
         final INsdManagerCallback cb1 = getCallback();
@@ -304,6 +388,7 @@
     @Test
     @EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testCleanupDelayNoRequestActive() throws Exception {
         final NsdManager client = connectClient(mService);
 
@@ -340,6 +425,7 @@
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testDiscoverOnTetheringDownstream() throws Exception {
         final NsdManager client = connectClient(mService);
         final int interfaceIdx = 123;
@@ -354,9 +440,11 @@
         // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
         // this needs to use a timeout
         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
+        final int discId = discIdCaptor.getValue();
+        verify(mMetrics).reportServiceDiscoveryStarted(true /* isLegacy */, discId);
 
         final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
-                discIdCaptor.getValue(),
+                discId,
                 IMDnsEventListener.SERVICE_FOUND,
                 SERVICE_NAME,
                 SERVICE_TYPE,
@@ -407,19 +495,24 @@
                 eq(interfaceIdx));
 
         final String serviceAddress = "192.0.2.123";
+        final int getAddrId = getAddrIdCaptor.getValue();
         final GetAddressInfo addressInfo = new GetAddressInfo(
-                getAddrIdCaptor.getValue(),
+                getAddrId,
                 IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
                 SERVICE_FULL_NAME,
                 serviceAddress,
                 interfaceIdx,
                 INetd.LOCAL_NET_ID);
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         eventListener.onGettingServiceAddressStatus(addressInfo);
         waitForIdle();
 
         final ArgumentCaptor<NsdServiceInfo> resInfoCaptor =
                 ArgumentCaptor.forClass(NsdServiceInfo.class);
         verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(resInfoCaptor.capture());
+        verify(mMetrics).reportServiceResolved(true /* isLegacy */, getAddrId, 10L /* durationMs */,
+                false /* isServiceFromCache */, 0 /* sentQueryCount */);
+
         final NsdServiceInfo resolvedService = resInfoCaptor.getValue();
         assertEquals(SERVICE_NAME, resolvedService.getServiceName());
         assertEquals("." + SERVICE_TYPE, resolvedService.getServiceType());
@@ -430,7 +523,58 @@
     }
 
     @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 {
         final NsdManager client = connectClient(mService);
         final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -444,9 +588,11 @@
         // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
         // this needs to use a timeout
         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
+        final int discId = discIdCaptor.getValue();
+        verify(mMetrics).reportServiceDiscoveryStarted(true /* isLegacy */, discId);
 
         final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
-                discIdCaptor.getValue(),
+                discId,
                 IMDnsEventListener.SERVICE_FOUND,
                 SERVICE_NAME,
                 SERVICE_TYPE,
@@ -461,6 +607,7 @@
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testServiceRegistrationSuccessfulAndFailed() throws Exception {
         final NsdManager client = connectClient(mService);
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -475,14 +622,16 @@
                 eq(SERVICE_NAME), eq(SERVICE_TYPE), eq(PORT), any(), eq(IFACE_IDX_ANY));
 
         // Register service successfully.
+        final int regId = regIdCaptor.getValue();
         final RegistrationInfo registrationInfo = new RegistrationInfo(
-                regIdCaptor.getValue(),
+                regId,
                 IMDnsEventListener.SERVICE_REGISTERED,
                 SERVICE_NAME,
                 SERVICE_TYPE,
                 PORT,
                 new byte[0] /* txtRecord */,
                 IFACE_IDX_ANY);
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         eventListener.onServiceRegistrationStatus(registrationInfo);
 
         final ArgumentCaptor<NsdServiceInfo> registeredInfoCaptor =
@@ -491,23 +640,29 @@
                 .onServiceRegistered(registeredInfoCaptor.capture());
         final NsdServiceInfo registeredInfo = registeredInfoCaptor.getValue();
         assertEquals(SERVICE_NAME, registeredInfo.getServiceName());
+        verify(mMetrics).reportServiceRegistrationSucceeded(
+                true /* isLegacy */, regId, 10L /* durationMs */);
 
         // Fail to register service.
         final RegistrationInfo registrationFailedInfo = new RegistrationInfo(
-                regIdCaptor.getValue(),
+                regId,
                 IMDnsEventListener.SERVICE_REGISTRATION_FAILED,
                 null /* serviceName */,
                 null /* registrationType */,
                 0 /* port */,
                 new byte[0] /* txtRecord */,
                 IFACE_IDX_ANY);
+        doReturn(TEST_TIME_MS + 20L).when(mClock).elapsedRealtime();
         eventListener.onServiceRegistrationStatus(registrationFailedInfo);
         verify(regListener, timeout(TIMEOUT_MS))
                 .onRegistrationFailed(any(), eq(FAILURE_INTERNAL_ERROR));
+        verify(mMetrics).reportServiceRegistrationFailed(
+                true /* isLegacy */, regId, 20L /* durationMs */);
     }
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testServiceDiscoveryFailed() throws Exception {
         final NsdManager client = connectClient(mService);
         final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -518,23 +673,29 @@
         final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
         verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(SERVICE_TYPE), eq(IFACE_IDX_ANY));
         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
+        final int discId = discIdCaptor.getValue();
+        verify(mMetrics).reportServiceDiscoveryStarted(true /* isLegacy */, discId);
 
         // Fail to discover service.
         final DiscoveryInfo discoveryFailedInfo = new DiscoveryInfo(
-                discIdCaptor.getValue(),
+                discId,
                 IMDnsEventListener.SERVICE_DISCOVERY_FAILED,
                 null /* serviceName */,
                 null /* registrationType */,
                 null /* domainName */,
                 IFACE_IDX_ANY,
                 0 /* netId */);
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         eventListener.onServiceDiscoveryStatus(discoveryFailedInfo);
         verify(discListener, timeout(TIMEOUT_MS))
                 .onStartDiscoveryFailed(SERVICE_TYPE, FAILURE_INTERNAL_ERROR);
+        verify(mMetrics).reportServiceDiscoveryFailed(
+                true /* isLegacy */, discId, 10L /* durationMs */);
     }
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testServiceResolutionFailed() throws Exception {
         final NsdManager client = connectClient(mService);
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -548,8 +709,9 @@
                 eq("local.") /* domain */, eq(IFACE_IDX_ANY));
 
         // Fail to resolve service.
+        final int resolvId = resolvIdCaptor.getValue();
         final ResolutionInfo resolutionFailedInfo = new ResolutionInfo(
-                resolvIdCaptor.getValue(),
+                resolvId,
                 IMDnsEventListener.SERVICE_RESOLUTION_FAILED,
                 null /* serviceName */,
                 null /* serviceType */,
@@ -559,13 +721,17 @@
                 0 /* port */,
                 new byte[0] /* txtRecord */,
                 IFACE_IDX_ANY);
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         eventListener.onServiceResolutionStatus(resolutionFailedInfo);
         verify(resolveListener, timeout(TIMEOUT_MS))
                 .onResolveFailed(any(), eq(FAILURE_INTERNAL_ERROR));
+        verify(mMetrics).reportServiceResolutionFailed(
+                true /* isLegacy */, resolvId, 10L /* durationMs */);
     }
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testGettingAddressFailed() throws Exception {
         final NsdManager client = connectClient(mService);
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -599,20 +765,105 @@
                 eq(IFACE_IDX_ANY));
 
         // Fail to get service address.
+        final int getAddrId = getAddrIdCaptor.getValue();
         final GetAddressInfo gettingAddrFailedInfo = new GetAddressInfo(
-                getAddrIdCaptor.getValue(),
+                getAddrId,
                 IMDnsEventListener.SERVICE_GET_ADDR_FAILED,
                 null /* hostname */,
                 null /* address */,
                 IFACE_IDX_ANY,
                 0 /* netId */);
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         eventListener.onGettingServiceAddressStatus(gettingAddrFailedInfo);
         verify(resolveListener, timeout(TIMEOUT_MS))
                 .onResolveFailed(any(), eq(FAILURE_INTERNAL_ERROR));
+        verify(mMetrics).reportServiceResolutionFailed(
+                true /* isLegacy */, getAddrId, 10L /* durationMs */);
+    }
+
+    @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @Test
+    public void testPerClientListenerLimit() throws Exception {
+        final NsdManager client1 = connectClient(mService);
+        final NsdManager client2 = connectClient(mService);
+
+        final String testType1 = "_testtype1._tcp";
+        final NsdServiceInfo testServiceInfo1 = new NsdServiceInfo("MyTestService1", testType1);
+        testServiceInfo1.setPort(12345);
+        final String testType2 = "_testtype2._tcp";
+        final NsdServiceInfo testServiceInfo2 = new NsdServiceInfo("MyTestService2", testType2);
+        testServiceInfo2.setPort(12345);
+
+        // Each client can register 200 requests (for example 100 discover and 100 register).
+        final int numEachListener = 100;
+        final ArrayList<DiscoveryListener> discListeners = new ArrayList<>(numEachListener);
+        final ArrayList<RegistrationListener> regListeners = new ArrayList<>(numEachListener);
+        for (int i = 0; i < numEachListener; i++) {
+            final DiscoveryListener discListener1 = mock(DiscoveryListener.class);
+            discListeners.add(discListener1);
+            final RegistrationListener regListener1 = mock(RegistrationListener.class);
+            regListeners.add(regListener1);
+            final DiscoveryListener discListener2 = mock(DiscoveryListener.class);
+            discListeners.add(discListener2);
+            final RegistrationListener regListener2 = mock(RegistrationListener.class);
+            regListeners.add(regListener2);
+            client1.discoverServices(testType1, NsdManager.PROTOCOL_DNS_SD,
+                    (Network) null, Runnable::run, discListener1);
+            client1.registerService(testServiceInfo1, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+                    regListener1);
+
+            client2.registerService(testServiceInfo2, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+                    regListener2);
+            client2.discoverServices(testType2, NsdManager.PROTOCOL_DNS_SD,
+                    (Network) null, Runnable::run, discListener2);
+        }
+
+        // Use a longer timeout than usual for the handler to process all the events. The
+        // registrations take about 1s on a high-end 2013 device.
+        HandlerUtils.waitForIdle(mHandler, 30_000L);
+        for (int i = 0; i < discListeners.size(); i++) {
+            // Callbacks are sent on the manager handler which is different from mHandler, so use
+            // a short timeout (each callback should come quickly after the previous one).
+            verify(discListeners.get(i), timeout(TEST_TIME_MS))
+                    .onDiscoveryStarted(i % 2 == 0 ? testType1 : testType2);
+
+            // registerService does not get a callback before probing finishes (will not happen as
+            // this is mocked)
+            verifyNoMoreInteractions(regListeners.get(i));
+        }
+
+        // The next registrations should fail
+        final DiscoveryListener failDiscListener1 = mock(DiscoveryListener.class);
+        final RegistrationListener failRegListener1 = mock(RegistrationListener.class);
+        final DiscoveryListener failDiscListener2 = mock(DiscoveryListener.class);
+        final RegistrationListener failRegListener2 = mock(RegistrationListener.class);
+
+        client1.discoverServices(testType1, NsdManager.PROTOCOL_DNS_SD,
+                (Network) null, Runnable::run, failDiscListener1);
+        verify(failDiscListener1, timeout(TEST_TIME_MS))
+                .onStartDiscoveryFailed(testType1, FAILURE_MAX_LIMIT);
+
+        client1.registerService(testServiceInfo1, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+                failRegListener1);
+        verify(failRegListener1, timeout(TEST_TIME_MS)).onRegistrationFailed(
+                argThat(a -> testServiceInfo1.getServiceName().equals(a.getServiceName())),
+                eq(FAILURE_MAX_LIMIT));
+
+        client1.discoverServices(testType2, NsdManager.PROTOCOL_DNS_SD,
+                (Network) null, Runnable::run, failDiscListener2);
+        verify(failDiscListener2, timeout(TEST_TIME_MS))
+                .onStartDiscoveryFailed(testType2, FAILURE_MAX_LIMIT);
+
+        client1.registerService(testServiceInfo2, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+                failRegListener2);
+        verify(failRegListener2, timeout(TEST_TIME_MS)).onRegistrationFailed(
+                argThat(a -> testServiceInfo2.getServiceName().equals(a.getServiceName())),
+                eq(FAILURE_MAX_LIMIT));
     }
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testNoCrashWhenProcessResolutionAfterBinderDied() throws Exception {
         final NsdManager client = connectClient(mService);
         final INsdManagerCallback cb = getCallback();
@@ -633,6 +884,7 @@
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testStopServiceResolution() {
         final NsdManager client = connectClient(mService);
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -645,6 +897,7 @@
                 eq("local.") /* domain */, eq(IFACE_IDX_ANY));
 
         final int resolveId = resolvIdCaptor.getValue();
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         client.stopServiceResolution(resolveListener);
         waitForIdle();
 
@@ -652,10 +905,13 @@
         verify(resolveListener, timeout(TIMEOUT_MS)).onResolutionStopped(argThat(ns ->
                 request.getServiceName().equals(ns.getServiceName())
                         && request.getServiceType().equals(ns.getServiceType())));
+        verify(mMetrics).reportServiceResolutionStop(
+                true /* isLegacy */, resolveId, 10L /* durationMs */, 0 /* sentQueryCount */);
     }
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testStopResolutionFailed() {
         final NsdManager client = connectClient(mService);
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -681,6 +937,7 @@
 
     @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testStopResolutionDuringGettingAddress() throws RemoteException {
         final NsdManager client = connectClient(mService);
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -714,6 +971,7 @@
                 eq(IFACE_IDX_ANY));
 
         final int getAddrId = getAddrIdCaptor.getValue();
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         client.stopServiceResolution(resolveListener);
         waitForIdle();
 
@@ -721,6 +979,8 @@
         verify(resolveListener, timeout(TIMEOUT_MS)).onResolutionStopped(argThat(ns ->
                 request.getServiceName().equals(ns.getServiceName())
                         && request.getServiceType().equals(ns.getServiceType())));
+        verify(mMetrics).reportServiceResolutionStop(
+                true /* isLegacy */, getAddrId, 10L /* durationMs */,  0 /* sentQueryCount */);
     }
 
     private void verifyUpdatedServiceInfo(NsdServiceInfo info, String serviceName,
@@ -738,21 +998,25 @@
     public void testRegisterAndUnregisterServiceInfoCallback() {
         final NsdManager client = connectClient(mService);
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
-        final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
-                NsdManager.ServiceInfoCallback.class);
+        final ServiceInfoCallback serviceInfoCallback = mock(
+                ServiceInfoCallback.class);
         final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
         final Network network = new Network(999);
         request.setNetwork(network);
         client.registerServiceInfoCallback(request, Runnable::run, serviceInfoCallback);
         waitForIdle();
         // Verify the registration callback start.
-        final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
-                ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
+        final ArgumentCaptor<MdnsListener> listenerCaptor =
+                ArgumentCaptor.forClass(MdnsListener.class);
         verify(mSocketProvider).startMonitoringSockets();
         verify(mDiscoveryManager).registerListener(eq(serviceTypeWithLocalDomain),
                 listenerCaptor.capture(), argThat(options -> network.equals(options.getNetwork())));
 
-        final MdnsServiceBrowserListener listener = listenerCaptor.getValue();
+        final MdnsListener listener = listenerCaptor.getValue();
+        final int servInfoId = listener.mTransactionId;
+        // Verify the service info callback registered.
+        verify(mMetrics).reportServiceInfoCallbackRegistered(servInfoId);
+
         final MdnsServiceInfo mdnsServiceInfo = new MdnsServiceInfo(
                 SERVICE_NAME,
                 serviceTypeWithLocalDomain.split("\\."),
@@ -764,10 +1028,14 @@
                 List.of() /* textStrings */,
                 List.of() /* textEntries */,
                 1234,
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
+
+        // Callbacks for query sent.
+        listener.onDiscoveryQuerySent(Collections.emptyList(), 1 /* transactionId */);
 
         // Verify onServiceFound callback
-        listener.onServiceFound(mdnsServiceInfo);
+        listener.onServiceFound(mdnsServiceInfo, true /* isServiceFromCache */);
         final ArgumentCaptor<NsdServiceInfo> updateInfoCaptor =
                 ArgumentCaptor.forClass(NsdServiceInfo.class);
         verify(serviceInfoCallback, timeout(TIMEOUT_MS).times(1))
@@ -791,7 +1059,8 @@
                 List.of() /* textStrings */,
                 List.of() /* textEntries */,
                 1234,
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
 
         // Verify onServiceUpdated callback.
         listener.onServiceUpdated(updatedServiceInfo);
@@ -802,10 +1071,18 @@
                 List.of(parseNumericAddress(v4Address), parseNumericAddress(v6Address)),
                 PORT, IFACE_IDX_ANY, new Network(999));
 
+        // Service lost then recovered.
+        listener.onServiceRemoved(updatedServiceInfo);
+        listener.onServiceFound(updatedServiceInfo, false /* isServiceFromCache */);
+
         // Verify service callback unregistration.
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         client.unregisterServiceInfoCallback(serviceInfoCallback);
         waitForIdle();
         verify(serviceInfoCallback, timeout(TIMEOUT_MS)).onServiceInfoCallbackUnregistered();
+        verify(mMetrics).reportServiceInfoCallbackUnregistered(servInfoId, 10L /* durationMs */,
+                3 /* updateCallbackCount */, 1 /* lostCallbackCount */,
+                true /* isServiceFromCache */, 1 /* sentQueryCount */);
     }
 
     @Test
@@ -813,21 +1090,22 @@
         final NsdManager client = connectClient(mService);
         final String invalidServiceType = "a_service";
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, invalidServiceType);
-        final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
-                NsdManager.ServiceInfoCallback.class);
+        final ServiceInfoCallback serviceInfoCallback = mock(
+                ServiceInfoCallback.class);
         client.registerServiceInfoCallback(request, Runnable::run, serviceInfoCallback);
         waitForIdle();
 
         // Fail to register service callback.
         verify(serviceInfoCallback, timeout(TIMEOUT_MS))
                 .onServiceInfoCallbackRegistrationFailed(eq(FAILURE_BAD_PARAMETERS));
+        verify(mMetrics).reportServiceInfoCallbackRegistrationFailed(NO_TRANSACTION);
     }
 
     @Test
     public void testUnregisterNotRegisteredCallback() {
         final NsdManager client = connectClient(mService);
-        final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
-                NsdManager.ServiceInfoCallback.class);
+        final ServiceInfoCallback serviceInfoCallback = mock(
+                ServiceInfoCallback.class);
 
         assertThrows(IllegalArgumentException.class, () ->
                 client.unregisterServiceInfoCallback(serviceInfoCallback));
@@ -843,6 +1121,7 @@
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testMdnsDiscoveryManagerFeature() {
         // Create NsdService w/o feature enabled.
         final NsdManager client = connectClient(mService);
@@ -884,8 +1163,8 @@
         final Network network = new Network(999);
         final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
         // Verify the discovery start / stop.
-        final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
-                ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
+        final ArgumentCaptor<MdnsListener> listenerCaptor =
+                ArgumentCaptor.forClass(MdnsListener.class);
         client.discoverServices(SERVICE_TYPE, PROTOCOL, network, r -> r.run(), discListener);
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
@@ -893,7 +1172,15 @@
                 listenerCaptor.capture(), argThat(options -> network.equals(options.getNetwork())));
         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
 
-        final MdnsServiceBrowserListener listener = listenerCaptor.getValue();
+        final MdnsListener listener = listenerCaptor.getValue();
+        final int discId = listener.mTransactionId;
+        verify(mMetrics).reportServiceDiscoveryStarted(false /* isLegacy */, discId);
+
+        // Callbacks for query sent.
+        listener.onDiscoveryQuerySent(Collections.emptyList(), 1 /* transactionId */);
+        listener.onDiscoveryQuerySent(Collections.emptyList(), 2 /* transactionId */);
+        listener.onDiscoveryQuerySent(Collections.emptyList(), 3 /* transactionId */);
+
         final MdnsServiceInfo foundInfo = new MdnsServiceInfo(
                 SERVICE_NAME, /* serviceInstanceName */
                 serviceTypeWithLocalDomain.split("\\."), /* serviceType */
@@ -905,10 +1192,11 @@
                 List.of(), /* textStrings */
                 List.of(), /* textEntries */
                 1234, /* interfaceIndex */
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
 
         // Verify onServiceNameDiscovered callback
-        listener.onServiceNameDiscovered(foundInfo);
+        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
@@ -926,7 +1214,8 @@
                 null, /* textStrings */
                 null, /* textEntries */
                 1234, /* interfaceIndex */
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
         // Verify onServiceNameRemoved callback
         listener.onServiceNameRemoved(removedInfo);
         verify(discListener, timeout(TIMEOUT_MS)).onServiceLost(argThat(info ->
@@ -935,11 +1224,15 @@
                         && info.getServiceType().equals(SERVICE_TYPE + ".")
                         && info.getNetwork().equals(network)));
 
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         client.stopServiceDiscovery(discListener);
         waitForIdle();
         verify(mDiscoveryManager).unregisterListener(eq(serviceTypeWithLocalDomain), any());
         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStopped(SERVICE_TYPE);
         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 */, true /* isServiceFromCache */);
     }
 
     @Test
@@ -955,6 +1248,8 @@
         waitForIdle();
         verify(discListener, timeout(TIMEOUT_MS))
                 .onStartDiscoveryFailed(invalidServiceType, FAILURE_INTERNAL_ERROR);
+        verify(mMetrics, times(1)).reportServiceDiscoveryFailed(
+                false /* isLegacy */, NO_TRANSACTION, 0L /* durationMs */);
 
         final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
         client.discoverServices(
@@ -962,6 +1257,8 @@
         waitForIdle();
         verify(discListener, timeout(TIMEOUT_MS))
                 .onStartDiscoveryFailed(serviceTypeWithLocalDomain, FAILURE_INTERNAL_ERROR);
+        verify(mMetrics, times(2)).reportServiceDiscoveryFailed(
+                false /* isLegacy */, NO_TRANSACTION, 0L /* durationMs */);
 
         final String serviceTypeWithoutTcpOrUdpEnding = "_test._com";
         client.discoverServices(
@@ -969,6 +1266,8 @@
         waitForIdle();
         verify(discListener, timeout(TIMEOUT_MS))
                 .onStartDiscoveryFailed(serviceTypeWithoutTcpOrUdpEnding, FAILURE_INTERNAL_ERROR);
+        verify(mMetrics, times(3)).reportServiceDiscoveryFailed(
+                false /* isLegacy */, NO_TRANSACTION, 0L /* durationMs */);
     }
 
     @Test
@@ -985,9 +1284,10 @@
         final RegistrationListener regListener = mock(RegistrationListener.class);
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
-        verify(mAdvertiser).addService(anyInt(), argThat(s ->
+        verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(s ->
                 "Instance".equals(s.getServiceName())
-                        && SERVICE_TYPE.equals(s.getServiceType())), eq("_subtype"));
+                        && SERVICE_TYPE.equals(s.getServiceType())
+                        && s.getSubtypes().equals(Set.of("_subtype"))), any(), anyInt());
 
         final DiscoveryListener discListener = mock(DiscoveryListener.class);
         client.discoverServices(typeWithSubtype, PROTOCOL, network, Runnable::run, discListener);
@@ -1008,8 +1308,8 @@
         final Network network = new Network(999);
         final String serviceType = "_nsd._service._tcp";
         final String constructedServiceType = "_service._tcp.local";
-        final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
-                ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
+        final ArgumentCaptor<MdnsListener> listenerCaptor =
+                ArgumentCaptor.forClass(MdnsListener.class);
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, serviceType);
         request.setNetwork(network);
         client.resolveService(request, resolveListener);
@@ -1024,7 +1324,7 @@
         // Subtypes are not used for resolution, only for discovery
         assertEquals(Collections.emptyList(), optionsCaptor.getValue().getSubtypes());
 
-        final MdnsServiceBrowserListener listener = listenerCaptor.getValue();
+        final MdnsListener listener = listenerCaptor.getValue();
         final MdnsServiceInfo mdnsServiceInfo = new MdnsServiceInfo(
                 SERVICE_NAME,
                 constructedServiceType.split("\\."),
@@ -1037,13 +1337,18 @@
                 List.of(MdnsServiceInfo.TextEntry.fromBytes(new byte[]{
                         'k', 'e', 'y', '=', (byte) 0xFF, (byte) 0xFE})) /* textEntries */,
                 1234,
-                network);
+                network,
+                Instant.ofEpochSecond(1000_000L) /* expirationTime */);
 
         // Verify onServiceFound callback
-        listener.onServiceFound(mdnsServiceInfo);
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
+        listener.onServiceFound(mdnsServiceInfo, true /* isServiceFromCache */);
         final ArgumentCaptor<NsdServiceInfo> infoCaptor =
                 ArgumentCaptor.forClass(NsdServiceInfo.class);
         verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(infoCaptor.capture());
+        verify(mMetrics).reportServiceResolved(false /* isLegacy */, listener.mTransactionId,
+                10 /* durationMs */, true /* isServiceFromCache */, 0 /* sendQueryCount */);
+
         final NsdServiceInfo info = infoCaptor.getValue();
         assertEquals(SERVICE_NAME, info.getServiceName());
         assertEquals("._service._tcp", info.getServiceType());
@@ -1058,6 +1363,7 @@
         assertTrue(info.getHostAddresses().stream().anyMatch(
                 address -> address.equals(parseNumericAddress("2001:db8::2"))));
         assertEquals(network, info.getNetwork());
+        assertEquals(Instant.ofEpochSecond(1000_000L), info.getExpirationTime());
 
         // Verify the listener has been unregistered.
         verify(mDiscoveryManager, timeout(TIMEOUT_MS))
@@ -1067,6 +1373,7 @@
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testMdnsAdvertiserFeatureFlagging() {
         // Create NsdService w/o feature enabled.
         final NsdManager client = connectClient(mService);
@@ -1088,14 +1395,16 @@
         waitForIdle();
 
         final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
-        verify(mAdvertiser).addService(serviceIdCaptor.capture(),
-                argThat(info -> matches(info, regInfo)), eq(null) /* subtype */);
+        verify(mAdvertiser).addOrUpdateService(serviceIdCaptor.capture(),
+                argThat(info -> matches(info, regInfo)), any(), anyInt());
 
         client.unregisterService(regListenerWithoutFeature);
         waitForIdle();
         verify(mMockMDnsM).stopOperation(legacyIdCaptor.getValue());
         verify(mAdvertiser, never()).removeService(anyInt());
 
+        doReturn(mock(MdnsAdvertiser.AdvertiserMetrics.class))
+                .when(mAdvertiser).getAdvertiserMetrics(anyInt());
         client.unregisterService(regListenerWithFeature);
         waitForIdle();
         verify(mAdvertiser).removeService(serviceIdCaptor.getValue());
@@ -1103,6 +1412,7 @@
 
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testTypeSpecificFeatureFlagging() {
         doReturn("_type1._tcp:flag1,_type2._tcp:flag2").when(mDeps).getTypeAllowlistFlags();
         doReturn(true).when(mDeps).isFeatureEnabled(any(),
@@ -1115,7 +1425,7 @@
         service1.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
         service1.setPort(1234);
         final NsdServiceInfo service2 = new NsdServiceInfo(SERVICE_NAME, "_type2._tcp");
-        service2.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
+        service1.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
         service2.setPort(1234);
 
         client.discoverServices(service1.getServiceType(),
@@ -1146,10 +1456,10 @@
         waitForIdle();
 
         // The advertiser is enabled for _type2 but not _type1
-        verify(mAdvertiser, never()).addService(
-                anyInt(), argThat(info -> matches(info, service1)), eq(null) /* subtype */);
-        verify(mAdvertiser).addService(
-                anyInt(), argThat(info -> matches(info, service2)), eq(null) /* subtype */);
+        verify(mAdvertiser, never()).addOrUpdateService(anyInt(),
+                argThat(info -> matches(info, service1)), any(), anyInt());
+        verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(info -> matches(info, service2)),
+                any(), anyInt());
     }
 
     @Test
@@ -1161,7 +1471,7 @@
         // final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
         final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
                 ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
-        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any());
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
 
         final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
         regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1173,22 +1483,34 @@
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
         final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
-        verify(mAdvertiser).addService(idCaptor.capture(), argThat(info ->
-                matches(info, regInfo)), eq(null) /* subtype */);
+        verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), argThat(info ->
+                matches(info, regInfo)), any(), anyInt());
 
         // Verify onServiceRegistered callback
         final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
-        cb.onRegisterServiceSucceeded(idCaptor.getValue(), regInfo);
+        final int regId = idCaptor.getValue();
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
+        cb.onRegisterServiceSucceeded(regId, regInfo);
 
         verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(argThat(info -> matches(info,
                 new NsdServiceInfo(regInfo.getServiceName(), null))));
+        verify(mMetrics).reportServiceRegistrationSucceeded(
+                false /* isLegacy */, regId, 10L /* durationMs */);
 
+        final MdnsAdvertiser.AdvertiserMetrics metrics = new MdnsAdvertiser.AdvertiserMetrics(
+                50 /* repliedRequestCount */, 100 /* sentPacketCount */,
+                3 /* conflictDuringProbingCount */, 2 /* conflictAfterProbingCount */);
+        doReturn(TEST_TIME_MS + 100L).when(mClock).elapsedRealtime();
+        doReturn(metrics).when(mAdvertiser).getAdvertiserMetrics(regId);
         client.unregisterService(regListener);
         waitForIdle();
         verify(mAdvertiser).removeService(idCaptor.getValue());
         verify(regListener, timeout(TIMEOUT_MS)).onServiceUnregistered(
                 argThat(info -> matches(info, regInfo)));
         verify(mSocketProvider, timeout(TIMEOUT_MS)).requestStopWhenInactive();
+        verify(mMetrics).reportServiceUnregistration(false /* isLegacy */, regId,
+                100L /* durationMs */, 50 /* repliedRequestCount */, 100 /* sentPacketCount */,
+                3 /* conflictDuringProbingCount */, 2 /* conflictAfterProbingCount */);
     }
 
     @Test
@@ -1200,7 +1522,7 @@
         // final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
         final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
                 ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
-        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any());
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
 
         final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, "invalid_type");
         regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1210,10 +1532,12 @@
 
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
-        verify(mAdvertiser, never()).addService(anyInt(), any(), any());
+        verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any(), anyInt());
 
         verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
                 argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
+        verify(mMetrics).reportServiceRegistrationFailed(
+                false /* isLegacy */, NO_TRANSACTION, 0L /* durationMs */);
     }
 
     @Test
@@ -1225,7 +1549,7 @@
         // final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
         final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
                 ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
-        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any());
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
 
         final NsdServiceInfo regInfo = new NsdServiceInfo("a".repeat(70), SERVICE_TYPE);
         regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1237,16 +1561,99 @@
         waitForIdle();
         final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
         // Service name is truncated to 63 characters
-        verify(mAdvertiser).addService(idCaptor.capture(),
-                argThat(info -> info.getServiceName().equals("a".repeat(63))),
-                eq(null) /* subtype */);
+        verify(mAdvertiser)
+                .addOrUpdateService(
+                        idCaptor.capture(),
+                        argThat(info -> info.getServiceName().equals("a".repeat(63))),
+                        any(),
+                        anyInt());
 
         // Verify onServiceRegistered callback
         final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
-        cb.onRegisterServiceSucceeded(idCaptor.getValue(), regInfo);
+        final int regId = idCaptor.getValue();
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
+        cb.onRegisterServiceSucceeded(regId, regInfo);
 
         verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(
                 argThat(info -> matches(info, new NsdServiceInfo(regInfo.getServiceName(), null))));
+        verify(mMetrics).reportServiceRegistrationSucceeded(
+                false /* isLegacy */, regId, 10L /* durationMs */);
+    }
+
+    @Test
+    public void testAdvertiseCustomTtl_validTtl_success() {
+        runValidTtlAdvertisingTest(30L);
+        runValidTtlAdvertisingTest(10 * 3600L);
+    }
+
+    @Test
+    public void testAdvertiseCustomTtl_ttlSmallerThan30SecondsButClientIsSystemServer_success() {
+        when(mDeps.getCallingUid()).thenReturn(Process.SYSTEM_UID);
+
+        runValidTtlAdvertisingTest(29L);
+    }
+
+    @Test
+    public void testAdvertiseCustomTtl_ttlLargerThan10HoursButClientIsSystemServer_success() {
+        when(mDeps.getCallingUid()).thenReturn(Process.SYSTEM_UID);
+
+        runValidTtlAdvertisingTest(10 * 3600L + 1);
+        runValidTtlAdvertisingTest(0xffffffffL);
+    }
+
+    private void runValidTtlAdvertisingTest(long validTtlSeconds) {
+        setMdnsAdvertiserEnabled();
+
+        final NsdManager client = connectClient(mService);
+        final RegistrationListener regListener = mock(RegistrationListener.class);
+        final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+                ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
+
+        final NsdServiceInfo regInfo = new NsdServiceInfo("Service custom TTL", SERVICE_TYPE);
+        regInfo.setPort(1234);
+        final AdvertisingRequest request =
+                new AdvertisingRequest.Builder(regInfo, NsdManager.PROTOCOL_DNS_SD)
+                    .setTtl(Duration.ofSeconds(validTtlSeconds)).build();
+
+        client.registerService(request, Runnable::run, regListener);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
+        final MdnsAdvertisingOptions expectedAdverstingOptions =
+                MdnsAdvertisingOptions.newBuilder().setTtl(request.getTtl()).build();
+        verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), any(),
+                eq(expectedAdverstingOptions), anyInt());
+
+        // Verify onServiceRegistered callback
+        final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
+        final int regId = idCaptor.getValue();
+        cb.onRegisterServiceSucceeded(regId, regInfo);
+
+        verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(
+                argThat(info -> matches(info, new NsdServiceInfo(regInfo.getServiceName(), null))));
+    }
+
+    @Test
+    public void testAdvertiseCustomTtl_invalidTtl_FailsWithBadParameters() {
+        setMdnsAdvertiserEnabled();
+        final long invalidTtlSeconds = 29L;
+        final NsdManager client = connectClient(mService);
+        final RegistrationListener regListener = mock(RegistrationListener.class);
+        final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+                ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
+
+        final NsdServiceInfo regInfo = new NsdServiceInfo("Service custom TTL", SERVICE_TYPE);
+        regInfo.setPort(1234);
+        final AdvertisingRequest request =
+                new AdvertisingRequest.Builder(regInfo, NsdManager.PROTOCOL_DNS_SD)
+                    .setTtl(Duration.ofSeconds(invalidTtlSeconds)).build();
+        client.registerService(request, Runnable::run, regListener);
+        waitForIdle();
+
+        verify(regListener, timeout(TIMEOUT_MS))
+                .onRegistrationFailed(any(), eq(FAILURE_BAD_PARAMETERS));
     }
 
     @Test
@@ -1258,8 +1665,8 @@
         final Network network = new Network(999);
         final String serviceType = "_nsd._service._tcp";
         final String constructedServiceType = "_service._tcp.local";
-        final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
-                ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
+        final ArgumentCaptor<MdnsListener> listenerCaptor =
+                ArgumentCaptor.forClass(MdnsListener.class);
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, serviceType);
         request.setNetwork(network);
         client.resolveService(request, resolveListener);
@@ -1274,16 +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.
         verify(mDiscoveryManager, timeout(TIMEOUT_MS))
-                .unregisterListener(eq(constructedServiceType), eq(listenerCaptor.getValue()));
+                .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 */, 1 /* sentQueryCount */);
     }
 
     @Test
@@ -1295,14 +1709,52 @@
         final String serviceType5 = "_TEST._999._tcp.";
         final String serviceType6 = "_998._tcp.,_TEST";
         final String serviceType7 = "_997._tcp,_TEST";
+        final String serviceType8 = "_997._tcp,_test1,_test2,_test3";
+        final String serviceType9 = "_test4._997._tcp,_test1,_test2,_test3";
 
         assertNull(parseTypeAndSubtype(serviceType1));
         assertNull(parseTypeAndSubtype(serviceType2));
         assertNull(parseTypeAndSubtype(serviceType3));
-        assertEquals(new Pair<>("_123._udp", null), parseTypeAndSubtype(serviceType4));
-        assertEquals(new Pair<>("_999._tcp", "_TEST"), parseTypeAndSubtype(serviceType5));
-        assertEquals(new Pair<>("_998._tcp", "_TEST"), parseTypeAndSubtype(serviceType6));
-        assertEquals(new Pair<>("_997._tcp", "_TEST"), parseTypeAndSubtype(serviceType7));
+        assertEquals(new Pair<>("_123._udp", Collections.emptyList()),
+                parseTypeAndSubtype(serviceType4));
+        assertEquals(new Pair<>("_999._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType5));
+        assertEquals(new Pair<>("_998._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType6));
+        assertEquals(new Pair<>("_997._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType7));
+
+        assertEquals(new Pair<>("_997._tcp", List.of("_test1", "_test2", "_test3")),
+                parseTypeAndSubtype(serviceType8));
+        assertEquals(new Pair<>("_997._tcp", List.of("_test4")),
+                parseTypeAndSubtype(serviceType9));
+    }
+
+    @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
@@ -1321,7 +1773,7 @@
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
-        verify(mAdvertiser).addService(anyInt(), any(), any());
+        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), anyInt());
 
         // Verify the discovery uses MdnsDiscoveryManager
         final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -1336,6 +1788,259 @@
         verify(mDiscoveryManager, times(2)).registerListener(anyString(), any(), any());
     }
 
+    @Test
+    @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    public void testTakeMulticastLockOnBehalfOfClient_ForWifiNetworksOnly() {
+        // Test on one client in the foreground
+        mUidImportanceListener.onUidImportance(123, IMPORTANCE_FOREGROUND);
+        doReturn(123).when(mDeps).getCallingUid();
+        final NsdManager client = connectClient(mService);
+
+        final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        regInfo.setHostAddresses(List.of(parseNumericAddress("192.0.2.123")));
+        regInfo.setPort(12345);
+        // File a request for all networks
+        regInfo.setNetwork(null);
+
+        final RegistrationListener regListener = mock(RegistrationListener.class);
+        client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
+        waitForIdle();
+        verify(mSocketProvider).startMonitoringSockets();
+        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), anyInt());
+
+        final Network wifiNetwork1 = new Network(123);
+        final Network wifiNetwork2 = new Network(124);
+        final Network ethernetNetwork = new Network(125);
+
+        final MdnsInterfaceSocket wifiNetworkSocket1 = mock(MdnsInterfaceSocket.class);
+        final MdnsInterfaceSocket wifiNetworkSocket2 = mock(MdnsInterfaceSocket.class);
+        final MdnsInterfaceSocket ethernetNetworkSocket = mock(MdnsInterfaceSocket.class);
+
+        // Nothing happens for networks with no transports, no Wi-Fi transport, or VPN transport
+        mHandler.post(() -> {
+            mSocketRequestMonitor.onSocketRequestFulfilled(
+                    new Network(125), mock(MdnsInterfaceSocket.class), new int[0]);
+            mSocketRequestMonitor.onSocketRequestFulfilled(
+                    ethernetNetwork, ethernetNetworkSocket,
+                    new int[] { TRANSPORT_ETHERNET });
+            mSocketRequestMonitor.onSocketRequestFulfilled(
+                    new Network(127), mock(MdnsInterfaceSocket.class),
+                    new int[] { TRANSPORT_WIFI, TRANSPORT_VPN });
+        });
+        waitForIdle();
+        verify(mWifiManager, never()).createMulticastLock(any());
+
+        // First Wi-Fi network
+        mHandler.post(() -> mSocketRequestMonitor.onSocketRequestFulfilled(
+                wifiNetwork1, wifiNetworkSocket1, new int[] { TRANSPORT_WIFI }));
+        waitForIdle();
+        verify(mWifiManager).createMulticastLock(any());
+        verify(mMulticastLock).acquire();
+
+        // Second Wi-Fi network
+        mHandler.post(() -> mSocketRequestMonitor.onSocketRequestFulfilled(
+                wifiNetwork2, wifiNetworkSocket2, new int[] { TRANSPORT_WIFI }));
+        waitForIdle();
+        verifyNoMoreInteractions(mMulticastLock);
+
+        // One Wi-Fi network becomes unused, nothing happens
+        mHandler.post(() -> mSocketRequestMonitor.onSocketDestroyed(
+                wifiNetwork1, wifiNetworkSocket1));
+        waitForIdle();
+        verifyNoMoreInteractions(mMulticastLock);
+
+        // Ethernet network becomes unused, still nothing
+        mHandler.post(() -> mSocketRequestMonitor.onSocketDestroyed(
+                ethernetNetwork, ethernetNetworkSocket));
+        waitForIdle();
+        verifyNoMoreInteractions(mMulticastLock);
+
+        // The second Wi-Fi network becomes unused, the lock is released
+        mHandler.post(() -> mSocketRequestMonitor.onSocketDestroyed(
+                wifiNetwork2, wifiNetworkSocket2));
+        waitForIdle();
+        verify(mMulticastLock).release();
+    }
+
+    @Test
+    @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    public void testTakeMulticastLockOnBehalfOfClient_ForForegroundAppsOnly() {
+        final int uid1 = 12;
+        final int uid2 = 34;
+        final int uid3 = 56;
+        final int uid4 = 78;
+        final InOrder lockOrder = inOrder(mMulticastLock);
+        // Connect one client without any foreground info
+        doReturn(uid1).when(mDeps).getCallingUid();
+        final NsdManager client1 = connectClient(mService);
+
+        // Connect client2 as visible, but not foreground
+        mUidImportanceListener.onUidImportance(uid2, IMPORTANCE_VISIBLE);
+        waitForIdle();
+        doReturn(uid2).when(mDeps).getCallingUid();
+        final NsdManager client2 = connectClient(mService);
+
+        // Connect client3, client4 as foreground
+        mUidImportanceListener.onUidImportance(uid3, IMPORTANCE_FOREGROUND);
+        waitForIdle();
+        doReturn(uid3).when(mDeps).getCallingUid();
+        final NsdManager client3 = connectClient(mService);
+
+        mUidImportanceListener.onUidImportance(uid4, IMPORTANCE_FOREGROUND);
+        waitForIdle();
+        doReturn(uid4).when(mDeps).getCallingUid();
+        final NsdManager client4 = connectClient(mService);
+
+        // First client advertises on any network
+        final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        regInfo.setHostAddresses(List.of(parseNumericAddress("192.0.2.123")));
+        regInfo.setPort(12345);
+        regInfo.setNetwork(null);
+        final RegistrationListener regListener = mock(RegistrationListener.class);
+        client1.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
+        waitForIdle();
+
+        final MdnsInterfaceSocket wifiSocket = mock(MdnsInterfaceSocket.class);
+        final Network wifiNetwork = new Network(123);
+
+        final MdnsInterfaceSocket ethSocket = mock(MdnsInterfaceSocket.class);
+        final Network ethNetwork = new Network(234);
+
+        mHandler.post(() -> {
+            mSocketRequestMonitor.onSocketRequestFulfilled(
+                    wifiNetwork, wifiSocket, new int[] { TRANSPORT_WIFI });
+            mSocketRequestMonitor.onSocketRequestFulfilled(
+                    ethNetwork, ethSocket, new int[] { TRANSPORT_ETHERNET });
+        });
+        waitForIdle();
+
+        // No multicast lock since client1 has no foreground info
+        lockOrder.verifyNoMoreInteractions();
+
+        // Second client discovers specifically on the Wi-Fi network
+        final DiscoveryListener discListener = mock(DiscoveryListener.class);
+        client2.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, wifiNetwork,
+                Runnable::run, discListener);
+        waitForIdle();
+        mHandler.post(() -> mSocketRequestMonitor.onSocketRequestFulfilled(
+                wifiNetwork, wifiSocket, new int[] { TRANSPORT_WIFI }));
+        waitForIdle();
+        // No multicast lock since client2 is not visible enough
+        lockOrder.verifyNoMoreInteractions();
+
+        // Third client registers a callback on all networks
+        final NsdServiceInfo cbInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        cbInfo.setNetwork(null);
+        final ServiceInfoCallback infoCb = mock(ServiceInfoCallback.class);
+        client3.registerServiceInfoCallback(cbInfo, Runnable::run, infoCb);
+        waitForIdle();
+        mHandler.post(() -> {
+            mSocketRequestMonitor.onSocketRequestFulfilled(
+                    wifiNetwork, wifiSocket, new int[] { TRANSPORT_WIFI });
+            mSocketRequestMonitor.onSocketRequestFulfilled(
+                    ethNetwork, ethSocket, new int[] { TRANSPORT_ETHERNET });
+        });
+        waitForIdle();
+
+        // Multicast lock is taken for third client
+        lockOrder.verify(mMulticastLock).acquire();
+
+        // Client3 goes to the background
+        mUidImportanceListener.onUidImportance(uid3, IMPORTANCE_CACHED);
+        waitForIdle();
+        lockOrder.verify(mMulticastLock).release();
+
+        // client4 resolves on a different network
+        final ResolveListener resolveListener = mock(ResolveListener.class);
+        final NsdServiceInfo resolveInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        resolveInfo.setNetwork(ethNetwork);
+        client4.resolveService(resolveInfo, Runnable::run, resolveListener);
+        waitForIdle();
+        mHandler.post(() -> mSocketRequestMonitor.onSocketRequestFulfilled(
+                ethNetwork, ethSocket, new int[] { TRANSPORT_ETHERNET }));
+        waitForIdle();
+
+        // client4 is foreground, but not Wi-Fi
+        lockOrder.verifyNoMoreInteractions();
+
+        // Second client becomes foreground
+        mUidImportanceListener.onUidImportance(uid2, IMPORTANCE_FOREGROUND);
+        waitForIdle();
+
+        lockOrder.verify(mMulticastLock).acquire();
+
+        // Second client is lost
+        mUidImportanceListener.onUidImportance(uid2, IMPORTANCE_GONE);
+        waitForIdle();
+
+        lockOrder.verify(mMulticastLock).release();
+    }
+
+    @Test
+    public void testNullINsdManagerCallback() {
+        final NsdService service = new NsdService(mContext, mHandler, CLEANUP_DELAY_MS, mDeps) {
+            @Override
+            public INsdServiceConnector connect(INsdManagerCallback baseCb,
+                    boolean runNewMdnsBackend) {
+                // Pass null INsdManagerCallback
+                return super.connect(null /* cb */, runNewMdnsBackend);
+            }
+        };
+
+        assertThrows(IllegalArgumentException.class, () -> new NsdManager(mContext, service));
+    }
+
+    @Test
+    @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testRegisterOffloadEngine_checkPermission_V() {
+        final NsdManager client = connectClient(mService);
+        final OffloadEngine offloadEngine = mock(OffloadEngine.class);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(NETWORK_STACK);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(
+                PERMISSION_MAINLINE_NETWORK_STACK);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(NETWORK_SETTINGS);
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+                REGISTER_NSD_OFFLOAD_ENGINE);
+
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(
+                REGISTER_NSD_OFFLOAD_ENGINE);
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(DEVICE_POWER);
+        assertThrows(SecurityException.class,
+                () -> client.registerOffloadEngine("iface1", OffloadEngine.OFFLOAD_TYPE_REPLY,
+                        OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK, Runnable::run,
+                        offloadEngine));
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+                REGISTER_NSD_OFFLOAD_ENGINE);
+        final OffloadEngine offloadEngine2 = mock(OffloadEngine.class);
+        client.registerOffloadEngine("iface2", OffloadEngine.OFFLOAD_TYPE_REPLY,
+                OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK, Runnable::run,
+                offloadEngine2);
+        client.unregisterOffloadEngine(offloadEngine2);
+    }
+
+    @Test
+    @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testRegisterOffloadEngine_checkPermission_U() {
+        final NsdManager client = connectClient(mService);
+        final OffloadEngine offloadEngine = mock(OffloadEngine.class);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(NETWORK_STACK);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(
+                PERMISSION_MAINLINE_NETWORK_STACK);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(NETWORK_SETTINGS);
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+                REGISTER_NSD_OFFLOAD_ENGINE);
+
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(DEVICE_POWER);
+        client.registerOffloadEngine("iface2", OffloadEngine.OFFLOAD_TYPE_REPLY,
+                OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK, Runnable::run,
+                offloadEngine);
+        client.unregisterOffloadEngine(offloadEngine);
+    }
+
+
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
     }
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
deleted file mode 100644
index bf23cd1..0000000
--- a/tests/unit/java/com/android/server/VpnManagerServiceTest.java
+++ /dev/null
@@ -1,409 +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.server;
-
-import static android.os.Build.VERSION_CODES.R;
-
-import static com.android.testutils.ContextUtils.mockService;
-import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import static com.android.testutils.MiscAsserts.assertThrows;
-
-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.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.annotation.UserIdInt;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.net.ConnectivityManager;
-import android.net.INetd;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.INetworkManagementService;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.security.Credentials;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.net.VpnProfile;
-import com.android.server.connectivity.Vpn;
-import com.android.server.connectivity.VpnProfileStore;
-import com.android.server.net.LockdownVpnTracker;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-import com.android.testutils.HandlerUtils;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-
-@RunWith(DevSdkIgnoreRunner.class)
-@IgnoreUpTo(R) // VpnManagerService is not available before R
-@SmallTest
-public class VpnManagerServiceTest extends VpnTestBase {
-    private static final String CONTEXT_ATTRIBUTION_TAG = "VPN_MANAGER";
-
-    @Rule
-    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
-    private static final int TIMEOUT_MS = 2_000;
-
-    @Mock Context mContext;
-    @Mock Context mContextWithoutAttributionTag;
-    @Mock Context mSystemContext;
-    @Mock Context mUserAllContext;
-    private HandlerThread mHandlerThread;
-    @Mock private Vpn mVpn;
-    @Mock private INetworkManagementService mNms;
-    @Mock private ConnectivityManager mCm;
-    @Mock private UserManager mUserManager;
-    @Mock private INetd mNetd;
-    @Mock private PackageManager mPackageManager;
-    @Mock private VpnProfileStore mVpnProfileStore;
-    @Mock private LockdownVpnTracker mLockdownVpnTracker;
-
-    private VpnManagerServiceDependencies mDeps;
-    private VpnManagerService mService;
-    private BroadcastReceiver mUserPresentReceiver;
-    private BroadcastReceiver mIntentReceiver;
-    private final String mNotMyVpnPkg = "com.not.my.vpn";
-
-    class VpnManagerServiceDependencies extends VpnManagerService.Dependencies {
-        @Override
-        public HandlerThread makeHandlerThread() {
-            return mHandlerThread;
-        }
-
-        @Override
-        public INetworkManagementService getINetworkManagementService() {
-            return mNms;
-        }
-
-        @Override
-        public INetd getNetd() {
-            return mNetd;
-        }
-
-        @Override
-        public Vpn createVpn(Looper looper, Context context, INetworkManagementService nms,
-                INetd netd, @UserIdInt int userId) {
-            return mVpn;
-        }
-
-        @Override
-        public VpnProfileStore getVpnProfileStore() {
-            return mVpnProfileStore;
-        }
-
-        @Override
-        public LockdownVpnTracker createLockDownVpnTracker(Context context, Handler handler,
-                Vpn vpn, VpnProfile profile) {
-            return mLockdownVpnTracker;
-        }
-
-        @Override
-        public @UserIdInt int getMainUserId() {
-            return UserHandle.USER_SYSTEM;
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mHandlerThread = new HandlerThread("TestVpnManagerService");
-        mDeps = new VpnManagerServiceDependencies();
-
-        // The attribution tag is a dependency for IKE library to collect VPN metrics correctly
-        // and thus should not be changed without updating the IKE code.
-        doReturn(mContext)
-                .when(mContextWithoutAttributionTag)
-                .createAttributionContext(CONTEXT_ATTRIBUTION_TAG);
-
-        doReturn(mUserAllContext).when(mContext).createContextAsUser(UserHandle.ALL, 0);
-        doReturn(mSystemContext).when(mContext).createContextAsUser(UserHandle.SYSTEM, 0);
-        doReturn(mPackageManager).when(mContext).getPackageManager();
-        setMockedPackages(mPackageManager, sPackages);
-
-        mockService(mContext, ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mCm);
-        mockService(mContext, UserManager.class, Context.USER_SERVICE, mUserManager);
-        doReturn(SYSTEM_USER).when(mUserManager).getUserInfo(eq(SYSTEM_USER_ID));
-
-        mService = new VpnManagerService(mContextWithoutAttributionTag, mDeps);
-        mService.systemReady();
-
-        final ArgumentCaptor<BroadcastReceiver> intentReceiverCaptor =
-                ArgumentCaptor.forClass(BroadcastReceiver.class);
-        final ArgumentCaptor<BroadcastReceiver> userPresentReceiverCaptor =
-                ArgumentCaptor.forClass(BroadcastReceiver.class);
-        verify(mSystemContext).registerReceiver(
-                userPresentReceiverCaptor.capture(), any(), any(), any());
-        verify(mUserAllContext, times(2)).registerReceiver(
-                intentReceiverCaptor.capture(), any(), any(), any());
-        mUserPresentReceiver = userPresentReceiverCaptor.getValue();
-        mIntentReceiver = intentReceiverCaptor.getValue();
-
-        // Add user to create vpn in mVpn
-        onUserStarted(SYSTEM_USER_ID);
-        assertNotNull(mService.mVpns.get(SYSTEM_USER_ID));
-    }
-
-    @Test
-    public void testUpdateAppExclusionList() {
-        // Start vpn
-        mService.startVpnProfile(TEST_VPN_PKG);
-        verify(mVpn).startVpnProfile(eq(TEST_VPN_PKG));
-
-        // Remove package due to package replaced.
-        onPackageRemoved(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
-        verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
-
-        // Add package due to package replaced.
-        onPackageAdded(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
-        verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
-
-        // Remove package
-        onPackageRemoved(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
-        verify(mVpn).refreshPlatformVpnAppExclusionList();
-
-        // Add the package back
-        onPackageAdded(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
-        verify(mVpn, times(2)).refreshPlatformVpnAppExclusionList();
-    }
-
-    @Test
-    public void testStartVpnProfileFromDiffPackage() {
-        assertThrows(
-                SecurityException.class, () -> mService.startVpnProfile(mNotMyVpnPkg));
-    }
-
-    @Test
-    public void testStopVpnProfileFromDiffPackage() {
-        assertThrows(SecurityException.class, () -> mService.stopVpnProfile(mNotMyVpnPkg));
-    }
-
-    @Test
-    public void testGetProvisionedVpnProfileStateFromDiffPackage() {
-        assertThrows(SecurityException.class, () ->
-                mService.getProvisionedVpnProfileState(mNotMyVpnPkg));
-    }
-
-    @Test
-    public void testGetProvisionedVpnProfileState() {
-        mService.getProvisionedVpnProfileState(TEST_VPN_PKG);
-        verify(mVpn).getProvisionedVpnProfileState(TEST_VPN_PKG);
-    }
-
-    private Intent buildIntent(String action, String packageName, int userId, int uid,
-            boolean isReplacing) {
-        final Intent intent = new Intent(action);
-        intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
-        intent.putExtra(Intent.EXTRA_UID, uid);
-        intent.putExtra(Intent.EXTRA_REPLACING, isReplacing);
-        if (packageName != null) {
-            intent.setData(Uri.fromParts("package" /* scheme */, packageName, null /* fragment */));
-        }
-
-        return intent;
-    }
-
-    private void sendIntent(Intent intent) {
-        sendIntent(mIntentReceiver, mContext, intent);
-    }
-
-    private void sendIntent(BroadcastReceiver receiver, Context context, Intent intent) {
-        final Handler h = mHandlerThread.getThreadHandler();
-
-        // Send in handler thread.
-        h.post(() -> receiver.onReceive(context, intent));
-        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
-    }
-
-    private void onUserStarted(int userId) {
-        sendIntent(buildIntent(Intent.ACTION_USER_STARTED,
-                null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
-    }
-
-    private void onUserUnlocked(int userId) {
-        sendIntent(buildIntent(Intent.ACTION_USER_UNLOCKED,
-                null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
-    }
-
-    private void onUserStopped(int userId) {
-        sendIntent(buildIntent(Intent.ACTION_USER_STOPPED,
-                null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
-    }
-
-    private void onLockDownReset() {
-        sendIntent(buildIntent(LockdownVpnTracker.ACTION_LOCKDOWN_RESET, null /* packageName */,
-                UserHandle.USER_SYSTEM, -1 /* uid */, false /* isReplacing */));
-    }
-
-    private void onPackageAdded(String packageName, int userId, int uid, boolean isReplacing) {
-        sendIntent(buildIntent(Intent.ACTION_PACKAGE_ADDED, packageName, userId, uid, isReplacing));
-    }
-
-    private void onPackageAdded(String packageName, int uid, boolean isReplacing) {
-        onPackageAdded(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
-    }
-
-    private void onPackageRemoved(String packageName, int userId, int uid, boolean isReplacing) {
-        sendIntent(buildIntent(Intent.ACTION_PACKAGE_REMOVED, packageName, userId, uid,
-                isReplacing));
-    }
-
-    private void onPackageRemoved(String packageName, int uid, boolean isReplacing) {
-        onPackageRemoved(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
-    }
-
-    @Test
-    public void testReceiveIntentFromNonHandlerThread() {
-        assertThrows(IllegalStateException.class, () ->
-                mIntentReceiver.onReceive(mContext, buildIntent(Intent.ACTION_PACKAGE_REMOVED,
-                        PKGS[0], UserHandle.USER_SYSTEM, PKG_UIDS[0], true /* isReplacing */)));
-
-        assertThrows(IllegalStateException.class, () ->
-                mUserPresentReceiver.onReceive(mContext, new Intent(Intent.ACTION_USER_PRESENT)));
-    }
-
-    private void setupLockdownVpn(String packageName) {
-        final byte[] profileTag = packageName.getBytes(StandardCharsets.UTF_8);
-        doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN);
-    }
-
-    private void setupVpnProfile(String profileName) {
-        final VpnProfile profile = new VpnProfile(profileName);
-        profile.name = profileName;
-        profile.server = "192.0.2.1";
-        profile.dnsServers = "8.8.8.8";
-        profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK;
-        final byte[] encodedProfile = profile.encode();
-        doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName);
-    }
-
-    @Test
-    public void testUserPresent() {
-        // Verify that LockDownVpnTracker is not created.
-        verify(mLockdownVpnTracker, never()).init();
-
-        setupLockdownVpn(TEST_VPN_PKG);
-        setupVpnProfile(TEST_VPN_PKG);
-
-        // mUserPresentReceiver only registers ACTION_USER_PRESENT intent and does no verification
-        // on action, so an empty intent is enough.
-        sendIntent(mUserPresentReceiver, mSystemContext, new Intent());
-
-        verify(mLockdownVpnTracker).init();
-        verify(mSystemContext).unregisterReceiver(mUserPresentReceiver);
-        verify(mUserAllContext, never()).unregisterReceiver(any());
-    }
-
-    @Test
-    public void testUpdateLockdownVpn() {
-        setupLockdownVpn(TEST_VPN_PKG);
-        onUserUnlocked(SYSTEM_USER_ID);
-
-        // Will not create lockDownVpnTracker w/o valid profile configured in the keystore
-        verify(mLockdownVpnTracker, never()).init();
-
-        setupVpnProfile(TEST_VPN_PKG);
-
-        // Remove the user from mVpns
-        onUserStopped(SYSTEM_USER_ID);
-        onUserUnlocked(SYSTEM_USER_ID);
-        verify(mLockdownVpnTracker, never()).init();
-
-        // Add user back
-        onUserStarted(SYSTEM_USER_ID);
-        verify(mLockdownVpnTracker).init();
-
-        // Trigger another update. The existing LockDownVpnTracker should be shut down and
-        // initialize another one.
-        onUserUnlocked(SYSTEM_USER_ID);
-        verify(mLockdownVpnTracker).shutdown();
-        verify(mLockdownVpnTracker, times(2)).init();
-    }
-
-    @Test
-    public void testLockdownReset() {
-        // Init LockdownVpnTracker
-        setupLockdownVpn(TEST_VPN_PKG);
-        setupVpnProfile(TEST_VPN_PKG);
-        onUserUnlocked(SYSTEM_USER_ID);
-        verify(mLockdownVpnTracker).init();
-
-        onLockDownReset();
-        verify(mLockdownVpnTracker).reset();
-    }
-
-    @Test
-    public void testLockdownResetWhenLockdownVpnTrackerIsNotInit() {
-        setupLockdownVpn(TEST_VPN_PKG);
-        setupVpnProfile(TEST_VPN_PKG);
-
-        onLockDownReset();
-
-        // LockDownVpnTracker is not created. Lockdown reset will not take effect.
-        verify(mLockdownVpnTracker, never()).reset();
-    }
-
-    @Test
-    public void testIsVpnLockdownEnabled() {
-        // Vpn is created but the VPN lockdown is not enabled.
-        assertFalse(mService.isVpnLockdownEnabled(SYSTEM_USER_ID));
-
-        // Set lockdown for the SYSTEM_USER_ID VPN.
-        doReturn(true).when(mVpn).getLockdown();
-        assertTrue(mService.isVpnLockdownEnabled(SYSTEM_USER_ID));
-
-        // Even lockdown is enabled but no Vpn is created for SECONDARY_USER.
-        assertFalse(mService.isVpnLockdownEnabled(SECONDARY_USER.id));
-    }
-
-    @Test
-    public void testGetVpnLockdownAllowlist() {
-        doReturn(null).when(mVpn).getLockdownAllowlist();
-        assertNull(mService.getVpnLockdownAllowlist(SYSTEM_USER_ID));
-
-        final List<String> expected = List.of(PKGS);
-        doReturn(expected).when(mVpn).getLockdownAllowlist();
-        assertEquals(expected, mService.getVpnLockdownAllowlist(SYSTEM_USER_ID));
-
-        // Even lockdown is enabled but no Vpn is created for SECONDARY_USER.
-        assertNull(mService.getVpnLockdownAllowlist(SECONDARY_USER.id));
-    }
-}
diff --git a/tests/unit/java/com/android/server/VpnTestBase.java b/tests/unit/java/com/android/server/VpnTestBase.java
deleted file mode 100644
index 6113872..0000000
--- a/tests/unit/java/com/android/server/VpnTestBase.java
+++ /dev/null
@@ -1,97 +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.server;
-
-import static android.content.pm.UserInfo.FLAG_ADMIN;
-import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
-import static android.content.pm.UserInfo.FLAG_PRIMARY;
-import static android.content.pm.UserInfo.FLAG_RESTRICTED;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
-
-import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
-import android.os.Process;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/** Common variables or methods shared between VpnTest and VpnManagerServiceTest. */
-public class VpnTestBase {
-    protected static final String TEST_VPN_PKG = "com.testvpn.vpn";
-    /**
-     * Names and UIDs for some fake packages. Important points:
-     *  - UID is ordered increasing.
-     *  - One pair of packages have consecutive UIDs.
-     */
-    protected static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
-    protected static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
-    // Mock packages
-    protected static final Map<String, Integer> sPackages = new ArrayMap<>();
-    static {
-        for (int i = 0; i < PKGS.length; i++) {
-            sPackages.put(PKGS[i], PKG_UIDS[i]);
-        }
-        sPackages.put(TEST_VPN_PKG, Process.myUid());
-    }
-
-    // Mock users
-    protected static final int SYSTEM_USER_ID = 0;
-    protected static final UserInfo SYSTEM_USER = new UserInfo(0, "system", UserInfo.FLAG_PRIMARY);
-    protected static final UserInfo PRIMARY_USER = new UserInfo(27, "Primary",
-            FLAG_ADMIN | FLAG_PRIMARY);
-    protected static final UserInfo SECONDARY_USER = new UserInfo(15, "Secondary", FLAG_ADMIN);
-    protected static final UserInfo RESTRICTED_PROFILE_A = new UserInfo(40, "RestrictedA",
-            FLAG_RESTRICTED);
-    protected static final UserInfo RESTRICTED_PROFILE_B = new UserInfo(42, "RestrictedB",
-            FLAG_RESTRICTED);
-    protected static final UserInfo MANAGED_PROFILE_A = new UserInfo(45, "ManagedA",
-            FLAG_MANAGED_PROFILE);
-    static {
-        RESTRICTED_PROFILE_A.restrictedProfileParentId = PRIMARY_USER.id;
-        RESTRICTED_PROFILE_B.restrictedProfileParentId = SECONDARY_USER.id;
-        MANAGED_PROFILE_A.profileGroupId = PRIMARY_USER.id;
-    }
-
-    // Populate a fake packageName-to-UID mapping.
-    protected void setMockedPackages(PackageManager mockPm, final Map<String, Integer> packages) {
-        try {
-            doAnswer(invocation -> {
-                final String appName = (String) invocation.getArguments()[0];
-                final int userId = (int) invocation.getArguments()[1];
-
-                final Integer appId = packages.get(appName);
-                if (appId == null) {
-                    throw new PackageManager.NameNotFoundException(appName);
-                }
-
-                return UserHandle.getUid(userId, appId);
-            }).when(mockPm).getPackageUidAsUser(anyString(), anyInt());
-        } catch (Exception e) {
-        }
-    }
-
-    protected List<Integer> toList(int[] arr) {
-        return Arrays.stream(arr).boxed().collect(Collectors.toList());
-    }
-}
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 0b20227..c53feee 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -20,10 +20,8 @@
 import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-
 import static com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS;
 import static com.android.testutils.HandlerUtils.visibleOnHandlerThread;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -32,7 +30,6 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.longThat;
@@ -52,6 +49,7 @@
 import android.content.res.Resources;
 import android.net.INetd;
 import android.net.ISocketKeepaliveCallback;
+import android.net.InetAddresses;
 import android.net.KeepalivePacketData;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -71,21 +69,26 @@
 import android.os.Message;
 import android.os.SystemClock;
 import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Log;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-
-import com.android.connectivity.resources.R;
+import androidx.test.filters.SmallTest;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.AutomaticOnOffKeepalive;
 import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
-
+import java.io.FileDescriptor;
+import java.io.StringWriter;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
 import libcore.util.HexEncoding;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -94,14 +97,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.io.FileDescriptor;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.ArrayList;
-import java.util.List;
-
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@@ -116,7 +111,8 @@
     private static final int MOCK_RESOURCE_ID = 5;
     private static final int TEST_KEEPALIVE_INTERVAL_SEC = 10;
     private static final int TEST_KEEPALIVE_INVALID_INTERVAL_SEC = 9;
-
+    private static final byte[] V4_SRC_ADDR = new byte[] { (byte) 192, 0, 0, (byte) 129 };
+    private static final String TEST_V4_IFACE = "v4-testIface";
     private AutomaticOnOffKeepaliveTracker mAOOKeepaliveTracker;
     private HandlerThread mHandlerThread;
 
@@ -126,7 +122,7 @@
     @Mock AlarmManager mAlarmManager;
     @Mock NetworkAgentInfo mNai;
     @Mock SubscriptionManager mSubscriptionManager;
-
+    @Mock KeepaliveTracker.Dependencies mKeepaliveTrackerDeps;
     KeepaliveStatsTracker mKeepaliveStatsTracker;
     TestKeepaliveTracker mKeepaliveTracker;
     AOOTestHandler mTestHandler;
@@ -135,81 +131,81 @@
     // Hexadecimal representation of a SOCK_DIAG response with tcp info.
     private static final String SOCK_DIAG_TCP_INET_HEX =
             // struct nlmsghdr.
-            "14010000" +        // length = 276
-            "1400" +            // type = SOCK_DIAG_BY_FAMILY
-            "0301" +            // flags = NLM_F_REQUEST | NLM_F_DUMP
-            "00000000" +        // seqno
-            "00000000" +        // pid (0 == kernel)
+            "14010000"        // length = 276
+            + "1400"            // type = SOCK_DIAG_BY_FAMILY
+            + "0301"            // flags = NLM_F_REQUEST | NLM_F_DUMP
+            + "00000000"        // seqno
+            + "00000000"        // pid (0 == kernel)
             // struct inet_diag_req_v2
-            "02" +              // family = AF_INET
-            "06" +              // state
-            "00" +              // timer
-            "00" +              // retrans
+            + "02"              // family = AF_INET
+            + "06"              // state
+            + "00"              // timer
+            + "00"              // retrans
             // inet_diag_sockid
-            "DEA5" +            // idiag_sport = 42462
-            "71B9" +            // idiag_dport = 47473
-            "0a006402000000000000000000000000" + // idiag_src = 10.0.100.2
-            "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8
-            "00000000" +            // idiag_if
-            "34ED000076270000" +    // idiag_cookie = 43387759684916
-            "00000000" +            // idiag_expires
-            "00000000" +            // idiag_rqueue
-            "00000000" +            // idiag_wqueue
-            "00000000" +            // idiag_uid
-            "00000000" +            // idiag_inode
+            + "DEA5"            // idiag_sport = 42462
+            + "71B9"            // idiag_dport = 47473
+            + "0a006402000000000000000000000000" // idiag_src = 10.0.100.2
+            + "08080808000000000000000000000000" // idiag_dst = 8.8.8.8
+            + "00000000"            // idiag_if
+            + "34ED000076270000"    // idiag_cookie = 43387759684916
+            + "00000000"            // idiag_expires
+            + "00000000"            // idiag_rqueue
+            + "00000000"            // idiag_wqueue
+            + "39300000"            // idiag_uid = 12345
+            + "00000000"            // idiag_inode
             // rtattr
-            "0500" +            // len = 5
-            "0800" +            // type = 8
-            "00000000" +        // data
-            "0800" +            // len = 8
-            "0F00" +            // type = 15(INET_DIAG_MARK)
-            "850A0C00" +        // data, socket mark=789125
-            "AC00" +            // len = 172
-            "0200" +            // type = 2(INET_DIAG_INFO)
+            + "0500"            // len = 5
+            + "0800"            // type = 8
+            + "00000000"        // data
+            + "0800"            // len = 8
+            + "0F00"            // type = 15(INET_DIAG_MARK)
+            + "850A0C00"        // data, socket mark=789125
+            + "AC00"            // len = 172
+            + "0200"            // type = 2(INET_DIAG_INFO)
             // tcp_info
-            "01" +              // state = TCP_ESTABLISHED
-            "00" +              // ca_state = TCP_CA_OPEN
-            "05" +              // retransmits = 5
-            "00" +              // probes = 0
-            "00" +              // backoff = 0
-            "07" +              // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
-            "88" +              // wscale = 8
-            "00" +              // delivery_rate_app_limited = 0
-            "4A911B00" +        // rto = 1806666
-            "00000000" +        // ato = 0
-            "2E050000" +        // sndMss = 1326
-            "18020000" +        // rcvMss = 536
-            "00000000" +        // unsacked = 0
-            "00000000" +        // acked = 0
-            "00000000" +        // lost = 0
-            "00000000" +        // retrans = 0
-            "00000000" +        // fackets = 0
-            "BB000000" +        // lastDataSent = 187
-            "00000000" +        // lastAckSent = 0
-            "BB000000" +        // lastDataRecv = 187
-            "BB000000" +        // lastDataAckRecv = 187
-            "DC050000" +        // pmtu = 1500
-            "30560100" +        // rcvSsthresh = 87600
-            "3E2C0900" +        // rttt = 601150
-            "1F960400" +        // rttvar = 300575
-            "78050000" +        // sndSsthresh = 1400
-            "0A000000" +        // sndCwnd = 10
-            "A8050000" +        // advmss = 1448
-            "03000000" +        // reordering = 3
-            "00000000" +        // rcvrtt = 0
-            "30560100" +        // rcvspace = 87600
-            "00000000" +        // totalRetrans = 0
-            "53AC000000000000" +    // pacingRate = 44115
-            "FFFFFFFFFFFFFFFF" +    // maxPacingRate = 18446744073709551615
-            "0100000000000000" +    // bytesAcked = 1
-            "0000000000000000" +    // bytesReceived = 0
-            "0A000000" +        // SegsOut = 10
-            "00000000" +        // SegsIn = 0
-            "00000000" +        // NotSentBytes = 0
-            "3E2C0900" +        // minRtt = 601150
-            "00000000" +        // DataSegsIn = 0
-            "00000000" +        // DataSegsOut = 0
-            "0000000000000000"; // deliverRate = 0
+            + "01"               // state = TCP_ESTABLISHED
+            + "00"               // ca_state = TCP_CA_OPEN
+            + "05"               // retransmits = 5
+            + "00"               // probes = 0
+            + "00"               // backoff = 0
+            + "07"               // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
+            + "88"               // wscale = 8
+            + "00"               // delivery_rate_app_limited = 0
+            + "4A911B00"         // rto = 1806666
+            + "00000000"         // ato = 0
+            + "2E050000"         // sndMss = 1326
+            + "18020000"         // rcvMss = 536
+            + "00000000"         // unsacked = 0
+            + "00000000"         // acked = 0
+            + "00000000"         // lost = 0
+            + "00000000"         // retrans = 0
+            + "00000000"         // fackets = 0
+            + "BB000000"         // lastDataSent = 187
+            + "00000000"         // lastAckSent = 0
+            + "BB000000"         // lastDataRecv = 187
+            + "BB000000"         // lastDataAckRecv = 187
+            + "DC050000"         // pmtu = 1500
+            + "30560100"         // rcvSsthresh = 87600
+            + "3E2C0900"         // rttt = 601150
+            + "1F960400"         // rttvar = 300575
+            + "78050000"         // sndSsthresh = 1400
+            + "0A000000"         // sndCwnd = 10
+            + "A8050000"         // advmss = 1448
+            + "03000000"         // reordering = 3
+            + "00000000"         // rcvrtt = 0
+            + "30560100"         // rcvspace = 87600
+            + "00000000"         // totalRetrans = 0
+            + "53AC000000000000"     // pacingRate = 44115
+            + "FFFFFFFFFFFFFFFF"     // maxPacingRate = 18446744073709551615
+            + "0100000000000000"     // bytesAcked = 1
+            + "0000000000000000"     // bytesReceived = 0
+            + "0A000000"         // SegsOut = 10
+            + "00000000"         // SegsIn = 0
+            + "00000000"         // NotSentBytes = 0
+            + "3E2C0900"         // minRtt = 601150
+            + "00000000"         // DataSegsIn = 0
+            + "00000000"         // DataSegsOut = 0
+            + "0000000000000000"; // deliverRate = 0
     private static final String SOCK_DIAG_NO_TCP_INET_HEX =
             // struct nlmsghdr
             "14000000"     // length = 20
@@ -265,7 +261,7 @@
 
         TestKeepaliveTracker(@NonNull final Context context, @NonNull final Handler handler,
                 @NonNull final TcpKeepaliveController tcpController) {
-            super(context, handler, tcpController);
+            super(context, handler, tcpController, mKeepaliveTrackerDeps);
         }
 
         public void setReturnedKeepaliveInfo(@NonNull final KeepaliveInfo ki) {
@@ -327,13 +323,13 @@
                 NetworkInfo.DetailedState.CONNECTED, "test reason", "test extra info");
         doReturn(new Network(TEST_NETID)).when(mNai).network();
         mNai.linkProperties = new LinkProperties();
+        doReturn(null).when(mNai).translateV4toClatV6(any());
+        doReturn(null).when(mNai).getClatv6SrcAddress();
 
         doReturn(PERMISSION_GRANTED).when(mCtx).checkPermission(any() /* permission */,
                 anyInt() /* pid */, anyInt() /* uid */);
         ConnectivityResources.setResourcesContextForTest(mCtx);
         final Resources mockResources = mock(Resources.class);
-        doReturn(new String[] { "0,3", "3,3" }).when(mockResources)
-                .getStringArray(R.array.config_networkSupportedKeepaliveCount);
         doReturn(mockResources).when(mCtx).getResources();
         doReturn(mNetd).when(mDependencies).getNetd();
         doReturn(mAlarmManager).when(mDependencies).getAlarmManager(any());
@@ -341,6 +337,10 @@
                 .getFwmarkForNetwork(TEST_NETID);
 
         doNothing().when(mDependencies).sendRequest(any(), any());
+        doReturn(true).when(mKeepaliveTrackerDeps).isAddressTranslationEnabled(mCtx);
+        doReturn(new ConnectivityResources(mCtx)).when(mKeepaliveTrackerDeps)
+                .createConnectivityResources(mCtx);
+        doReturn(new int[] {3, 0, 0, 3}).when(mKeepaliveTrackerDeps).getSupportedKeepalives(mCtx);
 
         mHandlerThread = new HandlerThread("KeepaliveTrackerTest");
         mHandlerThread.start();
@@ -353,7 +353,7 @@
                 .when(mDependencies)
                 .newKeepaliveStatsTracker(mCtx, mTestHandler);
 
-        doReturn(true).when(mDependencies).isFeatureEnabled(any(), anyBoolean());
+        doReturn(true).when(mDependencies).isTetheringFeatureNotChickenedOut(any());
         doReturn(0L).when(mDependencies).getElapsedRealtime();
         mAOOKeepaliveTracker =
                 new AutomaticOnOffKeepaliveTracker(mCtx, mTestHandler, mDependencies);
@@ -362,6 +362,10 @@
     @After
     public void teardown() throws Exception {
         TestKeepaliveInfo.closeAllSockets();
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
     }
 
     private final class AOOTestHandler extends Handler {
@@ -404,22 +408,22 @@
     @Test
     public void testIsAnyTcpSocketConnected_withTargetNetId() throws Exception {
         setupResponseWithSocketExisting();
-        mTestHandler.post(
-                () -> assertTrue(mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+        assertTrue(visibleOnHandlerThread(mTestHandler,
+                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
     }
 
     @Test
     public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
         setupResponseWithSocketExisting();
-        mTestHandler.post(
-                () -> assertFalse(mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
+        assertFalse(visibleOnHandlerThread(mTestHandler,
+                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
     }
 
     @Test
     public void testIsAnyTcpSocketConnected_noSocketExists() throws Exception {
         setupResponseWithoutSocketExisting();
-        mTestHandler.post(
-                () -> assertFalse(mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+        assertFalse(visibleOnHandlerThread(mTestHandler,
+                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
     }
 
     private void triggerEventKeepalive(int slot, int reason) {
@@ -429,8 +433,7 @@
     }
 
     private TestKeepaliveInfo doStartNattKeepalive(int intervalSeconds) throws Exception {
-        final InetAddress srcAddress = InetAddress.getByAddress(
-                new byte[] { (byte) 192, 0, 0, (byte) 129 });
+        final InetAddress srcAddress = InetAddress.getByAddress(V4_SRC_ADDR);
         final int srcPort = 12345;
         final InetAddress dstAddress = InetAddress.getByAddress(new byte[] {8, 8, 8, 8});
         final int dstPort = 12345;
@@ -497,9 +500,7 @@
         final AlarmManager.OnAlarmListener listener = listenerCaptor.getValue();
 
         // For realism, the listener should be posted on the handler
-        mTestHandler.post(() -> listener.onAlarm());
-        // Wait for the listener to be called. The listener enqueues a message to the handler.
-        HandlerUtils.waitForIdle(mTestHandler, TIMEOUT_MS);
+        visibleOnHandlerThread(mTestHandler, () -> listener.onAlarm());
         // Wait for the message posted by the listener to be processed.
         HandlerUtils.waitForIdle(mTestHandler, TIMEOUT_MS);
 
@@ -522,8 +523,7 @@
 
         doReturn(METRICS_COLLECTION_DURATION_MS).when(mDependencies).getElapsedRealtime();
         // For realism, the listener should be posted on the handler
-        mTestHandler.post(() -> listener.onAlarm());
-        HandlerUtils.waitForIdle(mTestHandler, TIMEOUT_MS);
+        visibleOnHandlerThread(mTestHandler, () -> listener.onAlarm());
 
         verify(mKeepaliveStatsTracker).writeAndResetMetrics();
         // Alarm is rescheduled.
@@ -609,6 +609,104 @@
         verifyNoMoreInteractions(ignoreStubs(testInfo.socketKeepaliveCallback));
     }
 
+    private void setupTestNaiForClat(InetAddress v6Src, InetAddress v6Dst) throws Exception {
+        doReturn(v6Dst).when(mNai).translateV4toClatV6(any());
+        doReturn(v6Src).when(mNai).getClatv6SrcAddress();
+        doReturn(InetAddress.getByAddress(V4_SRC_ADDR)).when(mNai).getClatv4SrcAddress();
+        // Setup nai to add clat address
+        final LinkProperties stacked = new LinkProperties();
+        stacked.setInterfaceName(TEST_V4_IFACE);
+        final InetAddress srcAddress = InetAddress.getByAddress(
+                new byte[] { (byte) 192, 0, 0, (byte) 129 });
+        mNai.linkProperties.addLinkAddress(new LinkAddress(srcAddress, 24));
+        mNai.linkProperties.addStackedLink(stacked);
+    }
+
+    private TestKeepaliveInfo doStartTcpKeepalive(InetAddress srcAddr) throws Exception {
+        final KeepalivePacketData kpd = new TcpKeepalivePacketData(
+                srcAddr,
+                12345 /* srcPort */,
+                InetAddress.getByAddress(new byte[] { 8, 8, 8, 8}) /* dstAddr */,
+                12345 /* dstPort */, new byte[] {1},  111 /* tcpSeq */,
+                222 /* tcpAck */, 800 /* tcpWindow */, 2 /* tcpWindowScale */,
+                4 /* ipTos */, 64 /* ipTtl */);
+        final TestKeepaliveInfo testInfo = new TestKeepaliveInfo(kpd);
+
+        final KeepaliveInfo ki = mKeepaliveTracker.new KeepaliveInfo(
+                testInfo.socketKeepaliveCallback, mNai, kpd,
+                TEST_KEEPALIVE_INTERVAL_SEC, KeepaliveInfo.TYPE_TCP, testInfo.fd);
+        mKeepaliveTracker.setReturnedKeepaliveInfo(ki);
+
+        // Setup TCP keepalive.
+        mAOOKeepaliveTracker.startTcpKeepalive(mNai, testInfo.fd, TEST_KEEPALIVE_INTERVAL_SEC,
+                testInfo.socketKeepaliveCallback);
+        HandlerUtils.waitForIdle(mTestHandler, TIMEOUT_MS);
+        return testInfo;
+    }
+    @Test
+    public void testStartTcpKeepalive_addressTranslationOnClat() throws Exception {
+        setupTestNaiForClat(InetAddresses.parseNumericAddress("2001:db8::1") /* v6Src */,
+                InetAddresses.parseNumericAddress("2001:db8::2") /* v6Dst */);
+        final InetAddress srcAddr = InetAddress.getByAddress(V4_SRC_ADDR);
+        doStartTcpKeepalive(srcAddr);
+        final ArgumentCaptor<TcpKeepalivePacketData> tpdCaptor =
+                ArgumentCaptor.forClass(TcpKeepalivePacketData.class);
+        verify(mNai).onStartTcpSocketKeepalive(
+                eq(TEST_SLOT), eq(TEST_KEEPALIVE_INTERVAL_SEC), tpdCaptor.capture());
+        final TcpKeepalivePacketData tpd = tpdCaptor.getValue();
+        // Verify the addresses still be the same address when clat is started.
+        assertEquals(srcAddr, tpd.getSrcAddress());
+    }
+
+    @Test
+    public void testStartNattKeepalive_addressTranslationOnClatNotSupported() throws Exception {
+        // Disable address translation feature and verify the behavior
+        doReturn(false).when(mKeepaliveTrackerDeps).isAddressTranslationEnabled(mCtx);
+
+        setupTestNaiForClat(InetAddresses.parseNumericAddress("2001:db8::1"),
+                InetAddresses.parseNumericAddress("2001:db8::2"));
+
+        doStartNattKeepalive();
+        final ArgumentCaptor<NattKeepalivePacketData> kpdCaptor =
+                ArgumentCaptor.forClass(NattKeepalivePacketData.class);
+        verify(mNai).onStartNattSocketKeepalive(
+                eq(TEST_SLOT), eq(TEST_KEEPALIVE_INTERVAL_SEC), kpdCaptor.capture());
+        // Verify that address translation is not triggered so the addresses are still v4.
+        final NattKeepalivePacketData kpd = kpdCaptor.getValue();
+        assertTrue(kpd.getSrcAddress() instanceof Inet4Address);
+        assertTrue(kpd.getDstAddress() instanceof Inet4Address);
+    }
+
+    @Test
+    public void testStartNattKeepalive_addressTranslationOnClat() throws Exception {
+        final InetAddress v6AddrSrc = InetAddresses.parseNumericAddress("2001:db8::1");
+        final InetAddress v6AddrDst = InetAddresses.parseNumericAddress("2001:db8::2");
+        setupTestNaiForClat(v6AddrSrc, v6AddrDst);
+
+        final TestKeepaliveInfo testInfo = doStartNattKeepalive();
+        final ArgumentCaptor<NattKeepalivePacketData> kpdCaptor =
+                ArgumentCaptor.forClass(NattKeepalivePacketData.class);
+        verify(mNai).onStartNattSocketKeepalive(
+                eq(TEST_SLOT), eq(TEST_KEEPALIVE_INTERVAL_SEC), kpdCaptor.capture());
+        final NattKeepalivePacketData kpd = kpdCaptor.getValue();
+        // Verify the addresses are updated to v6 when clat is started.
+        assertEquals(v6AddrSrc, kpd.getSrcAddress());
+        assertEquals(v6AddrDst, kpd.getDstAddress());
+
+        triggerEventKeepalive(TEST_SLOT, SocketKeepalive.SUCCESS);
+        verify(testInfo.socketKeepaliveCallback).onStarted();
+
+        // Remove clat address should stop the keepalive.
+        doReturn(null).when(mNai).getClatv6SrcAddress();
+        visibleOnHandlerThread(
+                mTestHandler, () -> mAOOKeepaliveTracker.handleCheckKeepalivesStillValid(mNai));
+        checkAndProcessKeepaliveStop();
+        assertNull(getAutoKiForBinder(testInfo.binder));
+
+        verify(testInfo.socketKeepaliveCallback).onError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
+        verifyNoMoreInteractions(ignoreStubs(testInfo.socketKeepaliveCallback));
+    }
+
     @Test
     public void testHandleEventSocketKeepalive_startingFailureHardwareError() throws Exception {
         final TestKeepaliveInfo testInfo = doStartNattKeepalive();
@@ -860,24 +958,8 @@
                 new byte[] { (byte) 192, 0, 0, (byte) 129 });
         mNai.linkProperties.addLinkAddress(new LinkAddress(srcAddress, 24));
 
-        final KeepalivePacketData kpd = new TcpKeepalivePacketData(
-                InetAddress.getByAddress(new byte[] { (byte) 192, 0, 0, (byte) 129 }) /* srcAddr */,
-                12345 /* srcPort */,
-                InetAddress.getByAddress(new byte[] { 8, 8, 8, 8}) /* dstAddr */,
-                12345 /* dstPort */, new byte[] {1},  111 /* tcpSeq */,
-                222 /* tcpAck */, 800 /* tcpWindow */, 2 /* tcpWindowScale */,
-                4 /* ipTos */, 64 /* ipTtl */);
-        final TestKeepaliveInfo testInfo = new TestKeepaliveInfo(kpd);
-
-        final KeepaliveInfo ki = mKeepaliveTracker.new KeepaliveInfo(
-                testInfo.socketKeepaliveCallback, mNai, kpd,
-                TEST_KEEPALIVE_INTERVAL_SEC, KeepaliveInfo.TYPE_TCP, testInfo.fd);
-        mKeepaliveTracker.setReturnedKeepaliveInfo(ki);
-
-        // Setup TCP keepalive.
-        mAOOKeepaliveTracker.startTcpKeepalive(mNai, testInfo.fd, TEST_KEEPALIVE_INTERVAL_SEC,
-                testInfo.socketKeepaliveCallback);
-        HandlerUtils.waitForIdle(mTestHandler, TIMEOUT_MS);
+        final TestKeepaliveInfo testInfo =
+                doStartTcpKeepalive(InetAddress.getByAddress(V4_SRC_ADDR));
 
         // A closed socket will result in EVENT_HANGUP and trigger error to
         // FileDescriptorEventListener.
@@ -885,6 +967,21 @@
         HandlerUtils.waitForIdle(mTestHandler, TIMEOUT_MS);
 
         // The keepalive should be removed in AutomaticOnOffKeepaliveTracker.
-        getAutoKiForBinder(testInfo.binder);
+        assertNull(getAutoKiForBinder(testInfo.binder));
+    }
+
+    @Test
+    public void testDumpDoesNotCrash() throws Exception {
+        final TestKeepaliveInfo testInfo1 = doStartNattKeepalive();
+        final TestKeepaliveInfo testInfo2 = doStartNattKeepalive();
+        checkAndProcessKeepaliveStart(TEST_SLOT, testInfo1.kpd);
+        checkAndProcessKeepaliveStart(TEST_SLOT + 1, testInfo2.kpd);
+        final AutomaticOnOffKeepalive autoKi1  = getAutoKiForBinder(testInfo1.binder);
+        doPauseKeepalive(autoKi1);
+
+        final StringWriter stringWriter = new StringWriter();
+        final IndentingPrintWriter pw = new IndentingPrintWriter(stringWriter, "   ");
+        visibleOnHandlerThread(mTestHandler, () -> mAOOKeepaliveTracker.dump(pw));
+        assertFalse(stringWriter.toString().isEmpty());
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index 3849e49..ab81abc 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -20,12 +20,16 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 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;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
@@ -34,31 +38,41 @@
 import static org.mockito.Mockito.verify;
 
 import android.annotation.NonNull;
+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.net.NetworkCapabilities;
 import android.net.TelephonyNetworkSpecifier;
 import android.os.Build;
-import android.telephony.SubscriptionManager;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.telephony.TelephonyManager;
 
 import com.android.net.module.util.CollectionUtils;
 import com.android.networkstack.apishim.TelephonyManagerShimImpl;
 import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.server.connectivity.CarrierPrivilegeAuthenticator.Dependencies;
+import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
 
-import org.junit.Before;
+import org.junit.After;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 import org.mockito.ArgumentCaptor;
 
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
 
 /**
  * Tests for CarrierPrivilegeAuthenticatorTest.
@@ -69,38 +83,67 @@
 @RunWith(DevSdkIgnoreRunner.class)
 @IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class CarrierPrivilegeAuthenticatorTest {
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
     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;
     @NonNull private final TelephonyManagerShimImpl mTelephonyManagerShim;
     @NonNull private final PackageManager mPackageManager;
     @NonNull private TestCarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
+    @NonNull private final BiConsumer<Integer, Integer> mListener;
     private final int mCarrierConfigPkgUid = 12345;
+    private final boolean mUseCallbacks;
     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 TelephonyManager t) {
-            super(c, t, mTelephonyManagerShim);
+                @NonNull final Dependencies deps,
+                @NonNull final TelephonyManager t,
+                @NonNull final Handler handler) {
+            super(c, deps, t, mTelephonyManagerShim, true /* requestRestrictedWifiEnabled */,
+                    mListener, handler);
         }
         @Override
-        protected int getSlotIndex(int subId) {
-            if (SubscriptionManager.DEFAULT_SUBSCRIPTION_ID == subId) return TEST_SUBSCRIPTION_ID;
-            return subId;
+        protected int getSubId(int slotIndex) {
+            return TEST_SUBSCRIPTION_ID;
         }
     }
 
-    public CarrierPrivilegeAuthenticatorTest() {
+    @After
+    public void tearDown() throws Exception {
+        mHandlerThread.quit();
+        mHandlerThread.join();
+        mCsHandlerThread.quit();
+        mCsHandlerThread.join();
+    }
+
+    /** Parameters to test both using callbacks or the old broadcast */
+    @Parameterized.Parameters
+    public static Collection<Boolean> shouldUseCallbacks() {
+        return Arrays.asList(true, false);
+    }
+
+    public CarrierPrivilegeAuthenticatorTest(final boolean useCallbacks) throws Exception {
         mContext = mock(Context.class);
         mTelephonyManager = mock(TelephonyManager.class);
         mTelephonyManagerShim = mock(TelephonyManagerShimImpl.class);
         mPackageManager = mock(PackageManager.class);
-    }
-
-    @Before
-    public void setUp() throws Exception {
+        mListener = mock(BiConsumer.class);
+        mHandlerThread = new HandlerThread(CarrierPrivilegeAuthenticatorTest.class.getSimpleName());
+        mUseCallbacks = useCallbacks;
+        final Dependencies deps = mock(Dependencies.class);
+        doReturn(useCallbacks).when(deps).isFeatureEnabled(any() /* context */,
+                eq(CARRIER_SERVICE_CHANGED_USE_CALLBACK));
+        doReturn(mHandlerThread).when(deps).makeHandlerThread();
         doReturn(SUBSCRIPTION_COUNT).when(mTelephonyManager).getActiveModemCount();
         doReturn(mTestPkg).when(mTelephonyManagerShim)
                 .getCarrierServicePackageNameForLogicalSlot(anyInt());
@@ -108,14 +151,20 @@
         final ApplicationInfo applicationInfo = new ApplicationInfo();
         applicationInfo.uid = mCarrierConfigPkgUid;
         doReturn(applicationInfo).when(mPackageManager).getApplicationInfo(eq(mTestPkg), anyInt());
-        mCarrierPrivilegeAuthenticator =
-                new TestCarrierPrivilegeAuthenticator(mContext, mTelephonyManager);
-    }
-
-    private IntentFilter getIntentFilter() {
-        final ArgumentCaptor<IntentFilter> captor = ArgumentCaptor.forClass(IntentFilter.class);
-        verify(mContext).registerReceiver(any(), captor.capture(), any(), any());
-        return captor.getValue();
+        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 ->
+                filter.getAction(0).equals(ACTION_MULTI_SIM_CONFIG_CHANGED)
+        ), any() /* broadcast permissions */, any() /* handler */);
+        mMultiSimBroadcastReceiver = receiverCaptor.getValue();
     }
 
     private Map<Integer, CarrierPrivilegesListenerShim> getCarrierPrivilegesListeners() {
@@ -138,15 +187,6 @@
     }
     @Test
     public void testConstructor() throws Exception {
-        verify(mContext).registerReceiver(
-                        eq(mCarrierPrivilegeAuthenticator),
-                        any(IntentFilter.class),
-                        any(),
-                        any());
-        final IntentFilter filter = getIntentFilter();
-        assertEquals(1, filter.countActions());
-        assertTrue(filter.hasAction(ACTION_MULTI_SIM_CONFIG_CHANGED));
-
         // Two listeners originally registered, one for slot 0 and one for slot 1
         final Map<Integer, CarrierPrivilegesListenerShim> initialListeners =
                 getCarrierPrivilegesListeners();
@@ -154,13 +194,17 @@
         assertNotNull(initialListeners.get(1));
         assertEquals(2, initialListeners.size());
 
+        visibleOnHandlerThread(mCsHandler, () -> {
+            initialListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        });
+
         final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
-                .setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+                .setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
 
-        assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
-        assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid + 1, ncBuilder.build()));
     }
 
@@ -174,8 +218,11 @@
         assertEquals(2, initialListeners.size());
 
         doReturn(1).when(mTelephonyManager).getActiveModemCount();
-        mCarrierPrivilegeAuthenticator.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));
@@ -187,22 +234,49 @@
         assertNotNull(newListeners.get(0));
         assertEquals(1, newListeners.size());
 
-        final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(0);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            newListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        });
+
+        final TelephonyNetworkSpecifier specifier =
+                new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID);
         final NetworkCapabilities nc = new NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
                 .setNetworkSpecifier(specifier)
                 .build();
-        assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, nc));
-        assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid + 1, nc));
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testCarrierPrivilegesLostDueToCarrierServiceUpdate() throws Exception {
+        final CarrierPrivilegesListenerShim l = getCarrierPrivilegesListeners().get(0);
+
+        visibleOnHandlerThread(mCsHandler, () -> {
+            l.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+            l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 1);
+        });
+        if (mUseCallbacks) {
+            verify(mListener).accept(eq(mCarrierConfigPkgUid), eq(TEST_SUBSCRIPTION_ID));
+        }
+
+        visibleOnHandlerThread(mCsHandler, () -> {
+            l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 2);
+        });
+        if (mUseCallbacks) {
+            verify(mListener).accept(eq(mCarrierConfigPkgUid + 1), eq(TEST_SUBSCRIPTION_ID));
+        }
+    }
+
+    @Test
     public void testOnCarrierPrivilegesChanged() throws Exception {
         final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
 
-        final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(0);
+        final TelephonyNetworkSpecifier specifier =
+                new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID);
         final NetworkCapabilities nc = new NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
                 .setNetworkSpecifier(specifier)
@@ -211,23 +285,31 @@
         final ApplicationInfo applicationInfo = new ApplicationInfo();
         applicationInfo.uid = mCarrierConfigPkgUid + 1;
         doReturn(applicationInfo).when(mPackageManager).getApplicationInfo(eq(mTestPkg), anyInt());
-        listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[] {});
+        visibleOnHandlerThread(mCsHandler, () -> {
+            listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[]{});
+            listener.onCarrierServiceChanged(null, applicationInfo.uid);
+        });
 
-        assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, nc));
-        assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid + 1, nc));
     }
 
     @Test
     public void testDefaultSubscription() throws Exception {
+        final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        });
+
         final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
         ncBuilder.addTransportType(TRANSPORT_CELLULAR);
-        assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
 
-        ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
-        assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
+        assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
 
         // The builder for NetworkCapabilities doesn't allow removing the transport as long as a
@@ -235,8 +317,40 @@
         ncBuilder.setNetworkSpecifier(null);
         ncBuilder.removeTransportType(TRANSPORT_CELLULAR);
         ncBuilder.addTransportType(TRANSPORT_WIFI);
-        ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
-        assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+        ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
+                mCarrierConfigPkgUid, ncBuilder.build()));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testNetworkCapabilitiesContainOneSubId() throws Exception {
+        final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        });
+
+        final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
+        ncBuilder.addTransportType(TRANSPORT_WIFI);
+        ncBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        ncBuilder.setSubscriptionIds(Set.of(TEST_SUBSCRIPTION_ID));
+        assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
+                mCarrierConfigPkgUid, ncBuilder.build()));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testNetworkCapabilitiesContainTwoSubIds() throws Exception {
+        final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        });
+
+        final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
+        ncBuilder.addTransportType(TRANSPORT_WIFI);
+        ncBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        ncBuilder.setSubscriptionIds(Set.of(0, 1));
+        assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, ncBuilder.build()));
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index b651c33..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;
@@ -313,8 +314,7 @@
          * Stop clatd.
          */
         @Override
-        public void stopClatd(@NonNull String iface, @NonNull String pfx96, @NonNull String v4,
-                @NonNull String v6, int pid) throws IOException {
+        public void stopClatd(int pid) throws IOException {
             if (pid == -1) {
                 fail("unsupported arg: " + pid);
             }
@@ -419,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));
@@ -450,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)),
@@ -479,8 +479,7 @@
                 eq((short) PRIO_CLAT), eq((short) ETH_P_IP));
         inOrder.verify(mEgressMap).deleteEntry(eq(EGRESS_KEY));
         inOrder.verify(mIngressMap).deleteEntry(eq(INGRESS_KEY));
-        inOrder.verify(mDeps).stopClatd(eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
-                eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(CLATD_PID));
+        inOrder.verify(mDeps).stopClatd(eq(CLATD_PID));
         inOrder.verify(mCookieTagMap).deleteEntry(eq(COOKIE_TAG_KEY));
         assertNull(coordinator.getClatdTrackerForTesting());
         inOrder.verifyNoMoreInteractions();
@@ -510,10 +509,10 @@
         // Expected mtu is that the detected mtu minus MTU_DELTA(28).
         assertEquals(1372, ClatCoordinator.adjustMtu(1400));
         assertEquals(1472, ClatCoordinator.adjustMtu(ETHER_MTU));
-        assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU));
+        assertEquals(1500, ClatCoordinator.adjustMtu(CLAT_MAX_MTU));
 
-        // Expected mtu is that CLAT_MAX_MTU(65536) minus MTU_DELTA(28).
-        assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
+        // Expected mtu is that CLAT_MAX_MTU(1528) minus MTU_DELTA(28).
+        assertEquals(1500, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
     }
 
     private void verifyDump(final ClatCoordinator coordinator, boolean clatStarted) {
@@ -528,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);
@@ -632,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
@@ -645,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
@@ -658,7 +666,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
     }
 
@@ -670,7 +678,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 true /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
     }
 
@@ -683,7 +691,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
@@ -696,7 +704,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
@@ -723,7 +731,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
@@ -735,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 24aecdb..ea3d2dd 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -19,6 +19,7 @@
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_DEFAULT_MODE;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE;
 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.ConnectivitySettingsManager.PRIVATE_DNS_SPECIFIER;
 import static android.net.NetworkCapabilities.MAX_TRANSPORT;
@@ -28,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;
@@ -37,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;
@@ -73,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;
 
@@ -121,27 +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);
-        assertFieldCountEquals(16, ResolverParamsParcel.class);
-    }
-
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -169,10 +146,12 @@
         lp.addDnsServer(InetAddress.getByName("4.4.4.4"));
 
         // Send a validation event that is tracked on the alternate netId
-        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+        mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
         mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
         mDnsManager.flushVmDnsCache();
-        mDnsManager.updateTransportsForNetwork(TEST_NETID_ALTERNATE, TEST_TRANSPORT_TYPES);
+        mDnsManager.updateCapabilitiesForNetwork(TEST_NETID_ALTERNATE, nc);
         mDnsManager.noteDnsServersForNetwork(TEST_NETID_ALTERNATE, lp);
         mDnsManager.flushVmDnsCache();
         mDnsManager.updatePrivateDnsValidation(
@@ -205,7 +184,7 @@
                     InetAddress.parseNumericAddress("6.6.6.6"),
                     InetAddress.parseNumericAddress("2001:db8:66:66::1")
                     }));
-        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
         mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
         mDnsManager.flushVmDnsCache();
         fixedLp = new LinkProperties(lp);
@@ -242,7 +221,9 @@
         // be tracked.
         LinkProperties lp = new LinkProperties();
         lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
-        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+        mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
         mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
         mDnsManager.flushVmDnsCache();
         mDnsManager.updatePrivateDnsValidation(
@@ -256,7 +237,7 @@
         // Validation event has untracked netId
         mDnsManager.updatePrivateDns(new Network(TEST_NETID),
                 mDnsManager.getPrivateDnsConfig());
-        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
         mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
         mDnsManager.flushVmDnsCache();
         mDnsManager.updatePrivateDnsValidation(
@@ -307,7 +288,7 @@
         ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_OFF);
         mDnsManager.updatePrivateDns(new Network(TEST_NETID),
                 mDnsManager.getPrivateDnsConfig());
-        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
         mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
         mDnsManager.flushVmDnsCache();
         mDnsManager.updatePrivateDnsValidation(
@@ -323,14 +304,14 @@
     public void testOverrideDefaultMode() throws Exception {
         // Hard-coded default is opportunistic mode.
         final PrivateDnsConfig cfgAuto = DnsManager.getPrivateDnsConfig(mCtx);
-        assertTrue(cfgAuto.useTls);
+        assertEquals(PRIVATE_DNS_MODE_OPPORTUNISTIC, cfgAuto.mode);
         assertEquals("", cfgAuto.hostname);
         assertEquals(new InetAddress[0], cfgAuto.ips);
 
         // Pretend a gservices push sets the default to "off".
         ConnectivitySettingsManager.setPrivateDnsDefaultMode(mCtx, PRIVATE_DNS_MODE_OFF);
         final PrivateDnsConfig cfgOff = DnsManager.getPrivateDnsConfig(mCtx);
-        assertFalse(cfgOff.useTls);
+        assertEquals(PRIVATE_DNS_MODE_OFF, cfgOff.mode);
         assertEquals("", cfgOff.hostname);
         assertEquals(new InetAddress[0], cfgOff.ips);
 
@@ -338,7 +319,7 @@
         ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
         ConnectivitySettingsManager.setPrivateDnsHostname(mCtx, "strictmode.com");
         final PrivateDnsConfig cfgStrict = DnsManager.getPrivateDnsConfig(mCtx);
-        assertTrue(cfgStrict.useTls);
+        assertEquals(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, cfgStrict.mode);
         assertEquals("strictmode.com", cfgStrict.hostname);
         assertEquals(new InetAddress[0], cfgStrict.ips);
     }
@@ -352,15 +333,12 @@
         lp.setInterfaceName(TEST_IFACENAME);
         lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
         lp.addDnsServer(InetAddress.getByName("4.4.4.4"));
-        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+        mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
         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;
@@ -373,7 +351,10 @@
         expectedParams.tlsServers = new String[]{"3.3.3.3", "4.4.4.4"};
         expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
         expectedParams.resolverOptions = null;
-        assertResolverParamsEquals(actualParams, expectedParams);
+        expectedParams.meteredNetwork = true;
+        expectedParams.dohParams = null;
+        expectedParams.interfaceNames = new String[]{TEST_IFACENAME};
+        verify(mMockDnsResolver, times(1)).setResolverConfiguration(eq(expectedParams));
     }
 
     @Test
@@ -409,7 +390,7 @@
 
         // The PrivateDnsConfig map is empty, so the default PRIVATE_DNS_OFF is returned.
         PrivateDnsConfig privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
-        assertFalse(privateDnsCfg.useTls);
+        assertEquals(PRIVATE_DNS_MODE_OFF, privateDnsCfg.mode);
         assertEquals("", privateDnsCfg.hostname);
         assertEquals(new InetAddress[0], privateDnsCfg.ips);
 
@@ -421,7 +402,7 @@
                         VALIDATION_RESULT_SUCCESS));
         mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
         privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
-        assertTrue(privateDnsCfg.useTls);
+        assertEquals(PRIVATE_DNS_MODE_OPPORTUNISTIC, privateDnsCfg.mode);
         assertEquals("", privateDnsCfg.hostname);
         assertEquals(new InetAddress[0], privateDnsCfg.ips);
 
@@ -429,14 +410,14 @@
         mDnsManager.updatePrivateDns(network, new PrivateDnsConfig(tlsName, tlsAddrs));
         mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
         privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
-        assertTrue(privateDnsCfg.useTls);
+        assertEquals(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, privateDnsCfg.mode);
         assertEquals(tlsName, privateDnsCfg.hostname);
         assertEquals(tlsAddrs, privateDnsCfg.ips);
 
         // The network is removed, so the PrivateDnsConfig map becomes empty again.
         mDnsManager.removeNetwork(network);
         privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
-        assertFalse(privateDnsCfg.useTls);
+        assertEquals(PRIVATE_DNS_MODE_OFF, privateDnsCfg.mode);
         assertEquals("", privateDnsCfg.hostname);
         assertEquals(new InetAddress[0], privateDnsCfg.ips);
     }
diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
index 3520c5b..0a3822a 100644
--- a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
@@ -74,7 +74,7 @@
 
     private val TAG = this::class.simpleName
 
-    private var wtfHandler: Log.TerribleFailureHandler? = null
+    private lateinit var wtfHandler: Log.TerribleFailureHandler
 
     @Before
     fun setUp() {
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
index 52b05aa..ab1e467 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -26,7 +26,6 @@
 import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
 import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.MULTIPLE;
 import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.WIFI;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
@@ -43,17 +42,14 @@
 import android.net.metrics.ValidationProbeEvent;
 import android.net.metrics.WakeupStats;
 import android.os.Build;
-import android.test.suitebuilder.annotation.SmallTest;
-
+import androidx.test.filters.SmallTest;
 import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
 import java.util.Arrays;
 import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 // TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto.
 @RunWith(DevSdkIgnoreRunner.class)
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
index 5881a8e..91626d2 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -18,7 +18,6 @@
 
 import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO;
 import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.fail;
@@ -50,14 +49,14 @@
 import android.os.Parcelable;
 import android.os.SystemClock;
 import android.system.OsConstants;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Base64;
-
+import androidx.test.filters.SmallTest;
 import com.android.internal.util.BitUtils;
 import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
-
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -65,9 +64,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
index 0d2e540..294dacb 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -19,18 +19,25 @@
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import static com.android.testutils.HandlerUtils.visibleOnHandlerThread;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.content.BroadcastReceiver;
@@ -61,7 +68,9 @@
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -103,6 +112,8 @@
                 .build();
     }
 
+    @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
     private HandlerThread mHandlerThread;
     private Handler mTestHandler;
 
@@ -112,16 +123,25 @@
     @Mock private KeepaliveStatsTracker.Dependencies mDependencies;
     @Mock private SubscriptionManager mSubscriptionManager;
 
-    private void triggerBroadcastDefaultSubId(int subId) {
+    private BroadcastReceiver getBroadcastReceiver() {
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
-        verify(mContext).registerReceiver(receiverCaptor.capture(), /* filter= */ any(),
-                /* broadcastPermission= */ any(), eq(mTestHandler));
+        verify(mContext).registerReceiver(
+                receiverCaptor.capture(),
+                argThat(intentFilter -> intentFilter.matchAction(
+                        SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED)),
+                /* broadcastPermission= */ any(),
+                eq(mTestHandler));
+
+        return receiverCaptor.getValue();
+    }
+
+    private void triggerBroadcastDefaultSubId(int subId) {
         final Intent intent =
-                new Intent(TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED);
+                new Intent(SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED);
         intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId);
 
-        receiverCaptor.getValue().onReceive(mContext, intent);
+        getBroadcastReceiver().onReceive(mContext, intent);
     }
 
     private OnSubscriptionsChangedListener getOnSubscriptionsChangedListener() {
@@ -222,6 +242,14 @@
         HandlerUtils.waitForIdle(mTestHandler, TIMEOUT_MS);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+    }
+
     private void setElapsedRealtime(long time) {
         doReturn(time).when(mDependencies).getElapsedRealtime();
     }
@@ -427,6 +455,24 @@
         assertCarrierLifetimeMetrics(expectKeepaliveCarrierStatsArray, actualCarrierLifetime);
     }
 
+    // The KeepaliveStatsTracker will be disabled when an error occurs with the keepalive states.
+    // Most tests should assert that the tracker is still active to ensure no errors occurred.
+    private void assertKeepaliveStatsTrackerActive() {
+        assertTrue(mKeepaliveStatsTracker.isEnabled());
+    }
+
+    private void assertKeepaliveStatsTrackerDisabled() {
+        assertFalse(mKeepaliveStatsTracker.isEnabled());
+
+        final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener();
+        // BackgroundThread will remove the OnSubscriptionsChangedListener.
+        HandlerUtils.waitForIdle(BackgroundThread.getHandler(), TIMEOUT_MS);
+        verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(listener);
+
+        final BroadcastReceiver receiver = getBroadcastReceiver();
+        verify(mContext).unregisterReceiver(receiver);
+    }
+
     @Test
     public void testNoKeepalive() {
         final int writeTime = 5000;
@@ -446,6 +492,7 @@
                 expectRegisteredDurations,
                 expectActiveDurations,
                 new KeepaliveCarrierStats[0]);
+        assertKeepaliveStatsTrackerActive();
     }
 
     /*
@@ -479,6 +526,7 @@
                 new KeepaliveCarrierStats[] {
                     getDefaultCarrierStats(expectRegisteredDurations[1], expectActiveDurations[1])
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     /*
@@ -517,6 +565,7 @@
                 new KeepaliveCarrierStats[] {
                     getDefaultCarrierStats(expectRegisteredDurations[1], expectActiveDurations[1])
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     /*
@@ -561,6 +610,7 @@
                 new KeepaliveCarrierStats[] {
                     getDefaultCarrierStats(expectRegisteredDurations[1], expectActiveDurations[1])
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     /*
@@ -609,6 +659,7 @@
                 new KeepaliveCarrierStats[] {
                     getDefaultCarrierStats(expectRegisteredDurations[1], expectActiveDurations[1])
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     /*
@@ -651,6 +702,7 @@
                 new KeepaliveCarrierStats[] {
                     getDefaultCarrierStats(expectRegisteredDurations[1], expectActiveDurations[1])
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     /*
@@ -702,6 +754,7 @@
                 new KeepaliveCarrierStats[] {
                     getDefaultCarrierStats(expectRegisteredDurations[1], expectActiveDurations[1])
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     /*
@@ -782,6 +835,7 @@
                             expectRegisteredDurations[1] + 2 * expectRegisteredDurations[2],
                             expectActiveDurations[1] + 2 * expectActiveDurations[2])
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     /*
@@ -851,6 +905,7 @@
                 new KeepaliveCarrierStats[] {
                     getDefaultCarrierStats(expectRegisteredDurations2[1], expectActiveDurations2[1])
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     /*
@@ -940,6 +995,7 @@
                 expectRegisteredDurations2,
                 expectActiveDurations2,
                 new KeepaliveCarrierStats[] {expectKeepaliveCarrierStats3});
+        assertKeepaliveStatsTrackerActive();
     }
 
     @Test
@@ -951,7 +1007,10 @@
         onStartKeepalive(startTime1, TEST_SLOT);
 
         // Attempt to use the same (network, slot)
-        assertThrows(IllegalArgumentException.class, () -> onStartKeepalive(startTime2, TEST_SLOT));
+        onStartKeepalive(startTime2, TEST_SLOT);
+        // Starting a 2nd keepalive on the same slot is unexpected and an error so the stats tracker
+        // is disabled.
+        assertKeepaliveStatsTrackerDisabled();
 
         final DailykeepaliveInfoReported dailyKeepaliveInfoReported =
                 buildKeepaliveMetrics(writeTime);
@@ -1012,6 +1071,7 @@
                 new KeepaliveCarrierStats[] {
                     getDefaultCarrierStats(expectRegisteredDurations[1], expectActiveDurations[1])
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     @Test
@@ -1059,6 +1119,7 @@
                 new KeepaliveCarrierStats[] {
                     expectKeepaliveCarrierStats1, expectKeepaliveCarrierStats2
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     @Test
@@ -1100,6 +1161,7 @@
                 /* expectRegisteredDurations= */ new int[] {startTime, writeTime - startTime},
                 /* expectActiveDurations= */ new int[] {startTime, writeTime - startTime},
                 new KeepaliveCarrierStats[] {expectKeepaliveCarrierStats});
+        assertKeepaliveStatsTrackerActive();
     }
 
     @Test
@@ -1142,6 +1204,7 @@
                             writeTime * 3 - startTime1 - startTime2 - startTime3,
                             writeTime * 3 - startTime1 - startTime2 - startTime3)
                 });
+        assertKeepaliveStatsTrackerActive();
     }
 
     @Test
@@ -1191,5 +1254,58 @@
                 new KeepaliveCarrierStats[] {
                     expectKeepaliveCarrierStats1, expectKeepaliveCarrierStats2
                 });
+        assertKeepaliveStatsTrackerActive();
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testWriteMetrics_doNothingBeforeT() {
+        // Keepalive stats use repeated atoms, which are only supported on T+. If written to statsd
+        // on S- they will bootloop the system, so they must not be sent on S-. See b/289471411.
+        final int writeTime = 1000;
+        setElapsedRealtime(writeTime);
+        visibleOnHandlerThread(mTestHandler, () -> mKeepaliveStatsTracker.writeAndResetMetrics());
+        verify(mDependencies, never()).writeStats(any());
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testWriteMetrics() {
+        final int writeTime = 1000;
+
+        final ArgumentCaptor<DailykeepaliveInfoReported> dailyKeepaliveInfoReportedCaptor =
+                ArgumentCaptor.forClass(DailykeepaliveInfoReported.class);
+
+        setElapsedRealtime(writeTime);
+        visibleOnHandlerThread(mTestHandler, () -> mKeepaliveStatsTracker.writeAndResetMetrics());
+        // Ensure writeStats is called with the correct DailykeepaliveInfoReported metrics.
+        verify(mDependencies).writeStats(dailyKeepaliveInfoReportedCaptor.capture());
+        final DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                dailyKeepaliveInfoReportedCaptor.getValue();
+
+        // Same as the no keepalive case
+        final int[] expectRegisteredDurations = new int[] {writeTime};
+        final int[] expectActiveDurations = new int[] {writeTime};
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                /* expectRequestsCount= */ 0,
+                /* expectAutoRequestsCount= */ 0,
+                /* expectAppUids= */ new int[0],
+                expectRegisteredDurations,
+                expectActiveDurations,
+                new KeepaliveCarrierStats[0]);
+
+        assertTrue(mKeepaliveStatsTracker.allMetricsExpected(dailyKeepaliveInfoReported));
+
+        // Write time after 27 hours.
+        final int writeTime2 = 27 * 60 * 60 * 1000;
+        setElapsedRealtime(writeTime2);
+
+        visibleOnHandlerThread(mTestHandler, () -> mKeepaliveStatsTracker.writeAndResetMetrics());
+        verify(mDependencies, times(2)).writeStats(dailyKeepaliveInfoReportedCaptor.capture());
+        final DailykeepaliveInfoReported dailyKeepaliveInfoReported2 =
+                dailyKeepaliveInfoReportedCaptor.getValue();
+
+        assertFalse(mKeepaliveStatsTracker.allMetricsExpected(dailyKeepaliveInfoReported2));
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
index e6c0c83..07883ff 100644
--- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -372,9 +372,10 @@
         caps.addCapability(0);
         caps.addTransportType(transport);
         NetworkAgentInfo nai = new NetworkAgentInfo(null, new Network(netId), info,
-                new LinkProperties(), caps, new NetworkScore.Builder().setLegacyInt(50).build(),
-                mCtx, null, new NetworkAgentConfig.Builder().build(), mConnService, mNetd,
-                mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS,
+                new LinkProperties(), caps, null /* localNetworkConfiguration */,
+                new NetworkScore.Builder().setLegacyInt(50).build(), mCtx, null,
+                new NetworkAgentConfig.Builder().build(), mConnService, mNetd, mDnsResolver,
+                NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS,
                 mQosCallbackTracker, new ConnectivityService.Dependencies());
         if (setEverValidated) {
             // As tests in this class deal with testing lingering, most tests are interested
diff --git a/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt
new file mode 100644
index 0000000..5c994f5
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt
@@ -0,0 +1,502 @@
+/*
+ * 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.connectivity
+
+import android.net.MulticastRoutingConfig
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.ParcelFileDescriptor
+import android.os.SystemClock
+import android.os.test.TestLooper
+import android.system.Os
+import android.system.OsConstants.AF_INET6
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import android.util.Log
+import androidx.test.filters.LargeTest
+import com.android.net.module.util.structs.StructMf6cctl
+import com.android.net.module.util.structs.StructMrt6Msg
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.InetAddress
+import java.net.Inet6Address
+import java.net.InetSocketAddress
+import java.net.MulticastSocket
+import java.net.NetworkInterface
+import java.time.Clock
+import java.time.Instant
+import java.time.ZoneId
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+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 org.mockito.MockitoAnnotations
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+private const val TIMEOUT_MS = 2_000L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class MulticastRoutingCoordinatorServiceTest {
+
+    // mocks are lateinit as they need to be setup between tests
+    @Mock private lateinit var mDeps: MulticastRoutingCoordinatorService.Dependencies
+    @Mock private lateinit var mMulticastSocket: MulticastSocket
+
+    val mSock = DatagramSocket()
+    val mPfd = ParcelFileDescriptor.fromDatagramSocket(mSock)
+    val mFd = mPfd.getFileDescriptor()
+    val mIfName1 = "interface1"
+    val mIfName2 = "interface2"
+    val mIfName3 = "interface3"
+    val mIfPhysicalIndex1 = 10
+    val mIfPhysicalIndex2 = 11
+    val mIfPhysicalIndex3 = 12
+    val mSourceAddress = Inet6Address.getByName("2000::8888") as Inet6Address
+    val mGroupAddressScope5 = Inet6Address.getByName("ff05::1234") as Inet6Address
+    val mGroupAddressScope4 = Inet6Address.getByName("ff04::1234") as Inet6Address
+    val mGroupAddressScope3 = Inet6Address.getByName("ff03::1234") as Inet6Address
+    val mSocketAddressScope5 = InetSocketAddress(mGroupAddressScope5, 0)
+    val mSocketAddressScope4 = InetSocketAddress(mGroupAddressScope4, 0)
+    val mEmptyOifs = setOf<Int>()
+    val mClock = FakeClock()
+    val mNetworkInterface1 = createEmptyNetworkInterface()
+    val mNetworkInterface2 = createEmptyNetworkInterface()
+    // MulticastRoutingCoordinatorService needs to be initialized after the dependencies
+    // are mocked.
+    lateinit var mService: MulticastRoutingCoordinatorService
+    lateinit var mLooper: TestLooper
+
+    class FakeClock() : Clock() {
+        private var offsetMs = 0L
+
+        fun fastForward(ms: Long) {
+            offsetMs += ms
+        }
+
+        override fun instant(): Instant {
+            return Instant.now().plusMillis(offsetMs)
+        }
+
+        override fun getZone(): ZoneId {
+            throw RuntimeException("Not implemented");
+        }
+
+        override fun withZone(zone: ZoneId): Clock {
+            throw RuntimeException("Not implemented");
+        }
+
+    }
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        doReturn(mClock).`when`(mDeps).getClock()
+        doReturn(mFd).`when`(mDeps).createMulticastRoutingSocket()
+        doReturn(mMulticastSocket).`when`(mDeps).createMulticastSocket()
+        doReturn(mIfPhysicalIndex1).`when`(mDeps).getInterfaceIndex(mIfName1)
+        doReturn(mIfPhysicalIndex2).`when`(mDeps).getInterfaceIndex(mIfName2)
+        doReturn(mIfPhysicalIndex3).`when`(mDeps).getInterfaceIndex(mIfName3)
+        doReturn(mNetworkInterface1).`when`(mDeps).getNetworkInterface(mIfPhysicalIndex1)
+        doReturn(mNetworkInterface2).`when`(mDeps).getNetworkInterface(mIfPhysicalIndex2)
+    }
+
+    @After
+    fun tearDown() {
+        mSock.close()
+    }
+
+    // Functions under @Before and @Test run in different threads,
+    // (i.e. androidx.test.runner.AndroidJUnitRunner vs Time-limited test)
+    // MulticastRoutingCoordinatorService requires the jobs are run on the thread looper,
+    // so TestLooper needs to be created inside each test case to install the
+    // correct looper.
+    fun prepareService() {
+        mLooper = TestLooper()
+        val handler = Handler(mLooper.getLooper())
+
+        mService = MulticastRoutingCoordinatorService(handler, mDeps)
+    }
+
+    private fun createEmptyNetworkInterface(): NetworkInterface {
+        val constructor = NetworkInterface::class.java.getDeclaredConstructor()
+        constructor.isAccessible = true
+        return constructor.newInstance()
+    }
+
+    private fun createStructMf6cctl(src: Inet6Address, dst: Inet6Address, iifIdx: Int,
+            oifSet: Set<Int>): StructMf6cctl {
+        return StructMf6cctl(src, dst, iifIdx, oifSet)
+    }
+
+    // Send a MRT6MSG_NOCACHE packet to sock, to indicate a packet has arrived without matching MulticastRoutingCache
+    private fun sendMrt6msgNocachePacket(interfaceVirtualIndex: Int,
+            source: Inet6Address, destination: Inet6Address) {
+        mLooper.dispatchAll() // let MulticastRoutingCoordinatorService handle all msgs first to
+                              // apply any possible multicast routing config changes
+        val mrt6Msg = StructMrt6Msg(0 /* mbz must be 0 */, StructMrt6Msg.MRT6MSG_NOCACHE,
+                interfaceVirtualIndex, source, destination)
+        mLooper.getNewExecutor().execute({ mService.handleMulticastNocacheUpcall(mrt6Msg) })
+        mLooper.dispatchAll()
+    }
+
+    private fun applyMulticastForwardNone(fromIf: String, toIf: String) {
+        val configNone = MulticastRoutingConfig.CONFIG_FORWARD_NONE
+
+        mService.applyMulticastRoutingConfig(fromIf, toIf, configNone)
+    }
+
+    private fun applyMulticastForwardMinimumScope(fromIf: String, toIf: String, minScope: Int) {
+        val configMinimumScope = MulticastRoutingConfig.Builder(
+            MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, minScope).build()
+
+        mService.applyMulticastRoutingConfig(fromIf, toIf, configMinimumScope)
+    }
+
+    private fun applyMulticastForwardSelected(fromIf: String, toIf: String) {
+        val configSelected = MulticastRoutingConfig.Builder(
+            MulticastRoutingConfig.FORWARD_SELECTED)
+            .addListeningAddress(mGroupAddressScope5).build()
+
+        mService.applyMulticastRoutingConfig(fromIf, toIf, configSelected)
+    }
+
+    @Test
+    fun testConstructor_multicastRoutingSocketIsCreated() {
+        prepareService()
+        verify(mDeps).createMulticastRoutingSocket()
+    }
+
+    @Test
+    fun testMulticastRouting_applyForwardNone() {
+        prepareService()
+
+        applyMulticastForwardNone(mIfName1, mIfName2)
+        mLooper.dispatchAll()
+
+        // Both interfaces are not added as multicast routing interfaces
+        verify(mDeps, never()).setsockoptMrt6AddMif(eq(mFd), any())
+        // No MFC should be added for FORWARD_NONE
+        verify(mDeps, never()).setsockoptMrt6AddMfc(eq(mFd), any())
+        assertEquals(MulticastRoutingConfig.CONFIG_FORWARD_NONE,
+                mService.getMulticastRoutingConfig(mIfName1, mIfName2));
+    }
+
+    @Test
+    fun testMulticastRouting_applyForwardMinimumScope() {
+        prepareService()
+
+        applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+        mLooper.dispatchAll()
+
+        // No MFC is added for FORWARD_WITH_MIN_SCOPE
+        verify(mDeps, never()).setsockoptMrt6AddMfc(eq(mFd), any())
+        assertEquals(MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE,
+                mService.getMulticastRoutingConfig(mIfName1, mIfName2).getForwardingMode())
+        assertEquals(4, mService.getMulticastRoutingConfig(mIfName1, mIfName2).getMinimumScope())
+    }
+
+    @Test
+    fun testMulticastRouting_addressScopelargerThanMinScope_allowMfcIsAdded() {
+        prepareService()
+        applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+        mLooper.dispatchAll()
+        val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2))
+        val mf6cctl = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+                mService.getVirtualInterfaceIndex(mIfName1), oifs)
+
+        // simulate a MRT6MSG_NOCACHE upcall for a packet sent to group address of scope 5
+        sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5)
+
+        // an MFC is added for the packet
+        verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctl))
+    }
+
+    @Test
+    fun testMulticastRouting_addressScopeSmallerThanMinScope_blockingMfcIsAdded() {
+        prepareService()
+        applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4)
+        val mf6cctl = createStructMf6cctl(mSourceAddress, mGroupAddressScope3,
+                mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+        // simulate a MRT6MSG_NOCACHE upcall when a packet should not be forwarded
+        sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope3)
+
+        // a blocking MFC is added
+        verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctl))
+    }
+
+    @Test
+    fun testMulticastRouting_applyForwardSelected_joinsGroup() {
+        prepareService()
+
+        applyMulticastForwardSelected(mIfName1, mIfName2)
+        mLooper.dispatchAll()
+
+        verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+        assertEquals(MulticastRoutingConfig.FORWARD_SELECTED,
+                mService.getMulticastRoutingConfig(mIfName1, mIfName2).getForwardingMode())
+    }
+
+    @Test
+    fun testMulticastRouting_addListeningAddressInForwardSelected_joinsGroup() {
+        prepareService()
+
+        val configSelectedNoAddress = MulticastRoutingConfig.Builder(
+            MulticastRoutingConfig.FORWARD_SELECTED).build()
+        mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedNoAddress)
+        mLooper.dispatchAll()
+
+        val configSelectedWithAddresses = MulticastRoutingConfig.Builder(
+            MulticastRoutingConfig.FORWARD_SELECTED)
+            .addListeningAddress(mGroupAddressScope5)
+            .addListeningAddress(mGroupAddressScope4)
+            .build()
+        mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWithAddresses)
+        mLooper.dispatchAll()
+
+        verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+        verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1))
+    }
+
+    @Test
+    fun testMulticastRouting_removeListeningAddressInForwardSelected_leavesGroup() {
+        prepareService()
+        val configSelectedWith2Addresses = MulticastRoutingConfig.Builder(
+            MulticastRoutingConfig.FORWARD_SELECTED)
+            .addListeningAddress(mGroupAddressScope5)
+            .addListeningAddress(mGroupAddressScope4)
+            .build()
+        mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWith2Addresses)
+        mLooper.dispatchAll()
+
+        verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+        verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1))
+
+        // remove the scope4 address
+        val configSelectedWith1Address = MulticastRoutingConfig.Builder(
+            MulticastRoutingConfig.FORWARD_SELECTED)
+            .addListeningAddress(mGroupAddressScope5)
+            .build()
+        mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWith1Address)
+        mLooper.dispatchAll()
+
+        verify(mMulticastSocket).leaveGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1))
+        verify(mMulticastSocket, never())
+                .leaveGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+    }
+
+    @Test
+    fun testMulticastRouting_fromForwardSelectedToForwardNone_leavesGroup() {
+        prepareService()
+        applyMulticastForwardSelected(mIfName1, mIfName2)
+        mLooper.dispatchAll()
+
+        verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+
+        applyMulticastForwardNone(mIfName1, mIfName2)
+        mLooper.dispatchAll()
+
+        verify(mMulticastSocket).leaveGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+        assertEquals(MulticastRoutingConfig.CONFIG_FORWARD_NONE,
+                mService.getMulticastRoutingConfig(mIfName1, mIfName2));
+    }
+
+    @Test
+    fun testMulticastRouting_fromFowardSelectedToForwardNone_removesMulticastInterfaces() {
+        prepareService()
+
+        applyMulticastForwardSelected(mIfName1, mIfName2)
+        applyMulticastForwardSelected(mIfName1, mIfName3)
+        mLooper.dispatchAll()
+
+        assertNotNull(mService.getVirtualInterfaceIndex(mIfName1))
+        assertNotNull(mService.getVirtualInterfaceIndex(mIfName2))
+        assertNotNull(mService.getVirtualInterfaceIndex(mIfName3))
+
+        applyMulticastForwardNone(mIfName1, mIfName2)
+        mLooper.dispatchAll()
+
+        assertNotNull(mService.getVirtualInterfaceIndex(mIfName1))
+        assertNull(mService.getVirtualInterfaceIndex(mIfName2))
+        assertNotNull(mService.getVirtualInterfaceIndex(mIfName3))
+    }
+
+    @Test
+    fun testMulticastRouting_addMulticastRoutingInterfaces() {
+        prepareService()
+
+        applyMulticastForwardSelected(mIfName1, mIfName2)
+        mLooper.dispatchAll()
+
+        assertNotNull(mService.getVirtualInterfaceIndex(mIfName1))
+        assertNotNull(mService.getVirtualInterfaceIndex(mIfName2))
+        assertNotEquals(mService.getVirtualInterfaceIndex(mIfName1),
+                mService.getVirtualInterfaceIndex(mIfName2))
+    }
+
+    @Test
+    fun testMulticastRouting_removeMulticastRoutingInterfaces() {
+        prepareService()
+
+        applyMulticastForwardSelected(mIfName1, mIfName2)
+        mService.removeInterfaceFromMulticastRouting(mIfName1)
+        mLooper.dispatchAll()
+
+        assertNull(mService.getVirtualInterfaceIndex(mIfName1))
+        assertNotNull(mService.getVirtualInterfaceIndex(mIfName2))
+    }
+
+    @Test
+    fun testMulticastRouting_applyConfigNone_removesMfc() {
+        prepareService()
+
+        applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+        applyMulticastForwardSelected(mIfName1, mIfName3)
+
+        sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5)
+        val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2),
+                mService.getVirtualInterfaceIndex(mIfName3))
+        val oifsUpdate = setOf(mService.getVirtualInterfaceIndex(mIfName3))
+        val mf6cctlAdd = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+                mService.getVirtualInterfaceIndex(mIfName1), oifs)
+        val mf6cctlUpdate = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+                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(ifName1Copy, ifName2Copy)
+        mLooper.dispatchAll()
+
+        verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlUpdate))
+
+        applyMulticastForwardNone(ifName1Copy, ifName3Copy)
+        mLooper.dispatchAll()
+
+        verify(mDeps, timeout(TIMEOUT_MS).times(1)).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel))
+    }
+
+    @Test
+    @LargeTest
+    fun testMulticastRouting_maxNumberOfMfcs() {
+        prepareService()
+
+        // add MFC_MAX_NUMBER_OF_ENTRIES MFCs
+        applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+        for (i in 1..MulticastRoutingCoordinatorService.MFC_MAX_NUMBER_OF_ENTRIES) {
+            val groupAddress =
+                Inet6Address.getByName("ff05::" + Integer.toHexString(i)) as Inet6Address
+            sendMrt6msgNocachePacket(0, mSourceAddress, groupAddress)
+        }
+        val mf6cctlDel = createStructMf6cctl(mSourceAddress,
+                Inet6Address.getByName("ff05::1" ) as Inet6Address,
+                mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+        verify(mDeps, times(MulticastRoutingCoordinatorService.MFC_MAX_NUMBER_OF_ENTRIES)).
+            setsockoptMrt6AddMfc(eq(mFd), any())
+        // when number of mfcs reaches the max value, one mfc should be removed
+        verify(mDeps).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel))
+    }
+
+    @Test
+    fun testMulticastRouting_interfaceWithoutActiveConfig_isRemoved() {
+        prepareService()
+        applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+        mLooper.dispatchAll()
+        val virtualIndexIf1 = mService.getVirtualInterfaceIndex(mIfName1)
+        val virtualIndexIf2 = mService.getVirtualInterfaceIndex(mIfName2)
+
+        applyMulticastForwardNone(mIfName1, mIfName2)
+        mLooper.dispatchAll()
+
+        verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf1))
+        verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf2))
+    }
+
+    @Test
+    fun testMulticastRouting_interfaceWithActiveConfig_isNotRemoved() {
+        prepareService()
+        applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+        applyMulticastForwardMinimumScope(mIfName2, mIfName3, 4 /* minScope */)
+        mLooper.dispatchAll()
+        val virtualIndexIf1 = mService.getVirtualInterfaceIndex(mIfName1)
+        val virtualIndexIf2 = mService.getVirtualInterfaceIndex(mIfName2)
+        val virtualIndexIf3 = mService.getVirtualInterfaceIndex(mIfName3)
+
+        applyMulticastForwardNone(mIfName1, mIfName2)
+        mLooper.dispatchAll()
+
+        verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf1))
+        verify(mDeps, never()).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf2))
+        verify(mDeps, never()).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf3))
+    }
+
+    @Test
+    fun testMulticastRouting_unusedMfc_isRemovedAfterTimeout() {
+        prepareService()
+        applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+        sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5)
+        val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2))
+        val mf6cctlAdd = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+                mService.getVirtualInterfaceIndex(mIfName1), oifs)
+        val mf6cctlDel = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+                mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+        // An MFC is added
+        verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlAdd))
+
+        repeat(MulticastRoutingCoordinatorService.MFC_INACTIVE_TIMEOUT_MS /
+                MulticastRoutingCoordinatorService.MFC_INACTIVE_CHECK_INTERVAL_MS + 1) {
+            mClock.fastForward(MulticastRoutingCoordinatorService
+                    .MFC_INACTIVE_CHECK_INTERVAL_MS.toLong())
+            mLooper.moveTimeForward(MulticastRoutingCoordinatorService
+                    .MFC_INACTIVE_CHECK_INTERVAL_MS.toLong())
+            mLooper.dispatchAll();
+        }
+
+        verify(mDeps).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel))
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
index 06e0d6d..2fe8713 100644
--- a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
+++ b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -20,10 +20,12 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
@@ -72,6 +74,7 @@
     static final String STACKED_IFACE = "v4-test0";
     static final LinkAddress V6ADDR = new LinkAddress("2001:db8:1::f00/64");
     static final LinkAddress ADDR = new LinkAddress("192.0.2.5/29");
+    static final String CLAT_V6 = "64:ff9b::1";
     static final String NAT64_PREFIX = "64:ff9b::/96";
     static final String OTHER_NAT64_PREFIX = "2001:db8:0:64::/96";
     static final int NETID = 42;
@@ -83,7 +86,6 @@
     @Mock ClatCoordinator mClatCoordinator;
 
     TestLooper mLooper;
-    Handler mHandler;
     NetworkAgentConfig mAgentConfig = new NetworkAgentConfig();
 
     Nat464Xlat makeNat464Xlat(boolean isCellular464XlatEnabled) {
@@ -93,6 +95,14 @@
             }
         };
 
+        // The test looper needs to be created here on the test case thread and not in setUp,
+        // because setUp and test cases are run in different threads. Creating the test looper in
+        // setUp would make Looper.getThread() return the setUp thread, which does not match the
+        // test case thread that is actually used to process the messages.
+        mLooper = new TestLooper();
+        final Handler handler = new Handler(mLooper.getLooper());
+        doReturn(handler).when(mNai).handler();
+
         return new Nat464Xlat(mNai, mNetd, mDnsResolver, deps) {
             @Override protected int getNetId() {
                 return NETID;
@@ -114,9 +124,6 @@
 
     @Before
     public void setUp() throws Exception {
-        mLooper = new TestLooper();
-        mHandler = new Handler(mLooper.getLooper());
-
         MockitoAnnotations.initMocks(this);
 
         mNai.linkProperties = new LinkProperties();
@@ -127,11 +134,12 @@
         markNetworkConnected();
         when(mNai.connService()).thenReturn(mConnectivity);
         when(mNai.netAgentConfig()).thenReturn(mAgentConfig);
-        when(mNai.handler()).thenReturn(mHandler);
         final InterfaceConfigurationParcel mConfig = new InterfaceConfigurationParcel();
         when(mNetd.interfaceGetCfg(eq(STACKED_IFACE))).thenReturn(mConfig);
         mConfig.ipv4Addr = ADDR.getAddress().getHostAddress();
         mConfig.prefixLength =  ADDR.getPrefixLength();
+        doReturn(CLAT_V6).when(mClatCoordinator).clatStart(
+                BASE_IFACE, NETID, new IpPrefix(NAT64_PREFIX));
     }
 
     private void assertRequiresClat(boolean expected, NetworkAgentInfo nai) {
@@ -267,8 +275,7 @@
         verifyClatdStart(null /* inOrder */);
 
         // Stacked interface up notification arrives.
-        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
-        mLooper.dispatchNext();
+        nat.handleInterfaceLinkStateChanged(STACKED_IFACE, true);
 
         verify(mNetd).interfaceGetCfg(eq(STACKED_IFACE));
         verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
@@ -286,10 +293,10 @@
         assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
         verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
         assertIdle(nat);
-
+        // Verify the generated v6 is reset when clat is stopped.
+        assertNull(nat.mIPv6Address);
         // Stacked interface removed notification arrives and is ignored.
-        nat.interfaceRemoved(STACKED_IFACE);
-        mLooper.dispatchNext();
+        nat.handleInterfaceRemoved(STACKED_IFACE);
 
         verifyNoMoreInteractions(mNetd, mConnectivity);
     }
@@ -318,8 +325,7 @@
         verifyClatdStart(inOrder);
 
         // Stacked interface up notification arrives.
-        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
-        mLooper.dispatchNext();
+        nat.handleInterfaceLinkStateChanged(STACKED_IFACE, true);
 
         inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
         assertFalse(c.getValue().getStackedLinks().isEmpty());
@@ -338,10 +344,8 @@
 
         if (interfaceRemovedFirst) {
             // Stacked interface removed notification arrives and is ignored.
-            nat.interfaceRemoved(STACKED_IFACE);
-            mLooper.dispatchNext();
-            nat.interfaceLinkStateChanged(STACKED_IFACE, false);
-            mLooper.dispatchNext();
+            nat.handleInterfaceRemoved(STACKED_IFACE);
+            nat.handleInterfaceLinkStateChanged(STACKED_IFACE, false);
         }
 
         assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -355,15 +359,12 @@
 
         if (!interfaceRemovedFirst) {
             // Stacked interface removed notification arrives and is ignored.
-            nat.interfaceRemoved(STACKED_IFACE);
-            mLooper.dispatchNext();
-            nat.interfaceLinkStateChanged(STACKED_IFACE, false);
-            mLooper.dispatchNext();
+            nat.handleInterfaceRemoved(STACKED_IFACE);
+            nat.handleInterfaceLinkStateChanged(STACKED_IFACE, false);
         }
 
         // Stacked interface up notification arrives.
-        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
-        mLooper.dispatchNext();
+        nat.handleInterfaceLinkStateChanged(STACKED_IFACE, true);
 
         inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
         assertFalse(c.getValue().getStackedLinks().isEmpty());
@@ -405,8 +406,7 @@
         verifyClatdStart(null /* inOrder */);
 
         // Stacked interface up notification arrives.
-        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
-        mLooper.dispatchNext();
+        nat.handleInterfaceLinkStateChanged(STACKED_IFACE, true);
 
         verify(mNetd).interfaceGetCfg(eq(STACKED_IFACE));
         verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
@@ -415,8 +415,7 @@
         assertRunning(nat);
 
         // Stacked interface removed notification arrives (clatd crashed, ...).
-        nat.interfaceRemoved(STACKED_IFACE);
-        mLooper.dispatchNext();
+        nat.handleInterfaceRemoved(STACKED_IFACE);
 
         verifyClatdStop(null /* inOrder */);
         verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
@@ -451,12 +450,10 @@
         assertIdle(nat);
 
         // In-flight interface up notification arrives: no-op
-        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
-        mLooper.dispatchNext();
+        nat.handleInterfaceLinkStateChanged(STACKED_IFACE, true);
 
         // Interface removed notification arrives after stopClatd() takes effect: no-op.
-        nat.interfaceRemoved(STACKED_IFACE);
-        mLooper.dispatchNext();
+        nat.handleInterfaceRemoved(STACKED_IFACE);
 
         assertIdle(nat);
 
diff --git a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
index d667662..89e2a51 100644
--- a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -18,9 +18,7 @@
 
 import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO;
 import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME;
-
 import static com.android.testutils.MiscAsserts.assertStringContains;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
@@ -34,27 +32,23 @@
 import android.net.NetworkCapabilities;
 import android.os.Build;
 import android.system.OsConstants;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Base64;
-
+import androidx.test.filters.SmallTest;
 import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
 import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
-
-import libcore.util.EmptyArray;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-
 import java.io.FileOutputStream;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
+import libcore.util.EmptyArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index a27a0bf..727db58 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -50,9 +50,11 @@
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
+import android.net.TelephonyNetworkSpecifier;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.PowerManager;
@@ -60,8 +62,10 @@
 import android.telephony.TelephonyManager;
 import android.testing.PollingCheck;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 import androidx.test.filters.SmallTest;
@@ -104,12 +108,17 @@
     private static final long TEST_TIMEOUT_MS = 10_000L;
     private static final long UI_AUTOMATOR_WAIT_TIME_MILLIS = TEST_TIMEOUT_MS;
 
-    static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
-    static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
-    static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
+    private static final int TEST_SUB_ID = 43;
+    private static final String TEST_OPERATOR_NAME = "Test Operator";
+    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);
+        CELL_CAPABILITIES.setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(TEST_SUB_ID).build());
 
         WIFI_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
         WIFI_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
@@ -120,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);
     }
 
     /**
@@ -146,11 +158,14 @@
     @Mock DisplayMetrics mDisplayMetrics;
     @Mock PackageManager mPm;
     @Mock TelephonyManager mTelephonyManager;
+    @Mock TelephonyManager mTestSubIdTelephonyManager;
     @Mock NotificationManager mNotificationManager;
     @Mock NetworkAgentInfo mWifiNai;
     @Mock NetworkAgentInfo mCellNai;
     @Mock NetworkAgentInfo mVpnNai;
+    @Mock NetworkAgentInfo mBluetoothNai;
     @Mock NetworkInfo mNetworkInfo;
+    @Mock NetworkInfo mEmptyNetworkInfo;
     ArgumentCaptor<Notification> mCaptor;
 
     NetworkNotificationManager mManager;
@@ -165,20 +180,25 @@
         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();
-        when(mCtx.getResources()).thenReturn(mResources);
-        when(mCtx.getPackageManager()).thenReturn(mPm);
-        when(mCtx.getApplicationInfo()).thenReturn(new ApplicationInfo());
+        doReturn(mResources).when(mCtx).getResources();
+        doReturn(mPm).when(mCtx).getPackageManager();
+        doReturn(new ApplicationInfo()).when(mCtx).getApplicationInfo();
         final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mCtx));
         doReturn(UserHandle.ALL).when(asUserCtx).getUser();
-        when(mCtx.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx);
-        when(mCtx.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
-                .thenReturn(mNotificationManager);
-        when(mNetworkInfo.getExtraInfo()).thenReturn(TEST_EXTRA_INFO);
+        doReturn(asUserCtx).when(mCtx).createContextAsUser(eq(UserHandle.ALL), anyInt());
+        doReturn(mNotificationManager).when(mCtx)
+                .getSystemService(eq(Context.NOTIFICATION_SERVICE));
+        doReturn(TEST_EXTRA_INFO).when(mNetworkInfo).getExtraInfo();
         ConnectivityResources.setResourcesContextForTest(mCtx);
-        when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B);
-        when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
+        doReturn(0xFF607D8B).when(mResources).getColor(anyInt(), any());
+        doReturn(mDisplayMetrics).when(mResources).getDisplayMetrics();
+        doReturn(mTestSubIdTelephonyManager).when(mTelephonyManager)
+                .createForSubscriptionId(TEST_SUB_ID);
+        doReturn(TEST_OPERATOR_NAME).when(mTestSubIdTelephonyManager).getNetworkOperatorName();
 
         // Come up with some credible-looking transport names. The actual values do not matter.
         String[] transportNames = new String[NetworkCapabilities.MAX_TRANSPORT + 1];
@@ -386,14 +406,37 @@
     }
 
     @Test
-    public void testNotifyNoInternetAsDialogWhenHighPriority() throws Exception {
-        doReturn(true).when(mResources).getBoolean(
+    public void testNotifyNoInternet_asNotification() throws Exception {
+        doTestNotifyNotificationAsDialogWhenHighPriority(false, NO_INTERNET);
+    }
+    @Test
+        public void testNotifyNoInternet_asDialog() throws Exception {
+        doTestNotifyNotificationAsDialogWhenHighPriority(true, NO_INTERNET);
+    }
+
+    @Test
+    public void testNotifyLostInternet_asNotification() throws Exception {
+        doTestNotifyNotificationAsDialogWhenHighPriority(false, LOST_INTERNET);
+    }
+
+    @Test
+    public void testNotifyLostInternet_asDialog() throws Exception {
+        doTestNotifyNotificationAsDialogWhenHighPriority(true, LOST_INTERNET);
+    }
+
+    public void doTestNotifyNotificationAsDialogWhenHighPriority(final boolean configActive,
+            @NonNull final NotificationType notifType) throws Exception {
+        doReturn(configActive).when(mResources).getBoolean(
                 R.bool.config_notifyNoInternetAsDialogWhenHighPriority);
 
         final Instrumentation instr = InstrumentationRegistry.getInstrumentation();
         final UiDevice uiDevice =  UiDevice.getInstance(instr);
         final Context ctx = instr.getContext();
         final PowerManager pm = ctx.getSystemService(PowerManager.class);
+        // If the prio of this notif is < that of NETWORK_SWITCH, it's the lowest prio and
+        // therefore it can't be tested whether it cancels other lower-prio notifs.
+        final boolean isLowestPrioNotif = NetworkNotificationManager.priority(notifType)
+                < NetworkNotificationManager.priority(NETWORK_SWITCH);
 
         // Wake up the device (it has no effect if the device is already awake).
         uiDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP");
@@ -404,14 +447,34 @@
 
         // UiDevice.getLauncherPackageName() requires the test manifest to have a <queries> tag for
         // the launcher intent.
+        // Attempted workaround for b/286550950 where Settings is reported as the launcher
+        PollingCheck.check(
+                "Launcher package name was still settings after " + TEST_TIMEOUT_MS + "ms",
+                TEST_TIMEOUT_MS,
+                () -> {
+                    if ("com.android.settings".equals(uiDevice.getLauncherPackageName())) {
+                        final Intent intent = new Intent(Intent.ACTION_MAIN);
+                        intent.addCategory(Intent.CATEGORY_HOME);
+                        final List<ResolveInfo> acts = ctx.getPackageManager()
+                                .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+                        Log.e(NetworkNotificationManagerTest.class.getSimpleName(),
+                                "Got settings as launcher name; launcher activities: " + acts);
+                        return false;
+                    }
+                    return true;
+                });
         final String launcherPackageName = uiDevice.getLauncherPackageName();
         assertTrue(String.format("Launcher (%s) is not shown", launcherPackageName),
                 uiDevice.wait(Until.hasObject(By.pkg(launcherPackageName)),
                         UI_AUTOMATOR_WAIT_TIME_MILLIS));
 
-        mManager.showNotification(TEST_NOTIF_ID, NETWORK_SWITCH, mWifiNai, mCellNai, null, false);
-        // Non-"no internet" notifications are not affected
-        verify(mNotificationManager).notify(eq(TEST_NOTIF_TAG), eq(NETWORK_SWITCH.eventId), any());
+        if (!isLowestPrioNotif) {
+            mManager.showNotification(TEST_NOTIF_ID, NETWORK_SWITCH, mWifiNai, mCellNai,
+                    null, false);
+            // Non-"no internet" notifications are not affected
+            verify(mNotificationManager).notify(eq(TEST_NOTIF_TAG), eq(NETWORK_SWITCH.eventId),
+                    any());
+        }
 
         final String testAction = "com.android.connectivity.coverage.TEST_DIALOG";
         final Intent intent = new Intent(testAction)
@@ -420,22 +483,30 @@
         final PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0 /* requestCode */,
                 intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
 
-        mManager.showNotification(TEST_NOTIF_ID, NO_INTERNET, mWifiNai, null /* switchToNai */,
+        mManager.showNotification(TEST_NOTIF_ID, notifType, mWifiNai, null /* switchToNai */,
                 pendingIntent, true /* highPriority */);
 
-        // Previous notifications are still dismissed
-        verify(mNotificationManager).cancel(TEST_NOTIF_TAG, NETWORK_SWITCH.eventId);
+        if (!isLowestPrioNotif) {
+            // Previous notifications are still dismissed
+            verify(mNotificationManager).cancel(TEST_NOTIF_TAG, NETWORK_SWITCH.eventId);
+        }
 
-        // Verify that the activity is shown (the activity shows the action on screen)
-        final UiObject actionText = uiDevice.findObject(new UiSelector().text(testAction));
-        assertTrue("Activity not shown", actionText.waitForExists(TEST_TIMEOUT_MS));
+        if (configActive) {
+            // Verify that the activity is shown (the activity shows the action on screen)
+            final UiObject actionText = uiDevice.findObject(new UiSelector().text(testAction));
+            assertTrue("Activity not shown", actionText.waitForExists(TEST_TIMEOUT_MS));
 
-        // Tapping the text should dismiss the dialog
-        actionText.click();
-        assertTrue("Activity not dismissed", actionText.waitUntilGone(TEST_TIMEOUT_MS));
+            // Tapping the text should dismiss the dialog
+            actionText.click();
+            assertTrue("Activity not dismissed", actionText.waitUntilGone(TEST_TIMEOUT_MS));
 
-        // Verify no NO_INTERNET notification was posted
-        verify(mNotificationManager, never()).notify(any(), eq(NO_INTERNET.eventId), any());
+            // Verify that the notification was not posted
+            verify(mNotificationManager, never()).notify(any(), eq(notifType.eventId), any());
+        } else {
+            // Notification should have been posted, and will have overridden the previous
+            // one because it has the same id (hence no cancel).
+            verify(mNotificationManager).notify(eq(TEST_NOTIF_TAG), eq(notifType.eventId), any());
+        }
     }
 
     private void doNotificationTextTest(NotificationType type, @StringRes int expectedTitleRes,
@@ -478,4 +549,60 @@
                 R.string.wifi_no_internet, TEST_EXTRA_INFO,
                 R.string.wifi_no_internet_detailed);
     }
+
+    private void runSignInNotificationTest(NetworkAgentInfo nai, String testTitle,
+            String testContents) {
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+        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());
+        final Bundle noteExtras = noteCaptor.getValue().extras;
+        assertEquals(testTitle, noteExtras.getString(Notification.EXTRA_TITLE));
+        assertEquals(testContents, noteExtras.getString(Notification.EXTRA_TEXT));
+    }
+
+    @Test
+    public void testTelephonySignInNotification() {
+        final String testTitle = "Telephony no internet title";
+        final String testContents = "Add data for " + TEST_OPERATOR_NAME;
+        // The test does not use real resources as they are in the ConnectivityResources package,
+        // which is tricky to use (requires resolving the package, QUERY_ALL_PACKAGES permission).
+        doReturn(testTitle).when(mResources).getString(
+                R.string.mobile_network_available_no_internet);
+        doReturn(testContents).when(mResources).getString(
+                R.string.mobile_network_available_no_internet_detailed, TEST_OPERATOR_NAME);
+
+        runSignInNotificationTest(mCellNai, testTitle, testContents);
+    }
+
+    @Test
+    public void testTelephonySignInNotification_NoOperator() {
+        doReturn("").when(mTestSubIdTelephonyManager).getNetworkOperatorName();
+
+        final String testTitle = "Telephony no internet title";
+        final String testContents = "Add data";
+        doReturn(testTitle).when(mResources).getString(
+                R.string.mobile_network_available_no_internet);
+        doReturn(testContents).when(mResources).getString(
+                R.string.mobile_network_available_no_internet_detailed_unknown_carrier);
+
+        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/NetworkRequestStateInfoTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
new file mode 100644
index 0000000..44a645a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class NetworkRequestStateInfoTest {
+
+    @Mock
+    private NetworkRequestStateInfo.Dependencies mDependencies;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+    @Test
+    public void testSetNetworkRequestRemoved() {
+        final long nrStartTime = 1L;
+        final long nrEndTime = 101L;
+
+        NetworkRequest notMeteredWifiNetworkRequest = new NetworkRequest(
+                new NetworkCapabilities()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                        .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true),
+                0, 1, NetworkRequest.Type.REQUEST
+        );
+
+        // This call will be used to calculate NR received time
+        Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrStartTime);
+        NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+                notMeteredWifiNetworkRequest, mDependencies);
+
+        // This call will be used to calculate NR removed time
+        Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrEndTime);
+        networkRequestStateInfo.setNetworkRequestRemoved();
+        assertEquals(
+                nrEndTime - nrStartTime,
+                networkRequestStateInfo.getNetworkRequestDurationMillis());
+        assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(),
+                NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED);
+    }
+
+    @Test
+    public void testCheckInitialState() {
+        NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+                new NetworkRequest(new NetworkCapabilities(), 0, 1, NetworkRequest.Type.REQUEST),
+                mDependencies);
+        assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(),
+                NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED);
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
new file mode 100644
index 0000000..8dc0528
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.testutils.HandlerUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkRequestStateStatsMetricsTest {
+    @Mock
+    private NetworkRequestStateStatsMetrics.Dependencies mNRStateStatsDeps;
+    @Mock
+    private NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+    @Captor
+    private ArgumentCaptor<Handler> mHandlerCaptor;
+    @Captor
+    private ArgumentCaptor<Integer> mMessageWhatCaptor;
+
+    private NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+    private HandlerThread mHandlerThread;
+    private static final int TEST_REQUEST_ID = 10;
+    private static final int TEST_PACKAGE_UID = 20;
+    private static final int TIMEOUT_MS = 30_000;
+    private static final NetworkRequest NOT_METERED_WIFI_NETWORK_REQUEST = new NetworkRequest(
+            new NetworkCapabilities()
+                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                    .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true)
+                    .setCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET, false)
+                    .setRequestorUid(TEST_PACKAGE_UID),
+            0, TEST_REQUEST_ID, NetworkRequest.Type.REQUEST
+    );
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mHandlerThread = new HandlerThread("NetworkRequestStateStatsMetrics");
+        Mockito.when(mNRStateStatsDeps.makeHandlerThread("NetworkRequestStateStatsMetrics"))
+                .thenReturn(mHandlerThread);
+        Mockito.when(mNRStateStatsDeps.getMillisSinceEvent(anyLong())).thenReturn(0L);
+        Mockito.doAnswer(invocation -> {
+            mHandlerCaptor.getValue().sendMessage(
+                    Message.obtain(mHandlerCaptor.getValue(), mMessageWhatCaptor.getValue()));
+            return null;
+        }).when(mNRStateStatsDeps).sendMessageDelayed(
+                mHandlerCaptor.capture(), mMessageWhatCaptor.capture(), anyLong());
+        mNetworkRequestStateStatsMetrics = new NetworkRequestStateStatsMetrics(
+                mNRStateStatsDeps, mNRStateInfoDeps);
+    }
+
+    @Test
+    public void testNetworkRequestReceivedRemoved() {
+        final long nrStartTime = 1L;
+        final long nrEndTime = 101L;
+        // This call will be used to calculate NR received time
+        Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrStartTime);
+        mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+
+        ArgumentCaptor<NetworkRequestStateInfo> networkRequestStateInfoCaptor =
+                ArgumentCaptor.forClass(NetworkRequestStateInfo.class);
+        verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+                .writeStats(networkRequestStateInfoCaptor.capture());
+
+        NetworkRequestStateInfo nrStateInfoSent = networkRequestStateInfoCaptor.getValue();
+        assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED,
+                nrStateInfoSent.getNetworkRequestStateStatsType());
+        assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+        assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+        assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+        assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+        assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+        assertEquals(0, nrStateInfoSent.getNetworkRequestDurationMillis());
+
+        clearInvocations(mNRStateStatsDeps);
+        // This call will be used to calculate NR removed time
+        Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrEndTime);
+        mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+
+        verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+                .writeStats(networkRequestStateInfoCaptor.capture());
+
+        nrStateInfoSent = networkRequestStateInfoCaptor.getValue();
+        assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
+                nrStateInfoSent.getNetworkRequestStateStatsType());
+        assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+        assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+        assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+        assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+        assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+        assertEquals(nrEndTime - nrStartTime, nrStateInfoSent.getNetworkRequestDurationMillis());
+    }
+
+    @Test
+    public void testUnreceivedNetworkRequestRemoved() {
+        mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+        verify(mNRStateStatsDeps, never())
+                .writeStats(any(NetworkRequestStateInfo.class));
+    }
+
+    @Test
+    public void testNoMessagesWhenNetworkRequestReceived() {
+        mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+        verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+                .writeStats(any(NetworkRequestStateInfo.class));
+
+        clearInvocations(mNRStateStatsDeps);
+        mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+        verify(mNRStateStatsDeps, never())
+                .writeStats(any(NetworkRequestStateInfo.class));
+    }
+
+    @Test
+    public void testMessageQueueSizeLimitNotExceeded() {
+        // Imitate many events (MAX_QUEUED_REQUESTS) are coming together at once while
+        // the other event is being processed.
+        final ConditionVariable cv = new ConditionVariable();
+        mHandlerThread.getThreadHandler().post(() -> cv.block());
+        for (int i = 0; i < NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS / 2; i++) {
+            mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(new NetworkRequest(
+                    new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+                    0, i + 1, NetworkRequest.Type.REQUEST));
+            mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(new NetworkRequest(
+                    new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+                    0, i + 1, NetworkRequest.Type.REQUEST));
+        }
+
+        // When event queue is full, all other events should be dropped.
+        mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(new NetworkRequest(
+                new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+                0, 2 * NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS + 1,
+                NetworkRequest.Type.REQUEST));
+
+        cv.open();
+
+        // Check only first MAX_QUEUED_REQUESTS events are logged.
+        ArgumentCaptor<NetworkRequestStateInfo> networkRequestStateInfoCaptor =
+                ArgumentCaptor.forClass(NetworkRequestStateInfo.class);
+        verify(mNRStateStatsDeps, timeout(TIMEOUT_MS).times(
+                NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS))
+                .writeStats(networkRequestStateInfoCaptor.capture());
+        for (int i = 0; i < NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS; i++) {
+            NetworkRequestStateInfo nrStateInfoSent =
+                    networkRequestStateInfoCaptor.getAllValues().get(i);
+            assertEquals(i / 2 + 1, nrStateInfoSent.getRequestId());
+            assertEquals(
+                    (i % 2 == 0)
+                            ? NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED
+                            : NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
+                    nrStateInfoSent.getNetworkRequestStateStatsType());
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index cf02e3a..5bde31a 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -55,6 +55,7 @@
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.intThat;
+import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
@@ -99,6 +100,7 @@
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -126,12 +128,14 @@
     private static final int MOCK_APPID1 = 10001;
     private static final int MOCK_APPID2 = 10086;
     private static final int MOCK_APPID3 = 10110;
+    private static final int MOCK_APPID4 = 10111;
     private static final int SYSTEM_APPID1 = 1100;
     private static final int SYSTEM_APPID2 = 1108;
     private static final int VPN_APPID = 10002;
     private static final int MOCK_UID11 = MOCK_USER1.getUid(MOCK_APPID1);
     private static final int MOCK_UID12 = MOCK_USER1.getUid(MOCK_APPID2);
     private static final int MOCK_UID13 = MOCK_USER1.getUid(MOCK_APPID3);
+    private static final int MOCK_UID14 = MOCK_USER1.getUid(MOCK_APPID4);
     private static final int SYSTEM_APP_UID11 = MOCK_USER1.getUid(SYSTEM_APPID1);
     private static final int VPN_UID = MOCK_USER1.getUid(VPN_APPID);
     private static final int MOCK_UID21 = MOCK_USER2.getUid(MOCK_APPID1);
@@ -211,6 +215,14 @@
         onUserAdded(MOCK_USER1);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+    }
+
     private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion,
             String packageName, int uid, String... permissions) {
         final PackageInfo packageInfo =
@@ -965,6 +977,66 @@
     }
 
     @Test
+    public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAddAndOverlap() {
+        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID13),
+                buildPackageInfo(MOCK_PACKAGE2, MOCK_UID14),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        startMonitoring();
+        // MOCK_UID13 is subject to the VPN.
+        final UidRange range1 = new UidRange(MOCK_UID13, MOCK_UID13);
+        final UidRange[] lockdownRange1 = {range1};
+
+        // Add Lockdown uid range at 1st time, expect a rule to be set up
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange1);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID13, true /* add */);
+
+        reset(mBpfNetMaps);
+
+        // MOCK_UID13 and MOCK_UID14 are sequential and subject to the VPN in a separate range.
+        final UidRange range2 = new UidRange(MOCK_UID13, MOCK_UID14);
+        final UidRange[] lockdownRange2 = {range2};
+
+        // Add overlapping multiple-UID range. Rule may be set again, which is functionally
+        // a no-op, so it is fine.
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange2);
+        verify(mBpfNetMaps, atLeast(1)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID14, true /* add */);
+
+        reset(mBpfNetMaps);
+
+        // Remove the multiple-UID range. UID from first rule should not be removed.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, lockdownRange2);
+        verify(mBpfNetMaps, times(1)).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID14, false /* add */);
+
+        reset(mBpfNetMaps);
+
+        // Add the multiple-UID range back again to be able to test removing the first range, too.
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange2);
+        verify(mBpfNetMaps, atLeast(1)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID14, true /* add */);
+
+        reset(mBpfNetMaps);
+
+        // Remove the single-UID range. The rule for MOCK_UID11 should not change because it is
+        // still covered by the second, multiple-UID range rule.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, lockdownRange1);
+        verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(),  anyBoolean());
+
+        reset(mBpfNetMaps);
+
+        // Remove the multiple-UID range. Expect both UID rules to be torn down.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, lockdownRange2);
+        verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID13, false /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID14, false /* add */);
+    }
+
+    @Test
     public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
         doReturn(List.of(
                 buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
diff --git a/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
new file mode 100644
index 0000000..7885325
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
@@ -0,0 +1,334 @@
+/*
+ * 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
+
+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.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.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
+import org.mockito.ArgumentCaptor
+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
+
+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)
+private val SECONDARY_USER_SMS_APP_UID2 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID2)
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class SatelliteAccessControllerTest {
+    private val context = mock(Context::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 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() {
+        doReturn(emptyList<UserHandle>()).`when`(userManager).getUserHandles(true)
+        mockService(Context.USER_SERVICE, UserManager::class.java, userManager)
+
+        doReturn(primaryUserContext).`when`(context).createContextAsUser(PRIMARY_USER_HANDLE, 0)
+        doReturn(mPackageManagerPrimaryUser).`when`(primaryUserContext).packageManager
+
+        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`(mPackageManagerPrimaryUser)
+            .getApplicationInfo(eq(SMS_APP1), anyInt())
+
+        // Initialise message application primary user package2
+        val applicationInfo2 = ApplicationInfo()
+        applicationInfo2.uid = PRIMARY_USER_SMS_APP_UID2
+        doReturn(applicationInfo2)
+            .`when`(mPackageManagerPrimaryUser)
+            .getApplicationInfo(eq(SMS_APP2), anyInt())
+
+        // 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() {
+        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`(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`(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`(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() {
+        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`(mPackageManagerPrimaryUser)
+            .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
+        doReturn(listOf(SMS_APP1))
+            .`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`(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() {
+        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`(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`(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))
+
+        // check no uid is available as satellite network fallback uid at primary user
+        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
+        doReturn(listOf(SMS_APP2))
+            .`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`(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/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
deleted file mode 100644
index f7b9fcf..0000000
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ /dev/null
@@ -1,3216 +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.server.connectivity;
-
-import static android.Manifest.permission.BIND_VPN_SERVICE;
-import static android.Manifest.permission.CONTROL_VPN;
-import static android.content.pm.PackageManager.PERMISSION_DENIED;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
-import static android.net.ConnectivityDiagnosticsManager.DataStallReport;
-import static android.net.ConnectivityManager.NetworkCallback;
-import static android.net.INetd.IF_STATE_DOWN;
-import static android.net.INetd.IF_STATE_UP;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-import static android.net.RouteInfo.RTN_UNREACHABLE;
-import static android.net.VpnManager.TYPE_VPN_PLATFORM;
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.TEST_IDENTITY;
-import static android.net.cts.util.IkeSessionTestUtils.TEST_KEEPALIVE_TIMEOUT_UNSET;
-import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams;
-import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_AUTO;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_NONE;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_UDP;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_AUTO;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV4;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV6;
-import static android.os.Build.VERSION_CODES.S_V2;
-import static android.os.UserHandle.PER_USER_RANGE;
-import static android.telephony.CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL;
-import static android.telephony.CarrierConfigManager.KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
-import static android.telephony.CarrierConfigManager.KEY_PREFERRED_IKE_PROTOCOL_INT;
-
-import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
-import static com.android.server.connectivity.Vpn.AUTOMATIC_KEEPALIVE_DELAY_SECONDS;
-import static com.android.server.connectivity.Vpn.DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC;
-import static com.android.server.connectivity.Vpn.DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_AUTO;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV4_UDP;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_ESP;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_UDP;
-import static com.android.testutils.Cleanup.testAndCleanup;
-import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import static com.android.testutils.MiscAsserts.assertThrows;
-
-import static org.junit.Assert.assertArrayEquals;
-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.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.longThat;
-import static org.mockito.Mockito.after;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doCallRealMethod;
-import static org.mockito.Mockito.doNothing;
-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;
-import static org.mockito.Mockito.spy;
-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.annotation.NonNull;
-import android.annotation.UserIdInt;
-import android.app.AppOpsManager;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
-import android.net.ConnectivityDiagnosticsManager;
-import android.net.ConnectivityManager;
-import android.net.INetd;
-import android.net.Ikev2VpnProfile;
-import android.net.InetAddresses;
-import android.net.InterfaceConfigurationParcel;
-import android.net.IpPrefix;
-import android.net.IpSecConfig;
-import android.net.IpSecManager;
-import android.net.IpSecTransform;
-import android.net.IpSecTunnelInterfaceResponse;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
-import android.net.LocalSocket;
-import android.net.Network;
-import android.net.NetworkAgent;
-import android.net.NetworkAgentConfig;
-import android.net.NetworkCapabilities;
-import android.net.NetworkInfo.DetailedState;
-import android.net.RouteInfo;
-import android.net.TelephonyNetworkSpecifier;
-import android.net.UidRangeParcel;
-import android.net.VpnManager;
-import android.net.VpnProfileState;
-import android.net.VpnService;
-import android.net.VpnTransportInfo;
-import android.net.ipsec.ike.ChildSessionCallback;
-import android.net.ipsec.ike.ChildSessionConfiguration;
-import android.net.ipsec.ike.IkeFqdnIdentification;
-import android.net.ipsec.ike.IkeSessionCallback;
-import android.net.ipsec.ike.IkeSessionConfiguration;
-import android.net.ipsec.ike.IkeSessionConnectionInfo;
-import android.net.ipsec.ike.IkeSessionParams;
-import android.net.ipsec.ike.IkeTrafficSelector;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.net.ipsec.ike.exceptions.IkeException;
-import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
-import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
-import android.net.ipsec.ike.exceptions.IkeProtocolException;
-import android.net.ipsec.ike.exceptions.IkeTimeoutException;
-import android.net.vcn.VcnTransportInfo;
-import android.net.wifi.WifiInfo;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.os.ConditionVariable;
-import android.os.INetworkManagementService;
-import android.os.ParcelFileDescriptor;
-import android.os.PersistableBundle;
-import android.os.PowerWhitelistManager;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.os.test.TestLooper;
-import android.provider.Settings;
-import android.security.Credentials;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionInfo;
-import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.Pair;
-import android.util.Range;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.R;
-import com.android.internal.net.LegacyVpnInfo;
-import com.android.internal.net.VpnConfig;
-import com.android.internal.net.VpnProfile;
-import com.android.internal.util.HexDump;
-import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.DeviceIdleInternal;
-import com.android.server.IpSecService;
-import com.android.server.VpnTestBase;
-import com.android.server.vcn.util.PersistableBundleUtils;
-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 org.mockito.AdditionalAnswers;
-import org.mockito.Answers;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Stream;
-
-/**
- * Tests for {@link Vpn}.
- *
- * Build, install and run with:
- *  runtest frameworks-net -c com.android.server.connectivity.VpnTest
- */
-@RunWith(DevSdkIgnoreRunner.class)
-@SmallTest
-@IgnoreUpTo(S_V2)
-public class VpnTest extends VpnTestBase {
-    private static final String TAG = "VpnTest";
-
-    @Rule
-    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
-    static final Network EGRESS_NETWORK = new Network(101);
-    static final String EGRESS_IFACE = "wlan0";
-    private static final String TEST_VPN_CLIENT = "2.4.6.8";
-    private static final String TEST_VPN_SERVER = "1.2.3.4";
-    private static final String TEST_VPN_IDENTITY = "identity";
-    private static final byte[] TEST_VPN_PSK = "psk".getBytes();
-
-    private static final int IP4_PREFIX_LEN = 32;
-    private static final int IP6_PREFIX_LEN = 64;
-    private static final int MIN_PORT = 0;
-    private static final int MAX_PORT = 65535;
-
-    private static final InetAddress TEST_VPN_CLIENT_IP =
-            InetAddresses.parseNumericAddress(TEST_VPN_CLIENT);
-    private static final InetAddress TEST_VPN_SERVER_IP =
-            InetAddresses.parseNumericAddress(TEST_VPN_SERVER);
-    private static final InetAddress TEST_VPN_CLIENT_IP_2 =
-            InetAddresses.parseNumericAddress("192.0.2.200");
-    private static final InetAddress TEST_VPN_SERVER_IP_2 =
-            InetAddresses.parseNumericAddress("192.0.2.201");
-    private static final InetAddress TEST_VPN_INTERNAL_IP =
-            InetAddresses.parseNumericAddress("198.51.100.10");
-    private static final InetAddress TEST_VPN_INTERNAL_IP6 =
-            InetAddresses.parseNumericAddress("2001:db8::1");
-    private static final InetAddress TEST_VPN_INTERNAL_DNS =
-            InetAddresses.parseNumericAddress("8.8.8.8");
-    private static final InetAddress TEST_VPN_INTERNAL_DNS6 =
-            InetAddresses.parseNumericAddress("2001:4860:4860::8888");
-
-    private static final IkeTrafficSelector IN_TS =
-            new IkeTrafficSelector(MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP, TEST_VPN_INTERNAL_IP);
-    private static final IkeTrafficSelector IN_TS6 =
-            new IkeTrafficSelector(
-                    MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP6, TEST_VPN_INTERNAL_IP6);
-    private static final IkeTrafficSelector OUT_TS =
-            new IkeTrafficSelector(MIN_PORT, MAX_PORT,
-                    InetAddresses.parseNumericAddress("0.0.0.0"),
-                    InetAddresses.parseNumericAddress("255.255.255.255"));
-    private static final IkeTrafficSelector OUT_TS6 =
-            new IkeTrafficSelector(
-                    MIN_PORT,
-                    MAX_PORT,
-                    InetAddresses.parseNumericAddress("::"),
-                    InetAddresses.parseNumericAddress("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
-
-    private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE);
-    private static final Network TEST_NETWORK_2 = new Network(Integer.MAX_VALUE - 1);
-    private static final String TEST_IFACE_NAME = "TEST_IFACE";
-    private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
-    private static final long TEST_TIMEOUT_MS = 500L;
-    private static final long TIMEOUT_CROSSTHREAD_MS = 20_000L;
-    private static final String PRIMARY_USER_APP_EXCLUDE_KEY =
-            "VPNAPPEXCLUDED_27_com.testvpn.vpn";
-    static final String PKGS_BYTES = getPackageByteString(List.of(PKGS));
-    private static final Range<Integer> PRIMARY_USER_RANGE = uidRangeForUser(PRIMARY_USER.id);
-    private static final int TEST_KEEPALIVE_TIMER = 800;
-    private static final int TEST_SUB_ID = 1234;
-    private static final String TEST_MCCMNC = "12345";
-
-    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext;
-    @Mock private UserManager mUserManager;
-    @Mock private PackageManager mPackageManager;
-    @Mock private INetworkManagementService mNetService;
-    @Mock private INetd mNetd;
-    @Mock private AppOpsManager mAppOps;
-    @Mock private NotificationManager mNotificationManager;
-    @Mock private Vpn.SystemServices mSystemServices;
-    @Mock private Vpn.IkeSessionWrapper mIkeSessionWrapper;
-    @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
-    @Mock private Vpn.VpnNetworkAgentWrapper mMockNetworkAgent;
-    @Mock private ConnectivityManager mConnectivityManager;
-    @Mock private ConnectivityDiagnosticsManager mCdm;
-    @Mock private TelephonyManager mTelephonyManager;
-    @Mock private TelephonyManager mTmPerSub;
-    @Mock private CarrierConfigManager mConfigManager;
-    @Mock private SubscriptionManager mSubscriptionManager;
-    @Mock private IpSecService mIpSecService;
-    @Mock private VpnProfileStore mVpnProfileStore;
-    private final TestExecutor mExecutor;
-    @Mock DeviceIdleInternal mDeviceIdleInternal;
-    private final VpnProfile mVpnProfile;
-
-    private IpSecManager mIpSecManager;
-    private TestDeps mTestDeps;
-
-    public static class TestExecutor extends ScheduledThreadPoolExecutor {
-        public static final long REAL_DELAY = -1;
-
-        // For the purposes of the test, run all scheduled tasks after 10ms to save
-        // execution time, unless overridden by the specific test. Set to REAL_DELAY
-        // to actually wait for the delay specified by the real call to schedule().
-        public long delayMs = 10;
-        // If this is true, execute() will call the runnable inline. This is useful because
-        // super.execute() calls schedule(), which messes with checks that scheduled() is
-        // called a given number of times.
-        public boolean executeDirect = false;
-
-        public TestExecutor() {
-            super(1);
-        }
-
-        @Override
-        public void execute(final Runnable command) {
-            // See |executeDirect| for why this is necessary.
-            if (executeDirect) {
-                command.run();
-            } else {
-                super.execute(command);
-            }
-        }
-
-        @Override
-        public ScheduledFuture<?> schedule(final Runnable command, final long delay,
-                TimeUnit unit) {
-            if (0 == delay || delayMs == REAL_DELAY) {
-                // super.execute() calls schedule() with 0, so use the real delay if it's 0.
-                return super.schedule(command, delay, unit);
-            } else {
-                return super.schedule(command, delayMs, TimeUnit.MILLISECONDS);
-            }
-        }
-    }
-
-    public VpnTest() throws Exception {
-        // Build an actual VPN profile that is capable of being converted to and from an
-        // Ikev2VpnProfile
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY);
-        builder.setAuthPsk(TEST_VPN_PSK);
-        builder.setBypassable(true /* isBypassable */);
-        mExecutor = spy(new TestExecutor());
-        mVpnProfile = builder.build().toVpnProfile();
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mIpSecManager = new IpSecManager(mContext, mIpSecService);
-        mTestDeps = spy(new TestDeps());
-        doReturn(IPV6_MIN_MTU)
-                .when(mTestDeps)
-                .calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
-        doReturn(1500).when(mTestDeps).getJavaNetworkInterfaceMtu(any(), anyInt());
-
-        when(mContext.getPackageManager()).thenReturn(mPackageManager);
-        setMockedPackages(sPackages);
-
-        when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG);
-        when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG);
-        mockService(UserManager.class, Context.USER_SERVICE, mUserManager);
-        mockService(AppOpsManager.class, Context.APP_OPS_SERVICE, mAppOps);
-        mockService(NotificationManager.class, Context.NOTIFICATION_SERVICE, mNotificationManager);
-        mockService(ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mConnectivityManager);
-        mockService(IpSecManager.class, Context.IPSEC_SERVICE, mIpSecManager);
-        mockService(ConnectivityDiagnosticsManager.class, Context.CONNECTIVITY_DIAGNOSTICS_SERVICE,
-                mCdm);
-        mockService(TelephonyManager.class, Context.TELEPHONY_SERVICE, mTelephonyManager);
-        mockService(CarrierConfigManager.class, Context.CARRIER_CONFIG_SERVICE, mConfigManager);
-        mockService(SubscriptionManager.class, Context.TELEPHONY_SUBSCRIPTION_SERVICE,
-                mSubscriptionManager);
-        doReturn(mTmPerSub).when(mTelephonyManager).createForSubscriptionId(anyInt());
-        when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent))
-                .thenReturn(Resources.getSystem().getString(
-                        R.string.config_customVpnAlwaysOnDisconnectedDialogComponent));
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS))
-                .thenReturn(true);
-
-        // Used by {@link Notification.Builder}
-        ApplicationInfo applicationInfo = new ApplicationInfo();
-        applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
-        when(mContext.getApplicationInfo()).thenReturn(applicationInfo);
-        when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
-                .thenReturn(applicationInfo);
-
-        doNothing().when(mNetService).registerObserver(any());
-
-        // Deny all appops by default.
-        when(mAppOps.noteOpNoThrow(anyString(), anyInt(), anyString(), any(), any()))
-                .thenReturn(AppOpsManager.MODE_IGNORED);
-
-        // Setup IpSecService
-        final IpSecTunnelInterfaceResponse tunnelResp =
-                new IpSecTunnelInterfaceResponse(
-                        IpSecManager.Status.OK, TEST_TUNNEL_RESOURCE_ID, TEST_IFACE_NAME);
-        when(mIpSecService.createTunnelInterface(any(), any(), any(), any(), any()))
-                .thenReturn(tunnelResp);
-        doReturn(new LinkProperties()).when(mConnectivityManager).getLinkProperties(any());
-
-        // The unit test should know what kind of permission it needs and set the permission by
-        // itself, so set the default value of Context#checkCallingOrSelfPermission to
-        // PERMISSION_DENIED.
-        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
-
-        // Set up mIkev2SessionCreator and mExecutor
-        resetIkev2SessionCreator(mIkeSessionWrapper);
-    }
-
-    private void resetIkev2SessionCreator(Vpn.IkeSessionWrapper ikeSession) {
-        reset(mIkev2SessionCreator);
-        when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
-                .thenReturn(ikeSession);
-    }
-
-    private <T> void mockService(Class<T> clazz, String name, T service) {
-        doReturn(service).when(mContext).getSystemService(name);
-        doReturn(name).when(mContext).getSystemServiceName(clazz);
-        if (mContext.getSystemService(clazz).getClass().equals(Object.class)) {
-            // Test is using mockito-extended (mContext uses Answers.RETURNS_DEEP_STUBS and returned
-            // a mock object on a final method)
-            doCallRealMethod().when(mContext).getSystemService(clazz);
-        }
-    }
-
-    private Set<Range<Integer>> rangeSet(Range<Integer> ... ranges) {
-        final Set<Range<Integer>> range = new ArraySet<>();
-        for (Range<Integer> r : ranges) range.add(r);
-
-        return range;
-    }
-
-    private static Range<Integer> uidRangeForUser(int userId) {
-        return new Range<Integer>(userId * PER_USER_RANGE, (userId + 1) * PER_USER_RANGE - 1);
-    }
-
-    private Range<Integer> uidRange(int start, int stop) {
-        return new Range<Integer>(start, stop);
-    }
-
-    private static String getPackageByteString(List<String> packages) {
-        try {
-            return HexDump.toHexString(
-                    PersistableBundleUtils.toDiskStableBytes(PersistableBundleUtils.fromList(
-                            packages, PersistableBundleUtils.STRING_SERIALIZER)),
-                        true /* upperCase */);
-        } catch (IOException e) {
-            return null;
-        }
-    }
-
-    @Test
-    public void testRestrictedProfilesAreAddedToVpn() {
-        setMockedUsers(PRIMARY_USER, SECONDARY_USER, RESTRICTED_PROFILE_A, RESTRICTED_PROFILE_B);
-
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-
-        // Assume the user can have restricted profiles.
-        doReturn(true).when(mUserManager).canHaveRestrictedProfile();
-        final Set<Range<Integer>> ranges =
-                vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, null, null);
-
-        assertEquals(rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id)),
-                 ranges);
-    }
-
-    @Test
-    public void testManagedProfilesAreNotAddedToVpn() {
-        setMockedUsers(PRIMARY_USER, MANAGED_PROFILE_A);
-
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(
-                PRIMARY_USER.id, null, null);
-
-        assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
-    }
-
-    @Test
-    public void testAddUserToVpnOnlyAddsOneUser() {
-        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A, MANAGED_PROFILE_A);
-
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Set<Range<Integer>> ranges = new ArraySet<>();
-        vpn.addUserToRanges(ranges, PRIMARY_USER.id, null, null);
-
-        assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
-    }
-
-    @Test
-    public void testUidAllowAndDenylist() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Range<Integer> user = PRIMARY_USER_RANGE;
-        final int userStart = user.getLower();
-        final int userStop = user.getUpper();
-        final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
-
-        // Allowed list
-        final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
-                Arrays.asList(packages), null /* disallowedApplications */);
-        assertEquals(rangeSet(
-                uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]),
-                uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2]),
-                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0]),
-                         Process.toSdkSandboxUid(userStart + PKG_UIDS[0])),
-                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[1]),
-                         Process.toSdkSandboxUid(userStart + PKG_UIDS[2]))),
-                allow);
-
-        // Denied list
-        final Set<Range<Integer>> disallow =
-                vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
-                        null /* allowedApplications */, Arrays.asList(packages));
-        assertEquals(rangeSet(
-                uidRange(userStart, userStart + PKG_UIDS[0] - 1),
-                uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                /* Empty range between UIDS[1] and UIDS[2], should be excluded, */
-                uidRange(userStart + PKG_UIDS[2] + 1,
-                         Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                         Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)),
-                disallow);
-    }
-
-    private void verifyPowerSaveTempWhitelistApp(String packageName) {
-        verify(mDeviceIdleInternal, timeout(TEST_TIMEOUT_MS)).addPowerSaveTempWhitelistApp(
-                anyInt(), eq(packageName), anyLong(), anyInt(), eq(false),
-                eq(PowerWhitelistManager.REASON_VPN), eq("VpnManager event"));
-    }
-
-    @Test
-    public void testGetAlwaysAndOnGetLockDown() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-
-        // Default state.
-        assertFalse(vpn.getAlwaysOn());
-        assertFalse(vpn.getLockdown());
-
-        // Set always-on without lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList()));
-        assertTrue(vpn.getAlwaysOn());
-        assertFalse(vpn.getLockdown());
-
-        // Set always-on with lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList()));
-        assertTrue(vpn.getAlwaysOn());
-        assertTrue(vpn.getLockdown());
-
-        // Remove always-on configuration.
-        assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList()));
-        assertFalse(vpn.getAlwaysOn());
-        assertFalse(vpn.getLockdown());
-    }
-
-    @Test
-    public void testLockdownChangingPackage() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Range<Integer> user = PRIMARY_USER_RANGE;
-        final int userStart = user.getLower();
-        final int userStop = user.getUpper();
-        // Set always-on without lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null));
-
-        // Set always-on with lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
-        }));
-
-        // Switch to another app.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
-        }));
-    }
-
-    @Test
-    public void testLockdownAllowlist() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Range<Integer> user = PRIMARY_USER_RANGE;
-        final int userStart = user.getLower();
-        final int userStop = user.getUpper();
-        // Set always-on with lockdown and allow app PKGS[2] from lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[1], true, Collections.singletonList(PKGS[2])));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[]  {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1]) - 1),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
-        }));
-        // Change allowed app list to PKGS[3].
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[1], true, Collections.singletonList(PKGS[3])));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
-        }));
-
-        // Change the VPN app.
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[0], true, Collections.singletonList(PKGS[3])));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
-        }));
-
-        // Remove the list of allowed packages.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
-        }));
-
-        // Add the list of allowed packages.
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[0], true, Collections.singletonList(PKGS[1])));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
-        }));
-
-        // Try allowing a package with a comma, should be rejected.
-        assertFalse(vpn.setAlwaysOnPackage(
-                PKGS[0], true, Collections.singletonList("a.b,c.d")));
-
-        // Pass a non-existent packages in the allowlist, they (and only they) should be ignored.
-        // allowed package should change from PGKS[1] to PKGS[2].
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app")));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[2] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
-        }));
-    }
-
-    @Test
-    public void testLockdownRuleRepeatability() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
-                new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
-        // Given legacy lockdown is already enabled,
-        vpn.setLockdown(true);
-        verify(mConnectivityManager, times(1)).setRequireVpnForUids(true,
-                toRanges(primaryUserRangeParcel));
-
-        // Enabling legacy lockdown twice should do nothing.
-        vpn.setLockdown(true);
-        verify(mConnectivityManager, times(1)).setRequireVpnForUids(anyBoolean(), any());
-
-        // And disabling should remove the rules exactly once.
-        vpn.setLockdown(false);
-        verify(mConnectivityManager, times(1)).setRequireVpnForUids(false,
-                toRanges(primaryUserRangeParcel));
-
-        // Removing the lockdown again should have no effect.
-        vpn.setLockdown(false);
-        verify(mConnectivityManager, times(2)).setRequireVpnForUids(anyBoolean(), any());
-    }
-
-    private ArrayList<Range<Integer>> toRanges(UidRangeParcel[] ranges) {
-        ArrayList<Range<Integer>> rangesArray = new ArrayList<>(ranges.length);
-        for (int i = 0; i < ranges.length; i++) {
-            rangesArray.add(new Range<>(ranges[i].start, ranges[i].stop));
-        }
-        return rangesArray;
-    }
-
-    @Test
-    public void testLockdownRuleReversibility() throws Exception {
-        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final UidRangeParcel[] entireUser = {
-            new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())
-        };
-        final UidRangeParcel[] exceptPkg0 = {
-            new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1),
-            new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1,
-                               Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] - 1)),
-            new UidRangeParcel(Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] + 1),
-                               entireUser[0].stop),
-        };
-
-        final InOrder order = inOrder(mConnectivityManager);
-
-        // Given lockdown is enabled with no package (legacy VPN),
-        vpn.setLockdown(true);
-        order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
-
-        // When a new VPN package is set the rules should change to cover that package.
-        vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE);
-        order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(entireUser));
-        order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(exceptPkg0));
-
-        // When that VPN package is unset, everything should be undone again in reverse.
-        vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE);
-        order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(exceptPkg0));
-        order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
-    }
-
-    @Test
-    public void testPrepare_throwSecurityExceptionWhenGivenPackageDoesNotBelongToTheCaller()
-            throws Exception {
-        mTestDeps.mIgnoreCallingUidChecks = false;
-        final Vpn vpn = createVpn();
-        assertThrows(SecurityException.class,
-                () -> vpn.prepare("com.not.vpn.owner", null, VpnManager.TYPE_VPN_SERVICE));
-        assertThrows(SecurityException.class,
-                () -> vpn.prepare(null, "com.not.vpn.owner", VpnManager.TYPE_VPN_SERVICE));
-        assertThrows(SecurityException.class,
-                () -> vpn.prepare("com.not.vpn.owner1", "com.not.vpn.owner2",
-                        VpnManager.TYPE_VPN_SERVICE));
-    }
-
-    @Test
-    public void testPrepare_bothOldPackageAndNewPackageAreNull() throws Exception {
-        final Vpn vpn = createVpn();
-        assertTrue(vpn.prepare(null, null, VpnManager.TYPE_VPN_SERVICE));
-
-    }
-
-    @Test
-    public void testIsAlwaysOnPackageSupported() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-
-        ApplicationInfo appInfo = new ApplicationInfo();
-        when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(PRIMARY_USER.id)))
-                .thenReturn(appInfo);
-
-        ServiceInfo svcInfo = new ServiceInfo();
-        ResolveInfo resInfo = new ResolveInfo();
-        resInfo.serviceInfo = svcInfo;
-        when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA),
-                eq(PRIMARY_USER.id)))
-                .thenReturn(Collections.singletonList(resInfo));
-
-        // null package name should return false
-        assertFalse(vpn.isAlwaysOnPackageSupported(null));
-
-        // Pre-N apps are not supported
-        appInfo.targetSdkVersion = VERSION_CODES.M;
-        assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0]));
-
-        // N+ apps are supported by default
-        appInfo.targetSdkVersion = VERSION_CODES.N;
-        assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0]));
-
-        // Apps that opt out explicitly are not supported
-        appInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
-        Bundle metaData = new Bundle();
-        metaData.putBoolean(VpnService.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, false);
-        svcInfo.metaData = metaData;
-        assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0]));
-    }
-
-    @Test
-    public void testNotificationShownForAlwaysOnApp() throws Exception {
-        final UserHandle userHandle = UserHandle.of(PRIMARY_USER.id);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        setMockedUsers(PRIMARY_USER);
-
-        final InOrder order = inOrder(mNotificationManager);
-
-        // Don't show a notification for regular disconnected states.
-        vpn.updateState(DetailedState.DISCONNECTED, TAG);
-        order.verify(mNotificationManager, atLeastOnce()).cancel(anyString(), anyInt());
-
-        // Start showing a notification for disconnected once always-on.
-        vpn.setAlwaysOnPackage(PKGS[0], false, null);
-        order.verify(mNotificationManager).notify(anyString(), anyInt(), any());
-
-        // Stop showing the notification once connected.
-        vpn.updateState(DetailedState.CONNECTED, TAG);
-        order.verify(mNotificationManager).cancel(anyString(), anyInt());
-
-        // Show the notification if we disconnect again.
-        vpn.updateState(DetailedState.DISCONNECTED, TAG);
-        order.verify(mNotificationManager).notify(anyString(), anyInt(), any());
-
-        // Notification should be cleared after unsetting always-on package.
-        vpn.setAlwaysOnPackage(null, false, null);
-        order.verify(mNotificationManager).cancel(anyString(), anyInt());
-    }
-
-    /**
-     * The profile name should NOT change between releases for backwards compatibility
-     *
-     * <p>If this is changed between releases, the {@link Vpn#getVpnProfilePrivileged()} method MUST
-     * be updated to ensure backward compatibility.
-     */
-    @Test
-    public void testGetProfileNameForPackage() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        setMockedUsers(PRIMARY_USER);
-
-        final String expected = Credentials.PLATFORM_VPN + PRIMARY_USER.id + "_" + TEST_VPN_PKG;
-        assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG));
-    }
-
-    private Vpn createVpn(String... grantedOps) throws Exception {
-        return createVpn(PRIMARY_USER, grantedOps);
-    }
-
-    private Vpn createVpn(UserInfo user, String... grantedOps) throws Exception {
-        final Vpn vpn = createVpn(user.id);
-        setMockedUsers(user);
-
-        for (final String opStr : grantedOps) {
-            when(mAppOps.noteOpNoThrow(opStr, Process.myUid(), TEST_VPN_PKG,
-                    null /* attributionTag */, null /* message */))
-                    .thenReturn(AppOpsManager.MODE_ALLOWED);
-        }
-
-        return vpn;
-    }
-
-    private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, String... checkedOps) {
-        assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile));
-
-        // The profile should always be stored, whether or not consent has been previously granted.
-        verify(mVpnProfileStore)
-                .put(
-                        eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)),
-                        eq(mVpnProfile.encode()));
-
-        for (final String checkedOpStr : checkedOps) {
-            verify(mAppOps).noteOpNoThrow(checkedOpStr, Process.myUid(), TEST_VPN_PKG,
-                    null /* attributionTag */, null /* message */);
-        }
-    }
-
-    @Test
-    public void testProvisionVpnProfileNoIpsecTunnels() throws Exception {
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS))
-                .thenReturn(false);
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        try {
-            checkProvisionVpnProfile(
-                    vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-            fail("Expected exception due to missing feature");
-        } catch (UnsupportedOperationException expected) {
-        }
-    }
-
-    private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-        when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
-                .thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
-
-        vpn.startVpnProfile(TEST_VPN_PKG);
-        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-        vpn.mNetworkAgent = mMockNetworkAgent;
-        return vpn;
-    }
-
-    @Test
-    public void testSetAndGetAppExclusionList() throws Exception {
-        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
-        verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
-        vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
-        verify(mVpnProfileStore)
-                .put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
-                     eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
-        assertEquals(vpn.createUserAndRestrictedProfilesRanges(
-                PRIMARY_USER.id, null, Arrays.asList(PKGS)),
-                vpn.mNetworkCapabilities.getUids());
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-    }
-
-    @Test
-    public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
-        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
-        vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
-        verify(mMockNetworkAgent).doSendNetworkCapabilities(any());
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-
-        reset(mMockNetworkAgent);
-
-        // Remove one of the package
-        List<Integer> newExcludedUids = toList(PKG_UIDS);
-        newExcludedUids.remove((Integer) PKG_UIDS[0]);
-        sPackages.remove(PKGS[0]);
-        vpn.refreshPlatformVpnAppExclusionList();
-
-        // List in keystore is not changed, but UID for the removed packages is no longer exempted.
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
-                vpn.mNetworkCapabilities.getUids());
-        ArgumentCaptor<NetworkCapabilities> ncCaptor =
-                ArgumentCaptor.forClass(NetworkCapabilities.class);
-        verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
-        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
-                ncCaptor.getValue().getUids());
-
-        reset(mMockNetworkAgent);
-
-        // Add the package back
-        newExcludedUids.add(PKG_UIDS[0]);
-        sPackages.put(PKGS[0], PKG_UIDS[0]);
-        vpn.refreshPlatformVpnAppExclusionList();
-
-        // List in keystore is not changed and the uid list should be updated in the net cap.
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
-                vpn.mNetworkCapabilities.getUids());
-        verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
-        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
-                ncCaptor.getValue().getUids());
-    }
-
-    private Set<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedList) {
-        final SortedSet<Integer> list = new TreeSet<>();
-
-        final int userBase = userId * UserHandle.PER_USER_RANGE;
-        for (int uid : excludedList) {
-            final int applicationUid = UserHandle.getUid(userId, uid);
-            list.add(applicationUid);
-            list.add(Process.toSdkSandboxUid(applicationUid)); // Add Sdk Sandbox UID
-        }
-
-        final int minUid = userBase;
-        final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
-        final Set<Range<Integer>> ranges = new ArraySet<>();
-
-        // Iterate the list to create the ranges between each uid.
-        int start = minUid;
-        for (int uid : list) {
-            if (uid == start) {
-                start++;
-            } else {
-                ranges.add(new Range<>(start, uid - 1));
-                start = uid + 1;
-            }
-        }
-
-        // Create the range between last uid and max uid.
-        if (start <= maxUid) {
-            ranges.add(new Range<>(start, maxUid));
-        }
-
-        return ranges;
-    }
-
-    @Test
-    public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
-        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
-
-        // Mock it to restricted profile
-        when(mUserManager.getUserInfo(anyInt())).thenReturn(RESTRICTED_PROFILE_A);
-
-        // Restricted users cannot configure VPNs
-        assertThrows(SecurityException.class,
-                () -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>()));
-
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-    }
-
-    @Test
-    public void testProvisionVpnProfilePreconsented() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        checkProvisionVpnProfile(
-                vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-    }
-
-    @Test
-    public void testProvisionVpnProfileNotPreconsented() throws Exception {
-        final Vpn vpn = createVpn();
-
-        // Expect that both the ACTIVATE_VPN and ACTIVATE_PLATFORM_VPN were tried, but the caller
-        // had neither.
-        checkProvisionVpnProfile(vpn, false /* expectedResult */,
-                AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN);
-    }
-
-    @Test
-    public void testProvisionVpnProfileVpnServicePreconsented() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_VPN);
-
-        checkProvisionVpnProfile(vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_VPN);
-    }
-
-    @Test
-    public void testProvisionVpnProfileTooLarge() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        final VpnProfile bigProfile = new VpnProfile("");
-        bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]);
-
-        try {
-            vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile);
-            fail("Expected IAE due to profile size");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testProvisionVpnProfileRestrictedUser() throws Exception {
-        final Vpn vpn =
-                createVpn(
-                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        try {
-            vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile);
-            fail("Expected SecurityException due to restricted user");
-        } catch (SecurityException expected) {
-        }
-    }
-
-    @Test
-    public void testDeleteVpnProfile() throws Exception {
-        final Vpn vpn = createVpn();
-
-        vpn.deleteVpnProfile(TEST_VPN_PKG);
-
-        verify(mVpnProfileStore)
-                .remove(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-    }
-
-    @Test
-    public void testDeleteVpnProfileRestrictedUser() throws Exception {
-        final Vpn vpn =
-                createVpn(
-                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        try {
-            vpn.deleteVpnProfile(TEST_VPN_PKG);
-            fail("Expected SecurityException due to restricted user");
-        } catch (SecurityException expected) {
-        }
-    }
-
-    @Test
-    public void testGetVpnProfilePrivileged() throws Exception {
-        final Vpn vpn = createVpn();
-
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(new VpnProfile("").encode());
-
-        vpn.getVpnProfilePrivileged(TEST_VPN_PKG);
-
-        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-    }
-
-    private void verifyPlatformVpnIsActivated(String packageName) {
-        verify(mAppOps).noteOpNoThrow(
-                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                eq(Process.myUid()),
-                eq(packageName),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-        verify(mAppOps).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
-                eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                eq(packageName),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-    }
-
-    private void verifyPlatformVpnIsDeactivated(String packageName) {
-        // Add a small delay to double confirm that finishOp is only called once.
-        verify(mAppOps, after(100)).finishOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
-                eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                eq(packageName),
-                eq(null) /* attributionTag */);
-    }
-
-    @Test
-    public void testStartVpnProfile() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-
-        vpn.startVpnProfile(TEST_VPN_PKG);
-
-        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-    }
-
-    @Test
-    public void testStartVpnProfileVpnServicePreconsented() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_VPN);
-
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-
-        vpn.startVpnProfile(TEST_VPN_PKG);
-
-        // Verify that the ACTIVATE_VPN appop was checked, but no error was thrown.
-        verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
-                TEST_VPN_PKG, null /* attributionTag */, null /* message */);
-    }
-
-    @Test
-    public void testStartVpnProfileNotConsented() throws Exception {
-        final Vpn vpn = createVpn();
-
-        try {
-            vpn.startVpnProfile(TEST_VPN_PKG);
-            fail("Expected failure due to no user consent");
-        } catch (SecurityException expected) {
-        }
-
-        // Verify both appops were checked.
-        verify(mAppOps)
-                .noteOpNoThrow(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                        eq(Process.myUid()),
-                        eq(TEST_VPN_PKG),
-                        eq(null) /* attributionTag */,
-                        eq(null) /* message */);
-        verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
-                TEST_VPN_PKG, null /* attributionTag */, null /* message */);
-
-        // Keystore should never have been accessed.
-        verify(mVpnProfileStore, never()).get(any());
-    }
-
-    @Test
-    public void testStartVpnProfileMissingProfile() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null);
-
-        try {
-            vpn.startVpnProfile(TEST_VPN_PKG);
-            fail("Expected failure due to missing profile");
-        } catch (IllegalArgumentException expected) {
-        }
-
-        verify(mVpnProfileStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG));
-        verify(mAppOps)
-                .noteOpNoThrow(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                        eq(Process.myUid()),
-                        eq(TEST_VPN_PKG),
-                        eq(null) /* attributionTag */,
-                        eq(null) /* message */);
-    }
-
-    @Test
-    public void testStartVpnProfileRestrictedUser() throws Exception {
-        final Vpn vpn = createVpn(RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        try {
-            vpn.startVpnProfile(TEST_VPN_PKG);
-            fail("Expected SecurityException due to restricted user");
-        } catch (SecurityException expected) {
-        }
-    }
-
-    @Test
-    public void testStopVpnProfileRestrictedUser() throws Exception {
-        final Vpn vpn = createVpn(RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        try {
-            vpn.stopVpnProfile(TEST_VPN_PKG);
-            fail("Expected SecurityException due to restricted user");
-        } catch (SecurityException expected) {
-        }
-    }
-
-    @Test
-    public void testStartOpAndFinishOpWillBeCalledWhenPlatformVpnIsOnAndOff() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-        vpn.startVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-        // Add a small delay to make sure that startOp is only called once.
-        verify(mAppOps, after(100).times(1)).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
-                eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-        // Check that the startOp is not called with OPSTR_ESTABLISH_VPN_SERVICE.
-        verify(mAppOps, never()).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
-                eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-        vpn.stopVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
-    }
-
-    @Test
-    public void testStartOpWithSeamlessHandover() throws Exception {
-        // Create with SYSTEM_USER so that establish() will match the user ID when checking
-        // against Binder.getCallerUid
-        final Vpn vpn = createVpn(SYSTEM_USER, AppOpsManager.OPSTR_ACTIVATE_VPN);
-        assertTrue(vpn.prepare(TEST_VPN_PKG, null, VpnManager.TYPE_VPN_SERVICE));
-        final VpnConfig config = new VpnConfig();
-        config.user = "VpnTest";
-        config.addresses.add(new LinkAddress("192.0.2.2/32"));
-        config.mtu = 1450;
-        final ResolveInfo resolveInfo = new ResolveInfo();
-        final ServiceInfo serviceInfo = new ServiceInfo();
-        serviceInfo.permission = BIND_VPN_SERVICE;
-        resolveInfo.serviceInfo = serviceInfo;
-        when(mPackageManager.resolveService(any(), anyInt())).thenReturn(resolveInfo);
-        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
-        vpn.establish(config);
-        verify(mAppOps, times(1)).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-        // Call establish() twice with the same config, it should match seamless handover case and
-        // startOp() shouldn't be called again.
-        vpn.establish(config);
-        verify(mAppOps, times(1)).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-    }
-
-    private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass,
-            int errorCode, String[] packageName, @NonNull VpnProfileState... profileState) {
-        final Context userContext =
-                mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
-        final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
-
-        final int verifyTimes = profileState.length;
-        verify(userContext, times(verifyTimes)).startService(intentArgumentCaptor.capture());
-
-        for (int i = 0; i < verifyTimes; i++) {
-            final Intent intent = intentArgumentCaptor.getAllValues().get(i);
-            assertEquals(packageName[i], intent.getPackage());
-            assertEquals(sessionKey, intent.getStringExtra(VpnManager.EXTRA_SESSION_KEY));
-            final Set<String> categories = intent.getCategories();
-            assertTrue(categories.contains(category));
-            assertEquals(1, categories.size());
-            assertEquals(errorClass,
-                    intent.getIntExtra(VpnManager.EXTRA_ERROR_CLASS, -1 /* defaultValue */));
-            assertEquals(errorCode,
-                    intent.getIntExtra(VpnManager.EXTRA_ERROR_CODE, -1 /* defaultValue */));
-            // CATEGORY_EVENT_DEACTIVATED_BY_USER & CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED won't
-            // send NetworkCapabilities & LinkProperties to VPN app.
-            // For ERROR_CODE_NETWORK_LOST, the NetworkCapabilities & LinkProperties of underlying
-            // network will be cleared. So the VPN app will receive null for those 2 extra values.
-            if (category.equals(VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER)
-                    || category.equals(VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED)
-                    || errorCode == VpnManager.ERROR_CODE_NETWORK_LOST) {
-                assertNull(intent.getParcelableExtra(
-                        VpnManager.EXTRA_UNDERLYING_NETWORK_CAPABILITIES));
-                assertNull(intent.getParcelableExtra(VpnManager.EXTRA_UNDERLYING_LINK_PROPERTIES));
-            } else {
-                assertNotNull(intent.getParcelableExtra(
-                        VpnManager.EXTRA_UNDERLYING_NETWORK_CAPABILITIES));
-                assertNotNull(intent.getParcelableExtra(
-                        VpnManager.EXTRA_UNDERLYING_LINK_PROPERTIES));
-            }
-
-            assertEquals(profileState[i], intent.getParcelableExtra(
-                    VpnManager.EXTRA_VPN_PROFILE_STATE, VpnProfileState.class));
-        }
-        reset(userContext);
-    }
-
-    private void verifyDeactivatedByUser(String sessionKey, String[] packageName) {
-        // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
-        // errorCode won't be set.
-        verifyVpnManagerEvent(sessionKey, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
-                -1 /* errorClass */, -1 /* errorCode */, packageName,
-                // VPN NetworkAgnet does not switch to CONNECTED in the test, and the state is not
-                // important here. Verify that the state as it is, i.e. CONNECTING state.
-                new VpnProfileState(VpnProfileState.STATE_CONNECTING,
-                        sessionKey, false /* alwaysOn */, false /* lockdown */));
-    }
-
-    private void verifyAlwaysOnStateChanged(String[] packageName, VpnProfileState... profileState) {
-        verifyVpnManagerEvent(null /* sessionKey */,
-                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
-                -1 /* errorCode */, packageName, profileState);
-    }
-
-    @Test
-    public void testVpnManagerEventForUserDeactivated() throws Exception {
-        // For security reasons, Vpn#prepare() will check that oldPackage and newPackage are either
-        // null or the package of the caller. This test will call Vpn#prepare() to pretend the old
-        // VPN is replaced by a new one. But only Settings can change to some other packages, and
-        // this is checked with CONTROL_VPN so simulate holding CONTROL_VPN in order to pass the
-        // security checks.
-        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-
-        // Test the case that the user deactivates the vpn in vpn app.
-        final String sessionKey1 = vpn.startVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-        vpn.stopVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
-        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
-        reset(mDeviceIdleInternal);
-        verifyDeactivatedByUser(sessionKey1, new String[] {TEST_VPN_PKG});
-        reset(mAppOps);
-
-        // Test the case that the user chooses another vpn and the original one is replaced.
-        final String sessionKey2 = vpn.startVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-        vpn.prepare(TEST_VPN_PKG, "com.new.vpn" /* newPackage */, TYPE_VPN_PLATFORM);
-        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
-        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
-        reset(mDeviceIdleInternal);
-        verifyDeactivatedByUser(sessionKey2, new String[] {TEST_VPN_PKG});
-    }
-
-    @Test
-    public void testVpnManagerEventForAlwaysOnChanged() throws Exception {
-        // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN.
-        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        // Enable VPN always-on for PKGS[1].
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[1]);
-        reset(mDeviceIdleInternal);
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
-        // Enable VPN lockdown for PKGS[1].
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[1]);
-        reset(mDeviceIdleInternal);
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, true /* alwaysOn */, true /* lockdown */));
-
-        // Disable VPN lockdown for PKGS[1].
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[1]);
-        reset(mDeviceIdleInternal);
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
-        // Disable VPN always-on.
-        assertTrue(vpn.setAlwaysOnPackage(null, false /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[1]);
-        reset(mDeviceIdleInternal);
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, false /* alwaysOn */, false /* lockdown */));
-
-        // Enable VPN always-on for PKGS[1] again.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[1]);
-        reset(mDeviceIdleInternal);
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
-        // Enable VPN always-on for PKGS[2].
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[2], false /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[2]);
-        reset(mDeviceIdleInternal);
-        // PKGS[1] is replaced with PKGS[2].
-        // Pass 2 VpnProfileState objects to verifyVpnManagerEvent(), the first one is sent to
-        // PKGS[1] to notify PKGS[1] that the VPN always-on is disabled, the second one is sent to
-        // PKGS[2] to notify PKGS[2] that the VPN always-on is enabled.
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1], PKGS[2]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, false /* alwaysOn */, false /* lockdown */),
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-    }
-
-    @Test
-    public void testReconnectVpnManagerVpnWithAlwaysOnEnabled() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-        vpn.startVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-
-        // Enable VPN always-on for TEST_VPN_PKG.
-        assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
-                null /* lockdownAllowlist */));
-
-        // Reset to verify next startVpnProfile.
-        reset(mAppOps);
-
-        vpn.stopVpnProfile(TEST_VPN_PKG);
-
-        // Reconnect the vpn with different package will cause exception.
-        assertThrows(SecurityException.class, () -> vpn.startVpnProfile(PKGS[0]));
-
-        // Reconnect the vpn again with the vpn always on package w/o exception.
-        vpn.startVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-    }
-
-    @Test
-    public void testLockdown_enableDisableWhileConnected() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        final InOrder order = inOrder(mTestDeps);
-        order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
-                .newNetworkAgent(any(), any(), any(), any(), any(), any(),
-                        argThat(config -> config.allowBypass), any(), any());
-
-        // Make VPN lockdown.
-        assertTrue(vpnSnapShot.vpn.setAlwaysOnPackage(TEST_VPN_PKG, true /* lockdown */,
-                null /* lockdownAllowlist */));
-
-        order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
-                .newNetworkAgent(any(), any(), any(), any(), any(), any(),
-                argThat(config -> !config.allowBypass), any(), any());
-
-        // Disable lockdown.
-        assertTrue(vpnSnapShot.vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
-                null /* lockdownAllowlist */));
-
-        order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
-                .newNetworkAgent(any(), any(), any(), any(), any(), any(),
-                        argThat(config -> config.allowBypass), any(), any());
-    }
-
-    @Test
-    public void testSetPackageAuthorizationVpnService() throws Exception {
-        final Vpn vpn = createVpn();
-
-        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_SERVICE));
-        verify(mAppOps)
-                .setMode(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_VPN),
-                        eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                        eq(TEST_VPN_PKG),
-                        eq(AppOpsManager.MODE_ALLOWED));
-    }
-
-    @Test
-    public void testSetPackageAuthorizationPlatformVpn() throws Exception {
-        final Vpn vpn = createVpn();
-
-        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, TYPE_VPN_PLATFORM));
-        verify(mAppOps)
-                .setMode(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                        eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                        eq(TEST_VPN_PKG),
-                        eq(AppOpsManager.MODE_ALLOWED));
-    }
-
-    @Test
-    public void testSetPackageAuthorizationRevokeAuthorization() throws Exception {
-        final Vpn vpn = createVpn();
-
-        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_NONE));
-        verify(mAppOps)
-                .setMode(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_VPN),
-                        eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                        eq(TEST_VPN_PKG),
-                        eq(AppOpsManager.MODE_IGNORED));
-        verify(mAppOps)
-                .setMode(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                        eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                        eq(TEST_VPN_PKG),
-                        eq(AppOpsManager.MODE_IGNORED));
-    }
-
-    private NetworkCallback triggerOnAvailableAndGetCallback() throws Exception {
-        return triggerOnAvailableAndGetCallback(new NetworkCapabilities.Builder().build());
-    }
-
-    private NetworkCallback triggerOnAvailableAndGetCallback(
-            @NonNull final NetworkCapabilities caps) throws Exception {
-        final ArgumentCaptor<NetworkCallback> networkCallbackCaptor =
-                ArgumentCaptor.forClass(NetworkCallback.class);
-        verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
-                .registerSystemDefaultNetworkCallback(networkCallbackCaptor.capture(), any());
-
-        // onAvailable() will trigger onDefaultNetworkChanged(), so NetdUtils#setInterfaceUp will be
-        // invoked. Set the return value of INetd#interfaceGetCfg to prevent NullPointerException.
-        final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel();
-        config.flags = new String[] {IF_STATE_DOWN};
-        when(mNetd.interfaceGetCfg(anyString())).thenReturn(config);
-        final NetworkCallback cb = networkCallbackCaptor.getValue();
-        cb.onAvailable(TEST_NETWORK);
-        // Trigger onCapabilitiesChanged() and onLinkPropertiesChanged() so the test can verify that
-        // if NetworkCapabilities and LinkProperties of underlying network will be sent/cleared or
-        // not.
-        // See verifyVpnManagerEvent().
-        cb.onCapabilitiesChanged(TEST_NETWORK, caps);
-        cb.onLinkPropertiesChanged(TEST_NETWORK, new LinkProperties());
-        return cb;
-    }
-
-    private void verifyInterfaceSetCfgWithFlags(String flag) throws Exception {
-        // Add a timeout for waiting for interfaceSetCfg to be called.
-        verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetCfg(argThat(
-                config -> Arrays.asList(config.flags).contains(flag)));
-    }
-
-    private void doTestPlatformVpnWithException(IkeException exception,
-            String category, int errorType, int errorCode) throws Exception {
-        final ArgumentCaptor<IkeSessionCallback> captor =
-                ArgumentCaptor.forClass(IkeSessionCallback.class);
-
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-
-        doReturn(new NetworkCapabilities()).when(mConnectivityManager)
-                .getRedactedNetworkCapabilitiesForPackage(any(), anyInt(), anyString());
-        doReturn(new LinkProperties()).when(mConnectivityManager)
-                .getRedactedLinkPropertiesForPackage(any(), anyInt(), anyString());
-
-        final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
-        final NetworkCallback cb = triggerOnAvailableAndGetCallback();
-
-        verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
-        // Wait for createIkeSession() to be called before proceeding in order to ensure consistent
-        // state
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
-                .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
-        reset(mIkev2SessionCreator);
-        // For network lost case, the process should be triggered by calling onLost(), which is the
-        // same process with the real case.
-        if (errorCode == VpnManager.ERROR_CODE_NETWORK_LOST) {
-            cb.onLost(TEST_NETWORK);
-            verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-        } else {
-            final IkeSessionCallback ikeCb = captor.getValue();
-            ikeCb.onClosedWithException(exception);
-        }
-
-        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
-        reset(mDeviceIdleInternal);
-        verifyVpnManagerEvent(sessionKey, category, errorType, errorCode,
-                // VPN NetworkAgnet does not switch to CONNECTED in the test, and the state is not
-                // important here. Verify that the state as it is, i.e. CONNECTING state.
-                new String[] {TEST_VPN_PKG}, new VpnProfileState(VpnProfileState.STATE_CONNECTING,
-                        sessionKey, false /* alwaysOn */, false /* lockdown */));
-        if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
-            verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
-                    .unregisterNetworkCallback(eq(cb));
-        } else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE
-                // Vpn won't retry when there is no usable underlying network.
-                && errorCode != VpnManager.ERROR_CODE_NETWORK_LOST) {
-            int retryIndex = 0;
-            final IkeSessionCallback ikeCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
-
-            ikeCb2.onClosedWithException(exception);
-            verifyRetryAndGetNewIkeCb(retryIndex++);
-        }
-    }
-
-    private IkeSessionCallback verifyRetryAndGetNewIkeCb(int retryIndex) {
-        final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
-                ArgumentCaptor.forClass(IkeSessionCallback.class);
-
-        // Verify retry is scheduled
-        final long expectedDelayMs = mTestDeps.getNextRetryDelayMs(retryIndex);
-        final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
-        verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), delayCaptor.capture(),
-                eq(TimeUnit.MILLISECONDS));
-        final List<Long> delays = delayCaptor.getAllValues();
-        assertEquals(expectedDelayMs, (long) delays.get(delays.size() - 1));
-
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelayMs))
-                .createIkeSession(any(), any(), any(), any(), ikeCbCaptor.capture(), any());
-
-        // Forget the mIkev2SessionCreator#createIkeSession call and mExecutor#schedule call
-        // for the next retry verification
-        resetIkev2SessionCreator(mIkeSessionWrapper);
-
-        return ikeCbCaptor.getValue();
-    }
-
-    @Test
-    public void testStartPlatformVpnAuthenticationFailed() throws Exception {
-        final IkeProtocolException exception = mock(IkeProtocolException.class);
-        final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
-        when(exception.getErrorType()).thenReturn(errorCode);
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
-                errorCode);
-    }
-
-    @Test
-    public void testStartPlatformVpnFailedWithRecoverableError() throws Exception {
-        final IkeProtocolException exception = mock(IkeProtocolException.class);
-        final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
-        when(exception.getErrorType()).thenReturn(errorCode);
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode);
-    }
-
-    @Test
-    public void testStartPlatformVpnFailedWithUnknownHostException() throws Exception {
-        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
-        final UnknownHostException unknownHostException = new UnknownHostException();
-        final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST;
-        when(exception.getCause()).thenReturn(unknownHostException);
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
-                errorCode);
-    }
-
-    @Test
-    public void testStartPlatformVpnFailedWithIkeTimeoutException() throws Exception {
-        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
-        final IkeTimeoutException ikeTimeoutException =
-                new IkeTimeoutException("IkeTimeoutException");
-        final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT;
-        when(exception.getCause()).thenReturn(ikeTimeoutException);
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
-                errorCode);
-    }
-
-    @Test
-    public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception {
-        final IkeNetworkLostException exception = new IkeNetworkLostException(
-                new Network(100));
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
-                VpnManager.ERROR_CODE_NETWORK_LOST);
-    }
-
-    @Test
-    public void testStartPlatformVpnFailedWithIOException() throws Exception {
-        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
-        final IOException ioException = new IOException();
-        final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO;
-        when(exception.getCause()).thenReturn(ioException);
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
-                errorCode);
-    }
-
-    @Test
-    public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception {
-        when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
-                .thenThrow(new IllegalArgumentException());
-        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
-        final NetworkCallback cb = triggerOnAvailableAndGetCallback();
-
-        verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
-        // Wait for createIkeSession() to be called before proceeding in order to ensure consistent
-        // state
-        verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb));
-        assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state);
-    }
-
-    @Test
-    public void testVpnManagerEventWillNotBeSentToSettingsVpn() throws Exception {
-        startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
-        triggerOnAvailableAndGetCallback();
-
-        verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
-        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
-        final IkeTimeoutException ikeTimeoutException =
-                new IkeTimeoutException("IkeTimeoutException");
-        when(exception.getCause()).thenReturn(ikeTimeoutException);
-
-        final ArgumentCaptor<IkeSessionCallback> captor =
-                ArgumentCaptor.forClass(IkeSessionCallback.class);
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
-                .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
-        final IkeSessionCallback ikeCb = captor.getValue();
-        ikeCb.onClosedWithException(exception);
-
-        final Context userContext =
-                mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
-        verify(userContext, never()).startService(any());
-    }
-
-    private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) {
-        assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null));
-
-        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-        verify(mAppOps).setMode(
-                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), eq(uid), eq(TEST_VPN_PKG),
-                eq(AppOpsManager.MODE_ALLOWED));
-
-        verify(mSystemServices).settingsSecurePutStringForUser(
-                eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(PRIMARY_USER.id));
-        verify(mSystemServices).settingsSecurePutIntForUser(
-                eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0),
-                eq(PRIMARY_USER.id));
-        verify(mSystemServices).settingsSecurePutStringForUser(
-                eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(PRIMARY_USER.id));
-    }
-
-    @Test
-    public void testSetAndStartAlwaysOnVpn() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        setMockedUsers(PRIMARY_USER);
-
-        // UID checks must return a different UID; otherwise it'll be treated as already prepared.
-        final int uid = Process.myUid() + 1;
-        when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt()))
-                .thenReturn(uid);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-
-        setAndVerifyAlwaysOnPackage(vpn, uid, false);
-        assertTrue(vpn.startAlwaysOnVpn());
-
-        // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
-        // a subsequent CL.
-    }
-
-    private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
-        setMockedUsers(PRIMARY_USER);
-
-        // Dummy egress interface
-        final LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName(EGRESS_IFACE);
-
-        final RouteInfo defaultRoute = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
-                        InetAddresses.parseNumericAddress("192.0.2.0"), EGRESS_IFACE);
-        lp.addRoute(defaultRoute);
-
-        vpn.startLegacyVpn(vpnProfile, EGRESS_NETWORK, lp);
-        return vpn;
-    }
-
-    private IkeSessionConnectionInfo createIkeConnectInfo() {
-        return new IkeSessionConnectionInfo(TEST_VPN_CLIENT_IP, TEST_VPN_SERVER_IP, TEST_NETWORK);
-    }
-
-    private IkeSessionConnectionInfo createIkeConnectInfo_2() {
-        return new IkeSessionConnectionInfo(
-                TEST_VPN_CLIENT_IP_2, TEST_VPN_SERVER_IP_2, TEST_NETWORK_2);
-    }
-
-    private IkeSessionConfiguration createIkeConfig(
-            IkeSessionConnectionInfo ikeConnectInfo, boolean isMobikeEnabled) {
-        final IkeSessionConfiguration.Builder builder =
-                new IkeSessionConfiguration.Builder(ikeConnectInfo);
-
-        if (isMobikeEnabled) {
-            builder.addIkeExtension(EXTENSION_TYPE_MOBIKE);
-        }
-
-        return builder.build();
-    }
-
-    private ChildSessionConfiguration createChildConfig() {
-        return new ChildSessionConfiguration.Builder(
-                        Arrays.asList(IN_TS, IN_TS6), Arrays.asList(OUT_TS, OUT_TS6))
-                .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN))
-                .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN))
-                .addInternalDnsServer(TEST_VPN_INTERNAL_DNS)
-                .addInternalDnsServer(TEST_VPN_INTERNAL_DNS6)
-                .build();
-    }
-
-    private IpSecTransform createIpSecTransform() {
-        return new IpSecTransform(mContext, new IpSecConfig());
-    }
-
-    private void verifyApplyTunnelModeTransforms(int expectedTimes) throws Exception {
-        verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
-                eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_IN),
-                anyInt(), anyString());
-        verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
-                eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_OUT),
-                anyInt(), anyString());
-    }
-
-    private Pair<IkeSessionCallback, ChildSessionCallback> verifyCreateIkeAndCaptureCbs()
-            throws Exception {
-        final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
-                ArgumentCaptor.forClass(IkeSessionCallback.class);
-        final ArgumentCaptor<ChildSessionCallback> childCbCaptor =
-                ArgumentCaptor.forClass(ChildSessionCallback.class);
-
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)).createIkeSession(
-                any(), any(), any(), any(), ikeCbCaptor.capture(), childCbCaptor.capture());
-
-        return new Pair<>(ikeCbCaptor.getValue(), childCbCaptor.getValue());
-    }
-
-    private static class PlatformVpnSnapshot {
-        public final Vpn vpn;
-        public final NetworkCallback nwCb;
-        public final IkeSessionCallback ikeCb;
-        public final ChildSessionCallback childCb;
-
-        PlatformVpnSnapshot(Vpn vpn, NetworkCallback nwCb,
-                IkeSessionCallback ikeCb, ChildSessionCallback childCb) {
-            this.vpn = vpn;
-            this.nwCb = nwCb;
-            this.ikeCb = ikeCb;
-            this.childCb = childCb;
-        }
-    }
-
-    private PlatformVpnSnapshot verifySetupPlatformVpn(IkeSessionConfiguration ikeConfig)
-            throws Exception {
-        return verifySetupPlatformVpn(ikeConfig, true);
-    }
-
-    private PlatformVpnSnapshot verifySetupPlatformVpn(
-            IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception {
-        return verifySetupPlatformVpn(mVpnProfile, ikeConfig, mtuSupportsIpv6);
-    }
-
-    private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile,
-            IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception {
-        return verifySetupPlatformVpn(vpnProfile, ikeConfig,
-                new NetworkCapabilities.Builder().build() /* underlying network caps */,
-                mtuSupportsIpv6, false /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile,
-            IkeSessionConfiguration ikeConfig,
-            @NonNull final NetworkCapabilities underlyingNetworkCaps,
-            boolean mtuSupportsIpv6,
-            boolean areLongLivedTcpConnectionsExpensive) throws Exception {
-        if (!mtuSupportsIpv6) {
-            doReturn(IPV6_MIN_MTU - 1).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(),
-                    anyBoolean());
-        }
-
-        doReturn(mMockNetworkAgent).when(mTestDeps)
-                .newNetworkAgent(
-                        any(), any(), anyString(), any(), any(), any(), any(), any(), any());
-        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(vpnProfile.encode());
-
-        vpn.startVpnProfile(TEST_VPN_PKG);
-        final NetworkCallback nwCb = triggerOnAvailableAndGetCallback(underlyingNetworkCaps);
-        verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-        reset(mExecutor);
-
-        // Mock the setup procedure by firing callbacks
-        final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
-                verifyCreateIkeAndCaptureCbs();
-        final IkeSessionCallback ikeCb = cbPair.first;
-        final ChildSessionCallback childCb = cbPair.second;
-
-        ikeCb.onOpened(ikeConfig);
-        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
-        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
-        childCb.onOpened(createChildConfig());
-
-        // Verification VPN setup
-        verifyApplyTunnelModeTransforms(1);
-
-        ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
-        ArgumentCaptor<NetworkCapabilities> ncCaptor =
-                ArgumentCaptor.forClass(NetworkCapabilities.class);
-        ArgumentCaptor<NetworkAgentConfig> nacCaptor =
-                ArgumentCaptor.forClass(NetworkAgentConfig.class);
-        verify(mTestDeps).newNetworkAgent(
-                any(), any(), anyString(), ncCaptor.capture(), lpCaptor.capture(),
-                any(), nacCaptor.capture(), any(), any());
-        verify(mIkeSessionWrapper).setUnderpinnedNetwork(TEST_NETWORK);
-        // Check LinkProperties
-        final LinkProperties lp = lpCaptor.getValue();
-        final List<RouteInfo> expectedRoutes =
-                new ArrayList<>(
-                        Arrays.asList(
-                                new RouteInfo(
-                                        new IpPrefix(Inet4Address.ANY, 0),
-                                        null /* gateway */,
-                                        TEST_IFACE_NAME,
-                                        RouteInfo.RTN_UNICAST)));
-        final List<LinkAddress> expectedAddresses =
-                new ArrayList<>(
-                        Arrays.asList(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN)));
-        final List<InetAddress> expectedDns = new ArrayList<>(Arrays.asList(TEST_VPN_INTERNAL_DNS));
-
-        if (mtuSupportsIpv6) {
-            expectedRoutes.add(
-                    new RouteInfo(
-                            new IpPrefix(Inet6Address.ANY, 0),
-                            null /* gateway */,
-                            TEST_IFACE_NAME,
-                            RouteInfo.RTN_UNICAST));
-            expectedAddresses.add(new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN));
-            expectedDns.add(TEST_VPN_INTERNAL_DNS6);
-        } else {
-            expectedRoutes.add(
-                    new RouteInfo(
-                            new IpPrefix(Inet6Address.ANY, 0),
-                            null /* gateway */,
-                            TEST_IFACE_NAME,
-                            RTN_UNREACHABLE));
-        }
-
-        assertEquals(expectedRoutes, lp.getRoutes());
-        assertEquals(expectedAddresses, lp.getLinkAddresses());
-        assertEquals(expectedDns, lp.getDnsServers());
-
-        // Check NetworkCapabilities
-        assertEquals(Arrays.asList(TEST_NETWORK), ncCaptor.getValue().getUnderlyingNetworks());
-
-        // Check if allowBypass is set or not.
-        assertTrue(nacCaptor.getValue().isBypassableVpn());
-        final VpnTransportInfo info = (VpnTransportInfo) ncCaptor.getValue().getTransportInfo();
-        assertTrue(info.isBypassable());
-        assertEquals(areLongLivedTcpConnectionsExpensive,
-                info.areLongLivedTcpConnectionsExpensive());
-        return new PlatformVpnSnapshot(vpn, nwCb, ikeCb, childCb);
-    }
-
-    @Test
-    public void testStartPlatformVpn() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromIkeTunnConnParams_AutoTimerNoTimer() throws Exception {
-        doTestMigrateIkeSession_FromIkeTunnConnParams(
-                false /* isAutomaticIpVersionSelectionEnabled */,
-                true /* isAutomaticNattKeepaliveTimerEnabled */,
-                TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
-                ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
-                ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromIkeTunnConnParams_AutoTimerTimerSet() throws Exception {
-        doTestMigrateIkeSession_FromIkeTunnConnParams(
-                false /* isAutomaticIpVersionSelectionEnabled */,
-                true /* isAutomaticNattKeepaliveTimerEnabled */,
-                TEST_KEEPALIVE_TIMER /* keepaliveInProfile */,
-                ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
-                ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromIkeTunnConnParams_AutoIp() throws Exception {
-        doTestMigrateIkeSession_FromIkeTunnConnParams(
-                true /* isAutomaticIpVersionSelectionEnabled */,
-                false /* isAutomaticNattKeepaliveTimerEnabled */,
-                TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
-                ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
-                ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromIkeTunnConnParams_AssignedIpProtocol() throws Exception {
-        doTestMigrateIkeSession_FromIkeTunnConnParams(
-                false /* isAutomaticIpVersionSelectionEnabled */,
-                false /* isAutomaticNattKeepaliveTimerEnabled */,
-                TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
-                ESP_IP_VERSION_IPV4 /* ipVersionInProfile */,
-                ESP_ENCAP_TYPE_UDP /* encapTypeInProfile */);
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromNotIkeTunnConnParams_AutoTimer() throws Exception {
-        doTestMigrateIkeSession_FromNotIkeTunnConnParams(
-                false /* isAutomaticIpVersionSelectionEnabled */,
-                true /* isAutomaticNattKeepaliveTimerEnabled */);
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromNotIkeTunnConnParams_AutoIp() throws Exception {
-        doTestMigrateIkeSession_FromNotIkeTunnConnParams(
-                true /* isAutomaticIpVersionSelectionEnabled */,
-                false /* isAutomaticNattKeepaliveTimerEnabled */);
-    }
-
-    private void doTestMigrateIkeSession_FromNotIkeTunnConnParams(
-            boolean isAutomaticIpVersionSelectionEnabled,
-            boolean isAutomaticNattKeepaliveTimerEnabled) throws Exception {
-        final Ikev2VpnProfile ikeProfile =
-                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
-                        .setAuthPsk(TEST_VPN_PSK)
-                        .setBypassable(true /* isBypassable */)
-                        .setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled)
-                        .setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled)
-                        .build();
-
-        final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled
-                ? AUTOMATIC_KEEPALIVE_DELAY_SECONDS
-                : DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
-        doTestMigrateIkeSession(ikeProfile.toVpnProfile(),
-                expectedKeepalive,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                new NetworkCapabilities.Builder().build());
-    }
-
-    private Ikev2VpnProfile makeIkeV2VpnProfile(
-            boolean isAutomaticIpVersionSelectionEnabled,
-            boolean isAutomaticNattKeepaliveTimerEnabled,
-            int keepaliveInProfile,
-            int ipVersionInProfile,
-            int encapTypeInProfile) {
-        // TODO: Update helper function in IkeSessionTestUtils to support building IkeSessionParams
-        // with IP version and encap type when mainline-prod branch support these two APIs.
-        final IkeSessionParams params = getTestIkeSessionParams(true /* testIpv6 */,
-                new IkeFqdnIdentification(TEST_IDENTITY), keepaliveInProfile);
-        final IkeSessionParams ikeSessionParams = new IkeSessionParams.Builder(params)
-                .setIpVersion(ipVersionInProfile)
-                .setEncapType(encapTypeInProfile)
-                .build();
-
-        final IkeTunnelConnectionParams tunnelParams =
-                new IkeTunnelConnectionParams(ikeSessionParams, CHILD_PARAMS);
-        return new Ikev2VpnProfile.Builder(tunnelParams)
-                .setBypassable(true)
-                .setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled)
-                .setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled)
-                .build();
-    }
-
-    private void doTestMigrateIkeSession_FromIkeTunnConnParams(
-            boolean isAutomaticIpVersionSelectionEnabled,
-            boolean isAutomaticNattKeepaliveTimerEnabled,
-            int keepaliveInProfile,
-            int ipVersionInProfile,
-            int encapTypeInProfile) throws Exception {
-        doTestMigrateIkeSession_FromIkeTunnConnParams(isAutomaticIpVersionSelectionEnabled,
-                isAutomaticNattKeepaliveTimerEnabled, keepaliveInProfile, ipVersionInProfile,
-                encapTypeInProfile, new NetworkCapabilities.Builder().build());
-    }
-
-    private void doTestMigrateIkeSession_FromIkeTunnConnParams(
-            boolean isAutomaticIpVersionSelectionEnabled,
-            boolean isAutomaticNattKeepaliveTimerEnabled,
-            int keepaliveInProfile,
-            int ipVersionInProfile,
-            int encapTypeInProfile,
-            @NonNull final NetworkCapabilities nc) throws Exception {
-        final Ikev2VpnProfile ikeProfile = makeIkeV2VpnProfile(
-                isAutomaticIpVersionSelectionEnabled,
-                isAutomaticNattKeepaliveTimerEnabled,
-                keepaliveInProfile,
-                ipVersionInProfile,
-                encapTypeInProfile);
-
-        final IkeSessionParams ikeSessionParams =
-                ikeProfile.getIkeTunnelConnectionParams().getIkeSessionParams();
-        final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled
-                ? AUTOMATIC_KEEPALIVE_DELAY_SECONDS
-                : ikeSessionParams.getNattKeepAliveDelaySeconds();
-        final int expectedIpVersion = isAutomaticIpVersionSelectionEnabled
-                ? ESP_IP_VERSION_AUTO
-                : ikeSessionParams.getIpVersion();
-        final int expectedEncapType = isAutomaticIpVersionSelectionEnabled
-                ? ESP_ENCAP_TYPE_AUTO
-                : ikeSessionParams.getEncapType();
-        doTestMigrateIkeSession(ikeProfile.toVpnProfile(), expectedKeepalive,
-                expectedIpVersion, expectedEncapType, nc);
-    }
-
-    @Test
-    public void doTestMigrateIkeSession_Vcn() throws Exception {
-        final int expectedKeepalive = 2097; // Any unlikely number will do
-        final NetworkCapabilities vcnNc = new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .setTransportInfo(new VcnTransportInfo(TEST_SUB_ID, expectedKeepalive))
-                .build();
-        final Ikev2VpnProfile ikev2VpnProfile = makeIkeV2VpnProfile(
-                true /* isAutomaticIpVersionSelectionEnabled */,
-                true /* isAutomaticNattKeepaliveTimerEnabled */,
-                234 /* keepaliveInProfile */, // Should be ignored, any value will do
-                ESP_IP_VERSION_IPV4, // Should be ignored
-                ESP_ENCAP_TYPE_UDP // Should be ignored
-        );
-        doTestMigrateIkeSession(
-                ikev2VpnProfile.toVpnProfile(),
-                expectedKeepalive,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                vcnNc);
-    }
-
-    private void doTestMigrateIkeSession(
-            @NonNull final VpnProfile profile,
-            final int expectedKeepalive,
-            final int expectedIpVersion,
-            final int expectedEncapType,
-            @NonNull final NetworkCapabilities caps) throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(profile,
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
-                        caps /* underlying network capabilities */,
-                        false /* mtuSupportsIpv6 */,
-                        expectedKeepalive < DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC);
-        // Simulate a new network coming up
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, caps);
-        // Verify MOBIKE is triggered
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2,
-                expectedIpVersion, expectedEncapType, expectedKeepalive);
-
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    @Test
-    public void testLinkPropertiesUpdateTriggerReevaluation() throws Exception {
-        final boolean hasV6 = true;
-
-        mockCarrierConfig(TEST_SUB_ID, TelephonyManager.SIM_STATE_LOADED, TEST_KEEPALIVE_TIMER,
-                PREFERRED_IKE_PROTOCOL_IPV6_ESP);
-        final IkeSessionParams params = getTestIkeSessionParams(hasV6,
-                new IkeFqdnIdentification(TEST_IDENTITY), TEST_KEEPALIVE_TIMER);
-        final IkeTunnelConnectionParams tunnelParams =
-                new IkeTunnelConnectionParams(params, CHILD_PARAMS);
-        final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams)
-                .setBypassable(true)
-                .setAutomaticNattKeepaliveTimerEnabled(false)
-                .setAutomaticIpVersionSelectionEnabled(true)
-                .build();
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
-                        new NetworkCapabilities.Builder().build() /* underlying network caps */,
-                        hasV6 /* mtuSupportsIpv6 */,
-                        false /* areLongLivedTcpConnectionsExpensive */);
-        reset(mExecutor);
-
-        // Simulate a new network coming up
-        final LinkProperties lp = new LinkProperties();
-        lp.addLinkAddress(new LinkAddress("192.0.2.2/32"));
-
-        // Have the executor use the real delay to make sure schedule() was called only
-        // once for all calls. Also, arrange for execute() not to call schedule() to avoid
-        // messing with the checks for schedule().
-        mExecutor.delayMs = TestExecutor.REAL_DELAY;
-        mExecutor.executeDirect = true;
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(
-                TEST_NETWORK_2, new NetworkCapabilities.Builder().build());
-        vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
-        verify(mExecutor).schedule(any(Runnable.class), longThat(it -> it > 0), any());
-        reset(mExecutor);
-
-        final InOrder order = inOrder(mIkeSessionWrapper);
-
-        // Verify the network is started
-        order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
-                ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
-        // Send the same properties, check that no migration is scheduled
-        vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
-        verify(mExecutor, never()).schedule(any(Runnable.class), anyLong(), any());
-
-        // Add v6 address, verify MOBIKE is triggered
-        lp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
-        vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
-        order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
-                ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
-        // Add another v4 address, verify MOBIKE is triggered
-        final LinkProperties stacked = new LinkProperties();
-        stacked.setInterfaceName("v4-" + lp.getInterfaceName());
-        stacked.addLinkAddress(new LinkAddress("192.168.0.1/32"));
-        lp.addStackedLink(stacked);
-        vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
-        order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
-                ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    private void mockCarrierConfig(int subId, int simStatus, int keepaliveTimer, int ikeProtocol) {
-        final SubscriptionInfo subscriptionInfo = mock(SubscriptionInfo.class);
-        doReturn(subId).when(subscriptionInfo).getSubscriptionId();
-        doReturn(List.of(subscriptionInfo)).when(mSubscriptionManager)
-                .getActiveSubscriptionInfoList();
-
-        doReturn(simStatus).when(mTmPerSub).getSimApplicationState();
-        doReturn(TEST_MCCMNC).when(mTmPerSub).getSimOperator(subId);
-
-        final PersistableBundle persistableBundle = new PersistableBundle();
-        persistableBundle.putInt(KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT, keepaliveTimer);
-        persistableBundle.putInt(KEY_PREFERRED_IKE_PROTOCOL_INT, ikeProtocol);
-        // For CarrierConfigManager.isConfigForIdentifiedCarrier check
-        persistableBundle.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
-        doReturn(persistableBundle).when(mConfigManager).getConfigForSubId(subId);
-    }
-
-    private CarrierConfigManager.CarrierConfigChangeListener getCarrierConfigListener() {
-        final ArgumentCaptor<CarrierConfigManager.CarrierConfigChangeListener> listenerCaptor =
-                ArgumentCaptor.forClass(CarrierConfigManager.CarrierConfigChangeListener.class);
-
-        verify(mConfigManager).registerCarrierConfigChangeListener(any(), listenerCaptor.capture());
-
-        return listenerCaptor.getValue();
-    }
-
-    @Test
-    public void testNattKeepaliveTimerFromCarrierConfig_noSubId() throws Exception {
-        doTestReadCarrierConfig(new NetworkCapabilities(),
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_IPV4_UDP,
-                AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                false /* expectedReadFromCarrierConfig*/,
-                true /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testNattKeepaliveTimerFromCarrierConfig_simAbsent() throws Exception {
-        doTestReadCarrierConfig(new NetworkCapabilities.Builder().build(),
-                TelephonyManager.SIM_STATE_ABSENT,
-                PREFERRED_IKE_PROTOCOL_IPV4_UDP,
-                AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                false /* expectedReadFromCarrierConfig*/,
-                true /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testNattKeepaliveTimerFromCarrierConfig() throws Exception {
-        doTestReadCarrierConfig(createTestCellNc(),
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_AUTO,
-                TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                true /* expectedReadFromCarrierConfig*/,
-                false /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testNattKeepaliveTimerFromCarrierConfig_NotCell() throws Exception {
-        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_WIFI)
-                .setTransportInfo(new WifiInfo.Builder().build())
-                .build();
-        doTestReadCarrierConfig(nc,
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_IPV4_UDP,
-                AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                false /* expectedReadFromCarrierConfig*/,
-                true /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testPreferredIpProtocolFromCarrierConfig_v4UDP() throws Exception {
-        doTestReadCarrierConfig(createTestCellNc(),
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_IPV4_UDP,
-                TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_IPV4 /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_UDP /* expectedEncapType */,
-                true /* expectedReadFromCarrierConfig*/,
-                false /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testPreferredIpProtocolFromCarrierConfig_v6ESP() throws Exception {
-        doTestReadCarrierConfig(createTestCellNc(),
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_IPV6_ESP,
-                TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_IPV6 /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_NONE /* expectedEncapType */,
-                true /* expectedReadFromCarrierConfig*/,
-                false /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testPreferredIpProtocolFromCarrierConfig_v6UDP() throws Exception {
-        doTestReadCarrierConfig(createTestCellNc(),
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_IPV6_UDP,
-                TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_IPV6 /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_UDP /* expectedEncapType */,
-                true /* expectedReadFromCarrierConfig*/,
-                false /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    private NetworkCapabilities createTestCellNc() {
-        return new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
-                        .setSubscriptionId(TEST_SUB_ID)
-                        .build())
-                .build();
-    }
-
-    private void doTestReadCarrierConfig(NetworkCapabilities nc, int simState, int preferredIpProto,
-            int expectedKeepaliveTimer, int expectedIpVersion, int expectedEncapType,
-            boolean expectedReadFromCarrierConfig,
-            boolean areLongLivedTcpConnectionsExpensive)
-            throws Exception {
-        final Ikev2VpnProfile ikeProfile =
-                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
-                        .setAuthPsk(TEST_VPN_PSK)
-                        .setBypassable(true /* isBypassable */)
-                        .setAutomaticNattKeepaliveTimerEnabled(true)
-                        .setAutomaticIpVersionSelectionEnabled(true)
-                        .build();
-
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
-                        new NetworkCapabilities.Builder().build() /* underlying network caps */,
-                        false /* mtuSupportsIpv6 */,
-                        true /* areLongLivedTcpConnectionsExpensive */);
-
-        final CarrierConfigManager.CarrierConfigChangeListener listener =
-                getCarrierConfigListener();
-
-        // Simulate a new network coming up
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        // Migration will not be started until receiving network capabilities change.
-        verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
-        reset(mIkeSessionWrapper);
-        mockCarrierConfig(TEST_SUB_ID, simState, TEST_KEEPALIVE_TIMER, preferredIpProto);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, nc);
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2,
-                expectedIpVersion, expectedEncapType, expectedKeepaliveTimer);
-        if (expectedReadFromCarrierConfig) {
-            final ArgumentCaptor<NetworkCapabilities> ncCaptor =
-                    ArgumentCaptor.forClass(NetworkCapabilities.class);
-            verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
-
-            final VpnTransportInfo info =
-                    (VpnTransportInfo) ncCaptor.getValue().getTransportInfo();
-            assertEquals(areLongLivedTcpConnectionsExpensive,
-                    info.areLongLivedTcpConnectionsExpensive());
-        } else {
-            verify(mMockNetworkAgent, never()).doSendNetworkCapabilities(any());
-        }
-
-        reset(mExecutor);
-        reset(mIkeSessionWrapper);
-        reset(mMockNetworkAgent);
-
-        // Trigger carrier config change
-        listener.onCarrierConfigChanged(1 /* logicalSlotIndex */, TEST_SUB_ID,
-                -1 /* carrierId */, -1 /* specificCarrierId */);
-        verify(mIkeSessionWrapper).setNetwork(TEST_NETWORK_2,
-                expectedIpVersion, expectedEncapType, expectedKeepaliveTimer);
-        // Expect no NetworkCapabilities change.
-        // Call to doSendNetworkCapabilities() will not be triggered.
-        verify(mMockNetworkAgent, never()).doSendNetworkCapabilities(any());
-    }
-
-    @Test
-    public void testStartPlatformVpn_mtuDoesNotSupportIpv6() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
-                        false /* mtuSupportsIpv6 */);
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    @Test
-    public void testStartPlatformVpnMobility_mobikeEnabled() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        // Set new MTU on a different network
-        final int newMtu = IPV6_MIN_MTU + 1;
-        doReturn(newMtu).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
-
-        // Mock network loss and verify a cleanup task is scheduled
-        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
-        verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-
-        // Mock new network comes up and the cleanup task is cancelled
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
-                new NetworkCapabilities.Builder().build());
-        // Verify MOBIKE is triggered
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK_2),
-                eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
-                eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
-                eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
-
-        // Mock the MOBIKE procedure
-        vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
-        vpnSnapShot.childCb.onIpSecTransformsMigrated(
-                createIpSecTransform(), createIpSecTransform());
-
-        verify(mIpSecService).setNetworkForTunnelInterface(
-                eq(TEST_TUNNEL_RESOURCE_ID), eq(TEST_NETWORK_2), anyString());
-
-        // Expect 2 times: one for initial setup and one for MOBIKE
-        verifyApplyTunnelModeTransforms(2);
-
-        // Verify mNetworkCapabilities and mNetworkAgent are updated
-        assertEquals(
-                Collections.singletonList(TEST_NETWORK_2),
-                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-        verify(mMockNetworkAgent)
-                .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-        verify(mMockNetworkAgent).doSendLinkProperties(argThat(lp -> lp.getMtu() == newMtu));
-        verify(mMockNetworkAgent, never()).unregister();
-
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    @Test
-    public void testStartPlatformVpnMobility_mobikeEnabledMtuDoesNotSupportIpv6() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        // Set MTU below 1280
-        final int newMtu = IPV6_MIN_MTU - 1;
-        doReturn(newMtu).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
-
-        // Mock new network available & MOBIKE procedures
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
-        vpnSnapShot.childCb.onIpSecTransformsMigrated(
-                createIpSecTransform(), createIpSecTransform());
-
-        // Verify removal of IPv6 addresses and routes triggers a network agent restart
-        final ArgumentCaptor<LinkProperties> lpCaptor =
-                ArgumentCaptor.forClass(LinkProperties.class);
-        verify(mTestDeps, times(2))
-                .newNetworkAgent(any(), any(), anyString(), any(), lpCaptor.capture(), any(), any(),
-                        any(), any());
-        verify(mMockNetworkAgent).unregister();
-        // mMockNetworkAgent is an old NetworkAgent, so it won't update LinkProperties after
-        // unregistering.
-        verify(mMockNetworkAgent, never()).doSendLinkProperties(any());
-
-        final LinkProperties lp = lpCaptor.getValue();
-
-        for (LinkAddress addr : lp.getLinkAddresses()) {
-            if (addr.isIpv6()) {
-                fail("IPv6 address found on VPN with MTU < IPv6 minimum MTU");
-            }
-        }
-
-        for (InetAddress dnsAddr : lp.getDnsServers()) {
-            if (dnsAddr instanceof Inet6Address) {
-                fail("IPv6 DNS server found on VPN with MTU < IPv6 minimum MTU");
-            }
-        }
-
-        for (RouteInfo routeInfo : lp.getRoutes()) {
-            if (routeInfo.getDestinationLinkAddress().isIpv6()
-                    && !routeInfo.isIPv6UnreachableDefault()) {
-                fail("IPv6 route found on VPN with MTU < IPv6 minimum MTU");
-            }
-        }
-
-        assertEquals(newMtu, lp.getMtu());
-
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    @Test
-    public void testStartPlatformVpnReestablishes_mobikeDisabled() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-
-        // Forget the first IKE creation to be prepared to capture callbacks of the second
-        // IKE session
-        resetIkev2SessionCreator(mock(Vpn.IkeSessionWrapper.class));
-
-        // Mock network switch
-        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        // The old IKE Session will not be killed until receiving network capabilities change.
-        verify(mIkeSessionWrapper, never()).kill();
-
-        vpnSnapShot.nwCb.onCapabilitiesChanged(
-                TEST_NETWORK_2, new NetworkCapabilities.Builder().build());
-        // Verify the old IKE Session is killed
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).kill();
-
-        // Capture callbacks of the new IKE Session
-        final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
-                verifyCreateIkeAndCaptureCbs();
-        final IkeSessionCallback ikeCb = cbPair.first;
-        final ChildSessionCallback childCb = cbPair.second;
-
-        // Mock the IKE Session setup
-        ikeCb.onOpened(createIkeConfig(createIkeConnectInfo_2(), false /* isMobikeEnabled */));
-
-        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
-        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
-        childCb.onOpened(createChildConfig());
-
-        // Expect 2 times since there have been two Session setups
-        verifyApplyTunnelModeTransforms(2);
-
-        // Verify mNetworkCapabilities and mNetworkAgent are updated
-        assertEquals(
-                Collections.singletonList(TEST_NETWORK_2),
-                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-        verify(mMockNetworkAgent)
-                .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    private String getDump(@NonNull final Vpn vpn) {
-        final StringWriter sw = new StringWriter();
-        final IndentingPrintWriter writer = new IndentingPrintWriter(sw, "");
-        vpn.dump(writer);
-        writer.flush();
-        return sw.toString();
-    }
-
-    private int countMatches(@NonNull final Pattern regexp, @NonNull final String string) {
-        final Matcher m = regexp.matcher(string);
-        int i = 0;
-        while (m.find()) ++i;
-        return i;
-    }
-
-    @Test
-    public void testNCEventChanges() throws Exception {
-        final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .addCapability(NET_CAPABILITY_INTERNET)
-                .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
-                .setLinkDownstreamBandwidthKbps(1000)
-                .setLinkUpstreamBandwidthKbps(500);
-
-        final Ikev2VpnProfile ikeProfile =
-                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
-                        .setAuthPsk(TEST_VPN_PSK)
-                        .setBypassable(true /* isBypassable */)
-                        .setAutomaticNattKeepaliveTimerEnabled(true)
-                        .setAutomaticIpVersionSelectionEnabled(true)
-                        .build();
-
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
-                        ncBuilder.build(), false /* mtuSupportsIpv6 */,
-                        true /* areLongLivedTcpConnectionsExpensive */);
-
-        // Calls to onCapabilitiesChanged will be thrown to the executor for execution ; by
-        // default this will incur a 10ms delay before it's executed, messing with the timing
-        // of the log and having the checks for counts in equals() below flake.
-        mExecutor.executeDirect = true;
-
-        // First nc changed triggered by verifySetupPlatformVpn
-        final Pattern pattern = Pattern.compile("Cap changed from", Pattern.MULTILINE);
-        final String stage1 = getDump(vpnSnapShot.vpn);
-        assertEquals(1, countMatches(pattern, stage1));
-
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
-        final String stage2 = getDump(vpnSnapShot.vpn);
-        // Was the same caps, there should still be only 1 match
-        assertEquals(1, countMatches(pattern, stage2));
-
-        ncBuilder.setLinkDownstreamBandwidthKbps(1200)
-                .setLinkUpstreamBandwidthKbps(300);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
-        final String stage3 = getDump(vpnSnapShot.vpn);
-        // Was not an important change, should not be logged, still only 1 match
-        assertEquals(1, countMatches(pattern, stage3));
-
-        ncBuilder.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
-        final String stage4 = getDump(vpnSnapShot.vpn);
-        // Change to caps is important, should cause a new match
-        assertEquals(2, countMatches(pattern, stage4));
-
-        ncBuilder.removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
-        ncBuilder.setLinkDownstreamBandwidthKbps(600);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
-        final String stage5 = getDump(vpnSnapShot.vpn);
-        // Change to caps is important, should cause a new match even with the unimportant change
-        assertEquals(3, countMatches(pattern, stage5));
-    }
-    // TODO : beef up event logs tests
-
-    private void verifyHandlingNetworkLoss(PlatformVpnSnapshot vpnSnapShot) throws Exception {
-        // Forget the #sendLinkProperties during first setup.
-        reset(mMockNetworkAgent);
-
-        // Mock network loss
-        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
-
-        // Mock the grace period expires
-        verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-
-        final ArgumentCaptor<LinkProperties> lpCaptor =
-                ArgumentCaptor.forClass(LinkProperties.class);
-        verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
-                .doSendLinkProperties(lpCaptor.capture());
-        final LinkProperties lp = lpCaptor.getValue();
-
-        assertNull(lp.getInterfaceName());
-        final List<RouteInfo> expectedRoutes = Arrays.asList(
-                new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /* gateway */,
-                        null /* iface */, RTN_UNREACHABLE),
-                new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /* gateway */,
-                        null /* iface */, RTN_UNREACHABLE));
-        assertEquals(expectedRoutes, lp.getRoutes());
-
-        verify(mMockNetworkAgent).unregister();
-    }
-
-    @Test
-    public void testStartPlatformVpnHandlesNetworkLoss_mobikeEnabled() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-        verifyHandlingNetworkLoss(vpnSnapShot);
-    }
-
-    @Test
-    public void testStartPlatformVpnHandlesNetworkLoss_mobikeDisabled() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-        verifyHandlingNetworkLoss(vpnSnapShot);
-    }
-
-    private ConnectivityDiagnosticsCallback getConnectivityDiagCallback() {
-        final ArgumentCaptor<ConnectivityDiagnosticsCallback> cdcCaptor =
-                ArgumentCaptor.forClass(ConnectivityDiagnosticsCallback.class);
-        verify(mCdm).registerConnectivityDiagnosticsCallback(
-                any(), any(), cdcCaptor.capture());
-        return cdcCaptor.getValue();
-    }
-
-    private DataStallReport createDataStallReport() {
-        return new DataStallReport(TEST_NETWORK, 1234 /* reportTimestamp */,
-                1 /* detectionMethod */, new LinkProperties(), new NetworkCapabilities(),
-                new PersistableBundle());
-    }
-
-    private void verifyMobikeTriggered(List<Network> expected) {
-        final ArgumentCaptor<Network> networkCaptor = ArgumentCaptor.forClass(Network.class);
-        verify(mIkeSessionWrapper).setNetwork(networkCaptor.capture(),
-                anyInt() /* ipVersion */, anyInt() /* encapType */, anyInt() /* keepaliveDelay */);
-        assertEquals(expected, Collections.singletonList(networkCaptor.getValue()));
-    }
-
-    @Test
-    public void testDataStallInIkev2VpnMobikeDisabled() throws Exception {
-        verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-
-        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-        final ConnectivityDiagnosticsCallback connectivityDiagCallback =
-                getConnectivityDiagCallback();
-        final DataStallReport report = createDataStallReport();
-        connectivityDiagCallback.onDataStallSuspected(report);
-
-        // Should not trigger MOBIKE if MOBIKE is not enabled
-        verify(mIkeSessionWrapper, never()).setNetwork(any() /* network */,
-                anyInt() /* ipVersion */, anyInt() /* encapType */, anyInt() /* keepaliveDelay */);
-    }
-
-    @Test
-    public void testDataStallInIkev2VpnRecoveredByMobike() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-        final ConnectivityDiagnosticsCallback connectivityDiagCallback =
-                getConnectivityDiagCallback();
-        final DataStallReport report = createDataStallReport();
-        connectivityDiagCallback.onDataStallSuspected(report);
-
-        // Verify MOBIKE is triggered
-        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-
-        // Expect to skip other data stall event if MOBIKE was started.
-        reset(mIkeSessionWrapper);
-        connectivityDiagCallback.onDataStallSuspected(report);
-        verify(mIkeSessionWrapper, never()).setNetwork(any() /* network */,
-                anyInt() /* ipVersion */, anyInt() /* encapType */, anyInt() /* keepaliveDelay */);
-
-        reset(mIkev2SessionCreator);
-
-        // Send validation status update.
-        // Recovered and get network validated. It should not trigger the ike session reset.
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_VALID);
-        verify(mIkev2SessionCreator, never()).createIkeSession(
-                any(), any(), any(), any(), any(), any());
-
-        // Send invalid result to verify no ike session reset since the data stall suspected
-        // variables(timer counter and boolean) was reset.
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-        verify(mIkev2SessionCreator, never()).createIkeSession(
-                any(), any(), any(), any(), any(), any());
-    }
-
-    @Test
-    public void testDataStallInIkev2VpnNotRecoveredByMobike() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        final ConnectivityDiagnosticsCallback connectivityDiagCallback =
-                getConnectivityDiagCallback();
-
-        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-        final DataStallReport report = createDataStallReport();
-        connectivityDiagCallback.onDataStallSuspected(report);
-
-        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-
-        reset(mIkev2SessionCreator);
-
-        // Send validation status update should result in ike session reset.
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-
-        // Verify reset is scheduled and run.
-        verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-
-        // Another invalid status reported should not trigger other scheduled recovery.
-        reset(mExecutor);
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        verify(mExecutor, never()).schedule(any(Runnable.class), anyLong(), any());
-
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
-                .createIkeSession(any(), any(), any(), any(), any(), any());
-    }
-
-    @Test
-    public void testStartRacoonNumericAddress() throws Exception {
-        startRacoon("1.2.3.4", "1.2.3.4");
-    }
-
-    @Test
-    public void testStartRacoonHostname() throws Exception {
-        startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve
-    }
-
-    @Test
-    public void testStartPptp() throws Exception {
-        startPptp(true /* useMppe */);
-    }
-
-    @Test
-    public void testStartPptp_NoMppe() throws Exception {
-        startPptp(false /* useMppe */);
-    }
-
-    private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
-        assertNotNull(nc);
-        VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo();
-        assertNotNull(ti);
-        assertEquals(type, ti.getType());
-    }
-
-    private void startPptp(boolean useMppe) throws Exception {
-        final VpnProfile profile = new VpnProfile("testProfile" /* key */);
-        profile.type = VpnProfile.TYPE_PPTP;
-        profile.name = "testProfileName";
-        profile.username = "userName";
-        profile.password = "thePassword";
-        profile.server = "192.0.2.123";
-        profile.mppe = useMppe;
-
-        doReturn(new Network[] { new Network(101) }).when(mConnectivityManager).getAllNetworks();
-        doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(any(), any(),
-                any(), any(), any(), any(), anyInt());
-
-        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
-        final TestDeps deps = (TestDeps) vpn.mDeps;
-
-        testAndCleanup(() -> {
-            final String[] mtpdArgs = deps.mtpdArgs.get(10, TimeUnit.SECONDS);
-            final String[] argsPrefix = new String[]{
-                    EGRESS_IFACE, "pptp", profile.server, "1723", "name", profile.username,
-                    "password", profile.password, "linkname", "vpn", "refuse-eap", "nodefaultroute",
-                    "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270"
-            };
-            assertArrayEquals(argsPrefix, Arrays.copyOf(mtpdArgs, argsPrefix.length));
-            if (useMppe) {
-                assertEquals(argsPrefix.length + 2, mtpdArgs.length);
-                assertEquals("+mppe", mtpdArgs[argsPrefix.length]);
-                assertEquals("-pap", mtpdArgs[argsPrefix.length + 1]);
-            } else {
-                assertEquals(argsPrefix.length + 1, mtpdArgs.length);
-                assertEquals("nomppe", mtpdArgs[argsPrefix.length]);
-            }
-
-            verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
-                    any(), any(), any(), any(), anyInt());
-        }, () -> { // Cleanup
-                vpn.mVpnRunner.exitVpnRunner();
-                deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
-                vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
-            });
-    }
-
-    public void startRacoon(final String serverAddr, final String expectedAddr)
-            throws Exception {
-        final ConditionVariable legacyRunnerReady = new ConditionVariable();
-        final VpnProfile profile = new VpnProfile("testProfile" /* key */);
-        profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
-        profile.name = "testProfileName";
-        profile.username = "userName";
-        profile.password = "thePassword";
-        profile.server = serverAddr;
-        profile.ipsecIdentifier = "id";
-        profile.ipsecSecret = "secret";
-        profile.l2tpSecret = "l2tpsecret";
-
-        when(mConnectivityManager.getAllNetworks())
-            .thenReturn(new Network[] { new Network(101) });
-
-        when(mConnectivityManager.registerNetworkAgent(any(), any(), any(), any(),
-                any(), any(), anyInt())).thenAnswer(invocation -> {
-                    // The runner has registered an agent and is now ready.
-                    legacyRunnerReady.open();
-                    return new Network(102);
-                });
-        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
-        final TestDeps deps = (TestDeps) vpn.mDeps;
-        try {
-            // udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK
-            assertArrayEquals(
-                    new String[] { EGRESS_IFACE, expectedAddr, "udppsk",
-                            profile.ipsecIdentifier, profile.ipsecSecret, "1701" },
-                    deps.racoonArgs.get(10, TimeUnit.SECONDS));
-            // literal values are hardcoded in Vpn.java for mtpd args
-            assertArrayEquals(
-                    new String[] { EGRESS_IFACE, "l2tp", expectedAddr, "1701", profile.l2tpSecret,
-                            "name", profile.username, "password", profile.password,
-                            "linkname", "vpn", "refuse-eap", "nodefaultroute", "usepeerdns",
-                            "idle", "1800", "mtu", "1270", "mru", "1270" },
-                    deps.mtpdArgs.get(10, TimeUnit.SECONDS));
-
-            // Now wait for the runner to be ready before testing for the route.
-            ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
-            ArgumentCaptor<NetworkCapabilities> ncCaptor =
-                    ArgumentCaptor.forClass(NetworkCapabilities.class);
-            verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
-                    lpCaptor.capture(), ncCaptor.capture(), any(), any(), anyInt());
-
-            // In this test the expected address is always v4 so /32.
-            // Note that the interface needs to be specified because RouteInfo objects stored in
-            // LinkProperties objects always acquire the LinkProperties' interface.
-            final RouteInfo expectedRoute = new RouteInfo(new IpPrefix(expectedAddr + "/32"),
-                    null, EGRESS_IFACE, RouteInfo.RTN_THROW);
-            final List<RouteInfo> actualRoutes = lpCaptor.getValue().getRoutes();
-            assertTrue("Expected throw route (" + expectedRoute + ") not found in " + actualRoutes,
-                    actualRoutes.contains(expectedRoute));
-
-            assertTransportInfoMatches(ncCaptor.getValue(), VpnManager.TYPE_VPN_LEGACY);
-        } finally {
-            // Now interrupt the thread, unblock the runner and clean up.
-            vpn.mVpnRunner.exitVpnRunner();
-            deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
-            vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
-        }
-    }
-
-    // Make it public and un-final so as to spy it
-    public class TestDeps extends Vpn.Dependencies {
-        public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
-        public final CompletableFuture<String[]> mtpdArgs = new CompletableFuture();
-        public final File mStateFile;
-
-        private final HashMap<String, Boolean> mRunningServices = new HashMap<>();
-
-        TestDeps() {
-            try {
-                mStateFile = File.createTempFile("vpnTest", ".tmp");
-                mStateFile.deleteOnExit();
-            } catch (final IOException e) {
-                throw new RuntimeException(e);
-            }
-        }
-
-        @Override
-        public boolean isCallerSystem() {
-            return true;
-        }
-
-        @Override
-        public void startService(final String serviceName) {
-            mRunningServices.put(serviceName, true);
-        }
-
-        @Override
-        public void stopService(final String serviceName) {
-            mRunningServices.put(serviceName, false);
-        }
-
-        @Override
-        public boolean isServiceRunning(final String serviceName) {
-            return mRunningServices.getOrDefault(serviceName, false);
-        }
-
-        @Override
-        public boolean isServiceStopped(final String serviceName) {
-            return !isServiceRunning(serviceName);
-        }
-
-        @Override
-        public File getStateFile() {
-            return mStateFile;
-        }
-
-        @Override
-        public PendingIntent getIntentForStatusPanel(Context context) {
-            return null;
-        }
-
-        @Override
-        public void sendArgumentsToDaemon(
-                final String daemon, final LocalSocket socket, final String[] arguments,
-                final Vpn.RetryScheduler interruptChecker) throws IOException {
-            if ("racoon".equals(daemon)) {
-                racoonArgs.complete(arguments);
-            } else if ("mtpd".equals(daemon)) {
-                writeStateFile(arguments);
-                mtpdArgs.complete(arguments);
-            } else {
-                throw new UnsupportedOperationException("Unsupported daemon : " + daemon);
-            }
-        }
-
-        private void writeStateFile(final String[] arguments) throws IOException {
-            mStateFile.delete();
-            mStateFile.createNewFile();
-            mStateFile.deleteOnExit();
-            final BufferedWriter writer = new BufferedWriter(
-                    new FileWriter(mStateFile, false /* append */));
-            writer.write(EGRESS_IFACE);
-            writer.write("\n");
-            // addresses
-            writer.write("10.0.0.1/24\n");
-            // routes
-            writer.write("192.168.6.0/24\n");
-            // dns servers
-            writer.write("192.168.6.1\n");
-            // search domains
-            writer.write("vpn.searchdomains.com\n");
-            // endpoint - intentionally empty
-            writer.write("\n");
-            writer.flush();
-            writer.close();
-        }
-
-        @Override
-        @NonNull
-        public InetAddress resolve(final String endpoint) {
-            try {
-                // If a numeric IP address, return it.
-                return InetAddress.parseNumericAddress(endpoint);
-            } catch (IllegalArgumentException e) {
-                // Otherwise, return some token IP to test for.
-                return InetAddress.parseNumericAddress("5.6.7.8");
-            }
-        }
-
-        @Override
-        public boolean isInterfacePresent(final Vpn vpn, final String iface) {
-            return true;
-        }
-
-        @Override
-        public ParcelFileDescriptor adoptFd(Vpn vpn, int mtu) {
-            return new ParcelFileDescriptor(new FileDescriptor());
-        }
-
-        @Override
-        public int jniCreate(Vpn vpn, int mtu) {
-            // Pick a random positive number as fd to return.
-            return 345;
-        }
-
-        @Override
-        public String jniGetName(Vpn vpn, int fd) {
-            return TEST_IFACE_NAME;
-        }
-
-        @Override
-        public int jniSetAddresses(Vpn vpn, String interfaze, String addresses) {
-            if (addresses == null) return 0;
-            // Return the number of addresses.
-            return addresses.split(" ").length;
-        }
-
-        @Override
-        public void setBlocking(FileDescriptor fd, boolean blocking) {}
-
-        @Override
-        public DeviceIdleInternal getDeviceIdleInternal() {
-            return mDeviceIdleInternal;
-        }
-
-        @Override
-        public long getNextRetryDelayMs(int retryCount) {
-            // Simply return retryCount as the delay seconds for retrying.
-            return retryCount * 1000;
-        }
-
-        @Override
-        public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor() {
-            return mExecutor;
-        }
-
-        public boolean mIgnoreCallingUidChecks = true;
-        @Override
-        public void verifyCallingUidAndPackage(Context context, String packageName, int userId) {
-            if (!mIgnoreCallingUidChecks) {
-                super.verifyCallingUidAndPackage(context, packageName, userId);
-            }
-        }
-    }
-
-    /**
-     * Mock some methods of vpn object.
-     */
-    private Vpn createVpn(@UserIdInt int userId) {
-        final Context asUserContext = mock(Context.class, AdditionalAnswers.delegatesTo(mContext));
-        doReturn(UserHandle.of(userId)).when(asUserContext).getUser();
-        when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt()))
-                .thenReturn(asUserContext);
-        final TestLooper testLooper = new TestLooper();
-        final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, mTestDeps, mNetService,
-                mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator);
-        verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat(
-                provider -> provider.getName().contains("VpnNetworkProvider")
-        ));
-        return vpn;
-    }
-
-    /**
-     * Populate {@link #mUserManager} with a list of fake users.
-     */
-    private void setMockedUsers(UserInfo... users) {
-        final Map<Integer, UserInfo> userMap = new ArrayMap<>();
-        for (UserInfo user : users) {
-            userMap.put(user.id, user);
-        }
-
-        /**
-         * @see UserManagerService#getUsers(boolean)
-         */
-        doAnswer(invocation -> {
-            final ArrayList<UserInfo> result = new ArrayList<>(users.length);
-            for (UserInfo ui : users) {
-                if (ui.isEnabled() && !ui.partial) {
-                    result.add(ui);
-                }
-            }
-            return result;
-        }).when(mUserManager).getAliveUsers();
-
-        doAnswer(invocation -> {
-            final int id = (int) invocation.getArguments()[0];
-            return userMap.get(id);
-        }).when(mUserManager).getUserInfo(anyInt());
-    }
-
-    /**
-     * Populate {@link #mPackageManager} with a fake packageName-to-UID mapping.
-     */
-    private void setMockedPackages(final Map<String, Integer> packages) {
-        try {
-            doAnswer(invocation -> {
-                final String appName = (String) invocation.getArguments()[0];
-                final int userId = (int) invocation.getArguments()[1];
-                Integer appId = packages.get(appName);
-                if (appId == null) throw new PackageManager.NameNotFoundException(appName);
-                return UserHandle.getUid(userId, appId);
-            }).when(mPackageManager).getPackageUidAsUser(anyString(), anyInt());
-        } catch (Exception e) {
-        }
-    }
-
-    private void setMockedNetworks(final Map<Network, NetworkCapabilities> networks) {
-        doAnswer(invocation -> {
-            final Network network = (Network) invocation.getArguments()[0];
-            return networks.get(network);
-        }).when(mConnectivityManager).getNetworkCapabilities(any());
-    }
-
-    // Need multiple copies of this, but Java's Stream objects can't be reused or
-    // duplicated.
-    private Stream<String> publicIpV4Routes() {
-        return Stream.of(
-                "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4",
-                "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6",
-                "172.0.0.0/12", "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9",
-                "173.0.0.0/8", "174.0.0.0/7", "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11",
-                "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15", "192.172.0.0/14",
-                "192.176.0.0/12", "192.192.0.0/10", "193.0.0.0/8", "194.0.0.0/7",
-                "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4");
-    }
-
-    private Stream<String> publicIpV6Routes() {
-        return Stream.of(
-                "::/1", "8000::/2", "c000::/3", "e000::/4", "f000::/5", "f800::/6",
-                "fe00::/8", "2605:ef80:e:af1d::/64");
-    }
-}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
index 8fb7be1..bb59e0d 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
@@ -31,6 +31,7 @@
 import android.net.Network;
 import android.net.NetworkRequest;
 
+import com.android.net.module.util.SharedLog;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -49,6 +50,7 @@
     @Mock private Context mContext;
     @Mock private ConnectivityMonitor.Listener mockListener;
     @Mock private ConnectivityManager mConnectivityManager;
+    @Mock private SharedLog sharedLog;
 
     private ConnectivityMonitorWithConnectivityManager monitor;
 
@@ -57,7 +59,7 @@
         MockitoAnnotations.initMocks(this);
         doReturn(mConnectivityManager).when(mContext)
                 .getSystemService(Context.CONNECTIVITY_SERVICE);
-        monitor = new ConnectivityMonitorWithConnectivityManager(mContext, mockListener);
+        monitor = new ConnectivityMonitorWithConnectivityManager(mContext, mockListener, sharedLog);
     }
 
     @Test
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 b539fe0..df48f6c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -16,22 +16,34 @@
 
 package com.android.server.connectivity.mdns
 
+import android.content.Context
+import android.content.res.Resources
 import android.net.InetAddresses.parseNumericAddress
 import android.net.LinkAddress
 import android.net.Network
+import android.net.nsd.NsdManager
 import android.net.nsd.NsdServiceInfo
+import android.net.nsd.OffloadEngine
+import android.net.nsd.OffloadServiceInfo
 import android.os.Build
 import android.os.Handler
 import android.os.HandlerThread
+import com.android.connectivity.resources.R
 import com.android.net.module.util.SharedLog
+import com.android.server.connectivity.ConnectivityResources
 import com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserCallback
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
 import com.android.server.connectivity.mdns.MdnsSocketProvider.SocketCallback
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.waitForIdle
 import java.net.NetworkInterface
+import java.time.Duration
 import java.util.Objects
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
 import org.junit.After
+import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,15 +61,27 @@
 
 private const val SERVICE_ID_1 = 1
 private const val SERVICE_ID_2 = 2
-private const val LONG_SERVICE_ID_1 = 3
-private const val LONG_SERVICE_ID_2 = 4
+private const val SERVICE_ID_3 = 3
+private const val LONG_SERVICE_ID_1 = 4
+private const val LONG_SERVICE_ID_2 = 5
+private const val CASE_INSENSITIVE_TEST_SERVICE_ID = 6
 private const val TIMEOUT_MS = 10_000L
 private val TEST_ADDR = parseNumericAddress("2001:db8::123")
+private val TEST_ADDR2 = parseNumericAddress("2001:db8::124")
 private val TEST_LINKADDR = LinkAddress(TEST_ADDR, 64 /* prefixLength */)
+private val TEST_LINKADDR2 = LinkAddress(TEST_ADDR2, 64 /* prefixLength */)
 private val TEST_NETWORK_1 = mock(Network::class.java)
-private val TEST_NETWORK_2 = mock(Network::class.java)
+private val TEST_SOCKETKEY_1 = SocketKey(1001 /* interfaceIndex */)
+private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */)
 private val TEST_HOSTNAME = arrayOf("Android_test", "local")
 private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
+private val TEST_INTERFACE1 = "test_iface1"
+private val TEST_INTERFACE2 = "test_iface2"
+private val TEST_CLIENT_UID_1 = 10010
+private val TEST_OFFLOAD_PACKET1 = byteArrayOf(0x01, 0x02, 0x03)
+private val TEST_OFFLOAD_PACKET2 = byteArrayOf(0x02, 0x03, 0x04)
+private val DEFAULT_ADVERTISING_OPTION = MdnsAdvertisingOptions.getDefaultOptions()
 
 private val SERVICE_1 = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
     port = 12345
@@ -65,6 +89,13 @@
     network = TEST_NETWORK_1
 }
 
+private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
+    subtypes = setOf(TEST_SUBTYPE)
+    port = 12345
+    hostAddresses = listOf(TEST_ADDR)
+    network = TEST_NETWORK_1
+}
+
 private val LONG_SERVICE_1 =
     NsdServiceInfo("a".repeat(48) + "TestServiceName", "_longadvertisertest._tcp").apply {
     port = 12345
@@ -78,6 +109,21 @@
     network = null
 }
 
+private val ALL_NETWORKS_SERVICE_SUBTYPE =
+        NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
+    subtypes = setOf(TEST_SUBTYPE)
+    port = 12345
+    hostAddresses = listOf(TEST_ADDR)
+    network = null
+}
+
+private val ALL_NETWORKS_SERVICE_2 =
+    NsdServiceInfo("TESTSERVICENAME", "_ADVERTISERTEST._tcp").apply {
+        port = 12345
+        hostAddresses = listOf(TEST_ADDR)
+        network = null
+    }
+
 private val LONG_ALL_NETWORKS_SERVICE =
     NsdServiceInfo("a".repeat(48) + "TestServiceName", "_longadvertisertest._tcp").apply {
         port = 12345
@@ -85,6 +131,39 @@
         network = null
     }
 
+private val OFFLOAD_SERVICEINFO = OffloadServiceInfo(
+    OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
+    listOf(TEST_SUBTYPE),
+    "Android_test.local",
+    TEST_OFFLOAD_PACKET1,
+    0, /* priority */
+    OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+)
+
+private val OFFLOAD_SERVICEINFO_NO_SUBTYPE = OffloadServiceInfo(
+    OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
+    listOf(),
+    "Android_test.local",
+    TEST_OFFLOAD_PACKET1,
+    0, /* priority */
+    OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+)
+
+private val OFFLOAD_SERVICEINFO_NO_SUBTYPE2 = OffloadServiceInfo(
+    OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
+    listOf(),
+    "Android_test.local",
+    TEST_OFFLOAD_PACKET2,
+    0, /* priority */
+    OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+)
+
+private val SERVICES_PRIORITY_LIST = arrayOf(
+    "0:_advertisertest._tcp",
+    "5:_prioritytest._udp",
+    "5:_otherprioritytest._tcp"
+)
+
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsAdvertiserTest {
@@ -99,25 +178,43 @@
     private val mockInterfaceAdvertiser1 = mock(MdnsInterfaceAdvertiser::class.java)
     private val mockInterfaceAdvertiser2 = mock(MdnsInterfaceAdvertiser::class.java)
     private val mockDeps = mock(MdnsAdvertiser.Dependencies::class.java)
+    private val context = mock(Context::class.java)
+    private val resources = mock(Resources::class.java)
+    private val flags = MdnsFeatureFlags.newBuilder().setIsMdnsOffloadFeatureEnabled(true).build()
 
     @Before
     fun setUp() {
         thread.start()
         doReturn(TEST_HOSTNAME).`when`(mockDeps).generateHostname()
         doReturn(mockInterfaceAdvertiser1).`when`(mockDeps).makeAdvertiser(eq(mockSocket1),
-                any(), any(), any(), any(), eq(TEST_HOSTNAME), any()
+                any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
         )
         doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(eq(mockSocket2),
-                any(), any(), any(), any(), eq(TEST_HOSTNAME), any()
+                any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
         )
         doReturn(true).`when`(mockInterfaceAdvertiser1).isProbing(anyInt())
         doReturn(true).`when`(mockInterfaceAdvertiser2).isProbing(anyInt())
         doReturn(createEmptyNetworkInterface()).`when`(mockSocket1).getInterface()
         doReturn(createEmptyNetworkInterface()).`when`(mockSocket2).getInterface()
+        doReturn(TEST_INTERFACE1).`when`(mockInterfaceAdvertiser1).socketInterfaceName
+        doReturn(TEST_INTERFACE2).`when`(mockInterfaceAdvertiser2).socketInterfaceName
+        doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
+            SERVICE_ID_1)
+        doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
+            SERVICE_ID_2)
+        doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
+            SERVICE_ID_3)
+        doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser2).getRawOffloadPayload(
+            SERVICE_ID_1)
+        doReturn(resources).`when`(context).getResources()
+        doReturn(SERVICES_PRIORITY_LIST).`when`(resources).getStringArray(
+            R.array.config_nsdOffloadServicesPriority)
+        ConnectivityResources.setResourcesContextForTest(context)
     }
 
     @After
     fun tearDown() {
+        ConnectivityResources.setResourcesContextForTest(null)
         thread.quitSafely()
         thread.join()
     }
@@ -130,14 +227,16 @@
 
     @Test
     fun testAddService_OneNetwork() {
-        val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
-        postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
+        val advertiser =
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
 
         val socketCb = socketCbCaptor.value
-        postSync { socketCb.onSocketCreated(TEST_NETWORK_1, mockSocket1, listOf(TEST_LINKADDR)) }
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
 
         val intAdvCbCaptor = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
         verify(mockDeps).makeAdvertiser(
@@ -147,91 +246,219 @@
             any(),
             intAdvCbCaptor.capture(),
             eq(TEST_HOSTNAME),
+            any(),
             any()
         )
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
-        postSync { intAdvCbCaptor.value.onRegisterServiceSucceeded(
+        postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
                 mockInterfaceAdvertiser1, SERVICE_ID_1) }
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
 
-        postSync { socketCb.onInterfaceDestroyed(TEST_NETWORK_1, mockSocket1) }
+        // Service is conflicted.
+        postSync {
+            intAdvCbCaptor.value
+                    .onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1, CONFLICT_SERVICE)
+        }
+
+        // Verify the metrics data
+        doReturn(25).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
+        doReturn(40).`when`(mockInterfaceAdvertiser1).getSentPacketCount(SERVICE_ID_1)
+        val metrics = postReturn { advertiser.getAdvertiserMetrics(SERVICE_ID_1) }
+        assertEquals(25, metrics.mRepliedRequestsCount)
+        assertEquals(40, metrics.mSentPacketCount)
+        assertEquals(0, metrics.mConflictDuringProbingCount)
+        assertEquals(1, metrics.mConflictAfterProbingCount)
+
+        doReturn(TEST_OFFLOAD_PACKET2).`when`(mockInterfaceAdvertiser1)
+            .getRawOffloadPayload(
+                SERVICE_ID_1
+            )
+        postSync {
+            socketCb.onAddressesChanged(
+                TEST_SOCKETKEY_1,
+                mockSocket1,
+                listOf(TEST_LINKADDR2)
+            )
+        }
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE2))
+
+        postSync { socketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
         verify(mockInterfaceAdvertiser1).destroyNow()
+        verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE2))
     }
 
     @Test
-    fun testAddService_AllNetworks() {
-        val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
-        postSync { advertiser.addService(SERVICE_ID_1, ALL_NETWORKS_SERVICE, TEST_SUBTYPE) }
+    fun testAddService_AllNetworksWithSubType() {
+        val advertiser =
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
-        verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE.network),
+        verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
                 socketCbCaptor.capture())
 
         val socketCb = socketCbCaptor.value
-        postSync { socketCb.onSocketCreated(TEST_NETWORK_1, mockSocket1, listOf(TEST_LINKADDR)) }
-        postSync { socketCb.onSocketCreated(TEST_NETWORK_2, mockSocket2, listOf(TEST_LINKADDR)) }
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR)) }
 
         val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
         val intAdvCbCaptor2 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
         verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
-                eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any()
+                eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
         )
         verify(mockDeps).makeAdvertiser(eq(mockSocket2), eq(listOf(TEST_LINKADDR)),
-                eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any()
+                eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
         )
         verify(mockInterfaceAdvertiser1).addService(
-                anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
         verify(mockInterfaceAdvertiser2).addService(
-                anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
-        postSync { intAdvCbCaptor1.value.onRegisterServiceSucceeded(
+        postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
                 mockInterfaceAdvertiser1, SERVICE_ID_1) }
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO))
 
         // Need both advertisers to finish probing and call onRegisterServiceSucceeded
         verify(cb, never()).onRegisterServiceSucceeded(anyInt(), any())
         doReturn(false).`when`(mockInterfaceAdvertiser2).isProbing(SERVICE_ID_1)
-        postSync { intAdvCbCaptor2.value.onRegisterServiceSucceeded(
+        postSync { intAdvCbCaptor2.value.onServiceProbingSucceeded(
                 mockInterfaceAdvertiser2, SERVICE_ID_1) }
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
-                argThat { it.matches(ALL_NETWORKS_SERVICE) })
+                argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
+
+        // Services are conflicted.
+        postSync {
+            intAdvCbCaptor1.value
+                    .onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1, CONFLICT_SERVICE)
+        }
+        postSync {
+            intAdvCbCaptor1.value
+                    .onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1, CONFLICT_SERVICE)
+        }
+        postSync {
+            intAdvCbCaptor2.value
+                    .onServiceConflict(mockInterfaceAdvertiser2, SERVICE_ID_1, CONFLICT_SERVICE)
+        }
+
+        // Verify the metrics data
+        doReturn(10).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
+        doReturn(5).`when`(mockInterfaceAdvertiser2).getServiceRepliedRequestsCount(SERVICE_ID_1)
+        doReturn(22).`when`(mockInterfaceAdvertiser1).getSentPacketCount(SERVICE_ID_1)
+        doReturn(12).`when`(mockInterfaceAdvertiser2).getSentPacketCount(SERVICE_ID_1)
+        val metrics = postReturn { advertiser.getAdvertiserMetrics(SERVICE_ID_1) }
+        assertEquals(15, metrics.mRepliedRequestsCount)
+        assertEquals(34, metrics.mSentPacketCount)
+        assertEquals(2, metrics.mConflictDuringProbingCount)
+        assertEquals(1, metrics.mConflictAfterProbingCount)
 
         // Unregister the service
         postSync { advertiser.removeService(SERVICE_ID_1) }
         verify(mockInterfaceAdvertiser1).removeService(SERVICE_ID_1)
         verify(mockInterfaceAdvertiser2).removeService(SERVICE_ID_1)
+        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)
     }
 
     @Test
+    fun testAddService_OffloadPriority() {
+        val advertiser =
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        postSync {
+            advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, DEFAULT_ADVERTISING_OPTION,
+                    TEST_CLIENT_UID_1)
+            advertiser.addOrUpdateService(SERVICE_ID_2,
+                NsdServiceInfo("TestService2", "_PRIORITYTEST._udp").apply {
+                    port = 12345
+                    hostAddresses = listOf(TEST_ADDR)
+                }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+            advertiser.addOrUpdateService(
+                SERVICE_ID_3,
+                NsdServiceInfo("TestService3", "_notprioritized._tcp").apply {
+                    port = 12345
+                    hostAddresses = listOf(TEST_ADDR)
+                }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+        }
+
+        val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+        verify(socketProvider).requestSocket(eq(SERVICE_1.network), socketCbCaptor.capture())
+
+        val socketCb = socketCbCaptor.value
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+        val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
+        verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
+            eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
+        )
+
+        doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
+        doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_2)
+        doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_3)
+        postSync {
+            intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_1)
+            intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_2)
+            intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_3)
+        }
+
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OffloadServiceInfo(
+            OffloadServiceInfo.Key("TestService2", "_PRIORITYTEST._udp"),
+            emptyList() /* subtypes */,
+            "Android_test.local",
+            TEST_OFFLOAD_PACKET1,
+            5, /* priority */
+            OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+        )))
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OffloadServiceInfo(
+            OffloadServiceInfo.Key("TestService3", "_notprioritized._tcp"),
+            emptyList() /* subtypes */,
+            "Android_test.local",
+            TEST_OFFLOAD_PACKET1,
+            Integer.MAX_VALUE, /* priority */
+            OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+        )))
+    }
+
+    @Test
     fun testAddService_Conflicts() {
-        val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
-        postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
+        val advertiser =
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
         val oneNetSocketCb = oneNetSocketCbCaptor.value
 
         // Register a service with the same name on all networks (name conflict)
-        postSync { advertiser.addService(SERVICE_ID_2, ALL_NETWORKS_SERVICE, null /* subtype */) }
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
         val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
         val allNetSocketCb = allNetSocketCbCaptor.value
 
-        postSync { advertiser.addService(LONG_SERVICE_ID_1, LONG_SERVICE_1, null /* subtype */) }
-        postSync { advertiser.addService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
-                null /* subtype */) }
+        postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+
+        postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
+                ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
 
         // Callbacks for matching network and all networks both get the socket
         postSync {
-            oneNetSocketCb.onSocketCreated(TEST_NETWORK_1, mockSocket1, listOf(TEST_LINKADDR))
-            allNetSocketCb.onSocketCreated(TEST_NETWORK_1, mockSocket1, listOf(TEST_LINKADDR))
+            oneNetSocketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR))
+            allNetSocketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR))
         }
 
         val expectedRenamed = NsdServiceInfo(
@@ -249,42 +476,111 @@
             network = LONG_ALL_NETWORKS_SERVICE.network
         }
 
+        val expectedCaseInsensitiveRenamed = NsdServiceInfo(
+            "${ALL_NETWORKS_SERVICE_2.serviceName} (3)", ALL_NETWORKS_SERVICE_2.serviceType
+        ).apply {
+            port = ALL_NETWORKS_SERVICE_2.port
+            hostAddresses = ALL_NETWORKS_SERVICE_2.hostAddresses
+            network = ALL_NETWORKS_SERVICE_2.network
+        }
+
         val intAdvCbCaptor = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
         verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
-                eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any()
+                eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
         )
         verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(SERVICE_1) }, eq(null))
+                argThat { it.matches(SERVICE_1) }, any())
         verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
-                argThat { it.matches(expectedRenamed) }, eq(null))
+                argThat { it.matches(expectedRenamed) }, any())
         verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
-                argThat { it.matches(LONG_SERVICE_1) }, eq(null))
+                argThat { it.matches(LONG_SERVICE_1) }, any())
         verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
-            argThat { it.matches(expectedLongRenamed) }, eq(null))
+            argThat { it.matches(expectedLongRenamed) }, any())
+        verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
+            argThat { it.matches(expectedCaseInsensitiveRenamed) }, any())
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
-        postSync { intAdvCbCaptor.value.onRegisterServiceSucceeded(
+        postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
                 mockInterfaceAdvertiser1, SERVICE_ID_1) }
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_2)
-        postSync { intAdvCbCaptor.value.onRegisterServiceSucceeded(
+        postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
                 mockInterfaceAdvertiser1, SERVICE_ID_2) }
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_2),
                 argThat { it.matches(expectedRenamed) })
 
-        postSync { oneNetSocketCb.onInterfaceDestroyed(TEST_NETWORK_1, mockSocket1) }
-        postSync { allNetSocketCb.onInterfaceDestroyed(TEST_NETWORK_1, mockSocket1) }
+        postSync { oneNetSocketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
+        postSync { allNetSocketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
 
         // destroyNow can be called multiple times
         verify(mockInterfaceAdvertiser1, atLeastOnce()).destroyNow()
     }
 
     @Test
+    fun testAddOrUpdateService_Updates() {
+        val advertiser =
+                MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags,
+                    context)
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+
+        val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+        verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
+
+        val socketCb = socketCbCaptor.value
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+        verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
+                argThat { it.matches(ALL_NETWORKS_SERVICE) }, any())
+
+        val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
+
+        // Update with serviceId that is not registered yet should fail
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
+                updateOptions, TEST_CLIENT_UID_1) }
+        verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
+
+        // Update service with different NsdServiceInfo should fail
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions,
+                TEST_CLIENT_UID_1) }
+        verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
+
+        // Update service with same NsdServiceInfo but different subType should succeed
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
+                updateOptions, TEST_CLIENT_UID_1) }
+        verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
+
+        // Newly created MdnsInterfaceAdvertiser will get addService() call.
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
+        verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
+                argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }, any())
+    }
+
+    @Test
+    fun testAddOrUpdateService_customTtl_registeredSuccess() {
+        val advertiser = MdnsAdvertiser(
+                thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        val updateOptions =
+                MdnsAdvertisingOptions.newBuilder().setTtl(Duration.ofSeconds(30)).build()
+
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
+                updateOptions, TEST_CLIENT_UID_1) }
+
+        val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+        verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
+        val socketCb = socketCbCaptor.value
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+        verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1), any(), eq(updateOptions))
+    }
+
+    @Test
     fun testRemoveService_whenAllServiceRemoved_thenUpdateHostName() {
-        val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
+        val advertiser =
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
         verify(mockDeps, times(1)).generateHostname()
-        postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
+                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
         postSync { advertiser.removeService(SERVICE_ID_1) }
         verify(mockDeps, times(2)).generateHostname()
     }
@@ -293,6 +589,14 @@
         handler.post(r)
         handler.waitForIdle(TIMEOUT_MS)
     }
+
+    private fun <T> postReturn(r: (() -> T)): T {
+        val future = CompletableFuture<T>()
+        handler.post {
+            future.complete(r())
+        }
+        return future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
 }
 
 // NsdServiceInfo does not implement equals; this is useful to use in argument matchers
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
index 7c6cb3e..27242f1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
@@ -21,6 +21,7 @@
 import android.os.HandlerThread
 import android.os.SystemClock
 import com.android.internal.util.HexDump
+import com.android.net.module.util.SharedLog
 import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
 import com.android.server.connectivity.mdns.MdnsAnnouncer.BaseAnnouncementInfo
 import com.android.server.connectivity.mdns.MdnsRecordRepository.getReverseDnsAddress
@@ -52,7 +53,9 @@
 
     private val thread = HandlerThread(MdnsAnnouncerTest::class.simpleName)
     private val socket = mock(MdnsInterfaceSocket::class.java)
+    private val sharedLog = mock(SharedLog::class.java)
     private val buffer = ByteArray(1500)
+    private val flags = MdnsFeatureFlags.newBuilder().build()
 
     @Before
     fun setUp() {
@@ -80,11 +83,12 @@
 
     @Test
     fun testAnnounce() {
-        val replySender = MdnsReplySender("testiface", thread.looper, socket, buffer)
+        val replySender = MdnsReplySender(
+                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
         @Suppress("UNCHECKED_CAST")
         val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
                 as MdnsPacketRepeater.PacketRepeaterCallback<BaseAnnouncementInfo>
-        val announcer = MdnsAnnouncer("testiface", thread.looper, replySender, cb)
+        val announcer = MdnsAnnouncer(thread.looper, replySender, cb, sharedLog)
         /*
         The expected packet replicates records announced when registering a service, as observed in
         the legacy mDNS implementation (some ordering differs to be more readable).
@@ -251,7 +255,7 @@
 
         val captor = ArgumentCaptor.forClass(DatagramPacket::class.java)
         repeat(FIRST_ANNOUNCES_COUNT) { i ->
-            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, request)
+            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, request, 1 /* sentPacketCount */)
             verify(socket, atLeast(i + 1)).send(any())
             val now = SystemClock.elapsedRealtime()
             assertTrue(now > timeStart + startDelay + i * FIRST_ANNOUNCES_DELAY)
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 350bad0..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,8 +18,9 @@
 
 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.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
@@ -28,7 +29,6 @@
 import static org.mockito.Mockito.when;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.net.Network;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -55,8 +55,10 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
 
 /** Tests for {@link MdnsDiscoveryManager}. */
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class MdnsDiscoveryManagerTests {
@@ -65,18 +67,25 @@
     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 Pair<String, Network> PER_NETWORK_SERVICE_TYPE_1_NULL_NETWORK =
-            Pair.create(SERVICE_TYPE_1, null);
-    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_1_NETWORK_1 =
-            Pair.create(SERVICE_TYPE_1, NETWORK_1);
-    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_NULL_NETWORK =
-            Pair.create(SERVICE_TYPE_2, null);
-    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_NETWORK_1 =
-            Pair.create(SERVICE_TYPE_2, NETWORK_1);
-    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_NETWORK_2 =
-            Pair.create(SERVICE_TYPE_2, NETWORK_2);
-
+    private static final int INTERFACE_INDEX_NULL_NETWORK = 123;
+    private static final SocketKey SOCKET_KEY_NULL_NETWORK =
+            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 =
+            new SocketKey(NETWORK_2, 997 /* interfaceIndex */);
+    private static final Pair<String, SocketKey> PER_SOCKET_SERVICE_TYPE_1_NULL_NETWORK =
+            Pair.create(SERVICE_TYPE_1, SOCKET_KEY_NULL_NETWORK);
+    private static final Pair<String, SocketKey> PER_SOCKET_SERVICE_TYPE_2_NULL_NETWORK =
+            Pair.create(SERVICE_TYPE_2, SOCKET_KEY_NULL_NETWORK);
+    private static final Pair<String, SocketKey> PER_SOCKET_SERVICE_TYPE_1_NETWORK_1 =
+            Pair.create(SERVICE_TYPE_1, SOCKET_KEY_NETWORK_1);
+    private static final Pair<String, SocketKey> PER_SOCKET_SERVICE_TYPE_2_NETWORK_1 =
+            Pair.create(SERVICE_TYPE_2, SOCKET_KEY_NETWORK_1);
+    private static final Pair<String, SocketKey> PER_SOCKET_SERVICE_TYPE_2_NETWORK_2 =
+            Pair.create(SERVICE_TYPE_2, SOCKET_KEY_NETWORK_2);
     @Mock private ExecutorProvider executorProvider;
+    @Mock private ScheduledExecutorService mockExecutorService;
     @Mock private MdnsSocketClientBase socketClient;
     @Mock private MdnsServiceTypeClient mockServiceTypeClientType1NullNetwork;
     @Mock private MdnsServiceTypeClient mockServiceTypeClientType1Network1;
@@ -91,6 +100,8 @@
     private HandlerThread thread;
     private Handler handler;
 
+    private int createdServiceTypeClientCount;
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -99,37 +110,43 @@
         thread.start();
         handler = new Handler(thread.getLooper());
         doReturn(thread.getLooper()).when(socketClient).getLooper();
+        doReturn(true).when(socketClient).supportsRequestingSpecificNetworks();
+        createdServiceTypeClientCount = 0;
         discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient,
-                sharedLog) {
+                sharedLog, MdnsFeatureFlags.newBuilder().build()) {
                     @Override
                     MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
-                            @Nullable Network network) {
-                        final Pair<String, Network> perNetworkServiceType =
-                                Pair.create(serviceType, network);
-                        if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_1_NULL_NETWORK)) {
+                            @NonNull SocketKey socketKey) {
+                        createdServiceTypeClientCount++;
+                        final Pair<String, SocketKey> perSocketServiceType =
+                                Pair.create(serviceType, socketKey);
+                        if (perSocketServiceType.equals(PER_SOCKET_SERVICE_TYPE_1_NULL_NETWORK)) {
                             return mockServiceTypeClientType1NullNetwork;
-                        } else if (perNetworkServiceType.equals(
-                                PER_NETWORK_SERVICE_TYPE_1_NETWORK_1)) {
+                        } else if (perSocketServiceType.equals(
+                                PER_SOCKET_SERVICE_TYPE_1_NETWORK_1)) {
                             return mockServiceTypeClientType1Network1;
-                        } else if (perNetworkServiceType.equals(
-                                PER_NETWORK_SERVICE_TYPE_2_NULL_NETWORK)) {
+                        } else if (perSocketServiceType.equals(
+                                PER_SOCKET_SERVICE_TYPE_2_NULL_NETWORK)) {
                             return mockServiceTypeClientType2NullNetwork;
-                        } else if (perNetworkServiceType.equals(
-                                PER_NETWORK_SERVICE_TYPE_2_NETWORK_1)) {
+                        } else if (perSocketServiceType.equals(
+                                PER_SOCKET_SERVICE_TYPE_2_NETWORK_1)) {
                             return mockServiceTypeClientType2Network1;
-                        } else if (perNetworkServiceType.equals(
-                                PER_NETWORK_SERVICE_TYPE_2_NETWORK_2)) {
+                        } else if (perSocketServiceType.equals(
+                                PER_SOCKET_SERVICE_TYPE_2_NETWORK_2)) {
                             return mockServiceTypeClientType2Network2;
                         }
+                        fail("Unexpected perSocketServiceType: " + perSocketServiceType);
                         return null;
                     }
                 };
+        doReturn(mockExecutorService).when(mockServiceTypeClientType1NullNetwork).getExecutor();
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws Exception {
         if (thread != null) {
             thread.quitSafely();
+            thread.join();
         }
     }
 
@@ -155,32 +172,46 @@
                 MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
         final SocketCreationCallback callback = expectSocketCreationCallback(
                 SERVICE_TYPE_1, mockListenerOne, options);
-        runOnHandler(() -> callback.onSocketCreated(null /* network */));
+        runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NULL_NETWORK));
         verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(mockListenerOne, options);
 
         when(mockServiceTypeClientType1NullNetwork.stopSendAndReceive(mockListenerOne))
                 .thenReturn(true);
         runOnHandler(() -> discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne));
+        verify(executorProvider).shutdownExecutorService(mockExecutorService);
         verify(mockServiceTypeClientType1NullNetwork).stopSendAndReceive(mockListenerOne);
         verify(socketClient).stopDiscovery();
     }
 
     @Test
+    public void onSocketDestroy_shutdownExecutorService() throws IOException {
+        final MdnsSearchOptions options =
+                MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
+        final SocketCreationCallback callback = expectSocketCreationCallback(
+                SERVICE_TYPE_1, mockListenerOne, options);
+        runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NULL_NETWORK));
+        verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(mockListenerOne, options);
+
+        runOnHandler(() -> callback.onSocketDestroyed(SOCKET_KEY_NULL_NETWORK));
+        verify(executorProvider).shutdownExecutorService(mockExecutorService);
+    }
+
+    @Test
     public void registerMultipleListeners() throws IOException {
         final MdnsSearchOptions options =
                 MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
         final SocketCreationCallback callback = expectSocketCreationCallback(
                 SERVICE_TYPE_1, mockListenerOne, options);
-        runOnHandler(() -> callback.onSocketCreated(null /* network */));
+        runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NULL_NETWORK));
         verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(mockListenerOne, options);
-        runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
+        runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NETWORK_1));
         verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options);
 
         final SocketCreationCallback callback2 = expectSocketCreationCallback(
                 SERVICE_TYPE_2, mockListenerTwo, options);
-        runOnHandler(() -> callback2.onSocketCreated(null /* network */));
+        runOnHandler(() -> callback2.onSocketCreated(SOCKET_KEY_NULL_NETWORK));
         verify(mockServiceTypeClientType2NullNetwork).startSendAndReceive(mockListenerTwo, options);
-        runOnHandler(() -> callback2.onSocketCreated(NETWORK_2));
+        runOnHandler(() -> callback2.onSocketCreated(SOCKET_KEY_NETWORK_2));
         verify(mockServiceTypeClientType2Network2).startSendAndReceive(mockListenerTwo, options);
     }
 
@@ -190,49 +221,48 @@
                 MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
         final SocketCreationCallback callback = expectSocketCreationCallback(
                 SERVICE_TYPE_1, mockListenerOne, options1);
-        runOnHandler(() -> callback.onSocketCreated(null /* network */));
+        runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NULL_NETWORK));
         verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(
                 mockListenerOne, options1);
-        runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
+        runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NETWORK_1));
         verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options1);
 
         final MdnsSearchOptions options2 =
                 MdnsSearchOptions.newBuilder().setNetwork(NETWORK_2).build();
         final SocketCreationCallback callback2 = expectSocketCreationCallback(
                 SERVICE_TYPE_2, mockListenerTwo, options2);
-        runOnHandler(() -> callback2.onSocketCreated(NETWORK_2));
+        runOnHandler(() -> callback2.onSocketCreated(SOCKET_KEY_NETWORK_2));
         verify(mockServiceTypeClientType2Network2).startSendAndReceive(mockListenerTwo, options2);
 
         final MdnsPacket responseForServiceTypeOne = createMdnsPacket(SERVICE_TYPE_1);
-        final int ifIndex = 1;
         runOnHandler(() -> discoveryManager.onResponseReceived(
-                responseForServiceTypeOne, ifIndex, null /* network */));
+                responseForServiceTypeOne, SOCKET_KEY_NULL_NETWORK));
         // Packets for network null are only processed by the ServiceTypeClient for network null
-        verify(mockServiceTypeClientType1NullNetwork).processResponse(responseForServiceTypeOne,
-                ifIndex, null /* network */);
-        verify(mockServiceTypeClientType1Network1, never()).processResponse(any(), anyInt(), any());
-        verify(mockServiceTypeClientType2Network2, never()).processResponse(any(), anyInt(), any());
+        verify(mockServiceTypeClientType1NullNetwork).processResponse(
+                responseForServiceTypeOne, SOCKET_KEY_NULL_NETWORK);
+        verify(mockServiceTypeClientType1Network1, never()).processResponse(any(), any());
+        verify(mockServiceTypeClientType2Network2, never()).processResponse(any(), any());
 
         final MdnsPacket responseForServiceTypeTwo = createMdnsPacket(SERVICE_TYPE_2);
         runOnHandler(() -> discoveryManager.onResponseReceived(
-                responseForServiceTypeTwo, ifIndex, NETWORK_1));
-        verify(mockServiceTypeClientType1NullNetwork, never()).processResponse(any(), anyInt(),
-                eq(NETWORK_1));
-        verify(mockServiceTypeClientType1Network1).processResponse(responseForServiceTypeTwo,
-                ifIndex, NETWORK_1);
-        verify(mockServiceTypeClientType2Network2, never()).processResponse(any(), anyInt(),
-                eq(NETWORK_1));
+                responseForServiceTypeTwo, SOCKET_KEY_NETWORK_1));
+        verify(mockServiceTypeClientType1NullNetwork, never()).processResponse(any(),
+                eq(SOCKET_KEY_NETWORK_1));
+        verify(mockServiceTypeClientType1Network1).processResponse(
+                responseForServiceTypeTwo, SOCKET_KEY_NETWORK_1);
+        verify(mockServiceTypeClientType2Network2, never()).processResponse(any(),
+                eq(SOCKET_KEY_NETWORK_1));
 
         final MdnsPacket responseForSubtype =
                 createMdnsPacket("subtype._sub._googlecast._tcp.local");
         runOnHandler(() -> discoveryManager.onResponseReceived(
-                responseForSubtype, ifIndex, NETWORK_2));
-        verify(mockServiceTypeClientType1NullNetwork, never()).processResponse(
-                any(), anyInt(), eq(NETWORK_2));
-        verify(mockServiceTypeClientType1Network1, never()).processResponse(
-                any(), anyInt(), eq(NETWORK_2));
+                responseForSubtype, SOCKET_KEY_NETWORK_2));
+        verify(mockServiceTypeClientType1NullNetwork, never()).processResponse(any(),
+                eq(SOCKET_KEY_NETWORK_2));
+        verify(mockServiceTypeClientType1Network1, never()).processResponse(any(),
+                eq(SOCKET_KEY_NETWORK_2));
         verify(mockServiceTypeClientType2Network2).processResponse(
-                responseForSubtype, ifIndex, NETWORK_2);
+                responseForSubtype, SOCKET_KEY_NETWORK_2);
     }
 
     @Test
@@ -242,55 +272,51 @@
                 MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
         final SocketCreationCallback callback = expectSocketCreationCallback(
                 SERVICE_TYPE_1, mockListenerOne, network1Options);
-        runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
+        runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NETWORK_1));
         verify(mockServiceTypeClientType1Network1).startSendAndReceive(
                 mockListenerOne, network1Options);
 
         // Create a ServiceTypeClient for SERVICE_TYPE_2 and NETWORK_1
         final SocketCreationCallback callback2 = expectSocketCreationCallback(
                 SERVICE_TYPE_2, mockListenerTwo, network1Options);
-        runOnHandler(() -> callback2.onSocketCreated(NETWORK_1));
+        runOnHandler(() -> callback2.onSocketCreated(SOCKET_KEY_NETWORK_1));
         verify(mockServiceTypeClientType2Network1).startSendAndReceive(
                 mockListenerTwo, network1Options);
 
         // Receive a response, it should be processed on both clients.
         final MdnsPacket response = createMdnsPacket(SERVICE_TYPE_1);
-        final int ifIndex = 1;
-        runOnHandler(() -> discoveryManager.onResponseReceived(
-                response, ifIndex, NETWORK_1));
-        verify(mockServiceTypeClientType1Network1).processResponse(response, ifIndex, NETWORK_1);
-        verify(mockServiceTypeClientType2Network1).processResponse(response, ifIndex, NETWORK_1);
+        runOnHandler(() -> discoveryManager.onResponseReceived(response, SOCKET_KEY_NETWORK_1));
+        verify(mockServiceTypeClientType1Network1).processResponse(response, SOCKET_KEY_NETWORK_1);
+        verify(mockServiceTypeClientType2Network1).processResponse(response, SOCKET_KEY_NETWORK_1);
 
         // The first callback receives a notification that the network has been destroyed,
         // mockServiceTypeClientOne1 should send service removed notifications and remove from the
         // list of clients.
-        runOnHandler(() -> callback.onAllSocketsDestroyed(NETWORK_1));
+        runOnHandler(() -> callback.onSocketDestroyed(SOCKET_KEY_NETWORK_1));
         verify(mockServiceTypeClientType1Network1).notifySocketDestroyed();
 
         // Receive a response again, it should be processed only on
         // mockServiceTypeClientType2Network1. Because the mockServiceTypeClientType1Network1 is
         // removed from the list of clients, it is no longer able to process responses.
-        runOnHandler(() -> discoveryManager.onResponseReceived(
-                response, ifIndex, NETWORK_1));
+        runOnHandler(() -> discoveryManager.onResponseReceived(response, SOCKET_KEY_NETWORK_1));
         // Still times(1) as a response was received once previously
-        verify(mockServiceTypeClientType1Network1, times(1))
-                .processResponse(response, ifIndex, NETWORK_1);
-        verify(mockServiceTypeClientType2Network1, times(2))
-                .processResponse(response, ifIndex, NETWORK_1);
+        verify(mockServiceTypeClientType1Network1, times(1)).processResponse(
+                response, SOCKET_KEY_NETWORK_1);
+        verify(mockServiceTypeClientType2Network1, times(2)).processResponse(
+                response, SOCKET_KEY_NETWORK_1);
 
         // The client for NETWORK_1 receives the callback that the NETWORK_2 has been destroyed,
         // mockServiceTypeClientTwo2 shouldn't send any notifications.
-        runOnHandler(() -> callback2.onAllSocketsDestroyed(NETWORK_2));
+        runOnHandler(() -> callback2.onSocketDestroyed(SOCKET_KEY_NETWORK_2));
         verify(mockServiceTypeClientType2Network1, never()).notifySocketDestroyed();
 
         // Receive a response again, mockServiceTypeClientType2Network1 is still in the list of
         // clients, it's still able to process responses.
-        runOnHandler(() -> discoveryManager.onResponseReceived(
-                response, ifIndex, NETWORK_1));
-        verify(mockServiceTypeClientType1Network1, times(1))
-                .processResponse(response, ifIndex, NETWORK_1);
-        verify(mockServiceTypeClientType2Network1, times(3))
-                .processResponse(response, ifIndex, NETWORK_1);
+        runOnHandler(() -> discoveryManager.onResponseReceived(response, SOCKET_KEY_NETWORK_1));
+        verify(mockServiceTypeClientType1Network1, times(1)).processResponse(
+                response, SOCKET_KEY_NETWORK_1);
+        verify(mockServiceTypeClientType2Network1, times(3)).processResponse(
+                response, SOCKET_KEY_NETWORK_1);
     }
 
     @Test
@@ -300,27 +326,24 @@
                 MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
         final SocketCreationCallback callback = expectSocketCreationCallback(
                 SERVICE_TYPE_1, mockListenerOne, network1Options);
-        runOnHandler(() -> callback.onSocketCreated(null /* network */));
+        runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NULL_NETWORK));
         verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(
                 mockListenerOne, network1Options);
 
         // 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, ifIndex, null /* network */));
+        runOnHandler(() -> discoveryManager.onResponseReceived(response, SOCKET_KEY_NULL_NETWORK));
         verify(mockServiceTypeClientType1NullNetwork).processResponse(
-                response, ifIndex, null /* network */);
+                response, SOCKET_KEY_NULL_NETWORK);
 
-        runOnHandler(() -> callback.onAllSocketsDestroyed(null /* network */));
+        runOnHandler(() -> callback.onSocketDestroyed(SOCKET_KEY_NULL_NETWORK));
         verify(mockServiceTypeClientType1NullNetwork).notifySocketDestroyed();
 
         // Receive a response again, it should not be processed.
-        runOnHandler(() -> discoveryManager.onResponseReceived(
-                response, ifIndex, null /* network */));
+        runOnHandler(() -> discoveryManager.onResponseReceived(response, SOCKET_KEY_NULL_NETWORK));
         // Still times(1) as a response was received once previously
-        verify(mockServiceTypeClientType1NullNetwork, times(1))
-                .processResponse(response, ifIndex, null /* network */);
+        verify(mockServiceTypeClientType1NullNetwork, times(1)).processResponse(
+                response, SOCKET_KEY_NULL_NETWORK);
 
         // Unregister the listener, notifyNetworkUnrequested should be called but other stop methods
         // won't be call because the service type client was unregistered and destroyed. But those
@@ -329,11 +352,44 @@
         verify(socketClient).notifyNetworkUnrequested(mockListenerOne);
         verify(mockServiceTypeClientType1NullNetwork, never()).stopSendAndReceive(any());
         // The stopDiscovery() is only used by MdnsSocketClient, which doesn't send
-        // onAllSocketsDestroyed(). So the socket clients that send onAllSocketsDestroyed() do not
+        // onSocketDestroyed(). So the socket clients that send onSocketDestroyed() do not
         // need to call stopDiscovery().
         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 dd458b8..629ac67 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -26,6 +26,7 @@
 import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
 import com.android.server.connectivity.mdns.MdnsAnnouncer.BaseAnnouncementInfo
 import com.android.server.connectivity.mdns.MdnsAnnouncer.ExitAnnouncementInfo
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
 import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.EXIT_ANNOUNCEMENT_DELAY_MS
 import com.android.server.connectivity.mdns.MdnsPacketRepeater.PacketRepeaterCallback
 import com.android.server.connectivity.mdns.MdnsProber.ProbingInfo
@@ -35,6 +36,7 @@
 import java.net.InetSocketAddress
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
+import kotlin.test.assertNotSame
 import kotlin.test.assertTrue
 import org.junit.After
 import org.junit.Before
@@ -44,12 +46,15 @@
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.anyString
+import org.mockito.Mockito.argThat
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
+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
@@ -59,12 +64,28 @@
 private val TEST_HOSTNAME = arrayOf("Android_test", "local")
 
 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"
     port = 12345
 }
 
+private val TEST_SERVICE_1_SUBTYPE = NsdServiceInfo().apply {
+    subtypes = setOf("_sub")
+    serviceType = "_testservice._tcp"
+    serviceName = "MyTestService"
+    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 {
@@ -77,6 +98,8 @@
     private val announcer = mock(MdnsAnnouncer::class.java)
     private val prober = mock(MdnsProber::class.java)
     private val sharedlog = SharedLog("MdnsInterfaceAdvertiserTest")
+    private val flags = MdnsFeatureFlags.newBuilder()
+            .setIsKnownAnswerSuppressionEnabled(true).build()
     @Suppress("UNCHECKED_CAST")
     private val probeCbCaptor = ArgumentCaptor.forClass(PacketRepeaterCallback::class.java)
             as ArgumentCaptor<PacketRepeaterCallback<ProbingInfo>>
@@ -99,18 +122,18 @@
             cb,
             deps,
             TEST_HOSTNAME,
-            sharedlog
+            sharedlog,
+            flags
         )
     }
 
     @Before
     fun setUp() {
-        doReturn(repository).`when`(deps).makeRecordRepository(any(),
-            eq(TEST_HOSTNAME)
-        )
-        doReturn(replySender).`when`(deps).makeReplySender(anyString(), any(), any(), any())
-        doReturn(announcer).`when`(deps).makeMdnsAnnouncer(anyString(), any(), any(), any())
-        doReturn(prober).`when`(deps).makeMdnsProber(anyString(), any(), any(), any())
+        doReturn(repository).`when`(deps).makeRecordRepository(any(), eq(TEST_HOSTNAME), any())
+        doReturn(replySender).`when`(deps).makeReplySender(
+                anyString(), any(), any(), any(), any(), any())
+        doReturn(announcer).`when`(deps).makeMdnsAnnouncer(anyString(), any(), any(), any(), any())
+        doReturn(prober).`when`(deps).makeMdnsProber(anyString(), any(), any(), any(), any())
 
         val knownServices = mutableSetOf<Int>()
         doAnswer { inv ->
@@ -132,8 +155,8 @@
         advertiser.start()
 
         verify(socket).addPacketHandler(packetHandlerCaptor.capture())
-        verify(deps).makeMdnsProber(any(), any(), any(), probeCbCaptor.capture())
-        verify(deps).makeMdnsAnnouncer(any(), any(), any(), announceCbCaptor.capture())
+        verify(deps).makeMdnsProber(any(), any(), any(), probeCbCaptor.capture(), any())
+        verify(deps).makeMdnsAnnouncer(any(), any(), any(), announceCbCaptor.capture(), any())
     }
 
     @After
@@ -150,7 +173,7 @@
                 0L /* initialDelayMs */)
 
         thread.waitForIdle(TIMEOUT_MS)
-        verify(cb).onRegisterServiceSucceeded(advertiser, TEST_SERVICE_ID_1)
+        verify(cb).onServiceProbingSucceeded(advertiser, TEST_SERVICE_ID_1)
 
         // Remove the service: expect exit announcements
         val testExitInfo = mock(ExitAnnouncementInfo::class.java)
@@ -164,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
@@ -190,8 +300,9 @@
     fun testReplyToQuery() {
         addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
 
-        val mockReply = mock(MdnsRecordRepository.ReplyInfo::class.java)
-        doReturn(mockReply).`when`(repository).getReply(any(), any())
+        val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0),
+                InetSocketAddress(0), emptyList())
+        doReturn(testReply).`when`(repository).getReply(any(), any())
 
         // Query obtained with:
         // scapy.raw(scapy.DNS(
@@ -204,7 +315,12 @@
         packetHandler.handlePacket(query, query.size, src)
 
         val packetCaptor = ArgumentCaptor.forClass(MdnsPacket::class.java)
-        verify(repository).getReply(packetCaptor.capture(), eq(src))
+        val srcCaptor = ArgumentCaptor.forClass(InetSocketAddress::class.java)
+        verify(repository).getReply(packetCaptor.capture(), srcCaptor.capture())
+
+        assertEquals(src, srcCaptor.value)
+        assertNotSame(src, srcCaptor.value, "src will be reused by the packetHandler, references " +
+                "to it should not be used outside of handlePacket.")
 
         packetCaptor.value.let {
             assertEquals(1, it.questions.size)
@@ -216,13 +332,120 @@
             assertContentEquals(arrayOf("_testservice", "_tcp", "local"), it.questions[0].name)
         }
 
-        verify(replySender).queueReply(mockReply)
+        verify(replySender).queueReply(testReply)
+    }
+
+    @Test
+    fun testReplyToQuery_TruncatedBitSet() {
+        addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        val src = InetSocketAddress(parseNumericAddress("2001:db8::456"), MdnsConstants.MDNS_PORT)
+        val testReply = MdnsReplyInfo(emptyList(), emptyList(), 400L, InetSocketAddress(0), src,
+                emptyList())
+        val knownAnswersReply = MdnsReplyInfo(emptyList(), emptyList(), 400L, InetSocketAddress(0),
+                src, emptyList())
+        val knownAnswersReply2 = MdnsReplyInfo(emptyList(), emptyList(), 0L, InetSocketAddress(0),
+                src, emptyList())
+        doReturn(testReply).`when`(repository).getReply(
+                argThat { pkg -> pkg.questions.size != 0 && pkg.answers.size == 0 &&
+                        (pkg.flags and MdnsConstants.FLAG_TRUNCATED) != 0},
+                eq(src))
+        doReturn(knownAnswersReply).`when`(repository).getReply(
+                argThat { pkg -> pkg.questions.size == 0 && pkg.answers.size != 0 &&
+                        (pkg.flags and MdnsConstants.FLAG_TRUNCATED) != 0},
+                eq(src))
+        doReturn(knownAnswersReply2).`when`(repository).getReply(
+                argThat { pkg -> pkg.questions.size == 0 && pkg.answers.size != 0 &&
+                        (pkg.flags and MdnsConstants.FLAG_TRUNCATED) == 0},
+                eq(src))
+
+        // Query obtained with:
+        // scapy.raw(scapy.DNS(
+        //  tc = 1, qd = scapy.DNSQR(qtype='PTR', qname='_testservice._tcp.local'))
+        // ).hex().upper()
+        val query = HexDump.hexStringToByteArray(
+                "0000030000010000000000000C5F7465737473657276696365045F746370056C6F63616C00000C0001"
+        )
+
+        packetHandler.handlePacket(query, query.size, src)
+
+        val packetCaptor = ArgumentCaptor.forClass(MdnsPacket::class.java)
+        verify(repository).getReply(packetCaptor.capture(), eq(src))
+
+        packetCaptor.value.let {
+            assertTrue((it.flags and MdnsConstants.FLAG_TRUNCATED) != 0)
+            assertEquals(1, it.questions.size)
+            assertEquals(0, it.answers.size)
+            assertEquals(0, it.authorityRecords.size)
+            assertEquals(0, it.additionalRecords.size)
+
+            assertTrue(it.questions[0] is MdnsPointerRecord)
+            assertContentEquals(arrayOf("_testservice", "_tcp", "local"), it.questions[0].name)
+        }
+
+        verify(replySender).queueReply(testReply)
+
+        // Known-Answer packet with truncated bit set obtained with:
+        // scapy.raw(scapy.DNS(
+        //   tc = 1, qd = None, an = scapy.DNSRR(type='PTR', rrname='_testtype._tcp.local',
+        //   rdata='othertestservice._testtype._tcp.local', rclass='IN', ttl=4500))
+        // ).hex().upper()
+        val knownAnswers = HexDump.hexStringToByteArray(
+                "000003000000000100000000095F7465737474797065045F746370056C6F63616C00000C0001000" +
+                        "011940027106F746865727465737473657276696365095F7465737474797065045F7463" +
+                        "70056C6F63616C00"
+        )
+
+        packetHandler.handlePacket(knownAnswers, knownAnswers.size, src)
+
+        verify(repository, times(2)).getReply(packetCaptor.capture(), eq(src))
+
+        packetCaptor.value.let {
+            assertTrue((it.flags and MdnsConstants.FLAG_TRUNCATED) != 0)
+            assertEquals(0, it.questions.size)
+            assertEquals(1, it.answers.size)
+            assertEquals(0, it.authorityRecords.size)
+            assertEquals(0, it.additionalRecords.size)
+
+            assertTrue(it.answers[0] is MdnsPointerRecord)
+            assertContentEquals(arrayOf("_testtype", "_tcp", "local"), it.answers[0].name)
+        }
+
+        verify(replySender).queueReply(knownAnswersReply)
+
+        // Known-Answer packet obtained with:
+        // scapy.raw(scapy.DNS(
+        //   qd = None, an = scapy.DNSRR(type='PTR', rrname='_testtype._tcp.local',
+        //   rdata='testservice._testtype._tcp.local', rclass='IN', ttl=4500))
+        // ).hex().upper()
+        val knownAnswers2 = HexDump.hexStringToByteArray(
+                "000001000000000100000000095F7465737474797065045F746370056C6F63616C00000C0001000" +
+                        "0119400220B7465737473657276696365095F7465737474797065045F746370056C6F63" +
+                        "616C00"
+        )
+
+        packetHandler.handlePacket(knownAnswers2, knownAnswers2.size, src)
+
+        verify(repository, times(3)).getReply(packetCaptor.capture(), eq(src))
+
+        packetCaptor.value.let {
+            assertTrue((it.flags and MdnsConstants.FLAG_TRUNCATED) == 0)
+            assertEquals(0, it.questions.size)
+            assertEquals(1, it.answers.size)
+            assertEquals(0, it.authorityRecords.size)
+            assertEquals(0, it.additionalRecords.size)
+
+            assertTrue(it.answers[0] is MdnsPointerRecord)
+            assertContentEquals(arrayOf("_testtype", "_tcp", "local"), it.answers[0].name)
+        }
+
+        verify(replySender).queueReply(knownAnswersReply2)
     }
 
     @Test
     fun testConflict() {
         addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        doReturn(setOf(TEST_SERVICE_ID_1)).`when`(repository).getConflictingServices(any())
+        doReturn(mapOf(TEST_SERVICE_ID_1 to CONFLICT_SERVICE))
+                .`when`(repository).getConflictingServices(any())
 
         // Reply obtained with:
         // scapy.raw(scapy.DNS(
@@ -248,7 +471,7 @@
         }
 
         thread.waitForIdle(TIMEOUT_MS)
-        verify(cb).onServiceConflict(advertiser, TEST_SERVICE_ID_1)
+        verify(cb).onServiceConflict(advertiser, TEST_SERVICE_ID_1, CONFLICT_SERVICE)
     }
 
     @Test
@@ -256,7 +479,7 @@
         val mockProbingInfo = mock(ProbingInfo::class.java)
         doReturn(mockProbingInfo).`when`(repository).setServiceProbing(TEST_SERVICE_ID_1)
 
-        advertiser.restartProbingForConflict(TEST_SERVICE_ID_1)
+        advertiser.maybeRestartProbingForConflict(TEST_SERVICE_ID_1)
 
         verify(prober).restartForConflict(mockProbingInfo)
     }
@@ -272,18 +495,47 @@
         verify(prober).restartForConflict(mockProbingInfo)
     }
 
-    private fun addServiceAndFinishProbing(serviceId: Int, serviceInfo: NsdServiceInfo):
-            AnnouncementInfo {
+    @Test
+    fun testReplaceExitingService() {
+        doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
+                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
+        advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE,
+                MdnsAdvertisingOptions.getDefaultOptions())
+        verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
+        verify(announcer).stop(TEST_SERVICE_ID_DUPLICATE)
+        verify(prober).startProbing(any())
+    }
+
+    @Test
+    fun testUpdateExistingService() {
+        doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
+                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
+        val subTypes = setOf("_sub")
+        advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subTypes)
+        verify(repository).updateService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+        verify(announcer, never()).stop(TEST_SERVICE_ID_DUPLICATE)
+        verify(prober, never()).startProbing(any())
+    }
+
+    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)
 
-        advertiser.addService(serviceId, serviceInfo, null /* subtype */)
-        verify(repository).addService(serviceId, serviceInfo, null /* subtype */)
+        advertiser.addService(serviceId, serviceInfo, MdnsAdvertisingOptions.getDefaultOptions())
+        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 87ba5d7..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,35 +18,40 @@
 
 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.ArgumentMatchers.anyInt;
 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;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 import android.net.InetAddresses;
 import android.net.Network;
 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;
 import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
+import org.junit.After;
 import org.junit.Before;
 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)
@@ -68,30 +75,50 @@
     @Mock private MdnsServiceBrowserListener mListener;
     @Mock private MdnsSocketClientBase.Callback mCallback;
     @Mock private SocketCreationCallback mSocketCreationCallback;
+    @Mock private SharedLog mSharedLog;
     private MdnsMultinetworkSocketClient mSocketClient;
+    private HandlerThread mHandlerThread;
     private Handler mHandler;
+    private SocketKey mSocketKey;
 
     @Before
     public void setUp() throws SocketException {
         MockitoAnnotations.initMocks(this);
-        final HandlerThread thread = new HandlerThread("MdnsMultinetworkSocketClientTest");
-        thread.start();
-        mHandler = new Handler(thread.getLooper());
-        mSocketClient = new MdnsMultinetworkSocketClient(thread.getLooper(), mProvider);
+
+        mHandlerThread = new HandlerThread("MdnsMultinetworkSocketClientTest");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mSocketKey = new SocketKey(1000 /* interfaceIndex */);
+        mSocketClient = new MdnsMultinetworkSocketClient(mHandlerThread.getLooper(), mProvider,
+                mSharedLog, MdnsFeatureFlags.newBuilder().build());
         mHandler.post(() -> mSocketClient.setCallback(mCallback));
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+    }
+
     private SocketCallback expectSocketCallback() {
         return expectSocketCallback(mListener, mNetwork);
     }
 
     private SocketCallback expectSocketCallback(MdnsServiceBrowserListener listener,
             Network requestedNetwork) {
+        return expectSocketCallback(listener, requestedNetwork, mSocketCreationCallback,
+                1 /* requestSocketCount */);
+    }
+
+    private SocketCallback expectSocketCallback(MdnsServiceBrowserListener listener,
+                Network requestedNetwork, SocketCreationCallback callback, int requestSocketCount) {
         final ArgumentCaptor<SocketCallback> callbackCaptor =
                 ArgumentCaptor.forClass(SocketCallback.class);
         mHandler.post(() -> mSocketClient.notifyNetworkRequested(
-                listener, requestedNetwork, mSocketCreationCallback));
-        verify(mProvider, timeout(DEFAULT_TIMEOUT))
+                listener, requestedNetwork, callback));
+        verify(mProvider, timeout(DEFAULT_TIMEOUT).times(requestSocketCount))
                 .requestSocket(eq(requestedNetwork), callbackCaptor.capture());
         return callbackCaptor.getValue();
     }
@@ -123,27 +150,49 @@
             doReturn(createEmptyNetworkInterface()).when(socket).getInterface();
         }
 
+        final SocketKey tetherSocketKey1 = new SocketKey(1001 /* interfaceIndex */);
+        final SocketKey tetherSocketKey2 = new SocketKey(1002 /* interfaceIndex */);
         // Notify socket created
-        callback.onSocketCreated(mNetwork, mSocket, List.of());
-        verify(mSocketCreationCallback).onSocketCreated(mNetwork);
-        callback.onSocketCreated(null, tetherIfaceSock1, List.of());
-        verify(mSocketCreationCallback).onSocketCreated(null);
-        callback.onSocketCreated(null, tetherIfaceSock2, List.of());
-        verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
+        callback.onSocketCreated(mSocketKey, mSocket, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
+        callback.onSocketCreated(tetherSocketKey1, tetherIfaceSock1, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(tetherSocketKey1);
+        callback.onSocketCreated(tetherSocketKey2, tetherIfaceSock2, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(tetherSocketKey2);
 
-        // Send packet to IPv4 with target network and verify sending has been called.
-        mSocketClient.sendMulticastPacket(ipv4Packet, mNetwork);
+        // Send packet to IPv4 with mSocketKey and verify sending has been called.
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket).send(ipv4Packet);
         verify(tetherIfaceSock1, never()).send(any());
         verify(tetherIfaceSock2, never()).send(any());
 
-        // Send packet to IPv6 without target network and verify sending has been called.
-        mSocketClient.sendMulticastPacket(ipv6Packet, null);
+        // Send packet to IPv4 with onlyUseIpv6OnIpv6OnlyNetworks = true, the packet will be sent.
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
+                true /* onlyUseIpv6OnIpv6OnlyNetworks */);
+        HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+        verify(mSocket, times(2)).send(ipv4Packet);
+        verify(tetherIfaceSock1, never()).send(any());
+        verify(tetherIfaceSock2, never()).send(any());
+
+        // Send packet to IPv6 with tetherSocketKey1 and verify sending has been called.
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv6Packet), tetherSocketKey1,
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket, never()).send(ipv6Packet);
         verify(tetherIfaceSock1).send(ipv6Packet);
-        verify(tetherIfaceSock2).send(ipv6Packet);
+        verify(tetherIfaceSock2, never()).send(ipv6Packet);
+
+        // 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(List.of(ipv6Packet), tetherSocketKey1,
+                true /* onlyUseIpv6OnIpv6OnlyNetworks */);
+        HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+        verify(mSocket, never()).send(ipv6Packet);
+        verify(tetherIfaceSock1, times(1)).send(ipv6Packet);
+        verify(tetherIfaceSock2, never()).send(ipv6Packet);
     }
 
     @Test
@@ -164,8 +213,8 @@
 
         doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
         // Notify socket created
-        callback.onSocketCreated(mNetwork, mSocket, List.of());
-        verify(mSocketCreationCallback).onSocketCreated(mNetwork);
+        callback.onSocketCreated(mSocketKey, mSocket, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
 
         final ArgumentCaptor<PacketHandler> handlerCaptor =
                 ArgumentCaptor.forClass(PacketHandler.class);
@@ -176,7 +225,7 @@
         handler.handlePacket(data, data.length, null /* src */);
         final ArgumentCaptor<MdnsPacket> responseCaptor =
                 ArgumentCaptor.forClass(MdnsPacket.class);
-        verify(mCallback).onResponseReceived(responseCaptor.capture(), anyInt(), any());
+        verify(mCallback).onResponseReceived(responseCaptor.capture(), any());
         final MdnsPacket response = responseCaptor.getValue();
         assertEquals(0, response.questions.size());
         assertEquals(0, response.additionalRecords.size());
@@ -214,14 +263,18 @@
         doReturn(createEmptyNetworkInterface()).when(socket2).getInterface();
         doReturn(createEmptyNetworkInterface()).when(socket3).getInterface();
 
-        callback.onSocketCreated(mNetwork, mSocket, List.of());
-        callback.onSocketCreated(null, socket2, List.of());
-        callback.onSocketCreated(null, socket3, List.of());
-        verify(mSocketCreationCallback).onSocketCreated(mNetwork);
-        verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
+        final SocketKey socketKey2 = new SocketKey(1001 /* interfaceIndex */);
+        final SocketKey socketKey3 = new SocketKey(1002 /* interfaceIndex */);
+        callback.onSocketCreated(mSocketKey, mSocket, List.of());
+        callback.onSocketCreated(socketKey2, socket2, List.of());
+        callback.onSocketCreated(socketKey3, socket3, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
+        verify(mSocketCreationCallback).onSocketCreated(socketKey2);
+        verify(mSocketCreationCallback).onSocketCreated(socketKey3);
 
-        // Send IPv4 packet on the non-null Network and verify sending has been called.
-        mSocketClient.sendMulticastPacket(ipv4Packet, mNetwork);
+        // Send IPv4 packet on the mSocketKey and verify sending has been called.
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket).send(ipv4Packet);
         verify(socket2, never()).send(any());
@@ -241,80 +294,181 @@
         final SocketCallback callback2 = callback2Captor.getAllValues().get(1);
 
         // Notify socket created for all networks.
-        callback2.onSocketCreated(mNetwork, mSocket, List.of());
-        callback2.onSocketCreated(null, socket2, List.of());
-        callback2.onSocketCreated(null, socket3, List.of());
-        verify(socketCreationCb2).onSocketCreated(mNetwork);
-        verify(socketCreationCb2, times(2)).onSocketCreated(null);
+        callback2.onSocketCreated(mSocketKey, mSocket, List.of());
+        callback2.onSocketCreated(socketKey2, socket2, List.of());
+        callback2.onSocketCreated(socketKey3, socket3, List.of());
+        verify(socketCreationCb2).onSocketCreated(mSocketKey);
+        verify(socketCreationCb2).onSocketCreated(socketKey2);
+        verify(socketCreationCb2).onSocketCreated(socketKey3);
 
-        // Send IPv4 packet to null network and verify sending to the 2 tethered interface sockets.
-        mSocketClient.sendMulticastPacket(ipv4Packet, null);
+        // Send IPv4 packet on socket2 and verify sending to the socket2 only.
+        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
         // mNetwork
         verify(mSocket, times(1)).send(ipv4Packet);
         verify(socket2).send(ipv4Packet);
-        verify(socket3).send(ipv4Packet);
+        verify(socket3, never()).send(ipv4Packet);
 
         // Unregister the second request
         mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(listener2));
         verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback2);
 
         // Send IPv4 packet again and verify it's still sent a second time
-        mSocketClient.sendMulticastPacket(ipv4Packet, null);
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), socketKey2,
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(socket2, times(2)).send(ipv4Packet);
-        verify(socket3, times(2)).send(ipv4Packet);
+        verify(socket3, never()).send(ipv4Packet);
 
         // Unrequest remaining sockets
         mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(mListener));
         verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback);
 
         // Send IPv4 packet and verify no more sending.
-        mSocketClient.sendMulticastPacket(ipv4Packet, null);
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket, times(1)).send(ipv4Packet);
         verify(socket2, times(2)).send(ipv4Packet);
-        verify(socket3, times(2)).send(ipv4Packet);
+        verify(socket3, never()).send(ipv4Packet);
     }
 
     @Test
     public void testNotifyNetworkUnrequested_SocketsOnNullNetwork() {
         final MdnsInterfaceSocket otherSocket = mock(MdnsInterfaceSocket.class);
+        final SocketKey otherSocketKey = new SocketKey(1001 /* interfaceIndex */);
         final SocketCallback callback = expectSocketCallback(
                 mListener, null /* requestedNetwork */);
         doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
         doReturn(createEmptyNetworkInterface()).when(otherSocket).getInterface();
 
-        callback.onSocketCreated(null /* network */, mSocket, List.of());
-        verify(mSocketCreationCallback).onSocketCreated(null);
-        callback.onSocketCreated(null /* network */, otherSocket, List.of());
-        verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
+        callback.onSocketCreated(mSocketKey, mSocket, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
+        callback.onSocketCreated(otherSocketKey, otherSocket, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(otherSocketKey);
 
-        verify(mSocketCreationCallback, never()).onAllSocketsDestroyed(null /* network */);
+        verify(mSocketCreationCallback, never()).onSocketDestroyed(mSocketKey);
+        verify(mSocketCreationCallback, never()).onSocketDestroyed(otherSocketKey);
         mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(mListener));
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
 
         verify(mProvider).unrequestSocket(callback);
-        verify(mSocketCreationCallback).onAllSocketsDestroyed(null /* network */);
+        verify(mSocketCreationCallback).onSocketDestroyed(mSocketKey);
+        verify(mSocketCreationCallback).onSocketDestroyed(otherSocketKey);
     }
 
     @Test
     public void testSocketCreatedAndDestroyed_NullNetwork() throws IOException {
         final MdnsInterfaceSocket otherSocket = mock(MdnsInterfaceSocket.class);
+        final SocketKey otherSocketKey = new SocketKey(1001 /* interfaceIndex */);
         final SocketCallback callback = expectSocketCallback(mListener, null /* network */);
         doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
         doReturn(createEmptyNetworkInterface()).when(otherSocket).getInterface();
 
-        callback.onSocketCreated(null /* network */, mSocket, List.of());
-        verify(mSocketCreationCallback).onSocketCreated(null);
-        callback.onSocketCreated(null /* network */, otherSocket, List.of());
-        verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
+        callback.onSocketCreated(mSocketKey, mSocket, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
+        callback.onSocketCreated(otherSocketKey, otherSocket, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(otherSocketKey);
 
         // Notify socket destroyed
-        callback.onInterfaceDestroyed(null /* network */, mSocket);
-        verifyNoMoreInteractions(mSocketCreationCallback);
-        callback.onInterfaceDestroyed(null /* network */, otherSocket);
-        verify(mSocketCreationCallback).onAllSocketsDestroyed(null /* network */);
+        callback.onInterfaceDestroyed(mSocketKey, mSocket);
+        verify(mSocketCreationCallback).onSocketDestroyed(mSocketKey);
+        callback.onInterfaceDestroyed(otherSocketKey, otherSocket);
+        verify(mSocketCreationCallback).onSocketDestroyed(otherSocketKey);
+    }
+
+    @Test
+    public void testSocketDestroyed_MultipleCallbacks() {
+        final MdnsInterfaceSocket socket2 = mock(MdnsInterfaceSocket.class);
+        final SocketKey socketKey2 = new SocketKey(1001 /* interfaceIndex */);
+        final SocketCreationCallback creationCallback1 = mock(SocketCreationCallback.class);
+        final SocketCreationCallback creationCallback2 = mock(SocketCreationCallback.class);
+        final SocketCreationCallback creationCallback3 = mock(SocketCreationCallback.class);
+        final SocketCallback callback1 = expectSocketCallback(
+                mock(MdnsServiceBrowserListener.class), mNetwork, creationCallback1,
+                1 /* requestSocketCount */);
+        final SocketCallback callback2 = expectSocketCallback(
+                mock(MdnsServiceBrowserListener.class), mNetwork, creationCallback2,
+                2 /* requestSocketCount */);
+        final SocketCallback callback3 = expectSocketCallback(
+                mock(MdnsServiceBrowserListener.class), null /* requestedNetwork */,
+                creationCallback3, 1 /* requestSocketCount */);
+
+        doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+        callback1.onSocketCreated(mSocketKey, mSocket, List.of());
+        callback2.onSocketCreated(mSocketKey, mSocket, List.of());
+        callback3.onSocketCreated(mSocketKey, mSocket, List.of());
+        callback3.onSocketCreated(socketKey2, socket2, List.of());
+        verify(creationCallback1).onSocketCreated(mSocketKey);
+        verify(creationCallback2).onSocketCreated(mSocketKey);
+        verify(creationCallback3).onSocketCreated(mSocketKey);
+        verify(creationCallback3).onSocketCreated(socketKey2);
+
+        callback1.onInterfaceDestroyed(mSocketKey, mSocket);
+        callback2.onInterfaceDestroyed(mSocketKey, mSocket);
+        callback3.onInterfaceDestroyed(mSocketKey, mSocket);
+        verify(creationCallback1).onSocketDestroyed(mSocketKey);
+        verify(creationCallback2).onSocketDestroyed(mSocketKey);
+        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/MdnsPacketReaderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
index 19d8a00..0168b61 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
@@ -19,8 +19,10 @@
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
+import com.android.net.module.util.HexDump;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -75,7 +77,7 @@
                     + "the packet length");
         } catch (IOException e) {
             // Expected
-        } catch (Exception e) {
+        } catch (RuntimeException e) {
             fail(String.format(
                     Locale.ROOT,
                     "Should not have thrown any other exception except " + "for IOException: %s",
@@ -83,4 +85,17 @@
         }
         assertEquals(data.length, packetReader.getRemaining());
     }
-}
\ No newline at end of file
+
+    @Test
+    public void testInfinitePtrLoop() {
+        // Fake mdns response packet label portion which has infinite ptr loop.
+        final byte[] infinitePtrLoopData = HexDump.hexStringToByteArray(
+                "054C4142454C" // label "LABEL"
+                        + "0454455354" // label "TEST"
+                        + "C006"); // PTR to second label.
+        MdnsPacketReader packetReader = new MdnsPacketReader(
+                infinitePtrLoopData, infinitePtrLoopData.length,
+                MdnsFeatureFlags.newBuilder().setIsLabelCountLimitEnabled(true).build());
+        assertThrows(IOException.class, packetReader::readLabels);
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
index f88da1f..fc4796b 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
@@ -21,26 +21,31 @@
 import com.android.testutils.DevSdkIgnoreRunner
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
 import kotlin.test.assertTrue
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(DevSdkIgnoreRunner::class)
 class MdnsPacketTest {
+    private fun makeFlags(isLabelCountLimitEnabled: Boolean = false): MdnsFeatureFlags =
+            MdnsFeatureFlags.newBuilder()
+                    .setIsLabelCountLimitEnabled(isLabelCountLimitEnabled).build()
     @Test
     fun testParseQuery() {
         // Probe packet with 1 question for Android.local, and 4 additionalRecords with 4 addresses
         // for Android.local (similar to legacy mdnsresponder probes, although it used to put 4
         // identical questions(!!) for Android.local when there were 4 addresses).
-        val packetHex = "00000000000100000004000007416e64726f6964056c6f63616c0000ff0001c00c000100" +
+        val packetHex = "007b0000000100000004000007416e64726f6964056c6f63616c0000ff0001c00c000100" +
                 "01000000780004c000027bc00c001c000100000078001020010db8000000000000000000000123c0" +
                 "0c001c000100000078001020010db8000000000000000000000456c00c001c000100000078001020" +
                 "010db8000000000000000000000789"
 
         val bytes = HexDump.hexStringToByteArray(packetHex)
-        val reader = MdnsPacketReader(bytes, bytes.size)
+        val reader = MdnsPacketReader(bytes, bytes.size, makeFlags())
         val packet = MdnsPacket.parse(reader)
 
+        assertEquals(123, packet.transactionId)
         assertEquals(1, packet.questions.size)
         assertEquals(0, packet.answers.size)
         assertEquals(4, packet.authorityRecords.size)
@@ -59,12 +64,25 @@
         }
 
         assertEquals(InetAddresses.parseNumericAddress("192.0.2.123"),
-                (packet.authorityRecords[0] as MdnsInetAddressRecord).inet4Address)
+                (packet.authorityRecords[0] as MdnsInetAddressRecord).inet4Address!!)
         assertEquals(InetAddresses.parseNumericAddress("2001:db8::123"),
-                (packet.authorityRecords[1] as MdnsInetAddressRecord).inet6Address)
+                (packet.authorityRecords[1] as MdnsInetAddressRecord).inet6Address!!)
         assertEquals(InetAddresses.parseNumericAddress("2001:db8::456"),
-                (packet.authorityRecords[2] as MdnsInetAddressRecord).inet6Address)
+                (packet.authorityRecords[2] as MdnsInetAddressRecord).inet6Address!!)
         assertEquals(InetAddresses.parseNumericAddress("2001:db8::789"),
-                (packet.authorityRecords[3] as MdnsInetAddressRecord).inet6Address)
+                (packet.authorityRecords[3] as MdnsInetAddressRecord).inet6Address!!)
+    }
+
+    @Test
+    fun testParseQueryWithLabelLoop_ThrowsParseException() {
+        val packetWithErrorHex = "000084000000000100000000054C4142454C0454455354C006000C800100000" +
+                "07800140454455354056C6F63616C00"
+
+        val bytes = HexDump.hexStringToByteArray(packetWithErrorHex)
+        val reader = MdnsPacketReader(
+                bytes, bytes.size, makeFlags(isLabelCountLimitEnabled = true))
+        assertFailsWith<MdnsPacket.ParseException> {
+            MdnsPacket.parse(reader)
+        }
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketWriterTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketWriterTest.kt
index 5c9c294..a545373 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketWriterTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketWriterTest.kt
@@ -31,7 +31,7 @@
     @Test
     fun testNameCompression() {
         val writer = MdnsPacketWriter(ByteArray(1000))
-        writer.writeLabels(arrayOf("my", "first", "name"))
+        writer.writeLabels(arrayOf("my", "FIRST", "name"))
         writer.writeLabels(arrayOf("my", "second", "name"))
         writer.writeLabels(arrayOf("other", "first", "name"))
         writer.writeLabels(arrayOf("my", "second", "name"))
@@ -41,7 +41,7 @@
                 InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::123"), 123))
 
         // Each label takes length + 1. So "first.name" offset = 3, "name" offset = 9
-        val expected = "my".label() + "first".label() + "name".label() + 0x00.toByte() +
+        val expected = "my".label() + "FIRST".label() + "name".label() + 0x00.toByte() +
                 // "my.second.name" offset = 15
                 "my".label() + "second".label() + byteArrayOf(0xC0.toByte(), 9) +
                 "other".label() + byteArrayOf(0xC0.toByte(), 3) +
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
index 2b5423b..9befbc1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -21,6 +21,7 @@
 import android.os.HandlerThread
 import android.os.Looper
 import com.android.internal.util.HexDump
+import com.android.net.module.util.SharedLog
 import com.android.server.connectivity.mdns.MdnsProber.ProbingInfo
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
@@ -48,16 +49,19 @@
 
 private val TEST_SERVICE_NAME_1 = arrayOf("testservice", "_nmt", "_tcp", "local")
 private val TEST_SERVICE_NAME_2 = arrayOf("testservice2", "_nmt", "_tcp", "local")
+private val TEST_SERVICE_NAME_3 = arrayOf("Testservice", "_nmt", "_tcp", "local")
 
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsProberTest {
     private val thread = HandlerThread(MdnsProberTest::class.simpleName)
     private val socket = mock(MdnsInterfaceSocket::class.java)
+    private val sharedLog = mock(SharedLog::class.java)
     @Suppress("UNCHECKED_CAST")
     private val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
         as MdnsPacketRepeater.PacketRepeaterCallback<ProbingInfo>
     private val buffer = ByteArray(1500)
+    private val flags = MdnsFeatureFlags.newBuilder().build()
 
     @Before
     fun setUp() {
@@ -81,14 +85,15 @@
     private class TestProber(
         looper: Looper,
         replySender: MdnsReplySender,
-        cb: PacketRepeaterCallback<ProbingInfo>
-    ) : MdnsProber("testiface", looper, replySender, cb) {
+        cb: PacketRepeaterCallback<ProbingInfo>,
+        sharedLog: SharedLog
+    ) : MdnsProber(looper, replySender, cb, sharedLog) {
         override fun getInitialDelay() = 0L
     }
 
     private fun assertProbesSent(probeInfo: TestProbeInfo, expectedHex: String) {
         repeat(probeInfo.numSends) { i ->
-            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, probeInfo)
+            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, probeInfo, 1 /* sentPacketCount */)
             // If the probe interval is short, more than (i+1) probes may have been sent already
             verify(socket, atLeast(i + 1)).send(any())
         }
@@ -115,8 +120,9 @@
 
     @Test
     fun testProbe() {
-        val replySender = MdnsReplySender("testiface", thread.looper, socket, buffer)
-        val prober = TestProber(thread.looper, replySender, cb)
+        val replySender = MdnsReplySender(
+                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
+        val prober = TestProber(thread.looper, replySender, cb, sharedLog)
         val probeInfo = TestProbeInfo(
                 listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)))
         prober.startProbing(probeInfo)
@@ -129,9 +135,19 @@
     }
 
     @Test
+    fun testCreateProberCaseInsensitive() {
+        val probeInfo = TestProbeInfo(
+            listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890),
+                makeServiceRecord(TEST_SERVICE_NAME_2, 37890),
+                makeServiceRecord(TEST_SERVICE_NAME_3, 37890)))
+        assertEquals(2, probeInfo.getPacket(0).questions.size)
+    }
+
+    @Test
     fun testProbeMultipleRecords() {
-        val replySender = MdnsReplySender("testiface", thread.looper, socket, buffer)
-        val prober = TestProber(thread.looper, replySender, cb)
+        val replySender = MdnsReplySender(
+                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
+        val prober = TestProber(thread.looper, replySender, cb, sharedLog)
         val probeInfo = TestProbeInfo(listOf(
                 makeServiceRecord(TEST_SERVICE_NAME_1, 37890),
                 makeServiceRecord(TEST_SERVICE_NAME_2, 37891),
@@ -168,8 +184,9 @@
 
     @Test
     fun testStopProbing() {
-        val replySender = MdnsReplySender("testiface", thread.looper, socket, buffer)
-        val prober = TestProber(thread.looper, replySender, cb)
+        val replySender = MdnsReplySender(
+                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
+        val prober = TestProber(thread.looper, replySender, cb, sharedLog)
         val probeInfo = TestProbeInfo(
                 listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)),
                 // delayMs is the delay between each probe, so does not apply to the first one
@@ -177,7 +194,7 @@
         prober.startProbing(probeInfo)
 
         // Expect the initial probe
-        verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(0, probeInfo)
+        verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(0, probeInfo, 1 /* sentPacketCount */)
 
         // Stop probing
         val stopResult = CompletableFuture<Boolean>()
@@ -187,7 +204,7 @@
 
         // Wait for a bit (more than the probe delay) to ensure no more probes were sent
         Thread.sleep(SHORT_TIMEOUT_MS * 2)
-        verify(cb, never()).onSent(1, probeInfo)
+        verify(cb, never()).onSent(1, probeInfo, 1 /* sentPacketCount */)
         verify(cb, never()).onFinished(probeInfo)
 
         // Only one sent packet
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 4a39b93..2cb97c9 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -21,21 +21,34 @@
 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
 import com.android.server.connectivity.mdns.MdnsRecordRepository.Dependencies
 import com.android.server.connectivity.mdns.MdnsRecordRepository.getReverseDnsAddress
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
 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
 import kotlin.test.assertFalse
 import kotlin.test.assertNotNull
+import kotlin.test.assertNull
 import kotlin.test.assertTrue
+import kotlin.test.fail
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -43,8 +56,20 @@
 
 private const val TEST_SERVICE_ID_1 = 42
 private const val TEST_SERVICE_ID_2 = 43
+private const val TEST_SERVICE_ID_3 = 44
+private const val TEST_CUSTOM_HOST_ID_1 = 45
+private const val TEST_CUSTOM_HOST_ID_2 = 46
+private const val TEST_SERVICE_CUSTOM_HOST_ID_1 = 48
 private const val TEST_PORT = 12345
 private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
+// RFC6762 10. Resource Record TTL Values and Cache Coherency
+// The recommended TTL value for Multicast DNS resource records with a host name as the resource
+// record's name (e.g., A, AAAA, HINFO) or a host name contained within the resource record's rdata
+// (e.g., SRV, reverse mapping PTR record) SHOULD be 120 seconds. The recommended TTL value for
+// other Multicast DNS resource records is 75 minutes.
+private const val LONG_TTL = 4_500_000L
+private const val SHORT_TTL = 120_000L
 private val TEST_HOSTNAME = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
 private val TEST_ADDRESSES = listOf(
         LinkAddress(parseNumericAddress("192.0.2.111"), 24),
@@ -63,6 +88,54 @@
     port = TEST_PORT
 }
 
+private val TEST_SERVICE_3 = NsdServiceInfo().apply {
+    serviceType = "_TESTSERVICE._tcp"
+    serviceName = "MyTESTSERVICE"
+    port = TEST_PORT
+}
+
+private val TEST_CUSTOM_HOST_1 = NsdServiceInfo().apply {
+    hostname = "TestHost"
+    hostAddresses = listOf(parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2"))
+}
+
+private val TEST_CUSTOM_HOST_1_NAME = arrayOf("TestHost", "local")
+
+private val TEST_CUSTOM_HOST_2 = NsdServiceInfo().apply {
+    hostname = "OtherTestHost"
+    hostAddresses = listOf(parseNumericAddress("2001:db8::3"), parseNumericAddress("2001:db8::4"))
+}
+
+private val TEST_SERVICE_CUSTOM_HOST_1 = NsdServiceInfo().apply {
+    hostname = "TestHost"
+    hostAddresses = listOf(parseNumericAddress("2001:db8::1"))
+    serviceType = "_testservice._tcp"
+    serviceName = "TestService"
+    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 {
@@ -70,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()
     }
 
@@ -83,12 +169,22 @@
         thread.join()
     }
 
+    private fun makeFlags(
+        includeInetAddressesInProbing: Boolean = false,
+        isKnownAnswerSuppressionEnabled: Boolean = false,
+        unicastReplyEnabled: Boolean = true
+    ) = MdnsFeatureFlags.Builder()
+        .setIncludeInetAddressRecordsInProbing(includeInetAddressesInProbing)
+        .setIsKnownAnswerSuppressionEnabled(isKnownAnswerSuppressionEnabled)
+        .setIsUnicastReplyEnabled(unicastReplyEnabled)
+        .build()
+
     @Test
     fun testAddServiceAndProbe() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+        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 /* subtype */))
+        assertEquals(-1,
+                repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, Duration.ofSeconds(50)))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -98,6 +194,7 @@
         assertEquals(TEST_SERVICE_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)
@@ -110,7 +207,7 @@
         assertEquals(MdnsServiceRecord(expectedName,
                 0L /* receiptTimeMillis */,
                 false /* cacheFlush */,
-                120_000L /* ttlMillis */,
+                50_000L /* ttlMillis */,
                 0 /* servicePriority */, 0 /* serviceWeight */,
                 TEST_PORT, TEST_HOSTNAME), packet.authorityRecords[0])
 
@@ -119,33 +216,70 @@
 
     @Test
     fun testAddAndConflicts() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
         assertFailsWith(NameConflictException::class) {
-            repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */)
+            repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */)
+        }
+        assertFailsWith(NameConflictException::class) {
+            repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* ttl */)
         }
     }
 
     @Test
-    fun testInvalidReuseOfServiceId() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+    fun testAddAndUpdates() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
         assertFailsWith(IllegalArgumentException::class) {
-            repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
+            repository.updateService(TEST_SERVICE_ID_2, emptySet() /* subtype */)
+        }
+
+        repository.updateService(TEST_SERVICE_ID_1, setOf(TEST_SUBTYPE))
+
+        val queriedName = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
+        val questions = listOf(MdnsPointerRecord(queriedName, false /* 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)
+
+        // TTLs as per RFC6762 10.
+        val longTtl = 4_500_000L
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                        queriedName,
+                        0L /* receiptTimeMillis */,
+                        false /* cacheFlush */,
+                        longTtl,
+                        serviceName),
+        ), reply.answers)
+    }
+
+    @Test
+    fun testInvalidReuseOfServiceId() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+        assertFailsWith(IllegalArgumentException::class) {
+            repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* ttl */)
         }
     }
 
     @Test
     fun testHasActiveService() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
 
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
         assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
         repository.onProbingSucceeded(probingInfo)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
 
         repository.exitService(TEST_SERVICE_ID_1)
@@ -154,15 +288,16 @@
 
     @Test
     fun testExitAnnouncements() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
 
         val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
         assertNotNull(exitAnnouncement)
         assertEquals(1, repository.servicesCount)
         val packet = exitAnnouncement.getPacket(0)
 
+        assertEquals(0, packet.transactionId)
         assertEquals(0x8400 /* response, authoritative */, packet.flags)
         assertEquals(0, packet.questions.size)
         assertEquals(0, packet.authorityRecords.size)
@@ -172,7 +307,7 @@
                 MdnsPointerRecord(
                         arrayOf("_testservice", "_tcp", "local"),
                         0L /* receiptTimeMillis */,
-                        true /* cacheFlush */,
+                        false /* cacheFlush */,
                         0L /* ttlMillis */,
                         arrayOf("MyTestService", "_testservice", "_tcp", "local"))
         ), packet.answers)
@@ -182,35 +317,42 @@
     }
 
     @Test
-    fun testExitAnnouncements_WithSubtype() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
-        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+    fun testExitAnnouncements_WithSubtypes() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+                setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
 
         val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
         assertNotNull(exitAnnouncement)
         assertEquals(1, repository.servicesCount)
         val packet = exitAnnouncement.getPacket(0)
 
+        assertEquals(0, packet.transactionId)
         assertEquals(0x8400 /* response, authoritative */, packet.flags)
         assertEquals(0, packet.questions.size)
         assertEquals(0, packet.authorityRecords.size)
         assertEquals(0, packet.additionalRecords.size)
 
-        assertContentEquals(listOf(
+        assertThat(packet.answers).containsExactly(
                 MdnsPointerRecord(
                         arrayOf("_testservice", "_tcp", "local"),
                         0L /* receiptTimeMillis */,
-                        true /* cacheFlush */,
+                        false /* cacheFlush */,
                         0L /* ttlMillis */,
                         arrayOf("MyTestService", "_testservice", "_tcp", "local")),
                 MdnsPointerRecord(
                         arrayOf("_subtype", "_sub", "_testservice", "_tcp", "local"),
                         0L /* receiptTimeMillis */,
-                        true /* cacheFlush */,
+                        false /* cacheFlush */,
                         0L /* ttlMillis */,
                         arrayOf("MyTestService", "_testservice", "_tcp", "local")),
-        ), packet.answers)
+                MdnsPointerRecord(
+                        arrayOf("_subtype2", "_sub", "_testservice", "_tcp", "local"),
+                        0L /* receiptTimeMillis */,
+                        false /* cacheFlush */,
+                        0L /* ttlMillis */,
+                        arrayOf("MyTestService", "_testservice", "_tcp", "local")))
 
         repository.removeService(TEST_SERVICE_ID_1)
         assertEquals(0, repository.servicesCount)
@@ -218,13 +360,13 @@
 
     @Test
     fun testExitingServiceReAdded() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         repository.exitService(TEST_SERVICE_ID_1)
 
         assertEquals(TEST_SERVICE_ID_1,
-                repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */))
+                repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */))
         assertEquals(1, repository.servicesCount)
 
         repository.removeService(TEST_SERVICE_ID_2)
@@ -233,24 +375,26 @@
 
     @Test
     fun testOnProbingSucceeded() {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
-                TEST_SUBTYPE)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+                setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         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 serviceType = arrayOf("_testservice", "_tcp", "local")
         val serviceSubtype = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
+        val serviceSubtype2 = arrayOf(TEST_SUBTYPE2, "_sub", "_testservice", "_tcp", "local")
         val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
         val v4AddrRev = getReverseDnsAddress(TEST_ADDRESSES[0].address)
         val v6Addr1Rev = getReverseDnsAddress(TEST_ADDRESSES[1].address)
         val v6Addr2Rev = getReverseDnsAddress(TEST_ADDRESSES[2].address)
 
-        assertContentEquals(listOf(
+        assertThat(packet.answers).containsExactly(
                 // Reverse address and address records for the hostname
                 MdnsPointerRecord(v4AddrRev,
                         0L /* receiptTimeMillis */,
@@ -297,6 +441,13 @@
                         false /* cacheFlush */,
                         4500000L /* ttlMillis */,
                         serviceName),
+                MdnsPointerRecord(
+                        serviceSubtype2,
+                        0L /* receiptTimeMillis */,
+                        // Not a unique name owned by the announcer, so cacheFlush=false
+                        false /* cacheFlush */,
+                        4500000L /* ttlMillis */,
+                        serviceName),
                 MdnsServiceRecord(
                         serviceName,
                         0L /* receiptTimeMillis */,
@@ -318,8 +469,7 @@
                         0L /* receiptTimeMillis */,
                         false /* cacheFlush */,
                         4500000L /* ttlMillis */,
-                        serviceType)
-        ), packet.answers)
+                        serviceType))
 
         assertContentEquals(listOf(
                 MdnsNsecRecord(v4AddrRev,
@@ -327,35 +477,88 @@
                         true /* cacheFlush */,
                         120000L /* ttlMillis */,
                         v4AddrRev,
-                        intArrayOf(MdnsRecord.TYPE_PTR)),
+                        intArrayOf(TYPE_PTR)),
                 MdnsNsecRecord(TEST_HOSTNAME,
                         0L /* receiptTimeMillis */,
                         true /* cacheFlush */,
                         120000L /* ttlMillis */,
                         TEST_HOSTNAME,
-                        intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
                 MdnsNsecRecord(v6Addr1Rev,
                         0L /* receiptTimeMillis */,
                         true /* cacheFlush */,
                         120000L /* ttlMillis */,
                         v6Addr1Rev,
-                        intArrayOf(MdnsRecord.TYPE_PTR)),
+                        intArrayOf(TYPE_PTR)),
                 MdnsNsecRecord(v6Addr2Rev,
                         0L /* receiptTimeMillis */,
                         true /* cacheFlush */,
                         120000L /* ttlMillis */,
                         v6Addr2Rev,
-                        intArrayOf(MdnsRecord.TYPE_PTR)),
+                        intArrayOf(TYPE_PTR)),
                 MdnsNsecRecord(serviceName,
                         0L /* receiptTimeMillis */,
                         true /* cacheFlush */,
                         4500000L /* ttlMillis */,
                         serviceName,
-                        intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV))
+                        intArrayOf(TYPE_TXT, TYPE_SRV))
         ), packet.additionalRecords)
     }
 
     @Test
+    fun testGetOffloadPacket() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+        val serviceType = arrayOf("_testservice", "_tcp", "local")
+        val offloadPacket = repository.getOffloadPacket(TEST_SERVICE_ID_1)
+        assertEquals(0, offloadPacket.transactionId)
+        assertEquals(0x8400, offloadPacket.flags)
+        assertEquals(0, offloadPacket.questions.size)
+        assertEquals(0, offloadPacket.additionalRecords.size)
+        assertEquals(0, offloadPacket.authorityRecords.size)
+        assertContentEquals(listOf(
+            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_HOSTNAME),
+            MdnsTextRecord(
+                serviceName,
+                0L /* receiptTimeMillis */,
+                true /* cacheFlush */,
+                4500000L /* ttlMillis */,
+                emptyList() /* entries */),
+            MdnsInetAddressRecord(TEST_HOSTNAME,
+                0L /* receiptTimeMillis */,
+                true /* cacheFlush */,
+                120000L /* ttlMillis */,
+                TEST_ADDRESSES[0].address),
+            MdnsInetAddressRecord(TEST_HOSTNAME,
+                0L /* receiptTimeMillis */,
+                true /* cacheFlush */,
+                120000L /* ttlMillis */,
+                TEST_ADDRESSES[1].address),
+            MdnsInetAddressRecord(TEST_HOSTNAME,
+                0L /* receiptTimeMillis */,
+                true /* cacheFlush */,
+                120000L /* ttlMillis */,
+                TEST_ADDRESSES[2].address),
+        ), offloadPacket.answers)
+    }
+
+    @Test
     fun testGetReverseDnsAddress() {
         val expectedV6 = "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa"
                 .split(".").toTypedArray()
@@ -365,107 +568,719 @@
     }
 
     @Test
-    fun testGetReply() {
-        doGetReplyTest(subtype = null)
+    fun testGetReplyCaseInsensitive() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        val questionsCaseInSensitive = listOf(
+                MdnsPointerRecord(arrayOf("_TESTSERVICE", "_TCP", "local"), false /* isUnicast */))
+        val queryCaseInsensitive = MdnsPacket(0 /* flags */, questionsCaseInSensitive,
+            emptyList() /* answers */, emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val replyCaseInsensitive = repository.getReply(queryCaseInsensitive, src)
+        assertNotNull(replyCaseInsensitive)
+        assertEquals(1, replyCaseInsensitive.answers.size)
+        assertEquals(7, replyCaseInsensitive.additionalAnswers.size)
+    }
+
+    /**
+     * Creates mDNS query packet with given query names and types.
+     */
+    private fun makeQuery(vararg queries: Pair<Int, Array<String>>): MdnsPacket {
+        val questions = queries.map { (type, name) -> makeQuestionRecord(name, type) }
+        return MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+                emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+    }
+
+    private fun makeQuestionRecord(name: Array<String>, type: Int): MdnsRecord {
+        when (type) {
+            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")
+        }
     }
 
     @Test
-    fun testGetReply_WithSubtype() {
-        doGetReplyTest(TEST_SUBTYPE)
-    }
-
-    private fun doGetReplyTest(subtype: String?) {
-        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
-        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype)
-        val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local")
-        else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local")
-
-        val questions = listOf(MdnsPointerRecord(queriedName,
-                0L /* receiptTimeMillis */,
-                false /* cacheFlush */,
-                // TTL and data is empty for a question
-                0L /* ttlMillis */,
-                null /* pointer */))
-        val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
-                listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+    fun testGetReply_singlePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
         val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
         val reply = repository.getReply(query, src)
 
         assertNotNull(reply)
-        // Source address is IPv4
-        assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
-        assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
-
-        // TTLs as per RFC6762 10.
-        val longTtl = 4_500_000L
-        val shortTtl = 120_000L
-        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
-
         assertEquals(listOf(
                 MdnsPointerRecord(
-                        queriedName,
-                        0L /* receiptTimeMillis */,
-                        false /* cacheFlush */,
-                        longTtl,
-                        serviceName),
-        ), reply.answers)
-
+                    arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+            reply.answers)
         assertEquals(listOf(
-            MdnsTextRecord(
-                    serviceName,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    longTtl,
-                    listOf() /* entries */),
-            MdnsServiceRecord(
-                    serviceName,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    shortTtl,
-                    0 /* servicePriority */,
-                    0 /* serviceWeight */,
-                    TEST_PORT,
-                    TEST_HOSTNAME),
-            MdnsInetAddressRecord(
-                    TEST_HOSTNAME,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    shortTtl,
-                    TEST_ADDRESSES[0].address),
-            MdnsInetAddressRecord(
-                    TEST_HOSTNAME,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    shortTtl,
-                    TEST_ADDRESSES[1].address),
-            MdnsInetAddressRecord(
-                    TEST_HOSTNAME,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    shortTtl,
-                    TEST_ADDRESSES[2].address),
-            MdnsNsecRecord(
-                    serviceName,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    longTtl,
-                    serviceName /* nextDomain */,
-                    intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
-            MdnsNsecRecord(
-                    TEST_HOSTNAME,
-                    0L /* receiptTimeMillis */,
-                    true /* cacheFlush */,
-                    shortTtl,
-                    TEST_HOSTNAME /* nextDomain */,
-                    intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+            ), reply.additionalAnswers)
+    }
+
+
+    @Test
+    fun testGetReply_ptrQuestionForServiceWithCustomHost_customHostUsedInAdditionalAnswers() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1,
+                setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+        val src = InetSocketAddress(parseNumericAddress("fe80::1234"), 5353)
+        val serviceName = arrayOf("TestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                        arrayOf("_testservice", "_tcp", "local"),
+                        0L, false, LONG_TTL, serviceName)),
+                reply.answers)
+        assertEquals(listOf(
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL,
+                        0, 0, TEST_PORT, TEST_CUSTOM_HOST_1_NAME),
+                MdnsInetAddressRecord(
+                        TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        parseNumericAddress("2001:db8::1")),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+                        intArrayOf(TYPE_AAAA)),
         ), reply.additionalAnswers)
     }
 
     @Test
+    fun testGetReply_ptrQuestionForServicesWithSameCustomHost_customHostUsedInAdditionalAnswers() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        val serviceWithCustomHost1 = NsdServiceInfo().apply {
+            hostname = "TestHost"
+            hostAddresses = listOf(
+                    parseNumericAddress("2001:db8::1"),
+                    parseNumericAddress("192.0.2.1"))
+            serviceType = "_testservice._tcp"
+            serviceName = "TestService1"
+            port = TEST_PORT
+        }
+        val serviceWithCustomHost2 = NsdServiceInfo().apply {
+            hostname = "TestHost"
+            hostAddresses = listOf(
+                    parseNumericAddress("2001:db8::1"),
+                    parseNumericAddress("2001:db8::3"))
+        }
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, serviceWithCustomHost1)
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, serviceWithCustomHost2)
+        val src = InetSocketAddress(parseNumericAddress("fe80::1234"), 5353)
+        val serviceName = arrayOf("TestService1", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                        arrayOf("_testservice", "_tcp", "local"),
+                        0L, false, LONG_TTL, serviceName)),
+                reply.answers)
+        assertEquals(listOf(
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL,
+                        0, 0, TEST_PORT, TEST_CUSTOM_HOST_1_NAME),
+                MdnsInetAddressRecord(
+                        TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        parseNumericAddress("2001:db8::1")),
+                MdnsInetAddressRecord(
+                        TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        parseNumericAddress("192.0.2.1")),
+                MdnsInetAddressRecord(
+                        TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        parseNumericAddress("2001:db8::3")),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+                        TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+        ), reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_singleSubtypePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(
+                TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                    arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"), 0L, false,
+                    LONG_TTL, serviceName)),
+            reply.answers)
+        assertEquals(listOf(
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+            ), reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_duplicatePtrQuestions_doesNotReturnDuplicateRecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(
+                TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+                TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                    arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+            reply.answers)
+        assertEquals(listOf(
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+            ), reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_multiplePtrQuestionsWithSubtype_doesNotReturnDuplicateRecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(
+                TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+                TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                    arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+                MdnsPointerRecord(
+                    arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"),
+                    0L, false, LONG_TTL, serviceName)),
+            reply.answers)
+        assertEquals(listOf(
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                    TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+            ), reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_txtQuestion_returnsNoNsecRecord() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(TYPE_TXT to serviceName)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList())),
+                reply.answers)
+        // No NSEC records because the reply doesn't include the SRV record
+        assertTrue(reply.additionalAnswers.isEmpty())
+    }
+
+    @Test
+    fun testGetReply_AAAAQuestionButNoIpv6Address_returnsNsecRecord() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(
+                TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE),
+                listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+
+        val query = makeQuery(TYPE_AAAA to TEST_HOSTNAME)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertTrue(reply.answers.isEmpty())
+        assertEquals(listOf(
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, LONG_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_AAAA))),
+            reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_AAAAQuestionForCustomHost_returnsAAAARecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(
+                TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
+                listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+        val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+
+        val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+                        0, false, LONG_TTL, parseNumericAddress("2001:db8::1")),
+                MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+                        0, false, LONG_TTL, parseNumericAddress("2001:db8::2"))),
+                reply.answers)
+        assertEquals(
+                listOf(MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME,
+                        0L, true, SHORT_TTL,
+                        TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+                        intArrayOf(TYPE_AAAA))),
+                reply.additionalAnswers)
+    }
+
+
+    @Test
+    fun testGetReply_AAAAQuestionForCustomHostInMultipleRegistrations_returnsAAAARecords() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_1, NsdServiceInfo().apply {
+            hostname = "TestHost"
+            hostAddresses = listOf(
+                    parseNumericAddress("2001:db8::1"),
+                    parseNumericAddress("2001:db8::2"))
+        })
+        repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, NsdServiceInfo().apply {
+            hostname = "TestHost"
+            hostAddresses = listOf(
+                    parseNumericAddress("2001:db8::1"),
+                    parseNumericAddress("2001:db8::3"))
+        })
+        val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+
+        val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+                        0, false, LONG_TTL, parseNumericAddress("2001:db8::1")),
+                MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+                        0, false, LONG_TTL, parseNumericAddress("2001:db8::2")),
+                MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+                        0, false, LONG_TTL, parseNumericAddress("2001:db8::3"))),
+                reply.answers)
+        assertEquals(
+                listOf(MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME,
+                        0L, true, SHORT_TTL,
+                        TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+                        intArrayOf(TYPE_AAAA))),
+                reply.additionalAnswers)
+    }
+
+    @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(
+                TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
+                listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+        repository.addService(
+                TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1, null /* ttl */)
+        repository.removeService(TEST_CUSTOM_HOST_ID_1)
+        repository.removeService(TEST_SERVICE_CUSTOM_HOST_ID_1)
+
+        val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+
+        val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
+        val reply = repository.getReply(query, src)
+
+        assertNull(reply)
+    }
+
+    @Test
+    fun testGetReply_ptrAndSrvQuestions_doesNotReturnSrvRecordInAdditionalAnswerSection() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(
+                TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+                TYPE_SRV to serviceName)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsPointerRecord(
+                    arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+                MdnsServiceRecord(
+                    serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME)),
+            reply.answers)
+        assertFalse(reply.additionalAnswers.any { it -> it is MdnsServiceRecord })
+    }
+
+    @Test
+    fun testGetReply_srvTxtAddressQuestions_returnsAllRecordsInAnswerSectionExceptNsec() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+        val query = makeQuery(
+                TYPE_SRV to serviceName,
+                TYPE_TXT to serviceName,
+                TYPE_SRV to serviceName,
+                TYPE_A to TEST_HOSTNAME,
+                TYPE_AAAA to TEST_HOSTNAME)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(listOf(
+                MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+                MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address)),
+            reply.answers)
+        assertEquals(listOf(
+                MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)),
+                MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(TYPE_A, TYPE_AAAA))),
+            reply.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_queryWithIpv4Address_replyWithIpv4Address() {
+        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)
+
+        assertNotNull(replyIpv4)
+        assertEquals(MdnsConstants.getMdnsIPv4Address(), replyIpv4.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, replyIpv4.destination.port)
+    }
+
+    @Test
+    fun testGetReply_queryWithIpv6Address_replyWithIpv6Address() {
+        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 replyIpv6 = repository.getReply(query, srcIpv6)
+
+        assertNotNull(replyIpv6)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), replyIpv6.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, replyIpv6.destination.port)
+    }
+
+    @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)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
-        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
 
         val packet = MdnsPacket(
                 0 /* flags */,
@@ -485,15 +1300,373 @@
                 emptyList() /* authorityRecords */,
                 emptyList() /* additionalRecords */)
 
-        assertEquals(setOf(TEST_SERVICE_ID_1, TEST_SERVICE_ID_2),
+        assertEquals(
+                mapOf(
+                        TEST_SERVICE_ID_1 to CONFLICT_SERVICE,
+                        TEST_SERVICE_ID_2 to CONFLICT_SERVICE),
                 repository.getConflictingServices(packet))
     }
 
     @Test
+    fun testGetConflictingServicesCaseInsensitive() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
+
+        val packet = MdnsPacket(
+            0 /* flags */,
+            emptyList() /* questions */,
+            listOf(
+                MdnsServiceRecord(
+                    arrayOf("MYTESTSERVICE", "_TESTSERVICE", "_tcp", "local"),
+                    0L /* receiptTimeMillis */, true /* cacheFlush */, 0L /* ttlMillis */,
+                    0 /* servicePriority */, 0 /* serviceWeight */,
+                    TEST_SERVICE_1.port + 1,
+                    TEST_HOSTNAME),
+                MdnsTextRecord(
+                    arrayOf("MYOTHERTESTSERVICE", "_TESTSERVICE", "_tcp", "local"),
+                    0L /* receiptTimeMillis */, true /* cacheFlush */, 0L /* ttlMillis */,
+                    listOf(TextEntry.fromString("somedifferent=entry"))),
+            ) /* answers */,
+            emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */)
+
+        assertEquals(
+                mapOf(TEST_SERVICE_ID_1 to CONFLICT_SERVICE,
+                        TEST_SERVICE_ID_2 to CONFLICT_SERVICE),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_customHosts_differentAddresses() {
+        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 */)
+
+        val packet = MdnsPacket(
+                0, /* flags */
+                emptyList(), /* questions */
+                listOf(
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::5")),
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::6")),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(mapOf(TEST_CUSTOM_HOST_ID_1 to CONFLICT_HOST),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_customHosts_moreAddressesThanUs_conflict() {
+        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 */)
+
+        val packet = MdnsPacket(
+                0, /* flags */
+                emptyList(), /* questions */
+                listOf(
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::3")),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(mapOf(TEST_CUSTOM_HOST_ID_1 to CONFLICT_HOST),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_customHostsReplyHasFewerAddressesThanUs_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 */
+                emptyList(), /* questions */
+                listOf(
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(emptyMap(),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    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.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 */
+                emptyList(), /* questions */
+                listOf(
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+                        MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(emptyMap(),
+                repository.getConflictingServices(packet))
+    }
+
+
+    @Test
+    fun testGetConflictingServices_customHostsCaseInsensitiveReplyHasIdenticalHosts_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 */
+                emptyList(), /* questions */
+                listOf(
+                        MdnsInetAddressRecord(arrayOf("TESTHOST", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+                        MdnsInetAddressRecord(arrayOf("testhost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(emptyMap(),
+                repository.getConflictingServices(packet))
+    }
+
+    @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)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
-        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
 
         val otherTtlMillis = 1234L
         val packet = MdnsPacket(
@@ -505,7 +1678,7 @@
                                 0L /* receiptTimeMillis */, true /* cacheFlush */,
                                 otherTtlMillis, 0 /* servicePriority */, 0 /* serviceWeight */,
                                 TEST_SERVICE_1.port,
-                                TEST_HOSTNAME),
+                                arrayOf("ANDROID_000102030405060708090A0B0C0D0E0F", "local")),
                         MdnsTextRecord(
                                 arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
                                 0L /* receiptTimeMillis */, true /* cacheFlush */,
@@ -515,17 +1688,624 @@
                 emptyList() /* additionalRecords */)
 
         // Above records are identical to the actual registrations: no conflict
-        assertEquals(emptySet(), repository.getConflictingServices(packet))
+        assertEquals(emptyMap(), repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
+
+        val otherTtlMillis = 1234L
+        val packet = MdnsPacket(
+                0 /* flags */,
+                emptyList() /* questions */,
+                listOf(
+                        MdnsServiceRecord(
+                                arrayOf("MYTESTSERVICE", "_TESTSERVICE", "_tcp", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                otherTtlMillis, 0 /* servicePriority */, 0 /* serviceWeight */,
+                                TEST_SERVICE_1.port,
+                                TEST_HOSTNAME),
+                        MdnsTextRecord(
+                                arrayOf("MyOtherTestService", "_TESTSERVICE", "_tcp", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                otherTtlMillis, emptyList()),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        // Above records are identical to the actual registrations: no conflict
+        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)
+        // Verify that there is no packet replied.
+        assertEquals(MdnsConstants.NO_PACKET,
+                repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_1))
+
+        val questions = listOf(
+                MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), false /* isUnicast */))
+        val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+                emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+
+        // Reply to the question and verify there is one packet replied.
+        val reply = repository.getReply(query, src)
+        assertNotNull(reply)
+        assertEquals(1, repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_1))
+
+        // No package replied for unknown service.
+        assertEquals(MdnsConstants.NO_PACKET,
+                repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_2))
+    }
+
+    @Test
+    fun testIncludeInetAddressRecordsInProbing() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+            makeFlags(includeInetAddressesInProbing = true))
+        repository.updateAddresses(TEST_ADDRESSES)
+        assertEquals(0, repository.servicesCount)
+        assertEquals(-1,
+                repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
+        assertEquals(1, repository.servicesCount)
+
+        val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
+        assertNotNull(probingInfo)
+        assertTrue(repository.isProbing(TEST_SERVICE_ID_1))
+
+        assertEquals(TEST_SERVICE_ID_1, probingInfo.serviceId)
+        val packet = probingInfo.getPacket(0)
+
+        assertEquals(MdnsConstants.FLAGS_QUERY, packet.flags)
+        assertEquals(0, packet.answers.size)
+        assertEquals(0, packet.additionalRecords.size)
+
+        assertEquals(2, packet.questions.size)
+        val expectedName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+        assertContentEquals(listOf(
+            MdnsAnyRecord(expectedName, false /* unicast */),
+            MdnsAnyRecord(TEST_HOSTNAME, false /* unicast */),
+        ), packet.questions)
+
+        assertEquals(4, packet.authorityRecords.size)
+        assertContentEquals(listOf(
+            MdnsServiceRecord(
+                expectedName,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                SHORT_TTL /* ttlMillis */,
+                0 /* servicePriority */,
+                0 /* serviceWeight */,
+                TEST_PORT,
+                TEST_HOSTNAME),
+            MdnsInetAddressRecord(
+                TEST_HOSTNAME,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                SHORT_TTL /* ttlMillis */,
+                TEST_ADDRESSES[0].address),
+            MdnsInetAddressRecord(
+                TEST_HOSTNAME,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                SHORT_TTL /* ttlMillis */,
+                TEST_ADDRESSES[1].address),
+            MdnsInetAddressRecord(
+                TEST_HOSTNAME,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                SHORT_TTL /* ttlMillis */,
+                TEST_ADDRESSES[2].address)
+        ), packet.authorityRecords)
+
+        assertContentEquals(intArrayOf(TEST_SERVICE_ID_1), repository.clearServices())
+    }
+
+    private fun doGetReplyWithAnswersTest(
+            questions: List<MdnsRecord>,
+            knownAnswers: List<MdnsRecord>,
+            replyAnswers: List<MdnsRecord>,
+            additionalAnswers: List<MdnsRecord>
+    ) {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+            makeFlags(isKnownAnswerSuppressionEnabled = true))
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        val query = MdnsPacket(0 /* flags */, questions, knownAnswers,
+                emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val reply = repository.getReply(query, src)
+
+        if (replyAnswers.isEmpty() || additionalAnswers.isEmpty()) {
+            assertNull(reply)
+            return
+        }
+
+        assertNotNull(reply)
+        // Source address is IPv4
+        assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
+        assertEquals(replyAnswers, reply.answers)
+        assertEquals(additionalAnswers, reply.additionalAnswers)
+        assertEquals(knownAnswers, reply.knownAnswers)
+    }
+
+    @Test
+    fun testGetReply_HasAnswers() {
+        val queriedName = arrayOf("_testservice", "_tcp", "local")
+        val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+        val knownAnswers = listOf(MdnsPointerRecord(
+                arrayOf("_testservice", "_tcp", "local"),
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                LONG_TTL,
+                arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+        doGetReplyWithAnswersTest(questions, knownAnswers, emptyList() /* replyAnswers */,
+                emptyList() /* additionalAnswers */)
+    }
+
+    @Test
+    fun testGetReply_HasAnswers_TtlLessThanHalf() {
+        val queriedName = arrayOf("_testservice", "_tcp", "local")
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+        val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+        val knownAnswers = listOf(MdnsPointerRecord(
+                arrayOf("_testservice", "_tcp", "local"),
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                (LONG_TTL / 2 - 1000L),
+                arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+        val replyAnswers = listOf(MdnsPointerRecord(
+                queriedName,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                LONG_TTL,
+                serviceName))
+        val additionalAnswers = listOf(
+                MdnsTextRecord(
+                        serviceName,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        LONG_TTL,
+                        emptyList() /* entries */),
+                MdnsServiceRecord(
+                        serviceName,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        0 /* servicePriority */,
+                        0 /* serviceWeight */,
+                        TEST_PORT,
+                        TEST_HOSTNAME),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(
+                        serviceName,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        LONG_TTL,
+                        serviceName /* nextDomain */,
+                        intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+                MdnsNsecRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+        doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_HasAnotherAnswer() {
+        val queriedName = arrayOf("_testservice", "_tcp", "local")
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+        val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+        val knownAnswers = listOf(MdnsPointerRecord(
+                queriedName,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                LONG_TTL,
+                arrayOf("MyOtherTestService", "_testservice", "_tcp", "local")))
+        val replyAnswers = listOf(MdnsPointerRecord(
+                queriedName,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                LONG_TTL,
+                serviceName))
+        val additionalAnswers = listOf(
+                MdnsTextRecord(
+                        serviceName,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        LONG_TTL,
+                        emptyList() /* entries */),
+                MdnsServiceRecord(
+                        serviceName,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        0 /* servicePriority */,
+                        0 /* serviceWeight */,
+                        TEST_PORT,
+                        TEST_HOSTNAME),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(
+                        serviceName,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        LONG_TTL,
+                        serviceName /* nextDomain */,
+                        intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+                MdnsNsecRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+        doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_HasAnswers_MultiQuestions() {
+        val queriedName = arrayOf("_testservice", "_tcp", "local")
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+        val questions = listOf(
+                MdnsPointerRecord(queriedName, false /* isUnicast */),
+                MdnsServiceRecord(serviceName, false /* isUnicast */))
+        val knownAnswers = listOf(MdnsPointerRecord(
+                queriedName,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                LONG_TTL - 1000L,
+                serviceName))
+        val replyAnswers = listOf(MdnsServiceRecord(
+                serviceName,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                SHORT_TTL /* ttlMillis */,
+                0 /* servicePriority */,
+                0 /* serviceWeight */,
+                TEST_PORT,
+                TEST_HOSTNAME))
+        val additionalAnswers = listOf(
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_ADDRESSES[0].address),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_ADDRESSES[1].address),
+                MdnsInetAddressRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_ADDRESSES[2].address),
+                MdnsNsecRecord(
+                        TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        TEST_HOSTNAME /* nextDomain */,
+                        intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+        doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_HasAnswers_MultiQuestions_NoReply() {
+        val queriedName = arrayOf("_testservice", "_tcp", "local")
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+        val questions = listOf(
+                MdnsPointerRecord(queriedName, false /* isUnicast */),
+                MdnsServiceRecord(serviceName, false /* isUnicast */))
+        val knownAnswers = listOf(
+            MdnsPointerRecord(
+                queriedName,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                LONG_TTL - 1000L,
+                serviceName
+            ),
+            MdnsServiceRecord(
+                serviceName,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                SHORT_TTL - 15_000L,
+                0 /* servicePriority */,
+                0 /* serviceWeight */,
+                TEST_PORT,
+                TEST_HOSTNAME
+            )
+        )
+        doGetReplyWithAnswersTest(questions, knownAnswers, emptyList() /* replyAnswers */,
+                emptyList() /* additionalAnswers */)
+    }
+
+    @Test
+    fun testReplyUnicastToQueryUnicastQuestions() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        // Ask for 2 services, only the first one is known and requests unicast reply
+        val questions = listOf(
+            MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */),
+            MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), true /* isUnicast */))
+        val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+                emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+        // Reply to the question and verify it is sent to the source.
+        val reply = repository.getReply(query, src)
+        assertNotNull(reply)
+        assertEquals(src, reply.destination)
+    }
+
+    @Test
+    fun testReplyMulticastToQueryUnicastAndMulticastMixedQuestions() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply {
+            serviceType = "_otherservice._tcp"
+            serviceName = "OtherTestService"
+            port = TEST_PORT
+        })
+
+        // Ask for 2 services, both are known and only the first one requests unicast reply
+        val questions = listOf(
+            MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */),
+            MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), false /* isUnicast */))
+        val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+                emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+        // Reply to the question and verify it is sent multicast.
+        val reply = repository.getReply(query, src)
+        assertNotNull(reply)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
+    }
+
+    @Test
+    fun testReplyMulticastWhenNoUnicastQueryMatches() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        // Ask for 2 services, the first one requests a unicast reply but is unknown
+        val questions = listOf(
+            MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), true /* isUnicast */),
+            MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), false /* isUnicast */))
+        val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+                emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+        // Reply to the question and verify it is sent multicast.
+        val reply = repository.getReply(query, src)
+        assertNotNull(reply)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
+    }
+
+    @Test
+    fun testReplyMulticastWhenUnicastFeatureDisabled() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+            makeFlags(unicastReplyEnabled = false))
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        // The service is known and requests unicast reply, but the feature is disabled
+        val questions = listOf(
+            MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */))
+        val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+                emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+        // Reply to the question and verify it is sent multicast.
+        val reply = repository.getReply(query, src)
+        assertNotNull(reply)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
+    }
+
+    @Test
+    fun testGetReply_OnlyKnownAnswers() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+                makeFlags(isKnownAnswerSuppressionEnabled = true))
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        val knownAnswers = listOf(MdnsPointerRecord(
+                arrayOf("_testservice", "_tcp", "local"),
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                LONG_TTL - 1000L,
+                arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+        val query = MdnsPacket(MdnsConstants.FLAG_TRUNCATED /* flags */, emptyList(),
+                knownAnswers, emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val reply = repository.getReply(query, src)
+        assertNotNull(reply)
+        assertEquals(0, reply.answers.size)
+        assertEquals(0, reply.additionalAnswers.size)
+        assertEquals(knownAnswers, reply.knownAnswers)
+    }
+
+    @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(
     serviceId: Int,
     serviceInfo: NsdServiceInfo,
-    subtype: String? = null
+    subtypes: Set<String> = setOf(),
+    addresses: List<LinkAddress> = TEST_ADDRESSES
 ): AnnouncementInfo {
-    updateAddresses(TEST_ADDRESSES)
-    addService(serviceId, serviceInfo, subtype)
+    updateAddresses(addresses)
+    serviceInfo.setSubtypes(subtypes)
+    return addServiceAndFinishProbing(serviceId, serviceInfo)
+}
+
+private fun MdnsRecordRepository.addServiceAndFinishProbing(
+    serviceId: Int,
+    serviceInfo: NsdServiceInfo
+): AnnouncementInfo {
+    addService(serviceId, serviceInfo, null /* ttl */)
     val probingInfo = setServiceProbing(serviceId)
     assertNotNull(probingInfo)
     return onProbingSucceeded(probingInfo)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTest.kt
new file mode 100644
index 0000000..8f819b7
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.connectivity.mdns
+
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+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.S_V2)
+class MdnsRecordTest {
+
+    @Test
+    fun testPointerRecordHasSubType() {
+        val ptrRecord1 = MdnsPointerRecord(
+            arrayOf("_testtype", "_sub", "_tcp", "local"),
+            0L /* receiptTimeMillis */,
+            false /* cacheFlush */,
+            4500000 /* ttlMillis */,
+            arrayOf("testservice", "_testtype", "_tcp", "local")
+        )
+        val ptrRecord2 = MdnsPointerRecord(
+            arrayOf("_testtype", "_SUB", "_tcp", "local"),
+            0L /* receiptTimeMillis */,
+            false /* cacheFlush */,
+            4500000 /* ttlMillis */,
+            arrayOf("testservice", "_testtype", "_tcp", "local")
+        )
+        assertTrue(ptrRecord1.hasSubtype())
+        assertTrue(ptrRecord2.hasSubtype())
+    }
+
+    @Test
+    fun testEqualsCaseInsensitive() {
+        val ptrRecord1 = MdnsPointerRecord(
+            arrayOf("_testtype", "_tcp", "local"),
+            0L /* receiptTimeMillis */,
+            false /* cacheFlush */,
+            4500000 /* ttlMillis */,
+            arrayOf("testservice", "_testtype", "_tcp", "local")
+            )
+        val ptrRecord2 = MdnsPointerRecord(
+            arrayOf("_testType", "_tcp", "local"),
+            0L /* receiptTimeMillis */,
+            false /* cacheFlush */,
+            4500000 /* ttlMillis */,
+            arrayOf("testsErvice", "_testtype", "_Tcp", "local")
+        )
+        assertEquals(ptrRecord1, ptrRecord2)
+        assertEquals(ptrRecord1.hashCode(), ptrRecord2.hashCode())
+
+        val srvRecord1 = MdnsServiceRecord(
+            arrayOf("testservice", "_testtype", "_tcp", "local"),
+            123 /* receiptTimeMillis */,
+            false /* cacheFlush */,
+            2000 /* ttlMillis */,
+            0 /* servicePriority */,
+            0 /* serviceWeight */,
+            80 /* port */,
+            arrayOf("hostname")
+        )
+        val srvRecord2 = MdnsServiceRecord(
+            arrayOf("Testservice", "_testtype", "_tcp", "local"),
+            123 /* receiptTimeMillis */,
+            false /* cacheFlush */,
+            2000 /* ttlMillis */,
+            0 /* servicePriority */,
+            0 /* serviceWeight */,
+            80 /* port */,
+            arrayOf("Hostname")
+        )
+        assertEquals(srvRecord1, srvRecord2)
+        assertEquals(srvRecord1.hashCode(), srvRecord2.hashCode())
+
+        val nsecRecord1 = MdnsNsecRecord(
+            arrayOf("hostname"),
+            0L /* receiptTimeMillis */,
+            true /* cacheFlush */,
+            2000L, /* ttlMillis */
+            arrayOf("hostname"),
+            intArrayOf(1, 2, 3) /* types */
+        )
+        val nsecRecord2 = MdnsNsecRecord(
+            arrayOf("HOSTNAME"),
+            0L /* receiptTimeMillis */,
+            true /* cacheFlush */,
+            2000L, /* ttlMillis */
+            arrayOf("HOSTNAME"),
+            intArrayOf(1, 2, 3) /* types */
+        )
+        assertEquals(nsecRecord1, nsecRecord2)
+        assertEquals(nsecRecord1.hashCode(), nsecRecord2.hashCode())
+    }
+
+    @Test
+    fun testLabelsAreSuffix() {
+        val labels1 = arrayOf("a", "b", "c")
+        val labels2 = arrayOf("B", "C")
+        val labels3 = arrayOf("b", "c")
+        val labels4 = arrayOf("b", "d")
+        assertTrue(MdnsRecord.labelsAreSuffix(labels2, labels1))
+        assertTrue(MdnsRecord.labelsAreSuffix(labels3, labels1))
+        assertFalse(MdnsRecord.labelsAreSuffix(labels4, labels1))
+    }
+}
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..63548c1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -16,11 +16,13 @@
 
 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;
@@ -424,4 +426,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/MdnsReplySenderTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
new file mode 100644
index 0000000..9bd0530
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
@@ -0,0 +1,314 @@
+/*
+ * 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.connectivity.mdns
+
+import android.net.InetAddresses
+import android.net.LinkAddress
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Message
+import com.android.net.module.util.SharedLog
+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.MdnsReplySender.getReplyDestination
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import java.net.DatagramPacket
+import java.net.InetSocketAddress
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+private const val TEST_PORT = 12345
+private const val DEFAULT_TIMEOUT_MS = 2000L
+private const val LONG_TTL = 4_500_000L
+private const val SHORT_TTL = 120_000L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class MdnsReplySenderTest {
+    private val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+    private val otherServiceName = arrayOf("OtherTestService", "_testservice", "_tcp", "local")
+    private val serviceType = arrayOf("_testservice", "_tcp", "local")
+    private val source = InetSocketAddress(
+            InetAddresses.parseNumericAddress("192.0.2.1"), TEST_PORT)
+    private val hostname = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
+    private val otherHostname = arrayOf("Android_0F0E0D0C0B0A09080706050403020100", "local")
+    private val hostAddresses = listOf(
+            LinkAddress(InetAddresses.parseNumericAddress("192.0.2.111"), 24),
+            LinkAddress(InetAddresses.parseNumericAddress("2001:db8::111"), 64),
+            LinkAddress(InetAddresses.parseNumericAddress("2001:db8::222"), 64))
+    private val answers = listOf(
+            MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+                    LONG_TTL, serviceName))
+    private val otherAnswers = listOf(
+            MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+                    LONG_TTL, otherServiceName))
+    private val additionalAnswers = listOf(
+            MdnsTextRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
+                    emptyList() /* entries */),
+            MdnsServiceRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    SHORT_TTL, 0 /* servicePriority */, 0 /* serviceWeight */, TEST_PORT, hostname),
+            MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    SHORT_TTL, hostAddresses[0].address),
+            MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    SHORT_TTL, hostAddresses[1].address),
+            MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    SHORT_TTL, hostAddresses[2].address),
+            MdnsNsecRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
+                    serviceName /* nextDomain */,
+                    intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+            MdnsNsecRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */, SHORT_TTL,
+                    hostname /* nextDomain */, intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+    private val otherAdditionalAnswers = listOf(
+            MdnsTextRecord(otherServiceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    LONG_TTL, emptyList() /* entries */),
+            MdnsServiceRecord(otherServiceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    SHORT_TTL, 0 /* servicePriority */, 0 /* serviceWeight */, TEST_PORT,
+                    otherHostname),
+            MdnsInetAddressRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    SHORT_TTL, hostAddresses[0].address),
+            MdnsInetAddressRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    SHORT_TTL, hostAddresses[1].address),
+            MdnsInetAddressRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    SHORT_TTL, hostAddresses[2].address),
+            MdnsNsecRecord(otherServiceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    LONG_TTL, otherServiceName /* nextDomain */,
+                    intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+            MdnsNsecRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    SHORT_TTL, otherHostname /* nextDomain */,
+                    intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+    private val thread = HandlerThread(MdnsReplySenderTest::class.simpleName)
+    private val socket = mock(MdnsInterfaceSocket::class.java)
+    private val buffer = ByteArray(1500)
+    private val sharedLog = SharedLog(MdnsReplySenderTest::class.simpleName)
+    private val deps = mock(MdnsReplySender.Dependencies::class.java)
+    private val handler by lazy { Handler(thread.looper) }
+
+    @Before
+    fun setUp() {
+        thread.start()
+        doReturn(true).`when`(socket).hasJoinedIpv4()
+        doReturn(true).`when`(socket).hasJoinedIpv6()
+    }
+
+    @After
+    fun tearDown() {
+        thread.quitSafely()
+        thread.join()
+    }
+
+    private fun <T> runningOnHandlerAndReturn(functor: (() -> T)): T {
+        val future = CompletableFuture<T>()
+        handler.post {
+            future.complete(functor())
+        }
+        return future.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    private fun sendNow(sender: MdnsReplySender, packet: MdnsPacket, dest: InetSocketAddress):
+            Unit = runningOnHandlerAndReturn { sender.sendNow(packet, dest) }
+
+    private fun queueReply(sender: MdnsReplySender, reply: MdnsReplyInfo):
+            Unit = runningOnHandlerAndReturn { sender.queueReply(reply) }
+
+    private fun buildFlags(enableKAS: Boolean): MdnsFeatureFlags {
+        return MdnsFeatureFlags.newBuilder()
+                .setIsKnownAnswerSuppressionEnabled(enableKAS).build()
+    }
+
+    private fun createSender(enableKAS: Boolean): MdnsReplySender =
+            MdnsReplySender(thread.looper, socket, buffer, sharedLog, false /* enableDebugLog */,
+                    deps, buildFlags(enableKAS))
+
+    @Test
+    fun testSendNow() {
+        val replySender = createSender(enableKAS = false)
+        val packet = MdnsPacket(0x8400,
+                emptyList() /* questions */,
+                answers,
+                emptyList() /* authorityRecords */,
+                additionalAnswers)
+        sendNow(replySender, packet, IPV4_SOCKET_ADDR)
+        verify(socket).send(argThat{ it.socketAddress.equals(IPV4_SOCKET_ADDR) })
+    }
+
+    private fun verifyMessageQueued(
+            sender: MdnsReplySender,
+            replies: List<MdnsReplyInfo>
+    ): Pair<Handler, Message> {
+        val handlerCaptor = ArgumentCaptor.forClass(Handler::class.java)
+        val messageCaptor = ArgumentCaptor.forClass(Message::class.java)
+        for (reply in replies) {
+            queueReply(sender, reply)
+            verify(deps).sendMessageDelayed(
+                    handlerCaptor.capture(), messageCaptor.capture(), eq(reply.sendDelayMs))
+        }
+        return Pair(handlerCaptor.value, messageCaptor.value)
+    }
+
+    private fun verifyReplySent(
+            realHandler: Handler,
+            delayMessage: Message,
+            remainingAnswers: List<MdnsRecord>
+    ) {
+        val datagramPacketCaptor = ArgumentCaptor.forClass(DatagramPacket::class.java)
+        realHandler.sendMessage(delayMessage)
+        verify(socket, timeout(DEFAULT_TIMEOUT_MS)).send(datagramPacketCaptor.capture())
+
+        val dPacket = datagramPacketCaptor.value
+        val mdnsPacket = MdnsPacket.parse(MdnsPacketReader(
+                dPacket.data, dPacket.length, buildFlags(enableKAS = false)))
+        assertEquals(mdnsPacket.answers.toSet(), remainingAnswers.toSet())
+    }
+
+    @Test
+    fun testQueueReply() {
+        val replySender = createSender(enableKAS = false)
+        val reply = MdnsReplyInfo(answers, additionalAnswers, 20L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, emptyList())
+        val (handler, message) = verifyMessageQueued(replySender, listOf(reply))
+        verifyReplySent(handler, message, answers)
+    }
+
+    @Test
+    fun testQueueReply_KnownAnswerSuppressionEnabled() {
+        val replySender = createSender(enableKAS = true)
+        val reply = MdnsReplyInfo(answers, additionalAnswers, 20L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, emptyList())
+        val (handler, message) = verifyMessageQueued(replySender, listOf(reply))
+        verifyReplySent(handler, message, answers)
+    }
+
+    @Test
+    fun testQueueReply_MultiplePacket() {
+        val replySender = createSender(enableKAS = true)
+        val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, emptyList())
+        verifyMessageQueued(replySender, listOf(reply))
+
+        // Receive a known-answer packet and verify no message queued.
+        val knownAnswersReply = MdnsReplyInfo(emptyList(), emptyList(), 0L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, answers)
+        queueReply(replySender, knownAnswersReply)
+        verify(deps, times(1)).sendMessageDelayed(any(), any(), anyLong())
+    }
+
+    @Test
+    fun testQueueReply_MultiplePacket_LostSubsequentPacket() {
+        val replySender = createSender(enableKAS = true)
+        val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, emptyList())
+        val (handler, message) = verifyMessageQueued(replySender, listOf(reply))
+
+        // No subsequent packets
+        verifyReplySent(handler, message, answers)
+    }
+
+    @Test
+    fun testQueueReply_MultiplePacket_OtherKnownAnswer() {
+        val replySender = createSender(enableKAS = true)
+        val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, emptyList())
+        // Other known-answer service
+        val otherKnownAnswersReply = MdnsReplyInfo(emptyList(), emptyList(), 0L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, otherAnswers)
+        val (handler, message) = verifyMessageQueued(
+                replySender, listOf(reply, otherKnownAnswersReply))
+        verifyReplySent(handler, message, answers)
+    }
+
+    @Test
+    fun testQueueReply_MultiplePacket_TwoKnownAnswerPackets() {
+        val replySender = createSender(enableKAS = true)
+        val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, emptyList())
+        val firstKnownAnswerReply = MdnsReplyInfo(emptyList(), emptyList(), 401L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, otherAnswers)
+        verifyMessageQueued(replySender, listOf(reply, firstKnownAnswerReply))
+
+        // Second known-answer service
+        val secondKnownAnswerReply = MdnsReplyInfo(emptyList(), emptyList(), 0L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, answers)
+        queueReply(replySender, secondKnownAnswerReply)
+
+        // Verify that no reply is queued, as all answers are known.
+        verify(deps, times(2)).sendMessageDelayed(any(), any(), anyLong())
+    }
+
+    @Test
+    fun testQueueReply_MultiplePacket_LostSecondaryPacket() {
+        val replySender = createSender(enableKAS = true)
+        val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, emptyList())
+        val firstKnownAnswerReply = MdnsReplyInfo(emptyList(), emptyList(), 401L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, otherAnswers)
+        val (handler, message) = verifyMessageQueued(
+                replySender, listOf(reply, firstKnownAnswerReply))
+
+        // Second known-answer service lost
+        verifyReplySent(handler, message, answers)
+    }
+
+    @Test
+    fun testQueueReply_MultiplePacket_WithMultipleQuestions() {
+        val replySender = createSender(enableKAS = true)
+        val twoAnswers = listOf(
+                MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+                        LONG_TTL, serviceName),
+                MdnsServiceRecord(otherServiceName, 0L /* receiptTimeMillis */,
+                        true /* cacheFlush */, SHORT_TTL, 0 /* servicePriority */,
+                        0 /* serviceWeight */, TEST_PORT, otherHostname))
+        val reply = MdnsReplyInfo(twoAnswers, additionalAnswers, 400L /* sendDelayMs */,
+                IPV4_SOCKET_ADDR, source, emptyList())
+        val knownAnswersReply = MdnsReplyInfo(otherAnswers, otherAdditionalAnswers,
+                20L /* sendDelayMs */, IPV4_SOCKET_ADDR, source, answers)
+        val (handler, message) = verifyMessageQueued(replySender, listOf(reply, knownAnswersReply))
+
+        val remainingAnswers = listOf(
+                MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+                        LONG_TTL, otherServiceName),
+                MdnsServiceRecord(otherServiceName, 0L /* receiptTimeMillis */,
+                        true /* cacheFlush */, SHORT_TTL, 0 /* servicePriority */,
+                        0 /* serviceWeight */, TEST_PORT, otherHostname))
+        verifyReplySent(handler, message, remainingAnswers)
+    }
+
+    @Test
+    fun testGetReplyDestination() {
+        assertEquals(IPV4_SOCKET_ADDR, getReplyDestination(IPV4_SOCKET_ADDR, IPV4_SOCKET_ADDR))
+        assertEquals(IPV6_SOCKET_ADDR, getReplyDestination(IPV6_SOCKET_ADDR, IPV6_SOCKET_ADDR))
+        assertEquals(IPV4_SOCKET_ADDR, getReplyDestination(source, IPV4_SOCKET_ADDR))
+        assertEquals(IPV6_SOCKET_ADDR, getReplyDestination(source, IPV6_SOCKET_ADDR))
+        assertEquals(source, getReplyDestination(source, source))
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
index 0d479b1..a22e8c6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
@@ -18,7 +18,7 @@
 
 import static android.net.InetAddresses.parseNumericAddress;
 
-import static com.android.server.connectivity.mdns.MdnsResponseDecoder.Clock;
+import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -270,14 +270,9 @@
         assertEquals("st=0", textStrings.get(6));
     }
 
-    @Test
-    public void testDecodeIPv6AnswerPacket() throws IOException {
-        MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, CAST_SERVICE_TYPE);
-        assertNotNull(data6);
-
-        responses = decode(decoder, data6);
-        assertEquals(1, responses.size());
-        MdnsResponse response = responses.valueAt(0);
+    private void verifyResponse(ArraySet<MdnsResponse> responseArraySet) {
+        assertEquals(1, responseArraySet.size());
+        MdnsResponse response = responseArraySet.valueAt(0);
         assertTrue(response.isComplete());
 
         MdnsInetAddressRecord inet6AddressRecord = response.getInet6AddressRecord();
@@ -291,6 +286,22 @@
     }
 
     @Test
+    public void testDecodeIPv6AnswerPacket() throws IOException {
+        MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, CAST_SERVICE_TYPE);
+        assertNotNull(data6);
+        verifyResponse(decode(decoder, data6));
+    }
+
+    @Test
+    public void testDecodeCaseInsensitiveMatch() throws IOException {
+        final String[] castServiceTypeUpperCase =
+                new String[] {"_GOOGLECAST", "_TCP", "LOCAL"};
+        MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, castServiceTypeUpperCase);
+        assertNotNull(data6);
+        verifyResponse(decode(decoder, data6));
+    }
+
+    @Test
     public void testIsComplete() {
         MdnsResponse response = new MdnsResponse(responses.valueAt(0));
         assertTrue(response.isComplete());
@@ -328,13 +339,14 @@
         packet.setSocketAddress(
                 new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
 
-        final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(data6, data6.length);
+        final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(
+                data6, data6.length, MdnsFeatureFlags.newBuilder().build());
         assertNotNull(parsedPacket);
 
         final Network network = mock(Network.class);
-        responses = decoder.augmentResponses(parsedPacket,
+        responses = new ArraySet<>(decoder.augmentResponses(parsedPacket,
                 /* existingResponses= */ Collections.emptyList(),
-                /* interfaceIndex= */ 10, network /* expireOnExit= */).first;
+                /* interfaceIndex= */ 10, network /* expireOnExit= */).first);
 
         assertEquals(responses.size(), 1);
         assertEquals(responses.valueAt(0).getInterfaceIndex(), 10);
@@ -627,11 +639,12 @@
 
     private ArraySet<MdnsResponse> decode(MdnsResponseDecoder decoder, byte[] data,
             Collection<MdnsResponse> existingResponses) throws MdnsPacket.ParseException {
-        final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(data, data.length);
+        final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(
+                data, data.length, MdnsFeatureFlags.newBuilder().build());
         assertNotNull(parsedPacket);
 
-        return decoder.augmentResponses(parsedPacket,
+        return new ArraySet<>(decoder.augmentResponses(parsedPacket,
                 existingResponses,
-                MdnsSocket.INTERFACE_INDEX_UNSPECIFIED, mock(Network.class)).first;
+                MdnsSocket.INTERFACE_INDEX_UNSPECIFIED, mock(Network.class)).first);
     }
 }
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
index c10d9dd..3e189f1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
@@ -228,6 +228,12 @@
         final MdnsResponse response = makeCompleteResponse(TEST_TTL_MS, 0 /* receiptTimeMillis */);
 
         assertFalse(response.addPointerRecord(response.getPointerRecords().get(0)));
+        final String[] serviceName = new String[] { "MYSERVICE", "_TYPE", "_tcp", "local" };
+        final String[] serviceType = new String[] { "_TYPE", "_tcp", "local" };
+        MdnsPointerRecord pointerRecordCaseInsensitive =
+                new MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */,
+                        false /* cacheFlush */, TEST_TTL_MS, serviceName);
+        assertFalse(response.addPointerRecord(pointerRecordCaseInsensitive));
         assertFalse(response.addInet6AddressRecord(response.getInet6AddressRecord()));
         assertFalse(response.addInet4AddressRecord(response.getInet4AddressRecord()));
         assertFalse(response.setServiceRecord(response.getServiceRecord()));
@@ -310,4 +316,30 @@
         // All records were replaced, not added
         assertEquals(receiptTimeChangedResponse.getRecords().size(), response.getRecords().size());
     }
+
+    @Test
+    public void dropUnmatchedAddressRecords_caseInsensitive() {
+
+        final String[] hostname = new String[] { "MyHostname" };
+        final String[] upperCaseHostName = new String[] { "MYHOSTNAME" };
+        final String[] serviceName = new String[] { "MyService", "_type", "_tcp", "local" };
+        final String[] serviceType = new String[] { "_type", "_tcp", "local" };
+        final MdnsResponse response = new MdnsResponse(/* now= */ 0, serviceName, INTERFACE_INDEX,
+                mNetwork);
+        response.addPointerRecord(new MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */,
+                false /* cacheFlush */, TEST_TTL_MS, serviceName));
+        response.setServiceRecord(new MdnsServiceRecord(serviceName, 0L /* receiptTimeMillis */,
+                true /* cacheFlush */, TEST_TTL_MS, 0 /* servicePriority */,
+                0 /* serviceWeight */, 0 /* servicePort */, hostname));
+        response.setTextRecord(new MdnsTextRecord(serviceName, 0L /* receiptTimeMillis */,
+                true /* cacheFlush */, TEST_TTL_MS, emptyList() /* entries */));
+        response.addInet4AddressRecord(new MdnsInetAddressRecord(
+                upperCaseHostName , 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                TEST_TTL_MS, parseNumericAddress("192.0.2.123")));
+        response.addInet6AddressRecord(new MdnsInetAddressRecord(
+                upperCaseHostName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+                TEST_TTL_MS, parseNumericAddress("2001:db8::123")));
+
+        assertFalse(response.dropUnmatchedAddressRecords());
+    }
 }
\ No newline at end of file
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 f091eea..b040ab6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -16,10 +16,13 @@
 
 package com.android.server.connectivity.mdns
 
-import android.net.Network
 import android.os.Build
 import android.os.Handler
 import android.os.HandlerThread
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.server.connectivity.mdns.MdnsServiceCache.CacheKey
+import com.android.server.connectivity.mdns.MdnsServiceCacheTest.ExpiredRecord.ExpiredEvent.ServiceRecordExpired
+import com.android.server.connectivity.mdns.util.MdnsUtils
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import java.util.concurrent.CompletableFuture
@@ -32,25 +35,66 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
 
 private const val SERVICE_NAME_1 = "service-instance-1"
 private const val SERVICE_NAME_2 = "service-instance-2"
+private const val SERVICE_NAME_3 = "service-instance-3"
 private const val SERVICE_TYPE_1 = "_test1._tcp.local"
 private const val SERVICE_TYPE_2 = "_test2._tcp.local"
 private const val INTERFACE_INDEX = 999
 private const val DEFAULT_TIMEOUT_MS = 2000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
+private const val TEST_ELAPSED_REALTIME_MS = 123L
+private const val DEFAULT_TTL_TIME_MS = 120000L
 
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsServiceCacheTest {
-    private val network = mock(Network::class.java)
+    private val socketKey = SocketKey(null /* network */, INTERFACE_INDEX)
+    private val cacheKey1 = CacheKey(SERVICE_TYPE_1, socketKey)
+    private val cacheKey2 = CacheKey(SERVICE_TYPE_2, socketKey)
     private val thread = HandlerThread(MdnsServiceCacheTest::class.simpleName)
+    private val clock = mock(MdnsUtils.Clock::class.java)
     private val handler by lazy {
         Handler(thread.looper)
     }
-    private val serviceCache by lazy {
-        MdnsServiceCache(thread.looper)
+
+    private class ExpiredRecord : MdnsServiceCache.ServiceExpiredCallback {
+        val history = ArrayTrackRecord<ExpiredEvent>().newReadHead()
+
+        sealed class ExpiredEvent {
+            abstract val previousResponse: MdnsResponse
+            abstract val newResponse: MdnsResponse?
+            data class ServiceRecordExpired(
+                    override val previousResponse: MdnsResponse,
+                    override val newResponse: MdnsResponse?
+            ) : ExpiredEvent()
+        }
+
+        override fun onServiceRecordExpired(
+                previousResponse: MdnsResponse,
+                newResponse: MdnsResponse?
+        ) {
+            history.add(ServiceRecordExpired(previousResponse, newResponse))
+        }
+
+        fun expectedServiceRecordExpired(
+                serviceName: String,
+                timeoutMs: Long = DEFAULT_TIMEOUT_MS
+        ) {
+            val event = history.poll(timeoutMs)
+            assertNotNull(event)
+            assertTrue(event is ServiceRecordExpired)
+            assertEquals(serviceName, event.previousResponse.serviceInstanceName)
+        }
+
+        fun assertNoCallback() {
+            val cb = history.poll(NO_CALLBACK_TIMEOUT_MS)
+            assertNull("Expected no callback but got $cb", cb)
+        }
     }
 
     @Before
@@ -61,8 +105,14 @@
     @After
     fun tearDown() {
         thread.quitSafely()
+        thread.join()
     }
 
+    private fun makeFlags(isExpiredServicesRemovalEnabled: Boolean = false) =
+            MdnsFeatureFlags.Builder()
+                    .setIsExpiredServicesRemovalEnabled(isExpiredServicesRemovalEnabled)
+                    .build()
+
     private fun <T> runningOnHandlerAndReturn(functor: (() -> T)): T {
         val future = CompletableFuture<T>()
         handler.post {
@@ -71,39 +121,59 @@
         return future.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
     }
 
-    private fun addOrUpdateService(serviceType: String, network: Network, service: MdnsResponse):
-            Unit = runningOnHandlerAndReturn {
-        serviceCache.addOrUpdateService(serviceType, network, service) }
+    private fun addOrUpdateService(
+            serviceCache: MdnsServiceCache,
+            cacheKey: CacheKey,
+            service: MdnsResponse
+    ): Unit = runningOnHandlerAndReturn { serviceCache.addOrUpdateService(cacheKey, service) }
 
-    private fun removeService(serviceName: String, serviceType: String, network: Network):
-            Unit = runningOnHandlerAndReturn {
-        serviceCache.removeService(serviceName, serviceType, network) }
+    private fun removeService(
+            serviceCache: MdnsServiceCache,
+            serviceName: String,
+            cacheKey: CacheKey
+    ): Unit = runningOnHandlerAndReturn { serviceCache.removeService(serviceName, cacheKey) }
 
-    private fun getService(serviceName: String, serviceType: String, network: Network):
-            MdnsResponse? = runningOnHandlerAndReturn {
-        serviceCache.getCachedService(serviceName, serviceType, network) }
+    private fun getService(
+            serviceCache: MdnsServiceCache,
+            serviceName: String,
+            cacheKey: CacheKey
+    ): MdnsResponse? = runningOnHandlerAndReturn {
+        serviceCache.getCachedService(serviceName, cacheKey)
+    }
 
-    private fun getServices(serviceType: String, network: Network): List<MdnsResponse> =
-        runningOnHandlerAndReturn { serviceCache.getCachedServices(serviceType, network) }
+    private fun getServices(
+            serviceCache: MdnsServiceCache,
+            cacheKey: CacheKey
+    ): List<MdnsResponse> = runningOnHandlerAndReturn { serviceCache.getCachedServices(cacheKey) }
+
+    private fun registerServiceExpiredCallback(
+            serviceCache: MdnsServiceCache,
+            cacheKey: CacheKey,
+            callback: MdnsServiceCache.ServiceExpiredCallback
+    ) = runningOnHandlerAndReturn {
+        serviceCache.registerServiceExpiredCallback(cacheKey, callback)
+    }
 
     @Test
     fun testAddAndRemoveService() {
-        addOrUpdateService(SERVICE_TYPE_1, network, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
-        var response = getService(SERVICE_NAME_1, SERVICE_TYPE_1, network)
+        val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
+        addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+        var response = getService(serviceCache, SERVICE_NAME_1, cacheKey1)
         assertNotNull(response)
         assertEquals(SERVICE_NAME_1, response.serviceInstanceName)
-        removeService(SERVICE_NAME_1, SERVICE_TYPE_1, network)
-        response = getService(SERVICE_NAME_1, SERVICE_TYPE_1, network)
+        removeService(serviceCache, SERVICE_NAME_1, cacheKey1)
+        response = getService(serviceCache, SERVICE_NAME_1, cacheKey1)
         assertNull(response)
     }
 
     @Test
     fun testGetCachedServices_multipleServiceTypes() {
-        addOrUpdateService(SERVICE_TYPE_1, network, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
-        addOrUpdateService(SERVICE_TYPE_1, network, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
-        addOrUpdateService(SERVICE_TYPE_2, network, createResponse(SERVICE_NAME_2, SERVICE_TYPE_2))
+        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_2, SERVICE_TYPE_2))
 
-        val responses1 = getServices(SERVICE_TYPE_1, network)
+        val responses1 = getServices(serviceCache, cacheKey1)
         assertEquals(2, responses1.size)
         assertTrue(responses1.stream().anyMatch { response ->
             response.serviceInstanceName == SERVICE_NAME_1
@@ -111,26 +181,146 @@
         assertTrue(responses1.any { response ->
             response.serviceInstanceName == SERVICE_NAME_2
         })
-        val responses2 = getServices(SERVICE_TYPE_2, network)
+        val responses2 = getServices(serviceCache, cacheKey2)
         assertEquals(1, responses2.size)
         assertTrue(responses2.any { response ->
             response.serviceInstanceName == SERVICE_NAME_2
         })
 
-        removeService(SERVICE_NAME_2, SERVICE_TYPE_1, network)
-        val responses3 = getServices(SERVICE_TYPE_1, network)
+        removeService(serviceCache, SERVICE_NAME_2, cacheKey1)
+        val responses3 = getServices(serviceCache, cacheKey1)
         assertEquals(1, responses3.size)
         assertTrue(responses3.any { response ->
             response.serviceInstanceName == SERVICE_NAME_1
         })
-        val responses4 = getServices(SERVICE_TYPE_2, network)
+        val responses4 = getServices(serviceCache, cacheKey2)
         assertEquals(1, responses4.size)
         assertTrue(responses4.any { response ->
             response.serviceInstanceName == SERVICE_NAME_2
         })
     }
 
-    private fun createResponse(serviceInstanceName: String, serviceType: String) = MdnsResponse(
-        0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
-            INTERFACE_INDEX, network)
+    @Test
+    fun testServiceExpiredAndSendCallbacks() {
+        val serviceCache = MdnsServiceCache(
+                thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+        // Register service expired callbacks
+        val callback1 = ExpiredRecord()
+        val callback2 = ExpiredRecord()
+        registerServiceExpiredCallback(serviceCache, cacheKey1, callback1)
+        registerServiceExpiredCallback(serviceCache, cacheKey2, callback2)
+
+        doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
+
+        // Add multiple services with different ttl time.
+        addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1,
+                DEFAULT_TTL_TIME_MS))
+        addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1,
+                DEFAULT_TTL_TIME_MS + 20L))
+        addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_3, SERVICE_TYPE_2,
+                DEFAULT_TTL_TIME_MS + 10L))
+
+        // Check the service expiration immediately. Should be no callback.
+        assertEquals(2, getServices(serviceCache, cacheKey1).size)
+        assertEquals(1, getServices(serviceCache, cacheKey2).size)
+        callback1.assertNoCallback()
+        callback2.assertNoCallback()
+
+        // Simulate the case where the response is after TTL then check expired services.
+        // Expect SERVICE_NAME_1 expired.
+        doReturn(TEST_ELAPSED_REALTIME_MS + DEFAULT_TTL_TIME_MS).`when`(clock).elapsedRealtime()
+        assertEquals(1, getServices(serviceCache, cacheKey1).size)
+        assertEquals(1, getServices(serviceCache, cacheKey2).size)
+        callback1.expectedServiceRecordExpired(SERVICE_NAME_1)
+        callback2.assertNoCallback()
+
+        // Simulate the case where the response is after TTL then check expired services.
+        // Expect SERVICE_NAME_3 expired.
+        doReturn(TEST_ELAPSED_REALTIME_MS + DEFAULT_TTL_TIME_MS + 11L)
+                .`when`(clock).elapsedRealtime()
+        assertEquals(1, getServices(serviceCache, cacheKey1).size)
+        assertEquals(0, getServices(serviceCache, cacheKey2).size)
+        callback1.assertNoCallback()
+        callback2.expectedServiceRecordExpired(SERVICE_NAME_3)
+    }
+
+    @Test
+    fun testRemoveExpiredServiceWhenGetting() {
+        val serviceCache = MdnsServiceCache(
+                thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+
+        doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
+        addOrUpdateService(serviceCache, cacheKey1,
+                createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 1L /* ttlTime */))
+        doReturn(TEST_ELAPSED_REALTIME_MS + 2L).`when`(clock).elapsedRealtime()
+        assertNull(getService(serviceCache, SERVICE_NAME_1, cacheKey1))
+
+        addOrUpdateService(serviceCache, cacheKey2,
+                createResponse(SERVICE_NAME_2, SERVICE_TYPE_2, 3L /* ttlTime */))
+        doReturn(TEST_ELAPSED_REALTIME_MS + 4L).`when`(clock).elapsedRealtime()
+        assertEquals(0, getServices(serviceCache, cacheKey2).size)
+    }
+
+    @Test
+    fun testInsertResponseAndSortList() {
+        val responses = ArrayList<MdnsResponse>()
+        val response1 = createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 100L /* ttlTime */)
+        MdnsServiceCache.insertResponseAndSortList(responses, response1, TEST_ELAPSED_REALTIME_MS)
+        assertEquals(1, responses.size)
+        assertEquals(response1, responses[0])
+
+        val response2 = createResponse(SERVICE_NAME_2, SERVICE_TYPE_1, 50L /* ttlTime */)
+        MdnsServiceCache.insertResponseAndSortList(responses, response2, TEST_ELAPSED_REALTIME_MS)
+        assertEquals(2, responses.size)
+        assertEquals(response2, responses[0])
+        assertEquals(response1, responses[1])
+
+        val response3 = createResponse(SERVICE_NAME_3, SERVICE_TYPE_1, 75L /* ttlTime */)
+        MdnsServiceCache.insertResponseAndSortList(responses, response3, TEST_ELAPSED_REALTIME_MS)
+        assertEquals(3, responses.size)
+        assertEquals(response2, responses[0])
+        assertEquals(response3, responses[1])
+        assertEquals(response1, responses[2])
+
+        val response4 = createResponse("service-instance-4", SERVICE_TYPE_1, 125L /* ttlTime */)
+        MdnsServiceCache.insertResponseAndSortList(responses, response4, TEST_ELAPSED_REALTIME_MS)
+        assertEquals(4, responses.size)
+        assertEquals(response2, responses[0])
+        assertEquals(response3, responses[1])
+        assertEquals(response1, responses[2])
+        assertEquals(response4, responses[3])
+    }
+
+    private fun createResponse(
+            serviceInstanceName: String,
+            serviceType: String,
+            ttlTime: Long = 120000L
+    ): MdnsResponse {
+        val serviceName = "$serviceInstanceName.$serviceType".split(".").toTypedArray()
+        val response = MdnsResponse(
+                0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
+                socketKey.interfaceIndex, socketKey.network)
+
+        // Set PTR record
+        val pointerRecord = MdnsPointerRecord(
+                serviceType.split(".").toTypedArray(),
+                TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                ttlTime /* ttlMillis */,
+                serviceName)
+        response.addPointerRecord(pointerRecord)
+
+        // Set SRV record.
+        val serviceRecord = MdnsServiceRecord(
+                serviceName,
+                TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                ttlTime /* ttlMillis */,
+                0 /* servicePriority */,
+                0 /* serviceWeight */,
+                12345 /* port */,
+                arrayOf("hostname"))
+        response.serviceRecord = serviceRecord
+        return response
+    }
 }
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 e7d7a98..4ce8ba6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -35,6 +35,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 
@@ -53,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"));
@@ -72,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"));
@@ -92,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());
@@ -112,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());
@@ -130,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"));
@@ -149,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);
     }
@@ -202,7 +210,8 @@
                         List.of(),
                         /* textEntries= */ null,
                         /* interfaceIndex= */ 20,
-                        network);
+                        network,
+                        Instant.MAX /* expirationTime */);
 
         assertEquals(network, info2.getNetwork());
     }
@@ -225,7 +234,8 @@
                                 MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"),
                                 MdnsServiceInfo.TextEntry.fromString("test=")),
                         20 /* interfaceIndex */,
-                        new Network(123));
+                        new Network(123),
+                        Instant.MAX /* expirationTime */);
 
         beforeParcel.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
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 a61e8b2..44fa55c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -16,6 +16,13 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.ACTIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsServiceTypeClient.EVENT_START_QUERYTASK;
+import static com.android.server.connectivity.mdns.QueryTaskConfig.INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
+import static com.android.server.connectivity.mdns.QueryTaskConfig.MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS;
+import static com.android.server.connectivity.mdns.QueryTaskConfig.TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -25,14 +32,19 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 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;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -41,15 +53,20 @@
 import android.annotation.Nullable;
 import android.net.InetAddresses;
 import android.net.Network;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
 import android.text.TextUtils;
 
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
-import com.android.server.connectivity.mdns.MdnsServiceTypeClient.QueryTaskConfig;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -78,11 +95,14 @@
 import java.util.stream.Stream;
 
 /** Tests for {@link MdnsServiceTypeClient}. */
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class MdnsServiceTypeClientTests {
     private static final int INTERFACE_INDEX = 999;
+    private static final long DEFAULT_TIMEOUT = 2000L;
     private static final String SERVICE_TYPE = "_googlecast._tcp.local";
+    private static final String SUBTYPE = "_subtype";
     private static final String[] SERVICE_TYPE_LABELS = TextUtils.split(SERVICE_TYPE, "\\.");
     private static final InetSocketAddress IPV4_ADDRESS = new InetSocketAddress(
             MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
@@ -98,15 +118,15 @@
     @Mock
     private MdnsServiceBrowserListener mockListenerTwo;
     @Mock
-    private MdnsPacketWriter mockPacketWriter;
-    @Mock
     private MdnsMultinetworkSocketClient mockSocketClient;
     @Mock
     private Network mockNetwork;
     @Mock
-    private MdnsResponseDecoder.Clock mockDecoderClock;
+    private MdnsUtils.Clock mockDecoderClock;
     @Mock
     private SharedLog mockSharedLog;
+    @Mock
+    private MdnsServiceTypeClient.Dependencies mockDeps;
     @Captor
     private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
 
@@ -114,10 +134,17 @@
 
     private DatagramPacket[] expectedIPv4Packets;
     private DatagramPacket[] expectedIPv6Packets;
-    private ScheduledFuture<?>[] expectedSendFutures;
     private FakeExecutor currentThreadExecutor = new FakeExecutor();
 
     private MdnsServiceTypeClient client;
+    private SocketKey socketKey;
+    private HandlerThread thread;
+    private Handler handler;
+    private MdnsServiceCache serviceCache;
+    private long latestDelayMs = 0;
+    private Message delayMessage = null;
+    private Handler realHandler = null;
+    private MdnsFeatureFlags featureFlags = MdnsFeatureFlags.newBuilder().build();
 
     @Before
     @SuppressWarnings("DoNotMock")
@@ -125,68 +152,149 @@
         MockitoAnnotations.initMocks(this);
         doReturn(TEST_ELAPSED_REALTIME).when(mockDecoderClock).elapsedRealtime();
 
-        expectedIPv4Packets = new DatagramPacket[16];
-        expectedIPv6Packets = new DatagramPacket[16];
-        expectedSendFutures = new ScheduledFuture<?>[16];
+        expectedIPv4Packets = new DatagramPacket[24];
+        expectedIPv6Packets = new DatagramPacket[24];
+        socketKey = new SocketKey(mockNetwork, INTERFACE_INDEX);
 
-        for (int i = 0; i < expectedSendFutures.length; ++i) {
+        for (int i = 0; i < expectedIPv4Packets.length; ++i) {
             expectedIPv4Packets[i] = new DatagramPacket(buf, 0 /* offset */, 5 /* length */,
                     MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
             expectedIPv6Packets[i] = new DatagramPacket(buf, 0 /* offset */, 5 /* length */,
                     MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
-            expectedSendFutures[i] = Mockito.mock(ScheduledFuture.class);
         }
-        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]);
+        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]);
+        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]));
 
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, mockNetwork, mockSharedLog) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
+        thread = new HandlerThread("MdnsServiceTypeClientTests");
+        thread.start();
+        handler = new Handler(thread.getLooper());
+        serviceCache = new MdnsServiceCache(
+                thread.getLooper(),
+                MdnsFeatureFlags.newBuilder().setIsExpiredServicesRemovalEnabled(false).build(),
+                mockDecoderClock);
+
+        doAnswer(inv -> {
+            latestDelayMs = 0;
+            delayMessage = null;
+            return true;
+        }).when(mockDeps).removeMessages(any(Handler.class), eq(EVENT_START_QUERYTASK));
+
+        doAnswer(inv -> {
+            realHandler = (Handler) inv.getArguments()[0];
+            delayMessage = (Message) inv.getArguments()[1];
+            latestDelayMs = (long) inv.getArguments()[2];
+            return true;
+        }).when(mockDeps).sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
+
+        doAnswer(inv -> {
+            final Handler handler = (Handler) inv.getArguments()[0];
+            final Message message = (Message) inv.getArguments()[1];
+            runOnHandler(() -> handler.dispatchMessage(message));
+            return true;
+        }).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
+
+        client = makeMdnsServiceTypeClient();
+    }
+
+    private MdnsServiceTypeClient makeMdnsServiceTypeClient() {
+        return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+                serviceCache, featureFlags);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (thread != null) {
+            thread.quitSafely();
+            thread.join();
+        }
+    }
+
+    private void runOnHandler(Runnable r) {
+        handler.post(r);
+        HandlerUtils.waitForIdle(handler, DEFAULT_TIMEOUT);
+    }
+
+    private void startSendAndReceive(MdnsServiceBrowserListener listener,
+            MdnsSearchOptions searchOptions) {
+        runOnHandler(() -> client.startSendAndReceive(listener, searchOptions));
+    }
+
+    private void processResponse(MdnsPacket packet, SocketKey socketKey) {
+        runOnHandler(() -> client.processResponse(packet, socketKey));
+    }
+
+    private void stopSendAndReceive(MdnsServiceBrowserListener listener) {
+        runOnHandler(() -> client.stopSendAndReceive(listener));
+    }
+
+    private void notifySocketDestroyed() {
+        runOnHandler(() -> client.notifySocketDestroyed());
+    }
+
+    private void dispatchMessage() {
+        runOnHandler(() -> realHandler.dispatchMessage(delayMessage));
+        delayMessage = null;
     }
 
     @Test
     public void sendQueries_activeScanMode() {
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        // Always try to remove the task.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
 
         // First burst, 3 queries.
         verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
@@ -224,17 +332,21 @@
                 13, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
         verifyAndSendQuery(
                 14, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Verify that Task is not removed before stopSendAndReceive was called.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
 
         // Stop sending packets.
-        client.stopSendAndReceive(mockListenerOne);
-        verify(expectedSendFutures[15]).cancel(true);
+        stopSendAndReceive(mockListenerOne);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
     }
 
     @Test
     public void sendQueries_reentry_activeScanMode() {
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        // Always try to remove the task.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
 
         // First burst, first query is sent.
         verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
@@ -242,13 +354,13 @@
         // After the first query is sent, change the subtypes, and restart.
         searchOptions =
                 MdnsSearchOptions.newBuilder()
-                        .addSubtype("12345")
-                        .addSubtype("abcde")
-                        .setIsPassiveMode(false)
+                        .addSubtype(SUBTYPE)
+                        .addSubtype("_subtype2")
+                        .setQueryMode(ACTIVE_QUERY_MODE)
                         .build();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        startSendAndReceive(mockListenerOne, searchOptions);
         // The previous scheduled task should be canceled.
-        verify(expectedSendFutures[1]).cancel(true);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
 
         // Queries should continue to be sent.
         verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
@@ -258,15 +370,17 @@
                 3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
 
         // Stop sending packets.
-        client.stopSendAndReceive(mockListenerOne);
-        verify(expectedSendFutures[5]).cancel(true);
+        stopSendAndReceive(mockListenerOne);
+        verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
     }
 
     @Test
     public void sendQueries_passiveScanMode() {
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        // Always try to remove the task.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
 
         // First burst, 3 query.
         verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
@@ -282,15 +396,139 @@
                 false);
 
         // Stop sending packets.
-        client.stopSendAndReceive(mockListenerOne);
-        verify(expectedSendFutures[5]).cancel(true);
+        stopSendAndReceive(mockListenerOne);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+    }
+
+    @Test
+    public void sendQueries_activeScanWithQueryBackoff() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder()
+                        .addSubtype(SUBTYPE)
+                        .setQueryMode(ACTIVE_QUERY_MODE)
+                        .setNumOfQueriesBeforeBackoff(11).build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        // Always try to remove the task.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        // First burst, 3 queries.
+        verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+        verifyAndSendQuery(
+                1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Second burst will be sent after initialTimeBetweenBurstsMs, 3 queries.
+        verifyAndSendQuery(
+                3, MdnsConfigs.initialTimeBetweenBurstsMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                4, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                5, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Third burst will be sent after initialTimeBetweenBurstsMs * 2, 3 queries.
+        verifyAndSendQuery(
+                6, MdnsConfigs.initialTimeBetweenBurstsMs() * 2, /* expectsUnicastResponse= */
+                false);
+        verifyAndSendQuery(
+                7, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                8, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Forth burst will be sent after initialTimeBetweenBurstsMs * 4, 3 queries.
+        verifyAndSendQuery(
+                9, MdnsConfigs.initialTimeBetweenBurstsMs() * 4, /* expectsUnicastResponse= */
+                false);
+        verifyAndSendQuery(
+                10, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                11, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // In backoff mode, the current scheduled task will be canceled and reschedule if the
+        // 0.8 * smallestRemainingTtl is larger than time to next run.
+        long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        doReturn(true).when(mockDeps).hasMessages(any(), eq(EVENT_START_QUERYTASK));
+        processResponse(createResponse(
+                "service-instance-1", "192.0.2.123", 5353,
+                SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), socketKey);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        assertNotNull(delayMessage);
+        verifyAndSendQuery(12 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */,
+                false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                14 /* scheduledCount */);
+        currentTime += (long) (TEST_TTL / 2 * 0.8);
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        verifyAndSendQuery(13 /* index */, MdnsConfigs.timeBetweenQueriesInBurstMs(),
+                false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                15 /* scheduledCount */);
+    }
+
+    @Test
+    public void sendQueries_passiveScanWithQueryBackoff() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder()
+                        .addSubtype(SUBTYPE)
+                        .setQueryMode(PASSIVE_QUERY_MODE)
+                        .setNumOfQueriesBeforeBackoff(3).build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        // Always try to remove the task.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        verifyAndSendQuery(0 /* index */, 0 /* timeInMs */, true /* expectsUnicastResponse */,
+                true /* multipleSocketDiscovery */, 1 /* scheduledCount */);
+        verifyAndSendQuery(1 /* index */, MdnsConfigs.timeBetweenQueriesInBurstMs(),
+                false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                2 /* scheduledCount */);
+        verifyAndSendQuery(2 /* index */, MdnsConfigs.timeBetweenQueriesInBurstMs(),
+                false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                3 /* scheduledCount */);
+        verifyAndSendQuery(3 /* index */, MdnsConfigs.timeBetweenBurstsMs(),
+                false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                4 /* scheduledCount */);
+
+        // In backoff mode, the current scheduled task will be canceled and reschedule if the
+        // 0.8 * smallestRemainingTtl is larger than time to next run.
+        doReturn(TEST_ELAPSED_REALTIME + 20000).when(mockDecoderClock).elapsedRealtime();
+        doReturn(true).when(mockDeps).hasMessages(any(), eq(EVENT_START_QUERYTASK));
+        processResponse(createResponse(
+                "service-instance-1", "192.0.2.123", 5353,
+                SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), socketKey);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        assertNotNull(delayMessage);
+        verifyAndSendQuery(4 /* index */, 80000 /* timeInMs */, false /* expectsUnicastResponse */,
+                true /* multipleSocketDiscovery */, 6 /* scheduledCount */);
+        // Next run should also be scheduled in 0.8 * smallestRemainingTtl
+        verifyAndSendQuery(5 /* index */, 80000 /* timeInMs */, false /* expectsUnicastResponse */,
+                true /* multipleSocketDiscovery */, 7 /* scheduledCount */);
+
+        // If the records is not refreshed, the current scheduled task will not be canceled.
+        doReturn(TEST_ELAPSED_REALTIME + 20001).when(mockDecoderClock).elapsedRealtime();
+        processResponse(createResponse(
+                "service-instance-1", "192.0.2.123", 5353,
+                SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL,
+                TEST_ELAPSED_REALTIME - 1), socketKey);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        // In backoff mode, the current scheduled task will not be canceled if the
+        // 0.8 * smallestRemainingTtl is smaller than time to next run.
+        doReturn(TEST_ELAPSED_REALTIME).when(mockDecoderClock).elapsedRealtime();
+        processResponse(createResponse(
+                "service-instance-1", "192.0.2.123", 5353,
+                SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), socketKey);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        stopSendAndReceive(mockListenerOne);
+        verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
     }
 
     @Test
     public void sendQueries_reentry_passiveScanMode() {
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        // Always try to remove the task.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
 
         // First burst, first query is sent.
         verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
@@ -298,13 +536,13 @@
         // After the first query is sent, change the subtypes, and restart.
         searchOptions =
                 MdnsSearchOptions.newBuilder()
-                        .addSubtype("12345")
-                        .addSubtype("abcde")
-                        .setIsPassiveMode(true)
+                        .addSubtype(SUBTYPE)
+                        .addSubtype("_subtype2")
+                        .setQueryMode(PASSIVE_QUERY_MODE)
                         .build();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        startSendAndReceive(mockListenerOne, searchOptions);
         // The previous scheduled task should be canceled.
-        verify(expectedSendFutures[1]).cancel(true);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
 
         // Queries should continue to be sent.
         verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
@@ -314,22 +552,23 @@
                 3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
 
         // Stop sending packets.
-        client.stopSendAndReceive(mockListenerOne);
-        verify(expectedSendFutures[5]).cancel(true);
+        stopSendAndReceive(mockListenerOne);
+        verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
     }
 
     @Test
     @Ignore("MdnsConfigs is not configurable currently.")
     public void testQueryTaskConfig_alwaysAskForUnicastResponse() {
         //MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
         QueryTaskConfig config = new QueryTaskConfig(
-                searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1, mockNetwork);
+                searchOptions.getQueryMode(),
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
+                socketKey);
 
         // This is the first query. We will ask for unicast response.
         assertTrue(config.expectUnicastResponse);
-        assertEquals(config.subtypes, searchOptions.getSubtypes());
         assertEquals(config.transactionId, 1);
 
         // For the rest of queries in this burst, we will NOT ask for unicast response.
@@ -337,7 +576,6 @@
             int oldTransactionId = config.transactionId;
             config = config.getConfigForNextRun();
             assertFalse(config.expectUnicastResponse);
-            assertEquals(config.subtypes, searchOptions.getSubtypes());
             assertEquals(config.transactionId, oldTransactionId + 1);
         }
 
@@ -345,20 +583,20 @@
         int oldTransactionId = config.transactionId;
         config = config.getConfigForNextRun();
         assertTrue(config.expectUnicastResponse);
-        assertEquals(config.subtypes, searchOptions.getSubtypes());
         assertEquals(config.transactionId, oldTransactionId + 1);
     }
 
     @Test
     public void testQueryTaskConfig_askForUnicastInFirstQuery() {
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
         QueryTaskConfig config = new QueryTaskConfig(
-                searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1, mockNetwork);
+                searchOptions.getQueryMode(),
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
+                socketKey);
 
         // This is the first query. We will ask for unicast response.
         assertTrue(config.expectUnicastResponse);
-        assertEquals(config.subtypes, searchOptions.getSubtypes());
         assertEquals(config.transactionId, 1);
 
         // For the rest of queries in this burst, we will NOT ask for unicast response.
@@ -366,7 +604,6 @@
             int oldTransactionId = config.transactionId;
             config = config.getConfigForNextRun();
             assertFalse(config.expectUnicastResponse);
-            assertEquals(config.subtypes, searchOptions.getSubtypes());
             assertEquals(config.transactionId, oldTransactionId + 1);
         }
 
@@ -374,27 +611,24 @@
         int oldTransactionId = config.transactionId;
         config = config.getConfigForNextRun();
         assertFalse(config.expectUnicastResponse);
-        assertEquals(config.subtypes, searchOptions.getSubtypes());
         assertEquals(config.transactionId, oldTransactionId + 1);
     }
 
     @Test
-    @Ignore("MdnsConfigs is not configurable currently.")
     public void testIfPreviousTaskIsCanceledWhenNewSessionStarts() {
-        //MdnsConfigsFlagsImpl.useSessionIdToScheduleMdnsTask.override(true);
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
+        startSendAndReceive(mockListenerOne, searchOptions);
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
         // Change the sutypes and start a new session.
         searchOptions =
                 MdnsSearchOptions.newBuilder()
-                        .addSubtype("12345")
-                        .addSubtype("abcde")
-                        .setIsPassiveMode(true)
+                        .addSubtype(SUBTYPE)
+                        .addSubtype("_subtype2")
+                        .setQueryMode(PASSIVE_QUERY_MODE)
                         .build();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        startSendAndReceive(mockListenerOne, searchOptions);
 
         // Clear the scheduled runnable.
         currentThreadExecutor.getAndClearLastScheduledRunnable();
@@ -411,11 +645,11 @@
     @Ignore("MdnsConfigs is not configurable currently.")
     public void testIfPreviousTaskIsCanceledWhenSessionStops() {
         //MdnsConfigsFlagsImpl.shouldCancelScanTaskWhenFutureIsNull.override(true);
-        MdnsSearchOptions searchOptions =
-                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
+        startSendAndReceive(mockListenerOne, searchOptions);
         // Change the sutypes and start a new session.
-        client.stopSendAndReceive(mockListenerOne);
+        stopSendAndReceive(mockListenerOne);
         // Clear the scheduled runnable.
         currentThreadExecutor.getAndClearLastScheduledRunnable();
 
@@ -430,35 +664,108 @@
     @Test
     public void testQueryScheduledWhenAnsweredFromCache() {
         final MdnsSearchOptions searchOptions = MdnsSearchOptions.getDefaultOptions();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        startSendAndReceive(mockListenerOne, searchOptions);
         assertNotNull(currentThreadExecutor.getAndClearSubmittedRunnable());
 
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                 "service-instance-1", "192.0.2.123", 5353,
                 SERVICE_TYPE_LABELS,
-                Collections.emptyMap(), TEST_TTL), /* interfaceIndex= */ 20, mockNetwork);
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
-        verify(mockListenerOne).onServiceNameDiscovered(any());
-        verify(mockListenerOne).onServiceFound(any());
+        verify(mockListenerOne).onServiceNameDiscovered(any(), eq(false) /* isServiceFromCache */);
+        verify(mockListenerOne).onServiceFound(any(), eq(false) /* isServiceFromCache */);
 
         // File another identical query
-        client.startSendAndReceive(mockListenerTwo, searchOptions);
+        startSendAndReceive(mockListenerTwo, searchOptions);
 
-        verify(mockListenerTwo).onServiceNameDiscovered(any());
-        verify(mockListenerTwo).onServiceFound(any());
+        verify(mockListenerTwo).onServiceNameDiscovered(any(), eq(true) /* isServiceFromCache */);
+        verify(mockListenerTwo).onServiceFound(any(), eq(true) /* isServiceFromCache */);
 
         // This time no query is submitted, only scheduled
         assertNull(currentThreadExecutor.getAndClearSubmittedRunnable());
-        assertNotNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
         // This just skips the first query of the first burst
-        assertEquals(MdnsConfigs.timeBetweenQueriesInBurstMs(),
-                currentThreadExecutor.getAndClearLastScheduledDelayInMs());
+        verify(mockDeps).sendMessageDelayed(
+                any(), any(), eq(MdnsConfigs.timeBetweenQueriesInBurstMs()));
+    }
+
+    @Test
+    public void testCombinedSubtypesQueriedWithMultipleListeners() throws Exception {
+        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.getAndClearLastScheduledRunnable().run();
+
+        InOrder inOrder = inOrder(mockListenerOne, mockSocketClient, mockDeps);
+
+        // Verify the query asks for subtype1
+        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().get(0)));
+
+        assertEquals(2, subtype1Query.questions.size());
+        assertTrue(hasQuestion(subtype1Query, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertTrue(hasQuestion(subtype1Query, MdnsRecord.TYPE_PTR,
+                getServiceTypeWithSubtype("_subtype1")));
+
+        // Add subtype2
+        startSendAndReceive(mockListenerTwo, searchOptions2);
+        inOrder.verify(mockDeps).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+
+        final ArgumentCaptor<List<DatagramPacket>> combinedSubtypesQueryCaptor =
+                ArgumentCaptor.forClass(List.class);
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+                combinedSubtypesQueryCaptor.capture(),
+                eq(socketKey), eq(false));
+        // The next query must have been scheduled
+        inOrder.verify(mockDeps).sendMessageDelayed(any(), any(), anyLong());
+
+        final MdnsPacket combinedSubtypesQuery = MdnsPacket.parse(
+                new MdnsPacketReader(combinedSubtypesQueryCaptor.getValue().get(0)));
+
+        assertEquals(3, combinedSubtypesQuery.questions.size());
+        assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR,
+                getServiceTypeWithSubtype("_subtype1")));
+        assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR,
+                getServiceTypeWithSubtype("_subtype2")));
+
+        // Remove subtype1
+        stopSendAndReceive(mockListenerOne);
+
+        // Queries are not rescheduled, but the next query is affected
+        dispatchMessage();
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+
+        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().get(0)));
+
+        assertEquals(2, subtype2Query.questions.size());
+        assertTrue(hasQuestion(subtype2Query, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertTrue(hasQuestion(subtype2Query, MdnsRecord.TYPE_PTR,
+                getServiceTypeWithSubtype("_subtype2")));
     }
 
     private static void verifyServiceInfo(MdnsServiceInfo serviceInfo, String serviceName,
             String[] serviceType, List<String> ipv4Addresses, List<String> ipv6Addresses, int port,
-            List<String> subTypes, Map<String, String> attributes, int interfaceIndex,
-            Network network) {
+            List<String> subTypes, Map<String, String> attributes, SocketKey socketKey) {
         assertEquals(serviceName, serviceInfo.getServiceInstanceName());
         assertArrayEquals(serviceType, serviceInfo.getServiceType());
         assertEquals(ipv4Addresses, serviceInfo.getIpv4Addresses());
@@ -469,19 +776,20 @@
             assertTrue(attributes.containsKey(key));
             assertEquals(attributes.get(key), serviceInfo.getAttributeByKey(key));
         }
-        assertEquals(interfaceIndex, serviceInfo.getInterfaceIndex());
-        assertEquals(network, serviceInfo.getNetwork());
+        assertEquals(socketKey.getInterfaceIndex(), serviceInfo.getInterfaceIndex());
+        assertEquals(socketKey.getNetwork(), serviceInfo.getNetwork());
     }
 
     @Test
     public void processResponse_incompleteResponse() {
-        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
 
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                 "service-instance-1", null /* host */, 0 /* port */,
                 SERVICE_TYPE_LABELS,
-                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
-        verify(mockListenerOne).onServiceNameDiscovered(serviceInfoCaptor.capture());
+                Collections.emptyMap(), TEST_TTL), socketKey);
+        verify(mockListenerOne).onServiceNameDiscovered(
+                serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
         verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
                 "service-instance-1",
                 SERVICE_TYPE_LABELS,
@@ -490,54 +798,52 @@
                 /* port= */ 0,
                 /* subTypes= */ List.of(),
                 Collections.emptyMap(),
-                INTERFACE_INDEX,
-                mockNetwork);
+                socketKey);
 
-        verify(mockListenerOne, never()).onServiceFound(any(MdnsServiceInfo.class));
+        verify(mockListenerOne, never()).onServiceFound(any(MdnsServiceInfo.class), anyBoolean());
         verify(mockListenerOne, never()).onServiceUpdated(any(MdnsServiceInfo.class));
     }
 
     @Test
     public void processIPv4Response_completeResponseForNewServiceInstance() throws Exception {
         final String ipV4Address = "192.168.1.1";
-        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
 
         // Process the initial response.
-        client.processResponse(createResponse(
-                "service-instance-1", ipV4Address, 5353,
-                /* subtype= */ "ABCDE",
-                Collections.emptyMap(), TEST_TTL), /* interfaceIndex= */ 20, mockNetwork);
+        processResponse(createResponse(
+                "service-instance-1", ipV4Address, 5353, SUBTYPE,
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
         // Process a second response with a different port and updated text attributes.
-        client.processResponse(createResponse(
-                "service-instance-1", ipV4Address, 5354,
-                /* subtype= */ "ABCDE",
-                Collections.singletonMap("key", "value"), TEST_TTL),
-                /* interfaceIndex= */ 20, mockNetwork);
+        processResponse(createResponse(
+                        "service-instance-1", ipV4Address, 5354, SUBTYPE,
+                        Collections.singletonMap("key", "value"), TEST_TTL),
+                socketKey);
 
         // Verify onServiceNameDiscovered was called once for the initial response.
-        verify(mockListenerOne).onServiceNameDiscovered(serviceInfoCaptor.capture());
+        verify(mockListenerOne).onServiceNameDiscovered(
+                serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
         verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
                 "service-instance-1",
                 SERVICE_TYPE_LABELS,
                 List.of(ipV4Address) /* ipv4Address */,
                 List.of() /* ipv6Address */,
                 5353 /* port */,
-                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
                 Collections.singletonMap("key", null) /* attributes */,
-                20 /* interfaceIndex */,
-                mockNetwork);
+                socketKey);
 
         // Verify onServiceFound was called once for the initial response.
-        verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        verify(mockListenerOne).onServiceFound(
+                serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
         MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(1);
         assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
         assertEquals(initialServiceInfo.getIpv4Address(), ipV4Address);
         assertEquals(initialServiceInfo.getPort(), 5353);
-        assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList(SUBTYPE));
         assertNull(initialServiceInfo.getAttributeByKey("key"));
-        assertEquals(initialServiceInfo.getInterfaceIndex(), 20);
-        assertEquals(mockNetwork, initialServiceInfo.getNetwork());
+        assertEquals(socketKey.getInterfaceIndex(), initialServiceInfo.getInterfaceIndex());
+        assertEquals(socketKey.getNetwork(), initialServiceInfo.getNetwork());
 
         // Verify onServiceUpdated was called once for the second response.
         verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
@@ -546,53 +852,52 @@
         assertEquals(updatedServiceInfo.getIpv4Address(), ipV4Address);
         assertEquals(updatedServiceInfo.getPort(), 5354);
         assertTrue(updatedServiceInfo.hasSubtypes());
-        assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList(SUBTYPE));
         assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
-        assertEquals(updatedServiceInfo.getInterfaceIndex(), 20);
-        assertEquals(mockNetwork, updatedServiceInfo.getNetwork());
+        assertEquals(socketKey.getInterfaceIndex(), updatedServiceInfo.getInterfaceIndex());
+        assertEquals(socketKey.getNetwork(), updatedServiceInfo.getNetwork());
     }
 
     @Test
     public void processIPv6Response_getCorrectServiceInfo() throws Exception {
         final String ipV6Address = "2000:3333::da6c:63ff:fe7c:7483";
-        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
 
         // Process the initial response.
-        client.processResponse(createResponse(
-                "service-instance-1", ipV6Address, 5353,
-                /* subtype= */ "ABCDE",
-                Collections.emptyMap(), TEST_TTL), /* interfaceIndex= */ 20, mockNetwork);
+        processResponse(createResponse(
+                "service-instance-1", ipV6Address, 5353, SUBTYPE,
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
         // Process a second response with a different port and updated text attributes.
-        client.processResponse(createResponse(
-                "service-instance-1", ipV6Address, 5354,
-                /* subtype= */ "ABCDE",
-                Collections.singletonMap("key", "value"), TEST_TTL),
-                /* interfaceIndex= */ 20, mockNetwork);
+        processResponse(createResponse(
+                        "service-instance-1", ipV6Address, 5354, SUBTYPE,
+                        Collections.singletonMap("key", "value"), TEST_TTL),
+                socketKey);
 
         // Verify onServiceNameDiscovered was called once for the initial response.
-        verify(mockListenerOne).onServiceNameDiscovered(serviceInfoCaptor.capture());
+        verify(mockListenerOne).onServiceNameDiscovered(
+                serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
         verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
                 "service-instance-1",
                 SERVICE_TYPE_LABELS,
                 List.of() /* ipv4Address */,
                 List.of(ipV6Address) /* ipv6Address */,
                 5353 /* port */,
-                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
                 Collections.singletonMap("key", null) /* attributes */,
-                20 /* interfaceIndex */,
-                mockNetwork);
+                socketKey);
 
         // Verify onServiceFound was called once for the initial response.
-        verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        verify(mockListenerOne).onServiceFound(
+                serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
         MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(1);
         assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
         assertEquals(initialServiceInfo.getIpv6Address(), ipV6Address);
         assertEquals(initialServiceInfo.getPort(), 5353);
-        assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList(SUBTYPE));
         assertNull(initialServiceInfo.getAttributeByKey("key"));
-        assertEquals(initialServiceInfo.getInterfaceIndex(), 20);
-        assertEquals(mockNetwork, initialServiceInfo.getNetwork());
+        assertEquals(socketKey.getInterfaceIndex(), initialServiceInfo.getInterfaceIndex());
+        assertEquals(socketKey.getNetwork(), initialServiceInfo.getNetwork());
 
         // Verify onServiceUpdated was called once for the second response.
         verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
@@ -601,10 +906,10 @@
         assertEquals(updatedServiceInfo.getIpv6Address(), ipV6Address);
         assertEquals(updatedServiceInfo.getPort(), 5354);
         assertTrue(updatedServiceInfo.hasSubtypes());
-        assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList(SUBTYPE));
         assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
-        assertEquals(updatedServiceInfo.getInterfaceIndex(), 20);
-        assertEquals(mockNetwork, updatedServiceInfo.getNetwork());
+        assertEquals(socketKey.getInterfaceIndex(), updatedServiceInfo.getInterfaceIndex());
+        assertEquals(socketKey.getNetwork(), updatedServiceInfo.getNetwork());
     }
 
     private void verifyServiceRemovedNoCallback(MdnsServiceBrowserListener listener) {
@@ -613,119 +918,114 @@
     }
 
     private void verifyServiceRemovedCallback(MdnsServiceBrowserListener listener,
-            String serviceName, String[] serviceType, int interfaceIndex, Network network) {
+            String serviceName, String[] serviceType, SocketKey socketKey) {
         verify(listener).onServiceRemoved(argThat(
                 info -> serviceName.equals(info.getServiceInstanceName())
                         && Arrays.equals(serviceType, info.getServiceType())
-                        && info.getInterfaceIndex() == interfaceIndex
-                        && network.equals(info.getNetwork())));
+                        && info.getInterfaceIndex() == socketKey.getInterfaceIndex()
+                        && socketKey.getNetwork().equals(info.getNetwork())));
         verify(listener).onServiceNameRemoved(argThat(
                 info -> serviceName.equals(info.getServiceInstanceName())
                         && Arrays.equals(serviceType, info.getServiceType())
-                        && info.getInterfaceIndex() == interfaceIndex
-                        && network.equals(info.getNetwork())));
+                        && info.getInterfaceIndex() == socketKey.getInterfaceIndex()
+                        && socketKey.getNetwork().equals(info.getNetwork())));
     }
 
     @Test
     public void processResponse_goodBye() throws Exception {
-        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
-        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
 
         final String serviceName = "service-instance-1";
         final String ipV6Address = "2000:3333::da6c:63ff:fe7c:7483";
         // Process the initial response.
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                 serviceName, ipV6Address, 5353,
                 SERVICE_TYPE_LABELS,
-                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                 "goodbye-service", ipV6Address, 5353,
                 SERVICE_TYPE_LABELS,
-                Collections.emptyMap(), /* ptrTtlMillis= */ 0L), INTERFACE_INDEX, mockNetwork);
+                Collections.emptyMap(), /* ptrTtlMillis= */ 0L), socketKey);
 
         // Verify removed callback won't be called if the service is not existed.
         verifyServiceRemovedNoCallback(mockListenerOne);
         verifyServiceRemovedNoCallback(mockListenerTwo);
 
         // Verify removed callback would be called.
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                 serviceName, ipV6Address, 5353,
                 SERVICE_TYPE_LABELS,
-                Collections.emptyMap(), 0L), INTERFACE_INDEX, mockNetwork);
+                Collections.emptyMap(), 0L), socketKey);
         verifyServiceRemovedCallback(
-                mockListenerOne, serviceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX, mockNetwork);
+                mockListenerOne, serviceName, SERVICE_TYPE_LABELS, socketKey);
         verifyServiceRemovedCallback(
-                mockListenerTwo, serviceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX, mockNetwork);
+                mockListenerTwo, serviceName, SERVICE_TYPE_LABELS, socketKey);
     }
 
     @Test
     public void reportExistingServiceToNewlyRegisteredListeners() throws Exception {
         // Process the initial response.
-        client.processResponse(createResponse(
-                "service-instance-1", "192.168.1.1", 5353,
-                /* subtype= */ "ABCDE",
-                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+        processResponse(createResponse(
+                "service-instance-1", "192.168.1.1", 5353, SUBTYPE,
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
-        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
 
         // Verify onServiceNameDiscovered was called once for the existing response.
-        verify(mockListenerOne).onServiceNameDiscovered(serviceInfoCaptor.capture());
+        verify(mockListenerOne).onServiceNameDiscovered(
+                serviceInfoCaptor.capture(), eq(true) /* isServiceFromCache */);
         verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
                 "service-instance-1",
                 SERVICE_TYPE_LABELS,
                 List.of("192.168.1.1") /* ipv4Address */,
                 List.of() /* ipv6Address */,
                 5353 /* port */,
-                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
                 Collections.singletonMap("key", null) /* attributes */,
-                INTERFACE_INDEX,
-                mockNetwork);
+                socketKey);
 
         // Verify onServiceFound was called once for the existing response.
-        verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        verify(mockListenerOne).onServiceFound(
+                serviceInfoCaptor.capture(), eq(true) /* isServiceFromCache */);
         MdnsServiceInfo existingServiceInfo = serviceInfoCaptor.getAllValues().get(1);
         assertEquals(existingServiceInfo.getServiceInstanceName(), "service-instance-1");
         assertEquals(existingServiceInfo.getIpv4Address(), "192.168.1.1");
         assertEquals(existingServiceInfo.getPort(), 5353);
-        assertEquals(existingServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertEquals(existingServiceInfo.getSubtypes(), Collections.singletonList(SUBTYPE));
         assertNull(existingServiceInfo.getAttributeByKey("key"));
 
         // Process a goodbye message for the existing response.
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                 "service-instance-1", "192.168.1.1", 5353,
                 SERVICE_TYPE_LABELS,
-                Collections.emptyMap(), /* ptrTtlMillis= */ 0L), INTERFACE_INDEX, mockNetwork);
+                Collections.emptyMap(), /* ptrTtlMillis= */ 0L), socketKey);
 
-        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
 
         // Verify onServiceFound was not called on the newly registered listener after the existing
         // response is gone.
-        verify(mockListenerTwo, never()).onServiceNameDiscovered(any(MdnsServiceInfo.class));
-        verify(mockListenerTwo, never()).onServiceFound(any(MdnsServiceInfo.class));
+        verify(mockListenerTwo, never()).onServiceNameDiscovered(
+                any(MdnsServiceInfo.class), eq(false));
+        verify(mockListenerTwo, never()).onServiceFound(any(MdnsServiceInfo.class), anyBoolean());
     }
 
     @Test
     public void processResponse_searchOptionsEnableServiceRemoval_shouldRemove()
             throws Exception {
         final String serviceInstanceName = "service-instance-1";
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, mockNetwork, mockSharedLog) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
-        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder().setRemoveExpiredService(
-                true).build();
-        client.startSendAndReceive(mockListenerOne, searchOptions);
+        MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .setRemoveExpiredService(true)
+                .setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
+                .build();
+        startSendAndReceive(mockListenerOne, searchOptions);
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
         // Process the initial response.
-        client.processResponse(createResponse(
-                serviceInstanceName, "192.168.1.1", 5353, /* subtype= */ "ABCDE",
-                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+        processResponse(createResponse(
+                serviceInstanceName, "192.168.1.1", 5353, SUBTYPE,
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
         // Clear the scheduled runnable.
         currentThreadExecutor.getAndClearLastScheduledRunnable();
@@ -733,6 +1033,7 @@
         // Simulate the case where the response is under TTL.
         doReturn(TEST_ELAPSED_REALTIME + TEST_TTL - 1L).when(mockDecoderClock).elapsedRealtime();
         firstMdnsTask.run();
+        verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
 
         // Verify removed callback was not called.
         verifyServiceRemovedNoCallback(mockListenerOne);
@@ -740,31 +1041,24 @@
         // Simulate the case where the response is after TTL.
         doReturn(TEST_ELAPSED_REALTIME + TEST_TTL + 1L).when(mockDecoderClock).elapsedRealtime();
         firstMdnsTask.run();
+        verify(mockDeps, times(2)).sendMessage(any(), any(Message.class));
 
         // Verify removed callback was called.
-        verifyServiceRemovedCallback(mockListenerOne, serviceInstanceName, SERVICE_TYPE_LABELS,
-                INTERFACE_INDEX, mockNetwork);
+        verifyServiceRemovedCallback(
+                mockListenerOne, serviceInstanceName, SERVICE_TYPE_LABELS, socketKey);
     }
 
     @Test
     public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
             throws Exception {
         final String serviceInstanceName = "service-instance-1";
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, mockNetwork, mockSharedLog) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
-        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
         // Process the initial response.
-        client.processResponse(createResponse(
-                serviceInstanceName, "192.168.1.1", 5353, /* subtype= */ "ABCDE",
-                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+        processResponse(createResponse(
+                serviceInstanceName, "192.168.1.1", 5353, SUBTYPE,
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
         // Clear the scheduled runnable.
         currentThreadExecutor.getAndClearLastScheduledRunnable();
@@ -783,21 +1077,13 @@
             throws Exception {
         //MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
         final String serviceInstanceName = "service-instance-1";
-        client =
-                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                        mockDecoderClock, mockNetwork, mockSharedLog) {
-                    @Override
-                    MdnsPacketWriter createMdnsPacketWriter() {
-                        return mockPacketWriter;
-                    }
-                };
-        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
         // Process the initial response.
-        client.processResponse(createResponse(
-                serviceInstanceName, "192.168.1.1", 5353, /* subtype= */ "ABCDE",
-                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+        processResponse(createResponse(
+                serviceInstanceName, "192.168.1.1", 5353, SUBTYPE,
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
         // Clear the scheduled runnable.
         currentThreadExecutor.getAndClearLastScheduledRunnable();
@@ -807,8 +1093,8 @@
         firstMdnsTask.run();
 
         // Verify removed callback was called.
-        verifyServiceRemovedCallback(mockListenerOne, serviceInstanceName, SERVICE_TYPE_LABELS,
-                INTERFACE_INDEX, mockNetwork);
+        verifyServiceRemovedCallback(
+                mockListenerOne, serviceInstanceName, SERVICE_TYPE_LABELS, socketKey);
     }
 
     @Test
@@ -816,56 +1102,55 @@
         final String serviceName = "service-instance";
         final String ipV4Address = "192.0.2.0";
         final String ipV6Address = "2001:db8::";
-        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
         InOrder inOrder = inOrder(mockListenerOne);
 
         // Process the initial response which is incomplete.
-        final String subtype = "ABCDE";
-        client.processResponse(createResponse(
-                serviceName, null, 5353, subtype,
-                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+        processResponse(createResponse(
+                serviceName, null, 5353, SUBTYPE,
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
         // Process a second response which has ip address to make response become complete.
-        client.processResponse(createResponse(
-                serviceName, ipV4Address, 5353, subtype,
-                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+        processResponse(createResponse(
+                serviceName, ipV4Address, 5353, SUBTYPE,
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
         // Process a third response with a different ip address, port and updated text attributes.
-        client.processResponse(createResponse(
-                serviceName, ipV6Address, 5354, subtype,
-                Collections.singletonMap("key", "value"), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+        processResponse(createResponse(
+                serviceName, ipV6Address, 5354, SUBTYPE,
+                Collections.singletonMap("key", "value"), TEST_TTL), socketKey);
 
         // Process the last response which is goodbye message (with the main type, not subtype).
-        client.processResponse(createResponse(
-                serviceName, ipV6Address, 5354, SERVICE_TYPE_LABELS,
-                Collections.singletonMap("key", "value"), /* ptrTtlMillis= */ 0L),
-                INTERFACE_INDEX, mockNetwork);
+        processResponse(createResponse(
+                        serviceName, ipV6Address, 5354, SERVICE_TYPE_LABELS,
+                        Collections.singletonMap("key", "value"), /* ptrTtlMillis= */ 0L),
+                socketKey);
 
         // Verify onServiceNameDiscovered was first called for the initial response.
-        inOrder.verify(mockListenerOne).onServiceNameDiscovered(serviceInfoCaptor.capture());
+        inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+                serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
         verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
                 serviceName,
                 SERVICE_TYPE_LABELS,
                 List.of() /* ipv4Address */,
                 List.of() /* ipv6Address */,
                 5353 /* port */,
-                Collections.singletonList(subtype) /* subTypes */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
                 Collections.singletonMap("key", null) /* attributes */,
-                INTERFACE_INDEX,
-                mockNetwork);
+                socketKey);
 
         // Verify onServiceFound was second called for the second response.
-        inOrder.verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        inOrder.verify(mockListenerOne).onServiceFound(
+                serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
         verifyServiceInfo(serviceInfoCaptor.getAllValues().get(1),
                 serviceName,
                 SERVICE_TYPE_LABELS,
                 List.of(ipV4Address) /* ipv4Address */,
                 List.of() /* ipv6Address */,
                 5353 /* port */,
-                Collections.singletonList(subtype) /* subTypes */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
                 Collections.singletonMap("key", null) /* attributes */,
-                INTERFACE_INDEX,
-                mockNetwork);
+                socketKey);
 
         // Verify onServiceUpdated was third called for the third response.
         inOrder.verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
@@ -875,10 +1160,9 @@
                 List.of(ipV4Address) /* ipv4Address */,
                 List.of(ipV6Address) /* ipv6Address */,
                 5354 /* port */,
-                Collections.singletonList(subtype) /* subTypes */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
                 Collections.singletonMap("key", "value") /* attributes */,
-                INTERFACE_INDEX,
-                mockNetwork);
+                socketKey);
 
         // Verify onServiceRemoved was called for the last response.
         inOrder.verify(mockListenerOne).onServiceRemoved(serviceInfoCaptor.capture());
@@ -888,10 +1172,9 @@
                 List.of(ipV4Address) /* ipv4Address */,
                 List.of(ipV6Address) /* ipv6Address */,
                 5354 /* port */,
-                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
                 Collections.singletonMap("key", "value") /* attributes */,
-                INTERFACE_INDEX,
-                mockNetwork);
+                socketKey);
 
         // Verify onServiceNameRemoved was called for the last response.
         inOrder.verify(mockListenerOne).onServiceNameRemoved(serviceInfoCaptor.capture());
@@ -901,43 +1184,54 @@
                 List.of(ipV4Address) /* ipv4Address */,
                 List.of(ipV6Address) /* ipv6Address */,
                 5354 /* port */,
-                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
                 Collections.singletonMap("key", "value") /* attributes */,
-                INTERFACE_INDEX,
-                mockNetwork);
+                socketKey);
     }
 
     @Test
     public void testProcessResponse_Resolve() throws Exception {
-        client = new MdnsServiceTypeClient(
-                SERVICE_TYPE, mockSocketClient, currentThreadExecutor, mockNetwork, mockSharedLog);
-
         final String instanceName = "service-instance";
         final String[] hostname = new String[] { "testhost "};
         final String ipV4Address = "192.0.2.0";
         final String ipV6Address = "2001:db8::";
 
-        final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
+        final MdnsSearchOptions resolveOptions1 = MdnsSearchOptions.newBuilder()
+                .setResolveInstanceName(instanceName).build();
+        final MdnsSearchOptions resolveOptions2 = MdnsSearchOptions.newBuilder()
                 .setResolveInstanceName(instanceName).build();
 
-        client.startSendAndReceive(mockListenerOne, resolveOptions);
+        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)).sendUnicastPacket(srvTxtQueryCaptor.capture(),
-                eq(mockNetwork));
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+                srvTxtQueryCaptor.capture(),
+                eq(socketKey), eq(false));
+        verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
+        assertNotNull(delayMessage);
+        inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
+        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());
         assertFalse(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_PTR));
-        assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_SRV, serviceName));
-        assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_TXT, serviceName));
+        assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_ANY, serviceName));
+        assertEquals(0, srvTxtQueryPacket.answers.size());
+        assertEquals(0, srvTxtQueryPacket.authorityRecords.size());
+        assertEquals(0, srvTxtQueryPacket.additionalRecords.size());
 
         // Process a response with SRV+TXT
         final MdnsPacket srvTxtResponse = new MdnsPacket(
@@ -953,19 +1247,32 @@
                 Collections.emptyList() /* authorityRecords */,
                 Collections.emptyList() /* additionalRecords */);
 
-        client.processResponse(srvTxtResponse, INTERFACE_INDEX, mockNetwork);
+        processResponse(srvTxtResponse, socketKey);
+        inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+                matchServiceName(instanceName), eq(false) /* isServiceFromCache */);
+        verify(mockListenerTwo).onServiceNameDiscovered(
+                matchServiceName(instanceName), eq(false) /* isServiceFromCache */);
 
         // Expect a query for A/AAAA
-        final ArgumentCaptor<DatagramPacket> addressQueryCaptor =
-                ArgumentCaptor.forClass(DatagramPacket.class);
+        dispatchMessage();
+        final ArgumentCaptor<List<DatagramPacket>> addressQueryCaptor =
+                ArgumentCaptor.forClass(List.class);
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
-        inOrder.verify(mockSocketClient, times(2)).sendMulticastPacket(addressQueryCaptor.capture(),
-                eq(mockNetwork));
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+                addressQueryCaptor.capture(),
+                eq(socketKey), eq(false));
+        inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
+        // onDiscoveryQuerySent was called 2 times in total
+        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));
+        assertEquals(0, addressQueryPacket.answers.size());
+        assertEquals(0, addressQueryPacket.authorityRecords.size());
+        assertEquals(0, addressQueryPacket.additionalRecords.size());
 
         // Process a response with address records
         final MdnsPacket addressResponse = new MdnsPacket(
@@ -981,10 +1288,13 @@
                 Collections.emptyList() /* authorityRecords */,
                 Collections.emptyList() /* additionalRecords */);
 
-        inOrder.verify(mockListenerOne, never()).onServiceNameDiscovered(any());
-        client.processResponse(addressResponse, INTERFACE_INDEX, mockNetwork);
+        inOrder.verify(mockListenerOne, never()).onServiceNameDiscovered(any(), anyBoolean());
+        verifyNoMoreInteractions(mockListenerTwo);
+        processResponse(addressResponse, socketKey);
 
-        inOrder.verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        inOrder.verify(mockListenerOne).onServiceFound(
+                serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
+        verify(mockListenerTwo).onServiceFound(any(), anyBoolean());
         verifyServiceInfo(serviceInfoCaptor.getValue(),
                 instanceName,
                 SERVICE_TYPE_LABELS,
@@ -993,15 +1303,11 @@
                 1234 /* port */,
                 Collections.emptyList() /* subTypes */,
                 Collections.emptyMap() /* attributes */,
-                INTERFACE_INDEX,
-                mockNetwork);
+                socketKey);
     }
 
     @Test
     public void testRenewTxtSrvInResolve() throws Exception {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, mockNetwork, mockSharedLog);
-
         final String instanceName = "service-instance";
         final String[] hostname = new String[] { "testhost "};
         final String ipV4Address = "192.0.2.0";
@@ -1010,23 +1316,28 @@
         final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
                 .setResolveInstanceName(instanceName).build();
 
-        client.startSendAndReceive(mockListenerOne, resolveOptions);
+        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)).sendUnicastPacket(srvTxtQueryCaptor.capture(),
-                eq(mockNetwork));
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+                srvTxtQueryCaptor.capture(),
+                eq(socketKey), eq(false));
+        verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
+        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_SRV, serviceName));
-        assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_TXT, serviceName));
+        assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_ANY, serviceName));
 
         // Process a response with all records
         final MdnsPacket srvTxtResponse = new MdnsPacket(
@@ -1047,9 +1358,12 @@
                                 InetAddresses.parseNumericAddress(ipV6Address))),
                 Collections.emptyList() /* authorityRecords */,
                 Collections.emptyList() /* additionalRecords */);
-        client.processResponse(srvTxtResponse, INTERFACE_INDEX, mockNetwork);
-        inOrder.verify(mockListenerOne).onServiceNameDiscovered(any());
-        inOrder.verify(mockListenerOne).onServiceFound(any());
+        processResponse(srvTxtResponse, socketKey);
+        dispatchMessage();
+        inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+                any(), eq(false) /* isServiceFromCache */);
+        inOrder.verify(mockListenerOne).onServiceFound(
+                any(), eq(false) /* isServiceFromCache */);
 
         // Expect no query on the next run
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
@@ -1058,19 +1372,24 @@
         // Advance time so 75% of TTL passes and re-execute
         doReturn(TEST_ELAPSED_REALTIME + (long) (TEST_TTL * 0.75))
                 .when(mockDecoderClock).elapsedRealtime();
+        verify(mockDeps, times(2)).sendMessage(any(), any(Message.class));
+        assertNotNull(delayMessage);
+        dispatchMessage();
         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)).sendMulticastPacket(renewalQueryCaptor.capture(),
-                eq(mockNetwork));
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+                renewalQueryCaptor.capture(),
+                eq(socketKey), eq(false));
+        verify(mockDeps, times(3)).sendMessage(any(), any(Message.class));
+        assertNotNull(delayMessage);
         inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
         final MdnsPacket renewalPacket = MdnsPacket.parse(
-                new MdnsPacketReader(renewalQueryCaptor.getValue()));
-        assertTrue(hasQuestion(renewalPacket, MdnsRecord.TYPE_SRV, serviceName));
-        assertTrue(hasQuestion(renewalPacket, MdnsRecord.TYPE_TXT, serviceName));
+                new MdnsPacketReader(renewalQueryCaptor.getValue().get(0)));
+        assertTrue(hasQuestion(renewalPacket, MdnsRecord.TYPE_ANY, serviceName));
         inOrder.verifyNoMoreInteractions();
 
         long updatedReceiptTime =  TEST_ELAPSED_REALTIME + TEST_TTL;
@@ -1092,7 +1411,8 @@
                                 InetAddresses.parseNumericAddress(ipV6Address))),
                 Collections.emptyList() /* authorityRecords */,
                 Collections.emptyList() /* additionalRecords */);
-        client.processResponse(refreshedSrvTxtResponse, INTERFACE_INDEX, mockNetwork);
+        processResponse(refreshedSrvTxtResponse, socketKey);
+        dispatchMessage();
 
         // Advance time to updatedReceiptTime + 1, expected no refresh query because the cache
         // should contain the record that have update last receipt time.
@@ -1103,70 +1423,72 @@
 
     @Test
     public void testProcessResponse_ResolveExcludesOtherServices() {
-        client = new MdnsServiceTypeClient(
-                SERVICE_TYPE, mockSocketClient, currentThreadExecutor, mockNetwork, mockSharedLog);
-
         final String requestedInstance = "instance1";
         final String otherInstance = "instance2";
         final String ipV4Address = "192.0.2.0";
         final String ipV6Address = "2001:db8::";
+        final String capitalizedRequestInstance = "Instance1";
 
         final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
                 // Use different case in the options
-                .setResolveInstanceName("Instance1").build();
+                .setResolveInstanceName(capitalizedRequestInstance).build();
 
-        client.startSendAndReceive(mockListenerOne, resolveOptions);
-        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerOne, resolveOptions);
+        startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
 
         // Complete response from instanceName
-        client.processResponse(createResponse(
-                requestedInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+        processResponse(createResponse(
+                        requestedInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
                         Collections.emptyMap() /* textAttributes */, TEST_TTL),
-                INTERFACE_INDEX, mockNetwork);
+                socketKey);
 
         // Complete response from otherInstanceName
-        client.processResponse(createResponse(
-                otherInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+        processResponse(createResponse(
+                        otherInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
                         Collections.emptyMap() /* textAttributes */, TEST_TTL),
-                INTERFACE_INDEX, mockNetwork);
+                socketKey);
 
         // Address update from otherInstanceName
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                 otherInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
-                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
         // Goodbye from otherInstanceName
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                 otherInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
-                Collections.emptyMap(), 0L /* ttl */), INTERFACE_INDEX, mockNetwork);
+                Collections.emptyMap(), 0L /* ttl */), socketKey);
 
         // mockListenerOne gets notified for the requested instance
-        verify(mockListenerOne).onServiceNameDiscovered(matchServiceName(requestedInstance));
-        verify(mockListenerOne).onServiceFound(matchServiceName(requestedInstance));
+        verify(mockListenerOne).onServiceNameDiscovered(
+                matchServiceName(capitalizedRequestInstance), eq(false) /* isServiceFromCache */);
+        verify(mockListenerOne).onServiceFound(
+                matchServiceName(capitalizedRequestInstance), eq(false) /* isServiceFromCache */);
 
         // ...but does not get any callback for the other instance
-        verify(mockListenerOne, never()).onServiceFound(matchServiceName(otherInstance));
-        verify(mockListenerOne, never()).onServiceNameDiscovered(matchServiceName(otherInstance));
+        verify(mockListenerOne, never()).onServiceFound(
+                matchServiceName(otherInstance), anyBoolean());
+        verify(mockListenerOne, never()).onServiceNameDiscovered(
+                matchServiceName(otherInstance), anyBoolean());
         verify(mockListenerOne, never()).onServiceUpdated(matchServiceName(otherInstance));
         verify(mockListenerOne, never()).onServiceRemoved(matchServiceName(otherInstance));
 
         // mockListenerTwo gets notified for both though
         final InOrder inOrder = inOrder(mockListenerTwo);
         inOrder.verify(mockListenerTwo).onServiceNameDiscovered(
-                matchServiceName(requestedInstance));
-        inOrder.verify(mockListenerTwo).onServiceFound(matchServiceName(requestedInstance));
+                matchServiceName(capitalizedRequestInstance), eq(false) /* isServiceFromCache */);
+        inOrder.verify(mockListenerTwo).onServiceFound(
+                matchServiceName(capitalizedRequestInstance), eq(false) /* isServiceFromCache */);
 
-        inOrder.verify(mockListenerTwo).onServiceNameDiscovered(matchServiceName(otherInstance));
-        inOrder.verify(mockListenerTwo).onServiceFound(matchServiceName(otherInstance));
+        inOrder.verify(mockListenerTwo).onServiceNameDiscovered(
+                matchServiceName(otherInstance), eq(false) /* isServiceFromCache */);
+        inOrder.verify(mockListenerTwo).onServiceFound(
+                matchServiceName(otherInstance), eq(false) /* isServiceFromCache */);
         inOrder.verify(mockListenerTwo).onServiceUpdated(matchServiceName(otherInstance));
         inOrder.verify(mockListenerTwo).onServiceRemoved(matchServiceName(otherInstance));
     }
 
     @Test
     public void testProcessResponse_SubtypeDiscoveryLimitedToSubtype() {
-        client = new MdnsServiceTypeClient(
-                SERVICE_TYPE, mockSocketClient, currentThreadExecutor, mockNetwork, mockSharedLog);
-
         final String matchingInstance = "instance1";
         final String subtype = "_subtype";
         final String otherInstance = "instance2";
@@ -1177,8 +1499,8 @@
                 // Search with different case. Note MdnsSearchOptions subtype doesn't start with "_"
                 .addSubtype("Subtype").build();
 
-        client.startSendAndReceive(mockListenerOne, options);
-        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerOne, options);
+        startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
 
         // Complete response from instanceName
         final MdnsPacket packetWithoutSubtype = createResponse(
@@ -1201,113 +1523,543 @@
                 newAnswers,
                 packetWithoutSubtype.authorityRecords,
                 packetWithoutSubtype.additionalRecords);
-        client.processResponse(packetWithSubtype, INTERFACE_INDEX, mockNetwork);
+        processResponse(packetWithSubtype, socketKey);
 
         // Complete response from otherInstanceName, without subtype
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                         otherInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
                         Collections.emptyMap() /* textAttributes */, TEST_TTL),
-                INTERFACE_INDEX, mockNetwork);
+                socketKey);
 
         // Address update from otherInstanceName
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                 otherInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
-                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+                Collections.emptyMap(), TEST_TTL), socketKey);
 
         // Goodbye from otherInstanceName
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                 otherInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
-                Collections.emptyMap(), 0L /* ttl */), INTERFACE_INDEX, mockNetwork);
+                Collections.emptyMap(), 0L /* ttl */), socketKey);
 
         // mockListenerOne gets notified for the requested instance
         final ArgumentMatcher<MdnsServiceInfo> subtypeInstanceMatcher = info ->
                 info.getServiceInstanceName().equals(matchingInstance)
                         && info.getSubtypes().equals(Collections.singletonList(subtype));
-        verify(mockListenerOne).onServiceNameDiscovered(argThat(subtypeInstanceMatcher));
-        verify(mockListenerOne).onServiceFound(argThat(subtypeInstanceMatcher));
+        verify(mockListenerOne).onServiceNameDiscovered(
+                argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
+        verify(mockListenerOne).onServiceFound(
+                argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
 
         // ...but does not get any callback for the other instance
-        verify(mockListenerOne, never()).onServiceFound(matchServiceName(otherInstance));
-        verify(mockListenerOne, never()).onServiceNameDiscovered(matchServiceName(otherInstance));
+        verify(mockListenerOne, never()).onServiceFound(
+                matchServiceName(otherInstance), anyBoolean());
+        verify(mockListenerOne, never()).onServiceNameDiscovered(
+                matchServiceName(otherInstance), anyBoolean());
         verify(mockListenerOne, never()).onServiceUpdated(matchServiceName(otherInstance));
         verify(mockListenerOne, never()).onServiceRemoved(matchServiceName(otherInstance));
 
         // mockListenerTwo gets notified for both though
         final InOrder inOrder = inOrder(mockListenerTwo);
-        inOrder.verify(mockListenerTwo).onServiceNameDiscovered(argThat(subtypeInstanceMatcher));
-        inOrder.verify(mockListenerTwo).onServiceFound(argThat(subtypeInstanceMatcher));
+        inOrder.verify(mockListenerTwo).onServiceNameDiscovered(
+                argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
+        inOrder.verify(mockListenerTwo).onServiceFound(
+                argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
 
-        inOrder.verify(mockListenerTwo).onServiceNameDiscovered(matchServiceName(otherInstance));
-        inOrder.verify(mockListenerTwo).onServiceFound(matchServiceName(otherInstance));
+        inOrder.verify(mockListenerTwo).onServiceNameDiscovered(
+                matchServiceName(otherInstance), eq(false) /* isServiceFromCache */);
+        inOrder.verify(mockListenerTwo).onServiceFound(
+                matchServiceName(otherInstance), eq(false) /* isServiceFromCache */);
         inOrder.verify(mockListenerTwo).onServiceUpdated(matchServiceName(otherInstance));
         inOrder.verify(mockListenerTwo).onServiceRemoved(matchServiceName(otherInstance));
     }
 
     @Test
-    public void testNotifySocketDestroyed() throws Exception {
-        client = new MdnsServiceTypeClient(
-                SERVICE_TYPE, mockSocketClient, currentThreadExecutor, mockNetwork, mockSharedLog);
+    public void testProcessResponse_SubtypeChange() {
+        final String matchingInstance = "instance1";
+        final String subtype = "_subtype";
+        final String ipV4Address = "192.0.2.0";
+        final String ipV6Address = "2001:db8::";
 
+        final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
+                .addSubtype("othersub").build();
+
+        startSendAndReceive(mockListenerOne, options);
+
+        // Complete response from instanceName
+        final MdnsPacket packetWithoutSubtype = createResponse(
+                matchingInstance, 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(
+                // PTR should be _subtype._sub._type._tcp.local -> instance1._type._tcp.local
+                Stream.concat(Stream.of(subtype, "_sub"), Arrays.stream(SERVICE_TYPE_LABELS))
+                        .toArray(String[]::new),
+                originalPtr.getReceiptTime(), originalPtr.getCacheFlush(), originalPtr.getTtl(),
+                originalPtr.getPointer()));
+        processResponse(new MdnsPacket(
+                packetWithoutSubtype.flags,
+                packetWithoutSubtype.questions,
+                newAnswers,
+                packetWithoutSubtype.authorityRecords,
+                packetWithoutSubtype.additionalRecords), socketKey);
+
+        // The subtype does not match
+        final InOrder inOrder = inOrder(mockListenerOne);
+        inOrder.verify(mockListenerOne, never()).onServiceNameDiscovered(any(), anyBoolean());
+
+        // Add another matching subtype
+        newAnswers.add(new MdnsPointerRecord(
+                // PTR should be _subtype._sub._type._tcp.local -> instance1._type._tcp.local
+                Stream.concat(Stream.of("_othersub", "_sub"), Arrays.stream(SERVICE_TYPE_LABELS))
+                        .toArray(String[]::new),
+                originalPtr.getReceiptTime(), originalPtr.getCacheFlush(), originalPtr.getTtl(),
+                originalPtr.getPointer()));
+        processResponse(new MdnsPacket(
+                packetWithoutSubtype.flags,
+                packetWithoutSubtype.questions,
+                newAnswers,
+                packetWithoutSubtype.authorityRecords,
+                packetWithoutSubtype.additionalRecords), socketKey);
+
+        final ArgumentMatcher<MdnsServiceInfo> subtypeInstanceMatcher = info ->
+                info.getServiceInstanceName().equals(matchingInstance)
+                        && info.getSubtypes().equals(List.of("_subtype", "_othersub"));
+
+        // Service found callbacks are sent now
+        inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+                argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
+        inOrder.verify(mockListenerOne).onServiceFound(
+                argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
+
+        // Address update: update callbacks are sent
+        processResponse(createResponse(
+                matchingInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), socketKey);
+
+        inOrder.verify(mockListenerOne).onServiceUpdated(argThat(info ->
+                subtypeInstanceMatcher.matches(info)
+                        && info.getIpv4Addresses().equals(List.of(ipV4Address))
+                        && info.getIpv6Addresses().equals(List.of(ipV6Address))));
+
+        // Goodbye: service removed callbacks are sent
+        processResponse(createResponse(
+                matchingInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), 0L /* ttl */), socketKey);
+
+        inOrder.verify(mockListenerOne).onServiceRemoved(matchServiceName(matchingInstance));
+        inOrder.verify(mockListenerOne).onServiceNameRemoved(matchServiceName(matchingInstance));
+    }
+
+    @Test
+    public void testNotifySocketDestroyed() throws Exception {
         final String requestedInstance = "instance1";
         final String otherInstance = "instance2";
         final String ipV4Address = "192.0.2.0";
 
         final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
-                // Use different case in the options
-                .setResolveInstanceName("Instance1").build();
+                .setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
+                .setResolveInstanceName("instance1").build();
 
-        client.startSendAndReceive(mockListenerOne, resolveOptions);
+        startSendAndReceive(mockListenerOne, resolveOptions);
+        // Always try to remove the task.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
         // Ensure the first task is executed so it schedules a future task
         currentThreadExecutor.getAndClearSubmittedFuture().get(
                 TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+        startSendAndReceive(mockListenerTwo,
+                MdnsSearchOptions.newBuilder().setNumOfQueriesBeforeBackoff(
+                        Integer.MAX_VALUE).build());
 
         // Filing the second request cancels the first future
-        verify(expectedSendFutures[0]).cancel(true);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
 
         // Ensure it gets executed too
         currentThreadExecutor.getAndClearSubmittedFuture().get(
                 TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
 
         // Complete response from instanceName
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                         requestedInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
                         Collections.emptyMap() /* textAttributes */, TEST_TTL),
-                INTERFACE_INDEX, mockNetwork);
+                socketKey);
 
         // Complete response from otherInstanceName
-        client.processResponse(createResponse(
+        processResponse(createResponse(
                         otherInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
                         Collections.emptyMap() /* textAttributes */, TEST_TTL),
-                INTERFACE_INDEX, mockNetwork);
+                socketKey);
 
-        verify(expectedSendFutures[1], never()).cancel(true);
-        client.notifySocketDestroyed();
-        verify(expectedSendFutures[1]).cancel(true);
+        notifySocketDestroyed();
+        verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
 
         // mockListenerOne gets notified for the requested instance
         final InOrder inOrder1 = inOrder(mockListenerOne);
         inOrder1.verify(mockListenerOne).onServiceNameDiscovered(
-                matchServiceName(requestedInstance));
-        inOrder1.verify(mockListenerOne).onServiceFound(matchServiceName(requestedInstance));
+                matchServiceName(requestedInstance), eq(false) /* isServiceFromCache */);
+        inOrder1.verify(mockListenerOne).onServiceFound(
+                matchServiceName(requestedInstance), eq(false) /* isServiceFromCache */);
         inOrder1.verify(mockListenerOne).onServiceRemoved(matchServiceName(requestedInstance));
         inOrder1.verify(mockListenerOne).onServiceNameRemoved(matchServiceName(requestedInstance));
-        verify(mockListenerOne, never()).onServiceFound(matchServiceName(otherInstance));
-        verify(mockListenerOne, never()).onServiceNameDiscovered(matchServiceName(otherInstance));
+        verify(mockListenerOne, never()).onServiceFound(
+                matchServiceName(otherInstance), anyBoolean());
+        verify(mockListenerOne, never()).onServiceNameDiscovered(
+                matchServiceName(otherInstance), anyBoolean());
         verify(mockListenerOne, never()).onServiceRemoved(matchServiceName(otherInstance));
         verify(mockListenerOne, never()).onServiceNameRemoved(matchServiceName(otherInstance));
 
         // mockListenerTwo gets notified for both though
         final InOrder inOrder2 = inOrder(mockListenerTwo);
         inOrder2.verify(mockListenerTwo).onServiceNameDiscovered(
-                matchServiceName(requestedInstance));
-        inOrder2.verify(mockListenerTwo).onServiceFound(matchServiceName(requestedInstance));
-        inOrder2.verify(mockListenerTwo).onServiceNameDiscovered(matchServiceName(otherInstance));
-        inOrder2.verify(mockListenerTwo).onServiceFound(matchServiceName(otherInstance));
-        inOrder2.verify(mockListenerTwo).onServiceRemoved(matchServiceName(otherInstance));
-        inOrder2.verify(mockListenerTwo).onServiceNameRemoved(matchServiceName(otherInstance));
+                matchServiceName(requestedInstance), eq(false) /* isServiceFromCache */);
+        inOrder2.verify(mockListenerTwo).onServiceFound(
+                matchServiceName(requestedInstance), eq(false) /* isServiceFromCache */);
         inOrder2.verify(mockListenerTwo).onServiceRemoved(matchServiceName(requestedInstance));
         inOrder2.verify(mockListenerTwo).onServiceNameRemoved(matchServiceName(requestedInstance));
+        verify(mockListenerTwo).onServiceNameDiscovered(
+                matchServiceName(otherInstance), eq(false) /* isServiceFromCache */);
+        verify(mockListenerTwo).onServiceFound(
+                matchServiceName(otherInstance), eq(false) /* isServiceFromCache */);
+        verify(mockListenerTwo).onServiceRemoved(matchServiceName(otherInstance));
+        verify(mockListenerTwo).onServiceNameRemoved(matchServiceName(otherInstance));
+    }
+
+    @Test
+    public void testServicesAreCached() throws Exception {
+        final String serviceName = "service-instance";
+        final String ipV4Address = "192.0.2.0";
+        // Register a listener
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        InOrder inOrder = inOrder(mockListenerOne);
+
+        // Process a response which has ip address to make response become complete.
+
+        processResponse(createResponse(
+                        serviceName, ipV4Address, 5353, SUBTYPE,
+                        Collections.emptyMap(), TEST_TTL),
+                socketKey);
+
+        // Verify that onServiceNameDiscovered is called.
+        inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+                serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
+                serviceName,
+                SERVICE_TYPE_LABELS,
+                List.of(ipV4Address) /* ipv4Address */,
+                List.of() /* ipv6Address */,
+                5353 /* port */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
+                Collections.singletonMap("key", null) /* attributes */,
+                socketKey);
+
+        // Verify that onServiceFound is called.
+        inOrder.verify(mockListenerOne).onServiceFound(
+                serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(1),
+                serviceName,
+                SERVICE_TYPE_LABELS,
+                List.of(ipV4Address) /* ipv4Address */,
+                List.of() /* ipv6Address */,
+                5353 /* port */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
+                Collections.singletonMap("key", null) /* attributes */,
+                socketKey);
+
+        // Unregister the listener
+        stopSendAndReceive(mockListenerOne);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        // Register another listener.
+        startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+        verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        InOrder inOrder2 = inOrder(mockListenerTwo);
+
+        // The services are cached in MdnsServiceCache, verify that onServiceNameDiscovered is
+        // called immediately.
+        inOrder2.verify(mockListenerTwo).onServiceNameDiscovered(
+                serviceInfoCaptor.capture(), eq(true) /* isServiceFromCache */);
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(2),
+                serviceName,
+                SERVICE_TYPE_LABELS,
+                List.of(ipV4Address) /* ipv4Address */,
+                List.of() /* ipv6Address */,
+                5353 /* port */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
+                Collections.singletonMap("key", null) /* attributes */,
+                socketKey);
+
+        // The services are cached in MdnsServiceCache, verify that onServiceFound is
+        // called immediately.
+        inOrder2.verify(mockListenerTwo).onServiceFound(
+                serviceInfoCaptor.capture(), eq(true) /* isServiceFromCache */);
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(3),
+                serviceName,
+                SERVICE_TYPE_LABELS,
+                List.of(ipV4Address) /* ipv4Address */,
+                List.of() /* ipv6Address */,
+                5353 /* port */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
+                Collections.singletonMap("key", null) /* attributes */,
+                socketKey);
+
+        // Process a response with a different ip address, port and updated text attributes.
+        final String ipV6Address = "2001:db8::";
+        processResponse(createResponse(
+                serviceName, ipV6Address, 5354, SUBTYPE,
+                Collections.singletonMap("key", "value"), TEST_TTL), socketKey);
+
+        // Verify the onServiceUpdated is called.
+        inOrder2.verify(mockListenerTwo).onServiceUpdated(serviceInfoCaptor.capture());
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(4),
+                serviceName,
+                SERVICE_TYPE_LABELS,
+                List.of(ipV4Address) /* ipv4Address */,
+                List.of(ipV6Address) /* ipv6Address */,
+                5354 /* port */,
+                Collections.singletonList(SUBTYPE) /* subTypes */,
+                Collections.singletonMap("key", "value") /* attributes */,
+                socketKey);
+    }
+
+    @Test
+    public void sendQueries_aggressiveScanMode() {
+        final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(AGGRESSIVE_QUERY_MODE).build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        // Always try to remove the task.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        int burstCounter = 0;
+        int betweenBurstTime = 0;
+        for (int i = 0; i < expectedIPv4Packets.length; i += 3) {
+            verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true);
+            verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+            verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+                    /* expectsUnicastResponse= */ false);
+            betweenBurstTime = Math.min(
+                    INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS * (int) Math.pow(2, burstCounter),
+                    MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+            burstCounter++;
+        }
+        // Verify that Task is not removed before stopSendAndReceive was called.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        // Stop sending packets.
+        stopSendAndReceive(mockListenerOne);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+    }
+
+    @Test
+    public void sendQueries_reentry_aggressiveScanMode() {
+        final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE).setQueryMode(AGGRESSIVE_QUERY_MODE).build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        // Always try to remove the task.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        // First burst, first query is sent.
+        verifyAndSendQuery(0, /* timeInMs= */ 0, /* expectsUnicastResponse= */ true);
+
+        // After the first query is sent, change the subtypes, and restart.
+        final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE)
+                .addSubtype("_subtype2").setQueryMode(AGGRESSIVE_QUERY_MODE).build();
+        startSendAndReceive(mockListenerOne, searchOptions2);
+        // The previous scheduled task should be canceled.
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        // Queries should continue to be sent.
+        verifyAndSendQuery(1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ true);
+        verifyAndSendQuery(2, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(3, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+                /* expectsUnicastResponse= */ false);
+
+        // Stop sending packets.
+        stopSendAndReceive(mockListenerOne);
+        verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+    }
+
+    @Test
+    public void sendQueries_blendScanWithQueryBackoff() {
+        final int numOfQueriesBeforeBackoff = 11;
+        final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE)
+                .setQueryMode(AGGRESSIVE_QUERY_MODE)
+                .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+                .build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        // Always try to remove the task.
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        int burstCounter = 0;
+        int betweenBurstTime = 0;
+        for (int i = 0; i < numOfQueriesBeforeBackoff; i += 3) {
+            verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true);
+            verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+            verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+                    /* expectsUnicastResponse= */ false);
+            betweenBurstTime = Math.min(
+                    INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS * (int) Math.pow(2, burstCounter),
+                    MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+            burstCounter++;
+        }
+        // In backoff mode, the current scheduled task will be canceled and reschedule if the
+        // 0.8 * smallestRemainingTtl is larger than time to next run.
+        long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        doReturn(true).when(mockDeps).hasMessages(any(), eq(EVENT_START_QUERYTASK));
+        processResponse(createResponse(
+                "service-instance-1", "192.0.2.123", 5353,
+                SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), socketKey);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        assertNotNull(delayMessage);
+        verifyAndSendQuery(12 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */,
+                true /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                14 /* scheduledCount */);
+        currentTime += (long) (TEST_TTL / 2 * 0.8);
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        verifyAndSendQuery(13 /* index */, 0 /* timeInMs */,
+                false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                15 /* scheduledCount */);
+        verifyAndSendQuery(14 /* index */, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+                false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                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) {
@@ -1317,29 +2069,42 @@
     // verifies that the right query was enqueued with the right delay, and send query by executing
     // the runnable.
     private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse) {
-        verifyAndSendQuery(
-                index, timeInMs, expectsUnicastResponse, true /* multipleSocketDiscovery */);
+        verifyAndSendQuery(index, timeInMs, expectsUnicastResponse,
+                true /* multipleSocketDiscovery */, index + 1 /* scheduledCount */);
     }
 
     private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
-            boolean multipleSocketDiscovery) {
-        assertEquals(currentThreadExecutor.getAndClearLastScheduledDelayInMs(), timeInMs);
+            boolean multipleSocketDiscovery, int scheduledCount) {
+        // Dispatch the message
+        if (delayMessage != null && realHandler != null) {
+            dispatchMessage();
+        }
+        assertEquals(timeInMs, latestDelayMs);
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
         if (expectsUnicastResponse) {
-            verify(mockSocketClient).sendUnicastPacket(
-                    expectedIPv4Packets[index], mockNetwork);
+            verify(mockSocketClient).sendPacketRequestingUnicastResponse(
+                    argThat(pkts -> pkts.get(0).equals(expectedIPv4Packets[index])),
+                    eq(socketKey), eq(false));
             if (multipleSocketDiscovery) {
-                verify(mockSocketClient).sendUnicastPacket(
-                        expectedIPv6Packets[index], mockNetwork);
+                verify(mockSocketClient).sendPacketRequestingUnicastResponse(
+                        argThat(pkts -> pkts.get(0).equals(expectedIPv6Packets[index])),
+                        eq(socketKey), eq(false));
             }
         } else {
-            verify(mockSocketClient).sendMulticastPacket(
-                    expectedIPv4Packets[index], mockNetwork);
+            verify(mockSocketClient).sendPacketRequestingMulticastResponse(
+                    argThat(pkts -> pkts.get(0).equals(expectedIPv4Packets[index])),
+                    eq(socketKey), eq(false));
             if (multipleSocketDiscovery) {
-                verify(mockSocketClient).sendMulticastPacket(
-                        expectedIPv6Packets[index], mockNetwork);
+                verify(mockSocketClient).sendPacketRequestingMulticastResponse(
+                        argThat(pkts -> pkts.get(0).equals(expectedIPv6Packets[index])),
+                        eq(socketKey), eq(false));
             }
         }
+        verify(mockDeps, times(index + 1))
+                .sendMessage(any(Handler.class), any(Message.class));
+        // Verify the task has been scheduled.
+        verify(mockDeps, times(scheduledCount))
+                .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
     }
 
     private static String[] getTestServiceName(String instanceName) {
@@ -1347,6 +2112,11 @@
                 Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
     }
 
+    private static String[] getServiceTypeWithSubtype(String subtype) {
+        return Stream.concat(Stream.of(subtype, "_sub"),
+                Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+    }
+
     private static boolean hasQuestion(MdnsPacket packet, int type) {
         return hasQuestion(packet, type, null);
     }
@@ -1356,6 +2126,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 {
@@ -1384,7 +2160,7 @@
         public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
             lastScheduledDelayInMs = delay;
             lastScheduledRunnable = command;
-            return expectedSendFutures[futureIndex++];
+            return Mockito.mock(ScheduledFuture.class);
         }
 
         // Returns the delay of the last scheduled task, and clear it.
@@ -1430,7 +2206,7 @@
                 textAttributes, ptrTtlMillis);
     }
 
-    // Creates a mDNS response.
+
     private MdnsPacket createResponse(
             @NonNull String serviceInstanceName,
             @Nullable String host,
@@ -1438,6 +2214,19 @@
             @NonNull String[] type,
             @NonNull Map<String, String> textAttributes,
             long ptrTtlMillis) {
+        return createResponse(serviceInstanceName, host, port, type, textAttributes, ptrTtlMillis,
+                TEST_ELAPSED_REALTIME);
+    }
+
+    // Creates a mDNS response.
+    private MdnsPacket createResponse(
+            @NonNull String serviceInstanceName,
+            @Nullable String host,
+            int port,
+            @NonNull String[] type,
+            @NonNull Map<String, String> textAttributes,
+            long ptrTtlMillis,
+            long receiptTimeMillis) {
 
         final ArrayList<MdnsRecord> answerRecords = new ArrayList<>();
 
@@ -1448,7 +2237,7 @@
         final String[] serviceName = serviceNameList.toArray(new String[0]);
         final MdnsPointerRecord pointerRecord = new MdnsPointerRecord(
                 type,
-                TEST_ELAPSED_REALTIME /* receiptTimeMillis */,
+                receiptTimeMillis,
                 false /* cacheFlush */,
                 ptrTtlMillis,
                 serviceName);
@@ -1457,7 +2246,7 @@
         // Set SRV record.
         final MdnsServiceRecord serviceRecord = new MdnsServiceRecord(
                 serviceName,
-                TEST_ELAPSED_REALTIME /* receiptTimeMillis */,
+                receiptTimeMillis,
                 false /* cacheFlush */,
                 TEST_TTL,
                 0 /* servicePriority */,
@@ -1471,7 +2260,7 @@
             final InetAddress addr = InetAddresses.parseNumericAddress(host);
             final MdnsInetAddressRecord inetAddressRecord = new MdnsInetAddressRecord(
                     new String[] {"hostname"} /* name */,
-                    TEST_ELAPSED_REALTIME /* receiptTimeMillis */,
+                    receiptTimeMillis,
                     false /* cacheFlush */,
                     TEST_TTL,
                     addr);
@@ -1485,7 +2274,7 @@
         }
         final MdnsTextRecord textRecord = new MdnsTextRecord(
                 serviceName,
-                TEST_ELAPSED_REALTIME /* receiptTimeMillis */,
+                receiptTimeMillis,
                 false /* cacheFlush */,
                 TEST_TTL,
                 textEntries);
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 abb1747..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;
@@ -23,21 +24,29 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+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;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -46,13 +55,18 @@
 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;
 
 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;
@@ -68,23 +82,29 @@
 
     @Mock private Context mContext;
     @Mock private WifiManager mockWifiManager;
+    @Mock private ConnectivityManager mockConnectivityManager;
     @Mock private MdnsSocket mockMulticastSocket;
     @Mock private MdnsSocket mockUnicastSocket;
     @Mock private MulticastLock mockMulticastLock;
     @Mock private MdnsSocketClient.Callback mockCallback;
+    @Mock private SharedLog sharedLog;
 
     private MdnsSocketClient mdnsClient;
+    private MdnsFeatureFlags flags = MdnsFeatureFlags.newBuilder().build();
 
     @Before
     public void setup() throws RuntimeException, IOException {
         MockitoAnnotations.initMocks(this);
 
+        doReturn(mockConnectivityManager).when(mContext).getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+
         when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
                 .thenReturn(mockMulticastLock);
 
-        mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock) {
+        mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog, flags) {
                     @Override
-                    MdnsSocket createMdnsSocket(int port) throws IOException {
+                    MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) throws IOException {
                         if (port == MdnsConstants.MDNS_PORT) {
                             return mockMulticastSocket;
                         }
@@ -220,15 +240,17 @@
         assertTrue(unicastReceiverThread.isAlive());
 
         // Sends a packet.
-        DatagramPacket packet = new DatagramPacket(buf, 0, 5);
-        mdnsClient.sendMulticastPacket(packet);
+        DatagramPacket packet = getTestDatagramPacket();
+        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.
         verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
         verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
 
         // Verify the packet is sent by the unicast socket.
-        mdnsClient.sendUnicastPacket(packet);
+        mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
         verify(mockUnicastSocket, timeout(TIMEOUT).times(1)).send(packet);
 
@@ -271,15 +293,17 @@
         assertNull(unicastReceiverThread);
 
         // Sends a packet.
-        DatagramPacket packet = new DatagramPacket(buf, 0, 5);
-        mdnsClient.sendMulticastPacket(packet);
+        DatagramPacket packet = getTestDatagramPacket();
+        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.
         verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
         verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
 
         // Verify the packet is sent by the multicast socket as well.
-        mdnsClient.sendUnicastPacket(packet);
+        mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         verify(mockMulticastSocket, timeout(TIMEOUT).times(2)).send(packet);
         verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
 
@@ -311,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());
         }
     }
 
@@ -331,7 +361,8 @@
     public void testStopDiscovery_queueIsCleared() throws IOException {
         mdnsClient.startDiscovery();
         mdnsClient.stopDiscovery();
-        mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+        mdnsClient.sendPacketRequestingMulticastResponse(List.of(getTestDatagramPacket()),
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
 
         synchronized (mdnsClient.multicastPacketQueue) {
             assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
@@ -342,7 +373,8 @@
     public void testSendPacket_afterDiscoveryStops() throws IOException {
         mdnsClient.startDiscovery();
         mdnsClient.stopDiscovery();
-        mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+        mdnsClient.sendPacketRequestingMulticastResponse(List.of(getTestDatagramPacket()),
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
 
         synchronized (mdnsClient.multicastPacketQueue) {
             assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
@@ -355,7 +387,8 @@
         //MdnsConfigsFlagsImpl.mdnsPacketQueueMaxSize.override(2L);
         mdnsClient.startDiscovery();
         for (int i = 0; i < 100; i++) {
-            mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+            mdnsClient.sendPacketRequestingMulticastResponse(List.of(getTestDatagramPacket()),
+                    false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         }
 
         synchronized (mdnsClient.multicastPacketQueue) {
@@ -370,7 +403,7 @@
         mdnsClient.startDiscovery();
 
         verify(mockCallback, timeout(TIMEOUT).atLeast(1))
-                .onResponseReceived(any(MdnsPacket.class), anyInt(), any());
+                .onResponseReceived(any(MdnsPacket.class), any(SocketKey.class));
     }
 
     @Test
@@ -379,7 +412,7 @@
         mdnsClient.startDiscovery();
 
         verify(mockCallback, timeout(TIMEOUT).atLeastOnce())
-                .onResponseReceived(any(MdnsPacket.class), anyInt(), any());
+                .onResponseReceived(any(MdnsPacket.class), any(SocketKey.class));
 
         mdnsClient.stopDiscovery();
     }
@@ -451,9 +484,11 @@
         enableUnicastResponse.set(true);
 
         mdnsClient.startDiscovery();
-        DatagramPacket packet = new DatagramPacket(buf, 0, 5);
-        mdnsClient.sendUnicastPacket(packet);
-        mdnsClient.sendMulticastPacket(packet);
+        DatagramPacket packet = getTestDatagramPacket();
+        mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+        mdnsClient.sendPacketRequestingMulticastResponse(List.of(packet),
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
 
         // Wait for the timer to be triggered.
         Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
@@ -483,8 +518,10 @@
         assertFalse(mdnsClient.receivedUnicastResponse);
         assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
 
-        mdnsClient.sendUnicastPacket(packet);
-        mdnsClient.sendMulticastPacket(packet);
+        mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+        mdnsClient.sendPacketRequestingMulticastResponse(List.of(packet),
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
 
         // Verify cannotReceiveMulticastResponse is not set the true because we didn't receive the
@@ -500,9 +537,9 @@
         //MdnsConfigsFlagsImpl.allowNetworkInterfaceIndexPropagation.override(true);
 
         when(mockMulticastSocket.getInterfaceIndex()).thenReturn(21);
-        mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock) {
+        mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog, flags) {
                     @Override
-                    MdnsSocket createMdnsSocket(int port) {
+                    MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) {
                         if (port == MdnsConstants.MDNS_PORT) {
                             return mockMulticastSocket;
                         }
@@ -513,7 +550,7 @@
         mdnsClient.startDiscovery();
 
         verify(mockCallback, timeout(TIMEOUT).atLeastOnce())
-                .onResponseReceived(any(), eq(21), any());
+                .onResponseReceived(any(), argThat(key -> key.getInterfaceIndex() == 21));
     }
 
     @Test
@@ -523,9 +560,9 @@
         //MdnsConfigsFlagsImpl.allowNetworkInterfaceIndexPropagation.override(false);
 
         when(mockMulticastSocket.getInterfaceIndex()).thenReturn(21);
-        mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock) {
+        mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog, flags) {
                     @Override
-                    MdnsSocket createMdnsSocket(int port) {
+                    MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) {
                         if (port == MdnsConstants.MDNS_PORT) {
                             return mockMulticastSocket;
                         }
@@ -536,6 +573,55 @@
         mdnsClient.startDiscovery();
 
         verify(mockMulticastSocket, never()).getInterfaceIndex();
-        verify(mockCallback, timeout(TIMEOUT).atLeast(1)).onResponseReceived(any(), eq(-1), any());
+        verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+                .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 */));
     }
 }
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
index 4f56857..1cc9985 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
@@ -32,10 +32,12 @@
 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.doAnswer;
 import static org.mockito.Mockito.doCallRealMethod;
 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.times;
@@ -70,15 +72,18 @@
 import com.android.net.module.util.netlink.StructIfaddrMsg;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 import com.android.server.connectivity.mdns.MdnsSocketProvider.Dependencies;
+import com.android.server.connectivity.mdns.MdnsSocketProvider.SocketRequestMonitor;
 import com.android.server.connectivity.mdns.internal.SocketNetlinkMonitor;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
+import org.junit.After;
 import org.junit.Before;
 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;
 
@@ -114,6 +119,8 @@
     @Mock private NetworkInterfaceWrapper mTestNetworkIfaceWrapper;
     @Mock private NetworkInterfaceWrapper mLocalOnlyIfaceWrapper;
     @Mock private NetworkInterfaceWrapper mTetheredIfaceWrapper;
+    @Mock private SocketRequestMonitor mSocketRequestMonitor;
+    private HandlerThread mHandlerThread;
     private Handler mHandler;
     private MdnsSocketProvider mSocketProvider;
     private NetworkCallback mNetworkCallback;
@@ -147,14 +154,14 @@
                 .getNetworkInterfaceByName(WIFI_P2P_IFACE_NAME);
         doReturn(mTetheredIfaceWrapper).when(mDeps).getNetworkInterfaceByName(TETHERED_IFACE_NAME);
         doReturn(mock(MdnsInterfaceSocket.class))
-                .when(mDeps).createMdnsInterfaceSocket(any(), anyInt(), any(), any());
+                .when(mDeps).createMdnsInterfaceSocket(any(), anyInt(), any(), any(), any());
         doReturn(TETHERED_IFACE_IDX).when(mDeps).getNetworkInterfaceIndexByName(
-                TETHERED_IFACE_NAME);
+                eq(TETHERED_IFACE_NAME), any());
         doReturn(789).when(mDeps).getNetworkInterfaceIndexByName(
-                WIFI_P2P_IFACE_NAME);
-        final HandlerThread thread = new HandlerThread("MdnsSocketProviderTest");
-        thread.start();
-        mHandler = new Handler(thread.getLooper());
+                eq(WIFI_P2P_IFACE_NAME), any());
+        mHandlerThread = new HandlerThread("MdnsSocketProviderTest");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
 
         doReturn(mTestSocketNetLinkMonitor).when(mDeps).createSocketNetlinkMonitor(any(), any(),
                 any());
@@ -165,7 +172,16 @@
             return mTestSocketNetLinkMonitor;
         }).when(mDeps).createSocketNetlinkMonitor(any(), any(),
                 any());
-        mSocketProvider = new MdnsSocketProvider(mContext, thread.getLooper(), mDeps, mLog);
+        mSocketProvider = new MdnsSocketProvider(mContext, mHandlerThread.getLooper(), mDeps, mLog,
+                mSocketRequestMonitor);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
     }
 
     private void runOnHandler(Runnable r) {
@@ -221,30 +237,30 @@
 
     private class TestSocketCallback implements MdnsSocketProvider.SocketCallback {
         private class SocketEvent {
-            public final Network mNetwork;
+            public final SocketKey mSocketKey;
             public final List<LinkAddress> mAddresses;
 
-            SocketEvent(Network network, List<LinkAddress> addresses) {
-                mNetwork = network;
+            SocketEvent(SocketKey socketKey, List<LinkAddress> addresses) {
+                mSocketKey = socketKey;
                 mAddresses = Collections.unmodifiableList(addresses);
             }
         }
 
         private class SocketCreatedEvent extends SocketEvent {
-            SocketCreatedEvent(Network nw, List<LinkAddress> addresses) {
-                super(nw, addresses);
+            SocketCreatedEvent(SocketKey socketKey, List<LinkAddress> addresses) {
+                super(socketKey, addresses);
             }
         }
 
         private class InterfaceDestroyedEvent extends SocketEvent {
-            InterfaceDestroyedEvent(Network nw, List<LinkAddress> addresses) {
-                super(nw, addresses);
+            InterfaceDestroyedEvent(SocketKey socketKey, List<LinkAddress> addresses) {
+                super(socketKey, addresses);
             }
         }
 
         private class AddressesChangedEvent extends SocketEvent {
-            AddressesChangedEvent(Network nw, List<LinkAddress> addresses) {
-                super(nw, addresses);
+            AddressesChangedEvent(SocketKey socketKey, List<LinkAddress> addresses) {
+                super(socketKey, addresses);
             }
         }
 
@@ -252,27 +268,27 @@
                 new ArrayTrackRecord<SocketEvent>().newReadHead();
 
         @Override
-        public void onSocketCreated(Network network, MdnsInterfaceSocket socket,
+        public void onSocketCreated(SocketKey socketKey, MdnsInterfaceSocket socket,
                 List<LinkAddress> addresses) {
-            mHistory.add(new SocketCreatedEvent(network, addresses));
+            mHistory.add(new SocketCreatedEvent(socketKey, addresses));
         }
 
         @Override
-        public void onInterfaceDestroyed(Network network, MdnsInterfaceSocket socket) {
-            mHistory.add(new InterfaceDestroyedEvent(network, List.of()));
+        public void onInterfaceDestroyed(SocketKey socketKey, MdnsInterfaceSocket socket) {
+            mHistory.add(new InterfaceDestroyedEvent(socketKey, List.of()));
         }
 
         @Override
-        public void onAddressesChanged(Network network, MdnsInterfaceSocket socket,
+        public void onAddressesChanged(SocketKey socketKey, MdnsInterfaceSocket socket,
                 List<LinkAddress> addresses) {
-            mHistory.add(new AddressesChangedEvent(network, addresses));
+            mHistory.add(new AddressesChangedEvent(socketKey, addresses));
         }
 
         public void expectedSocketCreatedForNetwork(Network network, List<LinkAddress> addresses) {
             final SocketEvent event = mHistory.poll(0L /* timeoutMs */, c -> true);
             assertNotNull(event);
             assertTrue(event instanceof SocketCreatedEvent);
-            assertEquals(network, event.mNetwork);
+            assertEquals(network, event.mSocketKey.getNetwork());
             assertEquals(addresses, event.mAddresses);
         }
 
@@ -280,7 +296,7 @@
             final SocketEvent event = mHistory.poll(0L /* timeoutMs */, c -> true);
             assertNotNull(event);
             assertTrue(event instanceof InterfaceDestroyedEvent);
-            assertEquals(network, event.mNetwork);
+            assertEquals(network, event.mSocketKey.getNetwork());
         }
 
         public void expectedAddressesChangedForNetwork(Network network,
@@ -288,7 +304,7 @@
             final SocketEvent event = mHistory.poll(0L /* timeoutMs */, c -> true);
             assertNotNull(event);
             assertTrue(event instanceof AddressesChangedEvent);
-            assertEquals(network, event.mNetwork);
+            assertEquals(network, event.mSocketKey.getNetwork());
             assertEquals(event.mAddresses, addresses);
         }
 
@@ -319,23 +335,30 @@
     public void testSocketRequestAndUnrequestSocket() {
         startMonitoringSockets();
 
+        final InOrder cbMonitorOrder = inOrder(mSocketRequestMonitor);
         final TestSocketCallback testCallback1 = new TestSocketCallback();
         runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback1));
         testCallback1.expectedNoCallback();
 
         postNetworkAvailable(TRANSPORT_WIFI);
         testCallback1.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4));
+        cbMonitorOrder.verify(mSocketRequestMonitor).onSocketRequestFulfilled(eq(TEST_NETWORK),
+                any(), eq(new int[] { TRANSPORT_WIFI }));
 
         final TestSocketCallback testCallback2 = new TestSocketCallback();
         runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback2));
         testCallback1.expectedNoCallback();
         testCallback2.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4));
+        cbMonitorOrder.verify(mSocketRequestMonitor).onSocketRequestFulfilled(eq(TEST_NETWORK),
+                any(), eq(new int[] { TRANSPORT_WIFI }));
 
         final TestSocketCallback testCallback3 = new TestSocketCallback();
         runOnHandler(() -> mSocketProvider.requestSocket(null /* network */, testCallback3));
         testCallback1.expectedNoCallback();
         testCallback2.expectedNoCallback();
         testCallback3.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4));
+        cbMonitorOrder.verify(mSocketRequestMonitor).onSocketRequestFulfilled(eq(TEST_NETWORK),
+                any(), eq(new int[] { TRANSPORT_WIFI }));
 
         runOnHandler(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(
                 List.of(LOCAL_ONLY_IFACE_NAME)));
@@ -343,6 +366,8 @@
         testCallback1.expectedNoCallback();
         testCallback2.expectedNoCallback();
         testCallback3.expectedSocketCreatedForNetwork(null /* network */, List.of());
+        cbMonitorOrder.verify(mSocketRequestMonitor).onSocketRequestFulfilled(eq(null),
+                any(), eq(new int[0]));
 
         runOnHandler(() -> mTetheringEventCallback.onTetheredInterfacesChanged(
                 List.of(TETHERED_IFACE_NAME)));
@@ -350,6 +375,8 @@
         testCallback1.expectedNoCallback();
         testCallback2.expectedNoCallback();
         testCallback3.expectedSocketCreatedForNetwork(null /* network */, List.of());
+        cbMonitorOrder.verify(mSocketRequestMonitor).onSocketRequestFulfilled(eq(null),
+                any(), eq(new int[0]));
 
         runOnHandler(() -> mSocketProvider.unrequestSocket(testCallback1));
         testCallback1.expectedNoCallback();
@@ -360,17 +387,22 @@
         testCallback1.expectedNoCallback();
         testCallback2.expectedInterfaceDestroyedForNetwork(TEST_NETWORK);
         testCallback3.expectedInterfaceDestroyedForNetwork(TEST_NETWORK);
+        cbMonitorOrder.verify(mSocketRequestMonitor).onSocketDestroyed(eq(TEST_NETWORK), any());
 
         runOnHandler(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(List.of()));
         testCallback1.expectedNoCallback();
         testCallback2.expectedNoCallback();
         testCallback3.expectedInterfaceDestroyedForNetwork(null /* network */);
+        cbMonitorOrder.verify(mSocketRequestMonitor).onSocketDestroyed(eq(null), any());
 
         runOnHandler(() -> mSocketProvider.unrequestSocket(testCallback3));
         testCallback1.expectedNoCallback();
         testCallback2.expectedNoCallback();
         // There was still a tethered interface, but no callback should be sent once unregistered
         testCallback3.expectedNoCallback();
+
+        // However the socket is getting destroyed, so the callback monitor is notified
+        cbMonitorOrder.verify(mSocketRequestMonitor).onSocketDestroyed(eq(null), any());
     }
 
     private RtNetlinkAddressMessage createNetworkAddressUpdateNetLink(
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
index 73dbd38..5809684 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
@@ -21,6 +21,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.android.net.module.util.SharedLog;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -50,6 +51,7 @@
     @Mock private NetworkInterfaceWrapper mockNetworkInterfaceWrapper;
     @Mock private MulticastSocket mockMulticastSocket;
     @Mock private MulticastNetworkInterfaceProvider mockMulticastNetworkInterfaceProvider;
+    @Mock private SharedLog sharedLog;
     private SocketAddress socketIPv4Address;
     private SocketAddress socketIPv6Address;
 
@@ -75,7 +77,8 @@
 
     @Test
     public void mdnsSocket_basicFunctionality() throws IOException {
-        mdnsSocket = new MdnsSocket(mockMulticastNetworkInterfaceProvider, mockMulticastSocket);
+        mdnsSocket = new MdnsSocket(mockMulticastNetworkInterfaceProvider, mockMulticastSocket,
+                sharedLog);
         mdnsSocket.send(datagramPacket);
         verify(mockMulticastSocket).setNetworkInterface(networkInterface);
         verify(mockMulticastSocket).send(datagramPacket);
@@ -101,7 +104,8 @@
         when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
                 .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
 
-        mdnsSocket = new MdnsSocket(mockMulticastNetworkInterfaceProvider, mockMulticastSocket);
+        mdnsSocket = new MdnsSocket(mockMulticastNetworkInterfaceProvider, mockMulticastSocket,
+                sharedLog);
 
         when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
                 Collections.singletonList(mockNetworkInterfaceWrapper)))
@@ -125,7 +129,8 @@
         when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
                 .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
 
-        mdnsSocket = new MdnsSocket(mockMulticastNetworkInterfaceProvider, mockMulticastSocket);
+        mdnsSocket = new MdnsSocket(mockMulticastNetworkInterfaceProvider, mockMulticastSocket,
+                sharedLog);
 
         when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
                 Collections.singletonList(mockNetworkInterfaceWrapper)))
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
index 2268dfe..af233c9 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
@@ -30,6 +30,7 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.net.module.util.SharedLog;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -65,6 +66,8 @@
     @Mock private NetworkInterfaceWrapper multicastInterfaceOne;
     @Mock private NetworkInterfaceWrapper multicastInterfaceTwo;
 
+    @Mock private SharedLog sharedLog;
+
     private final List<NetworkInterfaceWrapper> networkInterfaces = new ArrayList<>();
     private MulticastNetworkInterfaceProvider provider;
     private Context context;
@@ -156,7 +159,7 @@
                 false /* isIpv6 */);
 
         provider =
-                new MulticastNetworkInterfaceProvider(context) {
+                new MulticastNetworkInterfaceProvider(context, sharedLog) {
                     @Override
                     List<NetworkInterfaceWrapper> getNetworkInterfaces() {
                         return networkInterfaces;
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt
new file mode 100644
index 0000000..3e1dab8
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt
@@ -0,0 +1,92 @@
+package com.android.server.connectivity.mdns.internal
+
+import android.net.LinkAddress
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.system.OsConstants
+import com.android.net.module.util.SharedLog
+import com.android.net.module.util.netlink.NetlinkConstants
+import com.android.net.module.util.netlink.RtNetlinkAddressMessage
+import com.android.net.module.util.netlink.StructIfaddrMsg
+import com.android.net.module.util.netlink.StructNlMsgHdr
+import com.android.server.connectivity.mdns.MdnsAdvertiserTest
+import com.android.server.connectivity.mdns.MdnsSocketProvider
+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.ArgumentMatchers.eq
+import org.mockito.Mockito
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+private val LINKADDRV4 = LinkAddress("192.0.2.0/24")
+private val IFACE_IDX = 32
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+internal class SocketNetlinkMonitorTest {
+    private val thread = HandlerThread(MdnsAdvertiserTest::class.simpleName)
+    private val sharedlog = Mockito.mock(SharedLog::class.java)
+    private val netlinkMonitorCallBack =
+            Mockito.mock(MdnsSocketProvider.NetLinkMonitorCallBack::class.java)
+
+    @Before
+    fun setUp() {
+        thread.start()
+    }
+
+    @After
+    fun tearDown() {
+        thread.quitSafely()
+        thread.join()
+    }
+
+    @Test
+    fun testHandleDeprecatedNetlinkMessage() {
+        val socketNetlinkMonitor = SocketNetlinkMonitor(Handler(thread.looper), sharedlog,
+                netlinkMonitorCallBack)
+        val nlmsghdr = StructNlMsgHdr().apply {
+            nlmsg_type = NetlinkConstants.RTM_NEWADDR
+            nlmsg_flags = (StructNlMsgHdr.NLM_F_REQUEST.toInt()
+                    or StructNlMsgHdr.NLM_F_ACK.toInt()).toShort()
+            nlmsg_seq = 1
+        }
+        val structIfaddrMsg = StructIfaddrMsg(OsConstants.AF_INET.toShort(),
+                LINKADDRV4.prefixLength.toShort(),
+                LINKADDRV4.flags.toShort(),
+                LINKADDRV4.scope.toShort(), IFACE_IDX)
+        // If the LinkAddress is not preferred, RTM_NEWADDR will not trigger
+        // addOrUpdateInterfaceAddress() callback.
+        val deprecatedAddNetLinkMessage = RtNetlinkAddressMessage(nlmsghdr, structIfaddrMsg,
+            LINKADDRV4.address, null /* structIfacacheInfo */,
+            LINKADDRV4.flags or OsConstants.IFA_F_DEPRECATED)
+        socketNetlinkMonitor.processNetlinkMessage(deprecatedAddNetLinkMessage, 0L /* whenMs */)
+        verify(netlinkMonitorCallBack, never()).addOrUpdateInterfaceAddress(eq(IFACE_IDX),
+                 argThat { it.address == LINKADDRV4.address })
+
+        // If the LinkAddress is preferred, RTM_NEWADDR will trigger addOrUpdateInterfaceAddress()
+        // callback.
+        val preferredAddNetLinkMessage = RtNetlinkAddressMessage(nlmsghdr, structIfaddrMsg,
+            LINKADDRV4.address, null /* structIfacacheInfo */,
+            LINKADDRV4.flags or OsConstants.IFA_F_OPTIMISTIC)
+        socketNetlinkMonitor.processNetlinkMessage(preferredAddNetLinkMessage, 0L /* whenMs */)
+        verify(netlinkMonitorCallBack).addOrUpdateInterfaceAddress(eq(IFACE_IDX),
+            argThat { it.address == LINKADDRV4.address })
+
+        // Even if the LinkAddress is not preferred, RTM_DELADDR will trigger
+        // deleteInterfaceAddress() callback.
+        nlmsghdr.nlmsg_type = NetlinkConstants.RTM_DELADDR
+        val deprecatedDelNetLinkMessage = RtNetlinkAddressMessage(nlmsghdr, structIfaddrMsg,
+            LINKADDRV4.address, null /* structIfacacheInfo */,
+            LINKADDRV4.flags or OsConstants.IFA_F_DEPRECATED)
+        socketNetlinkMonitor.processNetlinkMessage(deprecatedDelNetLinkMessage, 0L /* whenMs */)
+        verify(netlinkMonitorCallBack).deleteInterfaceAddress(eq(IFACE_IDX),
+                argThat { it.address == LINKADDRV4.address })
+    }
+}
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 61c3a70..cf88d05 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,12 +16,27 @@
 
 package com.android.server.connectivity.mdns.util
 
+import android.net.InetAddresses
 import android.os.Build
+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.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.util.MdnsUtils.truncateServiceName
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import java.net.DatagramPacket
+import kotlin.test.assertContentEquals
+import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -40,13 +55,27 @@
         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<-- "))
+        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" +
+        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<"))
+                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
@@ -58,13 +87,17 @@
         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<-- "))
+        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" +
+        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<"))
+                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+        ))
     }
 
     @Test
@@ -72,4 +105,122 @@
         assertEquals(truncateServiceName("测试abcde", 7), "测试a")
         assertEquals(truncateServiceName("测试abcde", 100), "测试abcde")
     }
+
+    @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")))
+    }
+
+    @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")
+        ))
+    }
+
+    @Test
+    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/CSBasicMethodsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
new file mode 100644
index 0000000..a5d5297
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("DEPRECATION") // This file tests a bunch of deprecated methods : don't warn about it
+
+package com.android.server
+
+import android.net.ConnectivityManager
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSBasicMethodsTest : CSTest() {
+    @Test
+    fun testNetworkTypes() {
+        // Ensure that mocks for the networkAttributes config variable work as expected. If they
+        // don't, then tests that depend on CONNECTIVITY_ACTION broadcasts for these network types
+        // will fail. Failing here is much easier to debug.
+        assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_WIFI))
+        assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE))
+        assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE_MMS))
+        assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE_FOTA))
+        assertFalse(cm.isNetworkSupported(ConnectivityManager.TYPE_PROXY))
+
+        // Check that TYPE_ETHERNET is supported. Unlike the asserts above, which only validate our
+        // mocks, this assert exercises the ConnectivityService code path that ensures that
+        // TYPE_ETHERNET is supported if the ethernet service is running.
+        assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_ETHERNET))
+    }
+}
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/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
new file mode 100644
index 0000000..8155fd0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.content.Intent
+import android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
+import android.os.Build
+import androidx.test.filters.SmallTest
+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.visibleOnHandlerThread
+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.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.S_V2) // Bpf only supports in T+.
+class CSBpfNetMapsTest : CSTest() {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
+    @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testCSTrackDataSaverBeforeV() {
+        val inOrder = inOrder(bpfNetMaps)
+        mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_WHITELISTED)
+        inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+        mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_DISABLED)
+        inOrder.verify(bpfNetMaps).setDataSaverEnabled(false)
+        mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_ENABLED)
+        inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+    }
+
+    // Data Saver Status is updated from platform code in V+.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testCSTrackDataSaverAboveU() {
+        listOf(RESTRICT_BACKGROUND_STATUS_WHITELISTED, RESTRICT_BACKGROUND_STATUS_ENABLED,
+            RESTRICT_BACKGROUND_STATUS_DISABLED).forEach {
+            mockDataSaverStatus(it)
+            verify(bpfNetMaps, never()).setDataSaverEnabled(anyBoolean())
+        }
+    }
+
+    private fun mockDataSaverStatus(status: Int) {
+        doReturn(status).`when`(context.networkPolicyManager).getRestrictBackgroundStatus(anyInt())
+        // While the production code dispatches the intent on the handler thread,
+        // The test would dispatch the intent in the caller thread. Make it dispatch
+        // on the handler thread to match production behavior.
+        visibleOnHandlerThread(csHandler) {
+            context.sendBroadcast(Intent(ACTION_RESTRICT_BACKGROUND_CHANGED))
+        }
+        waitForIdle()
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
new file mode 100644
index 0000000..0bad60d
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.Manifest.permission.NETWORK_STACK
+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
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+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.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.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
+
+// This allows keeping all the networks connected without having to file individual requests
+// for them.
+private fun keepScore() = FromS(
+        NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+)
+
+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 lp(iface: String) = LinkProperties().apply {
+    interfaceName = iface
+    addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+    addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSCaptivePortalAppTest : CSTest() {
+    private val WIFI_IFACE = "wifi0"
+    private val TEST_REDIRECT_URL = "http://example.com/firstPath"
+    private val TIMEOUT_MS = 2_000L
+
+    @Test
+    fun testCaptivePortalApp_Reevaluate_Nopermission() {
+        val captivePortalCallback = TestableNetworkCallback()
+        val captivePortalRequest = NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build()
+        cm.registerNetworkCallback(captivePortalRequest, captivePortalCallback)
+        val wifiAgent = createWifiAgent()
+        wifiAgent.connectWithCaptivePortal(TEST_REDIRECT_URL)
+        captivePortalCallback.expectAvailableCallbacksUnvalidated(wifiAgent)
+        val signInIntent = startCaptivePortalApp(wifiAgent)
+        // Remove the granted permissions
+        context.setPermission(
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                PERMISSION_DENIED
+        )
+        context.setPermission(NETWORK_STACK, PERMISSION_DENIED)
+        val captivePortal: CaptivePortal? = signInIntent.getParcelableExtra(EXTRA_CAPTIVE_PORTAL)
+        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)
+        )
+    }
+
+    private fun startCaptivePortalApp(networkAgent: CSAgentWrapper): Intent {
+        val network = networkAgent.network
+        cm.startCaptivePortalApp(network)
+        waitForIdle()
+        verify(networkAgent.networkMonitor).launchCaptivePortalApp()
+
+        val testBundle = Bundle()
+        val testKey = "testkey"
+        val testValue = "testvalue"
+        testBundle.putString(testKey, testValue)
+        context.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED)
+        cm.startCaptivePortalApp(network, testBundle)
+        val signInIntent: Intent = context.expectStartActivityIntent(TIMEOUT_MS)
+        assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, signInIntent.getAction())
+        assertEquals(testValue, signInIntent.getStringExtra(testKey))
+        return signInIntent
+    }
+}
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/CSDestroyedNetworkTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
new file mode 100644
index 0000000..5c29e3a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val LONG_TIMEOUT_MS = 5_000
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CSDestroyedNetworkTests : CSTest() {
+    @Test
+    fun testDestroyNetworkNotKeptWhenUnvalidated() {
+        val nc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .build()
+
+        val nr = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .build()
+        val cbRequest = TestableNetworkCallback()
+        val cbCallback = TestableNetworkCallback()
+        cm.requestNetwork(nr, cbRequest)
+        cm.registerNetworkCallback(nr, cbCallback)
+
+        val firstAgent = Agent(nc = nc)
+        firstAgent.connect()
+        cbCallback.expectAvailableCallbacks(firstAgent.network, validated = false)
+
+        firstAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+
+        val secondAgent = Agent(nc = nc)
+        secondAgent.connect()
+        cbCallback.expectAvailableCallbacks(secondAgent.network, validated = false)
+
+        cbCallback.expect<Lost>(timeoutMs = 500) { it.network == firstAgent.network }
+    }
+}
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..93f6e81
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
@@ -0,0 +1,313 @@
+/*
+ * 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_SERVICE
+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() = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_VPN)
+        .removeCapability(NET_CAPABILITY_NOT_VPN)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .setTransportInfo(
+                VpnTransportInfo(
+                        TYPE_VPN_SERVICE,
+                        "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())
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
new file mode 100644
index 0000000..94c68c0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.LocalNetworkConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+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.KEEP_CONNECTED_LOCAL_NETWORK
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSKeepConnectedTest : CSTest() {
+    @Test
+    fun testKeepConnectedLocalAgent() {
+        deps.setBuildSdk(VERSION_V)
+        val nc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build()
+        val keepConnectedAgent = Agent(nc = nc, score = FromS(NetworkScore.Builder()
+                .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                .build()),
+                lnc = FromS(LocalNetworkConfig.Builder().build()))
+        val dontKeepConnectedAgent = Agent(nc = nc,
+                lnc = FromS(LocalNetworkConfig.Builder().build()))
+        doTestKeepConnected(keepConnectedAgent, dontKeepConnectedAgent)
+    }
+
+    @Test
+    fun testKeepConnectedForTest() {
+        val keepAgent = Agent(score = FromS(NetworkScore.Builder()
+                .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+                .build()))
+        val dontKeepAgent = Agent()
+        doTestKeepConnected(keepAgent, dontKeepAgent)
+    }
+
+    fun doTestKeepConnected(keepAgent: CSAgentWrapper, dontKeepAgent: CSAgentWrapper) {
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+        keepAgent.connect()
+        dontKeepAgent.connect()
+
+        cb.expectAvailableCallbacks(keepAgent.network, validated = false)
+        cb.expectAvailableCallbacks(dontKeepAgent.network, validated = false)
+
+        // After the nascent timer, the agent without keep connected gets lost.
+        cb.expect<Lost>(dontKeepAgent.network)
+        cb.assertNoCallback()
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
new file mode 100644
index 0000000..cb98454
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.content.pm.PackageManager.FEATURE_LEANBACK
+import android.net.INetd
+import android.net.LocalNetworkConfig
+import android.net.NativeNetworkConfig
+import android.net.NativeNetworkType
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.VpnManager
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertFailsWith
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+
+private const val TIMEOUT_MS = 2_000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
+
+private fun keepConnectedScore() =
+        FromS(NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build())
+
+private fun defaultLnc() = FromS(LocalNetworkConfig.Builder().build())
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSLocalAgentCreationTests(
+        private val sdkLevel: Int,
+        private val isTv: Boolean,
+        private val addLocalNetCapToRequest: Boolean
+) : CSTest() {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters
+        fun arguments() = listOf(
+                arrayOf(VERSION_V, false /* isTv */, true /* addLocalNetCapToRequest */),
+                arrayOf(VERSION_V, false /* isTv */, false /* addLocalNetCapToRequest */),
+                arrayOf(VERSION_V, true /* isTv */, true /* addLocalNetCapToRequest */),
+                arrayOf(VERSION_V, true /* isTv */, false /* addLocalNetCapToRequest */),
+                arrayOf(VERSION_U, false /* isTv */, true /* addLocalNetCapToRequest */),
+                arrayOf(VERSION_U, false /* isTv */, false /* addLocalNetCapToRequest */),
+                arrayOf(VERSION_U, true /* isTv */, true /* addLocalNetCapToRequest */),
+                arrayOf(VERSION_U, true /* isTv */, false /* addLocalNetCapToRequest */),
+                arrayOf(VERSION_T, false /* isTv */, false /* addLocalNetCapToRequest */),
+                arrayOf(VERSION_T, true /* isTv */, false /* addLocalNetCapToRequest */),
+        )
+    }
+
+    private fun makeNativeNetworkConfigLocal(netId: Int, permission: Int) =
+            NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL_LOCAL, permission,
+                    false /* secure */, VpnManager.TYPE_VPN_NONE, false /* excludeLocalRoutes */)
+
+    @Test
+    fun testLocalAgents() {
+        val netdInOrder = inOrder(netd)
+        deps.setBuildSdk(sdkLevel)
+        doReturn(isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
+        val allNetworksCb = TestableNetworkCallback()
+        val request = NetworkRequest.Builder()
+        if (addLocalNetCapToRequest) {
+            request.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+        }
+        cm.registerNetworkCallback(request.build(), allNetworksCb)
+        val ncTemplate = NetworkCapabilities.Builder().run {
+            addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+            addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+        }.build()
+        val localAgent = if (sdkLevel >= VERSION_V || sdkLevel == VERSION_U && isTv) {
+            Agent(nc = ncTemplate, score = keepConnectedScore(), lnc = defaultLnc())
+        } else {
+            assertFailsWith<IllegalArgumentException> { Agent(nc = ncTemplate, lnc = defaultLnc()) }
+            netdInOrder.verify(netd, never()).networkCreate(any())
+            return
+        }
+        localAgent.connect()
+        netdInOrder.verify(netd).networkCreate(
+                makeNativeNetworkConfigLocal(localAgent.network.netId, INetd.PERMISSION_NONE))
+        if (addLocalNetCapToRequest) {
+            assertEquals(localAgent.network, allNetworksCb.expect<Available>().network)
+        } else {
+            allNetworksCb.assertNoCallback(NO_CALLBACK_TIMEOUT_MS)
+        }
+        cm.unregisterNetworkCallback(allNetworksCb)
+        localAgent.disconnect()
+        netdInOrder.verify(netd, timeout(TIMEOUT_MS)).networkDestroy(localAgent.network.netId)
+    }
+
+    @Test
+    fun testBadAgents() {
+        assertFailsWith<IllegalArgumentException> {
+            Agent(nc = NetworkCapabilities.Builder()
+                    .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                    .build(),
+                    lnc = null)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            Agent(nc = NetworkCapabilities.Builder().build(), lnc = defaultLnc())
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
new file mode 100644
index 0000000..83fff87
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -0,0 +1,891 @@
+/*
+ * 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.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.LocalNetworkConfig
+import android.net.MulticastRoutingConfig
+import android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_DUN
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+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_THREAD
+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.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
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+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
+private const val LONG_TIMEOUT_MS = 5_000
+
+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 lp(iface: String) = LinkProperties().apply {
+    interfaceName = iface
+    addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+    addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
+// This allows keeping all the networks connected without having to file individual requests
+// for them.
+private fun keepScore() = FromS(
+        NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+)
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSLocalAgentTests : CSTest() {
+    val multicastRoutingConfigMinScope =
+                MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, 4)
+                .build()
+    val multicastRoutingConfigSelected =
+                MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_SELECTED)
+                .build()
+    val upstreamSelectorAny = NetworkRequest.Builder()
+                .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build()
+    val upstreamSelectorWifi = NetworkRequest.Builder()
+                .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .addTransportType(TRANSPORT_WIFI)
+                .build()
+    val upstreamSelectorCell = NetworkRequest.Builder()
+                .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .addTransportType(TRANSPORT_CELLULAR)
+                .build()
+
+    @Test
+    fun testBadAgents() {
+        deps.setBuildSdk(VERSION_V)
+
+        assertFailsWith<IllegalArgumentException> {
+            Agent(nc = NetworkCapabilities.Builder()
+                    .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                    .build(),
+                    lnc = null)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            Agent(nc = NetworkCapabilities.Builder().build(),
+                    lnc = FromS(LocalNetworkConfig.Builder().build()))
+        }
+    }
+
+    @Test
+    fun testStructuralConstraintViolation() {
+        deps.setBuildSdk(VERSION_V)
+
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(NetworkRequest.Builder()
+                .clearCapabilities()
+                .build(),
+                cb)
+        val agent = Agent(nc = NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build(),
+                lnc = FromS(LocalNetworkConfig.Builder().build()))
+        agent.connect()
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        agent.sendNetworkCapabilities(NetworkCapabilities.Builder().build())
+        cb.expect<Lost>(agent.network)
+
+        val agent2 = Agent(nc = NetworkCapabilities.Builder()
+                .build(),
+                lnc = null)
+        agent2.connect()
+        cb.expectAvailableCallbacks(agent2.network, validated = false)
+        agent2.sendNetworkCapabilities(NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build())
+        cb.expect<Lost>(agent2.network)
+    }
+
+    @Test
+    fun testUpdateLocalAgentConfig() {
+        deps.setBuildSdk(VERSION_V)
+
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build(),
+                cb)
+
+        // Set up a local agent that should forward its traffic to the best DUN upstream.
+        val localAgent = Agent(
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = FromS(LocalNetworkConfig.Builder().build()),
+        )
+        localAgent.connect()
+
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val wifiAgent = Agent(score = keepScore(), lp = lp("wifi0"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
+
+        val newLnc = LocalNetworkConfig.Builder()
+                .setUpstreamSelector(NetworkRequest.Builder()
+                        .addTransportType(TRANSPORT_WIFI)
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .build())
+                .build()
+        localAgent.sendLocalNetworkConfig(newLnc)
+
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        localAgent.sendLocalNetworkConfig(LocalNetworkConfig.Builder().build())
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+        localAgent.sendLocalNetworkConfig(newLnc)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        wifiAgent.disconnect()
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+        localAgent.disconnect()
+    }
+
+    private fun createLocalAgent(name: String, localNetworkConfig: FromS<LocalNetworkConfig>):
+                CSAgentWrapper {
+        val localAgent = Agent(
+                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
+    }
+
+    private fun createWifiAgent(name: String): CSAgentWrapper {
+        return Agent(score = keepScore(), lp = lp(name),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+    }
+
+    private fun createCellAgent(name: String): CSAgentWrapper {
+        return Agent(score = keepScore(), lp = lp(name),
+                nc = nc(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET))
+    }
+
+    private fun sendLocalNetworkConfig(
+            localAgent: CSAgentWrapper,
+            upstreamSelector: NetworkRequest?,
+            upstreamConfig: MulticastRoutingConfig,
+            downstreamConfig: MulticastRoutingConfig
+    ) {
+        val newLnc = LocalNetworkConfig.Builder()
+                .setUpstreamSelector(upstreamSelector)
+                .setUpstreamMulticastRoutingConfig(upstreamConfig)
+                .setDownstreamMulticastRoutingConfig(downstreamConfig)
+                .build()
+        localAgent.sendLocalNetworkConfig(newLnc)
+    }
+
+    @Test
+    fun testMulticastRoutingConfig() {
+        deps.setBuildSdk(VERSION_V)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+        val inOrder = inOrder(multicastRoutingCoordinatorService)
+
+        val lnc = FromS(LocalNetworkConfig.Builder()
+                .setUpstreamSelector(upstreamSelectorWifi)
+                .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+                .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+                .build()
+        )
+        val localAgent = createLocalAgent("local0", lnc)
+        localAgent.connect()
+
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val wifiAgent = createWifiAgent("wifi0")
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "local0", "wifi0", multicastRoutingConfigMinScope)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "wifi0", "local0", multicastRoutingConfigSelected)
+
+        wifiAgent.disconnect()
+
+        inOrder.verify(multicastRoutingCoordinatorService)
+                .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+        inOrder.verify(multicastRoutingCoordinatorService)
+                .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
+
+        localAgent.disconnect()
+    }
+
+    @Test
+    fun testMulticastRoutingConfig_2LocalNetworks() {
+        deps.setBuildSdk(VERSION_V)
+        val inOrder = inOrder(multicastRoutingCoordinatorService)
+        val lnc = FromS(LocalNetworkConfig.Builder()
+                .setUpstreamSelector(upstreamSelectorWifi)
+                .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+                .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+                .build()
+        )
+        val localAgent0 = createLocalAgent("local0", lnc)
+        localAgent0.connect()
+
+        val wifiAgent = createWifiAgent("wifi0")
+        wifiAgent.connect()
+        waitForIdle()
+
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "local0", "wifi0", multicastRoutingConfigMinScope)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "wifi0", "local0", multicastRoutingConfigSelected)
+
+        val localAgent1 = createLocalAgent("local1", lnc)
+        localAgent1.connect()
+        waitForIdle()
+
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "local1", "wifi0", multicastRoutingConfigMinScope)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "wifi0", "local1", multicastRoutingConfigSelected)
+
+        localAgent0.disconnect()
+        localAgent1.disconnect()
+        wifiAgent.disconnect()
+    }
+
+    @Test
+    fun testMulticastRoutingConfig_UpstreamNetworkCellToWifi() {
+        deps.setBuildSdk(VERSION_V)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+                        .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .build(), cb)
+        val inOrder = inOrder(multicastRoutingCoordinatorService)
+        val lnc = FromS(LocalNetworkConfig.Builder()
+                .setUpstreamSelector(upstreamSelectorAny)
+                .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+                .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+                .build()
+        )
+        val localAgent = createLocalAgent("local0", lnc)
+        val wifiAgent = createWifiAgent("wifi0")
+        val cellAgent = createCellAgent("cell0")
+
+        localAgent.connect()
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        cellAgent.connect()
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == cellAgent.network
+        }
+
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "local0", "cell0", multicastRoutingConfigMinScope)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "cell0", "local0", multicastRoutingConfigSelected)
+
+        wifiAgent.connect()
+
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        // upstream should have been switched to wifi
+        inOrder.verify(multicastRoutingCoordinatorService)
+                .applyMulticastRoutingConfig("local0", "cell0", CONFIG_FORWARD_NONE)
+        inOrder.verify(multicastRoutingCoordinatorService)
+                .applyMulticastRoutingConfig("cell0", "local0", CONFIG_FORWARD_NONE)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "local0", "wifi0", multicastRoutingConfigMinScope)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "wifi0", "local0", multicastRoutingConfigSelected)
+
+        localAgent.disconnect()
+        cellAgent.disconnect()
+        wifiAgent.disconnect()
+    }
+
+    @Test
+    fun testMulticastRoutingConfig_UpstreamSelectorCellToWifi() {
+        deps.setBuildSdk(VERSION_V)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+                        .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .build(), cb)
+        val inOrder = inOrder(multicastRoutingCoordinatorService)
+        val lnc = FromS(LocalNetworkConfig.Builder()
+                .setUpstreamSelector(upstreamSelectorCell)
+                .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+                .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+                .build()
+        )
+        val localAgent = createLocalAgent("local0", lnc)
+        val wifiAgent = createWifiAgent("wifi0")
+        val cellAgent = createCellAgent("cell0")
+
+        localAgent.connect()
+        cellAgent.connect()
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == cellAgent.network
+        }
+
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "local0", "cell0", multicastRoutingConfigMinScope)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "cell0", "local0", multicastRoutingConfigSelected)
+
+        sendLocalNetworkConfig(localAgent, upstreamSelectorWifi, multicastRoutingConfigMinScope,
+                multicastRoutingConfigSelected)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        // upstream should have been switched to wifi
+        inOrder.verify(multicastRoutingCoordinatorService)
+                .applyMulticastRoutingConfig("local0", "cell0", CONFIG_FORWARD_NONE)
+        inOrder.verify(multicastRoutingCoordinatorService)
+                .applyMulticastRoutingConfig("cell0", "local0", CONFIG_FORWARD_NONE)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "local0", "wifi0", multicastRoutingConfigMinScope)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "wifi0", "local0", multicastRoutingConfigSelected)
+
+        localAgent.disconnect()
+        cellAgent.disconnect()
+        wifiAgent.disconnect()
+    }
+
+    @Test
+    fun testMulticastRoutingConfig_UpstreamSelectorWifiToNull() {
+        deps.setBuildSdk(VERSION_V)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+                        .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .build(), cb)
+        val inOrder = inOrder(multicastRoutingCoordinatorService)
+        val lnc = FromS(LocalNetworkConfig.Builder()
+                .setUpstreamSelector(upstreamSelectorWifi)
+                .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+                .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+                .build()
+        )
+        val localAgent = createLocalAgent("local0", lnc)
+        localAgent.connect()
+        val wifiAgent = createWifiAgent("wifi0")
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "local0", "wifi0", multicastRoutingConfigMinScope)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "wifi0", "local0", multicastRoutingConfigSelected)
+
+        sendLocalNetworkConfig(localAgent, null, multicastRoutingConfigMinScope,
+                multicastRoutingConfigSelected)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == null
+        }
+
+        // upstream should have been switched to null
+        inOrder.verify(multicastRoutingCoordinatorService)
+                .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+        inOrder.verify(multicastRoutingCoordinatorService)
+                .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
+        inOrder.verify(multicastRoutingCoordinatorService, never()).applyMulticastRoutingConfig(
+                eq("local0"), any(), eq(multicastRoutingConfigMinScope))
+        inOrder.verify(multicastRoutingCoordinatorService, never()).applyMulticastRoutingConfig(
+                any(), eq("local0"), eq(multicastRoutingConfigSelected))
+
+        localAgent.disconnect()
+        wifiAgent.disconnect()
+    }
+
+    @Test
+    fun testUnregisterUpstreamAfterReplacement_SameIfaceName() {
+        doTestUnregisterUpstreamAfterReplacement(true)
+    }
+
+    @Test
+    fun testUnregisterUpstreamAfterReplacement_DifferentIfaceName() {
+        doTestUnregisterUpstreamAfterReplacement(false)
+    }
+
+    fun doTestUnregisterUpstreamAfterReplacement(sameIfaceName: Boolean) {
+        deps.setBuildSdk(VERSION_V)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+        // Set up a local agent that should forward its traffic to the best wifi upstream.
+        val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = FromS(LocalNetworkConfig.Builder()
+                        .setUpstreamSelector(upstreamSelectorWifi)
+                        .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+                        .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+                        .build()),
+                score = FromS(NetworkScore.Builder()
+                        .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build())
+        )
+        localAgent.connect()
+
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val wifiAgent = Agent(lp = lp("wifi0"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
+
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        clearInvocations(netd)
+        clearInvocations(multicastRoutingCoordinatorService)
+        val inOrder = inOrder(netd, multicastRoutingCoordinatorService)
+        wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+        waitForIdle()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi0")
+        inOrder.verify(multicastRoutingCoordinatorService)
+                .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+        inOrder.verify(multicastRoutingCoordinatorService)
+                .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
+        inOrder.verify(netd).networkDestroy(wifiAgent.network.netId)
+
+        val wifiIface2 = if (sameIfaceName) "wifi0" else "wifi1"
+        val wifiAgent2 = Agent(lp = lp(wifiIface2),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent2.connect()
+
+        cb.expectAvailableCallbacks(wifiAgent2.network, validated = false)
+        cb.expect<LocalInfoChanged> { it.info.upstreamNetwork == wifiAgent2.network }
+        cb.expect<Lost> { it.network == wifiAgent.network }
+
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", wifiIface2)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                "local0", wifiIface2, multicastRoutingConfigMinScope)
+        inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+                wifiIface2, "local0", multicastRoutingConfigSelected)
+
+        inOrder.verify(netd, never()).ipfwdRemoveInterfaceForward(any(), any())
+        inOrder.verify(multicastRoutingCoordinatorService, never())
+                .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+        inOrder.verify(multicastRoutingCoordinatorService, never())
+                .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
+    }
+
+    @Test
+    fun testUnregisterUpstreamAfterReplacement_neverReplaced() {
+        deps.setBuildSdk(VERSION_V)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+        // Set up a local agent that should forward its traffic to the best wifi upstream.
+        val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = FromS(LocalNetworkConfig.Builder()
+                        .setUpstreamSelector(NetworkRequest.Builder()
+                                .addTransportType(TRANSPORT_WIFI)
+                                .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                                .build())
+                        .build()),
+                score = FromS(NetworkScore.Builder()
+                        .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build())
+        )
+        localAgent.connect()
+
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val wifiAgent = Agent(lp = lp("wifi0"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
+
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        clearInvocations(netd)
+        wifiAgent.unregisterAfterReplacement(TIMEOUT_MS.toInt())
+        waitForIdle()
+        verify(netd).networkDestroy(wifiAgent.network.netId)
+        verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi0")
+
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+        cb.expect<Lost> { it.network == wifiAgent.network }
+    }
+
+    @Test
+    fun testUnregisterLocalAgentAfterReplacement() {
+        deps.setBuildSdk(VERSION_V)
+
+        val localCb = TestableNetworkCallback()
+        cm.requestNetwork(NetworkRequest.Builder().clearCapabilities()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build(),
+                localCb)
+
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+        val localNc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK)
+        val lnc = FromS(LocalNetworkConfig.Builder()
+                .setUpstreamSelector(NetworkRequest.Builder()
+                        .addTransportType(TRANSPORT_WIFI)
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .build())
+                .build())
+        val localScore = FromS(NetworkScore.Builder().build())
+
+        // Set up a local agent that should forward its traffic to the best wifi upstream.
+        val localAgent = Agent(nc = localNc, lp = lp("local0"), lnc = lnc, score = localScore)
+        localAgent.connect()
+
+        localCb.expectAvailableCallbacks(localAgent.network, validated = false)
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val wifiAgent = Agent(lp = lp("wifi0"), nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
+
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        listOf(cb, localCb).forEach {
+            it.expect<LocalInfoChanged>(localAgent.network) {
+                it.info.upstreamNetwork == wifiAgent.network
+            }
+        }
+
+        verify(netd).ipfwdAddInterfaceForward("local0", "wifi0")
+
+        localAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+
+        val localAgent2 = Agent(nc = localNc, lp = lp("local0"), lnc = lnc, score = localScore)
+        localAgent2.connect()
+
+        localCb.expectAvailableCallbacks(localAgent2.network,
+                validated = false, upstream = wifiAgent.network)
+        cb.expectAvailableCallbacks(localAgent2.network,
+                validated = false, upstream = wifiAgent.network)
+        cb.expect<Lost> { it.network == localAgent.network }
+    }
+
+    @Test
+    fun testDestroyedNetworkAsSelectedUpstream() {
+        deps.setBuildSdk(VERSION_V)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+        val wifiAgent = Agent(lp = lp("wifi0"), nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Unregister wifi pending replacement, then set up a local agent that would have
+        // this network as its upstream.
+        wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+        val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = FromS(LocalNetworkConfig.Builder()
+                        .setUpstreamSelector(NetworkRequest.Builder()
+                                .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                                .addTransportType(TRANSPORT_WIFI)
+                                .build())
+                        .build()),
+                score = FromS(NetworkScore.Builder()
+                        .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build())
+        )
+
+        // Connect the local agent. The zombie wifi is its upstream, but the stack doesn't
+        // tell netd to add the forward since the wifi0 interface has gone.
+        localAgent.connect()
+        cb.expectAvailableCallbacks(localAgent.network,
+                validated = false, upstream = wifiAgent.network)
+
+        verify(netd, never()).ipfwdAddInterfaceForward("local0", "wifi0")
+
+        // Disconnect wifi without a replacement. Expect an update with upstream null.
+        wifiAgent.disconnect()
+        verify(netd, never()).ipfwdAddInterfaceForward("local0", "wifi0")
+        cb.expect<LocalInfoChanged> { it.info.upstreamNetwork == null }
+    }
+
+    @Test
+    fun testForwardingRules() {
+        deps.setBuildSdk(VERSION_V)
+        // Set up a local agent that should forward its traffic to the best DUN upstream.
+        val lnc = FromS(LocalNetworkConfig.Builder()
+                .setUpstreamSelector(NetworkRequest.Builder()
+                        .addCapability(NET_CAPABILITY_DUN)
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .build())
+                .build())
+        val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = lnc,
+                score = FromS(NetworkScore.Builder()
+                        .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build())
+        )
+        localAgent.connect()
+
+        val wifiAgent = Agent(score = keepScore(), lp = lp("wifi0"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        val cellAgentDun = Agent(score = keepScore(), lp = lp("cell0"),
+                nc = nc(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+        val wifiAgentDun = Agent(score = keepScore(), lp = lp("wifi1"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build(),
+                cb)
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val inOrder = inOrder(netd)
+        inOrder.verify(netd, never()).ipfwdAddInterfaceForward(any(), any())
+        cb.assertNoCallback()
+
+        wifiAgent.connect()
+        inOrder.verify(netd, never()).ipfwdAddInterfaceForward(any(), any())
+        cb.assertNoCallback()
+
+        cellAgentDun.connect()
+        inOrder.verify(netd).ipfwdEnableForwarding(any())
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "cell0")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == cellAgentDun.network
+        }
+
+        wifiAgentDun.connect()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "cell0")
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi1")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgentDun.network
+        }
+
+        // Make sure sending the same config again doesn't do anything
+        repeat(5) {
+            localAgent.sendLocalNetworkConfig(lnc.value)
+        }
+        inOrder.verifyNoMoreInteractions()
+
+        wifiAgentDun.disconnect()
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == cellAgentDun.network
+        }
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi1")
+        // This can take a little bit of time because it needs to wait for the rematch
+        inOrder.verify(netd, timeout(MEDIUM_TIMEOUT_MS)).ipfwdAddInterfaceForward("local0", "cell0")
+
+        cellAgentDun.disconnect()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "cell0")
+        inOrder.verify(netd).ipfwdDisableForwarding(any())
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+        val wifiAgentDun2 = Agent(score = keepScore(), lp = lp("wifi2"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+        wifiAgentDun2.connect()
+        inOrder.verify(netd).ipfwdEnableForwarding(any())
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi2")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgentDun2.network
+        }
+
+        wifiAgentDun2.disconnect()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi2")
+        inOrder.verify(netd).ipfwdDisableForwarding(any())
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+        val wifiAgentDun3 = Agent(score = keepScore(), lp = lp("wifi3"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+        wifiAgentDun3.connect()
+        inOrder.verify(netd).ipfwdEnableForwarding(any())
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi3")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgentDun3.network
+        }
+
+        localAgent.disconnect()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi3")
+        inOrder.verify(netd).ipfwdDisableForwarding(any())
+        cb.expect<Lost>(localAgent.network)
+        cb.assertNoCallback()
+    }
+
+    @Test
+    fun testLocalNetworkUnwanted_withUpstream() {
+        doTestLocalNetworkUnwanted(true)
+    }
+
+    @Test
+    fun testLocalNetworkUnwanted_withoutUpstream() {
+        doTestLocalNetworkUnwanted(false)
+    }
+
+    fun doTestLocalNetworkUnwanted(haveUpstream: Boolean) {
+        deps.setBuildSdk(VERSION_V)
+
+        val nr = NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build()
+        val requestCb = TestableNetworkCallback()
+        cm.requestNetwork(nr, requestCb)
+        val listenCb = TestableNetworkCallback()
+        cm.registerNetworkCallback(nr, listenCb)
+
+        val upstream = if (haveUpstream) {
+            Agent(score = keepScore(), lp = lp("wifi0"),
+                    nc = nc(TRANSPORT_WIFI)).also { it.connect() }
+        } else {
+            null
+        }
+
+        // Set up a local agent.
+        val lnc = FromS(LocalNetworkConfig.Builder().apply {
+            if (haveUpstream) {
+                setUpstreamSelector(NetworkRequest.Builder()
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .addTransportType(TRANSPORT_WIFI)
+                        .build())
+            }
+        }.build())
+        val localAgent = Agent(nc = nc(TRANSPORT_THREAD, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = lnc,
+                score = FromS(NetworkScore.Builder().build())
+        )
+        localAgent.connect()
+
+        requestCb.expectAvailableCallbacks(localAgent.network,
+                validated = false, upstream = upstream?.network)
+        listenCb.expectAvailableCallbacks(localAgent.network,
+                validated = false, upstream = upstream?.network)
+
+        cm.unregisterNetworkCallback(requestCb)
+
+        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/CSNetworkActivityTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
new file mode 100644
index 0000000..df0a2cc
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
@@ -0,0 +1,268 @@
+/*
+ * 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.ConnectivityManager
+import android.net.ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE
+import android.net.ConnectivityManager.EXTRA_DEVICE_TYPE
+import android.net.ConnectivityManager.EXTRA_IS_ACTIVE
+import android.net.ConnectivityManager.EXTRA_REALTIME_NS
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_IMS
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+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.os.Build
+import android.os.ConditionVariable
+import android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH
+import android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener
+import com.android.server.CSTest.CSContext
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertNotNull
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val DATA_CELL_IFNAME = "rmnet_data"
+private const val IMS_CELL_IFNAME = "rmnet_ims"
+private const val WIFI_IFNAME = "wlan0"
+private const val TIMESTAMP = 1234L
+private const val NETWORK_ACTIVITY_NO_UID = -1
+private const val PACKAGE_UID = 123
+private const val TIMEOUT_MS = 250L
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CSNetworkActivityTest : CSTest() {
+
+    private fun getRegisteredNetdUnsolicitedEventListener(): BaseNetdUnsolicitedEventListener {
+        val captor = ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener::class.java)
+        verify(netd).registerUnsolicitedEventListener(captor.capture())
+        return captor.value
+    }
+
+    @Test
+    fun testInterfaceClassActivityChanged_NonDefaultNetwork() {
+        val netdUnsolicitedEventListener = getRegisteredNetdUnsolicitedEventListener()
+        val batteryStatsInorder = inOrder(batteryStats)
+
+        val cellNr = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build()
+        val cellCb = TestableNetworkCallback()
+        // Request cell network to keep cell network up
+        cm.requestNetwork(cellNr, cellCb)
+
+        val defaultCb = TestableNetworkCallback()
+        cm.registerDefaultNetworkCallback(defaultCb)
+
+        val cellNc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .build()
+        val cellLp = LinkProperties().apply {
+            interfaceName = DATA_CELL_IFNAME
+        }
+        // Connect Cellular network
+        val cellAgent = Agent(nc = cellNc, lp = cellLp)
+        cellAgent.connect()
+        defaultCb.expectAvailableCallbacks(cellAgent.network, validated = false)
+
+        val wifiNc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .build()
+        val wifiLp = LinkProperties().apply {
+            interfaceName = WIFI_IFNAME
+        }
+        // Connect Wi-Fi network, Wi-Fi network should be the default network.
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        defaultCb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        batteryStatsInorder.verify(batteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+        val onNetworkActiveCv = ConditionVariable()
+        val listener = ConnectivityManager.OnNetworkActiveListener { onNetworkActiveCv::open }
+        cm.addDefaultNetworkActiveListener(listener)
+
+        // Cellular network (non default network) goes to inactive state.
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                cellAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+        // Non-default network activity change does not change default network activity
+        // But cellular radio power state is updated
+        assertFalse(onNetworkActiveCv.block(TIMEOUT_MS))
+        context.expectNoDataActivityBroadcast(0 /* timeoutMs */)
+        assertTrue(cm.isDefaultNetworkActive)
+        batteryStatsInorder.verify(batteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_LOW),
+                anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+        // Cellular network (non default network) goes to active state.
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+                cellAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+        // Non-default network activity change does not change default network activity
+        // But cellular radio power state is updated
+        assertFalse(onNetworkActiveCv.block(TIMEOUT_MS))
+        context.expectNoDataActivityBroadcast(0 /* timeoutMs */)
+        assertTrue(cm.isDefaultNetworkActive)
+        batteryStatsInorder.verify(batteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+                anyLong() /* timestampNs */, eq(PACKAGE_UID))
+
+        cm.unregisterNetworkCallback(cellCb)
+        cm.unregisterNetworkCallback(defaultCb)
+        cm.removeDefaultNetworkActiveListener(listener)
+    }
+
+    @Test
+    fun testDataActivityTracking_MultiCellNetwork() {
+        val netdUnsolicitedEventListener = getRegisteredNetdUnsolicitedEventListener()
+        val batteryStatsInorder = inOrder(batteryStats)
+
+        val dataNetworkNc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .build()
+        val dataNetworkNr = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build()
+        val dataNetworkLp = LinkProperties().apply {
+            interfaceName = DATA_CELL_IFNAME
+        }
+        val dataNetworkCb = TestableNetworkCallback()
+        cm.requestNetwork(dataNetworkNr, dataNetworkCb)
+        val dataNetworkAgent = Agent(nc = dataNetworkNc, lp = dataNetworkLp)
+        val dataNetworkNetId = dataNetworkAgent.network.netId.toString()
+
+        val imsNetworkNc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_IMS)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .build()
+        val imsNetworkNr = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_IMS)
+                .build()
+        val imsNetworkLp = LinkProperties().apply {
+            interfaceName = IMS_CELL_IFNAME
+        }
+        val imsNetworkCb = TestableNetworkCallback()
+        cm.requestNetwork(imsNetworkNr, imsNetworkCb)
+        val imsNetworkAgent = Agent(nc = imsNetworkNc, lp = imsNetworkLp)
+        val imsNetworkNetId = imsNetworkAgent.network.netId.toString()
+
+        dataNetworkAgent.connect()
+        dataNetworkCb.expectAvailableCallbacks(dataNetworkAgent.network, validated = false)
+
+        imsNetworkAgent.connect()
+        imsNetworkCb.expectAvailableCallbacks(imsNetworkAgent.network, validated = false)
+
+        // Both cell networks have idleTimers
+        verify(netd).idletimerAddInterface(eq(DATA_CELL_IFNAME), anyInt(), eq(dataNetworkNetId))
+        verify(netd).idletimerAddInterface(eq(IMS_CELL_IFNAME), anyInt(), eq(imsNetworkNetId))
+        verify(netd, never()).idletimerRemoveInterface(eq(DATA_CELL_IFNAME), anyInt(),
+                eq(dataNetworkNetId))
+        verify(netd, never()).idletimerRemoveInterface(eq(IMS_CELL_IFNAME), anyInt(),
+                eq(imsNetworkNetId))
+
+        // Both cell networks go to inactive state
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                imsNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                dataNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+
+        // Data cell network goes to active state. This should update the cellular radio power state
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+                dataNetworkAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+        batteryStatsInorder.verify(batteryStats, timeout(TIMEOUT_MS)).noteMobileRadioPowerState(
+                eq(DC_POWER_STATE_HIGH), anyLong() /* timestampNs */, eq(PACKAGE_UID))
+        // Ims cell network goes to active state. But this should not update the cellular radio
+        // power state since cellular radio power state is already high
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+                imsNetworkAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+        waitForIdle()
+        batteryStatsInorder.verify(batteryStats, never()).noteMobileRadioPowerState(anyInt(),
+                anyLong() /* timestampNs */, anyInt())
+
+        // Data cell network goes to inactive state. But this should not update the cellular radio
+        // power state ims cell network is still active state
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                dataNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+        waitForIdle()
+        batteryStatsInorder.verify(batteryStats, never()).noteMobileRadioPowerState(anyInt(),
+                anyLong() /* timestampNs */, anyInt())
+
+        // Ims cell network goes to inactive state.
+        // This should update the cellular radio power state
+        netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                imsNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+        batteryStatsInorder.verify(batteryStats, timeout(TIMEOUT_MS)).noteMobileRadioPowerState(
+                eq(DC_POWER_STATE_LOW), anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+        dataNetworkAgent.disconnect()
+        dataNetworkCb.expect<Lost>(dataNetworkAgent.network)
+        verify(netd).idletimerRemoveInterface(eq(DATA_CELL_IFNAME), anyInt(), eq(dataNetworkNetId))
+
+        imsNetworkAgent.disconnect()
+        imsNetworkCb.expect<Lost>(imsNetworkAgent.network)
+        verify(netd).idletimerRemoveInterface(eq(IMS_CELL_IFNAME), anyInt(), eq(imsNetworkNetId))
+
+        cm.unregisterNetworkCallback(dataNetworkCb)
+        cm.unregisterNetworkCallback(imsNetworkCb)
+    }
+}
+
+internal fun CSContext.expectDataActivityBroadcast(
+        deviceType: Int,
+        isActive: Boolean,
+        tsNanos: Long
+) {
+    assertNotNull(orderedBroadcastAsUserHistory.poll(BROADCAST_TIMEOUT_MS) {
+        intent -> intent.action.equals(ACTION_DATA_ACTIVITY_CHANGE) &&
+            intent.getIntExtra(EXTRA_DEVICE_TYPE, -1) == deviceType &&
+            intent.getBooleanExtra(EXTRA_IS_ACTIVE, !isActive) == isActive &&
+            intent.getLongExtra(EXTRA_REALTIME_NS, -1) == tsNanos
+    })
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
new file mode 100644
index 0000000..35f8ae5
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.NetworkCapabilities
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkRequestStateStatsMetricsTests : CSTest() {
+    private val CELL_INTERNET_NOT_METERED_NC = NetworkCapabilities.Builder()
+            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+            .build().setRequestorUidAndPackageName(Process.myUid(), context.getPackageName())
+
+    private val CELL_INTERNET_NOT_METERED_NR = NetworkRequest.Builder()
+            .setCapabilities(CELL_INTERNET_NOT_METERED_NC).build()
+
+    @Before
+    fun setup() {
+        waitForIdle()
+        clearInvocations(networkRequestStateStatsMetrics)
+    }
+
+    @Test
+    fun testRequestTypeNRProduceMetrics() {
+        cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+        waitForIdle()
+
+        verify(networkRequestStateStatsMetrics).onNetworkRequestReceived(
+                argThat{req -> req.networkCapabilities.equals(
+                        CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+    }
+
+    @Test
+    fun testListenTypeNRProduceNoMetrics() {
+        cm.registerNetworkCallback(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+        waitForIdle()
+        verify(networkRequestStateStatsMetrics, never()).onNetworkRequestReceived(any())
+    }
+
+    @Test
+    fun testRemoveRequestTypeNRProduceMetrics() {
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, cb)
+
+        waitForIdle()
+        clearInvocations(networkRequestStateStatsMetrics)
+
+        cm.unregisterNetworkCallback(cb)
+        waitForIdle()
+        verify(networkRequestStateStatsMetrics).onNetworkRequestRemoved(
+                argThat{req -> req.networkCapabilities.equals(
+                        CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
new file mode 100644
index 0000000..88c2738
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -0,0 +1,238 @@
+/*
+ * 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.IpPrefix
+import android.net.INetd
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NativeNetworkConfig
+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
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkScore
+import android.net.NetworkCapabilities.TRANSPORT_SATELLITE
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.RouteInfo
+import android.net.UidRange
+import android.net.UidRangeParcel
+import android.net.VpnManager
+import android.net.netd.aidl.NativeUidRangeConfig
+import android.os.Build
+import android.os.UserHandle
+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
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+private const val SECONDARY_USER = 10
+private val SECONDARY_USER_HANDLE = UserHandle(SECONDARY_USER)
+private const val TEST_PACKAGE_UID = 123
+private const val TEST_PACKAGE_UID2 = 321
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSSatelliteNetworkTest : CSTest() {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
+    /**
+     * Test createMultiLayerNrisFromSatelliteNetworkPreferredUids returns correct
+     * NetworkRequestInfo.
+     */
+    @Test
+    fun testCreateMultiLayerNrisFromSatelliteNetworkPreferredUids() {
+        // Verify that empty uid set should not create any NRI for it.
+        val nrisNoUid = service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(emptySet())
+        Assert.assertEquals(0, nrisNoUid.size.toLong())
+        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)
+        assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(mutableSetOf(uid1))
+        assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(mutableSetOf(uid1, uid3))
+        assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(mutableSetOf(uid1, uid2))
+    }
+
+    /**
+     * Test that satellite network satisfies satellite fallback per-app default network request and
+     * send correct net id and uid ranges to netd.
+     */
+    private fun doTestSatelliteNetworkFallbackUids(restricted: Boolean) {
+        val netdInOrder = inOrder(netd)
+
+        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, 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 fallback uids status.
+        updateSatelliteNetworkFallbackUids(setOf())
+        netdInOrder.verify(netd, never()).networkAddUidRangesParcel(any())
+        netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
+
+        // 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
+        )
+        updateSatelliteNetworkFallbackUids(uids)
+        netdInOrder.verify(netd).networkAddUidRangesParcel(config1)
+        netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
+
+        // 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
+        )
+        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)
+        val nri = nris.iterator().next()
+        // Verify that one NRI is created with multilayer requests. Because one NRI can contain
+        // multiple uid ranges, so it only need create one NRI here.
+        assertEquals(1, nris.size.toLong())
+        assertTrue(nri.isMultilayerRequest)
+        assertEquals(nri.uids, uidRangesForUids(uids))
+        assertEquals(PREFERENCE_ORDER_SATELLITE_FALLBACK, nri.mPreferenceOrder)
+    }
+
+    private fun updateSatelliteNetworkFallbackUids(uids: Set<Int>) {
+        visibleOnHandlerThread(csHandler) {
+            deps.satelliteNetworkFallbackUidUpdate!!.accept(uids)
+        }
+    }
+
+    private fun nativeNetworkConfigPhysical(netId: Int, permission: Int) =
+        NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission,
+            false /* secure */, VpnManager.TYPE_VPN_NONE, false /* excludeLocalRoutes */)
+
+    private fun createSatelliteAgent(name: String, restricted: Boolean = true): CSAgentWrapper {
+        return Agent(score = keepScore(), lp = lp(name),
+            nc = satelliteNc(restricted)
+        )
+    }
+
+    private fun toUidRangeStableParcels(ranges: Set<UidRange>): Array<UidRangeParcel?> {
+        val stableRanges = arrayOfNulls<UidRangeParcel>(ranges.size)
+        for ((index, range) in ranges.withIndex()) {
+            stableRanges[index] = UidRangeParcel(range.start, range.stop)
+        }
+        return stableRanges
+    }
+
+    private fun uidRangesForUids(vararg uids: Int): Set<UidRange> {
+        val ranges = ArraySet<UidRange>()
+        for (uid in uids) {
+            ranges.add(UidRange(uid, uid))
+        }
+        return ranges
+    }
+
+    private fun uidRangesForUids(uids: Collection<Int>): Set<UidRange> {
+        return uidRangesForUids(*CollectionUtils.toIntArray(uids))
+    }
+
+    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
+        addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+        addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+    }
+
+    // This allows keeping all the networks connected without having to file individual requests
+    // for them.
+    private fun keepScore() = FromS(
+        NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+    )
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
new file mode 100644
index 0000000..13c5cbc
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -0,0 +1,211 @@
+/*
+ * 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.content.Context
+import android.net.ConnectivityManager
+import android.net.INetworkMonitor
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP
+import android.net.INetworkMonitorCallbacks
+import android.net.LinkProperties
+import android.net.LocalNetworkConfig
+import android.net.Network
+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
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkTestResultParcelable
+import android.net.networkstack.NetworkStackClientBase
+import android.os.HandlerThread
+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
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.verify
+import org.mockito.stubbing.Answer
+
+const val SHORT_TIMEOUT_MS = 200L
+
+private inline fun <reified T> ArgumentCaptor() = ArgumentCaptor.forClass(T::class.java)
+
+private val agentCounter = AtomicInteger(1)
+private fun nextAgentId() = agentCounter.getAndIncrement()
+
+/**
+ * A wrapper for network agents, for use with CSTest.
+ *
+ * This class knows how to interact with CSTest and has helpful methods to make fake agents
+ * that can be manipulated directly from a test.
+ */
+class CSAgentWrapper(
+        val context: Context,
+        val deps: ConnectivityService.Dependencies,
+        csHandlerThread: HandlerThread,
+        networkStack: NetworkStackClientBase,
+        nac: NetworkAgentConfig,
+        val nc: NetworkCapabilities,
+        val lp: LinkProperties,
+        val lnc: FromS<LocalNetworkConfig>?,
+        val score: FromS<NetworkScore>,
+        val provider: NetworkProvider?
+) : TestableNetworkCallback.HasNetwork {
+    private val TAG = "CSAgent${nextAgentId()}"
+    private val VALIDATION_RESULT_INVALID = 0
+    private val NO_PROBE_RESULT = 0
+    private val VALIDATION_TIMESTAMP = 1234L
+    private val agent: NetworkAgent
+    private val nmCallbacks: INetworkMonitorCallbacks
+    val networkMonitor = mock<INetworkMonitor>()
+    private var nmValidationRedirectUrl: String? = null
+    private var nmValidationResult = NO_PROBE_RESULT
+    private var nmProbesCompleted = NO_PROBE_RESULT
+    private var nmProbesSucceeded = NO_PROBE_RESULT
+
+    override val network: Network get() = agent.network!!
+
+    init {
+        // Capture network monitor callbacks and simulate network monitor
+        val validateAnswer = Answer {
+            CSTest.CSTestExecutor.execute { onValidationRequested() }
+            null
+        }
+        doAnswer(validateAnswer).`when`(networkMonitor).notifyNetworkConnected(any(), any())
+        doAnswer(validateAnswer).`when`(networkMonitor).notifyNetworkConnectedParcel(any())
+        doAnswer(validateAnswer).`when`(networkMonitor).forceReevaluation(anyInt())
+        val nmNetworkCaptor = ArgumentCaptor<Network>()
+        val nmCbCaptor = ArgumentCaptor<INetworkMonitorCallbacks>()
+        doNothing().`when`(networkStack).makeNetworkMonitor(
+                nmNetworkCaptor.capture(),
+                any() /* name */,
+                nmCbCaptor.capture())
+
+        // Create the actual agent. NetworkAgent is abstract, so make an anonymous subclass.
+        if (deps.isAtLeastS()) {
+            agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
+                    nc, lp, lnc?.value, score.value, nac, provider) {}
+        } else {
+            agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
+                    nc, lp, 50 /* score */, nac, provider) {}
+        }
+        agent.register()
+        assertEquals(agent.network!!.netId, nmNetworkCaptor.value.netId)
+        nmCallbacks = nmCbCaptor.value
+        nmCallbacks.onNetworkMonitorCreated(networkMonitor)
+    }
+
+    private fun onValidationRequested() {
+        if (deps.isAtLeastT()) {
+            verify(networkMonitor).notifyNetworkConnectedParcel(any())
+        } else {
+            verify(networkMonitor).notifyNetworkConnected(any(), any())
+        }
+        nmCallbacks.notifyProbeStatusChanged(0 /* completed */, 0 /* succeeded */)
+        val p = NetworkTestResultParcelable()
+        p.result = nmValidationResult
+        p.probesAttempted = nmProbesCompleted
+        p.probesSucceeded = nmProbesSucceeded
+        p.redirectUrl = nmValidationRedirectUrl
+        p.timestampMillis = VALIDATION_TIMESTAMP
+        nmCallbacks.notifyNetworkTestedWithExtras(p)
+    }
+
+    fun connect() {
+        val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+        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)
+        agent.markConnected()
+        if (null == cb.poll { it is Available && agent.network == it.network }) {
+            if (!nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) &&
+                    nc.hasTransport(TRANSPORT_CELLULAR)) {
+                // ConnectivityService adds NOT_SUSPENDED by default to all non-cell agents. An
+                // agent without NOT_SUSPENDED will not connect, instead going into the SUSPENDED
+                // state, so this call will not terminate.
+                // Instead of forcefully adding NOT_SUSPENDED to all agents like older tools did,
+                // it's better to let the developer manage it as they see fit but help them
+                // debug if they forget.
+                fail("Could not connect the agent. Did you forget to add " +
+                        "NET_CAPABILITY_NOT_SUSPENDED ?")
+            }
+            fail("Could not connect the agent. Instrumentation failure ?")
+        }
+        mgr.unregisterNetworkCallback(cb)
+    }
+
+    fun disconnect() {
+        val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+        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)
+        cb.eventuallyExpect<Available> { it.network == agent.network }
+        agent.unregister()
+        cb.eventuallyExpect<Lost> { it.network == agent.network }
+    }
+
+    fun unregisterAfterReplacement(timeoutMs: Int) = agent.unregisterAfterReplacement(timeoutMs)
+
+    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) {
+        nmProbesCompleted = probesCompleted
+        nmProbesSucceeded = probesSucceeded
+    }
+
+    fun setCaptivePortal(redirectUrl: String) {
+        nmValidationResult = VALIDATION_RESULT_INVALID
+        nmValidationRedirectUrl = redirectUrl
+        // Suppose the portal is found when NetworkMonitor probes NETWORK_VALIDATION_PROBE_HTTP
+        // 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 */)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
new file mode 100644
index 0000000..de56ae5
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -0,0 +1,550 @@
+/*
+ * 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.app.AlarmManager
+import android.app.AppOpsManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.content.pm.UserInfo
+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
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkPolicyManager
+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
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Process
+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 androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.app.IBatteryStats
+import com.android.internal.util.test.BroadcastInterceptingContext
+import com.android.modules.utils.build.SdkLevel
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException
+import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker
+import com.android.server.connectivity.CarrierPrivilegeAuthenticator
+import com.android.server.connectivity.ClatCoordinator
+import com.android.server.connectivity.ConnectivityFlags
+import com.android.server.connectivity.MulticastRoutingCoordinatorService
+import com.android.server.connectivity.MultinetworkPolicyTracker
+import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
+import com.android.server.connectivity.NetworkRequestStateStatsMetrics
+import com.android.server.connectivity.ProxyTracker
+import com.android.server.connectivity.SatelliteAccessController
+import com.android.testutils.visibleOnHandlerThread
+import com.android.testutils.waitForIdle
+import java.util.concurrent.Executors
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
+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
+
+internal const val HANDLER_TIMEOUT_MS = 2_000L
+internal const val BROADCAST_TIMEOUT_MS = 3_000L
+internal const val TEST_PACKAGE_NAME = "com.android.test.package"
+internal const val WIFI_WOL_IFNAME = "test_wlan_wol"
+internal val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
+
+open class FromS<Type>(val value: Type)
+
+internal const val VERSION_UNMOCKED = -1
+internal const val VERSION_R = 1
+internal const val VERSION_S = 2
+internal const val VERSION_T = 3
+internal const val VERSION_U = 4
+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
+            TRANSPORT_CELLULAR -> ConnectivityManager.TYPE_MOBILE
+            TRANSPORT_ETHERNET -> ConnectivityManager.TYPE_ETHERNET
+            TRANSPORT_TEST -> ConnectivityManager.TYPE_TEST
+            TRANSPORT_VPN -> ConnectivityManager.TYPE_VPN
+            TRANSPORT_WIFI -> ConnectivityManager.TYPE_WIFI
+            else -> ConnectivityManager.TYPE_NONE
+        }
+
+/**
+ * Base class for tests testing ConnectivityService and its satellites.
+ *
+ * This class sets up a ConnectivityService running locally in the test.
+ */
+// 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)"
+            )
+        }
+    }
+
+    val instrumentationContext =
+            TestableContext(InstrumentationRegistry.getInstrumentation().context)
+    val context = CSContext(instrumentationContext)
+
+    // See constructor for default-enabled features. All queried features must be either enabled
+    // or disabled, because the test can't hold READ_DEVICE_CONFIG and device config utils query
+    // permissions using static contexts.
+    val enabledFeatures = HashMap<String, Boolean>().also {
+        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.ALLOW_SYSUI_CONNECTIVITY_REPORTS] = 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 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
+    // need changes when new details of instrumentation are needed.
+    val contentResolver = makeMockContentResolver(context)
+
+    val PRIMARY_USER = 0
+    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()
+
+    val networkStack = mock<NetworkStackClientBase>()
+    val csHandlerThread = HandlerThread("CSTestHandler")
+    val sysResources = mock<Resources>().also { initMockedResources(it) }
+    val packageManager = makeMockPackageManager(instrumentationContext)
+    val connResources = makeMockConnResources(sysResources, packageManager)
+
+    val netd = mock<INetd>()
+    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 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()
+
+    // Initializations that start threads are done from setUp to avoid thread leak
+    lateinit var alarmHandlerThread: HandlerThread
+    lateinit var alarmManager: AlarmManager
+    lateinit var service: ConnectivityService
+    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() }
+        cm = ConnectivityManager(context, service)
+        // csHandler initialization must be after makeConnectivityService since ConnectivityService
+        // constructor starts csHandlerThread
+        csHandler = Handler(csHandlerThread.looper)
+    }
+
+    @After
+    fun tearDown() {
+        csHandlerThread.quitSafely()
+        csHandlerThread.join()
+        alarmHandlerThread.quitSafely()
+        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
+        override fun getClatCoordinator(netd: INetd?) = this@CSTest.clatCoordinator
+        override fun getNetworkStack() = this@CSTest.networkStack
+
+        override fun makeHandlerThread(tag: String) = csHandlerThread
+        override fun makeProxyTracker(context: Context, connServiceHandler: Handler) = proxyTracker
+        override fun makeMulticastRoutingCoordinatorService(handler: Handler) =
+                this@CSTest.multicastRoutingCoordinatorService
+
+        override fun makeCarrierPrivilegeAuthenticator(
+                context: Context,
+                tm: TelephonyManager,
+                requestRestrictedWifiEnabled: Boolean,
+                listener: BiConsumer<Int, Int>,
+                handler: Handler
+        ) = if (SdkLevel.isAtLeastT()) mock<CarrierPrivilegeAuthenticator>() else null
+
+        var satelliteNetworkFallbackUidUpdate: Consumer<Set<Int>>? = null
+        override fun makeSatelliteAccessController(
+            context: Context,
+            updateSatelliteNetworkFallackUid: Consumer<Set<Int>>?,
+            csHandlerThread: Handler
+        ): SatelliteAccessController? {
+            satelliteNetworkFallbackUidUpdate = updateSatelliteNetworkFallackUid
+            return satelliteAccessController
+        }
+
+        private inner class AOOKTDeps(c: Context) : AutomaticOnOffKeepaliveTracker.Dependencies(c) {
+            override fun isTetheringFeatureNotChickenedOut(name: String): Boolean {
+                return isFeatureEnabled(context, name)
+            }
+        }
+        override fun makeAutomaticOnOffKeepaliveTracker(c: Context, h: Handler) =
+                AutomaticOnOffKeepaliveTracker(c, h, AOOKTDeps(c))
+
+        override fun makeMultinetworkPolicyTracker(c: Context, h: Handler, r: Runnable) =
+                MultinetworkPolicyTracker(
+                        c,
+                        h,
+                        r,
+                        MultinetworkPolicyTrackerTestDependencies(connResources.get())
+                )
+
+        override fun makeNetworkRequestStateStatsMetrics(c: Context) =
+                this@CSTest.networkRequestStateStatsMetrics
+
+        // All queried features must be mocked, because the test cannot hold the
+        // READ_DEVICE_CONFIG permission and device config utils use static methods for
+        // checking permissions.
+        override fun isFeatureEnabled(context: Context?, name: String?) =
+                enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
+        override fun isFeatureNotChickenedOut(context: Context?, name: String?) =
+                enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
+
+        // Mocked change IDs
+        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.
+            visibleOnHandlerThread(csHandler) {
+                if (enabled) {
+                    enabledChangeIds.add(changeId)
+                } else {
+                    enabledChangeIds.remove(changeId)
+                }
+            }
+        }
+
+        override fun isChangeEnabled(changeId: Long, pkg: String, user: UserHandle) =
+                changeId in enabledChangeIds
+        override fun isChangeEnabled(changeId: Long, uid: Int) =
+                changeId in enabledChangeIds
+
+        // In AOSP, build version codes can't always distinguish between some versions (e.g. at the
+        // time of this writing U == V). Define custom ones.
+        private var sdkLevel = VERSION_UNMOCKED
+        private val isSdkUnmocked get() = sdkLevel == VERSION_UNMOCKED
+
+        fun setBuildSdk(sdkLevel: Int) {
+            require(sdkLevel <= VERSION_MAX) {
+                "setBuildSdk must not be called with Build.VERSION constants but " +
+                        "CsTest.VERSION_* constants"
+            }
+            visibleOnHandlerThread(csHandler) { this.sdkLevel = sdkLevel }
+        }
+
+        override fun isAtLeastS() = if (isSdkUnmocked) super.isAtLeastS() else sdkLevel >= VERSION_S
+        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) {
+        val pacProxyManager = mock<PacProxyManager>()
+        val networkPolicyManager = mock<NetworkPolicyManager>()
+
+        // Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant
+        // For permissions granted across the board, the key is only the permission name.
+        // For permissions only granted to a combination of uid/pid, the key
+        // is "<permission name>,<pid>,<uid>". PID+UID permissions have priority over generic ones.
+        private val mMockedPermissions: HashMap<String, Int> = HashMap()
+        private val mStartedActivities = LinkedBlockingQueue<Intent>()
+        override fun getPackageManager() = this@CSTest.packageManager
+        override fun getContentResolver() = this@CSTest.contentResolver
+
+        // 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.
+        override fun checkPermission(permission: String, pid: Int, uid: Int) =
+            checkMockedPermission(permission, pid, uid, PERMISSION_GRANTED)
+
+        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
+            )
+            if (!granted.equals(PERMISSION_GRANTED)) {
+                throw SecurityException("[Test] permission denied: " + permission)
+            }
+        }
+
+        // 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.
+        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 {
+            val processSpecificKey = "$permission,$pid,$uid"
+            return mMockedPermissions[processSpecificKey]
+                    ?: mMockedPermissions[permission] ?: default
+        }
+
+        /**
+         * Mock checks for the specified permission, and have them behave as per `granted` or
+         * `denied`.
+         *
+         * This will apply to all calls no matter what the checked UID and PID are.
+         *
+         * @param granted One of {@link PackageManager#PermissionResult}.
+         */
+        fun setPermission(permission: String, @PermissionResult granted: Int) {
+            mMockedPermissions.put(permission, granted)
+        }
+
+        /**
+         * Mock checks for the specified permission, and have them behave as per `granted` or
+         * `denied`.
+         *
+         * This will only apply to the passed UID and PID.
+         *
+         * @param granted One of {@link PackageManager#PermissionResult}.
+         */
+        fun setPermission(permission: String, pid: Int, uid: Int, @PermissionResult granted: Int) {
+            mMockedPermissions.put("$permission,$pid,$uid", granted)
+        }
+
+        // Necessary for MultinetworkPolicyTracker, which tries to register a receiver for
+        // all users. The test can't do that since it doesn't hold INTERACT_ACROSS_USERS.
+        // TODO : ensure MultinetworkPolicyTracker's BroadcastReceiver is tested ; ideally,
+        // just returning null should not have tests pass
+        override fun registerReceiverForAllUsers(
+                receiver: BroadcastReceiver?,
+                filter: IntentFilter,
+                broadcastPermission: String?,
+                scheduler: Handler?
+        ): Intent? = null
+
+        // Create and cache user managers on the fly as necessary.
+        val userManagers = HashMap<UserHandle, UserManager>()
+        override fun createContextAsUser(user: UserHandle, flags: Int): Context {
+            val asUser = mock(Context::class.java, delegatesTo<Any>(this))
+            doReturn(user).`when`(asUser).getUser()
+            doAnswer { userManagers.computeIfAbsent(user) {
+                mock(UserManager::class.java, delegatesTo<Any>(userManager)) }
+            }.`when`(asUser).getSystemService(Context.USER_SERVICE)
+            return asUser
+        }
+
+        // List of mocked services. Add additional services here or in subclasses.
+        override fun getSystemService(serviceName: String) = when (serviceName) {
+            Context.CONNECTIVITY_SERVICE -> cm
+            Context.PAC_PROXY_SERVICE -> pacProxyManager
+            Context.NETWORK_POLICY_SERVICE -> networkPolicyManager
+            Context.ALARM_SERVICE -> alarmManager
+            Context.USER_SERVICE -> userManager
+            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()))
+        }
+
+        override fun sendOrderedBroadcastAsUser(
+                intent: Intent,
+                user: UserHandle,
+                receiverPermission: String?,
+                resultReceiver: BroadcastReceiver?,
+                scheduler: Handler?,
+                initialCode: Int,
+                initialData: String?,
+                initialExtras: Bundle?
+        ) {
+            orderedBroadcastAsUserHistory.add(intent)
+        }
+
+        override fun startActivityAsUser(intent: Intent, handle: UserHandle) {
+            mStartedActivities.put(intent)
+        }
+
+        fun expectStartActivityIntent(timeoutMs: Long = HANDLER_TIMEOUT_MS): Intent {
+            val intent = mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS)
+            assertNotNull(intent, "Did not receive sign-in intent after " + timeoutMs + "ms")
+            return intent
+        }
+    }
+
+    // Utility methods for subclasses to use
+    fun waitForIdle() = csHandlerThread.waitForIdle(HANDLER_TIMEOUT_MS)
+
+    // Network agents. See CSAgentWrapper. This class contains utility methods to simplify
+    // creation.
+    fun Agent(
+            nc: NetworkCapabilities = defaultNc(),
+            nac: NetworkAgentConfig = emptyAgentConfig(nc.getLegacyType()),
+            lp: LinkProperties = defaultLp(),
+            lnc: FromS<LocalNetworkConfig>? = null,
+            score: FromS<NetworkScore> = defaultScore(),
+            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 {
+            transports.forEach {
+                addTransportType(it)
+            }
+        }.addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .build()
+        return Agent(nc = nc, lp = lp)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
new file mode 100644
index 0000000..8ff790c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("CsTestHelpers")
+
+package com.android.server
+
+import android.app.ActivityManager
+import android.app.AlarmManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_BLUETOOTH
+import android.content.pm.PackageManager.FEATURE_ETHERNET
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT
+import android.content.pm.UserInfo
+import android.content.res.Resources
+import android.net.IDnsResolver
+import android.net.INetd
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkScore
+import android.net.RouteInfo
+import android.net.metrics.IpConnectivityLog
+import android.os.Binder
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.os.SystemConfigManager
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import android.test.mock.MockContentResolver
+import com.android.connectivity.resources.R
+import com.android.internal.util.WakeupMessage
+import com.android.internal.util.test.FakeSettingsProvider
+import com.android.modules.utils.build.SdkLevel
+import com.android.server.ConnectivityService.Dependencies
+import com.android.server.connectivity.ConnectivityResources
+import kotlin.test.fail
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+
+internal inline fun <reified T> mock() = Mockito.mock(T::class.java)
+internal inline fun <reified T> any() = any(T::class.java)
+
+internal fun emptyAgentConfig(legacyType: Int) = NetworkAgentConfig.Builder()
+        .setLegacyType(legacyType)
+        .build()
+
+internal fun defaultNc() = NetworkCapabilities.Builder()
+        // Add sensible defaults for agents that don't want to care
+        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+        .build()
+
+internal fun defaultScore() = FromS(NetworkScore.Builder().build())
+
+internal fun defaultLp() = LinkProperties().apply {
+    addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+    addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
+internal fun makeMockContentResolver(context: Context) = MockContentResolver(context).apply {
+    addProvider(Settings.AUTHORITY, FakeSettingsProvider())
+}
+
+internal fun makeMockUserManager(info: UserInfo, handle: UserHandle) = mock<UserManager>().also {
+    doReturn(listOf(info)).`when`(it).getAliveUsers()
+    doReturn(listOf(handle)).`when`(it).getUserHandles(ArgumentMatchers.anyBoolean())
+}
+
+internal fun makeActivityManager() = mock<ActivityManager>().also {
+    if (SdkLevel.isAtLeastU()) {
+        doNothing().`when`(it).registerUidFrozenStateChangedCallback(any(), any())
+    }
+}
+
+internal fun makeMockPackageManager(realContext: Context) = mock<PackageManager>().also { pm ->
+    val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET)
+    doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) })
+    val myPackageName = realContext.packageName
+    val myPackageInfo = realContext.packageManager.getPackageInfo(myPackageName,
+            PackageManager.GET_PERMISSIONS)
+    // Very high version code so that the checks for the module version will always
+    // say that it is recent enough. This is the most sensible default, but if some
+    // test needs to test with different version codes they can re-mock this with a
+    // different value.
+    myPackageInfo.longVersionCode = 9999999L
+    doReturn(arrayOf(myPackageName)).`when`(pm).getPackagesForUid(Binder.getCallingUid())
+    doReturn(myPackageInfo).`when`(pm).getPackageInfoAsUser(
+            eq(myPackageName), anyInt(), eq(UserHandle.getCallingUserId()))
+    doReturn(listOf(myPackageInfo)).`when`(pm)
+            .getInstalledPackagesAsUser(eq(PackageManager.GET_PERMISSIONS), anyInt())
+}
+
+internal fun makeMockConnResources(resources: Resources, pm: PackageManager) = mock<Context>().let {
+    doReturn(resources).`when`(it).resources
+    doReturn(pm).`when`(it).packageManager
+    ConnectivityResources.setResourcesContextForTest(it)
+    ConnectivityResources(it)
+}
+
+private val UNREASONABLY_LONG_ALARM_WAIT_MS = 1000
+internal fun makeMockAlarmManager(handlerThread: HandlerThread) = mock<AlarmManager>().also { am ->
+    val alrmHdlr = handlerThread.threadHandler
+    doAnswer {
+        val (_, date, _, wakeupMsg, handler) = it.arguments
+        wakeupMsg as WakeupMessage
+        handler as Handler
+        val delayMs = ((date as Long) - SystemClock.elapsedRealtime()).coerceAtLeast(0)
+        if (delayMs > UNREASONABLY_LONG_ALARM_WAIT_MS) {
+            fail("Attempting to send msg more than $UNREASONABLY_LONG_ALARM_WAIT_MS" +
+                    "ms into the future : $delayMs")
+        }
+        alrmHdlr.postDelayed({ handler.post(wakeupMsg::onAlarm) }, wakeupMsg, delayMs)
+    }.`when`(am).setExact(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), anyLong(), anyString(),
+            any<WakeupMessage>(), any())
+    doAnswer {
+        alrmHdlr.removeCallbacksAndMessages(it.getArgument<WakeupMessage>(0))
+    }.`when`(am).cancel(any<WakeupMessage>())
+}
+
+internal fun makeMockSystemConfigManager() = mock<SystemConfigManager>().also {
+    doReturn(intArrayOf(0)).`when`(it).getSystemPermissionUids(anyString())
+}
+
+// Mocking resources used by ConnectivityService. Note these can't be defined to return the
+// value returned by the mocking, because a non-null method would mean the helper would also
+// return non-null and the compiler would check that, but mockito has no qualms returning null
+// from a @NonNull method when stubbing. Hence, mock() = doReturn().getString() would crash
+// at runtime, because getString() returns non-null String, therefore mock returns non-null String,
+// and kotlinc adds an intrinsics check for that, which crashes at runtime when mockito actually
+// returns null.
+private fun Resources.mock(r: Int, v: Boolean) { doReturn(v).`when`(this).getBoolean(r) }
+private fun Resources.mock(r: Int, v: Int) { doReturn(v).`when`(this).getInteger(r) }
+private fun Resources.mock(r: Int, v: String) { doReturn(v).`when`(this).getString(r) }
+private fun Resources.mock(r: Int, v: Array<String?>) { doReturn(v).`when`(this).getStringArray(r) }
+private fun Resources.mock(r: Int, v: IntArray) { doReturn(v).`when`(this).getIntArray(r) }
+
+internal fun initMockedResources(res: Resources) {
+    // Resources accessed through reflection need to return the id
+    doReturn(R.array.config_networkSupportedKeepaliveCount).`when`(res)
+            .getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any())
+    doReturn(R.array.network_switch_type_name).`when`(res)
+            .getIdentifier(eq("network_switch_type_name"), eq("array"), any())
+    // Mock the values themselves
+    res.mock(R.integer.config_networkTransitionTimeout, 60_000)
+    res.mock(R.string.config_networkCaptivePortalServerUrl, "")
+    res.mock(R.array.config_wakeonlan_supported_interfaces, arrayOf(WIFI_WOL_IFNAME))
+    res.mock(R.array.config_networkSupportedKeepaliveCount, arrayOf("0,1", "1,3"))
+    res.mock(R.array.config_networkNotifySwitches, arrayOfNulls<String>(size = 0))
+    res.mock(R.array.config_protectedNetworks, intArrayOf(10, 11, 12, 14, 15))
+    res.mock(R.array.network_switch_type_name, arrayOfNulls<String>(size = 0))
+    res.mock(R.integer.config_networkAvoidBadWifi, 1)
+    res.mock(R.integer.config_activelyPreferBadWifi, 0)
+    res.mock(R.bool.config_cellular_radio_timesharing_capable, true)
+}
+
+private val TEST_LINGER_DELAY_MS = 400
+private val TEST_NASCENT_DELAY_MS = 300
+internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies) =
+        ConnectivityService(
+                context,
+                mock<IDnsResolver>(),
+                mock<IpConnectivityLog>(),
+                netd,
+                deps).also {
+            it.mLingerDelayMs = TEST_LINGER_DELAY_MS
+            it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
+        }
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/BpfInterfaceMapHelperTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapHelperTest.java
new file mode 100644
index 0000000..7b3bea3
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapHelperTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.server.net;
+
+import static android.system.OsConstants.EPERM;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.IndentingPrintWriter;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.S32;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestBpfMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+public final class BpfInterfaceMapHelperTest {
+    private static final int TEST_INDEX = 1;
+    private static final int TEST_INDEX2 = 2;
+    private static final String TEST_INTERFACE_NAME = "test1";
+    private static final String TEST_INTERFACE_NAME2 = "test2";
+
+    private BaseNetdUnsolicitedEventListener mListener;
+    private BpfInterfaceMapHelper mUpdater;
+    private IBpfMap<S32, InterfaceMapValue> mBpfMap =
+            spy(new TestBpfMap<>(S32.class, InterfaceMapValue.class));
+
+    private class TestDependencies extends BpfInterfaceMapHelper.Dependencies {
+        @Override
+        public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
+            return mBpfMap;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mUpdater = new BpfInterfaceMapHelper(new TestDependencies());
+    }
+
+    @Test
+    public void testGetIfNameByIndex() throws Exception {
+        mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
+        assertEquals(TEST_INTERFACE_NAME, mUpdater.getIfNameByIndex(TEST_INDEX));
+    }
+
+    @Test
+    public void testGetIfNameByIndexNoEntry() {
+        assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
+    }
+
+    @Test
+    public void testGetIfNameByIndexException() throws Exception {
+        doThrow(new ErrnoException("", EPERM)).when(mBpfMap).getValue(new S32(TEST_INDEX));
+        assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
+    }
+
+    private void assertDumpContains(final String dump, final String message) {
+        assertTrue(String.format("dump(%s) does not contain '%s'", dump, message),
+                dump.contains(message));
+    }
+
+    private String getDump() {
+        final StringWriter sw = new StringWriter();
+        mUpdater.dump(new IndentingPrintWriter(new PrintWriter(sw), " "));
+        return sw.toString();
+    }
+
+    @Test
+    public void testDump() throws ErrnoException {
+        mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
+        mBpfMap.updateEntry(new S32(TEST_INDEX2), new InterfaceMapValue(TEST_INTERFACE_NAME2));
+
+        final String dump = getDump();
+        assertDumpContains(dump, "IfaceIndexNameMap: OK");
+        assertDumpContains(dump, "ifaceIndex=1 ifaceName=test1");
+        assertDumpContains(dump, "ifaceIndex=2 ifaceName=test2");
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
deleted file mode 100644
index c730856..0000000
--- a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
+++ /dev/null
@@ -1,174 +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.server.net;
-
-import static android.system.OsConstants.EPERM;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.net.INetd;
-import android.net.MacAddress;
-import android.os.Build;
-import android.os.Handler;
-import android.os.test.TestLooper;
-import android.system.ErrnoException;
-import android.util.IndentingPrintWriter;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
-import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.Struct.S32;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-import com.android.testutils.TestBpfMap;
-
-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.io.PrintWriter;
-import java.io.StringWriter;
-
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
-public final class BpfInterfaceMapUpdaterTest {
-    private static final int TEST_INDEX = 1;
-    private static final int TEST_INDEX2 = 2;
-    private static final String TEST_INTERFACE_NAME = "test1";
-    private static final String TEST_INTERFACE_NAME2 = "test2";
-
-    private final TestLooper mLooper = new TestLooper();
-    private BaseNetdUnsolicitedEventListener mListener;
-    private BpfInterfaceMapUpdater mUpdater;
-    private IBpfMap<S32, InterfaceMapValue> mBpfMap =
-            spy(new TestBpfMap<>(S32.class, InterfaceMapValue.class));
-    @Mock private INetd mNetd;
-    @Mock private Context mContext;
-
-    private class TestDependencies extends BpfInterfaceMapUpdater.Dependencies {
-        @Override
-        public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
-            return mBpfMap;
-        }
-
-        @Override
-        public InterfaceParams getInterfaceParams(String ifaceName) {
-            if (ifaceName.equals(TEST_INTERFACE_NAME)) {
-                return new InterfaceParams(TEST_INTERFACE_NAME, TEST_INDEX,
-                        MacAddress.ALL_ZEROS_ADDRESS);
-            } else if (ifaceName.equals(TEST_INTERFACE_NAME2)) {
-                return new InterfaceParams(TEST_INTERFACE_NAME2, TEST_INDEX2,
-                        MacAddress.ALL_ZEROS_ADDRESS);
-            }
-
-            return null;
-        }
-
-        @Override
-        public INetd getINetd(Context ctx) {
-            return mNetd;
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        when(mNetd.interfaceGetList()).thenReturn(new String[] {TEST_INTERFACE_NAME});
-        mUpdater = new BpfInterfaceMapUpdater(mContext, new Handler(mLooper.getLooper()),
-                new TestDependencies());
-    }
-
-    private void verifyStartUpdater() throws Exception {
-        mUpdater.start();
-        mLooper.dispatchAll();
-        final ArgumentCaptor<BaseNetdUnsolicitedEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener.class);
-        verify(mNetd).registerUnsolicitedEventListener(listenerCaptor.capture());
-        mListener = listenerCaptor.getValue();
-        verify(mBpfMap).updateEntry(eq(new S32(TEST_INDEX)),
-                eq(new InterfaceMapValue(TEST_INTERFACE_NAME)));
-    }
-
-    @Test
-    public void testUpdateInterfaceMap() throws Exception {
-        verifyStartUpdater();
-
-        mListener.onInterfaceAdded(TEST_INTERFACE_NAME2);
-        mLooper.dispatchAll();
-        verify(mBpfMap).updateEntry(eq(new S32(TEST_INDEX2)),
-                eq(new InterfaceMapValue(TEST_INTERFACE_NAME2)));
-
-        // Check that when onInterfaceRemoved is called, nothing happens.
-        mListener.onInterfaceRemoved(TEST_INTERFACE_NAME);
-        mLooper.dispatchAll();
-        verifyNoMoreInteractions(mBpfMap);
-    }
-
-    @Test
-    public void testGetIfNameByIndex() throws Exception {
-        mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
-        assertEquals(TEST_INTERFACE_NAME, mUpdater.getIfNameByIndex(TEST_INDEX));
-    }
-
-    @Test
-    public void testGetIfNameByIndexNoEntry() {
-        assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
-    }
-
-    @Test
-    public void testGetIfNameByIndexException() throws Exception {
-        doThrow(new ErrnoException("", EPERM)).when(mBpfMap).getValue(new S32(TEST_INDEX));
-        assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
-    }
-
-    private void assertDumpContains(final String dump, final String message) {
-        assertTrue(String.format("dump(%s) does not contain '%s'", dump, message),
-                dump.contains(message));
-    }
-
-    private String getDump() {
-        final StringWriter sw = new StringWriter();
-        mUpdater.dump(new IndentingPrintWriter(new PrintWriter(sw), " "));
-        return sw.toString();
-    }
-
-    @Test
-    public void testDump() throws ErrnoException {
-        mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
-        mBpfMap.updateEntry(new S32(TEST_INDEX2), new InterfaceMapValue(TEST_INTERFACE_NAME2));
-
-        final String dump = getDump();
-        assertDumpContains(dump, "IfaceIndexNameMap: OK");
-        assertDumpContains(dump, "ifaceIndex=1 ifaceName=test1");
-        assertDumpContains(dump, "ifaceIndex=2 ifaceName=test2");
-    }
-}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java b/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java
index a058a46..2c9f212 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java
@@ -41,6 +41,7 @@
 abstract class NetworkStatsBaseTest {
     static final String TEST_IFACE = "test0";
     static final String TEST_IFACE2 = "test1";
+    static final String TEST_IFACE3 = "test2";
     static final String TUN_IFACE = "test_nss_tun0";
     static final String TUN_IFACE2 = "test_nss_tun1";
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt b/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt
new file mode 100644
index 0000000..9f2d4d3
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.util.IndentingPrintWriter
+import com.android.server.net.NetworkStatsEventLogger.MAX_POLL_REASON
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK
+import com.android.server.net.NetworkStatsEventLogger.PollEvent
+import com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf
+import com.android.testutils.DevSdkIgnoreRunner
+import java.io.StringWriter
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+const val TEST_PERSIST_FLAG = 0x101
+
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkStatsEventLoggerTest {
+    val logger = NetworkStatsEventLogger()
+    val stringWriter = TestStringWriter()
+    val pw = IndentingPrintWriter(stringWriter)
+
+    @Test
+    fun testDump_invalid() {
+        // Verify it won't crash.
+        logger.dump(pw)
+        // Clear output buffer.
+        stringWriter.getOutputAndClear()
+
+        // Verify log invalid event throws. And nothing output in the dump.
+        val invalidReasons = listOf(-1, MAX_POLL_REASON + 1)
+        invalidReasons.forEach {
+            assertFailsWith<IllegalArgumentException> {
+                logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(it))
+            }
+            logger.dumpRecentPollEvents(pw)
+            val output = stringWriter.getOutputAndClear()
+            assertStringNotContains(output, pollReasonNameOf(it))
+        }
+    }
+
+    @Test
+    fun testDump_valid() {
+        // Choose arbitrary set of reasons for testing.
+        val loggedReasons = listOf(
+            POLL_REASON_GLOBAL_ALERT,
+            POLL_REASON_FORCE_UPDATE,
+            POLL_REASON_DUMPSYS,
+            POLL_REASON_PERIODIC,
+            POLL_REASON_RAT_CHANGED
+        )
+        val nonLoggedReasons = listOf(
+            POLL_REASON_NETWORK_STATUS_CHANGED,
+            POLL_REASON_OPEN_SESSION,
+            POLL_REASON_REG_CALLBACK)
+
+        // Add some valid records.
+        loggedReasons.forEach {
+            logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(it))
+        }
+
+        // Collect dumps.
+        logger.dumpRecentPollEvents(pw)
+        val outputRecentEvents = stringWriter.getOutputAndClear()
+        logger.dumpPollCountsPerReason(pw)
+        val outputCountsPerReason = stringWriter.getOutputAndClear()
+
+        // Verify the output contains at least necessary information.
+        loggedReasons.forEach {
+            // Verify all events are shown in the recent event dump.
+            val eventString = PollEvent(it).toString()
+            assertStringContains(outputRecentEvents, TEST_PERSIST_FLAG.toString())
+            assertStringContains(eventString, pollReasonNameOf(it))
+            assertStringContains(outputRecentEvents, eventString)
+            // Verify counts are 1 for each reason.
+            assertCountForReason(outputCountsPerReason, it, 1)
+        }
+
+        // Verify the output remains untouched for other reasons.
+        nonLoggedReasons.forEach {
+            assertStringNotContains(outputRecentEvents, PollEvent(it).toString())
+            assertCountForReason(outputCountsPerReason, it, 0)
+        }
+    }
+
+    @Test
+    fun testDump_maxEventLogs() {
+        // Choose arbitrary reason.
+        val reasonToBeTested = POLL_REASON_PERIODIC
+        val repeatCount = NetworkStatsEventLogger.MAX_EVENTS_LOGS * 2
+
+        // Collect baseline.
+        logger.dumpRecentPollEvents(pw)
+        val lineCountBaseLine = getLineCount(stringWriter.getOutputAndClear())
+
+        repeat(repeatCount) {
+            logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(reasonToBeTested))
+        }
+
+        // Collect dump.
+        logger.dumpRecentPollEvents(pw)
+        val lineCountAfterTest = getLineCount(stringWriter.getOutputAndClear())
+
+        // Verify line count increment is limited.
+        assertEquals(
+            NetworkStatsEventLogger.MAX_EVENTS_LOGS,
+            lineCountAfterTest - lineCountBaseLine
+        )
+
+        // Verify count per reason increased for the testing reason.
+        logger.dumpPollCountsPerReason(pw)
+        val outputCountsPerReason = stringWriter.getOutputAndClear()
+        for (reason in 0..MAX_POLL_REASON) {
+            assertCountForReason(
+                outputCountsPerReason,
+                reason,
+                if (reason == reasonToBeTested) repeatCount else 0
+            )
+        }
+    }
+
+    private fun getLineCount(multilineString: String) = multilineString.lines().size
+
+    private fun assertStringContains(got: String, want: String) {
+        assertTrue(got.contains(want), "Wanted: $want, but got: $got")
+    }
+
+    private fun assertStringNotContains(got: String, unwant: String) {
+        assertFalse(got.contains(unwant), "Unwanted: $unwant, but got: $got")
+    }
+
+    /**
+     * Assert the reason and the expected count are at the same line.
+     */
+    private fun assertCountForReason(dump: String, reason: Int, expectedCount: Int) {
+        // Matches strings like "GLOBAL_ALERT: 50" but not too strict since the format might change.
+        val regex = Regex(pollReasonNameOf(reason) + "[^0-9]+" + expectedCount)
+        assertEquals(
+            1,
+            regex.findAll(dump).count(),
+            "Unexpected output: $dump " + " for reason: " + pollReasonNameOf(reason)
+        )
+    }
+
+    class TestStringWriter : StringWriter() {
+        fun getOutputAndClear() = toString().also { buffer.setLength(0) }
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 292f77e..e62ac74 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -59,6 +59,7 @@
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -124,7 +125,7 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        mObserverHandlerThread = new HandlerThread("HandlerThread");
+        mObserverHandlerThread = new HandlerThread("NetworkStatsObserversTest");
         mObserverHandlerThread.start();
         final Looper observerLooper = mObserverHandlerThread.getLooper();
         mStatsObservers = new NetworkStatsObservers() {
@@ -139,6 +140,14 @@
         mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mObserverHandlerThread != null) {
+            mObserverHandlerThread.quitSafely();
+            mObserverHandlerThread.join();
+        }
+    }
+
     @Test
     public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception {
         final long thresholdTooLowBytes = 1L;
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 99f6d63..7e0a225 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;
@@ -64,10 +65,18 @@
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
+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.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;
@@ -79,9 +88,9 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -94,6 +103,7 @@
 import android.app.AlarmManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.net.DataUsageRequest;
@@ -112,6 +122,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;
@@ -119,13 +130,17 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.PowerManager;
+import android.os.Process;
 import android.os.SimpleClock;
+import android.os.UserHandle;
 import android.provider.Settings;
 import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
 import android.util.Pair;
 
 import androidx.annotation.Nullable;
@@ -153,12 +168,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;
@@ -167,7 +185,6 @@
 
 import java.io.File;
 import java.io.FileDescriptor;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.nio.file.Files;
@@ -184,6 +201,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}.
@@ -191,11 +209,13 @@
  * TODO: This test used to be really brittle because it used Easymock - it uses Mockito now, but
  * still uses the Easymock structure, which could be simplified.
  */
+@DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 // 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;
@@ -239,14 +259,17 @@
     private static @Mock WifiInfo sWifiInfo;
     private @Mock INetd mNetd;
     private @Mock TetheringManager mTetheringManager;
+    private @Mock PackageManager mPm;
     private @Mock NetworkStatsFactory mStatsFactory;
-    private @Mock NetworkStatsSettings mSettings;
+    @NonNull
+    private final TestNetworkStatsSettings mSettings =
+            new TestNetworkStatsSettings(HOUR_IN_MILLIS, WEEK_IN_MILLIS);
     private @Mock IBinder mUsageCallbackBinder;
     private TestableUsageCallback mUsageCallback;
     private @Mock AlarmManager mAlarmManager;
     @Mock
     private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
-    private @Mock BpfInterfaceMapUpdater mBpfInterfaceMapUpdater;
+    private @Mock BpfInterfaceMapHelper mBpfInterfaceMapHelper;
     private HandlerThread mHandlerThread;
     @Mock
     private LocationPermissionChecker mLocationPermissionChecker;
@@ -279,8 +302,25 @@
     private @Mock PersistentInt mImportLegacyAttemptsCounter;
     private @Mock PersistentInt mImportLegacySuccessesCounter;
     private @Mock PersistentInt mImportLegacyFallbacksCounter;
+    private int mFastDataInputTargetAttempts = 0;
+    private @Mock PersistentInt mFastDataInputSuccessesCounter;
+    private @Mock PersistentInt mFastDataInputFallbacksCounter;
+    private String mCompareStatsResult = null;
     private @Mock Resources mResources;
     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;
@@ -291,6 +331,16 @@
         }
 
         @Override
+        public PackageManager getPackageManager() {
+            return mPm;
+        }
+
+        @Override
+        public Context createContextAsUser(UserHandle user, int flags) {
+            return this;
+        }
+
+        @Override
         public Object getSystemService(String name) {
             if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
             if (Context.TETHERING_SERVICE.equals(name)) return mTetheringManager;
@@ -362,15 +412,25 @@
         PowerManager.WakeLock wakeLock =
                 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
 
-        mHandlerThread = new HandlerThread("HandlerThread");
-        final NetworkStatsService.Dependencies deps = makeDependencies();
+        mHandlerThread = new HandlerThread("NetworkStatsServiceTest-HandlerThread");
+        // Create a separate thread for observers to run on. This thread cannot be the same
+        // as the handler thread, because the observer callback is fired on this thread, and
+        // it should not be blocked by client code. Additionally, creating the observers
+        // object requires a looper, which can only be obtained after a thread has been started.
+        mObserverHandlerThread = new HandlerThread("NetworkStatsServiceTest-ObserversThread");
+        mObserverHandlerThread.start();
+        final Looper observerLooper = mObserverHandlerThread.getLooper();
+        final NetworkStatsObservers statsObservers = new NetworkStatsObservers() {
+            @Override
+            protected Looper getHandlerLooperLocked() {
+                return observerLooper;
+            }
+        };
         mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
-                mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), deps);
+                mClock, mSettings, mStatsFactory, statsObservers, mDeps);
 
         mElapsedRealtime = 0L;
 
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
         // Verify that system ready fetches realtime stats
@@ -401,130 +461,201 @@
                 any(), tetheringEventCbCaptor.capture());
         mTetheringEventCallback = tetheringEventCbCaptor.getValue();
 
+        doReturn(Process.myUid()).when(mPm)
+                .getPackageUid(eq(mServiceContext.getPackageName()), anyInt());
+
         mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
     }
 
-    @NonNull
-    private NetworkStatsService.Dependencies makeDependencies() {
-        return new NetworkStatsService.Dependencies() {
-            @Override
-            public File getLegacyStatsDir() {
-                return mLegacyStatsDir;
-            }
+    class TestDependencies extends NetworkStatsService.Dependencies {
+        private int mCompareStatsInvocation = 0;
+        private NetworkStats.Entry mMockedTrafficStatsNativeStat = null;
 
-            @Override
-            public File getOrCreateStatsDir() {
-                return mStatsDir;
-            }
+        @Override
+        public File getLegacyStatsDir() {
+            return mLegacyStatsDir;
+        }
 
-            @Override
-            public boolean getStoreFilesInApexData() {
-                return mStoreFilesInApexData;
-            }
+        @Override
+        public File getOrCreateStatsDir() {
+            return mStatsDir;
+        }
 
-            @Override
-            public int getImportLegacyTargetAttempts() {
-                return mImportLegacyTargetAttempts;
-            }
+        @Override
+        public boolean getStoreFilesInApexData() {
+            return mStoreFilesInApexData;
+        }
 
-            @Override
-            public PersistentInt createPersistentCounter(@androidx.annotation.NonNull Path dir,
-                    @androidx.annotation.NonNull String name) throws IOException {
-                switch (name) {
-                    case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
-                        return mImportLegacyAttemptsCounter;
-                    case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
-                        return mImportLegacySuccessesCounter;
-                    case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
-                        return mImportLegacyFallbacksCounter;
-                    default:
-                        throw new IllegalArgumentException("Unknown counter name: " + name);
-                }
-            }
+        @Override
+        public int getImportLegacyTargetAttempts() {
+            return mImportLegacyTargetAttempts;
+        }
 
-            @Override
-            public NetworkStatsCollection readPlatformCollection(
-                    @NonNull String prefix, long bucketDuration) {
-                return mPlatformNetworkStatsCollection.get(prefix);
-            }
+        @Override
+        public int getUseFastDataInputTargetAttempts() {
+            return mFastDataInputTargetAttempts;
+        }
 
-            @Override
-            public HandlerThread makeHandlerThread() {
-                return mHandlerThread;
-            }
+        @Override
+        public String compareStats(NetworkStatsCollection a, NetworkStatsCollection b,
+                 boolean allowKeyChange) {
+            mCompareStatsInvocation++;
+            return mCompareStatsResult;
+        }
 
-            @Override
-            public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
-                    @NonNull Context context, @NonNull Executor executor,
-                    @NonNull NetworkStatsService service) {
+        int getCompareStatsInvocation() {
+            return mCompareStatsInvocation;
+        }
 
-                return mNetworkStatsSubscriptionsMonitor;
+        @Override
+        public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name) {
+            switch (name) {
+                case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
+                    return mImportLegacyAttemptsCounter;
+                case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
+                    return mImportLegacySuccessesCounter;
+                case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
+                    return mImportLegacyFallbacksCounter;
+                case NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME:
+                    return mFastDataInputSuccessesCounter;
+                case NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME:
+                    return mFastDataInputFallbacksCounter;
+                default:
+                    throw new IllegalArgumentException("Unknown counter name: " + name);
             }
+        }
 
-            @Override
-            public ContentObserver makeContentObserver(Handler handler,
-                    NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
-                mHandler = handler;
-                return mContentObserver = super.makeContentObserver(handler, settings, monitor);
-            }
+        @Override
+        public NetworkStatsCollection readPlatformCollection(
+                @NonNull String prefix, long bucketDuration) {
+            return mPlatformNetworkStatsCollection.get(prefix);
+        }
 
-            @Override
-            public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
-                return mLocationPermissionChecker;
-            }
+        @Override
+        public HandlerThread makeHandlerThread() {
+            return mHandlerThread;
+        }
 
-            @Override
-            public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
-                    @NonNull Context ctx, @NonNull Handler handler) {
-                return mBpfInterfaceMapUpdater;
-            }
+        @Override
+        public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
+                @NonNull Context context, @NonNull Executor executor,
+                @NonNull NetworkStatsService service) {
 
-            @Override
-            public IBpfMap<S32, U8> getUidCounterSetMap() {
-                return mUidCounterSetMap;
-            }
+            return mNetworkStatsSubscriptionsMonitor;
+        }
 
-            @Override
-            public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
-                return mCookieTagMap;
-            }
+        @Override
+        public ContentObserver makeContentObserver(Handler handler,
+                NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
+            mHandler = handler;
+            return mContentObserver = super.makeContentObserver(handler, settings, monitor);
+        }
 
-            @Override
-            public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
-                return mStatsMapA;
-            }
+        @Override
+        public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
+            return mLocationPermissionChecker;
+        }
 
-            @Override
-            public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
-                return mStatsMapB;
-            }
+        @Override
+        public BpfInterfaceMapHelper makeBpfInterfaceMapHelper() {
+            return mBpfInterfaceMapHelper;
+        }
 
-            @Override
-            public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
-                return mAppUidStatsMap;
-            }
+        @Override
+        public IBpfMap<S32, U8> getUidCounterSetMap() {
+            return mUidCounterSetMap;
+        }
 
-            @Override
-            public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
-                return mIfaceStatsMap;
-            }
+        @Override
+        public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
+            return mCookieTagMap;
+        }
 
-            @Override
-            public boolean isDebuggable() {
-                return mIsDebuggable == Boolean.TRUE;
-            }
+        @Override
+        public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
+            return mStatsMapA;
+        }
 
-            @Override
-            public BpfNetMaps makeBpfNetMaps(Context ctx) {
-                return mBpfNetMaps;
-            }
+        @Override
+        public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
+            return mStatsMapB;
+        }
 
-            @Override
-            public SkDestroyListener makeSkDestroyListener(
-                    IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
-                return mSkDestroyListener;
-            }
-        };
+        @Override
+        public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
+            return mAppUidStatsMap;
+        }
+
+        @Override
+        public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
+            return mIfaceStatsMap;
+        }
+
+        @Override
+        public boolean isDebuggable() {
+            return mIsDebuggable == Boolean.TRUE;
+        }
+
+        @Override
+        public BpfNetMaps makeBpfNetMaps(Context ctx) {
+            return mBpfNetMaps;
+        }
+
+        @Override
+        public SkDestroyListener makeSkDestroyListener(
+                IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
+            return mSkDestroyListener;
+        }
+
+        @Override
+        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 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
@@ -533,13 +664,18 @@
         mStatsDir = null;
 
         mNetd = null;
-        mSettings = null;
 
         mSession.close();
         mService = null;
 
-        mHandlerThread.quitSafely();
-        mHandlerThread.join();
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+        if (mObserverHandlerThread != null) {
+            mObserverHandlerThread.quitSafely();
+            mObserverHandlerThread.join();
+        }
     }
 
     private void initWifiStats(NetworkStateSnapshot snapshot) throws Exception {
@@ -676,8 +812,6 @@
         assertStatsFilesExist(true);
 
         // boot through serviceReady() again
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
 
         mService.systemReady();
@@ -876,7 +1010,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();
@@ -886,8 +1029,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());
@@ -901,7 +1042,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.
@@ -916,7 +1057,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)));
@@ -1250,8 +1391,9 @@
                 TEST_IFACE2, IMSI_1, null /* wifiNetworkKey */,
                 false /* isTemporarilyNotMetered */, false /* isRoaming */);
 
-        final NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {
-                mobileState, buildWifiState()};
+        final NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{
+                mobileState, buildWifiState(false, TEST_IFACE, null),
+                buildWifiState(false, TEST_IFACE3, null)};
         mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
         setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_LTE);
@@ -1266,16 +1408,22 @@
         final NetworkStats.Entry entry3 = new NetworkStats.Entry(
                 TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xBEEF, METERED_NO, ROAMING_NO,
                 DEFAULT_NETWORK_NO, 1024L, 8L, 512L, 4L, 2L);
+        // Add an entry that with different wifi interface, but expected to be merged into entry3
+        // after clearing interface information.
+        final NetworkStats.Entry entry4 = new NetworkStats.Entry(
+                TEST_IFACE3, UID_BLUE, SET_DEFAULT, 0xBEEF, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 1L, 2L, 3L, 4L, 5L);
 
         final TetherStatsParcel[] emptyTetherStats = {};
         // The interfaces that expect to be used to query the stats.
-        final String[] wifiIfaces = {TEST_IFACE};
+        final String[] wifiIfaces = {TEST_IFACE, TEST_IFACE3};
         incrementCurrentTime(HOUR_IN_MILLIS);
         mockDefaultSettings();
-        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4)
                 .insertEntry(entry1)
                 .insertEntry(entry2)
-                .insertEntry(entry3), emptyTetherStats, wifiIfaces);
+                .insertEntry(entry3)
+                .insertEntry(entry4), emptyTetherStats, wifiIfaces);
 
         // getUidStatsForTransport (through getNetworkStatsUidDetail) adds all operation counts
         // with active interface, and the interface here is mobile interface, so this test makes
@@ -1293,7 +1441,7 @@
         assertValues(wifiStats, null /* iface */, UID_RED, SET_DEFAULT, 0xF00D,
                 METERED_NO, ROAMING_NO, METERED_NO, 50L, 5L, 50L, 5L, 1L);
         assertValues(wifiStats, null /* iface */, UID_BLUE, SET_DEFAULT, 0xBEEF,
-                METERED_NO, ROAMING_NO, METERED_NO, 1024L, 8L, 512L, 4L, 2L);
+                METERED_NO, ROAMING_NO, METERED_NO, 1025L, 10L, 515L, 8L, 7L);
 
         final String[] mobileIfaces = {TEST_IFACE2};
         mockNetworkStatsUidDetail(buildEmptyStats(), emptyTetherStats, mobileIfaces);
@@ -1311,6 +1459,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();
@@ -1532,7 +1731,7 @@
 
         // Register and verify request and that binder was called
         DataUsageRequest request = mService.registerUsageCallback(
-                mServiceContext.getOpPackageName(), inputRequest, mUsageCallback);
+                mServiceContext.getPackageName(), inputRequest, mUsageCallback);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
         long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB
@@ -1758,7 +1957,7 @@
     }
 
     private void setCombineSubtypeEnabled(boolean enable) {
-        doReturn(enable).when(mSettings).getCombineSubtypeEnabled();
+        mSettings.setCombineSubtypeEnabled(enable);
         mHandler.post(() -> mContentObserver.onChange(false, Settings.Global
                     .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED)));
         waitForIdle();
@@ -1926,12 +2125,17 @@
         // Templates w/o wifi network keys can query stats as usual.
         assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
         assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
+        // Templates for test network does not need to enforce location permission.
+        final NetworkTemplate templateTestIface1 = new NetworkTemplate.Builder(MATCH_TEST)
+                .setWifiNetworkKeys(Set.of(TEST_IFACE)).build();
+        assertNetworkTotal(templateTestIface1, 0L, 0L, 0L, 0L, 0);
 
         doReturn(true).when(mLocationPermissionChecker)
                 .checkCallersLocationPermission(any(), any(), anyInt(), anyBoolean(), any());
         assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
         assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
         assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
+        assertNetworkTotal(templateTestIface1, 0L, 0L, 0L, 0L, 0);
     }
 
     /**
@@ -1988,8 +2192,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);
@@ -2001,8 +2203,6 @@
         assertStatsFilesExist(false);
 
         // Boot through systemReady() again.
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
 
@@ -2076,8 +2276,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);
@@ -2089,8 +2287,6 @@
         assertStatsFilesExist(false);
 
         // Boot through systemReady() again.
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
 
@@ -2125,6 +2321,71 @@
     }
 
     @Test
+    public void testAdoptFastDataInput_featureDisabled() throws Exception {
+        // Boot through serviceReady() with flag disabled, verify the persistent
+        // counters are not increased.
+        mFastDataInputTargetAttempts = 0;
+        doReturn(0).when(mFastDataInputSuccessesCounter).get();
+        doReturn(0).when(mFastDataInputFallbacksCounter).get();
+        mService.systemReady();
+        verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+        verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+        assertEquals(0, mDeps.getCompareStatsInvocation());
+    }
+
+    @Test
+    public void testAdoptFastDataInput_noRetryAfterFail() throws Exception {
+        // Boot through serviceReady(), verify the service won't retry unexpectedly
+        // since the target attempt remains the same.
+        mFastDataInputTargetAttempts = 1;
+        doReturn(0).when(mFastDataInputSuccessesCounter).get();
+        doReturn(1).when(mFastDataInputFallbacksCounter).get();
+        mService.systemReady();
+        verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+        verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+    }
+
+    @Test
+    public void testAdoptFastDataInput_noRetryAfterSuccess() throws Exception {
+        // Boot through serviceReady(), verify the service won't retry unexpectedly
+        // since the target attempt remains the same.
+        mFastDataInputTargetAttempts = 1;
+        doReturn(1).when(mFastDataInputSuccessesCounter).get();
+        doReturn(0).when(mFastDataInputFallbacksCounter).get();
+        mService.systemReady();
+        verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+        verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+    }
+
+    @Test
+    public void testAdoptFastDataInput_hasDiff() throws Exception {
+        // Boot through serviceReady() with flag enabled and assumes the stats are
+        // failed to compare, verify the fallbacks counter is increased.
+        mockDefaultSettings();
+        doReturn(0).when(mFastDataInputSuccessesCounter).get();
+        doReturn(0).when(mFastDataInputFallbacksCounter).get();
+        mFastDataInputTargetAttempts = 1;
+        mCompareStatsResult = "Has differences";
+        mService.systemReady();
+        verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+        verify(mFastDataInputFallbacksCounter).set(1);
+    }
+
+    @Test
+    public void testAdoptFastDataInput_noDiff() throws Exception {
+        // Boot through serviceReady() with target attempts increased,
+        // assumes there was a previous failure,
+        // and assumes the stats are successfully compared,
+        // verify the successes counter is increased.
+        mFastDataInputTargetAttempts = 2;
+        doReturn(1).when(mFastDataInputFallbacksCounter).get();
+        mCompareStatsResult = null;
+        mService.systemReady();
+        verify(mFastDataInputSuccessesCounter).set(1);
+        verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+    }
+
+    @Test
     public void testStatsFactoryRemoveUids() throws Exception {
         // pretend that network comes online
         mockDefaultSettings();
@@ -2177,6 +2438,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());
@@ -2189,7 +2528,8 @@
         final DropBoxManager dropBox = mock(DropBoxManager.class);
         return new NetworkStatsRecorder(new FileRotator(
                 directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
-                observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError);
+                observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError,
+                false /* useFastDataInput */, directory);
     }
 
     private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
@@ -2240,6 +2580,8 @@
     }
 
     private void prepareForSystemReady() throws Exception {
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(buildEmptyStats());
         mockNetworkStatsSummary(buildEmptyStats());
     }
 
@@ -2277,21 +2619,80 @@
         mockSettings(HOUR_IN_MILLIS, WEEK_IN_MILLIS);
     }
 
-    private void mockSettings(long bucketDuration, long deleteAge) throws Exception {
-        doReturn(HOUR_IN_MILLIS).when(mSettings).getPollInterval();
-        doReturn(0L).when(mSettings).getPollDelay();
-        doReturn(true).when(mSettings).getSampleEnabled();
-        doReturn(false).when(mSettings).getCombineSubtypeEnabled();
+    private void mockSettings(long bucketDuration, long deleteAge) {
+        mSettings.setConfig(new Config(bucketDuration, deleteAge, deleteAge));
+    }
 
-        final Config config = new Config(bucketDuration, deleteAge, deleteAge);
-        doReturn(config).when(mSettings).getXtConfig();
-        doReturn(config).when(mSettings).getUidConfig();
-        doReturn(config).when(mSettings).getUidTagConfig();
+    // Note that this object will be accessed from test main thread and service handler thread.
+    // Thus, it has to be thread safe in order to prevent from flakiness.
+    private static class TestNetworkStatsSettings
+            extends NetworkStatsService.DefaultNetworkStatsSettings {
 
-        doReturn(MB_IN_BYTES).when(mSettings).getGlobalAlertBytes(anyLong());
-        doReturn(MB_IN_BYTES).when(mSettings).getXtPersistBytes(anyLong());
-        doReturn(MB_IN_BYTES).when(mSettings).getUidPersistBytes(anyLong());
-        doReturn(MB_IN_BYTES).when(mSettings).getUidTagPersistBytes(anyLong());
+        @NonNull
+        private volatile Config mConfig;
+        private final AtomicBoolean mCombineSubtypeEnabled = new AtomicBoolean();
+
+        TestNetworkStatsSettings(long bucketDuration, long deleteAge) {
+            mConfig = new Config(bucketDuration, deleteAge, deleteAge);
+        }
+
+        void setConfig(@NonNull Config config) {
+            mConfig = config;
+        }
+
+        @Override
+        public long getPollDelay() {
+            return 0L;
+        }
+
+        @Override
+        public long getGlobalAlertBytes(long def) {
+            return MB_IN_BYTES;
+        }
+
+        @Override
+        public Config getXtConfig() {
+            return mConfig;
+        }
+
+        @Override
+        public Config getUidConfig() {
+            return mConfig;
+        }
+
+        @Override
+        public Config getUidTagConfig() {
+            return mConfig;
+        }
+
+        @Override
+        public long getXtPersistBytes(long def) {
+            return MB_IN_BYTES;
+        }
+
+        @Override
+        public long getUidPersistBytes(long def) {
+            return MB_IN_BYTES;
+        }
+
+        @Override
+        public long getUidTagPersistBytes(long def) {
+            return MB_IN_BYTES;
+        }
+
+        @Override
+        public boolean getCombineSubtypeEnabled() {
+            return mCombineSubtypeEnabled.get();
+        }
+
+        public void setCombineSubtypeEnabled(boolean enable) {
+            mCombineSubtypeEnabled.set(enable);
+        }
+
+        @Override
+        public boolean getAugmentEnabled() {
+            return false;
+        }
     }
 
     private void assertStatsFilesExist(boolean exist) {
@@ -2338,6 +2739,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,
@@ -2573,13 +2980,13 @@
 
     @Test
     public void testDumpStatsMap() throws ErrnoException {
-        doReturn("wlan0").when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+        doReturn("wlan0").when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
         doTestDumpStatsMap("wlan0");
     }
 
     @Test
     public void testDumpStatsMapUnknownInterface() throws ErrnoException {
-        doReturn(null).when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+        doReturn(null).when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
         doTestDumpStatsMap("unknown");
     }
 
@@ -2594,13 +3001,67 @@
 
     @Test
     public void testDumpIfaceStatsMap() throws Exception {
-        doReturn("wlan0").when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+        doReturn("wlan0").when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
         doTestDumpIfaceStatsMap("wlan0");
     }
 
     @Test
     public void testDumpIfaceStatsMapUnknownInterface() throws Exception {
-        doReturn(null).when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+        doReturn(null).when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
         doTestDumpIfaceStatsMap("unknown");
     }
+
+    // Basic test to ensure event logger dump is called.
+    // Note that tests to ensure detailed correctness is done in the dedicated tests.
+    // See NetworkStatsEventLoggerTest.
+    @Test
+    public void testDumpEventLogger() {
+        setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
+        final String dump = getDump();
+        assertDumpContains(dump, pollReasonNameOf(POLL_REASON_RAT_CHANGED));
+    }
+
+    @Test
+    public void testEnforcePackageNameMatchesUid() throws Exception {
+        final String testMyPackageName = "test.package.myname";
+        final String testRedPackageName = "test.package.red";
+        final String testInvalidPackageName = "test.package.notfound";
+
+        doReturn(UID_RED).when(mPm).getPackageUid(eq(testRedPackageName), anyInt());
+        doReturn(Process.myUid()).when(mPm).getPackageUid(eq(testMyPackageName), anyInt());
+        doThrow(new PackageManager.NameNotFoundException()).when(mPm)
+                .getPackageUid(eq(testInvalidPackageName), anyInt());
+
+        assertThrows(SecurityException.class, () ->
+                mService.openSessionForUsageStats(0 /* flags */, testRedPackageName));
+        assertThrows(SecurityException.class, () ->
+                mService.openSessionForUsageStats(0 /* flags */, testInvalidPackageName));
+        assertThrows(NullPointerException.class, () ->
+                mService.openSessionForUsageStats(0 /* flags */, null));
+        // Verify package name belongs to ourselves does not throw.
+        mService.openSessionForUsageStats(0 /* flags */, testMyPackageName);
+
+        long thresholdInBytes = 10 * 1024 * 1024;  // 10 MB
+        DataUsageRequest request = new DataUsageRequest(
+                2 /* requestId */, sTemplateImsi1, thresholdInBytes);
+        assertThrows(SecurityException.class, () ->
+                mService.registerUsageCallback(testRedPackageName, request, mUsageCallback));
+        assertThrows(SecurityException.class, () ->
+                mService.registerUsageCallback(testInvalidPackageName, request, mUsageCallback));
+        assertThrows(NullPointerException.class, () ->
+                mService.registerUsageCallback(null, request, mUsageCallback));
+        mService.registerUsageCallback(testMyPackageName, request, mUsageCallback);
+    }
+
+    @Test
+    public void testDumpSkDestroyListenerLogs() throws ErrnoException {
+        doAnswer((invocation) -> {
+            final IndentingPrintWriter ipw = (IndentingPrintWriter) invocation.getArgument(0);
+            ipw.println("Log for testing");
+            return null;
+        }).when(mSkDestroyListener).dump(any());
+
+        final String dump = getDump();
+        assertDumpContains(dump, "Log for testing");
+    }
 }
diff --git a/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt b/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
new file mode 100644
index 0000000..18785e5
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
@@ -0,0 +1,63 @@
+/*
+ * 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
+
+import android.os.Handler
+import android.os.HandlerThread
+import com.android.net.module.util.SharedLog
+import com.android.testutils.DevSdkIgnoreRunner
+import java.io.PrintWriter
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class SkDestroyListenerTest {
+    @Mock lateinit var sharedLog: SharedLog
+    val handlerThread = HandlerThread("SkDestroyListenerTest")
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        handlerThread.start()
+    }
+
+    @After
+    fun tearDown() {
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+
+    @Test
+    fun testDump() {
+        doReturn(sharedLog).`when`(sharedLog).forSubComponent(any())
+
+        val handler = Handler(handlerThread.looper)
+        val skDestroylistener = SkDestroyListener(null /* cookieTagMap */, handler, sharedLog)
+        val pw = PrintWriter(System.out)
+        skDestroylistener.dump(pw)
+
+        verify(sharedLog).reverseDump(pw)
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
new file mode 100644
index 0000000..99f762d
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
@@ -0,0 +1,150 @@
+/*
+ * 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
+
+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(Entry::class.java)
+    private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs, maxSize)
+
+    @Test
+    fun testGet_returnsEntryIfNotExpired() {
+        cache.put("iface", 2, entry)
+        doReturn(500L).`when`(clock).millis() // Set clock to before expiry
+        val result = cache.get("iface", 2)
+        assertEquals(entry, result)
+    }
+
+    @Test
+    fun testGet_returnsNullIfExpired() {
+        cache.put("iface", 2, entry)
+        doReturn(2000L).`when`(clock).millis() // Set clock to after expiry
+        assertNull(cache.get("iface", 2))
+    }
+
+    @Test
+    fun testGet_returnsNullForNonExistentKey() {
+        val result = cache.get("otherIface", 99)
+        assertNull(result)
+    }
+
+    @Test
+    fun testPutAndGet_retrievesCorrectEntryForDifferentKeys() {
+        val entry1 = mock(Entry::class.java)
+        val entry2 = mock(Entry::class.java)
+
+        cache.put("iface1", 2, entry1)
+        cache.put("iface2", 4, entry2)
+
+        assertEquals(entry1, cache.get("iface1", 2))
+        assertEquals(entry2, cache.get("iface2", 4))
+    }
+
+    @Test
+    fun testPut_overridesExistingEntry() {
+        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
+
+        assertEquals(entry2, cache.get("iface", 2))
+    }
+
+    @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()
+        assertNull(cache.get("iface", 2))
+    }
+}
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 616da81..57a157d 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
diff --git a/tests/unit/vpn-jarjar-rules.txt b/tests/unit/vpn-jarjar-rules.txt
deleted file mode 100644
index 16661b9..0000000
--- a/tests/unit/vpn-jarjar-rules.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# Only keep classes imported by ConnectivityServiceTest
-keep com.android.server.VpnManagerService
-keep com.android.server.connectivity.Vpn
-keep com.android.server.connectivity.VpnProfileStore
\ No newline at end of file
diff --git a/thread/OWNERS b/thread/OWNERS
new file mode 100644
index 0000000..c93ec4d
--- /dev/null
+++ b/thread/OWNERS
@@ -0,0 +1,11 @@
+# Bug component: 1203089
+
+# Primary reviewers
+wgtdkp@google.com
+handaw@google.com
+sunytt@google.com
+
+# Secondary reviewers
+jonhui@google.com
+xyk@google.com
+zhanglongxia@google.com
diff --git a/thread/README.md b/thread/README.md
new file mode 100644
index 0000000..41b73ac
--- /dev/null
+++ b/thread/README.md
@@ -0,0 +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
new file mode 100644
index 0000000..34d67bb
--- /dev/null
+++ b/thread/TEST_MAPPING
@@ -0,0 +1,18 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsThreadNetworkTestCases"
+    },
+    {
+      "name": "ThreadNetworkUnitTests"
+    },
+    {
+      "name": "ThreadNetworkIntegrationTests"
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "ThreadNetworkMultiDeviceTests"
+    }
+  ]
+}
diff --git a/thread/apex/Android.bp b/thread/apex/Android.bp
new file mode 100644
index 0000000..edf000a
--- /dev/null
+++ b/thread/apex/Android.bp
@@ -0,0 +1,30 @@
+//
+// 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_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Versioned ot-daemon init file which will be processed by the system on 34+ devices.
+// See https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
+// for details of versioned rc files.
+prebuilt_etc {
+    name: "ot-daemon.init.34rc",
+    src: "ot-daemon.34rc",
+    filename: "init.34rc",
+    installable: false,
+}
diff --git a/thread/apex/ot-daemon.34rc b/thread/apex/ot-daemon.34rc
new file mode 100644
index 0000000..86f6b69
--- /dev/null
+++ b/thread/apex/ot-daemon.34rc
@@ -0,0 +1,25 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+service ot-daemon /apex/com.android.tethering/bin/ot-daemon -I thread-wpan --auto-attach=0 threadnetwork_hal://binder?none
+    interface aidl ot_daemon
+    disabled
+    oneshot
+    updatable
+    class main
+    user thread_network
+    group thread_network inet system
+    seclabel u:r:ot_daemon:s0
+    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
new file mode 100644
index 0000000..117b4f9
--- /dev/null
+++ b/thread/demoapp/Android.bp
@@ -0,0 +1,52 @@
+// 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_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "ThreadNetworkDemoApp",
+    srcs: ["java/**/*.java"],
+    min_sdk_version: "34",
+    resource_dirs: ["res"],
+    static_libs: [
+        "androidx-constraintlayout_constraintlayout",
+        "androidx.appcompat_appcompat",
+        "androidx.navigation_navigation-common",
+        "androidx.navigation_navigation-fragment",
+        "androidx.navigation_navigation-ui",
+        "com.google.android.material_material",
+        "guava",
+    ],
+    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/AndroidManifest.xml b/thread/demoapp/AndroidManifest.xml
new file mode 100644
index 0000000..c31bb71
--- /dev/null
+++ b/thread/demoapp/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?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="com.android.threadnetwork.demoapp">
+
+    <uses-sdk android:minSdkVersion="34" android:targetSdkVersion="35"/>
+    <uses-feature android:name="android.hardware.threadnetwork" android:required="true" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED" />
+
+    <application
+        android:label="ThreadNetworkDemoApp"
+        android:theme="@style/Theme.ThreadNetworkDemoApp"
+        android:icon="@mipmap/ic_launcher"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:testOnly="true">
+        <activity android:name=".MainActivity" android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/ConnectivityToolsFragment.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/ConnectivityToolsFragment.java
new file mode 100644
index 0000000..6f616eb
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/ConnectivityToolsFragment.java
@@ -0,0 +1,317 @@
+/*
+ * 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.threadnetwork.demoapp;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+
+import com.android.threadnetwork.demoapp.concurrent.BackgroundExecutorProvider;
+
+import com.google.android.material.switchmaterial.SwitchMaterial;
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.common.io.CharStreams;
+import com.google.common.net.InetAddresses;
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+public final class ConnectivityToolsFragment extends Fragment {
+    private static final String TAG = "ConnectivityTools";
+
+    // This is a mirror of NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK which is @hide for now
+    private static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
+
+    private static final Duration PING_TIMEOUT = Duration.ofSeconds(10L);
+    private static final Duration UDP_TIMEOUT = Duration.ofSeconds(10L);
+    private final ListeningScheduledExecutorService mBackgroundExecutor =
+            BackgroundExecutorProvider.getBackgroundExecutor();
+    private final ArrayList<String> mServerIpCandidates = new ArrayList<>();
+    private final ArrayList<String> mServerPortCandidates = new ArrayList<>();
+    private Executor mMainExecutor;
+
+    private ListenableFuture<String> mPingFuture;
+    private ListenableFuture<String> mUdpFuture;
+    private ArrayAdapter<String> mPingServerIpAdapter;
+    private ArrayAdapter<String> mUdpServerIpAdapter;
+    private ArrayAdapter<String> mUdpServerPortAdapter;
+
+    private Network mThreadNetwork;
+    private boolean mBindThreadNetwork = false;
+
+    private void subscribeToThreadNetwork() {
+        ConnectivityManager cm = getActivity().getSystemService(ConnectivityManager.class);
+        cm.registerNetworkCallback(
+                new NetworkRequest.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                        .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .build(),
+                new ConnectivityManager.NetworkCallback() {
+                    @Override
+                    public void onAvailable(Network network) {
+                        mThreadNetwork = network;
+                    }
+
+                    @Override
+                    public void onLost(Network network) {
+                        mThreadNetwork = network;
+                    }
+                },
+                new Handler(Looper.myLooper()));
+    }
+
+    private static String getPingCommand(String serverIp) {
+        try {
+            InetAddress serverAddress = InetAddresses.forString(serverIp);
+            return (serverAddress instanceof Inet6Address)
+                    ? "/system/bin/ping6"
+                    : "/system/bin/ping";
+        } catch (IllegalArgumentException e) {
+            // The ping command can handle the illegal argument and output error message
+            return "/system/bin/ping6";
+        }
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.connectivity_tools_fragment, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mMainExecutor = ContextCompat.getMainExecutor(getActivity());
+
+        subscribeToThreadNetwork();
+
+        AutoCompleteTextView pingServerIpText = view.findViewById(R.id.ping_server_ip_address_text);
+        mPingServerIpAdapter =
+                new ArrayAdapter<String>(
+                        getActivity(), R.layout.list_server_ip_address_view, mServerIpCandidates);
+        pingServerIpText.setAdapter(mPingServerIpAdapter);
+        TextView pingOutputText = view.findViewById(R.id.ping_output_text);
+        Button pingButton = view.findViewById(R.id.ping_button);
+
+        pingButton.setOnClickListener(
+                v -> {
+                    if (mPingFuture != null) {
+                        mPingFuture.cancel(/* mayInterruptIfRunning= */ true);
+                        mPingFuture = null;
+                    }
+
+                    String serverIp = pingServerIpText.getText().toString().strip();
+                    updateServerIpCandidates(serverIp);
+                    pingOutputText.setText("Sending ping message to " + serverIp + "\n");
+
+                    mPingFuture = sendPing(serverIp);
+                    Futures.addCallback(
+                            mPingFuture,
+                            new FutureCallback<String>() {
+                                @Override
+                                public void onSuccess(String result) {
+                                    pingOutputText.append(result + "\n");
+                                }
+
+                                @Override
+                                public void onFailure(Throwable t) {
+                                    if (t instanceof CancellationException) {
+                                        // Ignore the cancellation error
+                                        return;
+                                    }
+                                    pingOutputText.append("Failed: " + t.getMessage() + "\n");
+                                }
+                            },
+                            mMainExecutor);
+                });
+
+        AutoCompleteTextView udpServerIpText = view.findViewById(R.id.udp_server_ip_address_text);
+        mUdpServerIpAdapter =
+                new ArrayAdapter<String>(
+                        getActivity(), R.layout.list_server_ip_address_view, mServerIpCandidates);
+        udpServerIpText.setAdapter(mUdpServerIpAdapter);
+        AutoCompleteTextView udpServerPortText = view.findViewById(R.id.udp_server_port_text);
+        mUdpServerPortAdapter =
+                new ArrayAdapter<String>(
+                        getActivity(), R.layout.list_server_port_view, mServerPortCandidates);
+        udpServerPortText.setAdapter(mUdpServerPortAdapter);
+        TextInputEditText udpMsgText = view.findViewById(R.id.udp_message_text);
+        TextView udpOutputText = view.findViewById(R.id.udp_output_text);
+
+        SwitchMaterial switchBindThreadNetwork = view.findViewById(R.id.switch_bind_thread_network);
+        switchBindThreadNetwork.setChecked(mBindThreadNetwork);
+        switchBindThreadNetwork.setOnCheckedChangeListener(
+                (buttonView, isChecked) -> {
+                    if (isChecked) {
+                        Log.i(TAG, "Binding to the Thread network");
+
+                        if (mThreadNetwork == null) {
+                            Log.e(TAG, "Thread network is not available");
+                            Toast.makeText(
+                                    getActivity().getApplicationContext(),
+                                    "Thread network is not available",
+                                    Toast.LENGTH_LONG);
+                            switchBindThreadNetwork.setChecked(false);
+                        } else {
+                            mBindThreadNetwork = true;
+                        }
+                    } else {
+                        mBindThreadNetwork = false;
+                    }
+                });
+
+        Button sendUdpButton = view.findViewById(R.id.send_udp_button);
+        sendUdpButton.setOnClickListener(
+                v -> {
+                    if (mUdpFuture != null) {
+                        mUdpFuture.cancel(/* mayInterruptIfRunning= */ true);
+                        mUdpFuture = null;
+                    }
+
+                    String serverIp = udpServerIpText.getText().toString().strip();
+                    String serverPort = udpServerPortText.getText().toString().strip();
+                    String udpMsg = udpMsgText.getText().toString().strip();
+                    updateServerIpCandidates(serverIp);
+                    updateServerPortCandidates(serverPort);
+                    udpOutputText.setText(
+                            String.format(
+                                    "Sending UDP message \"%s\" to [%s]:%s",
+                                    udpMsg, serverIp, serverPort));
+
+                    mUdpFuture = sendUdpMessage(serverIp, serverPort, udpMsg);
+                    Futures.addCallback(
+                            mUdpFuture,
+                            new FutureCallback<String>() {
+                                @Override
+                                public void onSuccess(String result) {
+                                    udpOutputText.append("\n" + result);
+                                }
+
+                                @Override
+                                public void onFailure(Throwable t) {
+                                    if (t instanceof CancellationException) {
+                                        // Ignore the cancellation error
+                                        return;
+                                    }
+                                    udpOutputText.append("\nFailed: " + t.getMessage());
+                                }
+                            },
+                            mMainExecutor);
+                });
+    }
+
+    private void updateServerIpCandidates(String newServerIp) {
+        if (!mServerIpCandidates.contains(newServerIp)) {
+            mServerIpCandidates.add(0, newServerIp);
+            mPingServerIpAdapter.notifyDataSetChanged();
+            mUdpServerIpAdapter.notifyDataSetChanged();
+        }
+    }
+
+    private void updateServerPortCandidates(String newServerPort) {
+        if (!mServerPortCandidates.contains(newServerPort)) {
+            mServerPortCandidates.add(0, newServerPort);
+            mUdpServerPortAdapter.notifyDataSetChanged();
+        }
+    }
+
+    private ListenableFuture<String> sendPing(String serverIp) {
+        return FluentFuture.from(Futures.submit(() -> doSendPing(serverIp), mBackgroundExecutor))
+                .withTimeout(PING_TIMEOUT.getSeconds(), TimeUnit.SECONDS, mBackgroundExecutor);
+    }
+
+    private String doSendPing(String serverIp) throws IOException {
+        String pingCommand = getPingCommand(serverIp);
+        Process process =
+                new ProcessBuilder()
+                        .command(pingCommand, "-c 1", serverIp)
+                        .redirectErrorStream(true)
+                        .start();
+
+        return CharStreams.toString(new InputStreamReader(process.getInputStream()));
+    }
+
+    private ListenableFuture<String> sendUdpMessage(
+            String serverIp, String serverPort, String msg) {
+        return FluentFuture.from(
+                        Futures.submit(
+                                () -> doSendUdpMessage(serverIp, serverPort, msg),
+                                mBackgroundExecutor))
+                .withTimeout(UDP_TIMEOUT.getSeconds(), TimeUnit.SECONDS, mBackgroundExecutor);
+    }
+
+    private String doSendUdpMessage(String serverIp, String serverPort, String msg)
+            throws IOException {
+        SocketAddress serverAddr = new InetSocketAddress(serverIp, Integer.parseInt(serverPort));
+
+        try (DatagramSocket socket = new DatagramSocket()) {
+            if (mBindThreadNetwork && mThreadNetwork != null) {
+                mThreadNetwork.bindSocket(socket);
+                Log.i(TAG, "Successfully bind the socket to the Thread network");
+            }
+
+            socket.connect(serverAddr);
+            Log.d(TAG, "connected " + serverAddr);
+
+            byte[] msgBytes = msg.getBytes();
+            DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length);
+
+            Log.d(TAG, String.format("Sending message to server %s: %s", serverAddr, msg));
+            socket.send(packet);
+            Log.d(TAG, "Send done");
+
+            Log.d(TAG, "Waiting for server reply");
+            socket.receive(packet);
+            return new String(packet.getData(), packet.getOffset(), packet.getLength(), UTF_8);
+        }
+    }
+}
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/MainActivity.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/MainActivity.java
new file mode 100644
index 0000000..ef97a6c
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/MainActivity.java
@@ -0,0 +1,61 @@
+/*
+ * 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.threadnetwork.demoapp;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
+import androidx.navigation.ui.AppBarConfiguration;
+import androidx.navigation.ui.NavigationUI;
+
+import com.google.android.material.navigation.NavigationView;
+
+public final class MainActivity extends AppCompatActivity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main_activity);
+
+        NavHostFragment navHostFragment =
+                (NavHostFragment)
+                        getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
+
+        NavController navController = navHostFragment.getNavController();
+
+        DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
+        Toolbar topAppBar = findViewById(R.id.top_app_bar);
+        AppBarConfiguration appBarConfig =
+                new AppBarConfiguration.Builder(navController.getGraph())
+                        .setOpenableLayout(drawerLayout)
+                        .build();
+
+        NavigationUI.setupWithNavController(topAppBar, navController, appBarConfig);
+
+        NavigationView navView = findViewById(R.id.nav_view);
+        NavigationUI.setupWithNavController(navView, navController);
+    }
+
+    @Override
+    protected void onActivityResult(int request, int result, Intent data) {
+        super.onActivityResult(request, result, data);
+    }
+}
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java
new file mode 100644
index 0000000..e95feaf
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java
@@ -0,0 +1,277 @@
+/*
+ * 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.threadnetwork.demoapp;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.RouteInfo;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkException;
+import android.net.thread.ThreadNetworkManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.OutcomeReceiver;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.Executor;
+
+public final class ThreadNetworkSettingsFragment extends Fragment {
+    private static final String TAG = "ThreadNetworkSettings";
+
+    // This is a mirror of NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK which is @hide for now
+    private static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
+
+    private ThreadNetworkController mThreadController;
+    private TextView mTextState;
+    private TextView mTextNetworkInfo;
+    private TextView mMigrateNetworkState;
+    private Executor mMainExecutor;
+
+    private int mDeviceRole;
+    private long mPartitionId;
+    private ActiveOperationalDataset mActiveDataset;
+
+    private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS =
+            base16().lowerCase()
+                    .decode(
+                            "0e080000000000010000000300001235060004001fffe00208dae21bccb8c321c40708fdc376ead74396bb0510c52f56cd2d38a9eb7a716954f8efd939030f4f70656e5468726561642d646231390102db190410fcb737e6fd6bb1b0fed524a4496363110c0402a0f7f8");
+    private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
+
+    private static String deviceRoleToString(int mDeviceRole) {
+        switch (mDeviceRole) {
+            case ThreadNetworkController.DEVICE_ROLE_STOPPED:
+                return "Stopped";
+            case ThreadNetworkController.DEVICE_ROLE_DETACHED:
+                return "Detached";
+            case ThreadNetworkController.DEVICE_ROLE_CHILD:
+                return "Child";
+            case ThreadNetworkController.DEVICE_ROLE_ROUTER:
+                return "Router";
+            case ThreadNetworkController.DEVICE_ROLE_LEADER:
+                return "Leader";
+            default:
+                return "Unknown";
+        }
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.thread_network_settings_fragment, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        ConnectivityManager cm = getActivity().getSystemService(ConnectivityManager.class);
+        cm.registerNetworkCallback(
+                new NetworkRequest.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                        .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                        .build(),
+                new ConnectivityManager.NetworkCallback() {
+                    @Override
+                    public void onAvailable(Network network) {
+                        Log.i(TAG, "New Thread network is available");
+                    }
+
+                    @Override
+                    public void onLinkPropertiesChanged(
+                            Network network, LinkProperties linkProperties) {
+                        updateNetworkInfo(linkProperties);
+                    }
+
+                    @Override
+                    public void onLost(Network network) {
+                        Log.i(TAG, "Thread network " + network + " is lost");
+                        updateNetworkInfo(null /* linkProperties */);
+                    }
+                },
+                new Handler(Looper.myLooper()));
+
+        mMainExecutor = ContextCompat.getMainExecutor(getActivity());
+        ThreadNetworkManager threadManager =
+                getActivity().getSystemService(ThreadNetworkManager.class);
+        if (threadManager != null) {
+            mThreadController = threadManager.getAllThreadNetworkControllers().get(0);
+            mThreadController.registerStateCallback(
+                    mMainExecutor,
+                    new ThreadNetworkController.StateCallback() {
+                        @Override
+                        public void onDeviceRoleChanged(int mDeviceRole) {
+                            ThreadNetworkSettingsFragment.this.mDeviceRole = mDeviceRole;
+                            updateState();
+                        }
+
+                        @Override
+                        public void onPartitionIdChanged(long mPartitionId) {
+                            ThreadNetworkSettingsFragment.this.mPartitionId = mPartitionId;
+                            updateState();
+                        }
+                    });
+            mThreadController.registerOperationalDatasetCallback(
+                    mMainExecutor,
+                    newActiveDataset -> {
+                        this.mActiveDataset = newActiveDataset;
+                        updateState();
+                    });
+        }
+
+        mTextState = (TextView) view.findViewById(R.id.text_state);
+        mTextNetworkInfo = (TextView) view.findViewById(R.id.text_network_info);
+
+        if (mThreadController == null) {
+            mTextState.setText("Thread not supported!");
+            return;
+        }
+
+        ((Button) view.findViewById(R.id.button_join_network)).setOnClickListener(v -> doJoin());
+        ((Button) view.findViewById(R.id.button_leave_network)).setOnClickListener(v -> doLeave());
+
+        mMigrateNetworkState = view.findViewById(R.id.text_migrate_network_state);
+        ((Button) view.findViewById(R.id.button_migrate_network))
+                .setOnClickListener(v -> doMigration());
+
+        updateState();
+    }
+
+    private void doJoin() {
+        mThreadController.join(
+                DEFAULT_ACTIVE_DATASET,
+                mMainExecutor,
+                new OutcomeReceiver<Void, ThreadNetworkException>() {
+                    @Override
+                    public void onError(ThreadNetworkException error) {
+                        Log.e(TAG, "Failed to join network " + DEFAULT_ACTIVE_DATASET, error);
+                    }
+
+                    @Override
+                    public void onResult(Void v) {
+                        Log.i(TAG, "Successfully Joined");
+                    }
+                });
+    }
+
+    private void doLeave() {
+        mThreadController.leave(
+                mMainExecutor,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onError(ThreadNetworkException error) {
+                        Log.e(TAG, "Failed to leave network " + DEFAULT_ACTIVE_DATASET, error);
+                    }
+
+                    @Override
+                    public void onResult(Void v) {
+                        Log.i(TAG, "Successfully Left");
+                    }
+                });
+    }
+
+    private void doMigration() {
+        var newActiveDataset =
+                new ActiveOperationalDataset.Builder(DEFAULT_ACTIVE_DATASET)
+                        .setNetworkName("NewThreadNet")
+                        .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now()))
+                        .build();
+        var pendingDataset =
+                new PendingOperationalDataset(
+                        newActiveDataset,
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        mThreadController.scheduleMigration(
+                pendingDataset,
+                mMainExecutor,
+                new OutcomeReceiver<Void, ThreadNetworkException>() {
+                    @Override
+                    public void onResult(Void v) {
+                        mMigrateNetworkState.setText(
+                                "Scheduled migration to network \"NewThreadNet\" in 30s");
+                        // TODO: update Pending Dataset state
+                    }
+
+                    @Override
+                    public void onError(ThreadNetworkException e) {
+                        mMigrateNetworkState.setText(
+                                "Failed to schedule migration: " + e.getMessage());
+                    }
+                });
+    }
+
+    private void updateState() {
+        Log.i(
+                TAG,
+                String.format(
+                        "Updating Thread states (mDeviceRole: %s)",
+                        deviceRoleToString(mDeviceRole)));
+
+        String state =
+                String.format(
+                        "Role             %s\n"
+                                + "Partition ID     %d\n"
+                                + "Network Name     %s\n"
+                                + "Extended PAN ID  %s",
+                        deviceRoleToString(mDeviceRole),
+                        mPartitionId,
+                        mActiveDataset != null ? mActiveDataset.getNetworkName() : null,
+                        mActiveDataset != null
+                                ? base16().encode(mActiveDataset.getExtendedPanId())
+                                : null);
+        mTextState.setText(state);
+    }
+
+    private void updateNetworkInfo(LinkProperties linProperties) {
+        if (linProperties == null) {
+            mTextNetworkInfo.setText("");
+            return;
+        }
+
+        StringBuilder sb = new StringBuilder("Interface name:\n");
+        sb.append(linProperties.getInterfaceName() + "\n");
+        sb.append("Addresses:\n");
+        for (LinkAddress la : linProperties.getLinkAddresses()) {
+            sb.append(la + "\n");
+        }
+        sb.append("Routes:\n");
+        for (RouteInfo route : linProperties.getRoutes()) {
+            sb.append(route + "\n");
+        }
+        mTextNetworkInfo.setText(sb.toString());
+    }
+}
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/concurrent/BackgroundExecutorProvider.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/concurrent/BackgroundExecutorProvider.java
new file mode 100644
index 0000000..d05ba73
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/concurrent/BackgroundExecutorProvider.java
@@ -0,0 +1,43 @@
+/*
+ * 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.threadnetwork.demoapp.concurrent;
+
+import androidx.annotation.GuardedBy;
+
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.Executors;
+
+/** Provides executors for executing tasks in background. */
+public final class BackgroundExecutorProvider {
+    private static final int CONCURRENCY = 4;
+
+    @GuardedBy("BackgroundExecutorProvider.class")
+    private static ListeningScheduledExecutorService backgroundExecutor;
+
+    private BackgroundExecutorProvider() {}
+
+    public static synchronized ListeningScheduledExecutorService getBackgroundExecutor() {
+        if (backgroundExecutor == null) {
+            backgroundExecutor =
+                    MoreExecutors.listeningDecorator(
+                            Executors.newScheduledThreadPool(/* maxConcurrency= */ CONCURRENCY));
+        }
+        return backgroundExecutor;
+    }
+}
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/demoapp/res/drawable/ic_launcher_foreground.xml b/thread/demoapp/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..4dd8163
--- /dev/null
+++ b/thread/demoapp/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,40 @@
+<?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.
+-->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+  <group android:scaleX="0.0612"
+      android:scaleY="0.0612"
+      android:translateX="23.4"
+      android:translateY="23.683332">
+    <path
+        android:pathData="M0,0h1000v1000h-1000z"
+        android:fillColor="#00FFCEC7"/>
+    <path
+        android:pathData="m630.6,954.5l-113.5,0l0,-567.2l-170.5,0c-50.6,0 -92,41.2 -92,91.9c0,50.6 41.4,91.8 92,91.8l0,113.5c-113.3,0 -205.5,-92.1 -205.5,-205.4c0,-113.3 92.2,-205.5 205.5,-205.5l170.5,0l0,-57.5c0,-94.2 76.7,-171 171.1,-171c94.2,0 170.8,76.7 170.8,171c0,94.2 -76.6,171 -170.8,171l-57.6,0l0,567.2zM630.6,273.9l57.6,0c31.7,0 57.3,-25.8 57.3,-57.5c0,-31.7 -25.7,-57.5 -57.3,-57.5c-31.8,0 -57.6,25.8 -57.6,57.5l0,57.5z"
+        android:strokeLineJoin="miter"
+        android:strokeWidth="0"
+        android:fillColor="#000000"
+        android:fillType="nonZero"
+        android:strokeColor="#00000000"
+        android:strokeLineCap="butt"/>
+  </group>
+</vector>
diff --git a/thread/demoapp/res/drawable/ic_menu_24dp.xml b/thread/demoapp/res/drawable/ic_menu_24dp.xml
new file mode 100644
index 0000000..8a4cf80
--- /dev/null
+++ b/thread/demoapp/res/drawable/ic_menu_24dp.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24.0dp"
+        android:height="24.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?android:attr/colorControlNormal">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M3.0,18.0l18.0,0.0l0.0,-2.0L3.0,16.0l0.0,2.0zm0.0,-5.0l18.0,0.0l0.0,-2.0L3.0,11.0l0.0,2.0zm0.0,-7.0l0.0,2.0l18.0,0.0L21.0,6.0L3.0,6.0z"/>
+</vector>
diff --git a/thread/demoapp/res/drawable/ic_thread_wordmark.xml b/thread/demoapp/res/drawable/ic_thread_wordmark.xml
new file mode 100644
index 0000000..babaf54
--- /dev/null
+++ b/thread/demoapp/res/drawable/ic_thread_wordmark.xml
@@ -0,0 +1,40 @@
+<?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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="167dp"
+    android:height="31dp"
+    android:viewportWidth="167"
+    android:viewportHeight="31">
+  <path
+      android:pathData="m32.413,7.977 l3.806,0 0,9.561 11.48,0 0,-9.561 3.837,0 0,22.957 -3.837,0 0,-9.558 -11.48,0 0,9.558 -3.806,0 0,-22.957z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="m76.761,30.934 l-4.432,-7.641 -6.483,0 0,7.641 -3.807,0 0,-22.957 11.48,0c2.095,0 3.894,0.75 5.392,2.246 1.501,1.504 2.249,3.298 2.249,5.392 0,1.591 -0.453,3.034 -1.356,4.335 -0.885,1.279 -2.006,2.193 -3.376,2.747l4.732,8.236 -4.4,0zM73.519,11.812l-7.673,0 0,7.645 7.673,0c1.034,0 1.928,-0.379 2.678,-1.124 0.75,-0.752 1.126,-1.657 1.126,-2.717 0,-1.034 -0.376,-1.926 -1.126,-2.678C75.448,12.188 74.554,11.812 73.519,11.812Z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="m106.945,7.977 l0,3.835 -11.478,0 0,5.757 11.478,0 0,3.807 -11.478,0 0,5.722 11.478,0 0,3.836 -15.277,0 0,-22.957 15.277,0z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="m132.325,27.08 l-10.586,0 -1.958,3.854 -4.283,0 11.517,-23.519 11.522,23.519 -4.283,0 -1.928,-3.854zM123.627,23.267 L130.404,23.267 127.014,16.013 123.627,23.267z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="m146.606,7.977 l7.638,0c1.569,0 3.044,0.304 4.436,0.907 1.387,0.609 2.608,1.437 3.656,2.485 1.047,1.047 1.869,2.266 2.479,3.653 0.609,1.391 0.909,2.866 0.909,4.435 0,1.563 -0.299,3.041 -0.909,4.432 -0.61,1.391 -1.425,2.608 -2.464,3.654 -1.037,1.05 -2.256,1.874 -3.656,2.48 -1.401,0.607 -2.882,0.91 -4.451,0.91l-7.638,0 0,-22.956zM154.244,27.098c1.06,0 2.054,-0.199 2.978,-0.599 0.925,-0.394 1.737,-0.945 2.432,-1.654 0.696,-0.702 1.241,-1.521 1.638,-2.446 0.397,-0.925 0.597,-1.907 0.597,-2.942 0,-1.037 -0.201,-2.02 -0.597,-2.948 -0.397,-0.925 -0.946,-1.737 -1.651,-2.447 -0.709,-0.703 -1.524,-1.256 -2.45,-1.653 -0.925,-0.397 -1.907,-0.597 -2.948,-0.597l-3.834,0 0,15.286 3.834,0z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="m16.491,30.934 l-3.828,0 0,-19.128 -5.749,0c-1.705,0 -3.102,1.391 -3.102,3.1 0,1.706 1.397,3.097 3.102,3.097l0,3.83c-3.821,0 -6.931,-3.106 -6.931,-6.926 0,-3.822 3.111,-6.929 6.931,-6.929l5.749,0 0,-1.938c0,-3.179 2.587,-5.766 5.77,-5.766 3.175,0 5.76,2.588 5.76,5.766 0,3.179 -2.584,5.766 -5.76,5.766l-1.942,0 0,19.128zM16.491,7.977 L18.433,7.977c1.069,0 1.934,-0.869 1.934,-1.938 0,-1.069 -0.865,-1.938 -1.934,-1.938 -1.072,0 -1.942,0.869 -1.942,1.938l0,1.938z"
+      android:fillColor="#ffffff"/>
+</vector>
diff --git a/thread/demoapp/res/layout/connectivity_tools_fragment.xml b/thread/demoapp/res/layout/connectivity_tools_fragment.xml
new file mode 100644
index 0000000..a1aa0d4
--- /dev/null
+++ b/thread/demoapp/res/layout/connectivity_tools_fragment.xml
@@ -0,0 +1,137 @@
+<?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.
+-->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".ConnectivityToolsFragment" >
+
+<LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="8dp"
+    android:paddingBottom="16dp"
+    android:orientation="vertical">
+
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/ping_server_ip_address_layout"
+        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:hint="Server IP Address">
+        <AutoCompleteTextView
+            android:id="@+id/ping_server_ip_address_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="fdde:ad00:beef::ff:fe00:7400"
+            android:textSize="14sp"/>
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <Button
+        android:id="@+id/ping_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:text="Ping"
+        android:textSize="20dp"/>
+
+    <TextView
+        android:id="@+id/ping_output_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:scrollbars="vertical"
+        android:textIsSelectable="true"/>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:orientation="horizontal" >
+
+        <com.google.android.material.textfield.TextInputLayout
+            android:id="@+id/udp_server_ip_address_layout"
+            style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:hint="Server IP Address">
+            <AutoCompleteTextView
+                android:id="@+id/udp_server_ip_address_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="fdde:ad00:beef::ff:fe00:7400"
+                android:textSize="14sp"/>
+        </com.google.android.material.textfield.TextInputLayout>
+
+        <com.google.android.material.textfield.TextInputLayout
+            android:id="@+id/udp_server_port_layout"
+            style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="2dp"
+            android:hint="Server Port">
+            <AutoCompleteTextView
+                android:id="@+id/udp_server_port_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:inputType="number"
+                android:text="12345"
+                android:textSize="14sp"/>
+        </com.google.android.material.textfield.TextInputLayout>
+    </LinearLayout>
+
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/udp_message_layout"
+        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:hint="UDP Message">
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/udp_message_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Hello Thread!"
+            android:textSize="14sp"/>
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <com.google.android.material.switchmaterial.SwitchMaterial
+        android:id="@+id/switch_bind_thread_network"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:checked="true"
+        android:text="Bind to Thread network" />
+
+    <Button
+        android:id="@+id/send_udp_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:text="Send UDP Message"
+        android:textSize="20dp"/>
+
+    <TextView
+        android:id="@+id/udp_output_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:scrollbars="vertical"
+        android:textIsSelectable="true"/>
+</LinearLayout>
+</ScrollView>
diff --git a/thread/demoapp/res/layout/list_server_ip_address_view.xml b/thread/demoapp/res/layout/list_server_ip_address_view.xml
new file mode 100644
index 0000000..1a8f02e
--- /dev/null
+++ b/thread/demoapp/res/layout/list_server_ip_address_view.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="8dp"
+    android:ellipsize="end"
+    android:maxLines="1"
+    android:textAppearance="?attr/textAppearanceBody2"
+    />
diff --git a/thread/demoapp/res/layout/list_server_port_view.xml b/thread/demoapp/res/layout/list_server_port_view.xml
new file mode 100644
index 0000000..1a8f02e
--- /dev/null
+++ b/thread/demoapp/res/layout/list_server_port_view.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="8dp"
+    android:ellipsize="end"
+    android:maxLines="1"
+    android:textAppearance="?attr/textAppearanceBody2"
+    />
diff --git a/thread/demoapp/res/layout/main_activity.xml b/thread/demoapp/res/layout/main_activity.xml
new file mode 100644
index 0000000..12072e5
--- /dev/null
+++ b/thread/demoapp/res/layout/main_activity.xml
@@ -0,0 +1,63 @@
+<?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.
+-->
+
+<androidx.drawerlayout.widget.DrawerLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/drawer_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <com.google.android.material.appbar.AppBarLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/top_app_bar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:navigationIcon="@drawable/ic_menu_24dp" />
+
+        </com.google.android.material.appbar.AppBarLayout>
+
+        <androidx.fragment.app.FragmentContainerView
+            android:id="@+id/nav_host_fragment"
+            android:name="androidx.navigation.fragment.NavHostFragment"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:defaultNavHost="true"
+            app:navGraph="@navigation/nav_graph" />
+
+    </LinearLayout>
+
+    <com.google.android.material.navigation.NavigationView
+        android:id="@+id/nav_view"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:padding="16dp"
+        android:layout_gravity="start"
+        android:fitsSystemWindows="true"
+        app:headerLayout="@layout/nav_header"
+        app:menu="@menu/nav_menu" />
+
+</androidx.drawerlayout.widget.DrawerLayout>
diff --git a/thread/demoapp/res/layout/nav_header.xml b/thread/demoapp/res/layout/nav_header.xml
new file mode 100644
index 0000000..b91fb9c
--- /dev/null
+++ b/thread/demoapp/res/layout/nav_header.xml
@@ -0,0 +1,30 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="bottom"
+    android:orientation="vertical" >
+
+  <ImageView
+      android:id="@+id/nav_header_image"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:paddingTop="@dimen/nav_header_vertical_spacing"
+      android:src="@drawable/ic_thread_wordmark" />
+</LinearLayout>
diff --git a/thread/demoapp/res/layout/thread_network_settings_fragment.xml b/thread/demoapp/res/layout/thread_network_settings_fragment.xml
new file mode 100644
index 0000000..cae46a3
--- /dev/null
+++ b/thread/demoapp/res/layout/thread_network_settings_fragment.xml
@@ -0,0 +1,71 @@
+<?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.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="8dp"
+    android:orientation="vertical"
+    tools:context=".ThreadNetworkSettingsFragment" >
+
+    <Button android:id="@+id/button_join_network"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Join Network" />
+    <Button android:id="@+id/button_leave_network"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Leave Network" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="16dp"
+        android:textStyle="bold"
+        android:text="State" />
+    <TextView
+        android:id="@+id/text_state"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="12dp"
+        android:typeface="monospace" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        android:textSize="16dp"
+        android:textStyle="bold"
+        android:text="Network Info" />
+    <TextView
+        android:id="@+id/text_network_info"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="12dp" />
+
+    <Button android:id="@+id/button_migrate_network"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Migrate Network" />
+    <TextView
+        android:id="@+id/text_migrate_network_state"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="12dp" />
+</LinearLayout>
diff --git a/thread/demoapp/res/menu/nav_menu.xml b/thread/demoapp/res/menu/nav_menu.xml
new file mode 100644
index 0000000..8d036c2
--- /dev/null
+++ b/thread/demoapp/res/menu/nav_menu.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/thread_network_settings"
+        android:title="Thread Network Settings" />
+    <item
+        android:id="@+id/connectivity_tools"
+        android:title="Connectivity Tools" />
+</menu>
diff --git a/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher.xml b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..b111e91
--- /dev/null
+++ b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+    <background android:drawable="@color/white"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
diff --git a/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher_round.xml b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..b111e91
--- /dev/null
+++ b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+    <background android:drawable="@color/white"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
diff --git a/thread/demoapp/res/mipmap-hdpi/ic_launcher.png b/thread/demoapp/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..94e778f
--- /dev/null
+++ b/thread/demoapp/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-hdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..074a671
--- /dev/null
+++ b/thread/demoapp/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-mdpi/ic_launcher.png b/thread/demoapp/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..674e51f
--- /dev/null
+++ b/thread/demoapp/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-mdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4e35c29
--- /dev/null
+++ b/thread/demoapp/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xhdpi/ic_launcher.png b/thread/demoapp/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..2ee5d92
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xhdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..78a3b7d
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxhdpi/ic_launcher.png b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..ffb6261
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxhdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..80fa037
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher.png b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..5ca1bfe
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..2fd92e3
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/navigation/nav_graph.xml b/thread/demoapp/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..472d1bb
--- /dev/null
+++ b/thread/demoapp/res/navigation/nav_graph.xml
@@ -0,0 +1,35 @@
+<?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.
+-->
+
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+            xmlns:tools="http://schemas.android.com/tools"
+            xmlns:app="http://schemas.android.com/apk/res-auto"
+            android:id="@+id/nav_graph"
+            app:startDestination="@+id/thread_network_settings" >
+    <fragment
+        android:id="@+id/thread_network_settings"
+        android:name=".ThreadNetworkSettingsFragment"
+        android:label="Thread Network Settings"
+        tools:layout="@layout/thread_network_settings_fragment">
+    </fragment>
+
+    <fragment
+        android:id="@+id/connectivity_tools"
+        android:name=".ConnectivityToolsFragment"
+        android:label="Connectivity Tools"
+        tools:layout="@layout/connectivity_tools_fragment">
+    </fragment>
+</navigation>
diff --git a/thread/demoapp/res/values/colors.xml b/thread/demoapp/res/values/colors.xml
new file mode 100644
index 0000000..6a65937
--- /dev/null
+++ b/thread/demoapp/res/values/colors.xml
@@ -0,0 +1,25 @@
+<?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>
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>
diff --git a/thread/demoapp/res/values/dimens.xml b/thread/demoapp/res/values/dimens.xml
new file mode 100644
index 0000000..5165951
--- /dev/null
+++ b/thread/demoapp/res/values/dimens.xml
@@ -0,0 +1,24 @@
+<?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>
+  <!-- Default screen margins, per the Android Design guidelines. -->
+  <dimen name="activity_horizontal_margin">16dp</dimen>
+  <dimen name="activity_vertical_margin">16dp</dimen>
+  <dimen name="nav_header_vertical_spacing">8dp</dimen>
+  <dimen name="nav_header_height">176dp</dimen>
+  <dimen name="fab_margin">16dp</dimen>
+</resources>
diff --git a/thread/demoapp/res/values/themes.xml b/thread/demoapp/res/values/themes.xml
new file mode 100644
index 0000000..9cb3403
--- /dev/null
+++ b/thread/demoapp/res/values/themes.xml
@@ -0,0 +1,32 @@
+<?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 xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Theme.ThreadNetworkDemoApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/purple_500</item>
+        <item name="colorPrimaryVariant">@color/purple_700</item>
+        <item name="colorOnPrimary">@color/white</item>
+        <!-- Secondary brand color. -->
+        <item name="colorSecondary">@color/teal_200</item>
+        <item name="colorSecondaryVariant">@color/teal_700</item>
+        <item name="colorOnSecondary">@color/black</item>
+        <!-- Status bar color. -->
+        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+        <!-- Customize your theme here. -->
+    </style>
+</resources>
diff --git a/thread/framework/Android.bp b/thread/framework/Android.bp
new file mode 100644
index 0000000..f8fe422
--- /dev/null
+++ b/thread/framework/Android.bp
@@ -0,0 +1,43 @@
+//
+// 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_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "framework-thread-sources",
+    srcs: [
+        "java/**/*.java",
+        "java/**/*.aidl",
+    ],
+    path: "java",
+    visibility: [
+        "//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.aidl b/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl
new file mode 100644
index 0000000..8bf12a4
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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.thread;
+
+parcelable ActiveOperationalDataset;
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
new file mode 100644
index 0000000..22457f5
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -0,0 +1,1117 @@
+/*
+ * 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.thread;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkState;
+import static com.android.net.module.util.HexDump.toHexString;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.SystemApi;
+import android.net.IpPrefix;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+/**
+ * Data interface for managing a Thread Active Operational Dataset.
+ *
+ * <p>An example usage of creating an Active Operational Dataset with randomized parameters:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset = controller.createRandomizedDataset("MyNet");
+ * }</pre>
+ *
+ * <p>or randomized Dataset with customized channel:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset =
+ *         new ActiveOperationalDataset.Builder(controller.createRandomizedDataset("MyNet"))
+ *                 .setChannel(CHANNEL_PAGE_24_GHZ, 17)
+ *                 .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now()))
+ *                 .build();
+ * }</pre>
+ *
+ * <p>If the Active Operational Dataset is already known as <a
+ * href="https://www.threadgroup.org">Thread TLVs</a>, you can simply use:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
+ * }</pre>
+ *
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+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;
+
+    /** @hide */
+    public static final byte MESH_LOCAL_PREFIX_FIRST_BYTE = (byte) 0xfd;
+
+    private static final int LENGTH_CHANNEL = 3;
+    private static final int LENGTH_PAN_ID = 2;
+
+    @NonNull
+    public static final Creator<ActiveOperationalDataset> CREATOR =
+            new Creator<>() {
+                @Override
+                public ActiveOperationalDataset createFromParcel(Parcel in) {
+                    return ActiveOperationalDataset.fromThreadTlvs(in.createByteArray());
+                }
+
+                @Override
+                public ActiveOperationalDataset[] newArray(int size) {
+                    return new ActiveOperationalDataset[size];
+                }
+            };
+
+    private final OperationalDatasetTimestamp mActiveTimestamp;
+    private final String mNetworkName;
+    private final byte[] mExtendedPanId;
+    private final int mPanId;
+    private final int mChannel;
+    private final int mChannelPage;
+    private final SparseArray<byte[]> mChannelMask;
+    private final byte[] mPskc;
+    private final byte[] mNetworkKey;
+    private final IpPrefix mMeshLocalPrefix;
+    private final SecurityPolicy mSecurityPolicy;
+    private final SparseArray<byte[]> mUnknownTlvs;
+
+    private ActiveOperationalDataset(Builder builder) {
+        this(
+                requireNonNull(builder.mActiveTimestamp),
+                requireNonNull(builder.mNetworkName),
+                requireNonNull(builder.mExtendedPanId),
+                requireNonNull(builder.mPanId),
+                requireNonNull(builder.mChannelPage),
+                requireNonNull(builder.mChannel),
+                requireNonNull(builder.mChannelMask),
+                requireNonNull(builder.mPskc),
+                requireNonNull(builder.mNetworkKey),
+                requireNonNull(builder.mMeshLocalPrefix),
+                requireNonNull(builder.mSecurityPolicy),
+                requireNonNull(builder.mUnknownTlvs));
+    }
+
+    private ActiveOperationalDataset(
+            OperationalDatasetTimestamp activeTimestamp,
+            String networkName,
+            byte[] extendedPanId,
+            int panId,
+            int channelPage,
+            int channel,
+            SparseArray<byte[]> channelMask,
+            byte[] pskc,
+            byte[] networkKey,
+            IpPrefix meshLocalPrefix,
+            SecurityPolicy securityPolicy,
+            SparseArray<byte[]> unknownTlvs) {
+        this.mActiveTimestamp = activeTimestamp;
+        this.mNetworkName = networkName;
+        this.mExtendedPanId = extendedPanId.clone();
+        this.mPanId = panId;
+        this.mChannel = channel;
+        this.mChannelPage = channelPage;
+        this.mChannelMask = deepCloneSparseArray(channelMask);
+        this.mPskc = pskc.clone();
+        this.mNetworkKey = networkKey.clone();
+        this.mMeshLocalPrefix = meshLocalPrefix;
+        this.mSecurityPolicy = securityPolicy;
+        this.mUnknownTlvs = deepCloneSparseArray(unknownTlvs);
+    }
+
+    /**
+     * Creates a new {@link ActiveOperationalDataset} object from a series of Thread TLVs.
+     *
+     * <p>{@code tlvs} can be obtained from the value of a Thread Active Operational Dataset TLV
+     * (see the <a href="https://www.threadgroup.org/support#specifications">Thread
+     * specification</a> for the definition) or the return value of {@link #toThreadTlvs}.
+     *
+     * @param tlvs a series of Thread TLVs which contain the Active Operational Dataset
+     * @return the decoded Active Operational Dataset
+     * @throws IllegalArgumentException if {@code tlvs} is malformed or the length is larger than
+     *     {@link LENGTH_MAX_DATASET_TLVS}
+     */
+    @NonNull
+    public static ActiveOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
+        requireNonNull(tlvs, "tlvs cannot be null");
+        if (tlvs.length > LENGTH_MAX_DATASET_TLVS) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "tlvs length exceeds max length %d (actual is %d)",
+                            LENGTH_MAX_DATASET_TLVS, tlvs.length));
+        }
+
+        Builder builder = new Builder();
+        int i = 0;
+        while (i < tlvs.length) {
+            int type = tlvs[i++] & 0xff;
+            if (i >= tlvs.length) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Found TLV type %d at end of operational dataset with length %d",
+                                type, tlvs.length));
+            }
+
+            int length = tlvs[i++] & 0xff;
+            if (i + length > tlvs.length) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Found TLV type %d with length %d which exceeds the remaining data"
+                                        + " in the operational dataset with length %d",
+                                type, length, tlvs.length));
+            }
+
+            initWithTlv(builder, type, Arrays.copyOfRange(tlvs, i, i + length));
+            i += length;
+        }
+        try {
+            return builder.build();
+        } catch (IllegalStateException e) {
+            throw new IllegalArgumentException(
+                    "Failed to build the ActiveOperationalDataset object", e);
+        }
+    }
+
+    private static void initWithTlv(Builder builder, int type, byte[] value) {
+        // The max length of the dataset is 254 bytes, so the max length of a single TLV value is
+        // 252 (254 - 1 - 1)
+        if (value.length > LENGTH_MAX_DATASET_TLVS - 2) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "Length of TLV %d exceeds %d (actualLength = %d)",
+                            (type & 0xff), LENGTH_MAX_DATASET_TLVS - 2, value.length));
+        }
+
+        switch (type) {
+            case TYPE_CHANNEL:
+                checkArgument(
+                        value.length == LENGTH_CHANNEL,
+                        "Invalid channel (length = %d, expectedLength = %d)",
+                        value.length,
+                        LENGTH_CHANNEL);
+                builder.setChannel((value[0] & 0xff), ((value[1] & 0xff) << 8) | (value[2] & 0xff));
+                break;
+            case TYPE_PAN_ID:
+                checkArgument(
+                        value.length == LENGTH_PAN_ID,
+                        "Invalid PAN ID (length = %d, expectedLength = %d)",
+                        value.length,
+                        LENGTH_PAN_ID);
+                builder.setPanId(((value[0] & 0xff) << 8) | (value[1] & 0xff));
+                break;
+            case TYPE_EXTENDED_PAN_ID:
+                builder.setExtendedPanId(value);
+                break;
+            case TYPE_NETWORK_NAME:
+                builder.setNetworkName(new String(value, UTF_8));
+                break;
+            case TYPE_PSKC:
+                builder.setPskc(value);
+                break;
+            case TYPE_NETWORK_KEY:
+                builder.setNetworkKey(value);
+                break;
+            case TYPE_MESH_LOCAL_PREFIX:
+                builder.setMeshLocalPrefix(value);
+                break;
+            case TYPE_SECURITY_POLICY:
+                builder.setSecurityPolicy(SecurityPolicy.fromTlvValue(value));
+                break;
+            case TYPE_ACTIVE_TIMESTAMP:
+                builder.setActiveTimestamp(OperationalDatasetTimestamp.fromTlvValue(value));
+                break;
+            case TYPE_CHANNEL_MASK:
+                builder.setChannelMask(decodeChannelMask(value));
+                break;
+            default:
+                builder.addUnknownTlv(type & 0xff, value);
+                break;
+        }
+    }
+
+    private static SparseArray<byte[]> decodeChannelMask(byte[] tlvValue) {
+        SparseArray<byte[]> channelMask = new SparseArray<>();
+        int i = 0;
+        while (i < tlvValue.length) {
+            int channelPage = tlvValue[i++] & 0xff;
+            if (i >= tlvValue.length) {
+                throw new IllegalArgumentException(
+                        "Invalid channel mask - channel mask length is missing");
+            }
+
+            int maskLength = tlvValue[i++] & 0xff;
+            if (i + maskLength > tlvValue.length) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Invalid channel mask - channel mask is incomplete "
+                                        + "(offset = %d, length = %d, totalLength = %d)",
+                                i, maskLength, tlvValue.length));
+            }
+
+            channelMask.put(channelPage, Arrays.copyOfRange(tlvValue, i, i + maskLength));
+            i += maskLength;
+        }
+        return channelMask;
+    }
+
+    private static void encodeChannelMask(
+            SparseArray<byte[]> channelMask, ByteArrayOutputStream outputStream) {
+        ByteArrayOutputStream entryStream = new ByteArrayOutputStream();
+
+        for (int i = 0; i < channelMask.size(); i++) {
+            int key = channelMask.keyAt(i);
+            byte[] value = channelMask.get(key);
+            entryStream.write(key);
+            entryStream.write(value.length);
+            entryStream.write(value, 0, value.length);
+        }
+
+        byte[] entries = entryStream.toByteArray();
+
+        outputStream.write(TYPE_CHANNEL_MASK);
+        outputStream.write(entries.length);
+        outputStream.write(entries, 0, entries.length);
+    }
+
+    private static boolean areByteSparseArraysEqual(
+            @NonNull SparseArray<byte[]> first, @NonNull SparseArray<byte[]> second) {
+        if (first == second) {
+            return true;
+        } else if (first == null || second == null) {
+            return false;
+        } else if (first.size() != second.size()) {
+            return false;
+        } else {
+            for (int i = 0; i < first.size(); i++) {
+                int firstKey = first.keyAt(i);
+                int secondKey = second.keyAt(i);
+                if (firstKey != secondKey) {
+                    return false;
+                }
+
+                byte[] firstValue = first.valueAt(i);
+                byte[] secondValue = second.valueAt(i);
+                if (!Arrays.equals(firstValue, secondValue)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    /** An easy-to-use wrapper of {@link Arrays#deepHashCode}. */
+    private static int deepHashCode(Object... values) {
+        return Arrays.deepHashCode(values);
+    }
+
+    /**
+     * Converts this {@link ActiveOperationalDataset} object to a series of Thread TLVs.
+     *
+     * <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
+     * specification</a> for the definition of the Thread TLV format.
+     *
+     * @return a series of Thread TLVs which contain this Active Operational Dataset
+     */
+    @NonNull
+    public byte[] toThreadTlvs() {
+        ByteArrayOutputStream dataset = new ByteArrayOutputStream();
+
+        dataset.write(TYPE_ACTIVE_TIMESTAMP);
+        byte[] activeTimestampBytes = mActiveTimestamp.toTlvValue();
+        dataset.write(activeTimestampBytes.length);
+        dataset.write(activeTimestampBytes, 0, activeTimestampBytes.length);
+
+        dataset.write(TYPE_NETWORK_NAME);
+        byte[] networkNameBytes = mNetworkName.getBytes(UTF_8);
+        dataset.write(networkNameBytes.length);
+        dataset.write(networkNameBytes, 0, networkNameBytes.length);
+
+        dataset.write(TYPE_EXTENDED_PAN_ID);
+        dataset.write(mExtendedPanId.length);
+        dataset.write(mExtendedPanId, 0, mExtendedPanId.length);
+
+        dataset.write(TYPE_PAN_ID);
+        dataset.write(LENGTH_PAN_ID);
+        dataset.write(mPanId >> 8);
+        dataset.write(mPanId);
+
+        dataset.write(TYPE_CHANNEL);
+        dataset.write(LENGTH_CHANNEL);
+        dataset.write(mChannelPage);
+        dataset.write(mChannel >> 8);
+        dataset.write(mChannel);
+
+        encodeChannelMask(mChannelMask, dataset);
+
+        dataset.write(TYPE_PSKC);
+        dataset.write(mPskc.length);
+        dataset.write(mPskc, 0, mPskc.length);
+
+        dataset.write(TYPE_NETWORK_KEY);
+        dataset.write(mNetworkKey.length);
+        dataset.write(mNetworkKey, 0, mNetworkKey.length);
+
+        dataset.write(TYPE_MESH_LOCAL_PREFIX);
+        dataset.write(mMeshLocalPrefix.getPrefixLength() / 8);
+        dataset.write(mMeshLocalPrefix.getRawAddress(), 0, mMeshLocalPrefix.getPrefixLength() / 8);
+
+        dataset.write(TYPE_SECURITY_POLICY);
+        byte[] securityPolicyBytes = mSecurityPolicy.toTlvValue();
+        dataset.write(securityPolicyBytes.length);
+        dataset.write(securityPolicyBytes, 0, securityPolicyBytes.length);
+
+        for (int i = 0; i < mUnknownTlvs.size(); i++) {
+            byte[] value = mUnknownTlvs.valueAt(i);
+            dataset.write(mUnknownTlvs.keyAt(i));
+            dataset.write(value.length);
+            dataset.write(value, 0, value.length);
+        }
+
+        return dataset.toByteArray();
+    }
+
+    /** Returns the Active Timestamp. */
+    @NonNull
+    public OperationalDatasetTimestamp getActiveTimestamp() {
+        return mActiveTimestamp;
+    }
+
+    /** Returns the Network Name. */
+    @NonNull
+    @Size(min = LENGTH_MIN_NETWORK_NAME_BYTES, max = LENGTH_MAX_NETWORK_NAME_BYTES)
+    public String getNetworkName() {
+        return mNetworkName;
+    }
+
+    /** Returns the Extended PAN ID. */
+    @NonNull
+    @Size(LENGTH_EXTENDED_PAN_ID)
+    public byte[] getExtendedPanId() {
+        return mExtendedPanId.clone();
+    }
+
+    /** Returns the PAN ID. */
+    @IntRange(from = 0, to = 0xfffe)
+    public int getPanId() {
+        return mPanId;
+    }
+
+    /** Returns the Channel. */
+    @IntRange(from = 0, to = 65535)
+    public int getChannel() {
+        return mChannel;
+    }
+
+    /** Returns the Channel Page. */
+    @IntRange(from = 0, to = 255)
+    public int getChannelPage() {
+        return mChannelPage;
+    }
+
+    /**
+     * Returns the Channel masks. For the returned {@link SparseArray}, the key is the Channel Page
+     * and the value is the Channel Mask.
+     */
+    @NonNull
+    @Size(min = 1)
+    public SparseArray<byte[]> getChannelMask() {
+        return deepCloneSparseArray(mChannelMask);
+    }
+
+    private static SparseArray<byte[]> deepCloneSparseArray(SparseArray<byte[]> src) {
+        SparseArray<byte[]> dst = new SparseArray<>(src.size());
+        for (int i = 0; i < src.size(); i++) {
+            dst.put(src.keyAt(i), src.valueAt(i).clone());
+        }
+        return dst;
+    }
+
+    /** Returns the PSKc. */
+    @NonNull
+    @Size(LENGTH_PSKC)
+    public byte[] getPskc() {
+        return mPskc.clone();
+    }
+
+    /** Returns the Network Key. */
+    @NonNull
+    @Size(LENGTH_NETWORK_KEY)
+    public byte[] getNetworkKey() {
+        return mNetworkKey.clone();
+    }
+
+    /**
+     * Returns the Mesh-local Prefix. The length of the returned prefix is always {@link
+     * #LENGTH_MESH_LOCAL_PREFIX_BITS}.
+     */
+    @NonNull
+    public IpPrefix getMeshLocalPrefix() {
+        return mMeshLocalPrefix;
+    }
+
+    /** Returns the Security Policy. */
+    @NonNull
+    public SecurityPolicy getSecurityPolicy() {
+        return mSecurityPolicy;
+    }
+
+    /**
+     * Returns Thread TLVs which are not recognized by this device. The returned {@link SparseArray}
+     * associates TLV values to their keys.
+     *
+     * @hide
+     */
+    @NonNull
+    public SparseArray<byte[]> getUnknownTlvs() {
+        return deepCloneSparseArray(mUnknownTlvs);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeByteArray(toThreadTlvs());
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) {
+            return true;
+        } else if (!(other instanceof ActiveOperationalDataset)) {
+            return false;
+        } else {
+            ActiveOperationalDataset otherDataset = (ActiveOperationalDataset) other;
+            return mActiveTimestamp.equals(otherDataset.mActiveTimestamp)
+                    && mNetworkName.equals(otherDataset.mNetworkName)
+                    && Arrays.equals(mExtendedPanId, otherDataset.mExtendedPanId)
+                    && mPanId == otherDataset.mPanId
+                    && mChannelPage == otherDataset.mChannelPage
+                    && mChannel == otherDataset.mChannel
+                    && areByteSparseArraysEqual(mChannelMask, otherDataset.mChannelMask)
+                    && Arrays.equals(mPskc, otherDataset.mPskc)
+                    && Arrays.equals(mNetworkKey, otherDataset.mNetworkKey)
+                    && mMeshLocalPrefix.equals(otherDataset.mMeshLocalPrefix)
+                    && mSecurityPolicy.equals(otherDataset.mSecurityPolicy)
+                    && areByteSparseArraysEqual(mUnknownTlvs, otherDataset.mUnknownTlvs);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return deepHashCode(
+                mActiveTimestamp,
+                mNetworkName,
+                mExtendedPanId,
+                mPanId,
+                mChannel,
+                mChannelPage,
+                mChannelMask,
+                mPskc,
+                mNetworkKey,
+                mMeshLocalPrefix,
+                mSecurityPolicy);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("{networkName=")
+                .append(getNetworkName())
+                .append(", extendedPanId=")
+                .append(toHexString(getExtendedPanId()))
+                .append(", panId=")
+                .append(getPanId())
+                .append(", channel=")
+                .append(getChannel())
+                .append(", activeTimestamp=")
+                .append(getActiveTimestamp())
+                .append("}");
+        return sb.toString();
+    }
+
+    static String checkNetworkName(@NonNull String networkName) {
+        requireNonNull(networkName, "networkName cannot be null");
+
+        int nameLength = networkName.getBytes(UTF_8).length;
+        checkArgument(
+                nameLength >= LENGTH_MIN_NETWORK_NAME_BYTES
+                        && nameLength <= LENGTH_MAX_NETWORK_NAME_BYTES,
+                "Invalid network name (length = %d, expectedLengthRange = [%d, %d])",
+                nameLength,
+                LENGTH_MIN_NETWORK_NAME_BYTES,
+                LENGTH_MAX_NETWORK_NAME_BYTES);
+        return networkName;
+    }
+
+    /** The builder for creating {@link ActiveOperationalDataset} objects. */
+    public static final class Builder {
+        private OperationalDatasetTimestamp mActiveTimestamp;
+        private String mNetworkName;
+        private byte[] mExtendedPanId;
+        private Integer mPanId;
+        private Integer mChannel;
+        private Integer mChannelPage;
+        private SparseArray<byte[]> mChannelMask;
+        private byte[] mPskc;
+        private byte[] mNetworkKey;
+        private IpPrefix mMeshLocalPrefix;
+        private SecurityPolicy mSecurityPolicy;
+        private SparseArray<byte[]> mUnknownTlvs;
+
+        /**
+         * Creates a {@link Builder} object with values from an {@link ActiveOperationalDataset}
+         * object.
+         */
+        public Builder(@NonNull ActiveOperationalDataset activeOpDataset) {
+            requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
+
+            this.mActiveTimestamp = activeOpDataset.mActiveTimestamp;
+            this.mNetworkName = activeOpDataset.mNetworkName;
+            this.mExtendedPanId = activeOpDataset.mExtendedPanId.clone();
+            this.mPanId = activeOpDataset.mPanId;
+            this.mChannel = activeOpDataset.mChannel;
+            this.mChannelPage = activeOpDataset.mChannelPage;
+            this.mChannelMask = deepCloneSparseArray(activeOpDataset.mChannelMask);
+            this.mPskc = activeOpDataset.mPskc.clone();
+            this.mNetworkKey = activeOpDataset.mNetworkKey.clone();
+            this.mMeshLocalPrefix = activeOpDataset.mMeshLocalPrefix;
+            this.mSecurityPolicy = activeOpDataset.mSecurityPolicy;
+            this.mUnknownTlvs = deepCloneSparseArray(activeOpDataset.mUnknownTlvs);
+        }
+
+        /**
+         * Creates an empty {@link Builder} object.
+         *
+         * <p>An empty builder cannot build a new {@link ActiveOperationalDataset} object. The
+         * Active Operational Dataset parameters must be set with setters of this builder.
+         */
+        public Builder() {
+            mChannelMask = new SparseArray<>();
+            mUnknownTlvs = new SparseArray<>();
+        }
+
+        /**
+         * Sets the Active Timestamp.
+         *
+         * @param activeTimestamp Active Timestamp of the Operational Dataset
+         */
+        @NonNull
+        public Builder setActiveTimestamp(@NonNull OperationalDatasetTimestamp activeTimestamp) {
+            requireNonNull(activeTimestamp, "activeTimestamp cannot be null");
+            this.mActiveTimestamp = activeTimestamp;
+            return this;
+        }
+
+        /**
+         * Sets the Network Name.
+         *
+         * @param networkName the name of the Thread network
+         * @throws IllegalArgumentException if length of the UTF-8 representation of {@code
+         *     networkName} isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
+         *     #LENGTH_MAX_NETWORK_NAME_BYTES}]
+         */
+        @NonNull
+        public Builder setNetworkName(
+                @NonNull
+                        @Size(
+                                min = LENGTH_MIN_NETWORK_NAME_BYTES,
+                                max = LENGTH_MAX_NETWORK_NAME_BYTES)
+                        String networkName) {
+            this.mNetworkName = checkNetworkName(networkName);
+            return this;
+        }
+
+        /**
+         * Sets the Extended PAN ID.
+         *
+         * <p>Use with caution. A randomized Extended PAN ID should be used for real Thread
+         * networks. It's discouraged to call this method to override the default value created by
+         * {@link ThreadNetworkController#createRandomizedDataset} in production.
+         *
+         * @throws IllegalArgumentException if length of {@code extendedPanId} is not {@link
+         *     #LENGTH_EXTENDED_PAN_ID}.
+         */
+        @NonNull
+        public Builder setExtendedPanId(
+                @NonNull @Size(LENGTH_EXTENDED_PAN_ID) byte[] extendedPanId) {
+            requireNonNull(extendedPanId, "extendedPanId cannot be null");
+            checkArgument(
+                    extendedPanId.length == LENGTH_EXTENDED_PAN_ID,
+                    "Invalid extended PAN ID (length = %d, expectedLength = %d)",
+                    extendedPanId.length,
+                    LENGTH_EXTENDED_PAN_ID);
+            this.mExtendedPanId = extendedPanId.clone();
+            return this;
+        }
+
+        /**
+         * Sets the PAN ID.
+         *
+         * @throws IllegalArgumentException if {@code panId} is not in range of 0x0-0xfffe
+         */
+        @NonNull
+        public Builder setPanId(@IntRange(from = 0, to = 0xfffe) int panId) {
+            checkArgument(
+                    panId >= 0 && panId <= 0xfffe,
+                    "PAN ID exceeds allowed range (panid = %d, allowedRange = [0x0, 0xffff])",
+                    panId);
+            this.mPanId = panId;
+            return this;
+        }
+
+        /**
+         * Sets the Channel Page and Channel.
+         *
+         * <p>Channel Pages other than {@link #CHANNEL_PAGE_24_GHZ} are undefined and may lead to
+         * unexpected behavior if it's applied to Thread devices.
+         *
+         * @throws IllegalArgumentException if invalid channel is specified for the {@code
+         *     channelPage}
+         */
+        @NonNull
+        public Builder setChannel(
+                @IntRange(from = 0, to = 255) int page,
+                @IntRange(from = 0, to = 65535) int channel) {
+            checkArgument(
+                    page >= 0 && page <= 255,
+                    "Invalid channel page (page = %d, allowedRange = [0, 255])",
+                    page);
+            if (page == CHANNEL_PAGE_24_GHZ) {
+                checkArgument(
+                        channel >= CHANNEL_MIN_24_GHZ && channel <= CHANNEL_MAX_24_GHZ,
+                        "Invalid channel %d in page %d (allowedChannelRange = [%d, %d])",
+                        channel,
+                        page,
+                        CHANNEL_MIN_24_GHZ,
+                        CHANNEL_MAX_24_GHZ);
+            } else {
+                checkArgument(
+                        channel >= 0 && channel <= 65535,
+                        "Invalid channel %d in page %d "
+                                + "(channel = %d, allowedChannelRange = [0, 65535])",
+                        channel,
+                        page,
+                        channel);
+            }
+
+            this.mChannelPage = page;
+            this.mChannel = channel;
+            return this;
+        }
+
+        /**
+         * Sets the Channel Mask.
+         *
+         * @throws IllegalArgumentException if {@code channelMask} is empty
+         */
+        @NonNull
+        public Builder setChannelMask(@NonNull @Size(min = 1) SparseArray<byte[]> channelMask) {
+            requireNonNull(channelMask, "channelMask cannot be null");
+            checkArgument(channelMask.size() > 0, "channelMask is empty");
+            this.mChannelMask = deepCloneSparseArray(channelMask);
+            return this;
+        }
+
+        /**
+         * Sets the PSKc.
+         *
+         * <p>Use with caution. A randomly generated PSKc should be used for real Thread networks.
+         * It's discouraged to call this method to override the default value created by {@link
+         * ThreadNetworkController#createRandomizedDataset} in production.
+         *
+         * @param pskc the key stretched version of the Commissioning Credential for the network
+         * @throws IllegalArgumentException if length of {@code pskc} is not {@link #LENGTH_PSKC}
+         */
+        @NonNull
+        public Builder setPskc(@NonNull @Size(LENGTH_PSKC) byte[] pskc) {
+            requireNonNull(pskc, "pskc cannot be null");
+            checkArgument(
+                    pskc.length == LENGTH_PSKC,
+                    "Invalid PSKc length (length = %d, expectedLength = %d)",
+                    pskc.length,
+                    LENGTH_PSKC);
+            this.mPskc = pskc.clone();
+            return this;
+        }
+
+        /**
+         * Sets the Network Key.
+         *
+         * <p>Use with caution, randomly generated Network Key should be used for real Thread
+         * networks. It's discouraged to call this method to override the default value created by
+         * {@link ThreadNetworkController#createRandomizedDataset} in production.
+         *
+         * @param networkKey a 128-bit security key-derivation key for the Thread Network
+         * @throws IllegalArgumentException if length of {@code networkKey} is not {@link
+         *     #LENGTH_NETWORK_KEY}
+         */
+        @NonNull
+        public Builder setNetworkKey(@NonNull @Size(LENGTH_NETWORK_KEY) byte[] networkKey) {
+            requireNonNull(networkKey, "networkKey cannot be null");
+            checkArgument(
+                    networkKey.length == LENGTH_NETWORK_KEY,
+                    "Invalid network key length (length = %d, expectedLength = %d)",
+                    networkKey.length,
+                    LENGTH_NETWORK_KEY);
+            this.mNetworkKey = networkKey.clone();
+            return this;
+        }
+
+        /**
+         * Sets the Mesh-Local Prefix.
+         *
+         * @param meshLocalPrefix the prefix used for realm-local traffic within the mesh
+         * @throws IllegalArgumentException if prefix length of {@code meshLocalPrefix} isn't {@link
+         *     #LENGTH_MESH_LOCAL_PREFIX_BITS} or {@code meshLocalPrefix} doesn't start with {@code
+         *     0xfd}
+         */
+        @NonNull
+        public Builder setMeshLocalPrefix(@NonNull IpPrefix meshLocalPrefix) {
+            requireNonNull(meshLocalPrefix, "meshLocalPrefix cannot be null");
+            checkArgument(
+                    meshLocalPrefix.getPrefixLength() == LENGTH_MESH_LOCAL_PREFIX_BITS,
+                    "Invalid mesh-local prefix length (length = %d, expectedLength = %d)",
+                    meshLocalPrefix.getPrefixLength(),
+                    LENGTH_MESH_LOCAL_PREFIX_BITS);
+            checkArgument(
+                    meshLocalPrefix.getRawAddress()[0] == MESH_LOCAL_PREFIX_FIRST_BYTE,
+                    "Mesh-local prefix must start with 0xfd: " + meshLocalPrefix);
+            this.mMeshLocalPrefix = meshLocalPrefix;
+            return this;
+        }
+
+        /**
+         * Sets the Mesh-Local Prefix.
+         *
+         * @param meshLocalPrefix the prefix used for realm-local traffic within the mesh
+         * @throws IllegalArgumentException if {@code meshLocalPrefix} doesn't start with {@code
+         *     0xfd} or has length other than {@code LENGTH_MESH_LOCAL_PREFIX_BITS / 8}
+         * @hide
+         */
+        @NonNull
+        public Builder setMeshLocalPrefix(byte[] meshLocalPrefix) {
+            final int prefixLength = meshLocalPrefix.length * 8;
+            checkArgument(
+                    prefixLength == LENGTH_MESH_LOCAL_PREFIX_BITS,
+                    "Invalid mesh-local prefix length (length = %d, expectedLength = %d)",
+                    prefixLength,
+                    LENGTH_MESH_LOCAL_PREFIX_BITS);
+            byte[] ip6RawAddress = new byte[16];
+            System.arraycopy(meshLocalPrefix, 0, ip6RawAddress, 0, meshLocalPrefix.length);
+            try {
+                return setMeshLocalPrefix(
+                        new IpPrefix(Inet6Address.getByAddress(ip6RawAddress), prefixLength));
+            } catch (UnknownHostException e) {
+                // Can't happen because numeric address is provided
+                throw new AssertionError(e);
+            }
+        }
+
+        /** Sets the Security Policy. */
+        @NonNull
+        public Builder setSecurityPolicy(@NonNull SecurityPolicy securityPolicy) {
+            requireNonNull(securityPolicy, "securityPolicy cannot be null");
+            this.mSecurityPolicy = securityPolicy;
+            return this;
+        }
+
+        /**
+         * Sets additional unknown TLVs.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setUnknownTlvs(@NonNull SparseArray<byte[]> unknownTlvs) {
+            requireNonNull(unknownTlvs, "unknownTlvs cannot be null");
+            mUnknownTlvs = deepCloneSparseArray(unknownTlvs);
+            return this;
+        }
+
+        /** Adds one more unknown TLV. @hide */
+        @VisibleForTesting
+        @NonNull
+        public Builder addUnknownTlv(int type, byte[] value) {
+            mUnknownTlvs.put(type, value);
+            return this;
+        }
+
+        /**
+         * Creates a new {@link ActiveOperationalDataset} object.
+         *
+         * @throws IllegalStateException if any of the fields isn't set or the total length exceeds
+         *     {@link #LENGTH_MAX_DATASET_TLVS} bytes
+         */
+        @NonNull
+        public ActiveOperationalDataset build() {
+            checkState(mActiveTimestamp != null, "Active Timestamp is missing");
+            checkState(mNetworkName != null, "Network Name is missing");
+            checkState(mExtendedPanId != null, "Extended PAN ID is missing");
+            checkState(mPanId != null, "PAN ID is missing");
+            checkState(mChannel != null, "Channel is missing");
+            checkState(mChannelPage != null, "Channel Page is missing");
+            checkState(mChannelMask.size() != 0, "Channel Mask is missing");
+            checkState(mPskc != null, "PSKc is missing");
+            checkState(mNetworkKey != null, "Network Key is missing");
+            checkState(mMeshLocalPrefix != null, "Mesh Local Prefix is missing");
+            checkState(mSecurityPolicy != null, "Security Policy is missing");
+
+            int length = getTotalDatasetLength();
+            if (length > LENGTH_MAX_DATASET_TLVS) {
+                throw new IllegalStateException(
+                        String.format(
+                                "Total dataset length exceeds max length %d (actual is %d)",
+                                LENGTH_MAX_DATASET_TLVS, length));
+            }
+
+            return new ActiveOperationalDataset(this);
+        }
+
+        private int getTotalDatasetLength() {
+            int length =
+                    2 * 9 // 9 fields with 1 byte of type and 1 byte of length
+                            + OperationalDatasetTimestamp.LENGTH_TIMESTAMP
+                            + mNetworkName.getBytes(UTF_8).length
+                            + LENGTH_EXTENDED_PAN_ID
+                            + LENGTH_PAN_ID
+                            + LENGTH_CHANNEL
+                            + LENGTH_PSKC
+                            + LENGTH_NETWORK_KEY
+                            + LENGTH_MESH_LOCAL_PREFIX_BITS / 8
+                            + mSecurityPolicy.toTlvValue().length;
+
+            for (int i = 0; i < mChannelMask.size(); i++) {
+                length += 2 + mChannelMask.valueAt(i).length;
+            }
+
+            // For the type and length bytes of the Channel Mask TLV because the masks are encoded
+            // as TLVs in TLV.
+            length += 2;
+
+            for (int i = 0; i < mUnknownTlvs.size(); i++) {
+                length += 2 + mUnknownTlvs.valueAt(i).length;
+            }
+
+            return length;
+        }
+    }
+
+    /**
+     * The Security Policy of Thread Operational Dataset which provides an administrator with a way
+     * to enable or disable certain security related behaviors.
+     */
+    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;
+
+        private final int mRotationTimeHours;
+        private final byte[] mFlags;
+
+        /**
+         * Creates a new {@link SecurityPolicy} object.
+         *
+         * @param rotationTimeHours the value for Thread key rotation in hours. Must be in range of
+         *     0x1-0xffff.
+         * @param flags security policy flags with length of either 1 byte for Thread 1.1 or 2 bytes
+         *     for Thread 1.2 or higher.
+         * @throws IllegalArgumentException if {@code rotationTimeHours} is not in range of
+         *     0x1-0xffff or length of {@code flags} is smaller than {@link
+         *     #LENGTH_MIN_SECURITY_POLICY_FLAGS}.
+         */
+        public SecurityPolicy(
+                @IntRange(from = 0x1, to = 0xffff) int rotationTimeHours,
+                @NonNull @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[] flags) {
+            requireNonNull(flags, "flags cannot be null");
+            checkArgument(
+                    rotationTimeHours >= 1 && rotationTimeHours <= 0xffff,
+                    "Rotation time exceeds allowed range (rotationTimeHours = %d, allowedRange ="
+                            + " [0x1, 0xffff])",
+                    rotationTimeHours);
+            checkArgument(
+                    flags.length >= LENGTH_MIN_SECURITY_POLICY_FLAGS,
+                    "Invalid security policy flags length (length = %d, minimumLength = %d)",
+                    flags.length,
+                    LENGTH_MIN_SECURITY_POLICY_FLAGS);
+            this.mRotationTimeHours = rotationTimeHours;
+            this.mFlags = flags.clone();
+        }
+
+        /**
+         * Creates a new {@link SecurityPolicy} object from the Security Policy TLV value.
+         *
+         * @hide
+         */
+        @VisibleForTesting
+        @NonNull
+        public static SecurityPolicy fromTlvValue(byte[] encodedSecurityPolicy) {
+            checkArgument(
+                    encodedSecurityPolicy.length
+                            >= LENGTH_SECURITY_POLICY_ROTATION_TIME
+                                    + LENGTH_MIN_SECURITY_POLICY_FLAGS,
+                    "Invalid Security Policy TLV length (length = %d, minimumLength = %d)",
+                    encodedSecurityPolicy.length,
+                    LENGTH_SECURITY_POLICY_ROTATION_TIME + LENGTH_MIN_SECURITY_POLICY_FLAGS);
+
+            return new SecurityPolicy(
+                    ((encodedSecurityPolicy[0] & 0xff) << 8) | (encodedSecurityPolicy[1] & 0xff),
+                    Arrays.copyOfRange(
+                            encodedSecurityPolicy,
+                            LENGTH_SECURITY_POLICY_ROTATION_TIME,
+                            encodedSecurityPolicy.length));
+        }
+
+        /**
+         * Converts this {@link SecurityPolicy} object to Security Policy TLV value.
+         *
+         * @hide
+         */
+        @VisibleForTesting
+        @NonNull
+        public byte[] toTlvValue() {
+            ByteArrayOutputStream result = new ByteArrayOutputStream();
+            result.write(mRotationTimeHours >> 8);
+            result.write(mRotationTimeHours);
+            result.write(mFlags, 0, mFlags.length);
+            return result.toByteArray();
+        }
+
+        /** Returns the Security Policy Rotation Time in hours. */
+        @IntRange(from = 0x1, to = 0xffff)
+        public int getRotationTimeHours() {
+            return mRotationTimeHours;
+        }
+
+        /** Returns 1 byte flags for Thread 1.1 or 2 bytes flags for Thread 1.2. */
+        @NonNull
+        @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS)
+        public byte[] getFlags() {
+            return mFlags.clone();
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            } else if (!(other instanceof SecurityPolicy)) {
+                return false;
+            } else {
+                SecurityPolicy otherSecurityPolicy = (SecurityPolicy) other;
+                return mRotationTimeHours == otherSecurityPolicy.mRotationTimeHours
+                        && Arrays.equals(mFlags, otherSecurityPolicy.mFlags);
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return deepHashCode(mRotationTimeHours, mFlags);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("{rotation=")
+                    .append(mRotationTimeHours)
+                    .append(", flags=")
+                    .append(toHexString(mFlags))
+                    .append("}");
+            return sb.toString();
+        }
+    }
+}
diff --git a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
new file mode 100644
index 0000000..bcda8a8
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+ /**
+  * 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/thread/framework/java/android/net/thread/IActiveOperationalDatasetReceiver.aidl b/thread/framework/java/android/net/thread/IActiveOperationalDatasetReceiver.aidl
new file mode 100644
index 0000000..aba54eb
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IActiveOperationalDatasetReceiver.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright 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.thread;
+
+import android.net.thread.ActiveOperationalDataset;
+
+/** Receives the result of an operation which returns an Active Operational Dataset. @hide */
+oneway interface IActiveOperationalDatasetReceiver {
+    void onSuccess(in ActiveOperationalDataset dataset);
+    void onError(int errorCode, String errorMessage);
+}
diff --git a/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl b/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
new file mode 100644
index 0000000..dcc4545
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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.net.thread.ThreadConfiguration;
+
+/** 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/IOperationReceiver.aidl b/thread/framework/java/android/net/thread/IOperationReceiver.aidl
new file mode 100644
index 0000000..42e157b
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IOperationReceiver.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright 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.thread;
+
+/** Receives the result of a Thread network operation. @hide */
+oneway interface IOperationReceiver {
+    void onSuccess();
+    void onError(int errorCode, String errorMessage);
+}
diff --git a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
new file mode 100644
index 0000000..b576b33
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright 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.thread;
+
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.PendingOperationalDataset;
+
+/**
+ * @hide
+ */
+oneway interface IOperationalDatasetCallback {
+    void onActiveOperationalDatasetChanged(in @nullable ActiveOperationalDataset activeOpDataset);
+    void onPendingOperationalDatasetChanged(in @nullable PendingOperationalDataset pendingOpDataset);
+}
diff --git a/thread/framework/java/android/net/thread/IScheduleMigrationReceiver.aidl b/thread/framework/java/android/net/thread/IScheduleMigrationReceiver.aidl
new file mode 100644
index 0000000..c45d463
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IScheduleMigrationReceiver.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 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.thread;
+
+/** Receives the result of {@link ThreadNetworkManager#scheduleMigration}. @hide */
+oneway interface IScheduleMigrationReceiver {
+    void onScheduled(long delayTimerMillis);
+    void onMigrated();
+    void onError(int errorCode, String errorMessage);
+}
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
new file mode 100644
index 0000000..9d0a571
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright 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.thread;
+
+/**
+ * @hide
+ */
+oneway interface IStateCallback {
+    void onDeviceRoleChanged(int deviceRole);
+    void onPartitionIdChanged(long partitionId);
+    void onThreadEnableStateChanged(int enabledState);
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
new file mode 100644
index 0000000..f50de74
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -0,0 +1,54 @@
+/**
+ * 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.thread;
+
+import android.net.thread.ActiveOperationalDataset;
+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.IScheduleMigrationReceiver;
+import android.net.thread.IStateCallback;
+import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
+
+/**
+* Interface for communicating with ThreadNetworkControllerService.
+* @hide
+*/
+interface IThreadNetworkController {
+    void registerStateCallback(in IStateCallback callback);
+    void unregisterStateCallback(in IStateCallback callback);
+    void registerOperationalDatasetCallback(in IOperationalDatasetCallback callback);
+    void unregisterOperationalDatasetCallback(in IOperationalDatasetCallback callback);
+
+    void join(in ActiveOperationalDataset activeOpDataset, 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 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
new file mode 100644
index 0000000..0e394b1
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
@@ -0,0 +1,27 @@
+/**
+ * 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.thread;
+
+import android.net.thread.IThreadNetworkController;
+
+/**
+* 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
new file mode 100644
index 0000000..cecb4e9
--- /dev/null
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -0,0 +1,234 @@
+/*
+ * 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.thread;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * The timestamp of Thread Operational Dataset.
+ *
+ * @see ActiveOperationalDataset
+ * @see PendingOperationalDataset
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class OperationalDatasetTimestamp {
+    /** @hide */
+    public static final int LENGTH_TIMESTAMP = Long.BYTES;
+
+    private static final int TICKS_UPPER_BOUND = 0x8000;
+
+    private final long mSeconds;
+    private final int mTicks;
+    private final boolean mIsAuthoritativeSource;
+
+    /**
+     * 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 true}.
+     *
+     * <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
+     */
+    @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, isAuthoritativeSource);
+    }
+
+    /**
+     * Converts this {@link OperationalDatasetTimestamp} object to an {@link Instant}.
+     *
+     * <p>Note that the return value may not equal exactly the {@code instant} if this object is
+     * created with {@link #fromInstant}.
+     *
+     * @see fromInstant
+     */
+    @NonNull
+    public Instant toInstant() {
+        long nanos = Math.round((double) mTicks * 1000000000L / TICKS_UPPER_BOUND);
+        return Instant.ofEpochSecond(mSeconds, nanos);
+    }
+
+    /**
+     * Creates a new {@link OperationalDatasetTimestamp} object from the OperationalDatasetTimestamp
+     * TLV value.
+     *
+     * @hide
+     */
+    @NonNull
+    public static OperationalDatasetTimestamp fromTlvValue(@NonNull byte[] encodedTimestamp) {
+        requireNonNull(encodedTimestamp, "encodedTimestamp cannot be null");
+        checkArgument(
+                encodedTimestamp.length == LENGTH_TIMESTAMP,
+                "Invalid Thread OperationalDatasetTimestamp length (length = %d,"
+                        + " expectedLength=%d)",
+                encodedTimestamp.length,
+                LENGTH_TIMESTAMP);
+        long longTimestamp = ByteBuffer.wrap(encodedTimestamp).getLong();
+        return new OperationalDatasetTimestamp(
+                (longTimestamp >> 16) & 0x0000ffffffffffffL,
+                (int) ((longTimestamp >> 1) & 0x7fffL),
+                (longTimestamp & 0x01) != 0);
+    }
+
+    /**
+     * Converts this {@link OperationalDatasetTimestamp} object to Thread TLV value.
+     *
+     * @hide
+     */
+    @NonNull
+    public byte[] toTlvValue() {
+        byte[] tlv = new byte[LENGTH_TIMESTAMP];
+        ByteBuffer buffer = ByteBuffer.wrap(tlv);
+        long encodedValue = (mSeconds << 16) | (mTicks << 1) | (mIsAuthoritativeSource ? 1 : 0);
+        buffer.putLong(encodedValue);
+        return tlv;
+    }
+
+    /**
+     * Creates a new {@link OperationalDatasetTimestamp} object.
+     *
+     * @param seconds the value encodes a Unix Time value. Must be in the range of
+     *     0x0-0xffffffffffffL
+     * @param ticks the value encodes the fractional Unix Time value in 32.768 kHz resolution. Must
+     *     be in the range of 0x0-0x7fff
+     * @param isAuthoritativeSource the flag indicates the time was obtained from an authoritative
+     *     source: either NTP (Network Time Protocol), GPS (Global Positioning System), cell
+     *     network, or other method
+     * @throws IllegalArgumentException if the {@code seconds} is not in range of
+     *     0x0-0xffffffffffffL or {@code ticks} is not in range of 0x0-0x7fff
+     */
+    public OperationalDatasetTimestamp(
+            @IntRange(from = 0x0, to = 0xffffffffffffL) long seconds,
+            @IntRange(from = 0x0, to = 0x7fff) int ticks,
+            boolean isAuthoritativeSource) {
+        checkArgument(
+                seconds >= 0 && seconds <= 0xffffffffffffL,
+                "seconds exceeds allowed range (seconds = %d,"
+                        + " allowedRange = [0x0, 0xffffffffffffL])",
+                seconds);
+        checkArgument(
+                ticks >= 0 && ticks <= 0x7fff,
+                "ticks exceeds allowed ranged (ticks = %d, allowedRange" + " = [0x0, 0x7fff])",
+                ticks);
+        mSeconds = seconds;
+        mTicks = ticks;
+        mIsAuthoritativeSource = isAuthoritativeSource;
+    }
+
+    /**
+     * Returns the rounded ticks converted from the nano seconds.
+     *
+     * <p>Note that rhe return value can be as large as {@code TICKS_UPPER_BOUND}.
+     */
+    private static int getRoundedTicks(long nanos) {
+        return (int) Math.round((double) nanos * TICKS_UPPER_BOUND / 1000000000L);
+    }
+
+    /** Returns the seconds portion of the timestamp. */
+    public @IntRange(from = 0x0, to = 0xffffffffffffL) long getSeconds() {
+        return mSeconds;
+    }
+
+    /** Returns the ticks portion of the timestamp. */
+    public @IntRange(from = 0x0, to = 0x7fff) int getTicks() {
+        return mTicks;
+    }
+
+    /** Returns {@code true} if the timestamp comes from an authoritative source. */
+    public boolean isAuthoritativeSource() {
+        return mIsAuthoritativeSource;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("{seconds=")
+                .append(getSeconds())
+                .append(", ticks=")
+                .append(getTicks())
+                .append(", isAuthoritativeSource=")
+                .append(isAuthoritativeSource())
+                .append(", instant=")
+                .append(toInstant())
+                .append("}");
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        } else if (!(other instanceof OperationalDatasetTimestamp)) {
+            return false;
+        } else {
+            OperationalDatasetTimestamp otherTimestamp = (OperationalDatasetTimestamp) other;
+            return mSeconds == otherTimestamp.mSeconds
+                    && mTicks == otherTimestamp.mTicks
+                    && mIsAuthoritativeSource == otherTimestamp.mIsAuthoritativeSource;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mSeconds, mTicks, mIsAuthoritativeSource);
+    }
+}
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl b/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl
new file mode 100644
index 0000000..e5bc05e
--- /dev/null
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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.thread;
+
+parcelable PendingOperationalDataset;
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
new file mode 100644
index 0000000..c1351af
--- /dev/null
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -0,0 +1,236 @@
+/*
+ * 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.thread;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * Data interface for managing a Thread Pending Operational Dataset.
+ *
+ * <p>The Pending Operational Dataset represents an Operational Dataset which will become Active in
+ * a given delay. This is typically used to deploy new network parameters (e.g. Network Key or
+ * Channel) to all devices in the network.
+ *
+ * @see ThreadNetworkController#scheduleMigration
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class PendingOperationalDataset implements Parcelable {
+    // Value defined in Thread spec 8.10.1.16
+    private static final int TYPE_PENDING_TIMESTAMP = 51;
+
+    // Values defined in Thread spec 8.10.1.17
+    private static final int TYPE_DELAY_TIMER = 52;
+    private static final int LENGTH_DELAY_TIMER_BYTES = 4;
+
+    @NonNull
+    public static final Creator<PendingOperationalDataset> CREATOR =
+            new Creator<>() {
+                @Override
+                public PendingOperationalDataset createFromParcel(Parcel in) {
+                    return PendingOperationalDataset.fromThreadTlvs(in.createByteArray());
+                }
+
+                @Override
+                public PendingOperationalDataset[] newArray(int size) {
+                    return new PendingOperationalDataset[size];
+                }
+            };
+
+    @NonNull private final ActiveOperationalDataset mActiveOpDataset;
+    @NonNull private final OperationalDatasetTimestamp mPendingTimestamp;
+    @NonNull private final Duration mDelayTimer;
+
+    /**
+     * Creates a new {@link PendingOperationalDataset} object.
+     *
+     * @param activeOpDataset the included Active Operational Dataset
+     * @param pendingTimestamp the Pending Timestamp which represents the version of this Pending
+     *     Dataset
+     * @param delayTimer the delay after when {@code activeOpDataset} will be committed on this
+     *     device; use {@link Duration#ZERO} to tell the system to choose a reasonable value
+     *     automatically
+     */
+    public PendingOperationalDataset(
+            @NonNull ActiveOperationalDataset activeOpDataset,
+            @NonNull OperationalDatasetTimestamp pendingTimestamp,
+            @NonNull Duration delayTimer) {
+        requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
+        requireNonNull(pendingTimestamp, "pendingTimestamp cannot be null");
+        requireNonNull(delayTimer, "delayTimer cannot be null");
+        this.mActiveOpDataset = activeOpDataset;
+        this.mPendingTimestamp = pendingTimestamp;
+        this.mDelayTimer = delayTimer;
+    }
+
+    /**
+     * Creates a new {@link PendingOperationalDataset} object from a series of Thread TLVs.
+     *
+     * <p>{@code tlvs} can be obtained from the value of a Thread Pending Operational Dataset TLV
+     * (see the <a href="https://www.threadgroup.org/support#specifications">Thread
+     * specification</a> for the definition) or the return value of {@link #toThreadTlvs}.
+     *
+     * @throws IllegalArgumentException if {@code tlvs} is malformed or contains an invalid Thread
+     *     TLV
+     */
+    @NonNull
+    public static PendingOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
+        requireNonNull(tlvs, "tlvs cannot be null");
+
+        SparseArray<byte[]> newUnknownTlvs = new SparseArray<>();
+        OperationalDatasetTimestamp pendingTimestamp = null;
+        Duration delayTimer = null;
+        ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(tlvs);
+        SparseArray<byte[]> unknownTlvs = activeDataset.getUnknownTlvs();
+        for (int i = 0; i < unknownTlvs.size(); i++) {
+            int key = unknownTlvs.keyAt(i);
+            byte[] value = unknownTlvs.valueAt(i);
+            switch (key) {
+                case TYPE_PENDING_TIMESTAMP:
+                    pendingTimestamp = OperationalDatasetTimestamp.fromTlvValue(value);
+                    break;
+                case TYPE_DELAY_TIMER:
+                    checkArgument(
+                            value.length == LENGTH_DELAY_TIMER_BYTES,
+                            "Invalid delay timer (length = %d, expectedLength = %d)",
+                            value.length,
+                            LENGTH_DELAY_TIMER_BYTES);
+                    int millis = ByteBuffer.wrap(value).getInt();
+                    delayTimer = Duration.ofMillis(Integer.toUnsignedLong(millis));
+                    break;
+                default:
+                    newUnknownTlvs.put(key, value);
+                    break;
+            }
+        }
+
+        if (pendingTimestamp == null) {
+            throw new IllegalArgumentException("Pending Timestamp is missing");
+        }
+        if (delayTimer == null) {
+            throw new IllegalArgumentException("Delay Timer is missing");
+        }
+
+        activeDataset =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setUnknownTlvs(newUnknownTlvs)
+                        .build();
+        return new PendingOperationalDataset(activeDataset, pendingTimestamp, delayTimer);
+    }
+
+    /** Returns the Active Operational Dataset. */
+    @NonNull
+    public ActiveOperationalDataset getActiveOperationalDataset() {
+        return mActiveOpDataset;
+    }
+
+    /** Returns the Pending Timestamp. */
+    @NonNull
+    public OperationalDatasetTimestamp getPendingTimestamp() {
+        return mPendingTimestamp;
+    }
+
+    /** Returns the Delay Timer. */
+    @NonNull
+    public Duration getDelayTimer() {
+        return mDelayTimer;
+    }
+
+    /**
+     * Converts this {@link PendingOperationalDataset} object to a series of Thread TLVs.
+     *
+     * <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
+     * specification</a> for the definition of the Thread TLV format.
+     */
+    @NonNull
+    public byte[] toThreadTlvs() {
+        ByteArrayOutputStream dataset = new ByteArrayOutputStream();
+
+        byte[] activeDatasetBytes = mActiveOpDataset.toThreadTlvs();
+        dataset.write(activeDatasetBytes, 0, activeDatasetBytes.length);
+
+        dataset.write(TYPE_PENDING_TIMESTAMP);
+        byte[] pendingTimestampBytes = mPendingTimestamp.toTlvValue();
+        dataset.write(pendingTimestampBytes.length);
+        dataset.write(pendingTimestampBytes, 0, pendingTimestampBytes.length);
+
+        dataset.write(TYPE_DELAY_TIMER);
+        byte[] delayTimerBytes = new byte[LENGTH_DELAY_TIMER_BYTES];
+        ByteBuffer.wrap(delayTimerBytes).putInt((int) mDelayTimer.toMillis());
+        dataset.write(delayTimerBytes.length);
+        dataset.write(delayTimerBytes, 0, delayTimerBytes.length);
+
+        return dataset.toByteArray();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (!(other instanceof PendingOperationalDataset)) {
+            return false;
+        } else {
+            PendingOperationalDataset otherDataset = (PendingOperationalDataset) other;
+            return mActiveOpDataset.equals(otherDataset.mActiveOpDataset)
+                    && mPendingTimestamp.equals(otherDataset.mPendingTimestamp)
+                    && mDelayTimer.equals(otherDataset.mDelayTimer);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mActiveOpDataset, mPendingTimestamp, mDelayTimer);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("{activeDataset=")
+                .append(getActiveOperationalDataset())
+                .append(", pendingTimestamp=")
+                .append(getPendingTimestamp())
+                .append(", delayTimer=")
+                .append(getDelayTimer())
+                .append("}");
+        return sb.toString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeByteArray(toThreadTlvs());
+    }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.aidl b/thread/framework/java/android/net/thread/ThreadConfiguration.aidl
new file mode 100644
index 0000000..9473411
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+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..e09b3a6
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -0,0 +1,170 @@
+/*
+ * 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.NonNull;
+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 mDhcp6PdEnabled;
+
+    private ThreadConfiguration(Builder builder) {
+        this(builder.mNat64Enabled, builder.mDhcp6PdEnabled);
+    }
+
+    private ThreadConfiguration(boolean nat64Enabled, boolean dhcp6PdEnabled) {
+        this.mNat64Enabled = nat64Enabled;
+        this.mDhcp6PdEnabled = dhcp6PdEnabled;
+    }
+
+    /** Returns {@code true} if NAT64 is enabled. */
+    public boolean isNat64Enabled() {
+        return mNat64Enabled;
+    }
+
+    /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
+    public boolean isDhcp6PdEnabled() {
+        return mDhcp6PdEnabled;
+    }
+
+    @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
+                    && mDhcp6PdEnabled == otherConfig.mDhcp6PdEnabled;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mNat64Enabled, mDhcp6PdEnabled);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append('{');
+        sb.append("Nat64Enabled=").append(mNat64Enabled);
+        sb.append(", Dhcp6PdEnabled=").append(mDhcp6PdEnabled);
+        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(mDhcp6PdEnabled);
+    }
+
+    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.setDhcp6PdEnabled(in.readBoolean());
+                    return builder.build();
+                }
+
+                @Override
+                public ThreadConfiguration[] newArray(int size) {
+                    return new ThreadConfiguration[size];
+                }
+            };
+
+    /** The builder for creating {@link ThreadConfiguration} objects. */
+    public static final class Builder {
+        private boolean mNat64Enabled = false;
+        private boolean mDhcp6PdEnabled = 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;
+            mDhcp6PdEnabled = config.mDhcp6PdEnabled;
+        }
+
+        /**
+         * 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 setDhcp6PdEnabled(boolean enabled) {
+            this.mDhcp6PdEnabled = 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
new file mode 100644
index 0000000..30b3d6a
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -0,0 +1,887 @@
+/*
+ * 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.thread;
+
+import static java.util.Objects.requireNonNull;
+
+import android.Manifest.permission;
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+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;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
+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.
+ *
+ * <p>For example, join this device to a Thread network with given Thread Operational Dataset, or
+ * migrate an existing network.
+ *
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class ThreadNetworkController {
+    private static final String TAG = "ThreadNetworkController";
+
+    /** The Thread stack is stopped. */
+    public static final int DEVICE_ROLE_STOPPED = 0;
+
+    /** The device is not currently participating in a Thread network/partition. */
+    public static final int DEVICE_ROLE_DETACHED = 1;
+
+    /** The device is a Thread Child. */
+    public static final int DEVICE_ROLE_CHILD = 2;
+
+    /** The device is a Thread Router. */
+    public static final int DEVICE_ROLE_ROUTER = 3;
+
+    /** The device is a Thread Leader. */
+    public static final int DEVICE_ROLE_LEADER = 4;
+
+    /** The Thread radio is disabled. */
+    public static final int STATE_DISABLED = 0;
+
+    /** The Thread radio is enabled. */
+    public static final int STATE_ENABLED = 1;
+
+    /** The Thread radio is being disabled. */
+    public static final int STATE_DISABLING = 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+        DEVICE_ROLE_STOPPED,
+        DEVICE_ROLE_DETACHED,
+        DEVICE_ROLE_CHILD,
+        DEVICE_ROLE_ROUTER,
+        DEVICE_ROLE_LEADER
+    })
+    public @interface DeviceRole {}
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = {"STATE_"},
+            value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
+    public @interface EnabledState {}
+
+    /** 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})
+    public @interface ThreadVersion {}
+
+    private final IThreadNetworkController mControllerService;
+
+    private final Object mStateCallbackMapLock = new Object();
+
+    @GuardedBy("mStateCallbackMapLock")
+    private final Map<StateCallback, StateCallbackProxy> mStateCallbackMap = new HashMap<>();
+
+    private final Object mOpDatasetCallbackMapLock = new Object();
+
+    @GuardedBy("mOpDatasetCallbackMapLock")
+    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");
+        mControllerService = controllerService;
+    }
+
+    /**
+     * Enables/Disables the radio of this ThreadNetworkController. The requested enabled state will
+     * be persistent and survives device reboots.
+     *
+     * <p>When Thread is in {@code STATE_DISABLED}, {@link ThreadNetworkController} APIs which
+     * require the Thread radio will fail with error code {@link
+     * ThreadNetworkException#ERROR_THREAD_DISABLED}. When Thread is in {@code STATE_DISABLING},
+     * {@link ThreadNetworkController} APIs that return a {@link ThreadNetworkException} will fail
+     * with error code {@link ThreadNetworkException#ERROR_BUSY}.
+     *
+     * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. It indicates
+     * the operation has completed. But there maybe subsequent calls to update the enabled state,
+     * callers of this method should use {@link #registerStateCallback} to subscribe to the Thread
+     * enabled state changes.
+     *
+     * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
+     * specific error in {@link ThreadNetworkException#ERROR_}.
+     *
+     * @param enabled {@code true} for enabling Thread
+     * @param executor the executor to execute {@code receiver}
+     * @param receiver the receiver to receive result of this operation
+     */
+    @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+    public void setEnabled(
+            boolean enabled,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        try {
+            mControllerService.setEnabled(enabled, new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** Returns the Thread version this device is operating on. */
+    @ThreadVersion
+    public int getThreadVersion() {
+        try {
+            return mControllerService.getThreadVersion();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Creates a new Active Operational Dataset with randomized parameters.
+     *
+     * <p>This method is the recommended way to create a randomized dataset which can be used with
+     * {@link #join} to securely join this device to the specified network . It's highly discouraged
+     * to change the randomly generated Extended PAN ID, Network Key or PSKc, as it will compromise
+     * the security of a Thread network.
+     *
+     * @throws IllegalArgumentException if length of the UTF-8 representation of {@code networkName}
+     *     isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
+     *     #LENGTH_MAX_NETWORK_NAME_BYTES}]
+     */
+    public void createRandomizedDataset(
+            @NonNull String networkName,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> receiver) {
+        ActiveOperationalDataset.checkNetworkName(networkName);
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(receiver, "receiver cannot be null");
+        try {
+            mControllerService.createRandomizedDataset(
+                    networkName, new ActiveDatasetReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /** Returns {@code true} if {@code deviceRole} indicates an attached state. */
+    public static boolean isAttached(@DeviceRole int deviceRole) {
+        return deviceRole == DEVICE_ROLE_CHILD
+                || deviceRole == DEVICE_ROLE_ROUTER
+                || deviceRole == DEVICE_ROLE_LEADER;
+    }
+
+    /**
+     * Callback to receive notifications when the Thread network states are changed.
+     *
+     * <p>Applications which are interested in monitoring Thread network states should implement
+     * this interface and register the callback with {@link #registerStateCallback}.
+     */
+    public interface StateCallback {
+        /**
+         * The Thread device role has changed.
+         *
+         * @param deviceRole the new Thread device role
+         */
+        void onDeviceRoleChanged(@DeviceRole int deviceRole);
+
+        /**
+         * The Thread network partition ID has changed.
+         *
+         * @param partitionId the new Thread partition ID
+         */
+        default void onPartitionIdChanged(long partitionId) {}
+
+        /**
+         * The Thread enabled state has changed.
+         *
+         * <p>The Thread enabled state can be set with {@link setEnabled}, it may also be updated by
+         * airplane mode or admin control.
+         *
+         * @param enabledState the new Thread enabled state
+         */
+        default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
+    }
+
+    private static final class StateCallbackProxy extends IStateCallback.Stub {
+        private final Executor mExecutor;
+        private final StateCallback mCallback;
+
+        StateCallbackProxy(@CallbackExecutor Executor executor, StateCallback callback) {
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onDeviceRoleChanged(@DeviceRole int deviceRole) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mCallback.onDeviceRoleChanged(deviceRole));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void onPartitionIdChanged(long partitionId) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mCallback.onPartitionIdChanged(partitionId));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void onThreadEnableStateChanged(@EnabledState int enabled) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mCallback.onThreadEnableStateChanged(enabled));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    /**
+     * Registers a callback to be called when Thread network states are changed.
+     *
+     * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
+     * existing states.
+     *
+     * @param executor the executor to execute the {@code callback}
+     * @param callback the callback to receive Thread network state changes
+     * @throws IllegalArgumentException if {@code callback} has already been registered
+     */
+    @RequiresPermission(permission.ACCESS_NETWORK_STATE)
+    public void registerStateCallback(
+            @NonNull @CallbackExecutor Executor executor, @NonNull StateCallback callback) {
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(callback, "callback cannot be null");
+        synchronized (mStateCallbackMapLock) {
+            if (mStateCallbackMap.containsKey(callback)) {
+                throw new IllegalArgumentException("callback has already been registered");
+            }
+            StateCallbackProxy callbackProxy = new StateCallbackProxy(executor, callback);
+            mStateCallbackMap.put(callback, callbackProxy);
+
+            try {
+                mControllerService.registerStateCallback(callbackProxy);
+            } catch (RemoteException e) {
+                mStateCallbackMap.remove(callback);
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Unregisters the Thread state changed callback.
+     *
+     * @param callback the callback which has been registered with {@link #registerStateCallback}
+     * @throws IllegalArgumentException if {@code callback} hasn't been registered
+     */
+    @RequiresPermission(permission.ACCESS_NETWORK_STATE)
+    public void unregisterStateCallback(@NonNull StateCallback callback) {
+        requireNonNull(callback, "callback cannot be null");
+        synchronized (mStateCallbackMapLock) {
+            StateCallbackProxy callbackProxy = mStateCallbackMap.get(callback);
+            if (callbackProxy == null) {
+                throw new IllegalArgumentException("callback hasn't been registered");
+            }
+            try {
+                mControllerService.unregisterStateCallback(callbackProxy);
+                mStateCallbackMap.remove(callback);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Callback to receive notifications when the Thread Operational Datasets are changed.
+     *
+     * <p>Applications which are interested in monitoring Thread network datasets should implement
+     * this interface and register the callback with {@link #registerOperationalDatasetCallback}.
+     */
+    public interface OperationalDatasetCallback {
+        /**
+         * Called when the Active Operational Dataset is changed.
+         *
+         * @param activeDataset the new Active Operational Dataset or {@code null} if the dataset is
+         *     absent
+         */
+        void onActiveOperationalDatasetChanged(@Nullable ActiveOperationalDataset activeDataset);
+
+        /**
+         * Called when the Pending Operational Dataset is changed.
+         *
+         * @param pendingDataset the new Pending Operational Dataset or {@code null} if the dataset
+         *     has been committed and removed
+         */
+        default void onPendingOperationalDatasetChanged(
+                @Nullable PendingOperationalDataset pendingDataset) {}
+    }
+
+    private static final class OperationalDatasetCallbackProxy
+            extends IOperationalDatasetCallback.Stub {
+        private final Executor mExecutor;
+        private final OperationalDatasetCallback mCallback;
+
+        OperationalDatasetCallbackProxy(
+                @CallbackExecutor Executor executor, OperationalDatasetCallback callback) {
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onActiveOperationalDatasetChanged(
+                @Nullable ActiveOperationalDataset activeDataset) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mCallback.onActiveOperationalDatasetChanged(activeDataset));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void onPendingOperationalDatasetChanged(
+                @Nullable PendingOperationalDataset pendingDataset) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(
+                        () -> mCallback.onPendingOperationalDatasetChanged(pendingDataset));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    /**
+     * Registers a callback to be called when Thread Operational Datasets are changed.
+     *
+     * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
+     * existing Operational Datasets.
+     *
+     * @param executor the executor to execute {@code callback}
+     * @param callback the callback to receive Operational Dataset changes
+     * @throws IllegalArgumentException if {@code callback} has already been registered
+     */
+    @RequiresPermission(
+            allOf = {
+                permission.ACCESS_NETWORK_STATE,
+                "android.permission.THREAD_NETWORK_PRIVILEGED"
+            })
+    public void registerOperationalDatasetCallback(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OperationalDatasetCallback callback) {
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(callback, "callback cannot be null");
+        synchronized (mOpDatasetCallbackMapLock) {
+            if (mOpDatasetCallbackMap.containsKey(callback)) {
+                throw new IllegalArgumentException("callback has already been registered");
+            }
+            OperationalDatasetCallbackProxy callbackProxy =
+                    new OperationalDatasetCallbackProxy(executor, callback);
+            mOpDatasetCallbackMap.put(callback, callbackProxy);
+
+            try {
+                mControllerService.registerOperationalDatasetCallback(callbackProxy);
+            } catch (RemoteException e) {
+                mOpDatasetCallbackMap.remove(callback);
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Unregisters the Thread Operational Dataset callback.
+     *
+     * @param callback the callback which has been registered with {@link
+     *     #registerOperationalDatasetCallback}
+     * @throws IllegalArgumentException if {@code callback} hasn't been registered
+     */
+    @RequiresPermission(
+            allOf = {
+                permission.ACCESS_NETWORK_STATE,
+                "android.permission.THREAD_NETWORK_PRIVILEGED"
+            })
+    public void unregisterOperationalDatasetCallback(@NonNull OperationalDatasetCallback callback) {
+        requireNonNull(callback, "callback cannot be null");
+        synchronized (mOpDatasetCallbackMapLock) {
+            OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.get(callback);
+            if (callbackProxy == null) {
+                throw new IllegalArgumentException("callback hasn't been registered");
+            }
+            try {
+                mControllerService.unregisterOperationalDatasetCallback(callbackProxy);
+                mOpDatasetCallbackMap.remove(callback);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Joins to a Thread network with given Active Operational Dataset.
+     *
+     * <p>This method does nothing if this device has already joined to the same network specified
+     * by {@code activeDataset}. If this device has already joined to a different network, this
+     * device will first leave from that network and then join the new network. This method changes
+     * only this device and all other connected devices will stay in the old network. To change the
+     * network for all connected devices together, use {@link #scheduleMigration}.
+     *
+     * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called and the Dataset
+     * will be persisted on this device; this device will try to attach to the Thread network and
+     * the state changes can be observed by {@link #registerStateCallback}. On failure, {@link
+     * OutcomeReceiver#onError} of {@code receiver} will be invoked with a specific error:
+     *
+     * <ul>
+     *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code activeDataset}
+     *       specifies a channel which is not supported in the current country or region; the {@code
+     *       activeDataset} is rejected and not persisted so this device won't auto re-join the next
+     *       time
+     *   <li>{@link ThreadNetworkException#ERROR_ABORTED} this operation is aborted by another
+     *       {@code join} or {@code leave} operation
+     * </ul>
+     *
+     * @param activeDataset the Active Operational Dataset represents the Thread network to join
+     * @param executor the executor to execute {@code receiver}
+     * @param receiver the receiver to receive result of this operation
+     */
+    @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+    public void join(
+            @NonNull ActiveOperationalDataset activeDataset,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        requireNonNull(activeDataset, "activeDataset cannot be null");
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(receiver, "receiver cannot be null");
+        try {
+            mControllerService.join(activeDataset, new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Schedules a network migration which moves all devices in the current connected network to a
+     * new network or updates parameters of the current connected network.
+     *
+     * <p>The migration doesn't happen immediately but is registered to the Leader device so that
+     * all devices in the current Thread network can be scheduled to apply the new dataset together.
+     *
+     * <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
+     * {@link OutcomeReceiver#onResult} of {@code receiver} will be called; Operational Dataset
+     * changes will be asynchronously delivered via {@link OperationalDatasetCallback} if a callback
+     * has been registered with {@link #registerOperationalDatasetCallback}. When failed, {@link
+     * OutcomeReceiver#onError} will be called with a specific error:
+     *
+     * <ul>
+     *   <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} the migration is rejected
+     *       because this device is not attached
+     *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code pendingDataset}
+     *       specifies a channel which is not supported in the current country or region; the {@code
+     *       pendingDataset} is rejected and not persisted
+     *   <li>{@link ThreadNetworkException#ERROR_REJECTED_BY_PEER} the Pending Dataset is rejected
+     *       by the Leader device
+     *   <li>{@link ThreadNetworkException#ERROR_BUSY} another {@code scheduleMigration} request is
+     *       being processed
+     *   <li>{@link ThreadNetworkException#ERROR_TIMEOUT} response from the Leader device hasn't
+     *       been received before deadline
+     * </ul>
+     *
+     * <p>The Delay Timer of {@code pendingDataset} can vary from several minutes to a few days.
+     * It's important to select a proper value to safely migrate all devices in the network without
+     * leaving sleepy end devices orphaned. Apps are not suggested to specify the Delay Timer value
+     * if it's unclear how long it can take to propagate the {@code pendingDataset} to the whole
+     * network. Instead, use {@link Duration#ZERO} to use the default value suggested by the system.
+     *
+     * @param pendingDataset the Pending Operational Dataset
+     * @param executor the executor to execute {@code receiver}
+     * @param receiver the receiver to receive result of this operation
+     */
+    @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+    public void scheduleMigration(
+            @NonNull PendingOperationalDataset pendingDataset,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        requireNonNull(pendingDataset, "pendingDataset cannot be null");
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(receiver, "receiver cannot be null");
+        try {
+            mControllerService.scheduleMigration(
+                    pendingDataset, new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Leaves from the Thread network.
+     *
+     * <p>This undoes a {@link join} operation. On success, this device is disconnected from the
+     * joined network and will not automatically join a network before {@link #join} is called
+     * again. Active and Pending Operational Dataset configured and persisted on this device will be
+     * removed too.
+     *
+     * @param executor the executor to execute {@code receiver}
+     * @param receiver the receiver to receive result of this operation
+     */
+    @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+    public void leave(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(receiver, "receiver cannot be null");
+        try {
+            mControllerService.leave(new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * 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 new
+     * {@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
+     * @hide
+     */
+    // @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
+     * @hide
+     */
+    // @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,
+     *     forbids using test network as an upstream.
+     * @param executor the executor to execute {@code receiver}
+     * @param receiver the receiver to receive result of this operation
+     * @hide
+     */
+    @VisibleForTesting
+    @RequiresPermission(
+            allOf = {"android.permission.THREAD_NETWORK_PRIVILEGED", permission.NETWORK_SETTINGS})
+    public void setTestNetworkAsUpstream(
+            @Nullable String testNetworkInterfaceName,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(receiver, "receiver cannot be null");
+        try {
+            mControllerService.setTestNetworkAsUpstream(
+                    testNetworkInterfaceName, new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * 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,
+            int errorCode,
+            String errorMsg) {
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            executor.execute(
+                    () -> receiver.onError(new ThreadNetworkException(errorCode, errorMsg)));
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private static final class ActiveDatasetReceiverProxy
+            extends IActiveOperationalDatasetReceiver.Stub {
+        final Executor mExecutor;
+        final OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> mResultReceiver;
+
+        ActiveDatasetReceiverProxy(
+                @CallbackExecutor Executor executor,
+                OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> resultReceiver) {
+            this.mExecutor = executor;
+            this.mResultReceiver = resultReceiver;
+        }
+
+        @Override
+        public void onSuccess(ActiveOperationalDataset dataset) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mResultReceiver.onResult(dataset));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void onError(int errorCode, String errorMessage) {
+            propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
+        }
+    }
+
+    private static final class OperationReceiverProxy extends IOperationReceiver.Stub {
+        final Executor mExecutor;
+        final OutcomeReceiver<Void, ThreadNetworkException> mResultReceiver;
+
+        OperationReceiverProxy(
+                @CallbackExecutor Executor executor,
+                OutcomeReceiver<Void, ThreadNetworkException> resultReceiver) {
+            this.mExecutor = executor;
+            this.mResultReceiver = resultReceiver;
+        }
+
+        @Override
+        public void onSuccess() {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mResultReceiver.onResult(null));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void onError(int errorCode, String errorMessage) {
+            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
new file mode 100644
index 0000000..f699c30
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -0,0 +1,180 @@
+/*
+ * 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.thread;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents a Thread network specific failure.
+ *
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public class ThreadNetworkException extends Exception {
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+        ERROR_INTERNAL_ERROR,
+        ERROR_ABORTED,
+        ERROR_TIMEOUT,
+        ERROR_UNAVAILABLE,
+        ERROR_BUSY,
+        ERROR_FAILED_PRECONDITION,
+        ERROR_UNSUPPORTED_CHANNEL,
+        ERROR_REJECTED_BY_PEER,
+        ERROR_RESPONSE_BAD_FORMAT,
+        ERROR_RESOURCE_EXHAUSTED,
+        ERROR_UNKNOWN,
+        ERROR_THREAD_DISABLED,
+    })
+    public @interface ErrorCode {}
+
+    /**
+     * The operation failed because some invariants expected by the underlying system have been
+     * broken. This error code is reserved for serious errors. The caller can do nothing to recover
+     * from this error. A bugreport should be created and sent to the Android community if this
+     * error is ever returned.
+     */
+    public static final int ERROR_INTERNAL_ERROR = 1;
+
+    /**
+     * The operation failed because concurrent operations are overriding this one. Retrying an
+     * aborted operation has the risk of aborting another ongoing operation again. So the caller
+     * should retry at a higher level where it knows there won't be race conditions.
+     */
+    public static final int ERROR_ABORTED = 2;
+
+    /**
+     * The operation failed because a deadline expired before the operation could complete. This may
+     * be caused by connectivity unavailability and the caller can retry the same operation when the
+     * connectivity issue is fixed.
+     */
+    public static final int ERROR_TIMEOUT = 3;
+
+    /**
+     * The operation failed because the service is currently unavailable and that this is most
+     * likely a transient condition. The caller can recover from this error by retrying with a
+     * back-off scheme. Note that it is not always safe to retry non-idempotent operations.
+     */
+    public static final int ERROR_UNAVAILABLE = 4;
+
+    /**
+     * The operation failed because this device is currently busy processing concurrent requests.
+     * The caller may recover from this error when the current operations has been finished.
+     */
+    public static final int ERROR_BUSY = 5;
+
+    /**
+     * The operation failed because required preconditions were not satisfied. For example, trying
+     * to schedule a network migration when this device is not attached will receive this error or
+     * enable Thread while User Resitration has disabled it. The caller should not retry the same
+     * operation before the precondition is satisfied.
+     */
+    public static final int ERROR_FAILED_PRECONDITION = 6;
+
+    /**
+     * The operation was rejected because the specified channel is currently not supported by this
+     * device in this country. For example, trying to join or migrate to a network with channel
+     * which is not supported. The caller should should change the channel or return an error to the
+     * user if the channel cannot be changed.
+     */
+    public static final int ERROR_UNSUPPORTED_CHANNEL = 7;
+
+    /**
+     * The operation failed because a request is rejected by the peer device. This happens because
+     * the peer device is not capable of processing the request, or a request from another device
+     * has already been accepted by the peer device. The caller may not be able to recover from this
+     * error by retrying the same operation.
+     */
+    public static final int ERROR_REJECTED_BY_PEER = 8;
+
+    /**
+     * The operation failed because the received response is malformed. This is typically because
+     * the peer device is misbehaving. The caller may only recover from this error by retrying with
+     * a different peer device.
+     */
+    public static final int ERROR_RESPONSE_BAD_FORMAT = 9;
+
+    /**
+     * The operation failed because some resource has been exhausted. For example, no enough
+     * allocated memory buffers, or maximum number of supported operations has been exceeded. The
+     * caller may retry and recover from this error when the resource has been freed.
+     */
+    public static final int ERROR_RESOURCE_EXHAUSTED = 10;
+
+    /**
+     * The operation failed because of an unknown error in the system. This typically indicates that
+     * the caller doesn't understand error codes added in newer Android versions.
+     */
+    public static final int ERROR_UNKNOWN = 11;
+
+    /**
+     * The operation failed because the Thread radio is disabled by {@link
+     * ThreadNetworkController#setEnabled}, airplane mode or device admin. The caller should retry
+     * only after Thread is enabled.
+     */
+    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_UNSUPPORTED_OPERATION;
+
+    private final int mErrorCode;
+
+    /**
+     * Creates a new {@link ThreadNetworkException} object with given error code and message.
+     *
+     * @throws IllegalArgumentException if {@code errorCode} is not a value in {@link #ERROR_}
+     * @throws NullPointerException if {@code message} is {@code null}
+     */
+    public ThreadNetworkException(@ErrorCode int errorCode, @NonNull String message) {
+        super(requireNonNull(message, "message cannot be null"));
+        if (errorCode < ERROR_MIN || errorCode > ERROR_MAX) {
+            throw new IllegalArgumentException(
+                    "errorCode cannot be "
+                            + errorCode
+                            + " (allowedRange = ["
+                            + ERROR_MIN
+                            + ", "
+                            + ERROR_MAX
+                            + "])");
+        }
+        this.mErrorCode = errorCode;
+    }
+
+    /** Returns the error code. */
+    public @ErrorCode int getErrorCode() {
+        return mErrorCode;
+    }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
new file mode 100644
index 0000000..691bbf5
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
@@ -0,0 +1,35 @@
+/*
+ * 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.thread;
+
+/**
+ * Container for flag constants defined in the "thread_network" namespace.
+ *
+ * @hide
+ */
+// TODO: replace this class with auto-generated "com.android.net.thread.flags.Flags" once the
+// flagging infra is fully supported for mainline modules.
+public final class ThreadNetworkFlags {
+    /** @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/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
new file mode 100644
index 0000000..150b759
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -0,0 +1,133 @@
+/*
+ * 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.thread;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+
+import com.android.net.module.util.CollectionUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Provides the primary API for managing app aspects of Thread network connectivity.
+ *
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+@SystemService(ThreadNetworkManager.SERVICE_NAME)
+public final class ThreadNetworkManager {
+    /**
+     * This value tracks {@link Context#THREAD_NETWORK_SERVICE}.
+     *
+     * <p>This is needed because at the time this service is created, it needs to support both
+     * Android U and V but {@link Context#THREAD_NETWORK_SERVICE} Is only available on the V branch.
+     *
+     * <p>Note that this is not added to NetworkStack ConstantsShim because we need this constant in
+     * the framework library while ConstantsShim is only linked against the service library.
+     *
+     * @hide
+     */
+    public static final String SERVICE_NAME = "thread_network";
+
+    /**
+     * This value tracks {@link PackageManager#FEATURE_THREAD_NETWORK}.
+     *
+     * <p>This is needed because at the time this service is created, it needs to support both
+     * Android U and V but {@link PackageManager#FEATURE_THREAD_NETWORK} Is only available on the V
+     * branch.
+     *
+     * <p>Note that this is not added to NetworkStack COnstantsShim because we need this constant in
+     * the framework library while ConstantsShim is only linked against the service library.
+     *
+     * @hide
+     */
+    public static final String FEATURE_NAME = "android.hardware.thread_network";
+
+    /**
+     * Permission allows changing Thread network state and access to Thread network credentials such
+     * as Network Key and PSKc.
+     *
+     * <p>This is the same value as android.Manifest.permission.THREAD_NETWORK_PRIVILEGED. That
+     * symbol is not available on U while this feature needs to support Android U TV devices, so
+     * here is making a copy of android.Manifest.permission.THREAD_NETWORK_PRIVILEGED.
+     *
+     * @hide
+     */
+    public static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
+            "android.permission.THREAD_NETWORK_PRIVILEGED";
+
+    /**
+     * This user restriction specifies if Thread network is disallowed on the device. If Thread
+     * network is disallowed it cannot be turned on via Settings.
+     *
+     * <p>this is a mirror of {@link UserManager#DISALLOW_THREAD_NETWORK} which is not available on
+     * Android U devices.
+     *
+     * @hide
+     */
+    public static final String DISALLOW_THREAD_NETWORK = "no_thread_network";
+
+    @NonNull private final Context mContext;
+    @NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;
+
+    /**
+     * Creates a new ThreadNetworkManager instance.
+     *
+     * @hide
+     */
+    public ThreadNetworkManager(
+            @NonNull Context context, @NonNull IThreadNetworkManager managerService) {
+        this(context, makeControllers(managerService));
+    }
+
+    private static List<ThreadNetworkController> makeControllers(
+            @NonNull IThreadNetworkManager managerService) {
+        requireNonNull(managerService, "managerService cannot be null");
+
+        List<IThreadNetworkController> controllerServices;
+
+        try {
+            controllerServices = managerService.getAllThreadNetworkControllers();
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return Collections.emptyList();
+        }
+
+        return CollectionUtils.map(controllerServices, ThreadNetworkController::new);
+    }
+
+    private ThreadNetworkManager(
+            @NonNull Context context, @NonNull List<ThreadNetworkController> controllerServices) {
+        mContext = context;
+        mUnmodifiableControllerServices = Collections.unmodifiableList(controllerServices);
+    }
+
+    /** Returns the {@link ThreadNetworkController} object of all Thread networks. */
+    @NonNull
+    public List<ThreadNetworkController> getAllThreadNetworkControllers() {
+        return mUnmodifiableControllerServices;
+    }
+}
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
new file mode 100755
index 0000000..c176bfa
--- /dev/null
+++ b/thread/scripts/make-pretty.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && 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
+
+$GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
+$ANDROID_BP_FORMAT -w $(find $SCRIPT_DIR/../ -name "*.bp")
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
new file mode 100644
index 0000000..a82a499
--- /dev/null
+++ b/thread/service/Android.bp
@@ -0,0 +1,75 @@
+//
+// 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_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "service-thread-sources",
+    srcs: ["java/**/*.java"],
+}
+
+java_library {
+    name: "service-thread-pre-jarjar",
+    defaults: ["framework-system-server-module-defaults"],
+    sdk_version: "system_server_current",
+    // 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",
+    srcs: [":service-thread-sources"],
+    libs: [
+        "framework-connectivity-pre-jarjar",
+        "framework-connectivity-t-pre-jarjar",
+        "framework-location.stubs.module_lib",
+        "framework-wifi",
+        "service-connectivity-pre-jarjar",
+        "ServiceConnectivityResources",
+    ],
+    static_libs: [
+        "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"],
+}
+
+cc_library_shared {
+    name: "libservice-thread-jni",
+    min_sdk_version: "30",
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    srcs: [
+        "jni/**/*.cpp",
+    ],
+    shared_libs: [
+        "libbase",
+        "libcutils",
+        "liblog",
+        "libnativehelper",
+    ],
+    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
new file mode 100644
index 0000000..43ff336
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.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.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;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A {@link IActiveOperationalDatasetReceiver} wrapper which makes it easier to invoke the
+ * callbacks.
+ */
+final class ActiveOperationalDatasetReceiverWrapper {
+    private final IActiveOperationalDatasetReceiver mReceiver;
+
+    private static final Object sPendingReceiversLock = new Object();
+
+    @GuardedBy("sPendingReceiversLock")
+    private static final Set<ActiveOperationalDatasetReceiverWrapper> sPendingReceivers =
+            new HashSet<>();
+
+    public ActiveOperationalDatasetReceiverWrapper(IActiveOperationalDatasetReceiver receiver) {
+        this.mReceiver = receiver;
+
+        synchronized (sPendingReceiversLock) {
+            sPendingReceivers.add(this);
+        }
+    }
+
+    public static void onOtDaemonDied() {
+        synchronized (sPendingReceiversLock) {
+            for (ActiveOperationalDatasetReceiverWrapper receiver : sPendingReceivers) {
+                try {
+                    receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+                } catch (RemoteException e) {
+                    // The client is dead, do nothing
+                }
+            }
+            sPendingReceivers.clear();
+        }
+    }
+
+    public void onSuccess(ActiveOperationalDataset dataset) {
+        synchronized (sPendingReceiversLock) {
+            sPendingReceivers.remove(this);
+        }
+
+        try {
+            mReceiver.onSuccess(dataset);
+        } catch (RemoteException e) {
+            // The client is dead, do nothing
+        }
+    }
+
+    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);
+        }
+
+        try {
+            mReceiver.onError(errorCode, errorMessage);
+        } catch (RemoteException e) {
+            // The client is dead, do nothing
+        }
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
new file mode 100644
index 0000000..e72c9ee
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -0,0 +1,73 @@
+/*
+ * 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.thread;
+
+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");
+    }
+
+    /**
+     * Creates a socket on the infrastructure network interface for sending/receiving ICMPv6
+     * Neighbor Discovery messages.
+     *
+     * @param infraInterfaceName the infrastructure network interface name.
+     * @return an ICMPv6 socket file descriptor on the Infrastructure network interface.
+     * @throws IOException when fails to create the socket.
+     */
+    public ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName) throws IOException {
+        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 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
new file mode 100644
index 0000000..1447ff8
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -0,0 +1,726 @@
+/*
+ * 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.thread;
+
+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;
+import android.util.Log;
+import android.util.SparseArray;
+
+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.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of {@link INsdPublisher}.
+ *
+ * <p>This class provides API for service registration and discovery over mDNS. This class is a
+ * proxy between ot-daemon and NsdManager.
+ *
+ * <p>All the data members of this class MUST be accessed in the {@code mHandler}'s Thread except
+ * {@code mHandler} itself.
+ */
+public final class NsdPublisher extends INsdPublisher.Stub {
+    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 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, 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), DnsResolver.getInstance(), handler);
+    }
+
+    // TODO: b/321883491 - NsdPublisher should be disabled when mNetwork is null
+    public void setNetworkForHostResolution(@Nullable Network network) {
+        mNetwork = network;
+    }
+
+    @Override
+    public void registerService(
+            String hostname,
+            String name,
+            String type,
+            List<String> subTypeList,
+            int port,
+            List<DnsTxtAttribute> txt,
+            INsdStatusReceiver receiver,
+            int listenerId) {
+        NsdServiceInfo serviceInfo =
+                buildServiceInfoForService(hostname, name, type, subTypeList, port, txt);
+        mHandler.post(() -> registerInternal(serviceInfo, receiver, listenerId, "service"));
+    }
+
+    private static NsdServiceInfo buildServiceInfoForService(
+            String hostname,
+            String name,
+            String type,
+            List<String> subTypeList,
+            int port,
+            List<DnsTxtAttribute> txt) {
+        NsdServiceInfo serviceInfo = new NsdServiceInfo();
+
+        serviceInfo.setServiceName(name);
+        if (!TextUtils.isEmpty(hostname)) {
+            serviceInfo.setHostname(hostname);
+        }
+        serviceInfo.setServiceType(type);
+        serviceInfo.setPort(port);
+        serviceInfo.setSubtypes(new HashSet<>(subTypeList));
+        for (DnsTxtAttribute attribute : txt) {
+            serviceInfo.setAttribute(attribute.name, attribute.value);
+        }
+
+        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,
+            int listenerId,
+            String registrationType) {
+        checkOnHandlerThread();
+        Log.i(
+                TAG,
+                "Registering "
+                        + registrationType
+                        + ". Listener ID: "
+                        + listenerId
+                        + ", serviceInfo: "
+                        + serviceInfo);
+        RegistrationListener listener = new RegistrationListener(serviceInfo, listenerId, receiver);
+        mRegistrationListeners.append(listenerId, listener);
+        try {
+            mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, mExecutor, listener);
+        } catch (IllegalArgumentException e) {
+            Log.i(TAG, "Failed to register service. serviceInfo: " + serviceInfo, e);
+            listener.onRegistrationFailed(serviceInfo, NsdManager.FAILURE_INTERNAL_ERROR);
+        }
+    }
+
+    public void unregister(INsdStatusReceiver receiver, int listenerId) {
+        mHandler.post(() -> unregisterInternal(receiver, listenerId));
+    }
+
+    public void unregisterInternal(INsdStatusReceiver receiver, int listenerId) {
+        checkOnHandlerThread();
+        RegistrationListener registrationListener = mRegistrationListeners.get(listenerId);
+        if (registrationListener == null) {
+            Log.w(
+                    TAG,
+                    "Failed to unregister service."
+                            + " Listener ID: "
+                            + listenerId
+                            + " The registrationListener is empty.");
+
+            return;
+        }
+        Log.i(
+                TAG,
+                "Unregistering service."
+                        + " Listener ID: "
+                        + listenerId
+                        + " serviceInfo: "
+                        + registrationListener.mServiceInfo);
+        registrationListener.addUnregistrationReceiver(receiver);
+        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(
+                    "Not running on handler Thread: " + Thread.currentThread().getName());
+        }
+    }
+
+    @Override
+    public void reset() {
+        mHandler.post(this::resetInternal);
+    }
+
+    private void resetInternal() {
+        checkOnHandlerThread();
+        for (int i = 0; i < mRegistrationListeners.size(); ++i) {
+            try {
+                mNsdManager.unregisterService(mRegistrationListeners.valueAt(i));
+            } catch (IllegalArgumentException e) {
+                Log.i(
+                        TAG,
+                        "Failed to unregister."
+                                + " Listener ID: "
+                                + mRegistrationListeners.keyAt(i)
+                                + " serviceInfo: "
+                                + mRegistrationListeners.valueAt(i).mServiceInfo,
+                        e);
+            }
+        }
+        mRegistrationListeners.clear();
+    }
+
+    /** On ot-daemon died, reset. */
+    public void onOtDaemonDied() {
+        reset();
+    }
+
+    private final class RegistrationListener implements NsdManager.RegistrationListener {
+        private final NsdServiceInfo mServiceInfo;
+        private final int mListenerId;
+        private final INsdStatusReceiver mRegistrationReceiver;
+        private final List<INsdStatusReceiver> mUnregistrationReceivers;
+
+        RegistrationListener(
+                @NonNull NsdServiceInfo serviceInfo,
+                int listenerId,
+                @NonNull INsdStatusReceiver registrationReceiver) {
+            mServiceInfo = serviceInfo;
+            mListenerId = listenerId;
+            mRegistrationReceiver = registrationReceiver;
+            mUnregistrationReceivers = new ArrayList<>();
+        }
+
+        void addUnregistrationReceiver(@NonNull INsdStatusReceiver unregistrationReceiver) {
+            mUnregistrationReceivers.add(unregistrationReceiver);
+        }
+
+        @Override
+        public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+            checkOnHandlerThread();
+            mRegistrationListeners.remove(mListenerId);
+            Log.i(
+                    TAG,
+                    "Failed to register listener ID: "
+                            + mListenerId
+                            + " error code: "
+                            + errorCode
+                            + " serviceInfo: "
+                            + serviceInfo);
+            try {
+                mRegistrationReceiver.onError(errorCode);
+            } catch (RemoteException ignored) {
+                // do nothing if the client is dead
+            }
+        }
+
+        @Override
+        public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+            checkOnHandlerThread();
+            for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
+                Log.i(
+                        TAG,
+                        "Failed to unregister."
+                                + "Listener ID: "
+                                + mListenerId
+                                + ", error code: "
+                                + errorCode
+                                + ", serviceInfo: "
+                                + serviceInfo);
+                try {
+                    receiver.onError(errorCode);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        @Override
+        public void onServiceRegistered(NsdServiceInfo serviceInfo) {
+            checkOnHandlerThread();
+            Log.i(
+                    TAG,
+                    "Registered successfully. "
+                            + "Listener ID: "
+                            + mListenerId
+                            + ", serviceInfo: "
+                            + serviceInfo);
+            try {
+                mRegistrationReceiver.onSuccess();
+            } catch (RemoteException ignored) {
+                // do nothing if the client is dead
+            }
+        }
+
+        @Override
+        public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
+            checkOnHandlerThread();
+            for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
+                Log.i(
+                        TAG,
+                        "Unregistered successfully. "
+                                + "Listener ID: "
+                                + mListenerId
+                                + ", serviceInfo: "
+                                + serviceInfo);
+                try {
+                    receiver.onSuccess();
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+            mRegistrationListeners.remove(mListenerId);
+        }
+    }
+
+    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
new file mode 100644
index 0000000..bad63f3
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
@@ -0,0 +1,112 @@
+/*
+ * 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.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;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** 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();
+
+    @GuardedBy("sPendingReceiversLock")
+    private static final Set<OperationReceiverWrapper> sPendingReceivers = new HashSet<>();
+
+    public OperationReceiverWrapper(IOperationReceiver 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);
+        }
+    }
+
+    public static void onOtDaemonDied() {
+        synchronized (sPendingReceiversLock) {
+            for (OperationReceiverWrapper receiver : sPendingReceivers) {
+                try {
+                    if (receiver.mExpectOtDaemonDied) {
+                        receiver.mReceiver.onSuccess();
+                    } else {
+                        receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+                    }
+                } catch (RemoteException e) {
+                    // The client is dead, do nothing
+                }
+            }
+            sPendingReceivers.clear();
+        }
+    }
+
+    public void onSuccess() {
+        synchronized (sPendingReceiversLock) {
+            sPendingReceivers.remove(this);
+        }
+
+        try {
+            mReceiver.onSuccess();
+        } catch (RemoteException e) {
+            // The client is dead, do nothing
+        }
+    }
+
+    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);
+        }
+
+        try {
+            mReceiver.onError(errorCode, String.format(errorMessage, messageArgs));
+        } catch (RemoteException e) {
+            // The client is dead, do nothing
+        }
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
new file mode 100644
index 0000000..e6f272b
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -0,0 +1,1661 @@
+/*
+ * 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.thread;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
+import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_MESH_LOCAL_PREFIX_BITS;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_PSKC;
+import static android.net.thread.ActiveOperationalDataset.MESH_LOCAL_PREFIX_FIRST_BYTE;
+import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
+import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
+import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
+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_REJECTED_BY_PEER;
+import static android.net.thread.ThreadNetworkException.ERROR_RESOURCE_EXHAUSTED;
+import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT;
+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;
+
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
+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;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REJECTED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_RESPONSE_TIMEOUT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_THREAD_DISABLED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_UNSUPPORTED_CHANNEL;
+import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_DISABLED;
+import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_DISABLING;
+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;
+import android.annotation.RequiresPermission;
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+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.LinkProperties;
+import android.net.LocalNetworkConfig;
+import android.net.LocalNetworkInfo;
+import android.net.MulticastRoutingConfig;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+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.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.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.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.
+ *
+ * <p>Threading model: This class is not Thread-safe and should only be accessed from the
+ * ThreadNetworkService class. Additional attention should be paid to handle the threading code
+ * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from the
+ * thread of `mHandler` 2. In the @Override methods, the actual work MUST be dispatched to the
+ * HandlerThread except for arguments or permissions checking
+ */
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+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;
+    private final Handler mHandler;
+
+    // Below member fields can only be accessed from the handler thread (`mHandler`). In
+    // particular, the constructor does not run on the handler thread, so it must not touch any of
+    // the non-final fields, nor must it mutate any of the non-final fields inside these objects.
+
+    private final NetworkProvider mNetworkProvider;
+    private final Supplier<IOtDaemon> mOtDaemonSupplier;
+    private final ConnectivityManager mConnectivityManager;
+    private final TunInterfaceController mTunIfController;
+    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;
+
+    private MulticastRoutingConfig mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+    private MulticastRoutingConfig mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+    private Network mUpstreamNetwork;
+    private NetworkRequest mUpstreamNetworkRequest;
+    private UpstreamNetworkCallback mUpstreamNetworkCallback;
+    private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
+    private final HashMap<Network, String> mNetworkToInterface;
+    private final ThreadPersistentSettings mPersistentSettings;
+    private final UserManager mUserManager;
+    private boolean mUserRestricted;
+    private boolean mForceStopOtDaemonEnabled;
+
+    private BorderRouterConfigurationParcel mBorderRouterConfig;
+
+    @VisibleForTesting
+    ThreadNetworkControllerService(
+            Context context,
+            Handler handler,
+            NetworkProvider networkProvider,
+            Supplier<IOtDaemon> otDaemonSupplier,
+            ConnectivityManager connectivityManager,
+            TunInterfaceController tunIfController,
+            InfraInterfaceController infraIfController,
+            ThreadPersistentSettings persistentSettings,
+            NsdPublisher nsdPublisher,
+            UserManager userManager,
+            ConnectivityResources resources,
+            Supplier<String> countryCodeSupplier) {
+        mContext = context;
+        mHandler = handler;
+        mNetworkProvider = networkProvider;
+        mOtDaemonSupplier = otDaemonSupplier;
+        mConnectivityManager = connectivityManager;
+        mTunIfController = tunIfController;
+        mInfraIfController = infraIfController;
+        mUpstreamNetworkRequest = newUpstreamNetworkRequest();
+        mNetworkToInterface = new HashMap<Network, String>();
+        mBorderRouterConfig = new BorderRouterConfigurationParcel();
+        mPersistentSettings = persistentSettings;
+        mNsdPublisher = nsdPublisher;
+        mUserManager = userManager;
+        mResources = resources;
+        mCountryCodeSupplier = countryCodeSupplier;
+    }
+
+    public static ThreadNetworkControllerService newInstance(
+            Context context,
+            ThreadPersistentSettings persistentSettings,
+            Supplier<String> countryCodeSupplier) {
+        HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
+        handlerThread.start();
+        Handler handler = new Handler(handlerThread.getLooper());
+        NetworkProvider networkProvider =
+                new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
+
+        return new ThreadNetworkControllerService(
+                context,
+                handler,
+                networkProvider,
+                () -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
+                context.getSystemService(ConnectivityManager.class),
+                new TunInterfaceController(TUN_IF_NAME),
+                new InfraInterfaceController(),
+                persistentSettings,
+                NsdPublisher.newInstance(context, handler),
+                context.getSystemService(UserManager.class),
+                new ConnectivityResources(context),
+                countryCodeSupplier);
+    }
+
+    private NetworkRequest newUpstreamNetworkRequest() {
+        NetworkRequest.Builder builder = new NetworkRequest.Builder().clearCapabilities();
+
+        if (mUpstreamTestNetworkSpecifier != null) {
+            return builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                    .setNetworkSpecifier(mUpstreamTestNetworkSpecifier)
+                    .build();
+        }
+        return builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .build();
+    }
+
+    private LocalNetworkConfig newLocalNetworkConfig() {
+        return new LocalNetworkConfig.Builder()
+                .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
+                .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
+                .setUpstreamSelector(mUpstreamNetworkRequest)
+                .build();
+    }
+
+    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", e);
+        } catch (ThreadNetworkException e) {
+            // no ThreadNetworkException.ERROR_THREAD_DISABLED error should be thrown
+            throw new AssertionError(e);
+        }
+    }
+
+    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;
+        }
+
+        IOtDaemon otDaemon = mOtDaemonSupplier.get();
+        if (otDaemon == null) {
+            throw new RemoteException("Internal error: failed to start OT daemon");
+        }
+
+        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...");
+
+        OperationReceiverWrapper.onOtDaemonDied();
+        mOtDaemonCallbackProxy.onOtDaemonDied();
+        mTunIfController.onOtDaemonDied();
+        mNsdPublisher.onOtDaemonDied();
+        mOtDaemon = null;
+        maybeInitializeOtDaemon();
+    }
+
+    public void initialize() {
+        mHandler.post(
+                () -> {
+                    Log.d(
+                            TAG,
+                            "Initializing Thread system service: Thread is "
+                                    + (shouldEnableThread() ? "enabled" : "disabled"));
+                    try {
+                        mTunIfController.createTunInterface();
+                    } catch (IOException e) {
+                        throw new IllegalStateException(
+                                "Failed to create Thread tunnel interface", e);
+                    }
+                    mConnectivityManager.registerNetworkProvider(mNetworkProvider);
+                    requestUpstreamNetwork();
+                    requestThreadNetwork();
+                    mUserRestricted = isThreadUserRestricted();
+                    registerUserRestrictionsReceiver();
+                    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);
+
+        mHandler.post(
+                () ->
+                        setEnabledInternal(
+                                isEnabled,
+                                true /* persist */,
+                                new OperationReceiverWrapper(receiver)));
+    }
+
+    private void setEnabledInternal(
+            boolean isEnabled, boolean persist, @NonNull OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+        if (isEnabled && isThreadUserRestricted()) {
+            receiver.onError(
+                    ERROR_FAILED_PRECONDITION,
+                    "Cannot enable Thread: forbidden by user restriction");
+            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
+            // to the desired value after reboot.
+            mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
+        }
+
+        try {
+            getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
+        } catch (RemoteException | ThreadNetworkException e) {
+            Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
+            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() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        onUserRestrictionsChanged(isThreadUserRestricted());
+                    }
+                },
+                new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED),
+                null /* broadcastPermission */,
+                mHandler);
+    }
+
+    private void onUserRestrictionsChanged(boolean newUserRestrictedState) {
+        checkOnHandlerThread();
+        if (mUserRestricted == newUserRestrictedState) {
+            return;
+        }
+        Log.i(
+                TAG,
+                "Thread user restriction changed: "
+                        + mUserRestricted
+                        + " -> "
+                        + newUserRestrictedState);
+        mUserRestricted = newUserRestrictedState;
+
+        final boolean shouldEnableThread = shouldEnableThread();
+        final IOperationReceiver receiver =
+                new IOperationReceiver.Stub() {
+                    @Override
+                    public void onSuccess() {
+                        Log.d(
+                                TAG,
+                                (shouldEnableThread ? "Enabled" : "Disabled")
+                                        + " Thread due to user restriction change");
+                    }
+
+                    @Override
+                    public void onError(int errorCode, String errorMessage) {
+                        Log.e(
+                                TAG,
+                                "Failed to "
+                                        + (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(
+                shouldEnableThread, false /* persist */, new OperationReceiverWrapper(receiver));
+    }
+
+    /** Returns {@code true} if Thread has been restricted for the user. */
+    private boolean isThreadUserRestricted() {
+        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.");
+        }
+        mUpstreamNetworkCallback = new UpstreamNetworkCallback();
+        mConnectivityManager.registerNetworkCallback(
+                mUpstreamNetworkRequest, mUpstreamNetworkCallback, mHandler);
+    }
+
+    private void cancelRequestUpstreamNetwork() {
+        if (mUpstreamNetworkCallback == null) {
+            throw new AssertionError("The upstream network request null.");
+        }
+        mNetworkToInterface.clear();
+        mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
+        mUpstreamNetworkCallback = null;
+    }
+
+    private final class UpstreamNetworkCallback extends ConnectivityManager.NetworkCallback {
+        @Override
+        public void onAvailable(@NonNull Network network) {
+            checkOnHandlerThread();
+            Log.i(TAG, "Upstream network available: " + network);
+        }
+
+        @Override
+        public void onLost(@NonNull Network network) {
+            checkOnHandlerThread();
+            Log.i(TAG, "Upstream network lost: " + network);
+
+            // TODO: disable border routing when upsteam network disconnected
+        }
+
+        @Override
+        public void onLinkPropertiesChanged(
+                @NonNull Network network, @NonNull LinkProperties linkProperties) {
+            checkOnHandlerThread();
+
+            String existingIfName = mNetworkToInterface.get(network);
+            String newIfName = linkProperties.getInterfaceName();
+            if (Objects.equals(existingIfName, newIfName)) {
+                return;
+            }
+            Log.i(TAG, "Upstream network changed: " + existingIfName + " -> " + newIfName);
+            mNetworkToInterface.put(network, newIfName);
+
+            // TODO: disable border routing if netIfName is null
+            if (network.equals(mUpstreamNetwork)) {
+                enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+            }
+        }
+    }
+
+    private final class ThreadNetworkCallback extends ConnectivityManager.NetworkCallback {
+        @Override
+        public void onAvailable(@NonNull Network network) {
+            checkOnHandlerThread();
+            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
+        public void onLocalNetworkInfoChanged(
+                @NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
+            checkOnHandlerThread();
+            Log.i(
+                    TAG,
+                    "LocalNetworkInfo of Thread network changed: {threadNetwork: "
+                            + network
+                            + ", localNetworkInfo: "
+                            + localNetworkInfo
+                            + "}");
+            if (localNetworkInfo.getUpstreamNetwork() == null) {
+                disableBorderRouting();
+                return;
+            }
+            if (!localNetworkInfo.getUpstreamNetwork().equals(mUpstreamNetwork)) {
+                mUpstreamNetwork = localNetworkInfo.getUpstreamNetwork();
+                if (mNetworkToInterface.containsKey(mUpstreamNetwork)) {
+                    enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+                }
+                mNsdPublisher.setNetworkForHostResolution(mUpstreamNetwork);
+            }
+        }
+    }
+
+    private void requestThreadNetwork() {
+        mConnectivityManager.registerNetworkCallback(
+                new NetworkRequest.Builder()
+                        // clearCapabilities() is needed to remove forbidden capabilities and UID
+                        // requirement.
+                        .clearCapabilities()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+                        .build(),
+                new ThreadNetworkCallback(),
+                mHandler);
+    }
+
+    /** Injects a {@link NetworkAgent} for testing. */
+    @VisibleForTesting
+    void setTestNetworkAgent(@Nullable NetworkAgent testNetworkAgent) {
+        mTestNetworkAgent = testNetworkAgent;
+    }
+
+    private NetworkAgent newNetworkAgent() {
+        if (mTestNetworkAgent != null) {
+            return mTestNetworkAgent;
+        }
+
+        final NetworkCapabilities netCaps =
+                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 =
+                new NetworkScore.Builder()
+                        .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build();
+        return new NetworkAgent(
+                mContext,
+                mHandler.getLooper(),
+                TAG,
+                netCaps,
+                mTunIfController.getLinkProperties(),
+                newLocalNetworkConfig(),
+                score,
+                new NetworkAgentConfig.Builder().build(),
+                mNetworkProvider) {};
+    }
+
+    private void registerThreadNetwork() {
+        if (mNetworkAgent != null) {
+            return;
+        }
+
+        mNetworkAgent = newNetworkAgent();
+        mNetworkAgent.register();
+        mNetworkAgent.markConnected();
+        Log.i(TAG, "Registered Thread network");
+    }
+
+    private void unregisterThreadNetwork() {
+        if (mNetworkAgent == null) {
+            // unregisterThreadNetwork can be called every time this device becomes detached or
+            // disabled and the mNetworkAgent may not be created in this cases
+            return;
+        }
+
+        Log.d(TAG, "Unregistering Thread network agent");
+
+        mNetworkAgent.unregister();
+        mNetworkAgent = null;
+    }
+
+    @Override
+    public int getThreadVersion() {
+        return THREAD_VERSION_1_3;
+    }
+
+    @Override
+    public void createRandomizedDataset(
+            String networkName, IActiveOperationalDatasetReceiver receiver) {
+        ActiveOperationalDatasetReceiverWrapper receiverWrapper =
+                new ActiveOperationalDatasetReceiverWrapper(receiver);
+        mHandler.post(() -> createRandomizedDatasetInternal(networkName, receiverWrapper));
+    }
+
+    private void createRandomizedDatasetInternal(
+            String networkName, @NonNull ActiveOperationalDatasetReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
+        } catch (RemoteException | ThreadNetworkException e) {
+            Log.e(TAG, "otDaemon.getChannelMasks failed", e);
+            receiver.onError(e);
+        }
+    }
+
+    private IChannelMasksReceiver newChannelMasksReceiver(
+            String networkName, ActiveOperationalDatasetReceiverWrapper receiver) {
+        return new IChannelMasksReceiver.Stub() {
+            @Override
+            public void onSuccess(int supportedChannelMask, int preferredChannelMask) {
+                ActiveOperationalDataset dataset =
+                        createRandomizedDataset(
+                                networkName,
+                                supportedChannelMask,
+                                preferredChannelMask,
+                                new Random(),
+                                new SecureRandom());
+
+                receiver.onSuccess(dataset);
+            }
+
+            @Override
+            public void onError(int errorCode, String errorMessage) {
+                receiver.onError(otErrorToAndroidError(errorCode), errorMessage);
+            }
+        };
+    }
+
+    private static ActiveOperationalDataset createRandomizedDataset(
+            String networkName,
+            int supportedChannelMask,
+            int preferredChannelMask,
+            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;
+
+        final SparseArray<byte[]> channelMask = new SparseArray<>(1);
+        channelMask.put(CHANNEL_PAGE_24_GHZ, channelMaskToByteArray(supportedChannelMask));
+        final int channel = selectChannel(supportedChannelMask, preferredChannelMask, random);
+
+        final byte[] securityFlags = new byte[] {(byte) 0xff, (byte) 0xf8};
+
+        return new ActiveOperationalDataset.Builder()
+                .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(now, authoritative))
+                .setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
+                .setPanId(panId)
+                .setNetworkName(networkName)
+                .setChannel(CHANNEL_PAGE_24_GHZ, channel)
+                .setChannelMask(channelMask)
+                .setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
+                .setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
+                .setMeshLocalPrefix(meshLocalPrefix)
+                .setSecurityPolicy(new SecurityPolicy(DEFAULT_ROTATION_TIME_HOURS, securityFlags))
+                .build();
+    }
+
+    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;
+        if (preferredChannelMask == 0) {
+            preferredChannelMask = supportedChannelMask;
+        }
+
+        return selectRandomChannel(preferredChannelMask, random);
+    }
+
+    private static byte[] newRandomBytes(Random random, int length) {
+        byte[] result = new byte[length];
+        random.nextBytes(result);
+        return result;
+    }
+
+    private static byte[] channelMaskToByteArray(int channelMask) {
+        // Per Thread spec, a Channel Mask is:
+        // A variable-length bit mask that identifies the channels within the channel page
+        // (1 = selected, 0 = unselected). The channels are represented in most significant bit
+        // order. For example, the most significant bit of the left-most byte indicates channel 0.
+        // If channel 0 and channel 10 are selected, the mask would be: 80 20 00 00. For IEEE
+        // 802.15.4-2006 2.4 GHz PHY, the ChannelMask is 27 bits and MaskLength is 4.
+        //
+        // The pass-in channelMask represents a channel K by (channelMask & (1 << K)), so here
+        // needs to do bit-wise reverse to convert it to the Thread spec format in bytes.
+        channelMask = Integer.reverse(channelMask);
+        return new byte[] {
+            (byte) (channelMask >>> 24),
+            (byte) (channelMask >>> 16),
+            (byte) (channelMask >>> 8),
+            (byte) channelMask
+        };
+    }
+
+    private static int selectRandomChannel(int supportedChannelMask, Random random) {
+        int num = random.nextInt(Integer.bitCount(supportedChannelMask));
+        for (int i = 0; i < 32; i++) {
+            if ((supportedChannelMask & 1) == 1 && (num--) == 0) {
+                return i;
+            }
+            supportedChannelMask >>>= 1;
+        }
+        return -1;
+    }
+
+    private void enforceAllPermissionsGranted(String... permissions) {
+        for (String permission : permissions) {
+            mContext.enforceCallingOrSelfPermission(
+                    permission, "Permission " + permission + " is missing");
+        }
+    }
+
+    @Override
+    public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
+        enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+        mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
+    }
+
+    @Override
+    public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
+        enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+        mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
+    }
+
+    @Override
+    public void registerOperationalDatasetCallback(IOperationalDatasetCallback callback)
+            throws RemoteException {
+        enforceAllPermissionsGranted(
+                permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+        mHandler.post(() -> mOtDaemonCallbackProxy.registerDatasetCallback(callback));
+    }
+
+    @Override
+    public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
+            throws RemoteException {
+        enforceAllPermissionsGranted(
+                permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+        mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
+    }
+
+    private void checkOnHandlerThread() {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            throw new IllegalStateException(
+                    "Not running on ThreadNetworkControllerService thread ("
+                            + mHandler.getLooper()
+                            + ") : "
+                            + Looper.myLooper());
+        }
+    }
+
+    private IOtStatusReceiver newOtStatusReceiver(OperationReceiverWrapper receiver) {
+        return new IOtStatusReceiver.Stub() {
+            @Override
+            public void onSuccess() {
+                receiver.onSuccess();
+            }
+
+            @Override
+            public void onError(int otError, String message) {
+                receiver.onError(otErrorToAndroidError(otError), message);
+            }
+        };
+    }
+
+    @ErrorCode
+    private static int otErrorToAndroidError(int otError) {
+        // See external/openthread/include/openthread/error.h for OT error definition
+        switch (otError) {
+            case OT_ERROR_ABORT:
+                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:
+                return ERROR_RESPONSE_BAD_FORMAT;
+            case OT_ERROR_REASSEMBLY_TIMEOUT:
+            case OT_ERROR_RESPONSE_TIMEOUT:
+                return ERROR_TIMEOUT;
+            case OT_ERROR_REJECTED:
+                return ERROR_REJECTED_BY_PEER;
+            case OT_ERROR_UNSUPPORTED_CHANNEL:
+                return ERROR_UNSUPPORTED_CHANNEL;
+            case OT_ERROR_THREAD_DISABLED:
+                return ERROR_THREAD_DISABLED;
+            case OT_ERROR_FAILED_PRECONDITION:
+                return ERROR_FAILED_PRECONDITION;
+            case OT_ERROR_INVALID_STATE:
+            default:
+                return ERROR_INTERNAL_ERROR;
+        }
+    }
+
+    @Override
+    public void join(
+            @NonNull ActiveOperationalDataset activeDataset, @NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+        mHandler.post(() -> joinInternal(activeDataset, receiverWrapper));
+    }
+
+    private void joinInternal(
+            @NonNull ActiveOperationalDataset activeDataset,
+            @NonNull OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            // The otDaemon.join() will leave first if this device is currently attached
+            getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
+        } catch (RemoteException | ThreadNetworkException e) {
+            Log.e(TAG, "otDaemon.join failed", e);
+            receiver.onError(e);
+        }
+    }
+
+    @Override
+    public void scheduleMigration(
+            @NonNull PendingOperationalDataset pendingDataset,
+            @NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+        mHandler.post(() -> scheduleMigrationInternal(pendingDataset, receiverWrapper));
+    }
+
+    public void scheduleMigrationInternal(
+            @NonNull PendingOperationalDataset pendingDataset,
+            @NonNull OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            getOtDaemon()
+                    .scheduleMigration(
+                            pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
+        } catch (RemoteException | ThreadNetworkException e) {
+            Log.e(TAG, "otDaemon.scheduleMigration failed", e);
+            receiver.onError(e);
+        }
+    }
+
+    @Override
+    public void leave(@NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
+    }
+
+    private void leaveInternal(@NonNull OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            getOtDaemon().leave(newOtStatusReceiver(receiver));
+        } catch (RemoteException | ThreadNetworkException e) {
+            Log.e(TAG, "otDaemon.leave failed", e);
+            receiver.onError(e);
+        }
+    }
+
+    /**
+     * Sets the country code.
+     *
+     * @param countryCode 2 characters string country code (as defined in ISO 3166) to set.
+     * @param receiver the receiver to receive result of this operation
+     */
+    @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+    public void setCountryCode(@NonNull String countryCode, @NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+        mHandler.post(() -> setCountryCodeInternal(countryCode, receiverWrapper));
+    }
+
+    private void setCountryCodeInternal(
+            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 | ThreadNetworkException e) {
+            Log.e(TAG, "otDaemon.setCountryCode failed", e);
+            receiver.onError(e);
+        }
+    }
+
+    @Override
+    public void setTestNetworkAsUpstream(
+            @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED, NETWORK_SETTINGS);
+
+        Log.i(TAG, "setTestNetworkAsUpstream: " + testNetworkInterfaceName);
+        mHandler.post(() -> setTestNetworkAsUpstreamInternal(testNetworkInterfaceName, receiver));
+    }
+
+    private void setTestNetworkAsUpstreamInternal(
+            @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
+        checkOnHandlerThread();
+
+        TestNetworkSpecifier testNetworkSpecifier = null;
+        if (testNetworkInterfaceName != null) {
+            testNetworkSpecifier = new TestNetworkSpecifier(testNetworkInterfaceName);
+        }
+
+        if (!Objects.equals(mUpstreamTestNetworkSpecifier, testNetworkSpecifier)) {
+            cancelRequestUpstreamNetwork();
+            mUpstreamTestNetworkSpecifier = testNetworkSpecifier;
+            mUpstreamNetworkRequest = newUpstreamNetworkRequest();
+            requestUpstreamNetwork();
+            sendLocalNetworkConfig();
+        }
+        try {
+            receiver.onSuccess();
+        } catch (RemoteException ignored) {
+            // do nothing if the client is dead
+        }
+    }
+
+    @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 enableBorderRouting(String infraIfName) {
+        if (mBorderRouterConfig.isBorderRoutingEnabled
+                && infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
+            return;
+        }
+        Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
+        try {
+            mBorderRouterConfig.infraInterfaceName = infraIfName;
+            mBorderRouterConfig.infraInterfaceIcmp6Socket =
+                    mInfraIfController.createIcmp6Socket(infraIfName);
+            mBorderRouterConfig.isBorderRoutingEnabled = true;
+
+            getOtDaemon()
+                    .configureBorderRouter(
+                            mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
+        } catch (RemoteException | IOException | ThreadNetworkException e) {
+            Log.w(TAG, "Failed to enable border routing", e);
+        }
+    }
+
+    private void disableBorderRouting() {
+        mUpstreamNetwork = null;
+        mBorderRouterConfig.infraInterfaceName = null;
+        mBorderRouterConfig.infraInterfaceIcmp6Socket = null;
+        mBorderRouterConfig.isBorderRoutingEnabled = false;
+        try {
+            getOtDaemon()
+                    .configureBorderRouter(
+                            mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
+        } catch (RemoteException | ThreadNetworkException e) {
+            Log.w(TAG, "Failed to disable border routing", e);
+        }
+    }
+
+    private void handleThreadInterfaceStateChanged(boolean isUp) {
+        try {
+            mTunIfController.setInterfaceUp(isUp);
+            Log.i(TAG, "Thread TUN interface becomes " + (isUp ? "up" : "down"));
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to handle Thread interface state changes", e);
+        }
+    }
+
+    private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
+        if (ThreadNetworkController.isAttached(deviceRole)) {
+            Log.i(TAG, "Attached to the Thread network");
+
+            // This is an idempotent method which can be called for multiple times when the device
+            // is already attached (e.g. going from Child to Router)
+            registerThreadNetwork();
+        } else {
+            Log.i(TAG, "Detached from the Thread network");
+
+            // This is an idempotent method which can be called for multiple times when the device
+            // is already detached or stopped
+            unregisterThreadNetwork();
+        }
+    }
+
+    private void handleAddressChanged(List<Ipv6AddressInfo> addressInfoList) {
+        checkOnHandlerThread();
+
+        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
+        if (mNetworkAgent != null) {
+            mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
+        }
+    }
+
+    private void sendLocalNetworkConfig() {
+        if (mNetworkAgent == null) {
+            return;
+        }
+        final LocalNetworkConfig localNetworkConfig = newLocalNetworkConfig();
+        mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
+        Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
+    }
+
+    private void handleMulticastForwardingChanged(BackboneRouterState state) {
+        MulticastRoutingConfig upstreamMulticastRoutingConfig;
+        MulticastRoutingConfig downstreamMulticastRoutingConfig;
+
+        if (state.multicastForwardingEnabled) {
+            // When multicast forwarding is enabled, setup upstream forwarding to any address
+            // with minimal scope 4
+            // setup downstream forwarding with addresses subscribed from Thread network
+            upstreamMulticastRoutingConfig =
+                    new MulticastRoutingConfig.Builder(FORWARD_WITH_MIN_SCOPE, 4).build();
+            downstreamMulticastRoutingConfig =
+                    buildDownstreamMulticastRoutingConfigSelected(state.listeningAddresses);
+        } else {
+            // When multicast forwarding is disabled, set both upstream and downstream
+            // forwarding config to FORWARD_NONE.
+            upstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+            downstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+        }
+
+        if (upstreamMulticastRoutingConfig.equals(mUpstreamMulticastRoutingConfig)
+                && downstreamMulticastRoutingConfig.equals(mDownstreamMulticastRoutingConfig)) {
+            return;
+        }
+
+        mUpstreamMulticastRoutingConfig = upstreamMulticastRoutingConfig;
+        mDownstreamMulticastRoutingConfig = downstreamMulticastRoutingConfig;
+        sendLocalNetworkConfig();
+    }
+
+    private MulticastRoutingConfig buildDownstreamMulticastRoutingConfigSelected(
+            List<String> listeningAddresses) {
+        MulticastRoutingConfig.Builder builder =
+                new MulticastRoutingConfig.Builder(FORWARD_SELECTED);
+        for (String addressStr : listeningAddresses) {
+            Inet6Address address = (Inet6Address) InetAddresses.parseNumericAddress(addressStr);
+            builder.addListeningAddress(address);
+        }
+        return builder.build();
+    }
+
+    private static final class CallbackMetadata {
+        private static long gId = 0;
+
+        // The unique ID
+        final long id;
+
+        final IBinder.DeathRecipient deathRecipient;
+
+        CallbackMetadata(IBinder.DeathRecipient deathRecipient) {
+            this.id = allocId();
+            this.deathRecipient = deathRecipient;
+        }
+
+        private static long allocId() {
+            if (gId == Long.MAX_VALUE) {
+                gId = 0;
+            }
+            return gId++;
+        }
+    }
+
+    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}.
+     */
+    private final class OtDaemonCallbackProxy extends IOtDaemonCallback.Stub {
+        private final Map<IStateCallback, CallbackMetadata> mStateCallbacks = new HashMap<>();
+        private final Map<IOperationalDatasetCallback, CallbackMetadata> mOpDatasetCallbacks =
+                new HashMap<>();
+
+        private OtDaemonState mState;
+        private ActiveOperationalDataset mActiveDataset;
+        private PendingOperationalDataset mPendingDataset;
+
+        public void registerStateCallback(IStateCallback callback) {
+            checkOnHandlerThread();
+            if (mStateCallbacks.containsKey(callback)) {
+                throw new IllegalStateException("Registering the same IStateCallback twice");
+            }
+
+            IBinder.DeathRecipient deathRecipient =
+                    () -> mHandler.post(() -> unregisterStateCallback(callback));
+            CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+            mStateCallbacks.put(callback, callbackMetadata);
+            try {
+                callback.asBinder().linkToDeath(deathRecipient, 0);
+            } catch (RemoteException e) {
+                mStateCallbacks.remove(callback);
+                // This is thrown when the client is dead, do nothing
+            }
+
+            try {
+                getOtDaemon().registerStateCallback(this, callbackMetadata.id);
+            } catch (RemoteException | ThreadNetworkException e) {
+                Log.e(TAG, "otDaemon.registerStateCallback failed", e);
+            }
+        }
+
+        public void unregisterStateCallback(IStateCallback callback) {
+            checkOnHandlerThread();
+            if (!mStateCallbacks.containsKey(callback)) {
+                return;
+            }
+            callback.asBinder().unlinkToDeath(mStateCallbacks.remove(callback).deathRecipient, 0);
+        }
+
+        public void registerDatasetCallback(IOperationalDatasetCallback callback) {
+            checkOnHandlerThread();
+            if (mOpDatasetCallbacks.containsKey(callback)) {
+                throw new IllegalStateException(
+                        "Registering the same IOperationalDatasetCallback twice");
+            }
+
+            IBinder.DeathRecipient deathRecipient =
+                    () -> mHandler.post(() -> unregisterDatasetCallback(callback));
+            CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+            mOpDatasetCallbacks.put(callback, callbackMetadata);
+            try {
+                callback.asBinder().linkToDeath(deathRecipient, 0);
+            } catch (RemoteException e) {
+                mOpDatasetCallbacks.remove(callback);
+            }
+
+            try {
+                getOtDaemon().registerStateCallback(this, callbackMetadata.id);
+            } catch (RemoteException | ThreadNetworkException e) {
+                Log.e(TAG, "otDaemon.registerStateCallback failed", e);
+            }
+        }
+
+        public void unregisterDatasetCallback(IOperationalDatasetCallback callback) {
+            checkOnHandlerThread();
+            if (!mOpDatasetCallbacks.containsKey(callback)) {
+                return;
+            }
+            callback.asBinder()
+                    .unlinkToDeath(mOpDatasetCallbacks.remove(callback).deathRecipient, 0);
+        }
+
+        public void onOtDaemonDied() {
+            checkOnHandlerThread();
+            if (mState == null) {
+                return;
+            }
+
+            final int deviceRole = mState.deviceRole;
+            mState = null;
+
+            // If this device is already STOPPED or DETACHED, do nothing
+            if (!ThreadNetworkController.isAttached(deviceRole)) {
+                return;
+            }
+
+            // The Thread device role is considered DETACHED when the OT daemon process is dead
+            handleDeviceRoleChanged(DEVICE_ROLE_DETACHED);
+            for (IStateCallback callback : mStateCallbacks.keySet()) {
+                try {
+                    callback.onDeviceRoleChanged(DEVICE_ROLE_DETACHED);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        private void onThreadEnabledChanged(int state, long listenerId) {
+            checkOnHandlerThread();
+            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
+                }
+            }
+        }
+
+        private static int otStateToAndroidState(int state) {
+            switch (state) {
+                case OT_STATE_ENABLED:
+                    return STATE_ENABLED;
+                case OT_STATE_DISABLED:
+                    return STATE_DISABLED;
+                case OT_STATE_DISABLING:
+                    return STATE_DISABLING;
+                default:
+                    throw new IllegalArgumentException("Unknown ot state " + state);
+            }
+        }
+
+        @Override
+        public void onStateChanged(OtDaemonState newState, long listenerId) {
+            mHandler.post(() -> onStateChangedInternal(newState, listenerId));
+        }
+
+        private void onStateChangedInternal(OtDaemonState newState, long listenerId) {
+            checkOnHandlerThread();
+            onInterfaceStateChanged(newState.isInterfaceUp);
+            onDeviceRoleChanged(newState.deviceRole, listenerId);
+            onPartitionIdChanged(newState.partitionId, listenerId);
+            onThreadEnabledChanged(newState.threadEnabled, listenerId);
+            mState = newState;
+
+            ActiveOperationalDataset newActiveDataset;
+            try {
+                if (newState.activeDatasetTlvs.length != 0) {
+                    newActiveDataset =
+                            ActiveOperationalDataset.fromThreadTlvs(newState.activeDatasetTlvs);
+                } else {
+                    newActiveDataset = null;
+                }
+                onActiveOperationalDatasetChanged(newActiveDataset, listenerId);
+                mActiveDataset = newActiveDataset;
+            } catch (IllegalArgumentException e) {
+                // Is unlikely that OT will generate invalid Operational Dataset
+                Log.wtf(TAG, "Invalid Active Operational Dataset from OpenThread", e);
+            }
+
+            PendingOperationalDataset newPendingDataset;
+            try {
+                if (newState.pendingDatasetTlvs.length != 0) {
+                    newPendingDataset =
+                            PendingOperationalDataset.fromThreadTlvs(newState.pendingDatasetTlvs);
+                } else {
+                    newPendingDataset = null;
+                }
+                onPendingOperationalDatasetChanged(newPendingDataset, listenerId);
+                mPendingDataset = newPendingDataset;
+            } catch (IllegalArgumentException e) {
+                // Is unlikely that OT will generate invalid Operational Dataset
+                Log.wtf(TAG, "Invalid Pending Operational Dataset from OpenThread", e);
+            }
+        }
+
+        private void onInterfaceStateChanged(boolean isUp) {
+            checkOnHandlerThread();
+            if (mState == null || mState.isInterfaceUp != isUp) {
+                handleThreadInterfaceStateChanged(isUp);
+            }
+        }
+
+        private void onDeviceRoleChanged(@DeviceRole int deviceRole, long listenerId) {
+            checkOnHandlerThread();
+            boolean hasChange = (mState == null || mState.deviceRole != deviceRole);
+            if (hasChange) {
+                handleDeviceRoleChanged(deviceRole);
+            }
+
+            for (var callbackEntry : mStateCallbacks.entrySet()) {
+                if (!hasChange && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                try {
+                    callbackEntry.getKey().onDeviceRoleChanged(deviceRole);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        private void onPartitionIdChanged(long partitionId, long listenerId) {
+            checkOnHandlerThread();
+            boolean hasChange = (mState == null || mState.partitionId != partitionId);
+
+            for (var callbackEntry : mStateCallbacks.entrySet()) {
+                if (!hasChange && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                try {
+                    callbackEntry.getKey().onPartitionIdChanged(partitionId);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        private void onActiveOperationalDatasetChanged(
+                ActiveOperationalDataset activeDataset, long listenerId) {
+            checkOnHandlerThread();
+            boolean hasChange = !Objects.equals(mActiveDataset, activeDataset);
+
+            for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
+                if (!hasChange && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                try {
+                    callbackEntry.getKey().onActiveOperationalDatasetChanged(activeDataset);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        private void onPendingOperationalDatasetChanged(
+                PendingOperationalDataset pendingDataset, long listenerId) {
+            checkOnHandlerThread();
+            boolean hasChange = !Objects.equals(mPendingDataset, pendingDataset);
+            for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
+                if (!hasChange && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                try {
+                    callbackEntry.getKey().onPendingOperationalDatasetChanged(pendingDataset);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        @Override
+        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
new file mode 100644
index 0000000..a194114
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -0,0 +1,608 @@
+/*
+ * 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.thread;
+
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_COUNTRY_CODE;
+
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationManager;
+import android.net.thread.IOperationReceiver;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
+import android.os.Build;
+import android.sysprop.ThreadNetworkProperties;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.ConnectivityResources;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Provide functions for making changes to Thread Network country code. This Country Code is from
+ * location, WiFi, telephony or OEM configuration. This class sends Country Code to Thread Network
+ * native layer.
+ *
+ * <p>This class is thread-safe.
+ */
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class ThreadNetworkCountryCode {
+    private static final String TAG = "ThreadNetworkCountryCode";
+    // To be used when there is no country code available.
+    @VisibleForTesting public static final String DEFAULT_COUNTRY_CODE = "WW";
+
+    // Wait 1 hour between updates.
+    private static final long TIME_BETWEEN_LOCATION_UPDATES_MS = 1000L * 60 * 60 * 1;
+    // Minimum distance before an update is triggered, in meters. We don't need this to be too
+    // exact because all we care about is what country the user is in.
+    private static final float DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS = 5_000.0f;
+
+    /** List of country code sources. */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(
+            prefix = "COUNTRY_CODE_SOURCE_",
+            value = {
+                COUNTRY_CODE_SOURCE_DEFAULT,
+                COUNTRY_CODE_SOURCE_LOCATION,
+                COUNTRY_CODE_SOURCE_OEM,
+                COUNTRY_CODE_SOURCE_OVERRIDE,
+                COUNTRY_CODE_SOURCE_TELEPHONY,
+                COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
+                COUNTRY_CODE_SOURCE_WIFI,
+                COUNTRY_CODE_SOURCE_SETTINGS,
+            })
+    private @interface CountryCodeSource {}
+
+    private static final String COUNTRY_CODE_SOURCE_DEFAULT = "Default";
+    private static final String COUNTRY_CODE_SOURCE_LOCATION = "Location";
+    private static final String COUNTRY_CODE_SOURCE_OEM = "Oem";
+    private static final String COUNTRY_CODE_SOURCE_OVERRIDE = "Override";
+    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);
+
+    private final ConnectivityResources mResources;
+    private final Context mContext;
+    private final LocationManager mLocationManager;
+    @Nullable private final Geocoder mGeocoder;
+    private final ThreadNetworkControllerService mThreadNetworkControllerService;
+    private final WifiManager mWifiManager;
+    private final TelephonyManager mTelephonyManager;
+    private final SubscriptionManager mSubscriptionManager;
+    private final Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeSlotInfoMap =
+            new ArrayMap();
+    private final ThreadPersistentSettings mPersistentSettings;
+
+    @Nullable private CountryCodeInfo mCurrentCountryCodeInfo;
+    @Nullable private CountryCodeInfo mLocationCountryCodeInfo;
+    @Nullable private CountryCodeInfo mOverrideCountryCodeInfo;
+    @Nullable private CountryCodeInfo mWifiCountryCodeInfo;
+    @Nullable private CountryCodeInfo mTelephonyCountryCodeInfo;
+    @Nullable private CountryCodeInfo mTelephonyLastCountryCodeInfo;
+    @Nullable private CountryCodeInfo mOemCountryCodeInfo;
+
+    /** Container class to store Thread country code information. */
+    private static final class CountryCodeInfo {
+        private String mCountryCode;
+        @CountryCodeSource private String mSource;
+        private final Instant mUpdatedTimestamp;
+
+        /**
+         * Constructs a new {@code CountryCodeInfo} from the given country code, country code source
+         * and country coode created time.
+         *
+         * @param countryCode a String representation of the country code as defined in ISO 3166.
+         * @param countryCodeSource a String representation of country code source.
+         * @param instant a Instant representation of the time when the country code was created.
+         * @throws IllegalArgumentException if {@code countryCode} contains invalid country code.
+         */
+        public CountryCodeInfo(
+                String countryCode, @CountryCodeSource String countryCodeSource, Instant instant) {
+            if (!isValidCountryCode(countryCode)) {
+                throw new IllegalArgumentException("Country code is invalid: " + countryCode);
+            }
+
+            mCountryCode = countryCode;
+            mSource = countryCodeSource;
+            mUpdatedTimestamp = instant;
+        }
+
+        /**
+         * Constructs a new {@code CountryCodeInfo} from the given country code, country code
+         * source. The updated timestamp of the country code will be set to the time when {@code
+         * CountryCodeInfo} was constructed.
+         *
+         * @param countryCode a String representation of the country code as defined in ISO 3166.
+         * @param countryCodeSource a String representation of country code source.
+         * @throws IllegalArgumentException if {@code countryCode} contains invalid country code.
+         */
+        public CountryCodeInfo(String countryCode, @CountryCodeSource String countryCodeSource) {
+            this(countryCode, countryCodeSource, Instant.now());
+        }
+
+        public String getCountryCode() {
+            return mCountryCode;
+        }
+
+        public boolean isCountryCodeMatch(CountryCodeInfo countryCodeInfo) {
+            if (countryCodeInfo == null) {
+                return false;
+            }
+
+            return Objects.equals(countryCodeInfo.mCountryCode, mCountryCode);
+        }
+
+        @Override
+        public String toString() {
+            return "CountryCodeInfo{ mCountryCode: "
+                    + mCountryCode
+                    + ", mSource: "
+                    + mSource
+                    + ", mUpdatedTimestamp: "
+                    + mUpdatedTimestamp
+                    + "}";
+        }
+    }
+
+    /** Container class to store country code per SIM slot. */
+    private static final class TelephonyCountryCodeSlotInfo {
+        public int slotIndex;
+        public String countryCode;
+        public String lastKnownCountryCode;
+        public Instant timestamp;
+
+        @Override
+        public String toString() {
+            return "TelephonyCountryCodeSlotInfo{ slotIndex: "
+                    + slotIndex
+                    + ", countryCode: "
+                    + countryCode
+                    + ", lastKnownCountryCode: "
+                    + lastKnownCountryCode
+                    + ", timestamp: "
+                    + timestamp
+                    + "}";
+        }
+    }
+
+    private boolean isLocationUseForCountryCodeEnabled() {
+        return mResources
+                .get()
+                .getBoolean(R.bool.config_thread_location_use_for_country_code_enabled);
+    }
+
+    public ThreadNetworkCountryCode(
+            LocationManager locationManager,
+            ThreadNetworkControllerService threadNetworkControllerService,
+            @Nullable Geocoder geocoder,
+            ConnectivityResources resources,
+            WifiManager wifiManager,
+            Context context,
+            TelephonyManager telephonyManager,
+            SubscriptionManager subscriptionManager,
+            @Nullable String oemCountryCode,
+            ThreadPersistentSettings persistentSettings) {
+        mLocationManager = locationManager;
+        mThreadNetworkControllerService = threadNetworkControllerService;
+        mGeocoder = geocoder;
+        mResources = resources;
+        mWifiManager = wifiManager;
+        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,
+            ThreadPersistentSettings persistentSettings) {
+        return new ThreadNetworkCountryCode(
+                context.getSystemService(LocationManager.class),
+                controllerService,
+                Geocoder.isPresent() ? new Geocoder(context) : null,
+                new ConnectivityResources(context),
+                context.getSystemService(WifiManager.class),
+                context,
+                context.getSystemService(TelephonyManager.class),
+                context.getSystemService(SubscriptionManager.class),
+                ThreadNetworkProperties.country_code().orElse(null),
+                persistentSettings);
+    }
+
+    /** Sets up this country code module to listen to location country code changes. */
+    public synchronized void initialize() {
+        registerGeocoderCountryCodeCallback();
+        registerWifiCountryCodeCallback();
+        registerTelephonyCountryCodeCallback();
+        updateTelephonyCountryCodeFromSimCard();
+        updateCountryCode(false /* forceUpdate */);
+    }
+
+    private synchronized void registerGeocoderCountryCodeCallback() {
+        if ((mGeocoder != null) && isLocationUseForCountryCodeEnabled()) {
+            mLocationManager.requestLocationUpdates(
+                    LocationManager.PASSIVE_PROVIDER,
+                    TIME_BETWEEN_LOCATION_UPDATES_MS,
+                    DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS,
+                    location -> setCountryCodeFromGeocodingLocation(location));
+        }
+    }
+
+    private synchronized void geocodeListener(List<Address> addresses) {
+        if (addresses != null && !addresses.isEmpty()) {
+            String countryCode = addresses.get(0).getCountryCode();
+
+            if (isValidCountryCode(countryCode)) {
+                Log.d(TAG, "Set location country code to: " + countryCode);
+                mLocationCountryCodeInfo =
+                        new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_LOCATION);
+            } else {
+                Log.d(TAG, "Received invalid location country code");
+                mLocationCountryCodeInfo = null;
+            }
+
+            updateCountryCode(false /* forceUpdate */);
+        }
+    }
+
+    private synchronized void setCountryCodeFromGeocodingLocation(@Nullable Location location) {
+        if ((location == null) || (mGeocoder == null)) return;
+
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
+            Log.wtf(
+                    TAG,
+                    "Unexpected call to set country code from the Geocoding location, "
+                            + "Thread code never runs under T or lower.");
+            return;
+        }
+
+        mGeocoder.getFromLocation(
+                location.getLatitude(),
+                location.getLongitude(),
+                1 /* maxResults */,
+                this::geocodeListener);
+    }
+
+    private synchronized void registerWifiCountryCodeCallback() {
+        if (mWifiManager != null) {
+            mWifiManager.registerActiveCountryCodeChangedCallback(
+                    r -> r.run(), new WifiCountryCodeCallback());
+        }
+    }
+
+    private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback {
+        @Override
+        public void onActiveCountryCodeChanged(String countryCode) {
+            Log.d(TAG, "Wifi country code is changed to " + countryCode);
+            synchronized ("ThreadNetworkCountryCode.this") {
+                if (isValidCountryCode(countryCode)) {
+                    mWifiCountryCodeInfo =
+                            new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_WIFI);
+                } else {
+                    Log.w(TAG, "WiFi country code " + countryCode + " is invalid");
+                    mWifiCountryCodeInfo = null;
+                }
+
+                updateCountryCode(false /* forceUpdate */);
+            }
+        }
+
+        @Override
+        public void onCountryCodeInactive() {
+            Log.d(TAG, "Wifi country code is inactived");
+            synchronized ("ThreadNetworkCountryCode.this") {
+                mWifiCountryCodeInfo = null;
+                updateCountryCode(false /* forceUpdate */);
+            }
+        }
+    }
+
+    private synchronized void registerTelephonyCountryCodeCallback() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            Log.wtf(
+                    TAG,
+                    "Unexpected call to register the telephony country code changed callback, "
+                            + "Thread code never runs under T or lower.");
+            return;
+        }
+
+        BroadcastReceiver broadcastReceiver =
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        int slotIndex =
+                                intent.getIntExtra(
+                                        SubscriptionManager.EXTRA_SLOT_INDEX,
+                                        SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+                        String lastKnownCountryCode = null;
+                        String countryCode =
+                                intent.getStringExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY);
+
+                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+                            lastKnownCountryCode =
+                                    intent.getStringExtra(
+                                            TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY);
+                        }
+
+                        setTelephonyCountryCodeAndLastKnownCountryCode(
+                                slotIndex, countryCode, lastKnownCountryCode);
+                    }
+                };
+
+        mContext.registerReceiver(
+                broadcastReceiver,
+                new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED),
+                Context.RECEIVER_EXPORTED);
+    }
+
+    private synchronized void updateTelephonyCountryCodeFromSimCard() {
+        List<SubscriptionInfo> subscriptionInfoList =
+                mSubscriptionManager.getActiveSubscriptionInfoList();
+
+        if (subscriptionInfoList == null) {
+            Log.d(TAG, "No SIM card is found");
+            return;
+        }
+
+        for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
+            String countryCode;
+            int slotIndex;
+
+            slotIndex = subscriptionInfo.getSimSlotIndex();
+            try {
+                countryCode = mTelephonyManager.getNetworkCountryIso(slotIndex);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "Failed to get country code for slot index:" + slotIndex, e);
+                continue;
+            }
+
+            Log.d(TAG, "Telephony slot " + slotIndex + " country code is " + countryCode);
+            setTelephonyCountryCodeAndLastKnownCountryCode(
+                    slotIndex, countryCode, null /* lastKnownCountryCode */);
+        }
+    }
+
+    private synchronized void setTelephonyCountryCodeAndLastKnownCountryCode(
+            int slotIndex, String countryCode, String lastKnownCountryCode) {
+        Log.d(
+                TAG,
+                "Set telephony country code to: "
+                        + countryCode
+                        + ", last country code to: "
+                        + lastKnownCountryCode
+                        + " for slotIndex: "
+                        + slotIndex);
+
+        TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot =
+                mTelephonyCountryCodeSlotInfoMap.computeIfAbsent(
+                        slotIndex, k -> new TelephonyCountryCodeSlotInfo());
+        telephonyCountryCodeInfoSlot.slotIndex = slotIndex;
+        telephonyCountryCodeInfoSlot.timestamp = Instant.now();
+        telephonyCountryCodeInfoSlot.countryCode = countryCode;
+        telephonyCountryCodeInfoSlot.lastKnownCountryCode = lastKnownCountryCode;
+
+        mTelephonyCountryCodeInfo = null;
+        mTelephonyLastCountryCodeInfo = null;
+
+        for (TelephonyCountryCodeSlotInfo slotInfo : mTelephonyCountryCodeSlotInfoMap.values()) {
+            if ((mTelephonyCountryCodeInfo == null) && isValidCountryCode(slotInfo.countryCode)) {
+                mTelephonyCountryCodeInfo =
+                        new CountryCodeInfo(
+                                slotInfo.countryCode,
+                                COUNTRY_CODE_SOURCE_TELEPHONY,
+                                slotInfo.timestamp);
+            }
+
+            if ((mTelephonyLastCountryCodeInfo == null)
+                    && isValidCountryCode(slotInfo.lastKnownCountryCode)) {
+                mTelephonyLastCountryCodeInfo =
+                        new CountryCodeInfo(
+                                slotInfo.lastKnownCountryCode,
+                                COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
+                                slotInfo.timestamp);
+            }
+        }
+
+        updateCountryCode(false /* forceUpdate */);
+    }
+
+    /**
+     * Priority order of country code sources (we stop at the first known country code source):
+     *
+     * <ul>
+     *   <li>1. Override country code - Country code forced via shell command (local/automated
+     *       testing)
+     *   <li>2. Telephony country code - Current country code retrieved via cellular. If there are
+     *       multiple SIM's, the country code chosen is non-deterministic if they return different
+     *       codes. The first valid country code with the lowest slot number will be used.
+     *   <li>3. Wifi country code - Current country code retrieved via wifi (via 80211.ad).
+     *   <li>4. Last known telephony country code - Last known country code retrieved via cellular.
+     *       If there are multiple SIM's, the country code chosen is non-deterministic if they
+     *       return different codes. The first valid last known country code with the lowest slot
+     *       number will be used.
+     *   <li>5. Location country code - Country code retrieved from LocationManager passive location
+     *       provider.
+     *   <li>6. OEM country code - Country code retrieved from the system property
+     *       `threadnetwork.country_code`.
+     *   <li>7. Default country code `WW`.
+     * </ul>
+     *
+     * @return the selected country code information.
+     */
+    private CountryCodeInfo pickCountryCode() {
+        if (mOverrideCountryCodeInfo != null) {
+            return mOverrideCountryCodeInfo;
+        }
+
+        if (mTelephonyCountryCodeInfo != null) {
+            return mTelephonyCountryCodeInfo;
+        }
+
+        if (mWifiCountryCodeInfo != null) {
+            return mWifiCountryCodeInfo;
+        }
+
+        if (mTelephonyLastCountryCodeInfo != null) {
+            return mTelephonyLastCountryCodeInfo;
+        }
+
+        if (mLocationCountryCodeInfo != null) {
+            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;
+        }
+
+        return DEFAULT_COUNTRY_CODE_INFO;
+    }
+
+    private IOperationReceiver newOperationReceiver(CountryCodeInfo countryCodeInfo) {
+        return new IOperationReceiver.Stub() {
+            @Override
+            public void onSuccess() {
+                synchronized ("ThreadNetworkCountryCode.this") {
+                    mCurrentCountryCodeInfo = countryCodeInfo;
+                    mPersistentSettings.put(
+                            THREAD_COUNTRY_CODE.key, countryCodeInfo.getCountryCode());
+                }
+            }
+
+            @Override
+            public void onError(int otError, String message) {
+                Log.e(
+                        TAG,
+                        "Error "
+                                + otError
+                                + ": "
+                                + message
+                                + ". Failed to set country code "
+                                + countryCodeInfo);
+            }
+        };
+    }
+
+    /**
+     * Updates country code to the Thread native layer.
+     *
+     * @param forceUpdate Force update the country code even if it was the same as previously cached
+     *     value.
+     */
+    @VisibleForTesting
+    public synchronized void updateCountryCode(boolean forceUpdate) {
+        CountryCodeInfo countryCodeInfo = pickCountryCode();
+
+        if (!forceUpdate && countryCodeInfo.isCountryCodeMatch(mCurrentCountryCodeInfo)) {
+            Log.i(TAG, "Ignoring already set country code " + countryCodeInfo.getCountryCode());
+            return;
+        }
+
+        Log.i(TAG, "Set country code: " + countryCodeInfo);
+        mThreadNetworkControllerService.setCountryCode(
+                countryCodeInfo.getCountryCode().toUpperCase(Locale.ROOT),
+                newOperationReceiver(countryCodeInfo));
+    }
+
+    /** Returns the current country code. */
+    public synchronized String getCountryCode() {
+        return mCurrentCountryCodeInfo.getCountryCode();
+    }
+
+    /**
+     * Returns {@code true} if {@code countryCode} is a valid country code.
+     *
+     * <p>A country code is valid if it consists of 2 alphabets.
+     */
+    public static boolean isValidCountryCode(String countryCode) {
+        return countryCode != null
+                && countryCode.length() == 2
+                && countryCode.chars().allMatch(Character::isLetter);
+    }
+
+    /**
+     * Overrides any existing country code.
+     *
+     * @param countryCode A 2-Character alphabetical country code (as defined in ISO 3166).
+     * @throws IllegalArgumentException if {@code countryCode} is an invalid country code.
+     */
+    public synchronized void setOverrideCountryCode(String countryCode) {
+        if (!isValidCountryCode(countryCode)) {
+            throw new IllegalArgumentException("The override country code is invalid");
+        }
+
+        mOverrideCountryCodeInfo = new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_OVERRIDE);
+        updateCountryCode(true /* forceUpdate */);
+    }
+
+    /** Clears the country code previously set through {@link #setOverrideCountryCode} method. */
+    public synchronized void clearOverrideCountryCode() {
+        mOverrideCountryCodeInfo = null;
+        updateCountryCode(true /* forceUpdate */);
+    }
+
+    /** Dumps the current state of this ThreadNetworkCountryCode object. */
+    public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("---- Dump of ThreadNetworkCountryCode begin ----");
+        pw.println("mOverrideCountryCodeInfo        : " + mOverrideCountryCodeInfo);
+        pw.println("mTelephonyCountryCodeSlotInfoMap: " + mTelephonyCountryCodeSlotInfoMap);
+        pw.println("mTelephonyCountryCodeInfo       : " + mTelephonyCountryCodeInfo);
+        pw.println("mWifiCountryCodeInfo            : " + mWifiCountryCodeInfo);
+        pw.println("mTelephonyLastCountryCodeInfo   : " + mTelephonyLastCountryCodeInfo);
+        pw.println("mLocationCountryCodeInfo        : " + mLocationCountryCodeInfo);
+        pw.println("mOemCountryCodeInfo             : " + mOemCountryCodeInfo);
+        pw.println("mCurrentCountryCodeInfo         : " + mCurrentCountryCodeInfo);
+        pw.println("---- Dump of ThreadNetworkCountryCode end ------");
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
new file mode 100644
index 0000000..4c22278
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -0,0 +1,125 @@
+/*
+ * 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.thread;
+
+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;
+import android.net.thread.IThreadNetworkController;
+import android.net.thread.IThreadNetworkManager;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+
+import com.android.server.SystemService;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Implementation of the Thread network service. This is the entry point of Android Thread feature.
+ */
+public class ThreadNetworkService extends IThreadNetworkManager.Stub {
+    private final Context mContext;
+    @Nullable private ThreadNetworkCountryCode mCountryCode;
+    @Nullable private ThreadNetworkControllerService mControllerService;
+    private final ThreadPersistentSettings mPersistentSettings;
+    @Nullable private ThreadNetworkShellCommand mShellCommand;
+
+    /** Creates a new {@link ThreadNetworkService} object. */
+    public ThreadNetworkService(Context context) {
+        mContext = context;
+        mPersistentSettings = ThreadPersistentSettings.newInstance(context);
+    }
+
+    /**
+     * Called by {@link com.android.server.ConnectivityServiceInitializer}.
+     *
+     * @see com.android.server.SystemService#onBootPhase
+     */
+    public void onBootPhase(int phase) {
+        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+            mPersistentSettings.initialize();
+            mControllerService =
+                    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.initialize();
+            mShellCommand =
+                    new ThreadNetworkShellCommand(
+                            mContext,
+                            requireNonNull(mControllerService),
+                            requireNonNull(mCountryCode));
+        }
+    }
+
+    @Override
+    public List<IThreadNetworkController> getAllThreadNetworkControllers() {
+        if (mControllerService == null) {
+            return Collections.emptyList();
+        }
+        return Collections.singletonList(mControllerService);
+    }
+
+    @Override
+    public int handleShellCommand(
+            @NonNull ParcelFileDescriptor in,
+            @NonNull ParcelFileDescriptor out,
+            @NonNull ParcelFileDescriptor err,
+            @NonNull String[] args) {
+        if (mShellCommand == null) {
+            return -1;
+        }
+        return mShellCommand.exec(
+                this,
+                in.getFileDescriptor(),
+                out.getFileDescriptor(),
+                err.getFileDescriptor(),
+                args);
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PERMISSION_GRANTED) {
+            pw.println(
+                    "Permission Denial: can't dump ThreadNetworkService from from pid="
+                            + Binder.getCallingPid()
+                            + ", uid="
+                            + Binder.getCallingUid());
+            return;
+        }
+
+        if (mCountryCode != null) {
+            mCountryCode.dump(fd, pw, args);
+        }
+
+        pw.println();
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
new file mode 100644
index 0000000..54155ee
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -0,0 +1,309 @@
+/*
+ * 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.thread;
+
+import android.annotation.Nullable;
+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.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 <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.
+ */
+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";
+
+    private final Context mContext;
+    private final ThreadNetworkControllerService mControllerService;
+    private final ThreadNetworkCountryCode mCountryCode;
+
+    @Nullable private PrintWriter mOutputWriter;
+    @Nullable private PrintWriter mErrorWriter;
+
+    public ThreadNetworkShellCommand(
+            Context context,
+            ThreadNetworkControllerService controllerService,
+            ThreadNetworkCountryCode countryCode) {
+        mContext = context;
+        mControllerService = controllerService;
+        mCountryCode = countryCode;
+    }
+
+    @VisibleForTesting
+    public void setPrintWriters(PrintWriter outputWriter, PrintWriter errorWriter) {
+        mOutputWriter = outputWriter;
+        mErrorWriter = errorWriter;
+    }
+
+    private PrintWriter getOutputWriter() {
+        return (mOutputWriter != null) ? mOutputWriter : getOutPrintWriter();
+    }
+
+    private PrintWriter getErrorWriter() {
+        return (mErrorWriter != null) ? mErrorWriter : getErrPrintWriter();
+    }
+
+    @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 the "help" command
+        if (TextUtils.isEmpty(cmd)) {
+            cmd = "help";
+        }
+
+        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":
+                return forceCountryCode();
+            case "get-country-code":
+                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;
+        } else if (falseString.equals(arg)) {
+            return false;
+        } else {
+            throw new IllegalArgumentException(
+                    "Expected '"
+                            + trueString
+                            + "' or '"
+                            + falseString
+                            + "' as next arg but got '"
+                            + arg
+                            + "'");
+        }
+    }
+
+    private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString) {
+        String nextArg = getNextArgRequired();
+        return argTrueOrFalse(nextArg, trueString, falseString);
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
new file mode 100644
index 0000000..747cc96
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -0,0 +1,331 @@
+/*
+ * 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.thread;
+
+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;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.ConnectivityResources;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Store persistent data for Thread network settings. These are key (string) / value pairs that are
+ * stored in ThreadPersistentSetting.xml file. The values allowed are those that can be serialized
+ * via {@link PersistableBundle}.
+ */
+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.
+     */
+    private static final String VERSION_KEY = "version";
+
+    /******** 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);
+
+    /**
+     * 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 ***************/
+
+    @GuardedBy("mLock")
+    private final AtomicFile mAtomicFile;
+
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private final PersistableBundle mSettings = new PersistableBundle();
+
+    private final ConnectivityResources mResources;
+
+    public static ThreadPersistentSettings newInstance(Context context) {
+        return new ThreadPersistentSettings(
+                new AtomicFile(new File(getOrCreateThreadNetworkDir(), FILE_NAME)),
+                new ConnectivityResources(context));
+    }
+
+    @VisibleForTesting
+    ThreadPersistentSettings(AtomicFile atomicFile, ConnectivityResources resources) {
+        mAtomicFile = atomicFile;
+        mResources = resources;
+    }
+
+    /** Initialize the settings by reading from the settings file. */
+    public void initialize() {
+        readFromStoreFile();
+        synchronized (mLock) {
+            if (!mSettings.containsKey(THREAD_ENABLED.key)) {
+                Log.i(TAG, "\"thread_enabled\" is missing in settings file, using default value");
+                put(
+                        THREAD_ENABLED.key,
+                        mResources.get().getBoolean(R.bool.config_thread_default_enabled));
+            }
+        }
+    }
+
+    private void putObject(String key, @Nullable Object value) {
+        synchronized (mLock) {
+            if (value == null) {
+                mSettings.putString(key, null);
+            } else if (value instanceof Boolean) {
+                mSettings.putBoolean(key, (Boolean) value);
+            } else if (value instanceof Integer) {
+                mSettings.putInt(key, (Integer) value);
+            } else if (value instanceof Long) {
+                mSettings.putLong(key, (Long) value);
+            } else if (value instanceof Double) {
+                mSettings.putDouble(key, (Double) value);
+            } else if (value instanceof String) {
+                mSettings.putString(key, (String) value);
+            } else {
+                throw new IllegalArgumentException("Unsupported type " + value.getClass());
+            }
+        }
+    }
+
+    private <T> T getObject(String key, T defaultValue) {
+        Object value;
+        synchronized (mLock) {
+            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);
+            } else if (defaultValue instanceof Long) {
+                value = mSettings.getLong(key, (Long) defaultValue);
+            } else if (defaultValue instanceof Double) {
+                value = mSettings.getDouble(key, (Double) defaultValue);
+            } else if (defaultValue instanceof String) {
+                value = mSettings.getString(key, (String) defaultValue);
+            } else {
+                throw new IllegalArgumentException("Unsupported type " + defaultValue.getClass());
+            }
+        }
+        return (T) value;
+    }
+
+    /**
+     * Store a value to the stored settings.
+     *
+     * @param key One of the settings keys.
+     * @param value Value to be stored.
+     */
+    public <T> void put(String key, @Nullable T value) {
+        putObject(key, value);
+        writeToStoreFile();
+    }
+
+    /**
+     * Retrieve a value from the stored settings.
+     *
+     * @param key One of the settings keys.
+     * @return value stored in settings, defValue if the key does not exist.
+     */
+    public <T> T get(Key<T> key) {
+        return getObject(key.key, key.defaultValue);
+    }
+
+    /**
+     * 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.isDhcp6PdEnabled());
+        writeToStoreFile();
+        return true;
+    }
+
+    /** Retrieve the {@link ThreadConfiguration} from the persistent settings. */
+    public ThreadConfiguration getConfiguration() {
+        return new ThreadConfiguration.Builder()
+                .setNat64Enabled(get(CONFIG_NAT64_ENABLED))
+                .setDhcp6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
+                .build();
+    }
+
+    /**
+     * Base class to store string key and its default value.
+     *
+     * @param <T> Type of the value.
+     */
+    public static class Key<T> {
+        public final String key;
+        public final T defaultValue;
+
+        private Key(String key, T defaultValue) {
+            this.key = key;
+            this.defaultValue = defaultValue;
+        }
+
+        @Override
+        public String toString() {
+            return "[Key: " + key + ", DefaultValue: " + defaultValue + "]";
+        }
+    }
+
+    private void writeToStoreFile() {
+        try {
+            final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            final PersistableBundle bundleToWrite;
+            synchronized (mLock) {
+                bundleToWrite = new PersistableBundle(mSettings);
+            }
+            bundleToWrite.putInt(VERSION_KEY, CURRENT_SETTINGS_STORE_DATA_VERSION);
+            bundleToWrite.writeToStream(outputStream);
+            synchronized (mLock) {
+                writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
+            }
+        } catch (IOException e) {
+            Log.wtf(TAG, "Write to store file failed", e);
+        }
+    }
+
+    private void readFromStoreFile() {
+        try {
+            final byte[] readData;
+            synchronized (mLock) {
+                Log.i(TAG, "Reading from store file: " + mAtomicFile.getBaseFile());
+                readData = readFromAtomicFile(mAtomicFile);
+            }
+            final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
+            final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream);
+            // Version unused for now. May be needed in the future for handling migrations.
+            bundleRead.remove(VERSION_KEY);
+            synchronized (mLock) {
+                mSettings.putAll(bundleRead);
+            }
+        } catch (FileNotFoundException e) {
+            Log.w(TAG, "No store file to read", e);
+        } catch (IOException e) {
+            Log.e(TAG, "Read from store file failed", e);
+        }
+    }
+
+    /**
+     * Read raw data from the atomic file. Note: This is a copy of {@link AtomicFile#readFully()}
+     * modified to use the passed in {@link InputStream} which was returned using {@link
+     * AtomicFile#openRead()}.
+     */
+    private static byte[] readFromAtomicFile(AtomicFile file) throws IOException {
+        FileInputStream stream = null;
+        try {
+            stream = file.openRead();
+            int pos = 0;
+            int avail = stream.available();
+            byte[] data = new byte[avail];
+            while (true) {
+                int amt = stream.read(data, pos, data.length - pos);
+                if (amt <= 0) {
+                    return data;
+                }
+                pos += amt;
+                avail = stream.available();
+                if (avail > data.length - pos) {
+                    byte[] newData = new byte[pos + avail];
+                    System.arraycopy(data, 0, newData, 0, pos);
+                    data = newData;
+                }
+            }
+        } finally {
+            if (stream != null) stream.close();
+        }
+    }
+
+    /** Write the raw data to the atomic file. */
+    private static void writeToAtomicFile(AtomicFile file, byte[] data) throws IOException {
+        // Write the data to the atomic file.
+        FileOutputStream out = null;
+        try {
+            out = file.startWrite();
+            out.write(data);
+            file.finishWrite(out);
+        } catch (IOException e) {
+            if (out != null) {
+                file.failWrite(out);
+            }
+            throw e;
+        }
+    }
+
+    /** Get device protected storage dir for the tethering apex. */
+    private static File getOrCreateThreadNetworkDir() {
+        final File threadnetworkDir;
+        final File apexDataDir =
+                ApexEnvironment.getApexEnvironment(TETHERING_MODULE_NAME)
+                        .getDeviceProtectedDataDir();
+        threadnetworkDir = new File(apexDataDir, "thread");
+
+        if (threadnetworkDir.exists() || threadnetworkDir.mkdirs()) {
+            return threadnetworkDir;
+        }
+        throw new IllegalStateException(
+                "Cannot write into thread network data directory: " + threadnetworkDir);
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
new file mode 100644
index 0000000..976f93d
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -0,0 +1,428 @@
+/*
+ * 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.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.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+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.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.IOException;
+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;
+
+    static {
+        System.loadLibrary("service-thread-jni");
+    }
+
+    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 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. */
+    public LinkProperties getLinkProperties() {
+        return mLinkProperties;
+    }
+
+    /**
+     * Creates the tunnel interface.
+     *
+     * @throws IOException if failed to create the interface
+     */
+    public void createTunInterface() throws IOException {
+        mParcelTunFd = ParcelFileDescriptor.adoptFd(nativeCreateTunInterface(mIfName, MTU));
+        try {
+            mNetworkInterface = NetworkInterface.getByName(mIfName);
+        } catch (SocketException e) {
+            throw new IOException("Failed to get NetworkInterface", e);
+        }
+
+        setAddrGenModeToNone();
+    }
+
+    public void destroyTunInterface() {
+        try {
+            mParcelTunFd.close();
+        } catch (IOException e) {
+            // Should never fail
+        }
+        mParcelTunFd = null;
+        mNetworkInterface = null;
+    }
+
+    /** Returns the FD of the tunnel interface. */
+    @Nullable
+    public ParcelFileDescriptor getTunFd() {
+        return mParcelTunFd;
+    }
+
+    private native int nativeCreateTunInterface(String interfaceName, int mtu) throws IOException;
+
+    /** Sets the interface up or down according to {@code isUp}. */
+    public void setInterfaceUp(boolean isUp) throws IOException {
+        if (!isUp) {
+            for (LinkAddress address : mLinkProperties.getAllLinkAddresses()) {
+                removeAddress(address);
+            }
+            for (RouteInfo route : mLinkProperties.getAllRoutes()) {
+                mLinkProperties.removeRoute(route);
+            }
+            mNetDataPrefixes.clear();
+        }
+        nativeSetInterfaceUp(mIfName, isUp);
+    }
+
+    private native void nativeSetInterfaceUp(String interfaceName, boolean isUp) throws IOException;
+
+    /** Adds a new address to the interface. */
+    public void addAddress(LinkAddress address) {
+        Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
+
+        long preferredLifetimeSeconds;
+        long validLifetimeSeconds;
+
+        if (address.getDeprecationTime() == LinkAddress.LIFETIME_PERMANENT
+                || address.getDeprecationTime() == LinkAddress.LIFETIME_UNKNOWN) {
+            preferredLifetimeSeconds = INFINITE_LIFETIME;
+        } else {
+            preferredLifetimeSeconds =
+                    Math.max(
+                            (address.getDeprecationTime() - SystemClock.elapsedRealtime()) / 1000L,
+                            0L);
+        }
+
+        if (address.getExpirationTime() == LinkAddress.LIFETIME_PERMANENT
+                || address.getExpirationTime() == LinkAddress.LIFETIME_UNKNOWN) {
+            validLifetimeSeconds = INFINITE_LIFETIME;
+        } else {
+            validLifetimeSeconds =
+                    Math.max(
+                            (address.getExpirationTime() - SystemClock.elapsedRealtime()) / 1000L,
+                            0L);
+        }
+
+        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);
+        mLinkProperties.addRoute(getRouteForAddress(address));
+    }
+
+    /** Removes an address from the interface. */
+    public void removeAddress(LinkAddress address) {
+        Log.d(TAG, "Removing address " + address);
+
+        // 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));
+        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 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. */
+    public void onOtDaemonDied() {
+        try {
+            setInterfaceUp(false);
+        } catch (IOException e) {
+            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
new file mode 100644
index 0000000..1f260f2
--- /dev/null
+++ b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "jniThreadInfra"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <ifaddrs.h>
+#include <inttypes.h>
+#include <linux/if_arp.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <netdb.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <private/android_filesystem_config.h>
+#include <signal.h>
+#include <spawn.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "jni.h"
+#include "nativehelper/JNIHelp.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+namespace android {
+static jint
+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) {
+    jniThrowExceptionFmt(env, "java/io/IOException", "failed to create the socket (%s)",
+                         strerror(errno));
+    return -1;
+  }
+
+  struct icmp6_filter filter;
+  // Only accept Router Advertisements, Router Solicitations and Neighbor
+  // Advertisements.
+  ICMP6_FILTER_SETBLOCKALL(&filter);
+  ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filter);
+  ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filter);
+  ICMP6_FILTER_SETPASS(ND_NEIGHBOR_ADVERT, &filter);
+
+  if (setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, &filter, sizeof(filter)) != 0) {
+    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt ICMP6_FILTER (%s)",
+                         strerror(errno));
+    close(sock);
+    return -1;
+  }
+
+  return sock;
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    {"nativeCreateFilteredIcmp6Socket", "()I",
+     (void *)com_android_server_thread_InfraInterfaceController_createFilteredIcmp6Socket},
+};
+
+int register_com_android_server_thread_InfraInterfaceController(JNIEnv *env) {
+  return jniRegisterNativeMethods(env, "com/android/server/thread/InfraInterfaceController",
+                                  gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
new file mode 100644
index 0000000..c56bc0b
--- /dev/null
+++ b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "jniThreadTun"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/if_arp.h>
+#include <linux/if_tun.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <spawn.h>
+#include <sys/wait.h>
+#include <string>
+
+#include <private/android_filesystem_config.h>
+
+#include "jni.h"
+#include "nativehelper/JNIHelp.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+namespace android {
+static jint com_android_server_thread_TunInterfaceController_createTunInterface(
+        JNIEnv* env, jobject clazz, jstring interfaceName, jint mtu) {
+    ScopedUtfChars ifName(env, interfaceName);
+
+    int fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC);
+    if (fd == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "open tun device failed (%s)",
+                             strerror(errno));
+        return -1;
+    }
+
+    struct ifreq ifr = {
+            .ifr_flags = IFF_TUN | IFF_NO_PI | static_cast<short>(IFF_TUN_EXCL),
+    };
+    strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
+
+    if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr)) != 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)",
+                             strerror(errno));
+        close(fd);
+        return -1;
+    }
+
+    int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
+    if (inet6 == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
+                             strerror(errno));
+        close(fd);
+        return -1;
+    }
+    ifr.ifr_mtu = mtu;
+    if (ioctl(inet6, SIOCSIFMTU, &ifr) != 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFMTU) failed (%s)",
+                             strerror(errno));
+        close(fd);
+        close(inet6);
+        return -1;
+    }
+
+    close(inet6);
+    return fd;
+}
+
+static void com_android_server_thread_TunInterfaceController_setInterfaceUp(
+        JNIEnv* env, jobject clazz, jstring interfaceName, jboolean isUp) {
+    struct ifreq ifr;
+    ScopedUtfChars ifName(env, interfaceName);
+
+    ifr.ifr_flags = isUp ? IFF_UP : 0;
+    strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
+
+    int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
+    if (inet6 == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
+                             strerror(errno));
+    }
+
+    if (ioctl(inet6, SIOCSIFFLAGS, &ifr) != 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFFLAGS) failed (%s)",
+                             strerror(errno));
+    }
+
+    close(inet6);
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"nativeCreateTunInterface",
+         "(Ljava/lang/String;I)I",
+         (void*)com_android_server_thread_TunInterfaceController_createTunInterface},
+        {"nativeSetInterfaceUp",
+         "(Ljava/lang/String;Z)V",
+         (void*)com_android_server_thread_TunInterfaceController_setInterfaceUp},
+};
+
+int register_com_android_server_thread_TunInterfaceController(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "com/android/server/thread/TunInterfaceController",
+                                    gMethods, NELEM(gMethods));
+}
+
+};  // namespace android
diff --git a/thread/service/jni/onload.cpp b/thread/service/jni/onload.cpp
new file mode 100644
index 0000000..66add74
--- /dev/null
+++ b/thread/service/jni/onload.cpp
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+#include "jni.h"
+#include "utils/Log.h"
+
+namespace android {
+int register_com_android_server_thread_TunInterfaceController(JNIEnv* env);
+int register_com_android_server_thread_InfraInterfaceController(JNIEnv* env);
+}
+
+using namespace android;
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
+    JNIEnv* env = NULL;
+
+    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
+        ALOGE("GetEnv failed!");
+        return -1;
+    }
+    ALOG_ASSERT(env != NULL, "Could not retrieve the env!");
+
+    register_com_android_server_thread_TunInterfaceController(env);
+    register_com_android_server_thread_InfraInterfaceController(env);
+    return JNI_VERSION_1_4;
+}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 9edbd8b..c1cf0a0 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -14,18 +14,44 @@
 // limitations under the License.
 //
 
-android_test_import {
-    name: "CtsThreadNetworkTestCases",
-    apk: "CtsThreadNetworkTestCases.apk",
+package {
+    default_team: "trendy_team_fwk_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
 
+android_test {
+    name: "CtsThreadNetworkTestCases",
+    defaults: ["cts_defaults"],
+    min_sdk_version: "33",
+    sdk_version: "test_current",
+    manifest: "AndroidManifest.xml",
+    test_config: "AndroidTest.xml",
+    srcs: [
+        "src/**/*.java",
+    ],
     test_suites: [
         "cts",
         "general-tests",
+        "mcts-tethering",
+        "mts-tethering",
     ],
-
-    presigned: true,
-    dex_preopt: {
-        enabled: false,
-    },
+    static_libs: [
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "guava",
+        "guava-android-testlib",
+        "net-tests-utils",
+        "ThreadNetworkTestUtils",
+        "truth",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+        "framework-connectivity-module-api-stubs-including-flagged",
+    ],
+    // Test coverage system runs on different devices. Need to
+    // compile for all architectures.
+    compile_multilib: "both",
+    platform_apis: true,
 }
-
diff --git a/thread/tests/cts/AndroidManifest.xml b/thread/tests/cts/AndroidManifest.xml
new file mode 100644
index 0000000..1541bf5
--- /dev/null
+++ b/thread/tests/cts/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?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.thread.cts">
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.net.thread.cts"
+        android:label="CTS tests for android.net.thread" />
+</manifest>
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index 6eda1e9..34aabe2 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -22,6 +22,7 @@
     <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="parameter" value="secondary_user_on_secondary_display" />
 
     <!--
         Only run tests if the device under test is SDK version 33 (Android 13) or above.
diff --git a/thread/tests/cts/CtsThreadNetworkTestCases.apk b/thread/tests/cts/CtsThreadNetworkTestCases.apk
deleted file mode 100755
index ef9d615..0000000
--- a/thread/tests/cts/CtsThreadNetworkTestCases.apk
+++ /dev/null
Binary files differ
diff --git a/thread/tests/cts/METADATA b/thread/tests/cts/METADATA
deleted file mode 100644
index 08c9e81..0000000
--- a/thread/tests/cts/METADATA
+++ /dev/null
@@ -1,17 +0,0 @@
-name: "CtsThreadNetworkTestCases"
-description: "Prebuilt of CtsThreadNetworkTestCases"
-third_party {
-  identifier {
-    type: "Git"
-    value: "https://android.googlesource.com/platform/packages/modules/Connectivity/"
-    version: "frc_350712000"
-  }
-  version: "15"
-  license_type: NOTICE
-  license_note: "None"
-  last_upgrade_date {
-    year: 2024
-    month: 5
-    day: 25
-  }
-}
diff --git a/thread/tests/cts/OWNERS b/thread/tests/cts/OWNERS
new file mode 100644
index 0000000..6065bf8
--- /dev/null
+++ b/thread/tests/cts/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 1203089
+
+include platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
new file mode 100644
index 0000000..996d22d
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
@@ -0,0 +1,736 @@
+/*
+ * 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.thread.cts;
+
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
+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;
+import androidx.test.filters.SmallTest;
+
+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;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+
+/** CTS tests for {@link ActiveOperationalDataset}. */
+@SmallTest
+@RequiresThreadFeature
+@RunWith(AndroidJUnit4.class)
+public final class ActiveOperationalDatasetTest {
+    private static final int TYPE_ACTIVE_TIMESTAMP = 14;
+    private static final int TYPE_CHANNEL = 0;
+    private static final int TYPE_CHANNEL_MASK = 53;
+    private static final int TYPE_EXTENDED_PAN_ID = 2;
+    private static final int TYPE_MESH_LOCAL_PREFIX = 7;
+    private static final int TYPE_NETWORK_KEY = 5;
+    private static final int TYPE_NETWORK_NAME = 3;
+    private static final int TYPE_PAN_ID = 1;
+    private static final int TYPE_PSKC = 4;
+    private static final int TYPE_SECURITY_POLICY = 12;
+
+    // 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 byte[] VALID_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+
+    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;
+        while (i < dataset.length) {
+            int ty = dataset[i++] & 0xff;
+            byte length = dataset[i++];
+            if (ty != type) {
+                byte[] value = Arrays.copyOfRange(dataset, i, i + length);
+                os.write(ty);
+                os.write(length);
+                os.writeBytes(value);
+            }
+            i += length;
+        }
+        return os.toByteArray();
+    }
+
+    private static byte[] addTlv(byte[] dataset, String tlvHex) {
+        return Bytes.concat(dataset, base16().decode(tlvHex));
+    }
+
+    private static byte[] replaceTlv(byte[] dataset, int type, String newTlvHex) {
+        return addTlv(removeTlv(dataset, type), newTlvHex);
+    }
+
+    @Test
+    public void parcelable_parcelingIsLossLess() {
+        ActiveOperationalDataset dataset =
+                ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS);
+
+        assertParcelingIsLossless(dataset);
+    }
+
+    @Test
+    public void fromThreadTlvs_tooLongTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = new byte[255];
+        invalidTlv[0] = (byte) 0xff;
+
+        // This is invalid because the TLV has max total length of 254 bytes and the value length
+        // can't exceeds 252 ( = 254 - 1 - 1)
+        invalidTlv[1] = (byte) 253;
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_invalidNetworkKeyTlv_throwsIllegalArgument() {
+        byte[] invalidTlv =
+                replaceTlv(VALID_DATASET_TLVS, TYPE_NETWORK_KEY, "05080000000000000000");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_noNetworkKeyTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_NETWORK_KEY);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_invalidActiveTimestampTlv_throwsIllegalArgument() {
+        byte[] invalidTlv =
+                replaceTlv(VALID_DATASET_TLVS, TYPE_ACTIVE_TIMESTAMP, "0E0700000000010000");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_noActiveTimestampTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_ACTIVE_TIMESTAMP);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_invalidNetworkNameTlv_emptyName_throwsIllegalArgument() {
+        byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_NETWORK_NAME, "0300");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_invalidNetworkNameTlv_tooLongName_throwsIllegalArgument() {
+        byte[] invalidTlv =
+                replaceTlv(
+                        VALID_DATASET_TLVS,
+                        TYPE_NETWORK_NAME,
+                        "03114142434445464748494A4B4C4D4E4F5051");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_noNetworkNameTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_NETWORK_NAME);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_invalidChannelTlv_channelMissing_throwsIllegalArgument() {
+        byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "000100");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_undefinedChannelPage_success() {
+        byte[] datasetTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "0003010020");
+
+        ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlv);
+
+        assertThat(dataset.getChannelPage()).isEqualTo(0x01);
+        assertThat(dataset.getChannel()).isEqualTo(0x20);
+    }
+
+    @Test
+    public void fromThreadTlvs_invalid2P4GhzChannel_throwsIllegalArgument() {
+        byte[] invalidTlv1 = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "000300000A");
+        byte[] invalidTlv2 = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "000300001B");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv1));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv2));
+    }
+
+    @Test
+    public void fromThreadTlvs_valid2P4GhzChannelTlv_success() {
+        byte[] validTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "0003000010");
+
+        ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validTlv);
+
+        assertThat(dataset.getChannel()).isEqualTo(16);
+    }
+
+    @Test
+    public void fromThreadTlvs_noChannelTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_CHANNEL);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_prematureEndOfChannelMaskEntry_throwsIllegalArgument() {
+        byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK, "350100");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_inconsistentChannelMaskLength_throwsIllegalArgument() {
+        byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK, "3506000500010000");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_unsupportedChannelMaskLength_success() {
+        ActiveOperationalDataset dataset =
+                ActiveOperationalDataset.fromThreadTlvs(
+                        replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK, "350700050001000000"));
+
+        SparseArray<byte[]> channelMask = dataset.getChannelMask();
+        assertThat(channelMask.size()).isEqualTo(1);
+        assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
+                .isEqualTo(new byte[] {0x00, 0x01, 0x00, 0x00, 0x00});
+    }
+
+    @Test
+    public void fromThreadTlvs_noChannelMaskTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_invalidPanIdTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_PAN_ID, "010101");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_noPanIdTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_PAN_ID);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_invalidExtendedPanIdTlv_throwsIllegalArgument() {
+        byte[] invalidTlv =
+                replaceTlv(VALID_DATASET_TLVS, TYPE_EXTENDED_PAN_ID, "020700010203040506");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_noExtendedPanIdTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_EXTENDED_PAN_ID);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_invalidPskcTlv_throwsIllegalArgument() {
+        byte[] invalidTlv =
+                replaceTlv(VALID_DATASET_TLVS, TYPE_PSKC, "0411000102030405060708090A0B0C0D0E0F10");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_noPskcTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_PSKC);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_invalidMeshLocalPrefixTlv_throwsIllegalArgument() {
+        byte[] invalidTlv =
+                replaceTlv(VALID_DATASET_TLVS, TYPE_MESH_LOCAL_PREFIX, "0709FD0001020304050607");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_noMeshLocalPrefixTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_MESH_LOCAL_PREFIX);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_tooShortSecurityPolicyTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_SECURITY_POLICY, "0C0101");
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_noSecurityPolicyTlv_throwsIllegalArgument() {
+        byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_SECURITY_POLICY);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+    }
+
+    @Test
+    public void fromThreadTlvs_lengthAndDataMissing_throwsIllegalArgument() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {(byte) 0x00}));
+    }
+
+    @Test
+    public void fromThreadTlvs_prematureEndOfData_throwsIllegalArgument() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {0x00, 0x03, 0x00, 0x00}));
+    }
+
+    @Test
+    public void fromThreadTlvs_validFullDataset_success() {
+        // A valid Thread active operational dataset:
+        // 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
+        byte[] validDatasetTlv =
+                base16().decode(
+                                "0E080000000000010000000300001335060004001FFFE002"
+                                        + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                        + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                        + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                        + "B9D351B40C0402A0FFF8");
+
+        ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validDatasetTlv);
+
+        assertThat(dataset.getNetworkKey())
+                .isEqualTo(base16().decode("F26B3153760F519A63BAFDDFFC80D2AF"));
+        assertThat(dataset.getPanId()).isEqualTo(0xd9a0);
+        assertThat(dataset.getExtendedPanId()).isEqualTo(base16().decode("ACC214689BC40BDF"));
+        assertThat(dataset.getChannel()).isEqualTo(19);
+        assertThat(dataset.getNetworkName()).isEqualTo("OpenThread-d9a0");
+        assertThat(dataset.getPskc())
+                .isEqualTo(base16().decode("A245479C836D551B9CA557F7B9D351B4"));
+        assertThat(dataset.getActiveTimestamp())
+                .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+        SparseArray<byte[]> channelMask = dataset.getChannelMask();
+        assertThat(channelMask.size()).isEqualTo(1);
+        assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
+                .isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+        assertThat(dataset.getMeshLocalPrefix())
+                .isEqualTo(new IpPrefix("fd64:db12:25f4:7e0b::/64"));
+        assertThat(dataset.getSecurityPolicy())
+                .isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
+    }
+
+    @Test
+    public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
+        final byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET_TLVS, "AA01FFBB020102");
+
+        ActiveOperationalDataset dataset =
+                ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
+
+        byte[] newDatasetTlvs = dataset.toThreadTlvs();
+        String newDatasetTlvsHex = base16().encode(newDatasetTlvs);
+        assertThat(newDatasetTlvs.length).isEqualTo(datasetWithUnknownTlvs.length);
+        assertThat(newDatasetTlvsHex).contains("AA01FF");
+        assertThat(newDatasetTlvsHex).contains("BB020102");
+    }
+
+    @Test
+    public void toThreadTlvs_conversionIsLossLess() {
+        ActiveOperationalDataset dataset1 = DEFAULT_DATASET;
+
+        ActiveOperationalDataset dataset2 =
+                ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+        assertThat(dataset2).isEqualTo(dataset1);
+    }
+
+    @Test
+    public void builder_buildWithdefaultValues_throwsIllegalState() {
+        assertThrows(IllegalStateException.class, () -> new Builder().build());
+    }
+
+    @Test
+    public void builder_setValidNetworkKey_success() {
+        final byte[] networkKey =
+                new byte[] {
+                    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
+                    0x0d, 0x0e, 0x0f
+                };
+
+        ActiveOperationalDataset dataset =
+                new Builder(DEFAULT_DATASET).setNetworkKey(networkKey).build();
+
+        assertThat(dataset.getNetworkKey()).isEqualTo(networkKey);
+    }
+
+    @Test
+    public void builder_setInvalidNetworkKey_throwsIllegalArgument() {
+        byte[] invalidNetworkKey = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+        Builder builder = new Builder();
+
+        assertThrows(
+                IllegalArgumentException.class, () -> builder.setNetworkKey(invalidNetworkKey));
+    }
+
+    @Test
+    public void builder_setValidExtendedPanId_success() {
+        byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+
+        ActiveOperationalDataset dataset =
+                new Builder(DEFAULT_DATASET).setExtendedPanId(extendedPanId).build();
+
+        assertThat(dataset.getExtendedPanId()).isEqualTo(extendedPanId);
+    }
+
+    @Test
+    public void builder_setInvalidExtendedPanId_throwsIllegalArgument() {
+        byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+        Builder builder = new Builder();
+
+        assertThrows(IllegalArgumentException.class, () -> builder.setExtendedPanId(extendedPanId));
+    }
+
+    @Test
+    public void builder_setValidPanId_success() {
+        ActiveOperationalDataset dataset = new Builder(DEFAULT_DATASET).setPanId(0xfffe).build();
+
+        assertThat(dataset.getPanId()).isEqualTo(0xfffe);
+    }
+
+    @Test
+    public void builder_setInvalidPanId_throwsIllegalArgument() {
+        Builder builder = new Builder();
+
+        assertThrows(IllegalArgumentException.class, () -> builder.setPanId(0xffff));
+    }
+
+    @Test
+    public void builder_setInvalidChannel_throwsIllegalArgument() {
+        Builder builder = new Builder();
+
+        assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 0));
+        assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 27));
+    }
+
+    @Test
+    public void builder_setValid2P4GhzChannel_success() {
+        ActiveOperationalDataset dataset =
+                new Builder(DEFAULT_DATASET).setChannel(CHANNEL_PAGE_24_GHZ, 16).build();
+
+        assertThat(dataset.getChannel()).isEqualTo(16);
+        assertThat(dataset.getChannelPage()).isEqualTo(CHANNEL_PAGE_24_GHZ);
+    }
+
+    @Test
+    public void builder_setValidNetworkName_success() {
+        ActiveOperationalDataset dataset =
+                new Builder(DEFAULT_DATASET).setNetworkName("ot-network").build();
+
+        assertThat(dataset.getNetworkName()).isEqualTo("ot-network");
+    }
+
+    @Test
+    public void builder_setEmptyNetworkName_throwsIllegalArgument() {
+        Builder builder = new Builder();
+
+        assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName(""));
+    }
+
+    @Test
+    public void builder_setTooLongNetworkName_throwsIllegalArgument() {
+        Builder builder = new Builder();
+
+        assertThrows(
+                IllegalArgumentException.class, () -> builder.setNetworkName("openthread-network"));
+    }
+
+    @Test
+    public void builder_setTooLongUtf8NetworkName_throwsIllegalArgument() {
+        Builder builder = new Builder();
+
+        // UTF-8 encoded length of "我的线程网络" is 18 bytes which exceeds the max length
+        assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName("我的线程网络"));
+    }
+
+    @Test
+    public void builder_setValidUtf8NetworkName_success() {
+        ActiveOperationalDataset dataset =
+                new Builder(DEFAULT_DATASET).setNetworkName("我的网络").build();
+
+        assertThat(dataset.getNetworkName()).isEqualTo("我的网络");
+    }
+
+    @Test
+    public void builder_setValidPskc_success() {
+        byte[] pskc = base16().decode("A245479C836D551B9CA557F7B9D351B4");
+
+        ActiveOperationalDataset dataset = new Builder(DEFAULT_DATASET).setPskc(pskc).build();
+
+        assertThat(dataset.getPskc()).isEqualTo(pskc);
+    }
+
+    @Test
+    public void builder_setTooLongPskc_throwsIllegalArgument() {
+        byte[] tooLongPskc = base16().decode("A245479C836D551B9CA557F7B9D351B400");
+        Builder builder = new Builder();
+
+        assertThrows(IllegalArgumentException.class, () -> builder.setPskc(tooLongPskc));
+    }
+
+    @Test
+    public void builder_setValidChannelMask_success() {
+        SparseArray<byte[]> channelMask = new SparseArray<byte[]>(1);
+        channelMask.put(0, new byte[] {0x00, 0x00, 0x01, 0x00});
+
+        ActiveOperationalDataset dataset =
+                new Builder(DEFAULT_DATASET).setChannelMask(channelMask).build();
+
+        SparseArray<byte[]> resultChannelMask = dataset.getChannelMask();
+        assertThat(resultChannelMask.size()).isEqualTo(1);
+        assertThat(resultChannelMask.get(0)).isEqualTo(new byte[] {0x00, 0x00, 0x01, 0x00});
+    }
+
+    @Test
+    public void builder_setEmptyChannelMask_throwsIllegalArgument() {
+        Builder builder = new Builder();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> builder.setChannelMask(new SparseArray<byte[]>()));
+    }
+
+    @Test
+    public void builder_setValidActiveTimestamp_success() {
+        ActiveOperationalDataset dataset =
+                new Builder(DEFAULT_DATASET)
+                        .setActiveTimestamp(
+                                new OperationalDatasetTimestamp(
+                                        /* seconds= */ 1,
+                                        /* ticks= */ 0,
+                                        /* isAuthoritativeSource= */ true))
+                        .build();
+
+        assertThat(dataset.getActiveTimestamp().getSeconds()).isEqualTo(1);
+        assertThat(dataset.getActiveTimestamp().getTicks()).isEqualTo(0);
+        assertThat(dataset.getActiveTimestamp().isAuthoritativeSource()).isTrue();
+    }
+
+    @Test
+    public void builder_wrongMeshLocalPrefixLength_throwsIllegalArguments() {
+        Builder builder = new Builder();
+
+        // The Mesh-Local Prefix length must be 64 bits
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/32")));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/96")));
+
+        // The Mesh-Local Prefix must start with 0xfd
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
+    }
+
+    @Test
+    public void builder_meshLocalPrefixNotStartWith0xfd_throwsIllegalArguments() {
+        Builder builder = new Builder();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
+    }
+
+    @Test
+    public void builder_setValidMeshLocalPrefix_success() {
+        ActiveOperationalDataset dataset =
+                new Builder(DEFAULT_DATASET).setMeshLocalPrefix(new IpPrefix("fd00::/64")).build();
+
+        assertThat(dataset.getMeshLocalPrefix()).isEqualTo(new IpPrefix("fd00::/64"));
+    }
+
+    @Test
+    public void builder_setValid1P2SecurityPolicy_success() {
+        ActiveOperationalDataset dataset =
+                new Builder(DEFAULT_DATASET)
+                        .setSecurityPolicy(
+                                new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+                        .build();
+
+        assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+        assertThat(dataset.getSecurityPolicy().getFlags())
+                .isEqualTo(new byte[] {(byte) 0xff, (byte) 0xf8});
+    }
+
+    @Test
+    public void builder_setValid1P1SecurityPolicy_success() {
+        ActiveOperationalDataset dataset =
+                new Builder(DEFAULT_DATASET)
+                        .setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff}))
+                        .build();
+
+        assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+        assertThat(dataset.getSecurityPolicy().getFlags()).isEqualTo(new byte[] {(byte) 0xff});
+    }
+
+    @Test
+    public void securityPolicy_invalidRotationTime_throwsIllegalArguments() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new SecurityPolicy(0, new byte[] {(byte) 0xff, (byte) 0xf8}));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new SecurityPolicy(0x1ffff, new byte[] {(byte) 0xff, (byte) 0xf8}));
+    }
+
+    @Test
+    public void securityPolicy_emptyFlags_throwsIllegalArguments() {
+        assertThrows(IllegalArgumentException.class, () -> new SecurityPolicy(672, new byte[] {}));
+    }
+
+    @Test
+    public void securityPolicy_tooLongFlags_success() {
+        SecurityPolicy securityPolicy =
+                new SecurityPolicy(672, new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
+
+        assertThat(securityPolicy.getFlags()).isEqualTo(new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
+    }
+
+    @Test
+    public void securityPolicy_equals() {
+        new EqualsTester()
+                .addEqualityGroup(
+                        new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}),
+                        new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+                .addEqualityGroup(
+                        new SecurityPolicy(1, new byte[] {(byte) 0xff}),
+                        new SecurityPolicy(1, new byte[] {(byte) 0xff}))
+                .addEqualityGroup(
+                        new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}),
+                        new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}))
+                .testEquals();
+    }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
new file mode 100644
index 0000000..4d7c7f1
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.thread.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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;
+
+import java.time.Instant;
+
+/** 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(
+                IllegalArgumentException.class,
+                () ->
+                        OperationalDatasetTimestamp.fromInstant(
+                                Instant.ofEpochSecond(0xffffffffffffL + 1L)));
+    }
+
+    @Test
+    public void fromInstant_ticksIsRounded() {
+        Instant instant = Instant.ofEpochSecond(100L);
+
+        // 32767.5 / 32768 * 1000000000 = 999984741.2109375 and given the `ticks` is rounded, so
+        // the `ticks` should be 32767 for 999984741 and 0 (carried over to seconds) for 999984742.
+        OperationalDatasetTimestamp timestampTicks32767 =
+                OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984741));
+        OperationalDatasetTimestamp timestampTicks0 =
+                OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984742));
+
+        assertThat(timestampTicks32767.getSeconds()).isEqualTo(100L);
+        assertThat(timestampTicks0.getSeconds()).isEqualTo(101L);
+        assertThat(timestampTicks32767.getTicks()).isEqualTo(32767);
+        assertThat(timestampTicks0.getTicks()).isEqualTo(0);
+        assertThat(timestampTicks32767.isAuthoritativeSource()).isTrue();
+        assertThat(timestampTicks0.isAuthoritativeSource()).isTrue();
+    }
+
+    @Test
+    public void toInstant_nanosIsRounded() {
+        // 32767 / 32768 * 1000000000 = 999969482.421875
+        assertThat(new OperationalDatasetTimestamp(100L, 32767, false).toInstant().getNano())
+                .isEqualTo(999969482);
+
+        // 32766 / 32768 * 1000000000 = 999938964.84375
+        assertThat(new OperationalDatasetTimestamp(100L, 32766, false).toInstant().getNano())
+                .isEqualTo(999938965);
+    }
+
+    @Test
+    public void toInstant_onlyAuthoritativeSourceDiscarded() {
+        OperationalDatasetTimestamp timestamp1 =
+                new OperationalDatasetTimestamp(100L, 0x7fff, false);
+
+        OperationalDatasetTimestamp timestamp2 =
+                OperationalDatasetTimestamp.fromInstant(timestamp1.toInstant());
+
+        assertThat(timestamp2.getSeconds()).isEqualTo(100L);
+        assertThat(timestamp2.getTicks()).isEqualTo(0x7fff);
+        assertThat(timestamp2.isAuthoritativeSource()).isTrue();
+    }
+
+    @Test
+    public void constructor_tooLargeSeconds_throwsIllegalArguments() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        new OperationalDatasetTimestamp(
+                                /* seconds= */ 0x0001112233445566L,
+                                /* ticks= */ 0,
+                                /* isAuthoritativeSource= */ true));
+    }
+
+    @Test
+    public void constructor_tooLargeTicks_throwsIllegalArguments() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        new OperationalDatasetTimestamp(
+                                /* seconds= */ 0x01L,
+                                /* ticks= */ 0x8000,
+                                /* isAuthoritativeSource= */ true));
+    }
+
+    @Test
+    public void equalityTests() {
+        new EqualsTester()
+                .addEqualityGroup(
+                        new OperationalDatasetTimestamp(100, 100, false),
+                        new OperationalDatasetTimestamp(100, 100, false))
+                .addEqualityGroup(
+                        new OperationalDatasetTimestamp(0, 0, false),
+                        new OperationalDatasetTimestamp(0, 0, false))
+                .addEqualityGroup(
+                        new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true),
+                        new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true))
+                .testEquals();
+    }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
new file mode 100644
index 0000000..76be054
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
@@ -0,0 +1,280 @@
+/*
+ * 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.thread.cts;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
+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;
+import androidx.test.filters.SmallTest;
+
+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;
+
+import java.net.InetAddress;
+import java.time.Duration;
+
+/** 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});
+
+        return new ActiveOperationalDataset.Builder()
+                .setActiveTimestamp(new OperationalDatasetTimestamp(100, 10, false))
+                .setExtendedPanId(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+                .setPanId(12345)
+                .setNetworkName("defaultNet")
+                .setChannel(0, 18)
+                .setChannelMask(channelMask)
+                .setPskc(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15})
+                .setNetworkKey(new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
+                .setMeshLocalPrefix(new IpPrefix(InetAddress.getByName("fd00::1"), 64))
+                .setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+                .build();
+    }
+
+    @Test
+    public void parcelable_parcelingIsLossLess() throws Exception {
+        PendingOperationalDataset dataset =
+                new PendingOperationalDataset(
+                        createActiveDataset(),
+                        new OperationalDatasetTimestamp(31536000, 200, false),
+                        Duration.ofHours(100));
+
+        assertParcelingIsLossless(dataset);
+    }
+
+    @Test
+    public void equalityTests() throws Exception {
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(createActiveDataset())
+                        .setNetworkName("net1")
+                        .build();
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(createActiveDataset())
+                        .setNetworkName("net2")
+                        .build();
+
+        new EqualsTester()
+                .addEqualityGroup(
+                        new PendingOperationalDataset(
+                                activeDataset1,
+                                new OperationalDatasetTimestamp(31536000, 100, false),
+                                Duration.ofMillis(0)),
+                        new PendingOperationalDataset(
+                                activeDataset1,
+                                new OperationalDatasetTimestamp(31536000, 100, false),
+                                Duration.ofMillis(0)))
+                .addEqualityGroup(
+                        new PendingOperationalDataset(
+                                activeDataset2,
+                                new OperationalDatasetTimestamp(31536000, 100, false),
+                                Duration.ofMillis(0)),
+                        new PendingOperationalDataset(
+                                activeDataset2,
+                                new OperationalDatasetTimestamp(31536000, 100, false),
+                                Duration.ofMillis(0)))
+                .addEqualityGroup(
+                        new PendingOperationalDataset(
+                                activeDataset2,
+                                new OperationalDatasetTimestamp(15768000, 0, false),
+                                Duration.ofMillis(0)),
+                        new PendingOperationalDataset(
+                                activeDataset2,
+                                new OperationalDatasetTimestamp(15768000, 0, false),
+                                Duration.ofMillis(0)))
+                .addEqualityGroup(
+                        new PendingOperationalDataset(
+                                activeDataset2,
+                                new OperationalDatasetTimestamp(15768000, 0, false),
+                                Duration.ofMillis(100)),
+                        new PendingOperationalDataset(
+                                activeDataset2,
+                                new OperationalDatasetTimestamp(15768000, 0, false),
+                                Duration.ofMillis(100)))
+                .testEquals();
+    }
+
+    @Test
+    public void constructor_correctValuesAreSet() throws Exception {
+        final ActiveOperationalDataset activeDataset = createActiveDataset();
+        PendingOperationalDataset dataset =
+                new PendingOperationalDataset(
+                        activeDataset,
+                        new OperationalDatasetTimestamp(31536000, 200, false),
+                        Duration.ofHours(100));
+
+        assertThat(dataset.getActiveOperationalDataset()).isEqualTo(activeDataset);
+        assertThat(dataset.getPendingTimestamp())
+                .isEqualTo(new OperationalDatasetTimestamp(31536000, 200, false));
+        assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofHours(100));
+    }
+
+    @Test
+    public void fromThreadTlvs_openthreadTlvs_success() {
+        // An example Pending Operational Dataset which is generated with OpenThread CLI:
+        // Pending Timestamp: 2
+        // Active Timestamp: 1
+        // Channel: 26
+        // Channel Mask: 0x07fff800
+        // Delay: 46354
+        // Ext PAN ID: a74182f4d3f4de41
+        // Mesh Local Prefix: fd46:c1b9:e159:5574::/64
+        // Network Key: ed916e454d96fd00184f10a6f5c9e1d3
+        // Network Name: OpenThread-bff8
+        // PAN ID: 0xbff8
+        // PSKc: 264f78414adc683191863d968f72d1b7
+        // Security Policy: 672 onrc
+        final byte[] OPENTHREAD_PENDING_DATASET_TLVS =
+                base16().lowerCase()
+                        .decode(
+                                "0e0800000000000100003308000000000002000034040000b51200030000"
+                                        + "1a35060004001fffe00208a74182f4d3f4de410708fd46c1b9"
+                                        + "e15955740510ed916e454d96fd00184f10a6f5c9e1d3030f4f"
+                                        + "70656e5468726561642d626666380102bff80410264f78414a"
+                                        + "dc683191863d968f72d1b70c0402a0f7f8");
+
+        PendingOperationalDataset pendingDataset =
+                PendingOperationalDataset.fromThreadTlvs(OPENTHREAD_PENDING_DATASET_TLVS);
+
+        ActiveOperationalDataset activeDataset = pendingDataset.getActiveOperationalDataset();
+        assertThat(pendingDataset.getPendingTimestamp().getSeconds()).isEqualTo(2L);
+        assertThat(activeDataset.getActiveTimestamp().getSeconds()).isEqualTo(1L);
+        assertThat(activeDataset.getChannel()).isEqualTo(26);
+        assertThat(activeDataset.getChannelMask().get(0))
+                .isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+        assertThat(pendingDataset.getDelayTimer().toMillis()).isEqualTo(46354);
+        assertThat(activeDataset.getExtendedPanId())
+                .isEqualTo(base16().lowerCase().decode("a74182f4d3f4de41"));
+        assertThat(activeDataset.getMeshLocalPrefix())
+                .isEqualTo(new IpPrefix("fd46:c1b9:e159:5574::/64"));
+        assertThat(activeDataset.getNetworkKey())
+                .isEqualTo(base16().lowerCase().decode("ed916e454d96fd00184f10a6f5c9e1d3"));
+        assertThat(activeDataset.getNetworkName()).isEqualTo("OpenThread-bff8");
+        assertThat(activeDataset.getPanId()).isEqualTo(0xbff8);
+        assertThat(activeDataset.getPskc())
+                .isEqualTo(base16().lowerCase().decode("264f78414adc683191863d968f72d1b7"));
+        assertThat(activeDataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+        assertThat(activeDataset.getSecurityPolicy().getFlags())
+                .isEqualTo(new byte[] {(byte) 0xf7, (byte) 0xf8});
+    }
+
+    @Test
+    public void fromThreadTlvs_completePendingDatasetTlvs_success() throws Exception {
+        final ActiveOperationalDataset activeDataset = createActiveDataset();
+
+        // Type Length Value
+        // 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
+        // 0x34 0x04 0x0000012C (Delay Timer TLV)
+        final byte[] pendingTimestampAndDelayTimerTlvs =
+                base16().decode("3308000000000001000034040000012C");
+        final byte[] pendingDatasetTlvs =
+                Bytes.concat(pendingTimestampAndDelayTimerTlvs, activeDataset.toThreadTlvs());
+
+        PendingOperationalDataset dataset =
+                PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs);
+
+        assertThat(dataset.getActiveOperationalDataset()).isEqualTo(activeDataset);
+        assertThat(dataset.getPendingTimestamp())
+                .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+        assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofMillis(300));
+    }
+
+    @Test
+    public void fromThreadTlvs_PendingTimestampTlvIsMissing_throwsIllegalArgument()
+            throws Exception {
+        // Type Length Value
+        // 0x34 0x04 0x00000064 (Delay Timer TLV)
+        final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("34040000012C");
+        final byte[] pendingDatasetTlvs =
+                Bytes.concat(
+                        pendingTimestampAndDelayTimerTlvs, createActiveDataset().toThreadTlvs());
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
+    }
+
+    @Test
+    public void fromThreadTlvs_delayTimerTlvIsMissing_throwsIllegalArgument() throws Exception {
+        // Type Length Value
+        // 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
+        final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("33080000000000010000");
+        final byte[] pendingDatasetTlvs =
+                Bytes.concat(
+                        pendingTimestampAndDelayTimerTlvs, createActiveDataset().toThreadTlvs());
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
+    }
+
+    @Test
+    public void fromThreadTlvs_activeDatasetTlvs_throwsIllegalArgument() throws Exception {
+        final byte[] activeDatasetTlvs = createActiveDataset().toThreadTlvs();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> PendingOperationalDataset.fromThreadTlvs(activeDatasetTlvs));
+    }
+
+    @Test
+    public void fromThreadTlvs_malformedTlvs_throwsIllegalArgument() {
+        final byte[] invalidTlvs = new byte[] {0x00};
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> PendingOperationalDataset.fromThreadTlvs(invalidTlvs));
+    }
+
+    @Test
+    public void toThreadTlvs_conversionIsLossLess() throws Exception {
+        PendingOperationalDataset dataset1 =
+                new PendingOperationalDataset(
+                        createActiveDataset(),
+                        new OperationalDatasetTimestamp(31536000, 200, false),
+                        Duration.ofHours(100));
+
+        PendingOperationalDataset dataset2 =
+                PendingOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+        assertThat(dataset2).isEqualTo(dataset1);
+    }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
new file mode 100644
index 0000000..22e7a98
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -0,0 +1,1245 @@
+/*
+ * 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.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;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
+import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+
+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 com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
+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;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+
+import com.android.net.module.util.ArrayTrackRecord;
+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 java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+
+/** CTS tests for {@link ThreadNetworkController}. */
+@LargeTest
+@RequiresThreadFeature
+public class ThreadNetworkControllerTest {
+    private static final int JOIN_TIMEOUT_MILLIS = 30 * 1000;
+    private static final int LEAVE_TIMEOUT_MILLIS = 2_000;
+    private static final int MIGRATION_TIMEOUT_MILLIS = 40 * 1_000;
+    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 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";
+
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private ExecutorService mExecutor;
+    private ThreadNetworkController mController;
+    private NsdManager mNsdManager;
+
+    private Set<String> mGrantedPermissions;
+    private HandlerThread mHandlerThread;
+    private TapTestNetworkTracker mTestNetworkTracker;
+
+    @Before
+    public void setUp() throws Exception {
+        mController =
+                mContext.getSystemService(ThreadNetworkManager.class)
+                        .getAllThreadNetworkControllers()
+                        .get(0);
+
+        mGrantedPermissions = new HashSet<String>();
+        mExecutor = Executors.newSingleThreadExecutor();
+        mNsdManager = mContext.getSystemService(NsdManager.class);
+        mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+        mHandlerThread.start();
+
+        setEnabledAndWait(mController, true);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        dropAllPermissions();
+        leaveAndWait(mController);
+        tearDownTestNetwork();
+    }
+
+    @Test
+    public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
+        assertThat(mController.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
+    }
+
+    @Test
+    public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = deviceRole::complete;
+
+        try {
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    () -> mController.registerStateCallback(mExecutor, callback));
+
+            assertThat(deviceRole.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+                    .isEqualTo(DEVICE_ROLE_STOPPED);
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+        }
+    }
+
+    @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<>();
+        EnabledStateListener listener = new EnabledStateListener(mController);
+
+        try {
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> {
+                        mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1));
+                    });
+            setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> {
+                        mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2));
+                    });
+            setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+            listener.expectThreadEnabledState(STATE_ENABLED);
+            listener.expectThreadEnabledState(STATE_DISABLING);
+            listener.expectThreadEnabledState(STATE_DISABLED);
+            listener.expectThreadEnabledState(STATE_ENABLED);
+        } finally {
+            listener.unregisterStateCallback();
+        }
+    }
+
+    @Test
+    public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
+        dropAllPermissions();
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.registerStateCallback(mExecutor, role -> {}));
+    }
+
+    @Test
+    public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+
+        mController.registerStateCallback(mExecutor, callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.registerStateCallback(mExecutor, callback));
+    }
+
+    @Test
+    public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+        runAsShell(
+                ACCESS_NETWORK_STATE, () -> mController.registerStateCallback(mExecutor, callback));
+
+        try {
+            dropAllPermissions();
+            assertThrows(
+                    SecurityException.class, () -> mController.unregisterStateCallback(callback));
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+        }
+    }
+
+    @Test
+    public void unregisterStateCallback_callbackRegistered_success() throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+
+        assertDoesNotThrow(() -> mController.registerStateCallback(mExecutor, callback));
+        mController.unregisterStateCallback(callback);
+    }
+
+    @Test
+    public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
+            throws Exception {
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterStateCallback(callback));
+    }
+
+    @Test
+    public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = deviceRole::complete;
+        mController.registerStateCallback(mExecutor, callback);
+        mController.unregisterStateCallback(callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterStateCallback(callback));
+    }
+
+    @Test
+    public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+        try {
+            mController.registerOperationalDatasetCallback(mExecutor, callback);
+
+            assertThat(activeFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+            assertThat(pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+        } finally {
+            mController.unregisterOperationalDatasetCallback(callback);
+        }
+    }
+
+    @Test
+    public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
+    }
+
+    @Test
+    public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+        mController.registerOperationalDatasetCallback(mExecutor, callback);
+
+        assertDoesNotThrow(() -> mController.unregisterOperationalDatasetCallback(callback));
+    }
+
+    @Test
+    public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
+
+        try {
+            dropAllPermissions();
+            assertThrows(
+                    SecurityException.class,
+                    () -> mController.unregisterOperationalDatasetCallback(callback));
+        } finally {
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> mController.unregisterOperationalDatasetCallback(callback));
+        }
+    }
+
+    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);
+            }
+        };
+    }
+
+    @Test
+    public void join_withPrivilegedPermission_success() throws Exception {
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        assertThat(isAttached(mController)).isTrue();
+        assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset);
+    }
+
+    @Test
+    public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
+        dropAllPermissions();
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+
+        assertThrows(
+                SecurityException.class, () -> mController.join(activeDataset, mExecutor, v -> {}));
+    }
+
+    @Test
+    public void join_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
+        setEnabledAndWait(mController, false);
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+
+        var thrown =
+                assertThrows(
+                        ExecutionException.class,
+                        () -> joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS));
+        var threadException = (ThreadNetworkException) thrown.getCause();
+        assertThat(threadException.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+    }
+
+    @Test
+    public void join_concurrentRequests_firstOneIsAborted() throws Exception {
+        final byte[] KEY_1 = new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
+        final byte[] KEY_2 = new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("TestNet", mController))
+                        .setNetworkKey(KEY_1)
+                        .build();
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset1).setNetworkKey(KEY_2).build();
+        CompletableFuture<Void> joinFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> joinFuture2 = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    mController.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
+                    mController.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
+                });
+
+        var thrown =
+                assertThrows(
+                        ExecutionException.class,
+                        () -> joinFuture1.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS));
+        var threadException = (ThreadNetworkException) thrown.getCause();
+        assertThat(threadException.getErrorCode()).isEqualTo(ERROR_ABORTED);
+        joinFuture2.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+        assertThat(isAttached(mController)).isTrue();
+        assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset2);
+    }
+
+    @Test
+    public void leave_withPrivilegedPermission_success() throws Exception {
+        CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+        joinRandomizedDatasetAndWait(mController);
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.leave(mExecutor, newOutcomeReceiver(leaveFuture)));
+        leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void leave_withoutPrivilegedPermission_throwsSecurityException() {
+        dropAllPermissions();
+
+        assertThrows(SecurityException.class, () -> mController.leave(mExecutor, v -> {}));
+    }
+
+    @Test
+    public void leave_threadDisabled_success() throws Exception {
+        setEnabledAndWait(mController, false);
+        CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+
+        leave(mController, newOutcomeReceiver(leaveFuture));
+        leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void leave_concurrentRequests_bothSuccess() throws Exception {
+        CompletableFuture<Void> leaveFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> leaveFuture2 = new CompletableFuture<>();
+        joinRandomizedDatasetAndWait(mController);
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    mController.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
+                    mController.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
+                });
+
+        leaveFuture1.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+        leaveFuture2.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("TestNet", mController))
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+                        .setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
+                        .build();
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset1)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+                        .setNetworkName("ThreadNet2")
+                        .build();
+        PendingOperationalDataset pendingDataset2 =
+                new PendingOperationalDataset(
+                        activeDataset2,
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+        mController.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        mController.scheduleMigration(
+                pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
+        migrateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+
+        CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>();
+        CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>();
+        OperationalDatasetCallback datasetCallback =
+                new OperationalDatasetCallback() {
+                    @Override
+                    public void onActiveOperationalDatasetChanged(
+                            ActiveOperationalDataset activeDataset) {
+                        if (activeDataset.equals(activeDataset2)) {
+                            dataset2IsApplied.complete(true);
+                        }
+                    }
+
+                    @Override
+                    public void onPendingOperationalDatasetChanged(
+                            PendingOperationalDataset pendingDataset) {
+                        if (pendingDataset == null) {
+                            pendingDatasetIsRemoved.complete(true);
+                        }
+                    }
+                };
+        mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+        try {
+            assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+            assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+        } finally {
+            mController.unregisterOperationalDatasetCallback(datasetCallback);
+        }
+    }
+
+    @Test
+    public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        PendingOperationalDataset pendingDataset =
+                new PendingOperationalDataset(
+                        newRandomizedDataset("TestNet", mController),
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+
+        mController.scheduleMigration(pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
+
+        ThreadNetworkException thrown =
+                (ThreadNetworkException)
+                        assertThrows(ExecutionException.class, migrateFuture::get).getCause();
+        assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+    }
+
+    @Test
+    public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        final ActiveOperationalDataset activeDataset =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("testNet", mController))
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+                        .build();
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+                        .setNetworkName("testNet1")
+                        .build();
+        PendingOperationalDataset pendingDataset1 =
+                new PendingOperationalDataset(
+                        activeDataset1,
+                        new OperationalDatasetTimestamp(100, 0, false),
+                        Duration.ofSeconds(30));
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+                        .setNetworkName("testNet2")
+                        .build();
+        PendingOperationalDataset pendingDataset2 =
+                new PendingOperationalDataset(
+                        activeDataset2,
+                        new OperationalDatasetTimestamp(20, 0, false),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
+        mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        mController.scheduleMigration(
+                pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+        migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+        mController.scheduleMigration(
+                pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+
+        ThreadNetworkException thrown =
+                (ThreadNetworkException)
+                        assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
+        assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
+    }
+
+    @Test
+    public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        final ActiveOperationalDataset activeDataset =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("validName", mController))
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+                        .build();
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+                        .setNetworkName("testNet1")
+                        .build();
+        PendingOperationalDataset pendingDataset1 =
+                new PendingOperationalDataset(
+                        activeDataset1,
+                        new OperationalDatasetTimestamp(100, 0, false),
+                        Duration.ofSeconds(30));
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+                        .setNetworkName("testNet2")
+                        .build();
+        PendingOperationalDataset pendingDataset2 =
+                new PendingOperationalDataset(
+                        activeDataset2,
+                        new OperationalDatasetTimestamp(200, 0, false),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
+        mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        mController.scheduleMigration(
+                pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+        migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+        mController.scheduleMigration(
+                pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+        migrateFuture2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+
+        CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>();
+        CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>();
+        OperationalDatasetCallback datasetCallback =
+                new OperationalDatasetCallback() {
+                    @Override
+                    public void onActiveOperationalDatasetChanged(
+                            ActiveOperationalDataset activeDataset) {
+                        if (activeDataset.equals(activeDataset2)) {
+                            dataset2IsApplied.complete(true);
+                        }
+                    }
+
+                    @Override
+                    public void onPendingOperationalDatasetChanged(
+                            PendingOperationalDataset pendingDataset) {
+                        if (pendingDataset == null) {
+                            pendingDatasetIsRemoved.complete(true);
+                        }
+                    }
+                };
+        mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+        try {
+            assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+            assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+        } finally {
+            mController.unregisterOperationalDatasetCallback(datasetCallback);
+        }
+    }
+
+    @Test
+    public void scheduleMigration_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        PendingOperationalDataset pendingDataset =
+                new PendingOperationalDataset(
+                        activeDataset,
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        joinRandomizedDatasetAndWait(mController);
+        CompletableFuture<Void> migrationFuture = new CompletableFuture<>();
+
+        setEnabledAndWait(mController, false);
+
+        scheduleMigration(mController, pendingDataset, newOutcomeReceiver(migrationFuture));
+
+        ThreadNetworkException thrown =
+                (ThreadNetworkException)
+                        assertThrows(ExecutionException.class, migrationFuture::get).getCause();
+        assertThat(thrown.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+    }
+
+    @Test
+    public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.createRandomizedDataset("", mExecutor, dataset -> {}));
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mController.createRandomizedDataset(
+                                "ANetNameIs17Bytes", mExecutor, dataset -> {}));
+    }
+
+    @Test
+    public void createRandomizedDataset_validNetworkName_success() throws Exception {
+        ActiveOperationalDataset dataset = newRandomizedDataset("validName", mController);
+
+        assertThat(dataset.getNetworkName()).isEqualTo("validName");
+        assertThat(dataset.getPanId()).isLessThan(0xffff);
+        assertThat(dataset.getChannelMask().size()).isAtLeast(1);
+        assertThat(dataset.getExtendedPanId()).hasLength(8);
+        assertThat(dataset.getNetworkKey()).hasLength(16);
+        assertThat(dataset.getPskc()).hasLength(16);
+        assertThat(dataset.getMeshLocalPrefix().getPrefixLength()).isEqualTo(64);
+        assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
+    }
+
+    @Test
+    public void setEnabled_permissionsGranted_succeeds() throws Exception {
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1)));
+        setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        waitForEnabledState(mController, booleanToEnabledState(false));
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2)));
+        setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        waitForEnabledState(mController, booleanToEnabledState(true));
+    }
+
+    @Test
+    public void setEnabled_noPermissions_throwsSecurityException() throws Exception {
+        CompletableFuture<Void> setFuture = new CompletableFuture<>();
+        assertThrows(
+                SecurityException.class,
+                () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture)));
+    }
+
+    @Test
+    public void setEnabled_disable_leavesThreadNetwork() throws Exception {
+        joinRandomizedDatasetAndWait(mController);
+        setEnabledAndWait(mController, false);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void setEnabled_enableFollowedByDisable_allSucceed() throws Exception {
+        joinRandomizedDatasetAndWait(mController);
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+        EnabledStateListener listener = new EnabledStateListener(mController);
+        listener.expectThreadEnabledState(STATE_ENABLED);
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture1));
+                    mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture2));
+                });
+        setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+        listener.expectThreadEnabledState(STATE_DISABLING);
+        listener.expectThreadEnabledState(STATE_DISABLED);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+        // FIXME: this is not called when a exception is thrown after the creation of `listener`
+        listener.unregisterStateCallback();
+    }
+
+    // TODO (b/322437869): add test case to verify when Thread is in DISABLING state, any commands
+    // (join/leave/scheduleMigration/setEnabled) fail with ERROR_BUSY. This is not currently tested
+    // because DISABLING has very short lifecycle, it's not possible to guarantee the command can be
+    // sent before state changes to DISABLED.
+
+    @Test
+    public void threadNetworkCallback_deviceAttached_threadNetworkIsAvailable() throws Exception {
+        CompletableFuture<Network> networkFuture = new CompletableFuture<>();
+        ConnectivityManager cm = mContext.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);
+                    }
+                };
+
+        joinRandomizedDatasetAndWait(mController);
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                () -> cm.registerNetworkCallback(networkRequest, networkCallback));
+
+        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);
+    }
+
+    private void grantPermissions(String... permissions) {
+        for (String permission : permissions) {
+            mGrantedPermissions.add(permission);
+        }
+        String[] allPermissions = new String[mGrantedPermissions.size()];
+        mGrantedPermissions.toArray(allPermissions);
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
+    }
+
+    @Test
+    public void meshcopService_threadEnabledButNotJoined_discoveredButNoNetwork() throws Exception {
+        setUpTestNetwork();
+
+        setEnabledAndWait(mController, true);
+        leaveAndWait(mController);
+
+        NsdServiceInfo serviceInfo =
+                expectServiceResolved(
+                        MESHCOP_SERVICE_TYPE,
+                        SERVICE_DISCOVERY_TIMEOUT_MILLIS,
+                        s -> s.getAttributes().get("at") == null);
+
+        Map<String, byte[]> txtMap = serviceInfo.getAttributes();
+
+        assertThat(txtMap.get("rv")).isNotNull();
+        assertThat(txtMap.get("tv")).isNotNull();
+        assertThat(txtMap.get("sb")).isNotNull();
+    }
+
+    @Test
+    @Ignore("b/333649897: Enable this when it's not flaky at all")
+    public void meshcopService_joinedNetwork_discoveredHasNetwork() throws Exception {
+        setUpTestNetwork();
+
+        String networkName = "TestNet" + new Random().nextInt(10_000);
+        joinRandomizedDatasetAndWait(mController, networkName);
+
+        Predicate<NsdServiceInfo> predicate =
+                serviceInfo ->
+                        serviceInfo.getAttributes().get("at") != null
+                                && Arrays.equals(
+                                        serviceInfo.getAttributes().get("nn"),
+                                        networkName.getBytes(StandardCharsets.UTF_8));
+
+        NsdServiceInfo resolvedService =
+                expectServiceResolved(
+                        MESHCOP_SERVICE_TYPE, SERVICE_DISCOVERY_TIMEOUT_MILLIS, predicate);
+
+        Map<String, byte[]> txtMap = resolvedService.getAttributes();
+        assertThat(txtMap.get("rv")).isNotNull();
+        assertThat(txtMap.get("tv")).isNotNull();
+        assertThat(txtMap.get("sb")).isNotNull();
+        assertThat(txtMap.get("id").length).isEqualTo(16);
+    }
+
+    @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_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, SERVICE_LOST_TIMEOUT_MILLIS));
+    }
+
+    private static void dropAllPermissions() {
+        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+    }
+
+    private static ActiveOperationalDataset newRandomizedDataset(
+            String networkName, ThreadNetworkController controller) throws Exception {
+        CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
+        controller.createRandomizedDataset(networkName, directExecutor(), future::complete);
+        return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
+    private static boolean isAttached(ThreadNetworkController controller) throws Exception {
+        return ThreadNetworkController.isAttached(getDeviceRole(controller));
+    }
+
+    private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        StateCallback callback = future::complete;
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                () -> controller.registerStateCallback(directExecutor(), callback));
+        try {
+            return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> controller.unregisterStateCallback(callback));
+        }
+    }
+
+    private static int waitForAttachedState(ThreadNetworkController controller) throws Exception {
+        List<Integer> attachedRoles = new ArrayList<>();
+        attachedRoles.add(DEVICE_ROLE_CHILD);
+        attachedRoles.add(DEVICE_ROLE_ROUTER);
+        attachedRoles.add(DEVICE_ROLE_LEADER);
+        return waitForStateAnyOf(controller, attachedRoles);
+    }
+
+    private static int waitForStateAnyOf(
+            ThreadNetworkController controller, List<Integer> deviceRoles) throws Exception {
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        StateCallback callback =
+                newRole -> {
+                    if (deviceRoles.contains(newRole)) {
+                        future.complete(newRole);
+                    }
+                };
+        controller.registerStateCallback(directExecutor(), callback);
+        int role = future.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+        controller.unregisterStateCallback(callback);
+        return role;
+    }
+
+    private static void waitForEnabledState(ThreadNetworkController controller, int state)
+            throws Exception {
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        StateCallback callback =
+                new ThreadNetworkController.StateCallback() {
+                    @Override
+                    public void onDeviceRoleChanged(int r) {}
+
+                    @Override
+                    public void onThreadEnableStateChanged(int enabled) {
+                        if (enabled == state) {
+                            future.complete(enabled);
+                        }
+                    }
+                };
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                () -> controller.registerStateCallback(directExecutor(), callback));
+        future.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        runAsShell(ACCESS_NETWORK_STATE, () -> controller.unregisterStateCallback(callback));
+    }
+
+    private void leave(
+            ThreadNetworkController controller,
+            OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        runAsShell(THREAD_NETWORK_PRIVILEGED, () -> controller.leave(mExecutor, receiver));
+    }
+
+    private void leaveAndWait(ThreadNetworkController controller) throws Exception {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        leave(controller, future::complete);
+        future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
+    private void scheduleMigration(
+            ThreadNetworkController controller,
+            PendingOperationalDataset pendingDataset,
+            OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> controller.scheduleMigration(pendingDataset, mExecutor, receiver));
+    }
+
+    private class EnabledStateListener {
+        private ArrayTrackRecord<Integer> mEnabledStates = new ArrayTrackRecord<>();
+        private final ArrayTrackRecord<Integer>.ReadHead mReadHead = mEnabledStates.newReadHead();
+        ThreadNetworkController mController;
+        StateCallback mCallback =
+                new ThreadNetworkController.StateCallback() {
+                    @Override
+                    public void onDeviceRoleChanged(int r) {}
+
+                    @Override
+                    public void onThreadEnableStateChanged(int enabled) {
+                        mEnabledStates.add(enabled);
+                    }
+                };
+
+        EnabledStateListener(ThreadNetworkController controller) {
+            this.mController = controller;
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    () -> controller.registerStateCallback(mExecutor, mCallback));
+        }
+
+        public void expectThreadEnabledState(int 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() {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
+        }
+    }
+
+    private int booleanToEnabledState(boolean enabled) {
+        return enabled ? STATE_ENABLED : STATE_DISABLED;
+    }
+
+    private void setEnabledAndWait(ThreadNetworkController controller, boolean enabled)
+            throws Exception {
+        CompletableFuture<Void> setFuture = new CompletableFuture<>();
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> controller.setEnabled(enabled, mExecutor, newOutcomeReceiver(setFuture)));
+        setFuture.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        waitForEnabledState(controller, booleanToEnabledState(enabled));
+    }
+
+    private CompletableFuture joinRandomizedDataset(
+            ThreadNetworkController controller, String networkName) throws Exception {
+        ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+        return joinFuture;
+    }
+
+    private void joinRandomizedDatasetAndWait(ThreadNetworkController controller) throws Exception {
+        joinRandomizedDatasetAndWait(controller, "TestNet");
+    }
+
+    private void joinRandomizedDatasetAndWait(
+            ThreadNetworkController controller, String networkName) throws Exception {
+        CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller, networkName);
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+        assertThat(isAttached(controller)).isTrue();
+    }
+
+    private static ActiveOperationalDataset getActiveOperationalDataset(
+            ThreadNetworkController controller) throws Exception {
+        CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
+        OperationalDatasetCallback callback = future::complete;
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                THREAD_NETWORK_PRIVILEGED,
+                () -> controller.registerOperationalDatasetCallback(directExecutor(), callback));
+        try {
+            return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+        } finally {
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> controller.unregisterOperationalDatasetCallback(callback));
+        }
+    }
+
+    private static PendingOperationalDataset getPendingOperationalDataset(
+            ThreadNetworkController controller) throws Exception {
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        controller.registerOperationalDatasetCallback(
+                directExecutor(), newDatasetCallback(activeFuture, pendingFuture));
+        return pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
+    private static OperationalDatasetCallback newDatasetCallback(
+            CompletableFuture<ActiveOperationalDataset> activeFuture,
+            CompletableFuture<PendingOperationalDataset> pendingFuture) {
+        return new OperationalDatasetCallback() {
+            @Override
+            public void onActiveOperationalDatasetChanged(
+                    ActiveOperationalDataset activeOpDataset) {
+                activeFuture.complete(activeOpDataset);
+            }
+
+            @Override
+            public void onPendingOperationalDatasetChanged(
+                    PendingOperationalDataset pendingOpDataset) {
+                pendingFuture.complete(pendingOpDataset);
+            }
+        };
+    }
+
+    private static void assertDoesNotThrow(ThrowingRunnable runnable) {
+        try {
+            runnable.run();
+        } catch (Throwable e) {
+            fail("Should not have thrown " + e);
+        }
+    }
+
+    // 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() {
+                    @Override
+                    public void onServiceFound(NsdServiceInfo serviceInfo) {
+                        serviceInfoFuture.complete(serviceInfo);
+                    }
+                };
+        mNsdManager.discoverServices(
+                serviceType,
+                NsdManager.PROTOCOL_DNS_SD,
+                mTestNetworkTracker.getNetwork(),
+                mExecutor,
+                listener);
+        try {
+            serviceInfoFuture.get(timeoutMilliseconds, MILLISECONDS);
+        } finally {
+            mNsdManager.stopServiceDiscovery(listener);
+        }
+
+        return serviceInfoFuture.get();
+    }
+
+    private NsdManager.DiscoveryListener discoverForServiceLost(
+            String serviceType, CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
+        NsdManager.DiscoveryListener listener =
+                new DefaultDiscoveryListener() {
+                    @Override
+                    public void onServiceLost(NsdServiceInfo serviceInfo) {
+                        serviceInfoFuture.complete(serviceInfo);
+                    }
+                };
+        mNsdManager.discoverServices(
+                serviceType,
+                NsdManager.PROTOCOL_DNS_SD,
+                mTestNetworkTracker.getNetwork(),
+                mExecutor,
+                listener);
+        return listener;
+    }
+
+    private NsdServiceInfo expectServiceResolved(
+            String serviceType, int timeoutMilliseconds, Predicate<NsdServiceInfo> predicate)
+            throws Exception {
+        NsdServiceInfo discoveredServiceInfo = discoverService(serviceType);
+        CompletableFuture<NsdServiceInfo> future = new CompletableFuture<>();
+        NsdManager.ServiceInfoCallback callback =
+                new DefaultServiceInfoCallback() {
+                    @Override
+                    public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
+                        if (predicate.test(serviceInfo)) {
+                            future.complete(serviceInfo);
+                        }
+                    }
+                };
+        mNsdManager.registerServiceInfoCallback(discoveredServiceInfo, mExecutor, callback);
+        try {
+            return future.get(timeoutMilliseconds, MILLISECONDS);
+        } finally {
+            mNsdManager.unregisterServiceInfoCallback(callback);
+        }
+    }
+
+    private void setUpTestNetwork() {
+        assertThat(mTestNetworkTracker).isNull();
+        mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
+    }
+
+    private void tearDownTestNetwork() throws InterruptedException {
+        if (mTestNetworkTracker != null) {
+            mTestNetworkTracker.tearDown();
+        }
+        mHandlerThread.quitSafely();
+        mHandlerThread.join();
+    }
+
+    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() {}
+    }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
new file mode 100644
index 0000000..4de2e13
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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 android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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 =
+                new ThreadNetworkException(ERROR_THREAD_DISABLED, "Thread disabled error!");
+        ThreadNetworkException errorInternalError =
+                new ThreadNetworkException(ERROR_INTERNAL_ERROR, "internal error!");
+
+        assertThat(errorThreadDisabled.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+        assertThat(errorThreadDisabled.getMessage()).isEqualTo("Thread disabled error!");
+        assertThat(errorInternalError.getErrorCode()).isEqualTo(ERROR_INTERNAL_ERROR);
+        assertThat(errorInternalError.getMessage()).isEqualTo("internal error!");
+    }
+
+    @Test
+    public void constructor_nullMessage_throwsNullPointerException() throws Exception {
+        assertThrows(
+                NullPointerException.class,
+                () -> new ThreadNetworkException(ERROR_UNKNOWN, null /* message */));
+    }
+
+    @Test
+    public void constructor_tooSmallErrorCode_throwsIllegalArgumentException() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> new ThreadNetworkException(0, "0"));
+        // TODO: add argument check for too large error code when mainline CTS is ready. This was
+        // not added here for CTS forward copatibility.
+    }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
new file mode 100644
index 0000000..b6d0d31
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.thread.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkManager;
+import android.os.Build;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** Tests for {@link ThreadNetworkManager}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ThreadNetworkManagerTest {
+    @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final PackageManager mPackageManager = mContext.getPackageManager();
+
+    private ThreadNetworkManager mManager;
+
+    @Before
+    public void setUp() {
+        mManager = mContext.getSystemService(ThreadNetworkManager.class);
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
+    public void getManager_onTOrLower_returnsNull() {
+        assertThat(mManager).isNull();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void getManager_hasThreadFeatureOnVOrHigher_returnsNonNull() {
+        assumeTrue(mPackageManager.hasSystemFeature("android.hardware.thread_network"));
+
+        assertThat(mManager).isNotNull();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void getManager_onUButNotTv_returnsNull() {
+        assumeFalse(mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+
+        assertThat(mManager).isNull();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void getManager_onUAndTv_returnsNonNull() {
+        assumeTrue(mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+
+        assertThat(mManager).isNotNull();
+    }
+
+    @Test
+    public void getManager_noThreadFeature_returnsNull() {
+        assumeFalse(mPackageManager.hasSystemFeature("android.hardware.thread_network"));
+
+        assertThat(mManager).isNull();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void getAllThreadNetworkControllers_managerIsNotNull_returnsNotEmptyList() {
+        assumeNotNull(mManager);
+
+        List<ThreadNetworkController> controllers = mManager.getAllThreadNetworkControllers();
+
+        assertThat(controllers).isNotEmpty();
+    }
+}
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
new file mode 100644
index 0000000..71693af
--- /dev/null
+++ b/thread/tests/integration/Android.bp
@@ -0,0 +1,63 @@
+//
+// 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_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "ThreadNetworkIntegrationTestsDefaults",
+    min_sdk_version: "30",
+    static_libs: [
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "guava",
+        "mockito-target-minus-junit4",
+        "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",
+        "android.test.base",
+        "android.test.mock",
+    ],
+}
+
+android_test {
+    name: "ThreadNetworkIntegrationTests",
+    platform_apis: true,
+    manifest: "AndroidManifest.xml",
+    test_config: "AndroidTest.xml",
+    defaults: [
+        "framework-connectivity-test-defaults",
+        "ThreadNetworkIntegrationTestsDefaults",
+    ],
+    test_suites: [
+        "mts-tethering",
+        "general-tests",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+    compile_multilib: "both",
+}
diff --git a/thread/tests/integration/AndroidManifest.xml b/thread/tests/integration/AndroidManifest.xml
new file mode 100644
index 0000000..a049184
--- /dev/null
+++ b/thread/tests/integration/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?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="com.android.thread.tests.integration">
+
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <!-- The test need CHANGE_NETWORK_STATE permission to use requestNetwork API to setup test
+         network. Since R shell application don't have such permission, grant permission to the test
+         here. TODO: Remove CHANGE_NETWORK_STATE permission here and use adopt shell permission to
+         obtain CHANGE_NETWORK_STATE for testing once R device is no longer supported. -->
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
+    <uses-permission android:name="android.permission.NETWORK_SETTINGS"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.thread.tests.integration"
+        android:label="Thread integration tests">
+    </instrumentation>
+</manifest>
diff --git a/thread/tests/integration/AndroidTest.xml b/thread/tests/integration/AndroidTest.xml
new file mode 100644
index 0000000..8f98941
--- /dev/null
+++ b/thread/tests/integration/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 integration tests">
+    <option name="test-tag" value="ThreadNetworkIntegrationTests" />
+    <option name="test-suite-tag" value="apct" />
+
+    <!--
+        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" />
+
+    <!-- Run tests in MTS only 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>
+
+    <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 -->
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="ThreadNetworkIntegrationTests.apk" />
+        <option name="check-min-sdk" value="true" />
+        <option name="cleanup-apks" value="true" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.thread.tests.integration" />
+    </test>
+</configuration>
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
new file mode 100644
index 0000000..8c63d37
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -0,0 +1,687 @@
+/*
+ * 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.thread;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
+import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
+import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
+import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
+import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
+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 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.TestNetworkTrackerKt.initTestNetwork;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+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 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.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;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.TapPacketReader;
+import com.android.testutils.TestNetworkTracker;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+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) 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);
+
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final ThreadNetworkControllerWrapper mController =
+            ThreadNetworkControllerWrapper.newInstance(mContext);
+    private OtDaemonController mOtCtl;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private TestNetworkTracker mInfraNetworkTracker;
+    private List<FullThreadDevice> mFtds;
+    private TapPacketReader mInfraNetworkReader;
+    private InfraNetworkDevice mInfraDevice;
+
+    @Before
+    public void setUp() throws Exception {
+        // 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();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mFtds = new ArrayList<>();
+
+        setUpInfraNetwork();
+        mController.setEnabledAndWait(true);
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        // Creates a infra network device.
+        mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+        startInfraDeviceAndWaitForOnLinkAddr();
+
+        // Create Ftds
+        for (int i = 0; i < NUM_FTD; ++i) {
+            mFtds.add(new FullThreadDevice(15 + i /* node ID */));
+        }
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mController.setTestNetworkAsUpstreamAndWait(null);
+        mController.leaveAndWait();
+        tearDownInfraNetwork();
+
+        mHandlerThread.quitSafely();
+        mHandlerThread.join();
+
+        for (var ftd : mFtds) {
+            ftd.destroy();
+        }
+        mFtds.clear();
+    }
+
+    @Test
+    public void unicastRouting_infraDevicePingThreadDeviceOmr_replyReceived() throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+
+        mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+
+        // Infra device receives an echo reply sent by FTD.
+        assertNotNull(pollForPacketOnInfraNetwork(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);
+        startFtdChild(ftd);
+
+        mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+
+        assertNotNull(pollForPacketOnInfraNetwork(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);
+        startFtdChild(ftd);
+        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(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+        } finally {
+            runAsShell(MANAGE_TEST_NETWORKS, () -> oldInfraNetworkTracker.teardown());
+        }
+    }
+
+    @Test
+    public void unicastRouting_borderRouterSendsUdpToThreadDevice_datagramReceived()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                   Thread
+         * Border Router -------------- Full Thread device
+         *  (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+        Inet6Address ftdOmr = requireNonNull(ftd.getOmrAddress());
+        Inet6Address ftdMlEid = requireNonNull(ftd.getMlEid());
+
+        ftd.udpBind(ftdOmr, 12345);
+        sendUdpMessage(ftdOmr, 12345, "aaaaaaaa");
+        assertEquals("aaaaaaaa", ftd.udpReceive());
+
+        ftd.udpBind(ftdMlEid, 12345);
+        sendUdpMessage(ftdMlEid, 12345, "bbbbbbbb");
+        assertEquals("bbbbbbbb", ftd.udpReceive());
+    }
+
+    @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 {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+
+        ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_5);
+
+        assertInfraLinkMemberOfGroup(GROUP_ADDR_SCOPE_5);
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void
+            multicastRouting_ftdSubscribedScope3MulticastAddress_infraLinkNotJoinMulticastGroup()
+                    throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+
+        ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
+
+        assertInfraLinkNotMemberOfGroup(GROUP_ADDR_SCOPE_3);
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_ftdSubscribedMulticastAddress_canPingfromInfraLink()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+        subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
+
+        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_inboundForwarding_afterBrRejoinFtdRepliesSubscribedAddress()
+            throws Exception {
+
+        // 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
+        // supported, to be implemented when possible.
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_ftdSubscribedScope3MulticastAddress_cannotPingfromInfraLink()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+        ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
+
+        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_3);
+
+        assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_ftdNotSubscribedMulticastAddress_cannotPingFromInfraDevice()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+
+        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
+
+        assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_multipleFtdsSubscribedDifferentAddresses_canPingFromInfraDevice()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device 1
+         *                                   (Cuttlefish)
+         *                                         |
+         *                                         | Thread
+         *                                         |
+         *                                  Full Thread device 2
+         * </pre>
+         */
+
+        FullThreadDevice ftd1 = mFtds.get(0);
+        startFtdChild(ftd1);
+        subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
+
+        FullThreadDevice ftd2 = mFtds.get(1);
+        startFtdChild(ftd2);
+        subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
+
+        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+        assertNotNull(pollForPacketOnInfraNetwork(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()));
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_multipleFtdsSubscribedSameAddress_canPingFromInfraDevice()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device 1
+         *                                   (Cuttlefish)
+         *                                         |
+         *                                         | Thread
+         *                                         |
+         *                                  Full Thread device 2
+         * </pre>
+         */
+
+        FullThreadDevice ftd1 = mFtds.get(0);
+        startFtdChild(ftd1);
+        subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
+
+        FullThreadDevice ftd2 = mFtds.get(1);
+        startFtdChild(ftd2);
+        subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
+
+        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+        assertNotNull(pollForPacketOnInfraNetwork(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()));
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_outboundForwarding_scopeLargerThan3IsForwarded() throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+        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));
+        assertNotNull(
+                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_outboundForwarding_scopeSmallerThan4IsNotForwarded()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+
+        ftd.ping(GROUP_ADDR_SCOPE_3);
+
+        assertNull(
+                pollForPacketOnInfraNetwork(
+                        ICMPV6_ECHO_REQUEST_TYPE, ftd.getOmrAddress(), GROUP_ADDR_SCOPE_3));
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_outboundForwarding_llaToScope4IsNotForwarded() throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+        Inet6Address ftdLla = ftd.getLinkLocalAddress();
+        assertNotNull(ftdLla);
+
+        ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla);
+
+        assertNull(
+                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_outboundForwarding_mlaToScope4IsNotForwarded() throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+        List<Inet6Address> ftdMlas = ftd.getMeshLocalAddresses();
+        assertFalse(ftdMlas.isEmpty());
+
+        for (Inet6Address ftdMla : ftdMlas) {
+            ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla);
+
+            assertNull(
+                    pollForPacketOnInfraNetwork(
+                            ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
+        }
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_infraNetworkSwitch_ftdRepliesToSubscribedAddress()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+        subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
+        Inet6Address ftdOmr = ftd.getOmrAddress();
+
+        // Destroy infra link and re-create
+        tearDownInfraNetwork();
+        setUpInfraNetwork();
+        mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+        startInfraDeviceAndWaitForOnLinkAddr();
+
+        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
+    public void multicastRouting_infraNetworkSwitch_outboundPacketIsForwarded() throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+        Inet6Address ftdOmr = ftd.getOmrAddress();
+
+        // Destroy infra link and re-create
+        tearDownInfraNetwork();
+        setUpInfraNetwork();
+        mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+        startInfraDeviceAndWaitForOnLinkAddr();
+
+        ftd.ping(GROUP_ADDR_SCOPE_4);
+
+        assertNotNull(
+                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+    }
+
+    private void setUpInfraNetwork() throws Exception {
+        mInfraNetworkTracker =
+                runAsShell(
+                        MANAGE_TEST_NETWORKS,
+                        () ->
+                                initTestNetwork(
+                                        mContext, new LinkProperties(), 5000 /* timeoutMs */));
+        mController.setTestNetworkAsUpstreamAndWait(
+                mInfraNetworkTracker.getTestIface().getInterfaceName());
+    }
+
+    private void tearDownInfraNetwork() {
+        runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+    }
+
+    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 startInfraDeviceAndWaitForOnLinkAddr() throws Exception {
+        mInfraDevice =
+                new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
+        mInfraDevice.runSlaac(Duration.ofSeconds(60));
+        assertNotNull(mInfraDevice.ipv6Addr);
+    }
+
+    private void assertInfraLinkMemberOfGroup(Inet6Address address) throws Exception {
+        waitFor(
+                () ->
+                        isInMulticastGroup(
+                                mInfraNetworkTracker.getTestIface().getInterfaceName(), address),
+                Duration.ofSeconds(3));
+    }
+
+    private void assertInfraLinkNotMemberOfGroup(Inet6Address address) throws Exception {
+        waitFor(
+                () ->
+                        !isInMulticastGroup(
+                                mInfraNetworkTracker.getTestIface().getInterfaceName(), address),
+                Duration.ofSeconds(3));
+    }
+
+    private void subscribeMulticastAddressAndWait(FullThreadDevice ftd, Inet6Address address)
+            throws Exception {
+        ftd.subscribeMulticastAddress(address);
+
+        assertInfraLinkMemberOfGroup(address);
+    }
+
+    private byte[] pollForPacketOnInfraNetwork(int type, Inet6Address srcAddress) {
+        return pollForPacketOnInfraNetwork(type, srcAddress, null);
+    }
+
+    private byte[] pollForPacketOnInfraNetwork(
+            int type, Inet6Address srcAddress, Inet6Address destAddress) {
+        Predicate<byte[]> filter;
+        filter =
+                p ->
+                        (isExpectedIcmpv6Packet(p, type)
+                                && (srcAddress == null ? true : isFromIpv6Source(p, srcAddress))
+                                && (destAddress == null
+                                        ? true
+                                        : isToIpv6Destination(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..e10f134
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -0,0 +1,502 @@
+/*
+ * 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.JOIN_TIMEOUT;
+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.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) {
+            ftd.joinNetwork(DEFAULT_DATASET);
+            ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        }
+
+        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);
+        ftd.joinNetwork(DEFAULT_DATASET);
+        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        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);
+        ftd.joinNetwork(DEFAULT_DATASET);
+        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        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);
+            ftd.joinNetwork(DEFAULT_DATASET);
+            ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+            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);
+        srpClient.joinNetwork(DEFAULT_DATASET);
+        srpClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        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);
+        dnsClient.joinNetwork(DEFAULT_DATASET);
+        dnsClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        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
new file mode 100644
index 0000000..61b6eac
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -0,0 +1,369 @@
+/*
+ * 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.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.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+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.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.truth.Truth.assertWithMessage;
+
+import android.content.Context;
+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;
+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.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.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 {
+    // 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 =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+    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 {
+        mExecutor = Executors.newSingleThreadExecutor();
+        mOtCtl = new OtDaemonController();
+        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 {
+        mController.setTestNetworkAsUpstreamAndWait(null);
+        mController.leaveAndWait();
+
+        mFtd.destroy();
+        mExecutor.shutdownNow();
+    }
+
+    @Test
+    public void otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped() throws Exception {
+        mController.leaveAndWait();
+
+        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);
+    }
+
+    @Test
+    public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoinedAndTunIfStateConsistent()
+            throws Exception {
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        runShellCommand("stop ot-daemon");
+
+        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 {
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        mOtCtl.factoryReset();
+
+        assertThat(mController.getDeviceRole()).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void otDaemonFactoryReset_addressesRemoved() throws Exception {
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        mOtCtl.factoryReset();
+
+        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_otAndTunAddressesMatch() throws Exception {
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        List<Inet6Address> otAddresses = mOtCtl.getAddresses();
+        assertThat(otAddresses).isNotEmpty();
+        // 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 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 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
new file mode 100644
index 0000000..c0a8eea
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -0,0 +1,613 @@
+/*
+ * 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.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 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;
+import java.io.IOException;
+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;
+
+/**
+ * A class that launches and controls a simulation Full Thread Device (FTD).
+ *
+ * <p>This class launches an `ot-cli-ftd` process and communicates with it via command line input
+ * and output. See <a
+ * href="https://github.com/openthread/openthread/blob/main/src/cli/README.md">this page</a> for
+ * 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;
+
+    /**
+     * Constructs a {@link FullThreadDevice} for the given node ID.
+     *
+     * <p>It launches an `ot-cli-ftd` process using the given node ID. The node ID is an integer in
+     * range [1, OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE]. `OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE`
+     * is defined in `external/openthread/examples/platforms/simulation/platform-config.h`.
+     *
+     * @param nodeId the node ID for the simulation Full Thread Device.
+     * @throws IllegalStateException the node ID is already occupied by another simulation Thread
+     *     device.
+     */
+    public FullThreadDevice(int nodeId) {
+        try {
+            mProcess = Runtime.getRuntime().exec("/system/bin/ot-cli-ftd -Leth1 " + nodeId);
+        } catch (IOException 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();
+    }
+
+    /**
+     * Returns an OMR (Off-Mesh-Routable) address on this device if any.
+     *
+     * <p>This methods goes through all unicast addresses on the device and returns the first
+     * address which is neither link-local nor mesh-local.
+     */
+    public Inet6Address getOmrAddress() {
+        List<String> addresses = executeCommand("ipaddr");
+        IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
+        for (String address : addresses) {
+            if (address.startsWith("fe80:")) {
+                continue;
+            }
+            Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
+            if (!meshLocalPrefix.contains(addr)) {
+                return addr;
+            }
+        }
+        return null;
+    }
+
+    /** Returns the Mesh-local EID address on this device if any. */
+    public Inet6Address getMlEid() {
+        List<String> addresses = executeCommand("ipaddr mleid");
+        return (Inet6Address) InetAddresses.parseNumericAddress(addresses.get(0));
+    }
+
+    /**
+     * Returns the link-local address of the device.
+     *
+     * <p>This methods goes through all unicast addresses on the device and returns the address that
+     * begins with fe80.
+     */
+    public Inet6Address getLinkLocalAddress() {
+        List<String> output = executeCommand("ipaddr linklocal");
+        if (!output.isEmpty() && output.get(0).startsWith("fe80:")) {
+            return (Inet6Address) InetAddresses.parseNumericAddress(output.get(0));
+        }
+        return null;
+    }
+
+    /**
+     * Returns the mesh-local addresses of the device.
+     *
+     * <p>This methods goes through all unicast addresses on the device and returns the address that
+     * begins with mesh-local prefix.
+     */
+    public List<Inet6Address> getMeshLocalAddresses() {
+        List<String> addresses = executeCommand("ipaddr");
+        List<Inet6Address> meshLocalAddresses = new ArrayList<>();
+        IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
+        for (String address : addresses) {
+            Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
+            if (meshLocalPrefix.contains(addr)) {
+                meshLocalAddresses.add(addr);
+            }
+        }
+        return meshLocalAddresses;
+    }
+
+    /**
+     * Joins the Thread network using the given {@link ActiveOperationalDataset}.
+     *
+     * @param dataset the Active Operational Dataset
+     */
+    public void joinNetwork(ActiveOperationalDataset dataset) {
+        mActiveOperationalDataset = dataset;
+        executeCommand("dataset set active " + base16().lowerCase().encode(dataset.toThreadTlvs()));
+        executeCommand("ifconfig up");
+        executeCommand("thread start");
+    }
+
+    /** Stops the Thread network radio. */
+    public void stopThreadRadio() {
+        executeCommand("thread stop");
+        executeCommand("ifconfig down");
+    }
+
+    /**
+     * Waits for the Thread device to enter the any state of the given {@link List<String>}.
+     *
+     * @param states the list of states to wait for. Valid states are "disabled", "detached",
+     *     "child", "router" and "leader".
+     * @param timeout the time to wait for the expected state before throwing
+     */
+    public void waitForStateAnyOf(List<String> states, Duration timeout) throws TimeoutException {
+        waitFor(() -> states.contains(getState()), timeout);
+    }
+
+    /**
+     * Gets the state of the Thread device.
+     *
+     * @return a string representing the state.
+     */
+    public String getState() {
+        return executeCommand("state").get(0);
+    }
+
+    /** Closes the UDP socket. */
+    public void udpClose() {
+        executeCommand("udp close");
+    }
+
+    /** Opens the UDP socket. */
+    public void udpOpen() {
+        executeCommand("udp open");
+    }
+
+    /** Opens the UDP socket and binds it to a specific address and port. */
+    public void udpBind(Inet6Address address, int port) {
+        udpClose();
+        udpOpen();
+        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(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);
+        }
+        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 {
+            mWriter.write("factoryreset\n");
+            mWriter.flush();
+            // fill the input buffer to avoid truncating next command
+            for (int i = 0; i < 1000; ++i) {
+                mWriter.write("\n");
+            }
+            mWriter.flush();
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to run factoryreset on ot-cli-ftd", e);
+        }
+    }
+
+    public void subscribeMulticastAddress(Inet6Address address) {
+        executeCommand("ipmaddr add " + address.getHostAddress());
+    }
+
+    public void ping(Inet6Address address, Inet6Address source) {
+        ping(
+                address,
+                source,
+                PING_SIZE,
+                1 /* count */,
+                PING_INTERVAL,
+                HOP_LIMIT,
+                PING_TIMEOUT_0_1_SECOND);
+    }
+
+    public void ping(Inet6Address 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(Inet6Address 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(
+            Inet6Address address,
+            Inet6Address source,
+            int size,
+            int count,
+            int interval,
+            int hopLimit,
+            float timeout) {
+        String cmd =
+                "ping"
+                        + ((source == null) ? "" : (" -I " + source.getHostAddress()))
+                        + " "
+                        + address.getHostAddress()
+                        + " "
+                        + size
+                        + " "
+                        + count
+                        + " "
+                        + interval
+                        + " "
+                        + hopLimit
+                        + " "
+                        + timeout;
+        return executeCommand(cmd);
+    }
+
+    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;
+    }
+
+    @FormatMethod
+    private List<String> executeCommand(String commandFormat, Object... args) {
+        return executeCommand(String.format(commandFormat, args));
+    }
+
+    private List<String> executeCommand(String command) {
+        try {
+            mWriter.write(command + "\n");
+            mWriter.flush();
+        } catch (IOException e) {
+            throw new IllegalStateException(
+                    "Failed to write the command " + command + " to ot-cli-ftd", e);
+        }
+        try {
+            return readUntilDone();
+        } catch (IOException e) {
+            throw new IllegalStateException(
+                    "Failed to read the ot-cli-ftd output of command: " + command, e);
+        }
+    }
+
+    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 = readLine()) != null) {
+            if (line.equals("Done")) {
+                break;
+            }
+            if (line.startsWith("Error")) {
+                throw new IOException("ot-cli-ftd reported an error: " + line);
+            }
+            if (!line.startsWith("> ")) {
+                result.add(line);
+            }
+        }
+        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/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
new file mode 100644
index 0000000..72a278c
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
@@ -0,0 +1,129 @@
+/*
+ * 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.thread.utils;
+
+import static android.net.thread.utils.IntegrationTestUtils.getRaPios;
+import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+
+import android.net.InetAddresses;
+import android.net.MacAddress;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.testutils.TapPacketReader;
+
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A class that simulates a device on the infrastructure network.
+ *
+ * <p>This class directly interacts with the TUN interface of the test network to pretend there's a
+ * device on the infrastructure network.
+ */
+public final class InfraNetworkDevice {
+    // The MAC address of this device.
+    public final MacAddress macAddr;
+    // The packet reader of the TUN interface of the test network.
+    public final TapPacketReader packetReader;
+    // The IPv6 address generated by SLAAC for the device.
+    public Inet6Address ipv6Addr;
+
+    /**
+     * Constructs an InfraNetworkDevice with the given {@link MAC address} and {@link
+     * TapPacketReader}.
+     *
+     * @param macAddr the MAC address of the device
+     * @param packetReader the packet reader of the TUN interface of the test network.
+     */
+    public InfraNetworkDevice(MacAddress macAddr, TapPacketReader packetReader) {
+        this.macAddr = macAddr;
+        this.packetReader = packetReader;
+    }
+
+    /**
+     * Sends an ICMPv6 echo request message to the given {@link Inet6Address}.
+     *
+     * @param dstAddr the destination address of the packet.
+     * @throws IOException when it fails to send the packet.
+     */
+    public void sendEchoRequest(Inet6Address dstAddr) throws IOException {
+        ByteBuffer icmp6Packet = Ipv6Utils.buildEchoRequestPacket(ipv6Addr, dstAddr);
+        packetReader.sendResponse(icmp6Packet);
+    }
+
+    /**
+     * Sends an ICMPv6 Router Solicitation (RS) message to all routers on the network.
+     *
+     * @throws IOException when it fails to send the packet.
+     */
+    public void sendRsPacket() throws IOException {
+        ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, macAddr);
+        ByteBuffer rs =
+                Ipv6Utils.buildRsPacket(
+                        (Inet6Address) InetAddresses.parseNumericAddress("fe80::1"),
+                        IPV6_ADDR_ALL_ROUTERS_MULTICAST,
+                        slla);
+        packetReader.sendResponse(rs);
+    }
+
+    /**
+     * Runs SLAAC to generate an IPv6 address for the device.
+     *
+     * <p>The devices sends an RS message, processes the received RA messages and generates an IPv6
+     * address if there's any available Prefix Information Option (PIO). For now it only generates
+     * one address in total and doesn't track the expiration.
+     *
+     * @param timeoutSeconds the number of seconds to wait for.
+     * @throws TimeoutException when the device fails to generate a SLAAC address in given timeout.
+     */
+    public void runSlaac(Duration timeout) throws TimeoutException {
+        waitFor(() -> (ipv6Addr = runSlaac()) != null, timeout);
+    }
+
+    private Inet6Address runSlaac() {
+        try {
+            sendRsPacket();
+
+            final byte[] raPacket = pollForPacket(packetReader, p -> !getRaPios(p).isEmpty());
+
+            final List<PrefixInformationOption> options = getRaPios(raPacket);
+
+            for (PrefixInformationOption pio : options) {
+                if (pio.validLifetime > 0 && pio.preferredLifetime > 0) {
+                    final byte[] addressBytes = pio.prefix;
+                    addressBytes[addressBytes.length - 1] = (byte) (new Random()).nextInt();
+                    addressBytes[addressBytes.length - 2] = (byte) (new Random()).nextInt();
+                    return (Inet6Address) InetAddress.getByAddress(addressBytes);
+                }
+            }
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to generate an address by SLAAC", e);
+        }
+        return null;
+    }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
new file mode 100644
index 0000000..ada46c8
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -0,0 +1,474 @@
+/*
+ * 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.thread.utils;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
+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.util.concurrent.MoreExecutors.directExecutor;
+
+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.ThreadNetworkController;
+import android.os.Build;
+import android.os.Handler;
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.testutils.HandlerUtils;
+import com.android.testutils.TapPacketReader;
+
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+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;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/** Static utility methods relating to Thread integration tests. */
+public final class IntegrationTestUtils {
+    // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
+    // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
+    // seconds to be safe
+    public static final Duration RESTART_JOIN_TIMEOUT = Duration.ofSeconds(40);
+    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);
+
+    private IntegrationTestUtils() {}
+
+    /**
+     * Waits for the given {@link Supplier} to be true until given timeout.
+     *
+     * @param condition the condition to check
+     * @param timeout the time to wait for the condition before throwing
+     * @throws TimeoutException if the condition is still not met when the timeout expires
+     */
+    public static void waitFor(Supplier<Boolean> condition, Duration timeout)
+            throws TimeoutException {
+        final long intervalMills = 500;
+        final long timeoutMills = timeout.toMillis();
+
+        for (long i = 0; i < timeoutMills; i += intervalMills) {
+            if (condition.get()) {
+                return;
+            }
+            SystemClock.sleep(intervalMills);
+        }
+        if (condition.get()) {
+            return;
+        }
+        throw new TimeoutException("The condition failed to become true in " + timeout);
+    }
+
+    /**
+     * Creates a {@link TapPacketReader} given the {@link TestNetworkInterface} and {@link Handler}.
+     *
+     * @param testNetworkInterface the TUN interface of the test network
+     * @param handler the handler to process the packets
+     * @return the {@link TapPacketReader}
+     */
+    public static TapPacketReader newPacketReader(
+            TestNetworkInterface testNetworkInterface, Handler handler) {
+        FileDescriptor fd = testNetworkInterface.getFileDescriptor().getFileDescriptor();
+        final TapPacketReader reader =
+                new TapPacketReader(handler, fd, testNetworkInterface.getMtu());
+        handler.post(() -> reader.start());
+        HandlerUtils.waitForIdle(handler, 5000 /* timeout in milliseconds */);
+        return reader;
+    }
+
+    /**
+     * Waits for the Thread module to enter any state of the given {@code deviceRoles}.
+     *
+     * @param controller the {@link ThreadNetworkController}
+     * @param deviceRoles the desired device roles. See also {@link
+     *     ThreadNetworkController.DeviceRole}
+     * @param timeout the time to wait for the expected state before throwing
+     * @return the {@link ThreadNetworkController.DeviceRole} after waiting
+     * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
+     *     expires
+     */
+    public static int waitForStateAnyOf(
+            ThreadNetworkController controller, List<Integer> deviceRoles, Duration timeout)
+            throws TimeoutException {
+        SettableFuture<Integer> future = SettableFuture.create();
+        ThreadNetworkController.StateCallback callback =
+                newRole -> {
+                    if (deviceRoles.contains(newRole)) {
+                        future.set(newRole);
+                    }
+                };
+        controller.registerStateCallback(directExecutor(), callback);
+        try {
+            return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
+        } catch (InterruptedException | ExecutionException e) {
+            throw new TimeoutException(
+                    String.format(
+                            "The device didn't become an expected role in %s: %s",
+                            timeout, e.getMessage()));
+        } finally {
+            controller.unregisterStateCallback(callback);
+        }
+    }
+
+    /**
+     * Polls for a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
+     *
+     * @param packetReader a TUN packet reader
+     * @param filter the filter to be applied on the packet
+     * @return the first IPv6 packet that satisfies the {@code filter}. If it has waited for more
+     *     than 3000ms to read the next packet, the method will return null
+     */
+    public static byte[] pollForPacket(TapPacketReader packetReader, Predicate<byte[]> filter) {
+        byte[] packet;
+        while ((packet = packetReader.poll(3000 /* timeoutMs */, filter)) != null) {
+            return packet;
+        }
+        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) {
+            return false;
+        }
+        ByteBuffer buf = ByteBuffer.wrap(packet);
+        try {
+            if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
+                return false;
+            }
+            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
+            // by anybody.
+        }
+        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.
+        }
+        return false;
+    }
+
+    public static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
+        if (packet == null) {
+            return false;
+        }
+        ByteBuffer buf = ByteBuffer.wrap(packet);
+        try {
+            return Struct.parse(Ipv6Header.class, buf).dstIp.equals(dest);
+        } catch (IllegalArgumentException ignored) {
+            // It's fine that the passed in packet is malformed because it's could be sent
+            // by anybody.
+        }
+        return false;
+    }
+
+    /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
+    public static List<PrefixInformationOption> getRaPios(byte[] raMsg) {
+        final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
+
+        if (raMsg == null) {
+            return pioList;
+        }
+
+        final ByteBuffer buf = ByteBuffer.wrap(raMsg);
+        final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buf);
+        if (ipv6Header.nextHeader != (byte) IPPROTO_ICMPV6) {
+            return pioList;
+        }
+
+        final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
+        if (icmpv6Header.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) {
+            return pioList;
+        }
+
+        Struct.parse(RaHeader.class, buf);
+        while (buf.position() < raMsg.length) {
+            final int currentPos = buf.position();
+            final int type = Byte.toUnsignedInt(buf.get());
+            final int length = Byte.toUnsignedInt(buf.get());
+            if (type == ICMPV6_ND_OPTION_PIO) {
+                final ByteBuffer pioBuf =
+                        ByteBuffer.wrap(
+                                buf.array(),
+                                currentPos,
+                                Struct.getSize(PrefixInformationOption.class));
+                final PrefixInformationOption pio =
+                        Struct.parse(PrefixInformationOption.class, pioBuf);
+                pioList.add(pio);
+
+                // Move ByteBuffer position to the next option.
+                buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+            } else {
+                // The length is in units of 8 octets.
+                buf.position(currentPos + (length * 8));
+            }
+        }
+        return pioList;
+    }
+
+    /**
+     * Sends a UDP message to a destination.
+     *
+     * @param dstAddress the IP address of the destination
+     * @param dstPort the port of the destination
+     * @param message the message in UDP payload
+     * @throws IOException if failed to send the message
+     */
+    public static void sendUdpMessage(InetAddress dstAddress, int dstPort, String message)
+            throws IOException {
+        SocketAddress dstSockAddr = new InetSocketAddress(dstAddress, dstPort);
+
+        try (DatagramSocket socket = new DatagramSocket()) {
+            socket.connect(dstSockAddr);
+
+            byte[] msgBytes = message.getBytes();
+            DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length);
+
+            socket.send(packet);
+        }
+    }
+
+    public static boolean isInMulticastGroup(String interfaceName, Inet6Address address) {
+        final String cmd = "ip -6 maddr show dev " + interfaceName;
+        final String output = runShellCommandOrThrow(cmd);
+        final String addressStr = address.getHostAddress();
+        for (final String line : output.split("\\n")) {
+            if (line.contains(addressStr)) {
+                return true;
+            }
+        }
+        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);
+    }
+
+    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
new file mode 100644
index 0000000..b3175fd
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -0,0 +1,123 @@
+/*
+ * 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 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;
+
+/**
+ * Wrapper of the "/system/bin/ot-ctl" which can be used to send CLI commands to ot-daemon to
+ * control its behavior.
+ *
+ * <p>Note that this class takes root privileged to run.
+ */
+public final class OtDaemonController {
+    private static final String OT_CTL = "/system/bin/ot-ctl";
+
+    /**
+     * Factory resets ot-daemon.
+     *
+     * <p>This will erase all persistent data written into apexdata/com.android.apex/ot-daemon and
+     * restart the ot-daemon service.
+     */
+    public void factoryReset() {
+        executeCommand("factoryreset");
+
+        // TODO(b/323164524): ot-ctl is a separate process so that the tests can't depend on the
+        // time sequence. Here needs to wait for system server to receive the ot-daemon death
+        // signal and take actions.
+        // A proper fix is to replace "ot-ctl" with "cmd thread_network ot-ctl" which is
+        // synchronized with the system server
+        SystemClock.sleep(500);
+    }
+
+    /** Returns the list of IPv6 addresses on ot-daemon. */
+    public List<Inet6Address> getAddresses() {
+        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);
+    }
+
+    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
new file mode 100644
index 0000000..9404d1b
--- /dev/null
+++ b/thread/tests/unit/Android.bp
@@ -0,0 +1,70 @@
+//
+// 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_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "ThreadNetworkUnitTests",
+    min_sdk_version: "33",
+    sdk_version: "module_current",
+    manifest: "AndroidManifest.xml",
+    test_config: "AndroidTest.xml",
+    srcs: [
+        "src/**/*.java",
+    ],
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
+    static_libs: [
+        "androidx.test.rules",
+        "frameworks-base-testutils",
+        "framework-connectivity-pre-jarjar",
+        "framework-connectivity-t-pre-jarjar",
+        "framework-location.stubs.module_lib",
+        "guava",
+        "guava-android-testlib",
+        "mockito-target-extended-minus-junit4",
+        "net-tests-utils",
+        "ot-daemon-aidl-java",
+        "ot-daemon-testing",
+        "service-connectivity-pre-jarjar",
+        "service-thread-pre-jarjar",
+        "truth",
+        "service-thread-pre-jarjar",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+        "ServiceConnectivityResources",
+        "framework-wifi",
+    ],
+    jni_libs: [
+        "libservice-thread-jni",
+
+        // these are needed for Extended Mockito
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    jni_uses_platform_apis: true,
+    jarjar_rules: ":connectivity-jarjar-rules",
+    // Test coverage system runs on different devices. Need to
+    // compile for all architectures.
+    compile_multilib: "both",
+}
diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..8442e80
--- /dev/null
+++ b/thread/tests/unit/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?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.thread.unittests">
+
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.net.thread.unittests"
+        android:label="Unit tests for android.net.thread" />
+</manifest>
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..58e9bdd
--- /dev/null
+++ b/thread/tests/unit/AndroidTest.xml
@@ -0,0 +1,54 @@
+<?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="Config for Thread network unit test cases">
+    <option name="test-tag" value="ThreadNetworkUnitTests" />
+    <option name="test-suite-tag" value="apct" />
+
+    <!--
+        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" />
+
+    <!-- Run tests in MTS only 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>
+
+    <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" />
+        <option name="cleanup-apks" value="true" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.net.thread.unittests" />
+        <option name="hidden-api-checks" value="false"/>
+        <!-- Ignores tests introduced by guava-android-testlib -->
+        <option name="exclude-annotation" value="org.junit.Ignore"/>
+        <!-- Ignores tests introduced by frameworks-base-testutils -->
+        <option name="exclude-filter" value="android.os.test.TestLooperTest"/>
+        <option name="exclude-filter" value="com.android.test.filters.SelectTestTests"/>
+    </test>
+</configuration>
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
new file mode 100644
index 0000000..e92dcb9
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.thread.ActiveOperationalDataset.Builder;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link ActiveOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActiveOperationalDatasetTest {
+    // 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 byte[] VALID_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private static byte[] addTlv(byte[] dataset, String tlvHex) {
+        return Bytes.concat(dataset, base16().decode(tlvHex));
+    }
+
+    @Test
+    public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
+        byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET_TLVS, "AA01FFBB020102");
+
+        ActiveOperationalDataset dataset1 =
+                ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
+        ActiveOperationalDataset dataset2 =
+                ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+        SparseArray<byte[]> unknownTlvs = dataset2.getUnknownTlvs();
+        assertThat(unknownTlvs.size()).isEqualTo(2);
+        assertThat(unknownTlvs.get(0xAA)).isEqualTo(new byte[] {(byte) 0xFF});
+        assertThat(unknownTlvs.get(0xBB)).isEqualTo(new byte[] {0x01, 0x02});
+        assertThat(dataset2).isEqualTo(dataset1);
+    }
+
+    @Test
+    public void builder_buildWithTooLongTlvs_throwsIllegalState() {
+        Builder builder = new Builder(ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS));
+        for (int i = 0; i < 10; i++) {
+            builder.addUnknownTlv(i, new byte[20]);
+        }
+
+        assertThrows(IllegalStateException.class, () -> new Builder().build());
+    }
+
+    @Test
+    public void builder_setUnknownTlvs_success() {
+        ActiveOperationalDataset dataset1 =
+                ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS);
+        SparseArray<byte[]> unknownTlvs = new SparseArray<>(2);
+        unknownTlvs.put(0x33, new byte[] {1, 2, 3});
+        unknownTlvs.put(0x44, new byte[] {1, 2, 3, 4});
+
+        ActiveOperationalDataset dataset2 =
+                new ActiveOperationalDataset.Builder(dataset1).setUnknownTlvs(unknownTlvs).build();
+
+        assertThat(dataset1.getUnknownTlvs().size()).isEqualTo(0);
+        assertThat(dataset2.getUnknownTlvs().size()).isEqualTo(2);
+        assertThat(dataset2.getUnknownTlvs().get(0x33)).isEqualTo(new byte[] {1, 2, 3});
+        assertThat(dataset2.getUnknownTlvs().get(0x44)).isEqualTo(new byte[] {1, 2, 3, 4});
+    }
+
+    @Test
+    public void securityPolicy_fromTooShortTlvValue_throwsIllegalArgument() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> SecurityPolicy.fromTlvValue(new byte[] {0x01}));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> SecurityPolicy.fromTlvValue(new byte[] {0x01, 0x02}));
+    }
+
+    @Test
+    public void securityPolicy_toTlvValue_conversionIsLossLess() {
+        SecurityPolicy policy1 = new SecurityPolicy(200, new byte[] {(byte) 0xFF, (byte) 0xF8});
+
+        SecurityPolicy policy2 = SecurityPolicy.fromTlvValue(policy1.toTlvValue());
+
+        assertThat(policy2).isEqualTo(policy1);
+    }
+}
diff --git a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
new file mode 100644
index 0000000..11c78e3
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.thread;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+
+/** Unit tests for {@link OperationalDatasetTimestamp}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class OperationalDatasetTimestampTest {
+    @Test
+    public void fromTlvValue_invalidTimestamp_throwsIllegalArguments() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> OperationalDatasetTimestamp.fromTlvValue(new byte[7]));
+    }
+
+    @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"));
+
+        assertThat(timestamp.getSeconds()).isEqualTo(0xFFEEDDCCBBAAL);
+        // 0x9989 is 0x4CC4 << 1 + 1
+        assertThat(timestamp.getTicks()).isEqualTo(0x4CC4);
+        assertThat(timestamp.isAuthoritativeSource()).isTrue();
+    }
+
+    @Test
+    public void toTlvValue_conversionIsLossLess() {
+        OperationalDatasetTimestamp timestamp1 = new OperationalDatasetTimestamp(100L, 10, true);
+
+        OperationalDatasetTimestamp timestamp2 =
+                OperationalDatasetTimestamp.fromTlvValue(timestamp1.toTlvValue());
+
+        assertThat(timestamp2).isEqualTo(timestamp1);
+    }
+
+    @Test
+    public void toTlvValue_timestampFromInstant_conversionIsLossLess() {
+        // This results in ticks = 999938900 / 1000000000 * 32768 = 32765.9978752 ~= 32766.
+        // The ticks 32766 is then converted back to 999938964.84375 ~= 999938965 nanoseconds.
+        // A wrong implementation may save Instant.getNano() and compare against the nanoseconds
+        // and results in precision loss when converted between OperationalDatasetTimestamp and the
+        // TLV values.
+        OperationalDatasetTimestamp timestamp1 =
+                OperationalDatasetTimestamp.fromInstant(Instant.ofEpochSecond(100, 999938900));
+
+        OperationalDatasetTimestamp timestamp2 =
+                OperationalDatasetTimestamp.fromTlvValue(timestamp1.toTlvValue());
+
+        assertThat(timestamp2.getSeconds()).isEqualTo(100);
+        assertThat(timestamp2.getTicks()).isEqualTo(32766);
+        assertThat(timestamp2).isEqualTo(timestamp1);
+    }
+}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
new file mode 100644
index 0000000..ac74372
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -0,0 +1,443 @@
+/*
+ * 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.thread;
+
+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;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+
+import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
+import android.net.thread.ThreadNetworkController.StateCallback;
+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;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** Unit tests for {@link ThreadNetworkController}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkControllerTest {
+
+    @Mock private IThreadNetworkController mMockService;
+    private ThreadNetworkController mController;
+
+    // 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 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 SparseIntArray DEFAULT_CHANNEL_POWERS =
+            new SparseIntArray() {
+                {
+                    put(20, 32767);
+                }
+            };
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mController = new ThreadNetworkController(mMockService);
+    }
+
+    private static void setBinderUid(int uid) {
+        // TODO: generally, it's not a good practice to depend on the implementation detail to set
+        // a custom UID, but Connectivity, Wifi, UWB and etc modules are using this trick. Maybe
+        // define a interface (e.b. CallerIdentityInjector) for easier mocking.
+        Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid());
+    }
+
+    private static IStateCallback getStateCallback(InvocationOnMock invocation) {
+        return (IStateCallback) invocation.getArguments()[0];
+    }
+
+    private static IOperationReceiver getOperationReceiver(InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[0];
+    }
+
+    private static IOperationReceiver getJoinReceiver(InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[1];
+    }
+
+    private static IOperationReceiver getScheduleMigrationReceiver(InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[1];
+    }
+
+    private static IOperationReceiver getSetTestNetworkAsUpstreamReceiver(
+            InvocationOnMock invocation) {
+        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];
+    }
+
+    private static IOperationalDatasetCallback getOperationalDatasetCallback(
+            InvocationOnMock invocation) {
+        return (IOperationalDatasetCallback) invocation.getArguments()[0];
+    }
+
+    @Test
+    public void registerStateCallback_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+        doAnswer(
+                        invoke -> {
+                            getStateCallback(invoke).onDeviceRoleChanged(DEVICE_ROLE_CHILD);
+                            return null;
+                        })
+                .when(mMockService)
+                .registerStateCallback(any(IStateCallback.class));
+        AtomicInteger callbackUid = new AtomicInteger(0);
+        StateCallback callback = state -> callbackUid.set(Binder.getCallingUid());
+
+        try {
+            mController.registerStateCallback(Runnable::run, callback);
+
+            assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
+            assertThat(callbackUid.get()).isEqualTo(Process.myUid());
+        } finally {
+            mController.unregisterStateCallback(callback);
+        }
+    }
+
+    @Test
+    public void registerOperationalDatasetCallback_callbackIsInvokedWithCallingAppIdentity()
+            throws Exception {
+        setBinderUid(SYSTEM_UID);
+        doAnswer(
+                        invoke -> {
+                            getOperationalDatasetCallback(invoke)
+                                    .onActiveOperationalDatasetChanged(null);
+                            getOperationalDatasetCallback(invoke)
+                                    .onPendingOperationalDatasetChanged(null);
+                            return null;
+                        })
+                .when(mMockService)
+                .registerOperationalDatasetCallback(any(IOperationalDatasetCallback.class));
+        AtomicInteger activeCallbackUid = new AtomicInteger(0);
+        AtomicInteger pendingCallbackUid = new AtomicInteger(0);
+        OperationalDatasetCallback callback =
+                new OperationalDatasetCallback() {
+                    @Override
+                    public void onActiveOperationalDatasetChanged(
+                            ActiveOperationalDataset dataset) {
+                        activeCallbackUid.set(Binder.getCallingUid());
+                    }
+
+                    @Override
+                    public void onPendingOperationalDatasetChanged(
+                            PendingOperationalDataset dataset) {
+                        pendingCallbackUid.set(Binder.getCallingUid());
+                    }
+                };
+
+        try {
+            mController.registerOperationalDatasetCallback(Runnable::run, callback);
+
+            assertThat(activeCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+            assertThat(activeCallbackUid.get()).isEqualTo(Process.myUid());
+            assertThat(pendingCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+            assertThat(pendingCallbackUid.get()).isEqualTo(Process.myUid());
+        } finally {
+            mController.unregisterOperationalDatasetCallback(callback);
+        }
+    }
+
+    @Test
+    public void createRandomizedDataset_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+        doAnswer(
+                        invoke -> {
+                            getCreateDatasetReceiver(invoke).onSuccess(DEFAULT_DATASET);
+                            return null;
+                        })
+                .when(mMockService)
+                .createRandomizedDataset(anyString(), any(IActiveOperationalDatasetReceiver.class));
+        mController.createRandomizedDataset(
+                "TestNet",
+                Runnable::run,
+                dataset -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getCreateDatasetReceiver(invoke).onError(ERROR_UNSUPPORTED_CHANNEL, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .createRandomizedDataset(anyString(), any(IActiveOperationalDatasetReceiver.class));
+        mController.createRandomizedDataset(
+                "TestNet",
+                Runnable::run,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(ActiveOperationalDataset dataset) {}
+
+                    @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 join_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+        doAnswer(
+                        invoke -> {
+                            getJoinReceiver(invoke).onSuccess();
+                            return null;
+                        })
+                .when(mMockService)
+                .join(any(ActiveOperationalDataset.class), any(IOperationReceiver.class));
+        mController.join(
+                DEFAULT_DATASET,
+                Runnable::run,
+                v -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getJoinReceiver(invoke).onError(ERROR_UNAVAILABLE, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .join(any(ActiveOperationalDataset.class), any(IOperationReceiver.class));
+        mController.join(
+                DEFAULT_DATASET,
+                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 scheduleMigration_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+        final PendingOperationalDataset pendingDataset =
+                new PendingOperationalDataset(
+                        DEFAULT_DATASET,
+                        new OperationalDatasetTimestamp(100, 0, false),
+                        Duration.ZERO);
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+        doAnswer(
+                        invoke -> {
+                            getScheduleMigrationReceiver(invoke).onSuccess();
+                            return null;
+                        })
+                .when(mMockService)
+                .scheduleMigration(
+                        any(PendingOperationalDataset.class), any(IOperationReceiver.class));
+        mController.scheduleMigration(
+                pendingDataset, Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getScheduleMigrationReceiver(invoke).onError(ERROR_UNAVAILABLE, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .scheduleMigration(
+                        any(PendingOperationalDataset.class), any(IOperationReceiver.class));
+        mController.scheduleMigration(
+                pendingDataset,
+                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 leave_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+        doAnswer(
+                        invoke -> {
+                            getOperationReceiver(invoke).onSuccess();
+                            return null;
+                        })
+                .when(mMockService)
+                .leave(any(IOperationReceiver.class));
+        mController.leave(Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getOperationReceiver(invoke).onError(ERROR_UNAVAILABLE, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .leave(any(IOperationReceiver.class));
+        mController.leave(
+                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 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);
+
+        AtomicInteger callbackUid = new AtomicInteger(0);
+
+        doAnswer(
+                        invoke -> {
+                            getSetTestNetworkAsUpstreamReceiver(invoke).onSuccess();
+                            return null;
+                        })
+                .when(mMockService)
+                .setTestNetworkAsUpstream(anyString(), any(IOperationReceiver.class));
+        mController.setTestNetworkAsUpstream(
+                null, Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+        mController.setTestNetworkAsUpstream(
+                new String("test0"), Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+
+        assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(callbackUid.get()).isEqualTo(Process.myUid());
+    }
+}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
new file mode 100644
index 0000000..5908c20
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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 org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link ThreadNetworkException} to cover what is not covered in CTS tests. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkExceptionTest {
+    @Test
+    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(14, "14"));
+    }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/BinderUtil.java b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java
new file mode 100644
index 0000000..3614bce
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java
@@ -0,0 +1,31 @@
+/*
+ * 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.thread;
+
+import android.os.Binder;
+
+/** Utilities for faking the calling uid in Binder. */
+public class BinderUtil {
+    /**
+     * Fake the calling uid in Binder.
+     *
+     * @param uid the calling uid that Binder should return from now on
+     */
+    public static void setUid(int uid) {
+        Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid());
+    }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
new file mode 100644
index 0000000..b32986d
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -0,0 +1,817 @@
+/*
+ * 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.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;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void registerService_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
+                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()).isEqualTo("MyService");
+        assertThat(actualServiceInfo.getServiceType()).isEqualTo("_test._tcp");
+        assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
+        assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
+        assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
+        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();
+    }
+
+    @Test
+    public void registerService_nsdManagerFails_serviceRegistrationFails() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
+                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.onRegistrationFailed(actualServiceInfo, FAILURE_INTERNAL_ERROR);
+        mTestLooper.dispatchAll();
+
+        assertThat(actualServiceInfo.getServiceName()).isEqualTo("MyService");
+        assertThat(actualServiceInfo.getServiceType()).isEqualTo("_test._tcp");
+        assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
+        assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
+        assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
+        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();
+        doThrow(new IllegalArgumentException("NsdManager fails"))
+                .when(mMockNsdManager)
+                .registerService(any(), anyInt(), any(Executor.class), any());
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void unregisterService_nsdManagerSucceeds_serviceUnregistrationSucceeds()
+            throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
+                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 unregisterService_nsdManagerFails_serviceUnregistrationFails() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
+                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 registerHost_nsdManagerSucceeds_serviceRegistrationSucceeds() 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());
+
+        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 =
+                ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService",
+                "_test._tcp",
+                List.of("_subtype1", "_subtype2"),
+                12345,
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        verify(mMockNsdManager, times(1))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(Executor.class),
+                        actualRegistrationListenerCaptor.capture());
+        NsdManager.RegistrationListener actualListener1 =
+                actualRegistrationListenerCaptor.getValue();
+        actualListener1.onServiceRegistered(actualServiceInfoCaptor.getValue());
+
+        mNsdPublisher.registerService(
+                null,
+                "MyService2",
+                "_test._udp",
+                Collections.emptyList(),
+                11111,
+                Collections.emptyList(),
+                mRegistrationReceiver,
+                17 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        verify(mMockNsdManager, times(2))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(Executor.class),
+                        actualRegistrationListenerCaptor.capture());
+        NsdManager.RegistrationListener actualListener2 =
+                actualRegistrationListenerCaptor.getAllValues().get(1);
+        actualListener2.onServiceRegistered(actualServiceInfoCaptor.getValue());
+
+        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);
+    }
+
+    @Test
+    public void onOtDaemonDied_resetIsCalled() {
+        prepareTest();
+        NsdPublisher spyNsdPublisher = spy(mNsdPublisher);
+
+        spyNsdPublisher.onOtDaemonDied();
+        mTestLooper.dispatchAll();
+
+        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 addresses;
+    }
+
+    // @Before and @Test run in different threads. NsdPublisher requires the jobs are run on the
+    // thread looper, so TestLooper needs to be created inside each test case to install the
+    // correct looper.
+    private void prepareTest() {
+        mTestLooper = new TestLooper();
+        Handler handler = new Handler(mTestLooper.getLooper());
+        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
new file mode 100644
index 0000000..eaf11b1
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -0,0 +1,740 @@
+/*
+ * 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.thread;
+
+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;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+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.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;
+import static org.mockito.Mockito.times;
+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.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;
+
+/** 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
+    // 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 byte[] DEFAULT_ACTIVE_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+    private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
+    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
+
+    // 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 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() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        doNothing()
+                .when(mContext)
+                .enforceCallingOrSelfPermission(
+                        eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), anyString());
+
+        mTestLooper = new TestLooper();
+        final Handler handler = new Handler(mTestLooper.getLooper());
+        NetworkProvider networkProvider =
+                new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
+
+        mFakeOtDaemon = new FakeOtDaemon(handler);
+        when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
+
+        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,
+                        handler,
+                        networkProvider,
+                        () -> mFakeOtDaemon,
+                        mMockConnectivityManager,
+                        mMockTunIfController,
+                        mMockInfraIfController,
+                        mPersistentSettings,
+                        mMockNsdPublisher,
+                        mMockUserManager,
+                        mConnectivityResources,
+                        () -> DEFAULT_COUNTRY_CODE);
+        mService.setTestNetworkAgent(mMockNetworkAgent);
+    }
+
+    @Test
+    public void initialize_tunInterfaceAndNsdPublisherSetToOtDaemon() throws Exception {
+        when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
+
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        verify(mMockTunIfController, times(1)).createTunInterface();
+        assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd);
+        assertThat(mFakeOtDaemon.getNsdPublisher()).isEqualTo(mMockNsdPublisher);
+    }
+
+    @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);
+        mFakeOtDaemon.setJoinException(new RemoteException("ot-daemon join() throws"));
+
+        mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
+        mTestLooper.dispatchAll();
+
+        verify(mockReceiver, never()).onSuccess();
+        verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
+    }
+
+    @Test
+    public void join_succeed_threadNetworkRegistered() throws Exception {
+        mService.initialize();
+        final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+        mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
+        // Here needs to call Testlooper#dispatchAll twices because TestLooper#moveTimeForward
+        // operates on only currently enqueued messages but the delayed message is posted from
+        // another Handler task.
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100);
+        mTestLooper.dispatchAll();
+
+        verify(mockReceiver, times(1)).onSuccess();
+        verify(mMockNetworkAgent, times(1)).register();
+    }
+
+    @Test
+    public void userRestriction_initWithUserRestricted_otDaemonNotStarted() {
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.isInitialized()).isFalse();
+    }
+
+    @Test
+    public void userRestriction_initWithUserNotRestricted_threadIsEnabled() {
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+    }
+
+    @Test
+    public void userRestriction_userBecomesRestricted_stateIsDisabledButNotPersisted() {
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+        AtomicReference<BroadcastReceiver> receiverRef =
+                captureBroadcastReceiver(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+        receiverRef.get().onReceive(mContext, new Intent());
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+        assertThat(mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED)).isTrue();
+    }
+
+    @Test
+    public void userRestriction_userBecomesNotRestricted_stateIsEnabled() {
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+        AtomicReference<BroadcastReceiver> receiverRef =
+                captureBroadcastReceiver(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+        receiverRef.get().onReceive(mContext, new Intent());
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+    }
+
+    @Test
+    public void userRestriction_setEnabledWhenUserRestricted_failedPreconditionError() {
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+        mService.initialize();
+
+        CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+        mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
+        mTestLooper.dispatchAll();
+
+        var thrown = assertThrows(ExecutionException.class, () -> setEnabledFuture.get());
+        ThreadNetworkException failure = (ThreadNetworkException) thrown.getCause();
+        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
+            public void onSuccess() {
+                future.complete(null);
+            }
+
+            @Override
+            public void onError(int errorCode, String errorMessage) {
+                future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
+            }
+        };
+    }
+
+    @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);
+        mFakeOtDaemon.setChannelMasks(
+                DEFAULT_SUPPORTED_CHANNEL_MASK, DEFAULT_PREFERRED_CHANNEL_MASK);
+        mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_NONE);
+
+        mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+        mTestLooper.dispatchAll();
+
+        verify(mockReceiver, never()).onError(anyInt(), anyString());
+        verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+        ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+        assertThat(activeDataset.getNetworkName()).isEqualTo(DEFAULT_NETWORK_NAME);
+        assertThat(activeDataset.getChannelMask().size()).isEqualTo(1);
+        assertThat(activeDataset.getChannelMask().get(CHANNEL_PAGE_24_GHZ))
+                .isEqualTo(DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY);
+        assertThat(activeDataset.getChannel()).isEqualTo(DEFAULT_SELECTED_CHANNEL);
+    }
+
+    @Test
+    public void createRandomizedDataset_otDaemonRemoteFailure_returnsPreconditionError()
+            throws Exception {
+        final IActiveOperationalDatasetReceiver mockReceiver =
+                mock(IActiveOperationalDatasetReceiver.class);
+        mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_INVALID_STATE);
+        when(mockReceiver.asBinder()).thenReturn(mIBinder);
+
+        mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+        mTestLooper.dispatchAll();
+
+        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)
+                        .setDhcp6PdEnabled(false)
+                        .build();
+        ThreadConfiguration config2 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcp6PdEnabled(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();
+    }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
new file mode 100644
index 0000000..ca9741d
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
@@ -0,0 +1,480 @@
+/*
+ * 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.thread;
+
+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;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyDouble;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.thread.IOperationReceiver;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+/** Unit tests for {@link ThreadNetworkCountryCode}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkCountryCodeTest {
+    private static final String TEST_COUNTRY_CODE_US = "US";
+    private static final String TEST_COUNTRY_CODE_CN = "CN";
+    private static final String TEST_COUNTRY_CODE_INVALID = "INVALID";
+    private static final String TEST_WIFI_DEFAULT_COUNTRY_CODE = "00";
+    private static final int TEST_SIM_SLOT_INDEX_0 = 0;
+    private static final int TEST_SIM_SLOT_INDEX_1 = 1;
+
+    @Mock Context mContext;
+    @Mock LocationManager mLocationManager;
+    @Mock Geocoder mGeocoder;
+    @Mock ThreadNetworkControllerService mThreadNetworkControllerService;
+    @Mock PackageManager mPackageManager;
+    @Mock Location mLocation;
+    @Mock Resources mResources;
+    @Mock ConnectivityResources mConnectivityResources;
+    @Mock WifiManager mWifiManager;
+    @Mock SubscriptionManager mSubscriptionManager;
+    @Mock TelephonyManager mTelephonyManager;
+    @Mock List<SubscriptionInfo> mSubscriptionInfoList;
+    @Mock SubscriptionInfo mSubscriptionInfo0;
+    @Mock SubscriptionInfo mSubscriptionInfo1;
+    @Mock ThreadPersistentSettings mPersistentSettings;
+
+    private ThreadNetworkCountryCode mThreadNetworkCountryCode;
+    private boolean mErrorSetCountryCode;
+
+    @Captor private ArgumentCaptor<LocationListener> mLocationListenerCaptor;
+    @Captor private ArgumentCaptor<Geocoder.GeocodeListener> mGeocodeListenerCaptor;
+    @Captor private ArgumentCaptor<IOperationReceiver> mOperationReceiverCaptor;
+    @Captor private ArgumentCaptor<ActiveCountryCodeChangedCallback> mWifiCountryCodeReceiverCaptor;
+    @Captor private ArgumentCaptor<BroadcastReceiver> mTelephonyCountryCodeReceiverCaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mConnectivityResources.get()).thenReturn(mResources);
+        when(mResources.getBoolean(anyInt())).thenReturn(true);
+
+        when(mSubscriptionManager.getActiveSubscriptionInfoList())
+                .thenReturn(mSubscriptionInfoList);
+        Iterator<SubscriptionInfo> iteratorMock = mock(Iterator.class);
+        when(mSubscriptionInfoList.size()).thenReturn(2);
+        when(mSubscriptionInfoList.iterator()).thenReturn(iteratorMock);
+        when(iteratorMock.hasNext()).thenReturn(true).thenReturn(true).thenReturn(false);
+        when(iteratorMock.next()).thenReturn(mSubscriptionInfo0).thenReturn(mSubscriptionInfo1);
+        when(mSubscriptionInfo0.getSimSlotIndex()).thenReturn(TEST_SIM_SLOT_INDEX_0);
+        when(mSubscriptionInfo1.getSimSlotIndex()).thenReturn(TEST_SIM_SLOT_INDEX_1);
+
+        when(mLocation.getLatitude()).thenReturn(0.0);
+        when(mLocation.getLongitude()).thenReturn(0.0);
+
+        Answer setCountryCodeCallback =
+                invocation -> {
+                    Object[] args = invocation.getArguments();
+                    IOperationReceiver cb = (IOperationReceiver) args[1];
+
+                    if (mErrorSetCountryCode) {
+                        cb.onError(ERROR_INTERNAL_ERROR, new String("Invalid country code"));
+                    } else {
+                        cb.onSuccess();
+                    }
+                    return new Object();
+                };
+
+        doAnswer(setCountryCodeCallback)
+                .when(mThreadNetworkControllerService)
+                .setCountryCode(any(), any(IOperationReceiver.class));
+
+        mThreadNetworkCountryCode = newCountryCodeWithOemSource(null);
+    }
+
+    private ThreadNetworkCountryCode newCountryCodeWithOemSource(@Nullable String oemCountryCode) {
+        return new ThreadNetworkCountryCode(
+                mLocationManager,
+                mThreadNetworkControllerService,
+                mGeocoder,
+                mConnectivityResources,
+                mWifiManager,
+                mContext,
+                mTelephonyManager,
+                mSubscriptionManager,
+                oemCountryCode,
+                mPersistentSettings);
+    }
+
+    private static Address newAddress(String countryCode) {
+        Address address = new Address(Locale.ROOT);
+        address.setCountryCode(countryCode);
+        return address;
+    }
+
+    @Test
+    public void threadNetworkCountryCode_invalidOemCountryCode_illegalArgumentExceptionIsThrown() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> newCountryCodeWithOemSource(TEST_COUNTRY_CODE_INVALID));
+    }
+
+    @Test
+    public void initialize_defaultCountryCodeIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+    }
+
+    @Test
+    public void initialize_oemCountryCodeAvailable_oemCountryCodeIsUsed() {
+        mThreadNetworkCountryCode = newCountryCodeWithOemSource(TEST_COUNTRY_CODE_US);
+
+        mThreadNetworkCountryCode.initialize();
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+    }
+
+    @Test
+    public void initialize_locationUseIsDisabled_locationFunctionIsNotCalled() {
+        when(mResources.getBoolean(R.bool.config_thread_location_use_for_country_code_enabled))
+                .thenReturn(false);
+
+        mThreadNetworkCountryCode.initialize();
+
+        verifyNoMoreInteractions(mGeocoder);
+        verifyNoMoreInteractions(mLocationManager);
+    }
+
+    @Test
+    public void locationCountryCode_locationChanged_locationCountryCodeIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+
+        verify(mLocationManager)
+                .requestLocationUpdates(
+                        anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+        mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+        verify(mGeocoder)
+                .getFromLocation(
+                        anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+        mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+    }
+
+    @Test
+    public void wifiCountryCode_bothWifiAndLocationAreAvailable_wifiCountryCodeIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+        verify(mLocationManager)
+                .requestLocationUpdates(
+                        anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+        mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+        verify(mGeocoder)
+                .getFromLocation(
+                        anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+
+        Address mockAddress = mock(Address.class);
+        when(mockAddress.getCountryCode()).thenReturn(TEST_COUNTRY_CODE_US);
+        List<Address> addresses = List.of(mockAddress);
+        mGeocodeListenerCaptor.getValue().onGeocode(addresses);
+
+        verify(mWifiManager)
+                .registerActiveCountryCodeChangedCallback(
+                        any(), mWifiCountryCodeReceiverCaptor.capture());
+        mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_CN);
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+    }
+
+    @Test
+    public void wifiCountryCode_wifiCountryCodeIsActive_wifiCountryCodeIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+
+        verify(mWifiManager)
+                .registerActiveCountryCodeChangedCallback(
+                        any(), mWifiCountryCodeReceiverCaptor.capture());
+        mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_US);
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+    }
+
+    @Test
+    public void wifiCountryCode_wifiDefaultCountryCodeIsActive_wifiCountryCodeIsNotUsed() {
+        mThreadNetworkCountryCode.initialize();
+
+        verify(mWifiManager)
+                .registerActiveCountryCodeChangedCallback(
+                        any(), mWifiCountryCodeReceiverCaptor.capture());
+        mWifiCountryCodeReceiverCaptor
+                .getValue()
+                .onActiveCountryCodeChanged(TEST_WIFI_DEFAULT_COUNTRY_CODE);
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode())
+                .isNotEqualTo(TEST_WIFI_DEFAULT_COUNTRY_CODE);
+    }
+
+    @Test
+    public void wifiCountryCode_wifiCountryCodeIsInactive_defaultCountryCodeIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+        verify(mWifiManager)
+                .registerActiveCountryCodeChangedCallback(
+                        any(), mWifiCountryCodeReceiverCaptor.capture());
+        mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_US);
+
+        mWifiCountryCodeReceiverCaptor.getValue().onCountryCodeInactive();
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode())
+                .isEqualTo(ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE);
+    }
+
+    @Test
+    public void telephonyCountryCode_bothTelephonyAndLocationAvailable_telephonyCodeIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+        verify(mLocationManager)
+                .requestLocationUpdates(
+                        anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+        mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+        verify(mGeocoder)
+                .getFromLocation(
+                        anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+        mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+        verify(mContext)
+                .registerReceiver(
+                        mTelephonyCountryCodeReceiverCaptor.capture(),
+                        any(),
+                        eq(Context.RECEIVER_EXPORTED));
+        Intent intent =
+                new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+                        .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+                        .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+        mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent);
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+    }
+
+    @Test
+    public void telephonyCountryCode_locationIsAvailable_lastKnownTelephonyCodeIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+        verify(mLocationManager)
+                .requestLocationUpdates(
+                        anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+        mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+        verify(mGeocoder)
+                .getFromLocation(
+                        anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+        mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+        verify(mContext)
+                .registerReceiver(
+                        mTelephonyCountryCodeReceiverCaptor.capture(),
+                        any(),
+                        eq(Context.RECEIVER_EXPORTED));
+        Intent intent =
+                new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+                        .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, "")
+                        .putExtra(
+                                TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY,
+                                TEST_COUNTRY_CODE_US)
+                        .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+        mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent);
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+    }
+
+    @Test
+    public void telephonyCountryCode_lastKnownCountryCodeAvailable_telephonyCodeIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+        verify(mContext)
+                .registerReceiver(
+                        mTelephonyCountryCodeReceiverCaptor.capture(),
+                        any(),
+                        eq(Context.RECEIVER_EXPORTED));
+        Intent intent0 =
+                new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+                        .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, "")
+                        .putExtra(
+                                TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY,
+                                TEST_COUNTRY_CODE_US)
+                        .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+        mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent0);
+
+        verify(mContext)
+                .registerReceiver(
+                        mTelephonyCountryCodeReceiverCaptor.capture(),
+                        any(),
+                        eq(Context.RECEIVER_EXPORTED));
+        Intent intent1 =
+                new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+                        .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+                        .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_1);
+        mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent1);
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+    }
+
+    @Test
+    public void telephonyCountryCode_multipleSims_firstSimIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+        verify(mContext)
+                .registerReceiver(
+                        mTelephonyCountryCodeReceiverCaptor.capture(),
+                        any(),
+                        eq(Context.RECEIVER_EXPORTED));
+        Intent intent1 =
+                new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+                        .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+                        .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_1);
+        mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent1);
+
+        Intent intent0 =
+                new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+                        .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+                        .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+        mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent0);
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+    }
+
+    @Test
+    public void updateCountryCode_noForceUpdateDefaultCountryCode_noCountryCodeIsUpdated() {
+        mThreadNetworkCountryCode.initialize();
+        clearInvocations(mThreadNetworkControllerService);
+
+        mThreadNetworkCountryCode.updateCountryCode(false /* forceUpdate */);
+
+        verify(mThreadNetworkControllerService, never()).setCountryCode(any(), any());
+    }
+
+    @Test
+    public void updateCountryCode_forceUpdateDefaultCountryCode_countryCodeIsUpdated() {
+        mThreadNetworkCountryCode.initialize();
+        clearInvocations(mThreadNetworkControllerService);
+
+        mThreadNetworkCountryCode.updateCountryCode(true /* forceUpdate */);
+
+        verify(mThreadNetworkControllerService)
+                .setCountryCode(eq(DEFAULT_COUNTRY_CODE), mOperationReceiverCaptor.capture());
+    }
+
+    @Test
+    public void setOverrideCountryCode_defaultCountryCodeAvailable_overrideCountryCodeIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+
+        mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+    }
+
+    @Test
+    public void clearOverrideCountryCode_defaultCountryCodeAvailable_defaultCountryCodeIsUsed() {
+        mThreadNetworkCountryCode.initialize();
+        mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+        mThreadNetworkCountryCode.clearOverrideCountryCode();
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+    }
+
+    @Test
+    public void setCountryCodeFailed_defaultCountryCodeAvailable_countryCodeIsNotUpdated() {
+        mThreadNetworkCountryCode.initialize();
+
+        mErrorSetCountryCode = true;
+        mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+        verify(mThreadNetworkControllerService)
+                .setCountryCode(eq(TEST_COUNTRY_CODE_CN), mOperationReceiverCaptor.capture());
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+    }
+
+    @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);
+
+        mThreadNetworkCountryCode.dump(new FileDescriptor(), printWriter, null);
+        String outputString = stringWriter.toString();
+
+        assertThat(outputString).contains("mOverrideCountryCodeInfo");
+        assertThat(outputString).contains("mTelephonyCountryCodeSlotInfoMap");
+        assertThat(outputString).contains("mTelephonyCountryCodeInfo");
+        assertThat(outputString).contains("mWifiCountryCodeInfo");
+        assertThat(outputString).contains("mTelephonyLastCountryCodeInfo");
+        assertThat(outputString).contains("mLocationCountryCodeInfo");
+        assertThat(outputString).contains("mOemCountryCodeInfo");
+        assertThat(outputString).contains("mCurrentCountryCodeInfo");
+    }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
new file mode 100644
index 0000000..dfb3129
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -0,0 +1,304 @@
+/*
+ * 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.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 androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+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.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/** Unit tests for {@link ThreadNetworkShellCommand}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkShellCommandTest {
+    // 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";
+
+    @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);
+
+        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
+    public void tearDown() throws Exception {
+        validateMockitoUsage();
+    }
+
+    @Test
+    public void getCountryCode_testingPermissionIsChecked() {
+        when(mCountryCode.getCountryCode()).thenReturn("US");
+
+        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(),
+                new FileDescriptor(),
+                new String[] {"get-country-code"});
+
+        verify(mOutputWriter).println(contains("US"));
+    }
+
+    @Test
+    public void forceSetCountryCodeEnabled_testingPermissionIsChecked() {
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"force-country-code", "enabled", "US"});
+
+        verify(mContext, times(1))
+                .enforceCallingOrSelfPermission(
+                        eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
+    }
+
+    @Test
+    public void forceSetCountryCodeEnabled_countryCodeIsOverridden() {
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"force-country-code", "enabled", "US"});
+
+        verify(mCountryCode).setOverrideCountryCode(eq("US"));
+    }
+
+    @Test
+    public void forceSetCountryCodeDisabled_overriddenCountryCodeIsCleared() {
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"force-country-code", "disabled"});
+
+        verify(mCountryCode).clearOverrideCountryCode();
+    }
+
+    @Test
+    public void forceStopOtDaemon_testingPermissionIsChecked() {
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"force-stop-ot-daemon", "enabled"});
+
+        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
new file mode 100644
index 0000000..c932ac8
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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.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.eq;
+import static org.mockito.Mockito.validateMockitoUsage;
+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 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 {
+    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);
+
+        when(mConnectivityResources.get()).thenReturn(mResources);
+        when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+
+        mAtomicFile = createAtomicFile();
+        mThreadPersistentSettings =
+                new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
+    }
+
+    /** Called after each test */
+    @After
+    public void tearDown() {
+        validateMockitoUsage();
+    }
+
+    @Test
+    public void initialize_readsFromFile() throws Exception {
+        byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
+        setupAtomicFileForRead(data);
+
+        mThreadPersistentSettings.initialize();
+
+        assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+    }
+
+    @Test
+    public void initialize_ThreadDisabledInResources_returnsThreadDisabled() throws Exception {
+        when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
+        setupAtomicFileForRead(new byte[0]);
+
+        mThreadPersistentSettings.initialize();
+
+        assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+    }
+
+    @Test
+    public void initialize_ThreadDisabledInResourcesButEnabledInXml_returnsThreadEnabled()
+            throws Exception {
+        when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
+        byte[] data = createXmlForParsing(THREAD_ENABLED.key, true);
+        setupAtomicFileForRead(data);
+
+        mThreadPersistentSettings.initialize();
+
+        assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
+    }
+
+    @Test
+    public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
+        mThreadPersistentSettings.put(THREAD_ENABLED.key, true);
+
+        assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
+    }
+
+    @Test
+    public void put_ThreadFeatureEnabledFalse_returnsFalse() throws Exception {
+        mThreadPersistentSettings.put(THREAD_ENABLED.key, false);
+
+        assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+        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)
+                        .setDhcp6PdEnabled(true)
+                        .build();
+        mThreadPersistentSettings.putConfiguration(configuration);
+
+        assertThat(mThreadPersistentSettings.putConfiguration(configuration)).isFalse();
+    }
+
+    @Test
+    public void putConfiguration_differentValues_returnsTrue() {
+        ThreadConfiguration configuration1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(false)
+                        .setDhcp6PdEnabled(false)
+                        .build();
+        mThreadPersistentSettings.putConfiguration(configuration1);
+        ThreadConfiguration configuration2 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcp6PdEnabled(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_dhcp6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder().setDhcp6PdEnabled(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 {
+        PersistableBundle bundle = new PersistableBundle();
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        bundle.putBoolean(key, value);
+        bundle.writeToStream(outputStream);
+        return outputStream.toByteArray();
+    }
+
+    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
new file mode 100644
index 0000000..726ec9d
--- /dev/null
+++ b/thread/tests/utils/Android.bp
@@ -0,0 +1,38 @@
+//
+// 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_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "ThreadNetworkTestUtils",
+    min_sdk_version: "30",
+    static_libs: [
+        "compatibility-device-util-axt",
+        "net-tests-utils",
+        "net-utils-device-common",
+        "net-utils-device-common-bpf",
+        "net-utils-device-common-struct-base",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+    defaults: [
+        "framework-connectivity-test-defaults",
+    ],
+}
diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
new file mode 100644
index 0000000..b586a19
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
@@ -0,0 +1,191 @@
+/*
+ * 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.MANAGE_TEST_NETWORKS;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static com.android.testutils.RecorderCallback.CallbackEntry.LINK_PROPERTIES_CHANGED;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.TestNetworkSpecifier;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.testutils.TestableNetworkAgent;
+import com.android.testutils.TestableNetworkCallback;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** A class that can create/destroy a test network based on TAP interface. */
+public final class TapTestNetworkTracker {
+    private static final Duration TIMEOUT = Duration.ofSeconds(2);
+    private final Context mContext;
+    private final Looper mLooper;
+    private TestNetworkInterface mInterface;
+    private TestableNetworkAgent mAgent;
+    private Network mNetwork;
+    private final TestableNetworkCallback mNetworkCallback;
+    private final ConnectivityManager mConnectivityManager;
+
+    /**
+     * Constructs a {@link TapTestNetworkTracker}.
+     *
+     * <p>It creates a TAP interface (e.g. testtap0) and registers a test network using that
+     * interface. It also requests the test network by {@link ConnectivityManager#requestNetwork} so
+     * the test network won't be automatically turned down by {@link
+     * com.android.server.ConnectivityService}.
+     */
+    public TapTestNetworkTracker(Context context, Looper looper) {
+        mContext = context;
+        mLooper = looper;
+        mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+        mNetworkCallback = new TestableNetworkCallback();
+        runAsShell(MANAGE_TEST_NETWORKS, this::setUpTestNetwork);
+    }
+
+    /** Tears down the test network. */
+    public void tearDown() {
+        runAsShell(MANAGE_TEST_NETWORKS, this::tearDownTestNetwork);
+    }
+
+    /** Returns the interface name of the test network. */
+    public String getInterfaceName() {
+        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();
+
+        mConnectivityManager.requestNetwork(newNetworkRequest(), mNetworkCallback);
+
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(getInterfaceName());
+        mAgent =
+                new TestableNetworkAgent(
+                        mContext,
+                        mLooper,
+                        newNetworkCapabilities(),
+                        lp,
+                        new NetworkAgentConfig.Builder().build());
+        mNetwork = mAgent.register();
+        mAgent.markConnected();
+
+        PollingCheck.check(
+                "No usable address on interface",
+                TIMEOUT.toMillis(),
+                () -> hasUsableAddress(mNetwork, getInterfaceName()));
+
+        lp.setLinkAddresses(makeLinkAddresses());
+        mAgent.sendLinkProperties(lp);
+        mNetworkCallback.eventuallyExpect(
+                LINK_PROPERTIES_CHANGED,
+                TIMEOUT.toMillis(),
+                l -> !l.getLp().getAddresses().isEmpty());
+    }
+
+    private void tearDownTestNetwork() throws IOException {
+        mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+        mAgent.unregister();
+        mInterface.getFileDescriptor().close();
+        mAgent.waitForIdle(TIMEOUT.toMillis());
+    }
+
+    private NetworkRequest newNetworkRequest() {
+        return new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()))
+                .build();
+    }
+
+    private NetworkCapabilities newNetworkCapabilities() {
+        return new NetworkCapabilities()
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()));
+    }
+
+    private List<LinkAddress> makeLinkAddresses() {
+        List<LinkAddress> linkAddresses = new ArrayList<>();
+        List<InterfaceAddress> interfaceAddresses = Collections.emptyList();
+
+        try {
+            interfaceAddresses =
+                    NetworkInterface.getByName(getInterfaceName()).getInterfaceAddresses();
+        } catch (SocketException ignored) {
+            // Ignore failures when getting the addresses.
+        }
+
+        for (InterfaceAddress address : interfaceAddresses) {
+            linkAddresses.add(
+                    new LinkAddress(address.getAddress(), address.getNetworkPrefixLength()));
+        }
+
+        return linkAddresses;
+    }
+
+    private static boolean hasUsableAddress(Network network, String interfaceName) {
+        try {
+            if (NetworkInterface.getByName(interfaceName).getInterfaceAddresses().isEmpty()) {
+                return false;
+            }
+        } catch (SocketException e) {
+            return false;
+        }
+        // Check if the link-local address can be used. Address flags are not available without
+        // elevated permissions, so check that bindSocket works.
+        try {
+            FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+            network.bindSocket(sock);
+            Os.connect(sock, parseNumericAddress("ff02::fb%" + interfaceName), 12345);
+            Os.close(sock);
+        } catch (ErrnoException | IOException e) {
+            return false;
+        }
+        return true;
+    }
+}
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 3ce76f6..2c2ed14 100644
--- a/tools/Android.bp
+++ b/tools/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_core_networking",
     // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
@@ -41,6 +42,7 @@
     name: "jarjar-rules-generator-testjavalib",
     srcs: ["testdata/java/**/*.java"],
     libs: ["unsupportedappusage"],
+    sdk_version: "core_platform",
     visibility: ["//visibility:private"],
 }
 
@@ -55,6 +57,7 @@
     static_libs: [
         "framework-connectivity.stubs.module_lib",
     ],
+    sdk_version: "module_current",
     // Not strictly necessary but specified as this MUST not have generate
     // a dex jar as that will break the tests.
     compile_dex: false,
@@ -66,6 +69,7 @@
     static_libs: [
         "framework-connectivity-t.stubs.module_lib",
     ],
+    sdk_version: "module_current",
     // Not strictly necessary but specified as this MUST not have generate
     // a dex jar as that will break the tests.
     compile_dex: false,
@@ -79,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